feat(web): 支持后台列表图片大图预览
This commit is contained in:
1 parent
046400ae4a
commit
8a84f24baf
10 files changed
+202
-13
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import AppImage from './src/AppImage.vue'
|
||||||
|
|
||||||
|
export { AppImage }
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { ElImage } from 'element-plus'
|
||||||
|
import { AppIcon } from '@/components/AppIcon'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
previewList: {
|
||||||
|
type: Array as () => string[],
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
initialIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
fit: {
|
||||||
|
type: String as PropType<'fill' | 'contain' | 'cover' | 'none' | 'scale-down'>,
|
||||||
|
default: 'cover'
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedPreviewList = computed(() => {
|
||||||
|
const images = (props.previewList || []).filter(Boolean)
|
||||||
|
return images.length ? images : [props.src]
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedInitialIndex = computed(() =>
|
||||||
|
Math.max(0, Math.min(props.initialIndex, resolvedPreviewList.value.length - 1))
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElImage
|
||||||
|
v-bind="$attrs"
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:fit="fit"
|
||||||
|
:preview-src-list="resolvedPreviewList"
|
||||||
|
:initial-index="resolvedInitialIndex"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:class="['app-image', props.class]"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="app-image-error">
|
||||||
|
<AppIcon :icon="AppIcons.error" :size="20" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElImage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-image {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-image :deep(.el-image__inner) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-image-error {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { AppIcon } from './AppIcon'
|
import { AppIcon } from './AppIcon'
|
||||||
|
import { AppImage } from './AppImage'
|
||||||
import { BaseButton } from './Button'
|
import { BaseButton } from './Button'
|
||||||
|
|
||||||
export const setupGlobCom = (app: App<Element>): void => {
|
export const setupGlobCom = (app: App<Element>): void => {
|
||||||
app.component('Icon', Icon)
|
app.component('Icon', Icon)
|
||||||
app.component('AppIcon', AppIcon)
|
app.component('AppIcon', AppIcon)
|
||||||
|
app.component('AppImage', AppImage)
|
||||||
app.component('BaseButton', BaseButton)
|
app.component('BaseButton', BaseButton)
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,8 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-user-avatar img {
|
.app-user-avatar img,
|
||||||
|
.app-user-avatar .app-user-avatar-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|||||||
@@ -59,8 +59,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.creator-drama-cover img,
|
.creator-drama-cover img,
|
||||||
|
.creator-drama-cover .creator-drama-image,
|
||||||
.creator-cover-preview img,
|
.creator-cover-preview img,
|
||||||
.creator-episode-cover img {
|
.creator-episode-cover img,
|
||||||
|
.creator-episode-cover .creator-episode-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<!-- Author: fruit.com.cn -->
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { AppIcons } from '@/constants/icons'
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
import { AppImage } from '@/components/AppImage'
|
||||||
import {
|
import {
|
||||||
createBannerApi,
|
createBannerApi,
|
||||||
deleteBannerApi,
|
deleteBannerApi,
|
||||||
@@ -53,6 +54,12 @@ const linkTypeOptions = [
|
|||||||
{ code: 'URL', text: '打开链接' }
|
{ code: 'URL', text: '打开链接' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const bannerPreviewList = computed(() => list.value.map((item) => item.imageUrl).filter(Boolean))
|
||||||
|
|
||||||
|
function bannerPreviewIndex(row) {
|
||||||
|
return Math.max(0, bannerPreviewList.value.indexOf(row.imageUrl))
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -253,7 +260,15 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<el-table-column label="预览" width="190">
|
<el-table-column label="预览" width="190">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img v-if="row.imageUrl" :src="row.imageUrl" class="banner-thumb" />
|
<AppImage
|
||||||
|
v-if="row.imageUrl"
|
||||||
|
:src="row.imageUrl"
|
||||||
|
:preview-list="bannerPreviewList"
|
||||||
|
:initial-index="bannerPreviewIndex(row)"
|
||||||
|
fit="cover"
|
||||||
|
class="banner-thumb"
|
||||||
|
:alt="row.title"
|
||||||
|
/>
|
||||||
<div v-else class="banner-thumb-empty">暂无图片</div>
|
<div v-else class="banner-thumb-empty">暂无图片</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { AppIcons } from '@/constants/icons'
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
import { AppImage } from '@/components/AppImage'
|
||||||
import {
|
import {
|
||||||
changeDramaStatusApi,
|
changeDramaStatusApi,
|
||||||
createDramaApi,
|
createDramaApi,
|
||||||
@@ -63,6 +64,12 @@ const coverDisplayName = computed(
|
|||||||
() => coverFileName.value || (form.cover ? coverBasename(form.cover) : '尚未选择封面')
|
() => coverFileName.value || (form.cover ? coverBasename(form.cover) : '尚未选择封面')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pageCoverPreviewList = computed(() => list.value.map((item) => item.cover).filter(Boolean))
|
||||||
|
|
||||||
|
function coverPreviewIndex(row) {
|
||||||
|
return Math.max(0, pageCoverPreviewList.value.indexOf(row.cover))
|
||||||
|
}
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ code: 'draft', text: '草稿' },
|
{ code: 'draft', text: '草稿' },
|
||||||
{ code: 'reviewing', text: '审核中' },
|
{ code: 'reviewing', text: '审核中' },
|
||||||
@@ -354,10 +361,15 @@ onMounted(async () => {
|
|||||||
<el-table-column label="短剧名称" min-width="220"
|
<el-table-column label="短剧名称" min-width="220"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><div class="drama-cell"
|
><div class="drama-cell"
|
||||||
><img v-if="row.cover" :src="row.cover" class="drama-cover-image" /><div
|
><AppImage
|
||||||
v-else
|
v-if="row.cover"
|
||||||
class="drama-cover"
|
:src="row.cover"
|
||||||
>暂无封面</div
|
:preview-list="pageCoverPreviewList"
|
||||||
|
:initial-index="coverPreviewIndex(row)"
|
||||||
|
fit="cover"
|
||||||
|
class="drama-cover-image"
|
||||||
|
:alt="row.title"
|
||||||
|
/><div v-else class="drama-cover">暂无封面</div
|
||||||
><div
|
><div
|
||||||
><b>{{ row.title }}</b></div
|
><b>{{ row.title }}</b></div
|
||||||
></div
|
></div
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<!-- Author: fruit.com.cn -->
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
import { AppIcons } from '@/constants/icons'
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
import { AppImage } from '@/components/AppImage'
|
||||||
import PageHeading from '@/views/components/PageHeading.vue'
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
import CreatorCenterPage from './components/CreatorCenterPage.vue'
|
import CreatorCenterPage from './components/CreatorCenterPage.vue'
|
||||||
import { useRbac } from '@/stores/rbac'
|
import { useRbac } from '@/stores/rbac'
|
||||||
@@ -47,6 +49,14 @@ const {
|
|||||||
withdraw
|
withdraw
|
||||||
} = useDramaEdit()
|
} = useDramaEdit()
|
||||||
|
|
||||||
|
const episodeCoverPreviewList = computed(() =>
|
||||||
|
episodes.value.map((row) => row.cover).filter(Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
function episodeCoverPreviewIndex(row) {
|
||||||
|
return Math.max(0, episodeCoverPreviewList.value.indexOf(row.cover))
|
||||||
|
}
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
@@ -193,7 +203,15 @@ function requestDramaCover(options) {
|
|||||||
<el-table-column label="封面" width="88">
|
<el-table-column label="封面" width="88">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="creator-episode-cover">
|
<div class="creator-episode-cover">
|
||||||
<img v-if="row.cover" :src="row.cover" alt="" />
|
<AppImage
|
||||||
|
v-if="row.cover"
|
||||||
|
:src="row.cover"
|
||||||
|
:preview-list="episodeCoverPreviewList"
|
||||||
|
:initial-index="episodeCoverPreviewIndex(row)"
|
||||||
|
fit="cover"
|
||||||
|
class="creator-episode-image"
|
||||||
|
:alt="row.title"
|
||||||
|
/>
|
||||||
<span v-else>无</span>
|
<span v-else>无</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<!-- Author: fruit.com.cn -->
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { AppIcons } from '@/constants/icons'
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
import { AppImage } from '@/components/AppImage'
|
||||||
import PageHeading from '@/views/components/PageHeading.vue'
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
import CreatorCenterPage from './components/CreatorCenterPage.vue'
|
import CreatorCenterPage from './components/CreatorCenterPage.vue'
|
||||||
import { useCreatorAccess } from './composables/useCreatorAccess'
|
import { useCreatorAccess } from './composables/useCreatorAccess'
|
||||||
@@ -43,6 +44,14 @@ const {
|
|||||||
|
|
||||||
const activeTab = ref('dramas')
|
const activeTab = ref('dramas')
|
||||||
|
|
||||||
|
const workspaceCoverPreviewList = computed(() =>
|
||||||
|
(dramas.rows || []).map((row) => row.cover).filter(Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
function workspaceCoverPreviewIndex(row) {
|
||||||
|
return Math.max(0, workspaceCoverPreviewList.value.indexOf(row.cover))
|
||||||
|
}
|
||||||
|
|
||||||
function onTabChange(name) {
|
function onTabChange(name) {
|
||||||
if (name === 'notices' && !notices.loaded) loadNotices()
|
if (name === 'notices' && !notices.loaded) loadNotices()
|
||||||
}
|
}
|
||||||
@@ -149,7 +158,15 @@ function openEdit(row) {
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="creator-drama-cell">
|
<div class="creator-drama-cell">
|
||||||
<div class="creator-drama-cover">
|
<div class="creator-drama-cover">
|
||||||
<img v-if="row.cover" :src="row.cover" alt="" />
|
<AppImage
|
||||||
|
v-if="row.cover"
|
||||||
|
:src="row.cover"
|
||||||
|
:preview-list="workspaceCoverPreviewList"
|
||||||
|
:initial-index="workspaceCoverPreviewIndex(row)"
|
||||||
|
fit="cover"
|
||||||
|
class="creator-drama-image"
|
||||||
|
:alt="row.title"
|
||||||
|
/>
|
||||||
<span v-else>{{ (row.title || '?').slice(0, 1) }}</span>
|
<span v-else>{{ (row.title || '?').slice(0, 1) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="creator-drama-meta">
|
<div class="creator-drama-meta">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { AppIcons } from '@/constants/icons'
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
import { AppImage } from '@/components/AppImage'
|
||||||
import PageHeading from '@/views/components/PageHeading.vue'
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
import { useRbac } from '@/stores/rbac'
|
import { useRbac } from '@/stores/rbac'
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +71,12 @@ function avatarText(row) {
|
|||||||
return name.slice(0, 1).toUpperCase()
|
return name.slice(0, 1).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarPreviewList = computed(() => users.value.map((row) => row.avatarUrl).filter(Boolean))
|
||||||
|
|
||||||
|
function avatarPreviewIndex(row) {
|
||||||
|
return Math.max(0, avatarPreviewList.value.indexOf(row.avatarUrl))
|
||||||
|
}
|
||||||
|
|
||||||
function canBeBanned(row) {
|
function canBeBanned(row) {
|
||||||
return row.status !== 'banned' && row.status !== 'deactivated'
|
return row.status !== 'banned' && row.status !== 'deactivated'
|
||||||
}
|
}
|
||||||
@@ -145,7 +152,15 @@ function canBeBanned(row) {
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="app-user-cell">
|
<div class="app-user-cell">
|
||||||
<span class="app-user-avatar">
|
<span class="app-user-avatar">
|
||||||
<img v-if="row.avatarUrl" :src="row.avatarUrl" alt="" />
|
<AppImage
|
||||||
|
v-if="row.avatarUrl"
|
||||||
|
:src="row.avatarUrl"
|
||||||
|
:preview-list="avatarPreviewList"
|
||||||
|
:initial-index="avatarPreviewIndex(row)"
|
||||||
|
fit="cover"
|
||||||
|
class="app-user-avatar-image"
|
||||||
|
:alt="row.nickname || row.uid"
|
||||||
|
/>
|
||||||
<template v-else>{{ avatarText(row) }}</template>
|
<template v-else>{{ avatarText(row) }}</template>
|
||||||
</span>
|
</span>
|
||||||
<div class="app-user-meta">
|
<div class="app-user-meta">
|
||||||
@@ -217,7 +232,15 @@ function canBeBanned(row) {
|
|||||||
<template v-if="detail">
|
<template v-if="detail">
|
||||||
<div class="app-user-summary">
|
<div class="app-user-summary">
|
||||||
<span class="app-user-avatar">
|
<span class="app-user-avatar">
|
||||||
<img v-if="detail.avatarUrl" :src="detail.avatarUrl" alt="" />
|
<AppImage
|
||||||
|
v-if="detail.avatarUrl"
|
||||||
|
:src="detail.avatarUrl"
|
||||||
|
:preview-list="avatarPreviewList"
|
||||||
|
:initial-index="avatarPreviewIndex(detail)"
|
||||||
|
fit="cover"
|
||||||
|
class="app-user-avatar-image"
|
||||||
|
:alt="detail.nickname || detail.uid"
|
||||||
|
/>
|
||||||
<template v-else>{{ avatarText(detail) }}</template>
|
<template v-else>{{ avatarText(detail) }}</template>
|
||||||
</span>
|
</span>
|
||||||
<div class="app-user-meta">
|
<div class="app-user-meta">
|
||||||
|
|||||||
Reference in new issue
Block a user