Files
tuanwei-admin/src/views/cms/photo/components/photo-edit.vue
T
weicw 42fddc5c85 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 的镜像,两边要一起改
2026-11-01 21:03:56 +08:00

257 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 素材编辑弹窗:文件在列表页上传时已入库,这里只改元数据和分享代码 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:confirm-loading="loading"
title="编辑素材"
:body-style="{ paddingBottom: '8px' }"
okText="保存"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="文件">
<div class="media-file">
<component
:is="fileIcon"
class="media-file-icon"
:style="{ color: fileColor }"
/>
<div class="media-file-info">
<div class="media-file-name" :title="record?.name">
{{ record?.name }}
</div>
<div class="ele-text-secondary media-file-sub">
{{ kindLabel }} · {{ fileSize }}
</div>
</div>
</div>
</a-form-item>
<a-form-item label="文件名称" name="name">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入文件名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="素材描述"
v-model:value="form.comments"
/>
</a-form-item>
<div style="margin-bottom: 22px">
<a-divider orientation="left">{{ shareTitle }}</a-divider>
</div>
<a-form-item label="素材链接">
<a-input v-model:value="share.url" @click="copyText(share.url)" />
</a-form-item>
<a-form-item label="网页代码(HTML)" v-if="share.html">
<a-input v-model:value="share.html" @click="copyText(share.html)" />
</a-form-item>
<a-form-item label="Markdown">
<a-input
v-model:value="share.Markdown"
@click="copyText(share.Markdown)"
/>
</a-form-item>
<a-form-item label="论坛BBCode" v-if="share.bbCode">
<a-input v-model:value="share.bbCode" @click="copyText(share.bbCode)" />
</a-form-item>
<a-form-item label="下载地址">
<a-input
v-model:value="share.download"
@click="copyText(share.download)"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { FileRecord } from '@/api/system/file/model';
import { updateFiles } from '@/api/system/file';
import { copyText } from '@/utils/common';
import {
formatFileSize,
mediaColor,
mediaIcon,
mediaKindOfRecord,
MEDIA_KIND_LABEL
} from '@/utils/media';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: FileRecord | null;
}>();
const formRef = ref<FormInstance | null>(null);
// 当前编辑的原始记录,分享代码依赖它的 url,不随表单变化
const record = ref<FileRecord | null>(null);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<FileRecord>({
id: 0,
name: '',
comments: ''
});
const kind = computed(() => mediaKindOfRecord(record.value));
const kindLabel = computed(() => MEDIA_KIND_LABEL[kind.value]);
const fileSize = computed(() => formatFileSize(record.value?.length));
const fileIcon = computed(() =>
mediaIcon(kind.value, record.value?.path || record.value?.name)
);
const fileColor = computed(() =>
mediaColor(kind.value, record.value?.path || record.value?.name)
);
// 文档给的是链接与下载地址,图片/音视频给的是嵌入代码
const shareTitle = computed(() =>
kind.value === 'doc' ? '素材链接' : '素材分享'
);
/**
* 分享代码按类型生成:图片沿用 img 系列,音视频给 video/audio 标签,
* 文档只给直链和下载地址(不提供 HTML 嵌入)。
*/
const share = computed(() => {
const data = record.value;
const url = data?.url || '';
const name = data?.name || '';
const result = {
url,
html: '',
Markdown: `[${name}](${url})`,
bbCode: `[url=${url}]${name}[/url]`,
download: data?.downloadUrl || ''
};
if (kind.value === 'image') {
result.html = `<a href="${url}"><img src="${url}" alt="${name}" border="0" /></a>`;
result.Markdown = `[![${name}](${url})](${url})`;
result.bbCode = `[url=${url}][img]${url}[/img][/url]`;
} else if (kind.value === 'video') {
result.html = `<video src="${url}" controls></video>`;
} else if (kind.value === 'audio') {
result.html = `<audio src="${url}" controls></audio>`;
}
return result;
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
name: [
{
required: true,
message: '请输入文件名称',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
updateFiles(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
record.value = props.data ?? null;
if (props.data) {
assignFields(props.data);
}
} else {
record.value = null;
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<style lang="less" scoped>
.media-file {
display: flex;
align-items: center;
min-width: 0;
}
.media-file-icon {
flex: none;
font-size: 32px;
}
.media-file-info {
flex: 1;
min-width: 0;
margin-left: 10px;
}
.media-file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-file-sub {
font-size: 12px;
}
</style>