feat(cms): 图片库升级为素材库,支持文档与音视频,缩略图统一 4:3

/cms/photo 原来只能传图片(accept=image/*,前后端各拦一次),甲方要求支持常见
文档格式与音视频,并统一缩略图宽高。

- index:标题改「素材库」,上传放开全类型,新增类型筛选(全部/图片/文档/音频/视频)
- image / list:封面统一 4:3(网格 aspect-ratio + object-fit: cover,列表 120x90);
  图片改用 thumbnail 地址(原来直接拉原图);文档/音频用类型图标卡,视频用图标卡
  加播放角标;点击视频/音频开 MediaPreview 弹窗,文档新窗口打开
- photo-edit:去掉上传表单(上传即入库,弹窗只改元数据),顺带修掉「上传已由后端
  落一条记录、点保存又 addFiles 插一条无 path 空记录」的重复入库 bug;分享代码按
  类型分流(图片 img 系列 / 视频 video / 音频 audio / 文档只给链接与下载)
- utils/media.ts:媒体大类判定、上传校验、大小格式化、类型图标与配色的唯一出口,
  MEDIA_ACCEPT 是后端 UploadRules 的镜像,两边要一起改
This commit is contained in:
2026-11-01 21:03:56 +08:00
parent 6d19cb5a05
commit 42fddc5c85
7 changed files with 729 additions and 199 deletions
+133 -31
View File
@@ -1,3 +1,4 @@
<!-- 素材库 · 列表视图 -->
<template>
<div class="page">
<a-card :bordered="false">
@@ -8,21 +9,21 @@
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 800 }"
cache-key="proCmsVideoTable"
:scroll="{ x: 860 }"
cache-key="proCmsPhotoTable"
>
<template #toolbar>
<a-space>
<a-upload
:show-upload-list="false"
:accept="'image/*'"
:accept="MEDIA_ACCEPT"
:customRequest="onUpload"
>
<a-button type="primary" class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
<span>上传图片</span>
<span>上传素材</span>
</a-button>
</a-upload>
<a-button
@@ -37,6 +38,19 @@
</template>
<span>删除</span>
</a-button>
<a-select
v-model:value="category"
style="width: 120px"
@change="reload()"
>
<a-select-option
v-for="item in MEDIA_CATEGORY_OPTIONS"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
<a-input-search
allow-clear
v-model:value="searchText"
@@ -60,7 +74,23 @@
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'path'">
<a-image :src="record.url" :width="120" />
<!-- 与网格视图同一套 4:3 缩略图规则 -->
<div class="media-thumb" @click="openPreview(record)">
<a-image
v-if="kindOf(record) === 'image'"
:src="record.thumbnail || record.url"
:preview="{ src: record.url }"
:width="120"
/>
<template v-else>
<component
:is="iconOf(record)"
class="media-thumb-icon"
:style="{ color: colorOf(record) }"
/>
<span class="media-thumb-ext">{{ extOf(record) }}</span>
</template>
</div>
</template>
<template v-if="column.dataIndex === 'name'">
<span>{{ record.name }}</span>
@@ -78,7 +108,7 @@
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此评价吗?"
title="确定要删除此素材吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
@@ -90,6 +120,8 @@
</a-card>
<!-- 编辑弹窗 -->
<PhotoEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 音视频预览 -->
<MediaPreview v-model:visible="showPreview" :record="current" />
</div>
</template>
@@ -109,6 +141,7 @@
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import PhotoEdit from './components/photo-edit.vue';
import MediaPreview from './components/media-preview.vue';
import {
pageFiles,
removeFile,
@@ -119,17 +152,33 @@
FileRecord,
FileRecordParam
} from '@/api/system/file/model/index';
import {
formatFileSize,
mediaColor,
mediaExtLabel,
mediaIcon,
mediaKindOfRecord,
MEDIA_ACCEPT,
MEDIA_CATEGORY_OPTIONS,
MEDIA_KIND_LABEL,
validateMediaFile,
type MediaKind
} from '@/utils/media';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<FileRecord[]>([]);
// 当前编辑数据
// 当前操作的数据(编辑 / 预览共用)
const current = ref<FileRecord | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示音视频预览
const showPreview = ref(false);
const type = ref('name');
const searchText = ref('');
// 媒体大类筛选,空串表示全部
const category = ref('');
// 表格列配置
const columns = ref<ColumnItem[]>([
{
@@ -141,11 +190,17 @@
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '图片',
title: '缩略图',
dataIndex: 'path',
width: 180,
width: 140,
key: 'path'
},
{
title: '类型',
dataIndex: 'contentType',
width: 100,
customRender: ({ record }) => MEDIA_KIND_LABEL[kindOf(record)]
},
{
title: '文件名称',
dataIndex: 'name'
@@ -161,26 +216,16 @@
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => {
if (text < 1024) {
return text + 'B';
} else if (text < 1024 * 1024) {
return (text / 1024).toFixed(1) + 'KB';
} else if (text < 1024 * 1024 * 1024) {
return (text / 1024 / 1024).toFixed(1) + 'M';
} else {
return (text / 1024 / 1024 / 1024).toFixed(1) + 'G';
}
},
customRender: ({ text }) => formatFileSize(text),
width: 120
},
{
title: '发布者',
title: '上传人',
width: 120,
dataIndex: 'createNickname'
},
{
title: '发布时间',
title: '上传时间',
dataIndex: 'createTime',
sorter: true,
width: 180,
@@ -196,6 +241,13 @@
}
]);
const kindOf = (record: FileRecord): MediaKind => mediaKindOfRecord(record);
const iconOf = (record: FileRecord) =>
mediaIcon(kindOf(record), record.path || record.name);
const colorOf = (record: FileRecord) =>
mediaColor(kindOf(record), record.path || record.name);
const extOf = (record: FileRecord) => mediaExtLabel(record);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where = {};
@@ -205,7 +257,8 @@
if (type.value == 'createNickname') {
where.createNickname = searchText.value;
}
where.contentType = 'image';
// 空串会被当成一个真实的筛选值,必须置空
where.mediaCategory = category.value || undefined;
return pageFiles({ ...where, ...orders, page, limit });
};
@@ -221,6 +274,20 @@
showEdit.value = true;
};
/* 点击缩略图:音视频开播放器,文档新窗口打开 */
const openPreview = (row: FileRecord) => {
const kind = kindOf(row);
if (kind === 'image') {
return;
}
if (kind === 'doc') {
window.open(row.url, '_blank');
return;
}
current.value = row;
showPreview.value = true;
};
/* 删除单个 */
const remove = (row: FileRecord) => {
const hide = messageLoading('请求中..', 0);
@@ -266,12 +333,9 @@
// 上传文件
const onUpload = (item) => {
const { file } = item;
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
const error = validateMediaFile(file);
if (error) {
message.error(error);
return;
}
const hide = messageLoading({
@@ -280,7 +344,7 @@
mask: true
});
uploadFile(file)
.then((data) => {
.then(() => {
hide();
message.success('上传成功');
reload();
@@ -294,6 +358,44 @@
<script lang="ts">
export default {
name: 'VideoIndex'
name: 'PhotoList'
};
</script>
<style lang="less" scoped>
.media-thumb {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 90px;
overflow: hidden;
background: #f5f6f8;
border-radius: 4px;
cursor: pointer;
:deep(.ant-image),
:deep(.ant-image-img) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.media-thumb-icon {
font-size: 28px;
}
.media-thumb-ext {
position: absolute;
right: 4px;
bottom: 4px;
padding: 0 4px;
font-size: 11px;
line-height: 16px;
color: #718096;
background: rgba(255, 255, 255, 0.9);
border-radius: 3px;
}
</style>