feat: 优化删除标签模态框和上传组件,新增文件预签名URL获取功能

- 简化 `delete-tag.vue` 模态框模板结构
- 更新 `add-raw-material-drawer/index.vue` 组件,增加文件预签名URL获取和上传状态显示
- 新增 `getFilePreSignedUrl` API 函数
- 更新 `tools.ts` 中的文件扩展名提取函数
- 优化按钮激活状态样式
- 添加 `icon-no-text.png` 图标文件
This commit is contained in:
rd
2025-09-17 11:59:31 +08:00
parent 7c85582564
commit c08b13673f
8 changed files with 214 additions and 80 deletions

View File

@ -73,6 +73,11 @@ export const getVideoPreSignedUrl = (params = {}) => {
return Http.get('/v1/oss/video-pre-signed-url', params); return Http.get('/v1/oss/video-pre-signed-url', params);
}; };
// 获取文件上传地址
export const getFilePreSignedUrl = (params = {}) => {
return Http.get('/v1/oss/file-pre-signed-url', params);
};
// 清除限流 // 清除限流
export const postClearRateLimiter = (params = {}) => { export const postClearRateLimiter = (params = {}) => {
return Http.post(`/v1/rate-limiter/clear`, params); return Http.post(`/v1/rate-limiter/clear`, params);

View File

@ -71,7 +71,7 @@
} }
&:active { &:active {
background: $color-primary-7 !important; background-color: $color-primary-7 !important;
} }
} }

View File

