diff --git a/src/api/system/file/model/index.ts b/src/api/system/file/model/index.ts index 3388507..aac5fe6 100644 --- a/src/api/system/file/model/index.ts +++ b/src/api/system/file/model/index.ts @@ -39,6 +39,8 @@ export interface FileRecordParam extends PageParam { name?: string; path?: string; contentType?: string; + // 媒体大类: image/video/audio/doc,空表示不限 + mediaCategory?: string; createNickname?: string; count?: number; page?: number; diff --git a/src/utils/media.ts b/src/utils/media.ts new file mode 100644 index 0000000..5df2b93 --- /dev/null +++ b/src/utils/media.ts @@ -0,0 +1,220 @@ +import type { Component } from 'vue'; +import { + CustomerServiceOutlined, + FileExcelOutlined, + FileMarkdownOutlined, + FilePdfOutlined, + FilePptOutlined, + FileTextOutlined, + FileUnknownOutlined, + FileWordOutlined, + VideoCameraOutlined +} from '@ant-design/icons-vue'; +import type { FileRecord } from '@/api/system/file/model'; + +/** + * 媒体大类。 + * + * 注意它和 `contentType`(MIME 字符串)不是一回事:MIME 里「文档」没有统一前缀 + * (pdf/docx/zip 都是 application/…),所以大类由 MIME 前缀 + 后缀一起判定。 + */ +export type MediaKind = 'image' | 'video' | 'audio' | 'doc'; + +/** 与后端 UploadRules 的白名单保持一致,改这里要同步改后端 */ +export const MEDIA_ACCEPT = [ + 'image/*', + '.pdf', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.ppt', + '.pptx', + '.txt', + '.md', + '.csv', + '.mp3', + '.wav', + '.m4a', + '.aac', + '.mp4', + '.mov', + '.webm' +].join(','); + +const DOC_SUFFIXES = [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt', + 'md', + 'csv' +]; +const VIDEO_SUFFIXES = ['mp4', 'mov', 'webm']; +const AUDIO_SUFFIXES = ['mp3', 'wav', 'm4a', 'aac']; + +/** 图片大小上限(MB),其余类型上限(MB),受后端 multipart 100MB 硬顶约束 */ +export const MEDIA_MAX_MB_IMAGE = 10; +export const MEDIA_MAX_MB_OTHER = 100; + +/** 类型筛选项,value 为空表示不限 */ +export const MEDIA_CATEGORY_OPTIONS: { value: string; label: string }[] = [ + { value: '', label: '全部类型' }, + { value: 'image', label: '图片' }, + { value: 'doc', label: '文档' }, + { value: 'audio', label: '音频' }, + { value: 'video', label: '视频' } +]; + +export const MEDIA_KIND_LABEL: Record = { + image: '图片', + video: '视频', + audio: '音频', + doc: '文档' +}; + +/** 取小写后缀,不带点 */ +export function mediaSuffix(name?: string | null): string { + if (!name || name.lastIndexOf('.') < 0) { + return ''; + } + return name.substring(name.lastIndexOf('.') + 1).toLowerCase(); +} + +/** 由 MIME + 文件名判定大类 */ +export function mediaKindOf( + contentType?: string | null, + name?: string | null +): MediaKind { + const mime = (contentType || '').toLowerCase(); + if (mime.startsWith('image/')) { + return 'image'; + } + if (mime.startsWith('video/')) { + return 'video'; + } + if (mime.startsWith('audio/')) { + return 'audio'; + } + const suffix = mediaSuffix(name); + if (VIDEO_SUFFIXES.includes(suffix)) { + return 'video'; + } + if (AUDIO_SUFFIXES.includes(suffix)) { + return 'audio'; + } + return 'doc'; +} + +/** 由文件记录判定大类 */ +export function mediaKindOfRecord(record?: FileRecord | null): MediaKind { + return mediaKindOf(record?.contentType, record?.path || record?.name); +} + +/** + * 上传前校验,通过返回 null,不通过返回给用户看的错误文案。 + * 与后端同规则:图片按 MIME 认,其余按后缀白名单认。 + */ +export function validateMediaFile(file: File): string | null { + const mime = (file.type || '').toLowerCase(); + const suffix = mediaSuffix(file.name); + const isImage = mime.startsWith('image/'); + if (!isImage) { + const allowed = + DOC_SUFFIXES.includes(suffix) || + VIDEO_SUFFIXES.includes(suffix) || + AUDIO_SUFFIXES.includes(suffix); + if (!allowed) { + return '不支持的文件格式,仅支持图片、文档、音频、视频'; + } + } + const maxMb = isImage ? MEDIA_MAX_MB_IMAGE : MEDIA_MAX_MB_OTHER; + if (file.size / 1024 / 1024 > maxMb) { + return `文件大小不能超过 ${maxMb}MB`; + } + return null; +} + +/** 文件大小格式化 */ +export function formatFileSize(length?: number | null): string { + const size = Number(length ?? 0); + if (!size || size < 0) { + return '-'; + } + if (size < 1024) { + return size + 'B'; + } + if (size < 1024 * 1024) { + return (size / 1024).toFixed(1) + 'KB'; + } + if (size < 1024 * 1024 * 1024) { + return (size / 1024 / 1024).toFixed(1) + 'M'; + } + return (size / 1024 / 1024 / 1024).toFixed(1) + 'G'; +} + +/** 卡片/表格里显示的类型图标 */ +export function mediaIcon(kind: MediaKind, name?: string | null): Component { + if (kind === 'audio') { + return CustomerServiceOutlined; + } + if (kind === 'video') { + return VideoCameraOutlined; + } + switch (mediaSuffix(name)) { + case 'pdf': + return FilePdfOutlined; + case 'doc': + case 'docx': + return FileWordOutlined; + case 'xls': + case 'xlsx': + case 'csv': + return FileExcelOutlined; + case 'ppt': + case 'pptx': + return FilePptOutlined; + case 'md': + return FileMarkdownOutlined; + case 'txt': + return FileTextOutlined; + default: + return FileUnknownOutlined; + } +} + +/** 图标色,按类型区分但都压在中低饱和,避免卡片花花绿绿 */ +export function mediaColor(kind: MediaKind, name?: string | null): string { + if (kind === 'audio') { + return '#805ad5'; + } + if (kind === 'video') { + return '#2b6cb0'; + } + switch (mediaSuffix(name)) { + case 'pdf': + return '#e5484d'; + case 'doc': + case 'docx': + return '#2b6cb0'; + case 'xls': + case 'xlsx': + case 'csv': + return '#2f855a'; + case 'ppt': + case 'pptx': + return '#c05621'; + default: + return '#718096'; + } +} + +/** 非图片卡片右下角的扩展名徽标文本 */ +export function mediaExtLabel(record?: FileRecord | null): string { + const suffix = mediaSuffix(record?.path || record?.name); + return suffix ? suffix.toUpperCase() : 'FILE'; +} diff --git a/src/views/cms/photo/components/media-preview.vue b/src/views/cms/photo/components/media-preview.vue new file mode 100644 index 0000000..7280aa3 --- /dev/null +++ b/src/views/cms/photo/components/media-preview.vue @@ -0,0 +1,93 @@ + + + + + + diff --git a/src/views/cms/photo/components/photo-edit.vue b/src/views/cms/photo/components/photo-edit.vue index 7e0c4f9..12ebeca 100644 --- a/src/views/cms/photo/components/photo-edit.vue +++ b/src/views/cms/photo/components/photo-edit.vue @@ -1,10 +1,10 @@ - + + + diff --git a/src/views/cms/photo/image.vue b/src/views/cms/photo/image.vue index b9337a7..3f550c1 100644 --- a/src/views/cms/photo/image.vue +++ b/src/views/cms/photo/image.vue @@ -1,9 +1,10 @@ +