feat: 添加原料库上传功能和相关组件

- 在 `raw-material` 组件中添加上传按钮和抽屉组件
- 新增 `add-raw-material-drawer` 组件及其样式文件
- 更新样式文件以支持新的上传界面
- 调整 Ant Select 和 Drawer 组件的样式
This commit is contained in:
rd
2025-09-16 17:03:06 +08:00
parent f1348469f3
commit c357e4a885
9 changed files with 407 additions and 24 deletions

View File

@ -0,0 +1,20 @@
.ant-drawer {
.ant-drawer-header {
.ant-drawer-header-title {
justify-content: space-between;
.ant-drawer-close {
order: 2;
}
.ant-drawer-title {
color: #211f24;
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px;
}
}
}
}

View File

@ -36,7 +36,6 @@
border-color: $color-error !important; border-color: $color-error !important;
} }
} }
.ant-select { .ant-select {
@ -103,20 +102,69 @@
} }
} }
&.ant-select-single {
.ant-select-selector {
height: 32px !important;
}
&.ant-select-lg {
.ant-select-selector {
height: 36px !important;
}
}
&.ant-select-sm {
.ant-select-selector {
height: 28px !important;
}
}
}
} }
.ant-select.ant-select-single { .ant-select-dropdown {
.ant-select-selector { padding: 4px 0;
height: 32px !important;
} .ant-select-item-option {
&.ant-select-lg { padding: 0 12px;
.ant-select-selector { height: 36px;
height: 36px !important; align-items: center;
.ant-select-item-option-state {
display: none;
} }
}
&.ant-select-sm { &:not(.ant-select-item-option-disabled) {
.ant-select-selector { &:hover {
height: 28px !important; background-color: #F2F3F5 !important;
}
.ant-select-item-option-content {
color: #211F24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
&.ant-select-item-option-selected {
background-color: transparent !important;
color: #6D4CFE !important;
&:hover {
background-color: #F2F3F5 !important;
}
.ant-select-item-option-content {
color: #6D4CFE;
font-weight: 500;
font-family: $font-family-medium;
}
}
&.ant-select-item-option-active {
}
} }
} }
} }

View File

@ -2,4 +2,18 @@
.ant-input { .ant-input {
padding: 8px 12px 4px 12px; padding: 8px 12px 4px 12px;
} }
&.ant-input-textarea-show-count {
&::after {
position: absolute;
right: 8px;
bottom: 4px;
font-family: $font-family-regular;
color: #939499;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
} }

View File

@ -27,3 +27,4 @@
@import "./ant-switch.scss"; @import "./ant-switch.scss";
@import "./ant-step.scss"; @import "./ant-step.scss";
@import "./ant-spin.scss"; @import "./ant-spin.scss";
@import "./ant-drawer.scss";

View File

@ -4,6 +4,7 @@
*/ */
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export function toFixed(num: number | string, n: number): number { export function toFixed(num: number | string, n: number): number {
return parseFloat(parseFloat(num.toString()).toFixed(n)); return parseFloat(parseFloat(num.toString()).toFixed(n));
} }

View File

@ -43,12 +43,12 @@
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template> </template>
</Input.Password> </Input.Password>
<p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg"> <p v-show="errMsg" class="color-#F64B31 h-20px text-12px font-400 lh-20px font-family-regular">
{{ errMsg }} {{ errMsg }}
</p> </p>
</FormItem> </FormItem>
<FormItem class="mt-52px"> <FormItem class="mt-32px">
<div class="text-12px flex justify-center items-center mb-16px"> <div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox> <Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular" <span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
@ -67,14 +67,18 @@
登录 登录
</Button> </Button>
<div class="flex justify-between btn-row"> <div class="flex justify-between btn-row">
<Button <div>
type="text" <Button
class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE" v-show="!isCaptchaLogin"
size="small" class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"
@click="onForgetPassword" size="small"
> type="text"
忘记密码 @click="onForgetPassword"
</Button> >
忘记密码
</Button>
</div>
<Button <Button
type="text" type="text"
class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE" class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"

View File

@ -0,0 +1,244 @@
<script lang="tsx">
import { Drawer, Button, Upload, Table, Input } from 'ant-design-vue';
const { Column } = Table;
const { TextArea } = Input;
import CommonSelect from '@/components/common-select';
import ImgLazyLoad from '@/components/img-lazy-load';
import { formatFileSize, getVideoInfo, formatDuration, formatUploadSpeed } from '@/utils/tools';
import { getRawMaterialTagsList } from '@/api/all/generationWorkshop';
import icon1 from '@/assets/img/media-account/icon-delete.png';
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.wmv', '.m4v'];
const documentExtensions = ['.txt', '.doc', '.docx', '.pdf', '.xls', '.xlsx'];
export default defineComponent({
setup(_, { attrs, slots, expose }) {
const visible = ref(false);
const submitLoading = ref(false);
const uploadData = ref([]);
const tagData = ref([]);
const checkSuccessNum = computed(() => {
return uploadData.value.filter((item) => item.status === 'success').length;
});
const getTagData = async () => {
const { code, data } = await getRawMaterialTagsList();
if (code === 200) {
tagData.value = data ?? [];
}
};
const open = () => {
getTagData();
visible.value = true;
};
const onClose = () => {
visible.value = false;
uploadData.value = [];
tagData.value = [];
submitLoading.value = false;
};
const handleUpload = async (option) => {
// console.log('handleUpload', option);
};
const getFileType = (fileName) => {
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
if (imageExtensions.includes(ext)) {
return '图片';
} else if (videoExtensions.includes(ext)) {
return '视频';
} else if (documentExtensions.includes(ext)) {
return '文档';
} else {
return '其他';
}
};
const onConfirm = () => {
console.log('onConfirm');
};
const handleDelete = (file) => {
uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid);
};
const renderUploadList = (file, actions) => {
if (!uploadData.value.length) return null;
console.log('renderUploadList', uploadData.value);
return (
<>
<p class="cts !color-#939499 mb-10px">
已上传<span class="!color-#211F24">{`${checkSuccessNum.value}/${uploadData.value.length}`}</span>
</p>
<Table ref="tableRef" dataSource={uploadData.value} pagination={false} class="manuscript-table w-100% flex-1">
<Column
title="文件名称"
dataIndex="name"
key="name"
width={347}
ellipsis={true}
customRender={({ text, record }) => {
return (
<div class="flex items-center">
<ImgLazyLoad width={64} height={64} src={record.cover} class="!rounded-6px mr-16px flex-shrink-0" />
<TextArea
v-model:value={record.name}
placeholder="请输入文件名称"
size="large"
class="w-full !h-72px"
showCount
maxlength={20}
/>
</div>
);
}}
/>
<Column
title="上传状态"
dataIndex="status"
key="status"
width={164}
customRender={({ text, 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
title="标签"
dataIndex="tags"
key="tags"
width={243}
customRender={({ text, record }) => {
return (
<CommonSelect
v-model={record.tags}
class="w-full"
multiple
options={tagData}
placeholder="请选择标签"
/>
);
}}
/>
<Column
title="类型"
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
title="操作"
key="action"
width={118}
fixed="right"
customRender={({ text, record }) => {
return (
<div class="flex items-center">
<Button type="text" class="!h-22px !p-0 mr-16px">
取消生成
</Button>
<img
class="cursor-pointer"
src={icon1}
width="14"
height="14"
onClick={() => handleDelete(record)}
/>
</div>
);
}}
/>
</Table>
</>
);
};
expose({
open,
});
return () => (
<Drawer
width={1100}
title="上传到原料库"
rootClassName="xt-add-raw-material-modal"
v-model:open={visible.value}
onClose={onClose}
>
<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">
<Upload
v-model:file-list={uploadData.value}
action="/"
multiple
customRequest={handleUpload}
class="w-full mb-16px"
accept={[...imageExtensions, ...videoExtensions, ...documentExtensions].join(',')}
showUploadList={false}
>
<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">
<icon-plus size="14" class="mb-10px color-#55585F" />
<span class="cts">点击或拖拽文件到此处上传</span>
</div>
</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">{`图片格式PNG、JPG、JPEG、GIF、WEBP、BMP大小<20MB`}</p>
<p class="cts !color-#939499 !text-12px !lh-20px">{`文本格式TXT、DOC、DOCX、PDF大小<20MB`}</p>
</div>
{renderUploadList()}
</section>
<footer class="footer h-68px px-24px flex items-center justify-end">
<div class="flex items-center">
<Button size="large" onClick={onClose} class="mr-12px">
取消
</Button>
<Button
size="large"
type="primary"
onClick={onConfirm}
loading={submitLoading.value}
disabled={!checkSuccessNum.value}
>
确定
</Button>
</div>
</footer>
</Drawer>
);
},
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,48 @@
.xt-add-raw-material-modal {
.ant-drawer-header {
height: 58px;
border-bottom: none;
}
.ant-upload {
width: 100%;
}
.ant-drawer-body {
padding: 0;
display: flex;
flex-direction: column;
.cts {
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.content {
.upload-box {
transition: all 0.3s;
&:hover {
border-color: #6d4cfe;
}
}
.upload-text {
color: #000;
text-align: center;
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.footer {
}
}
}

View File

@ -7,6 +7,7 @@ import FilterBlock from './components/filter-block/index.vue';
import RawMaterialTable from './components/table/index.vue'; import RawMaterialTable from './components/table/index.vue';
import DeleteRawMaterialModal from './components/table/delete-file-modal.vue'; import DeleteRawMaterialModal from './components/table/delete-file-modal.vue';
import TagsManageModal from './components/tags-manage-modal'; import TagsManageModal from './components/tags-manage-modal';
import AddRawMaterialDrawer from './components/add-raw-material-drawer';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination'; import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { getRawMaterialsPage } from '@/api/all/generationWorkshop'; import { getRawMaterialsPage } from '@/api/all/generationWorkshop';
@ -32,6 +33,7 @@ export default defineComponent({
const deleteRawMaterialModalRef = ref(null); const deleteRawMaterialModalRef = ref(null);
const tagsManageModalRef = ref(null); const tagsManageModalRef = ref(null);
const addRawMaterialDrawerRef = ref(null);
const query = ref(cloneDeep(INITIAL_QUERY)); const query = ref(cloneDeep(INITIAL_QUERY));
const handleSearch = () => { const handleSearch = () => {
@ -91,7 +93,7 @@ export default defineComponent({
tagsManageModalRef.value?.open(); tagsManageModalRef.value?.open();
}; };
const handleAddMaterial = () => { const handleAddMaterial = () => {
console.log('handleAddMaterial'); addRawMaterialDrawerRef.value?.open();
}; };
onMounted(() => { onMounted(() => {
@ -182,6 +184,7 @@ export default defineComponent({
</div> </div>
<TagsManageModal ref={tagsManageModalRef} /> <TagsManageModal ref={tagsManageModalRef} />
<AddRawMaterialDrawer ref={addRawMaterialDrawerRef} />
<DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} /> <DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} />
</div> </div>
); );