@ -112,11 +112,15 @@ export function genRandomId() {
return `id_${dayjs().unix()}_${Math.floor(Math.random() * 10000)}`; return `id_${dayjs().unix()}_${Math.floor(Math.random() * 10000)}`;
} }
export function getFileExtension(filename) {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
export function formatFileSize(bytes: number): string { export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'kb', 'mB', 'gB', 'tB']; const sizes = ['Bytes', 'kb', 'mb', 'gb', 'tb'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
@ -218,7 +222,7 @@ export function getVideoInfo(file: File): Promise<{ duration: number; firstFrame
}; };
// 设置视频源以触发加载 // 设置视频源以触发加载
video.src = URL.createObjectURL(file); video.src = window.URL.createObjectURL(file);
// 设置超时,防止长时间无响应 // 设置超时,防止长时间无响应
setTimeout(() => { setTimeout(() => {

View File

@ -1,5 +1,5 @@
<script lang="tsx"> <script lang="tsx">
import { Drawer, Button, Upload, Table, Input } from 'ant-design-vue'; import { Drawer, Button, Upload, Table, Input, Progress } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
const { TextArea } = Input; const { TextArea } = Input;
@ -7,15 +7,24 @@ const { TextArea } = Input;
import CommonSelect from '@/components/common-select'; import CommonSelect from '@/components/common-select';
import ImgLazyLoad from '@/components/img-lazy-load'; import ImgLazyLoad from '@/components/img-lazy-load';
import { formatFileSize, getVideoInfo, formatDuration, formatUploadSpeed } from '@/utils/tools'; import { formatFileSize, getVideoInfo, getFileExtension, formatUploadSpeed } from '@/utils/tools';
import { getRawMaterialTagsList } from '@/api/all/generationWorkshop'; import { getRawMaterialTagsList } from '@/api/all/generationWorkshop';
import { getFilePreSignedUrl } from '@/api/all/common';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '../../img/icon-no-text.png';
import axios from 'axios';
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']; const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.wmv', '.m4v']; const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.wmv', '.m4v'];
const documentExtensions = ['.txt', '.doc', '.docx', '.pdf', '.xls', '.xlsx']; const documentExtensions = ['.txt', '.doc', '.docx', '.pdf', '.xls', '.xlsx'];
enum EnumUploadStatus {
done = 'done',
error = 'error',
uploading = 'uploading',
}
export default defineComponent({ export default defineComponent({
setup(_, { attrs, slots, expose }) { setup(_, { attrs, slots, expose }) {
const visible = ref(false); const visible = ref(false);
@ -23,8 +32,8 @@ export default defineComponent({
const uploadData = ref([]); const uploadData = ref([]);
const tagData = ref([]); const tagData = ref([]);
const checkSuccessNum = computed(() => { const uploadSuccessNum = computed(() => {
return uploadData.value.filter((item) => item.status === 'success').length; return uploadData.value.filter((item) => item.uploadStatus === EnumUploadStatus.done).length;
}); });
const getTagData = async () => { const getTagData = async () => {
@ -48,31 +57,120 @@ export default defineComponent({
}; };
const handleUpload = async (option) => { const handleUpload = async (option) => {
// console.log('handleUpload', option); const { file } = option;
const { name, size, type, uid } = file;
let statusText = '';
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
const isImage = imageExtensions.includes(ext);
const isVideo = videoExtensions.includes(ext);
const isDocument = documentExtensions.includes(ext);
if (!isImage && !isVideo && !isDocument) {
statusText = '当前格式不支持';
} else if (
(isImage && size > 20 * 1024 * 1024) || // 图片大于20MB
(isVideo && size > 1000 * 1024 * 1024) || // 视频大于1000MB
(isDocument && size > 20 * 1024 * 1024) // 文档大于20MB
) {
statusText = '文件大小超出限制';
}
const { fileTypeLabel, cover } = await getFileInfo(file);
const currentData = {
uid,
name,
uploadStatus: statusText ? EnumUploadStatus.error : EnumUploadStatus.uploading,
statusText,
percent: 0,
size: formatFileSize(size),
fileTypeLabel,
tag_ids: [],
cover,
}; };
uploadData.value.push(currentData);
const getFileType = (fileName) => { if (statusText) return;
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
if (imageExtensions.includes(ext)) { try {
return '图片'; const response = await getFilePreSignedUrl({ suffix: getFileExtension(name) });
} else if (videoExtensions.includes(ext)) { const { upload_url } = response?.data;
return '视频'; if (!upload_url) {
} else if (documentExtensions.includes(ext)) { throw new Error('未能获取有效的预签名上传地址');
return '文档'; }
} else {
return '其他'; const blob = new Blob([file], { type });
await axios.put(upload_url, blob, {
headers: { 'Content-Type': type },
onUploadProgress: (progressEvent) => {
const _target = uploadData.value.find((item) => item.uid === uid);
const percentCompleted = Math.round(progressEvent.progress * 100);
_target.percent = percentCompleted;
percentCompleted === 100 && (_target.uploadStatus = EnumUploadStatus.done);
},
});
} catch {
currentData.uploadStatus = EnumUploadStatus.error;
} }
}; };
const onConfirm = () => { const getFileInfo = async (file: any) => {
const { name } = file;
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
if (imageExtensions.includes(ext)) {
return {
fileTypeLabel: '图片',
cover: URL.createObjectURL(file),
};
} else if (videoExtensions.includes(ext)) {
const { firstFrame } = await getVideoInfo(file);
return {
fileTypeLabel: '视频',
cover: firstFrame,
};
} else if (documentExtensions.includes(ext)) {
return {
fileTypeLabel: '文本',
cover: icon2,
};
} else {
return {
fileTypeLabel: '其他',
cover: '',
};
}
};
const onConfirm = async () => {
console.log('onConfirm'); console.log('onConfirm');
}; };
const handleDelete = (file) => { const handleDelete = (file) => {
uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid); uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid);
}; };
const renderUploadList = (file, actions) => { const renderUploadStatus = (record) => {
if (record.uploadStatus === EnumUploadStatus.error) {
return (
<div>
<p class="upload-text">上传失败</p>
<span class="upload-text color-#F64B31">{record.statusText}</span>
</div>
);
}
return (
<div>
<span class="upload-text ml-26px">
{record.uploadStatus === EnumUploadStatus.done ? '上传成功' : '上传中'}
</span>
<Progress percent={record.percent} class="m-0 p-0 " strokeColor="#25C883" />
</div>
);
};
const renderUploadList = () => {
if (!uploadData.value.length) return null; if (!uploadData.value.length) return null;
console.log('renderUploadList', uploadData.value); console.log('renderUploadList', uploadData.value);
@ -80,7 +178,7 @@ export default defineComponent({
return ( return (
<> <>
<p class="cts !color-#939499 mb-10px"> <p class="cts !color-#939499 mb-10px">
已上传<span class="!color-#211F24">{`${checkSuccessNum.value}/${uploadData.value.length}`}</span> 已上传<span class="!color-#211F24">{`${uploadSuccessNum.value}/${uploadData.value.length}`}</span>
</p> </p>
<Table ref="tableRef" dataSource={uploadData.value} pagination={false} class="manuscript-table w-100% flex-1"> <Table ref="tableRef" dataSource={uploadData.value} pagination={false} class="manuscript-table w-100% flex-1">
<Column <Column
@ -91,7 +189,7 @@ export default defineComponent({
ellipsis={true} ellipsis={true}
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
<div class="flex items-center"> <div class="flex items-center justify-end">
<ImgLazyLoad width={64} height={64} src={record.cover} class="!rounded-6px mr-16px flex-shrink-0" /> <ImgLazyLoad width={64} height={64} src={record.cover} class="!rounded-6px mr-16px flex-shrink-0" />
<TextArea <TextArea
@ -108,53 +206,30 @@ export default defineComponent({
/> />
<Column <Column
title="上传状态" title="上传状态"
dataIndex="status" dataIndex="uploadStatus"
key="status" key="uploadStatus"
width={164} width={164}
customRender={({ text, record }) => { customRender={({ text, record }) => renderUploadStatus(record)}
if (record.status === 'done') {
return <span class="upload-text">上传成功</span>;
} else if (record.status === 'error') {
return <span class="upload-text">上传失败</span>;
} else {
return <span class="upload-text">上传中</span>;
}
}}
/> />
<Column <Column
title="标签" title="标签"
dataIndex="tags" dataIndex="tag_ids"
key="tags" key="tag_ids"
width={243} width={243}
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
<CommonSelect <CommonSelect
v-model={record.tags} v-model={record.tag_ids}
class="w-full" class="w-full"
multiple multiple
options={tagData} options={tagData.value}
placeholder="请选择标签" placeholder="请选择标签"
/> />
); );
}} }}
/> />
<Column <Column title="类型" dataIndex="fileTypeLabel" key="fileTypeLabel" width={80} />
title="类型" <Column title="大小" dataIndex="size" key="size" width={100} />
dataIndex="type"
key="type"
width={80}
customRender={({ text, record }) => {
const fileName = record.name || '';
return <span>{getFileType(fileName)}</span>;
}}
/>
<Column
title="大小"
dataIndex="size"
key="size"
width={100}
customRender={({ text, record }) => formatFileSize(record.size)}
/>
<Column <Column
title="操作" title="操作"
key="action" key="action"
@ -163,10 +238,14 @@ export default defineComponent({
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
<div class="flex items-center"> <div class="flex items-center">
{record.uploadStatus === EnumUploadStatus.uploading && (
<Button type="text" class="!h-22px !p-0 mr-16px"> <Button type="text" class="!h-22px !p-0 mr-16px">
取消生成 取消生成
</Button> </Button>
)}
<img <img
alt=""
class="cursor-pointer" class="cursor-pointer"
src={icon1} src={icon1}
width="14" width="14"
@ -196,7 +275,7 @@ export default defineComponent({
<section class="content flex-1 pt-8px px-24px pb-24px"> <section class="content flex-1 pt-8px px-24px pb-24px">
<div class=" rounded-16px bg-#F7F8FA p-16px flex flex-col items-center mb-24px"> <div class=" rounded-16px bg-#F7F8FA p-16px flex flex-col items-center mb-24px">
<Upload <Upload
v-model:file-list={uploadData.value} file-list={uploadData.value}
action="/" action="/"
multiple multiple
customRequest={handleUpload} customRequest={handleUpload}
@ -204,15 +283,13 @@ export default defineComponent({
accept={[...imageExtensions, ...videoExtensions, ...documentExtensions].join(',')} accept={[...imageExtensions, ...videoExtensions, ...documentExtensions].join(',')}
showUploadList={false} showUploadList={false}
> >
<div <div class="upload-box rounded-8px cursor-pointer h-100px w-full border border-dashed border-#D7D7D9 flex flex-col items-center justify-center w-full">
class="upload-box rounded-8px cursor-pointer h-100px w-full border border-dashed border-#D7D7D9 flex flex-col items-center justify-center w-full">
<icon-plus size="14" class="mb-10px color-#55585F" /> <icon-plus size="14" class="mb-10px color-#55585F" />
<span class="cts">点击或拖拽文件到此处上传</span> <span class="cts">点击或拖拽文件到此处上传</span>
</div> </div>
</Upload> </Upload>
<p class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`视频格式视频格式MP4、AVI、MOV大小<1000MB`}</p> <p class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`视频格式视频格式MP4、AVI、MOV大小<1000MB`}</p>
<p <p class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`图片格式PNG、JPG、JPEG、GIF、WEBP、BMP大小<20MB`}</p>
class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`图片格式PNG、JPG、JPEG、GIF、WEBP、BMP大小<20MB`}</p>
<p class="cts !color-#939499 !text-12px !lh-20px">{`文本格式TXT、DOC、DOCX、PDF大小<20MB`}</p> <p class="cts !color-#939499 !text-12px !lh-20px">{`文本格式TXT、DOC、DOCX、PDF大小<20MB`}</p>
</div> </div>
{renderUploadList()} {renderUploadList()}
@ -227,7 +304,7 @@ export default defineComponent({
type="primary" type="primary"
onClick={onConfirm} onClick={onConfirm}
loading={submitLoading.value} loading={submitLoading.value}
disabled={!checkSuccessNum.value} disabled={!uploadSuccessNum.value}
> >
确定 确定
</Button> </Button>

View File

@ -40,6 +40,62 @@
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
} }
.ant-progress {
display: flex;
align-items: center;
justify-content: center;
.ant-progress-outer {
position: relative;
width: 100%;
height: 6px !important;
margin: 0;
padding: 0;
.ant-progress-inner {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 100px;
height: 6px !important;
background: var(--BG-200, #F2F3F5);
.ant-progress-bg {
height: 6px !important;
background-color: #6D4CFE;
}
}
}
.ant-progress-text {
color: var(--Text-1, #211F24);
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
.anticon {
font-size: 16px;
}
}
&.ant-progress-status-success {
.ant-progress-outer {
.ant-progress-bg {
background-color: #25C883;
}
}
.ant-progress-text {
color: #25C883;
}
}
}
} }
.footer { .footer {

View File

@ -4,7 +4,6 @@
:title="isEdit ? '编辑标签' : '添加新标签'" :title="isEdit ? '编辑标签' : '添加新标签'"
centered centered
width="400px" width="400px"
wrapClassName="tags-manage-modal"
@cancel="onClose" @cancel="onClose"
> >
<Form ref="formRef" :model="form" :rules="rules" auto-label-width layout="horizontal"> <Form ref="formRef" :model="form" :rules="rules" auto-label-width layout="horizontal">

View File

@ -3,28 +3,21 @@
* @Date: 2025-06-26 17:23:52 * @Date: 2025-06-26 17:23:52
--> -->
<template> <template>
<Modal <Modal v-model:open="visible" centered title="删除标签" width="400px" @cancel="onClose">
v-model:open="visible"
centered
title="删除标签"
width="400px"
wrapClassName="account-manage-modal"
@cancel="onClose"
>
<div class="flex items-center"> <div class="flex items-center">
<img :src="icon1" class="mr-12px" height="20" width="20" /> <img :src="icon1" class="mr-12px" height="20" width="20" />
<span>确认删除 "{{ tagName }}" 这个标签吗</span> <span>确认删除 "{{ tagName }}" 这个标签吗</span>
</div> </div>
<template #footer> <template #footer>
<Button size="large" @click="onClose">取消</Button> <Button size="large" @click="onClose">取消</Button>
<Button class="ml-16px" danger size="large" type="primary" @click="onDelete">确认删除</Button> <Button class="ml-16px" danger size="large" type="primary" @click="onSubmit">确认删除</Button>
</template> </template>
</Modal> </Modal>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { Button, message } from 'ant-design-vue'; import { Button, message, Modal } from 'ant-design-vue';
import { deleteRawMaterialTag } from '@/api/all/generationWorkshop'; import { deleteRawMaterialTag } from '@/api/all/generationWorkshop';
import icon1 from '@/assets/img/media-account/icon-warn-1.png'; import icon1 from '@/assets/img/media-account/icon-warn-1.png';
@ -35,12 +28,12 @@ const visible = ref(false);
const tagId = ref(''); const tagId = ref('');
const tagName = ref(''); const tagName = ref('');
function onClose() { const onClose = () => {
visible.value = false; visible.value = false;
tagId.value = ''; tagId.value = '';
tagName.value = ''; tagName.value = '';
emits('close'); emits('close');
} };
const open = (record) => { const open = (record) => {
const { id = '', name = '' } = record; const { id = '', name = '' } = record;
@ -50,14 +43,14 @@ const open = (record) => {
visible.value = true; visible.value = true;
}; };
async function onDelete() { const onSubmit = async () => {
const { code } = await deleteRawMaterialTag(tagId.value); const { code } = await deleteRawMaterialTag(tagId.value);
if (code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
emits('success'); emits('success');
onClose(); onClose();
} }
} };
defineExpose({ open }); defineExpose({ open });
</script> </script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB