feat: 优化原料库上传功能和标签管理

- 在 `AddRawMaterialDrawer` 组件中添加 `onUpdate` 事件以刷新数据
- 更新 `constants.ts` 中操作列的宽度
- 增加 Ant Select 和 Modal 的样式
- 新增批量添加、修改和详情的 API 函数
- 优化 `add-raw-material-drawer` 组件,增加标签输入和删除确认模态框
- 更新 `ant-select.scss` 和 `ant-modal.scss` 样式文件
This commit is contained in:
rd
2025-09-17 15:57:19 +08:00
parent c08b13673f
commit bf7963234e
8 changed files with 265 additions and 45 deletions

View File

@ -191,3 +191,19 @@ export const putRawMaterialTag = (params = {}) => {
export const deleteRawMaterialTag = (id: string) => { export const deleteRawMaterialTag = (id: string) => {
return Http.delete(`/v1/raw-material-tags/${id}`); return Http.delete(`/v1/raw-material-tags/${id}`);
}; };
// 原料库-本地批量添加
export const postBatchRawMaterial = (params = {}) => {
return Http.post('/v1/raw-materials/batch', params);
};
// 原料库-修改
export const putRawMaterial = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/raw-material/${id}`, rest);
};
// 原料库-详情
export const getRawMaterialDetail = (id: string) => {
return Http.get(`/v1/raw-material/${id}`);
};

View File

@ -22,7 +22,7 @@
} }
} }
.ant-modal-body { .ant-modal-body {
padding: 24px 20px; padding: 20px 24px;
} }
.ant-modal-footer { .ant-modal-footer {
margin-top: 0; margin-top: 0;
@ -38,5 +38,30 @@
} }
} }
} }
.ant-modal-confirm-body-wrapper {
.ant-modal-confirm-title {
.anticon {
font-size: 24px;
}
}
.ant-modal-confirm-content {
margin-top: 8px;
color: var(--Text-2, #55585F);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.ant-modal-confirm-btns {
display: flex;
justify-content: end;
margin-top: 24px;
}
}
} }
} }

View File

@ -36,6 +36,20 @@
border-color: $color-error !important; border-color: $color-error !important;
} }
&:not(.ant-select-disabled) {
&:hover {
.ant-select-selector {
border-color: rgb(var(--primary-6)) !important;
}
}
}
&-disabled {
.ant-select-selector {
background-color: var(--BG-200, #f2f3f5) !important;
}
}
} }
.ant-select { .ant-select {
@ -57,6 +71,7 @@
&.ant-select-multiple { &.ant-select-multiple {
.ant-select-selector { .ant-select-selector {
height: fit-content !important;
padding: 0 12px 0 4px !important; padding: 0 12px 0 4px !important;
.ant-select-selection-overflow-item { .ant-select-selection-overflow-item {

View File

@ -3,6 +3,13 @@
padding: 8px 12px 4px 12px; padding: 8px 12px 4px 12px;
} }
&:not(.ant-input-textarea-disabled) {
&:hover {
.ant-input {
border-color: rgb(var(--primary-6)) !important;
}
}
}
&.ant-input-textarea-show-count { &.ant-input-textarea-show-count {
&::after { &::after {
position: absolute; position: absolute;

View File

@ -1,14 +1,16 @@
<script lang="tsx"> <script lang="tsx">
import { Drawer, Button, Upload, Table, Input, Progress } from 'ant-design-vue'; import { Drawer, Button, Upload, Table, Input, Progress, message, Select, Modal } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select;
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 TextOverTips from '@/components/text-over-tips';
import { formatFileSize, getVideoInfo, getFileExtension, formatUploadSpeed } from '@/utils/tools'; import { formatFileSize, getVideoInfo, getFileExtension } from '@/utils/tools';
import { getRawMaterialTagsList } from '@/api/all/generationWorkshop'; import { getRawMaterialTagsList, postBatchRawMaterial, posRawMaterialTags } from '@/api/all/generationWorkshop';
import { getFilePreSignedUrl } from '@/api/all/common'; 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';
@ -26,11 +28,13 @@ enum EnumUploadStatus {
} }
export default defineComponent({ export default defineComponent({
setup(_, { attrs, slots, expose }) { emits: ['update'],
setup(_, { emit, expose }) {
const visible = ref(false); const visible = ref(false);
const submitLoading = ref(false); const submitLoading = ref(false);
const uploadData = ref([]); const uploadData = ref([]);
const tagData = ref([]); const tagData = ref([]);
const modalRef = ref(null);
const uploadSuccessNum = computed(() => { const uploadSuccessNum = computed(() => {
return uploadData.value.filter((item) => item.uploadStatus === EnumUploadStatus.done).length; return uploadData.value.filter((item) => item.uploadStatus === EnumUploadStatus.done).length;
@ -43,6 +47,80 @@ export default defineComponent({
} }
}; };
const handleTagChange = (record) => {
console.log(record.tag_ids);
};
// 添加处理标签输入的函数
const handleTagInputPressEnter = async (e, record) => {
const inputValue = e.target.value.trim();
if (!inputValue) return;
if (record.tag_ids.length >= 5) {
message.warning('最多选择5个');
return;
}
const _target = tagData.value.find((item) => item.name === inputValue);
if (_target) {
record.tag_ids = [...record.tag_ids, _target.id];
return;
}
try {
const { code, data } = await posRawMaterialTags({ name: inputValue });
if (code === 200 && data) {
tagData.value.push({
id: data.id,
name: data.name,
});
e.target.value = '';
record.tag_ids = [...record.tag_ids, data.id];
}
} catch (error) {
message.error('添加标签失败');
}
};
const onCancel = () => {
if (!uploadData.value.length) {
onClose();
return;
}
modalRef.value = Modal.warning({
title: '确定要取消上传吗?',
content: '取消后上传将中断,已传输的内容不会保存',
cancelText: '取消',
okText: '确定',
centered: true,
footer: (
<div class="flex items-center justify-end mt-24px">
<Button
onClick={() => {
modalRef.value.destroy();
onClose();
}}
class="mr-12px"
>
确认取消
</Button>
<Button
type="primary"
onClick={() => {
modalRef.value.destroy();
}}
>
继续上传
</Button>
</div>
),
});
};
const open = () => { const open = () => {
getTagData(); getTagData();
visible.value = true; visible.value = true;
@ -53,6 +131,8 @@ export default defineComponent({
uploadData.value = []; uploadData.value = [];
tagData.value = []; tagData.value = [];
modalRef.value?.destroy();
modalRef.value = null;
submitLoading.value = false; submitLoading.value = false;
}; };
@ -76,18 +156,21 @@ export default defineComponent({
statusText = '文件大小超出限制'; statusText = '文件大小超出限制';
} }
const { fileTypeLabel, cover } = await getFileInfo(file); const { fileTypeLabel, cover, fileType } = await getFileInfo(file);
const currentData = { const currentData = {
uid, uid,
name, name,
// type,
type: fileType,
uploadStatus: statusText ? EnumUploadStatus.error : EnumUploadStatus.uploading, uploadStatus: statusText ? EnumUploadStatus.error : EnumUploadStatus.uploading,
statusText, statusText,
percent: 0, percent: 0,
size: formatFileSize(size), size,
fileTypeLabel, fileTypeLabel,
tag_ids: [], tag_ids: [],
cover, cover,
file: '',
}; };
uploadData.value.push(currentData); uploadData.value.push(currentData);
@ -95,7 +178,9 @@ export default defineComponent({
try { try {
const response = await getFilePreSignedUrl({ suffix: getFileExtension(name) }); const response = await getFilePreSignedUrl({ suffix: getFileExtension(name) });
const { upload_url } = response?.data; const { upload_url, file_url } = response?.data;
const _target = uploadData.value.find((item) => item.uid === uid);
_target.file = file_url;
if (!upload_url) { if (!upload_url) {
throw new Error('未能获取有效的预签名上传地址'); throw new Error('未能获取有效的预签名上传地址');
} }
@ -104,7 +189,6 @@ export default defineComponent({
await axios.put(upload_url, blob, { await axios.put(upload_url, blob, {
headers: { 'Content-Type': type }, headers: { 'Content-Type': type },
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
const _target = uploadData.value.find((item) => item.uid === uid);
const percentCompleted = Math.round(progressEvent.progress * 100); const percentCompleted = Math.round(progressEvent.progress * 100);
_target.percent = percentCompleted; _target.percent = percentCompleted;
@ -123,21 +207,25 @@ export default defineComponent({
if (imageExtensions.includes(ext)) { if (imageExtensions.includes(ext)) {
return { return {
fileTypeLabel: '图片', fileTypeLabel: '图片',
fileType: 0,
cover: URL.createObjectURL(file), cover: URL.createObjectURL(file),
}; };
} else if (videoExtensions.includes(ext)) { } else if (videoExtensions.includes(ext)) {
const { firstFrame } = await getVideoInfo(file); const { firstFrame } = await getVideoInfo(file);
return { return {
fileTypeLabel: '视频', fileTypeLabel: '视频',
fileType: 1,
cover: firstFrame, cover: firstFrame,
}; };
} else if (documentExtensions.includes(ext)) { } else if (documentExtensions.includes(ext)) {
return { return {
fileTypeLabel: '文本', fileTypeLabel: '文本',
fileType: 2,
cover: icon2, cover: icon2,
}; };
} else { } else {
return { return {
fileType: '',
fileTypeLabel: '其他', fileTypeLabel: '其他',
cover: '', cover: '',
}; };
@ -145,10 +233,52 @@ export default defineComponent({
}; };
const onConfirm = async () => { const onConfirm = async () => {
console.log('onConfirm'); const hasUploading = uploadData.value.some((item) => item.uploadStatus === EnumUploadStatus.uploading);
if (hasUploading) {
modalRef.value = Modal.warning({
title: '上传未完成',
content: <p class="h-22px">当前原料正在上传中关闭弹窗将导致上传失败请等待上传完成后再点击确定</p>,
okText: '我知道了',
centered: true,
});
return;
}
const { code, data } = await postBatchRawMaterial({ raw_materials: uploadData.value });
if (code === 200) {
message.success('上传成功');
emit('update');
onClose();
}
}; };
const handleDelete = (file) => { const openDeleteModal = (file) => {
uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid); modalRef.value = Modal.warning({
title: '确定删除该文件吗?',
content: <p class="h-22px"></p>,
cancelText: '取消',
okText: '确定',
centered: true,
footer: (
<div class="flex items-center justify-end mt-24px">
<Button
onClick={() => {
modalRef.value.destroy();
}}
class="mr-12px"
>
取消
</Button>
<Button
type="primary"
onClick={() => {
uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid);
modalRef.value.destroy();
}}
>
确定
</Button>
</div>
),
});
}; };
const renderUploadStatus = (record) => { const renderUploadStatus = (record) => {
@ -165,7 +295,7 @@ export default defineComponent({
<span class="upload-text ml-26px"> <span class="upload-text ml-26px">
{record.uploadStatus === EnumUploadStatus.done ? '上传成功' : '上传中'} {record.uploadStatus === EnumUploadStatus.done ? '上传成功' : '上传中'}
</span> </span>
<Progress percent={record.percent} class="m-0 p-0 " strokeColor="#25C883" /> <Progress percent={record.percent} class="m-0 p-0 " />
</div> </div>
); );
}; };
@ -173,19 +303,23 @@ export default defineComponent({
const renderUploadList = () => { const renderUploadList = () => {
if (!uploadData.value.length) return null; if (!uploadData.value.length) return null;
console.log('renderUploadList', uploadData.value);
return ( return (
<> <>
<p class="cts !color-#939499 mb-10px"> <p class="cts !color-#939499 mb-10px">
已上传<span class="!color-#211F24">{`${uploadSuccessNum.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="w-100% flex-1"
scroll={{ y: '100%' }}
>
<Column <Column
title="文件名称" title="文件名称"
dataIndex="name" dataIndex="name"
key="name" key="name"
width={347} width={385}
ellipsis={true} ellipsis={true}
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
@ -218,39 +352,62 @@ export default defineComponent({
width={243} width={243}
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
<CommonSelect <Select
v-model={record.tag_ids} disabled={record.uploadStatus === EnumUploadStatus.uploading}
class="w-full" v-model:value={record.tag_ids}
multiple mode="tags"
options={tagData.value} size="middle"
placeholder="请选择标签" placeholder="请选择标签"
/> allowClear
class="w-full"
showSearch
showArrow
maxTagCount={5}
maxTagTextLength={5}
onChnange={() => handleTagChange(record)}
onInputKeyDown={(e) => {
// 检测回车键
if (e.key === 'Enter') {
e.preventDefault();
handleTagInputPressEnter(e, record);
}
}}
>
{tagData.value.map((item) => (
<Option value={item.id} key={item.id}>
{item.name}
</Option>
))}
</Select>
); );
}} }}
/> />
<Column title="类型" dataIndex="fileTypeLabel" key="fileTypeLabel" width={80} /> <Column title="类型" dataIndex="fileTypeLabel" key="fileTypeLabel" width={80} />
<Column title="大小" dataIndex="size" key="size" width={100} /> <Column
title="大小"
dataIndex="size"
key="size"
width={100}
customRender={({ record }) => {
return formatFileSize(record.size);
}}
/>
<Column <Column
title="操作" title="操作"
key="action" key="action"
width={118} width={80}
fixed="right" fixed="right"
align="right"
customRender={({ text, record }) => { customRender={({ text, record }) => {
return ( return (
<div class="flex items-center"> <div class="flex items-center justify-end">
{record.uploadStatus === EnumUploadStatus.uploading && (
<Button type="text" class="!h-22px !p-0 mr-16px">
取消生成
</Button>
)}
<img <img
alt="" alt=""
class="cursor-pointer" class="cursor-pointer"
src={icon1} src={icon1}
width="14" width="14"
height="14" height="14"
onClick={() => handleDelete(record)} onClick={() => openDeleteModal(record)}
/> />
</div> </div>
); );
@ -272,8 +429,8 @@ export default defineComponent({
v-model:open={visible.value} v-model:open={visible.value}
onClose={onClose} onClose={onClose}
> >
<section class="content flex-1 pt-8px px-24px pb-24px"> <section class="content flex-1 pt-8px px-24px pb-24px overflow-hidden flex flex-col">
<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
file-list={uploadData.value} file-list={uploadData.value}
action="/" action="/"
@ -296,16 +453,10 @@ export default defineComponent({
</section> </section>
<footer class="footer h-68px px-24px flex items-center justify-end"> <footer class="footer h-68px px-24px flex items-center justify-end">
<div class="flex items-center"> <div class="flex items-center">
<Button size="large" onClick={onClose} class="mr-12px"> <Button size="large" onClick={onCancel} class="mr-12px">
取消 取消
</Button> </Button>
<Button <Button size="large" type="primary" onClick={onConfirm} loading={submitLoading.value}>
size="large"
type="primary"
onClick={onConfirm}
loading={submitLoading.value}
disabled={!uploadSuccessNum.value}
>
确定 确定
</Button> </Button>
</div> </div>

View File

@ -8,6 +8,12 @@
width: 100%; width: 100%;
} }
.ant-select {
.ant-select-selection-item {
}
}
.ant-drawer-body { .ant-drawer-body {
padding: 0; padding: 0;
display: flex; display: flex;

View File

@ -81,7 +81,7 @@ export const TABLE_COLUMNS = [
{ {
title: '操作', title: '操作',
dataIndex: 'operation', dataIndex: 'operation',
width: 100, width: 120,
fixed: 'right', fixed: 'right',
} }
] ]

View File

@ -184,7 +184,7 @@ export default defineComponent({
</div> </div>
<TagsManageModal ref={tagsManageModalRef} /> <TagsManageModal ref={tagsManageModalRef} />
<AddRawMaterialDrawer ref={addRawMaterialDrawerRef} /> <AddRawMaterialDrawer ref={addRawMaterialDrawerRef} onUpdate={getData} />
<DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} /> <DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} />
</div> </div>
); );