feat: 内容稿件列表、上传内容、通用Textovertips组件封装

This commit is contained in:
rd
2025-07-28 17:55:04 +08:00
parent deea250557
commit 6513fcf2ed
23 changed files with 733 additions and 111 deletions

View File

@ -18,6 +18,7 @@
"axios": "^1.3.0", "axios": "^1.3.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-resize-detector": "^1.2.4",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.1", "jspdf": "^3.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

View File

@ -27,9 +27,11 @@
<script setup> <script setup>
import { useSidebarStore } from '@/stores/modules/side-bar'; import { useSidebarStore } from '@/stores/modules/side-bar';
import router from '@/router'; // import router from '@/router';
import { useRouter } from 'vue-router';
const sidebarStore = useSidebarStore(); const sidebarStore = useSidebarStore();
const router = useRouter();
const selectedKey = computed(() => { const selectedKey = computed(() => {
return [String(sidebarStore.activeMenuId)]; return [String(sidebarStore.activeMenuId)];

View File

@ -0,0 +1,143 @@
<template>
<a-tooltip :disabled="isShowBtn || (!isShowBtn && disabled)" :placement="props.placement">
<template #content>
<div :style="contentStyle" class="tip-content">{{ props.context }}</div>
</template>
<div v-bind="$attrs" class="overflow-hidden">
<div ref="Text" :class="`${isShow ? '' : `line-${props.line}`} `" class="overflow-text">
{{ props.context }}
</div>
<div
v-if="isShowBtn && !disabled"
class="color-#8C8C8C flex items-center cursor-pointer mt-2px"
@click="
() => {
isShow = !isShow;
}
"
>
{{ isShow ? '收起' : '展开' }}
<icon-up size="16" :class="{ active: isShow }" class="ml-2px color-#8C8C8C" />
</div>
</div>
</a-tooltip>
</template>
<script setup>
import { ref, reactive, toRefs, onBeforeMount, onMounted, watchEffect, computed, watch, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import elementResizeDetectorMaker from 'element-resize-detector';
const route = useRoute();
const router = useRouter();
const props = defineProps({
context: {
type: String,
default: '',
},
placement: {
type: String,
default: 'bottom',
},
line: {
type: Number,
default: 1,
},
maxHeight: {
type: [String, Number],
default: '',
},
maxWidth: {
type: [String, Number],
default: '',
},
isShowBtn: {
type: Boolean,
default: false,
},
});
const data = reactive({});
const isShow = ref(false);
const contentStyle = computed(() => {
let style = {
'max-height': props.maxHeight + 'px',
'max-width': props.maxWidth + 'px',
overflow: 'auto',
padding: '1px 0',
};
return props.maxHeight || props.maxWidth ? style : {};
});
const disabled = ref(true);
const Text = ref(null);
const textWidth = ref();
watch(
[() => props.context, textWidth],
async () => {
if (props.context) {
await nextTick();
nextTick(() => {
if (props.line < 2) {
if (Text.value?.clientWidth < Text.value?.scrollWidth) {
disabled.value = false;
} else {
disabled.value = true;
}
} else {
if (Text.value?.clientHeight < Text.value?.scrollHeight) {
disabled.value = false;
} else {
disabled.value = true;
}
}
});
}
},
{
deep: true,
immediate: true,
},
);
onBeforeMount(() => {});
onMounted(async () => {
await nextTick();
const erd = elementResizeDetectorMaker();
if (Text.value) {
erd.listenTo(Text.value, () => {
const _width = Text.value.getBoundingClientRect().width;
textWidth.value = _width;
});
}
});
watchEffect(() => {});
defineExpose({
...toRefs(data),
});
</script>
<style scoped lang="scss">
.overflow-text {
display: inline-block;
font-family: $font-family-regular;
width: 100%;
vertical-align: middle;
font-style: normal;
box-sizing: border-box;
white-space: pre-line;
&.line-1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.line-2 {
@include multi-ellipsis(2);
}
&.line-3 {
@include multi-ellipsis(3);
}
}
.tip-content {
white-space: pre-line;
}
.active {
transform: rotate(-180deg);
}
</style>

View File

@ -6,8 +6,8 @@ import App from './App.vue';
import router from './router'; import router from './router';
import store from './stores'; import store from './stores';
import NoData from '@/components/no-data'; import NoData from '@/components/no-data/index.vue';
import SvgIcon from "@/components/svg-icon"; import SvgIcon from '@/components/svg-icon/index.vue';
import '@/api/index'; import '@/api/index';
import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式 import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式
@ -15,7 +15,7 @@ import './core';
import 'normalize.css'; import 'normalize.css';
import 'uno.css'; import 'uno.css';
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register';
// import '@/styles/vars.css'; // 优先加载 // import '@/styles/vars.css'; // 优先加载
@ -24,6 +24,4 @@ const app = createApp(App);
app.component('NoData', NoData); app.component('NoData', NoData);
app.component('SvgIcon', SvgIcon); app.component('SvgIcon', SvgIcon);
app.use(store); app.use(store).use(router).mount('#app');
app.use(router);
app.mount('#app');

View File

@ -79,7 +79,7 @@ export const useUserStore = defineStore('user', {
getUserAllowAccessRoutes() { getUserAllowAccessRoutes() {
const sidebarStore = useSidebarStore(); const sidebarStore = useSidebarStore();
const menuList = sidebarStore.menuList; const menuList = sidebarStore.menuList;
const appRoutes = router.options?.routes ?? []; const appRoutes = router.getRoutes();
appRoutes.forEach((route: any) => { appRoutes.forEach((route: any) => {
if (!route.meta?.requiresAuth) { if (!route.meta?.requiresAuth) {

View File

@ -1,6 +1,9 @@
.arco-form { .arco-form {
.arco-form-item { .arco-form-item {
margin-bottom: 0;
&:not(:last-child) {
margin-bottom: 16px !important; margin-bottom: 16px !important;
}
.arco-form-item-label-col { .arco-form-item-label-col {
padding-right: 12px !important; padding-right: 12px !important;
.arco-form-item-label { .arco-form-item-label {

View File

@ -23,3 +23,23 @@ p {
a { a {
text-decoration: none; text-decoration: none;
} }
/* 滚动条样式 */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track-piece {
border-radius: 0;
}
::-webkit-scrollbar-thumb {
height: 10px;
background-color: #C9CDD4;
border-radius: 99px;
outline-offset: -2px;
}
::-webkit-scrollbar-button {
display: none;
}

View File

@ -74,3 +74,27 @@
.arco-link { .arco-link {
--color-primary-6: var(--arco-primary-6) !important; --color-primary-6: var(--arco-primary-6) !important;
} }
.common-filter-wrap {
padding: 24px 24px 8px;
.filter-row {
display: flex;
align-items: center;
flex-wrap: wrap;
.filter-row-item {
display: flex;
align-items: center;
margin: 0 24px 16px 0;
.label {
margin-right: 8px;
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px;
}
}
}
}

View File

@ -4,9 +4,9 @@
* @Date: 2025-06-25 14:02:40 * @Date: 2025-06-25 14:02:40
--> -->
<template> <template>
<div class="filter-wrap px-24px pt-12px pb-24px"> <div class="common-filter-wrap">
<div class="filter-row flex"> <div class="filter-row">
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<span class="label">内容稿件标题</span> <span class="label">内容稿件标题</span>
<a-space size="medium"> <a-space size="medium">
<a-input <a-input
@ -23,7 +23,7 @@
</a-input> </a-input>
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
@ -64,7 +64,3 @@ const handleReset = () => {
emits('reset'); emits('reset');
}; };
</script> </script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -1,23 +0,0 @@
.filter-wrap {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="filter-wrap px-24px py-24px"> <div class="common-filter-wrap">
<div class="filter-row flex mb-16px"> <div class="filter-row">
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<span class="label">内容稿件标题</span> <span class="label">内容稿件标题</span>
<a-input <a-input
v-model="query.name" v-model="query.title"
class="!w-240px" class="!w-240px"
placeholder="请输入内容稿件标题" placeholder="请输入内容稿件标题"
size="medium" size="medium"
@ -16,15 +16,15 @@
</template> </template>
</a-input> </a-input>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<span class="label">所属项目</span> <span class="label">所属项目</span>
<CommonSelect placeholder="请选择所属项目" v-model="query.project_id" class="!w-166px" /> <CommonSelect placeholder="请选择所属项目" v-model="query.project_id" class="!w-166px" />
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<span class="label">序号</span> <span class="label">序号</span>
<a-space size="medium"> <a-space size="medium">
<a-input <a-input
v-model="query.number" v-model="query.uid"
class="w-160px" class="w-160px"
placeholder="请输入序号" placeholder="请输入序号"
size="medium" size="medium"
@ -37,14 +37,12 @@
</a-input> </a-input>
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<span class="label">审核状态</span> <span class="label">审核状态</span>
<CommonSelect placeholder="请选择审核状态" v-model="query.status" class="!w-166px" /> <CommonSelect placeholder="请选择审核状态" v-model="query.audit_status" class="!w-166px" />
</div> </div>
</div> <div class="filter-row-item">
<div class="filter-row flex"> <span class="label">上传时间</span>
<div class="filter-row-item flex items-center">
<span class="label">发布日期</span>
<a-range-picker <a-range-picker
v-model="published_at" v-model="published_at"
size="medium" size="medium"
@ -54,7 +52,7 @@
@change="onDateChange" @change="onDateChange"
/> />
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item">
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
@ -84,7 +82,7 @@ const props = defineProps({
const emits = defineEmits('search', 'reset', 'update:query'); const emits = defineEmits('search', 'reset', 'update:query');
const published_at = ref([]); const created_at = ref([]);
const handleSearch = () => { const handleSearch = () => {
emits('update:query', props.query); emits('update:query', props.query);
@ -96,7 +94,7 @@ const handleSearch = () => {
const onDateChange = (value) => { const onDateChange = (value) => {
const [start, end] = value; const [start, end] = value;
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss'; const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
props.query.published_at = [ props.query.created_at = [
dayjs(start).startOf('day').format(FORMAT_DATE), dayjs(start).startOf('day').format(FORMAT_DATE),
dayjs(end).endOf('day').format(FORMAT_DATE), dayjs(end).endOf('day').format(FORMAT_DATE),
]; ];
@ -105,11 +103,7 @@ const onDateChange = (value) => {
}; };
const handleReset = () => { const handleReset = () => {
published_at.value = []; created_at.value = [];
emits('reset'); emits('reset');
}; };
</script> </script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -1,23 +0,0 @@
.filter-wrap {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}

View File

@ -4,6 +4,9 @@ export const TABLE_COLUMNS = [
dataIndex: 'index', dataIndex: 'index',
width: 120, width: 120,
fixed: 'left', fixed: 'left',
sortable: {
sortDirections: ['ascend', 'descend'],
},
}, },
{ {
title: '图片/视频', title: '图片/视频',
@ -12,12 +15,12 @@ export const TABLE_COLUMNS = [
}, },
{ {
title: '内容稿件标题', title: '内容稿件标题',
dataIndex: 'name', dataIndex: 'manuscript_name',
width: 240, width: 240,
}, },
{ {
title: '所属项目', title: '所属项目',
dataIndex: 'name', dataIndex: 'project_name',
width: 240, width: 240,
}, },
{ {
@ -27,7 +30,7 @@ export const TABLE_COLUMNS = [
}, },
{ {
title: '审核状态', title: '审核状态',
dataIndex: 'media_account_count', dataIndex: 'status',
width: 180, width: 180,
}, },
{ {

View File

@ -11,7 +11,7 @@
@sorter-change="handleSorterChange" @sorter-change="handleSorterChange"
> >
<template #empty> <template #empty>
<NoData text="暂无稿件"/> <NoData text="暂无稿件" />
</template> </template>
<template #columns> <template #columns>
<a-table-column <a-table-column
@ -38,14 +38,43 @@
<template v-if="column.dataIndex === 'create_at'" #cell="{ record }"> <template v-if="column.dataIndex === 'create_at'" #cell="{ record }">
{{ exactFormatTime(record.create_at) }} {{ exactFormatTime(record.create_at) }}
</template> </template>
<template v-else-if="column.dataIndex === 'manuscript_name'" #cell="{ record }">
<TextOverTips :context="record.name" :line="3" class="manuscript_name" />
</template>
<template v-else-if="column.dataIndex === 'project_name'" #cell="{ record }">
<TextOverTips :context="record.project_name" :line="3" />
</template>
<template v-else-if="column.dataIndex === 'status'" #cell="{ record }">
<div
class="flex items-center w-fit h-28px px-8px rounded-2px"
:style="{ backgroundColor: getStatusInfo(record.status).backgroundColor }"
>
<span class="cts s1" :style="{ color: getStatusInfo(record.status).color }">{{
getStatusInfo(record.status).label
}}</span>
</div>
</template>
<template v-else-if="column.dataIndex === 'budget'" #cell="{ record }">
<div class="flex items-center">
<img :src="record.budget === 1 ? icon2 : icon3" width="16" height="16" class="mr-4px" />
<span class="cts" :class="record.budget === 1 ? '!color-#25C883' : '!color-#6D4CFE'">{{
record.budget === 1 ? '图文' : '视频'
}}</span>
</div>
</template>
<template v-else-if="['create_at', 'last_create_at'].includes(column.dataIndex)" #cell="{ record }">
{{ exactFormatTime(record[column.dataIndex]) }}
</template>
<template v-else-if="column.dataIndex === 'picker'" #cell="{ record }">
<a-image :width="64" :height="64" :src="record.picker" class="rounded-6px" />
</template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }"> <template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" /> <img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
<a-button type="outline" size="mini" @click="onEdit(record)">编辑</a-button> <a-button type="outline" size="mini" class="mr-8px" @click="onEdit(record)">编辑</a-button>
<a-button type="outline" size="mini" @click="onDetail(record)">详情</a-button> <a-button type="outline" size="mini" @click="onDetail(record)">详情</a-button>
</div> </div>
</template> </template>
<template v-else #cell="{ record }"> <template v-else #cell="{ record }">
{{ formatTableField(column, record, true) }} {{ formatTableField(column, record, true) }}
</template> </template>
@ -58,8 +87,13 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { formatTableField, exactFormatTime } from '@/utils/tools'; import { formatTableField, exactFormatTime } from '@/utils/tools';
import { TABLE_COLUMNS } from './constants'; import { TABLE_COLUMNS } from './constants';
import { CHECK_STATUS } from '@/views/creative-generation-workshop/manuscript/manuscript-list/constants';
import TextOverTips from '@/components/text-over-tips';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
const emits = defineEmits(['edit', 'sorterChange', 'delete']); const emits = defineEmits(['edit', 'sorterChange', 'delete']);
@ -82,7 +116,11 @@ const onEdit = (item) => {
emits('edit', item); emits('edit', item);
}; };
const onDetail = (item) => { const onDetail = (item) => {
console.log('onDetail') console.log('onDetail');
};
const getStatusInfo = (status) => {
return CHECK_STATUS.find((v) => v.value === status) ?? {};
}; };
</script> </script>

View File

@ -7,4 +7,10 @@
font-weight: 400; font-weight: 400;
line-height: 22px; line-height: 22px;
} }
:deep(.manuscript_name) {
cursor: pointer;
&:hover {
color: #6d4cfe;
}
}
} }

View File

@ -0,0 +1,320 @@
<script lang="jsx">
import {
Modal,
Form,
FormItem,
Input,
RadioGroup,
Radio,
Upload,
Button,
Tooltip,
Message as AMessage,
Textarea,
} from '@arco-design/web-vue';
import { useClipboard } from '@vueuse/core';
import TextOverTips from '@/components/text-over-tips';
import icon1 from '@/assets/img/media-account/icon-feedback-fail.png';
const INITIAL_FORM = {
linkUrl: '',
link: '',
};
const FORM_TASK_STATUS = {
default: 1, // 默认状态,
importLoading: 2, // 上传中
importFailed: 3, // 上传失败
importSuccess: 4, // 上传成功
};
export default {
setup(props, { emit, expose }) {
const update = inject('update');
const visible = ref(false);
const formRef = ref(null);
const uploadRef = ref(null);
const uploadType = ref('link');
const taskStatus = ref(FORM_TASK_STATUS.default);
const form = ref(cloneDeep(INITIAL_FORM));
const { copy, copied, isSupported } = useClipboard({ source: form.value.link });
const isLink = computed(() => uploadType.value === 'link'); // 链接上传
const isLocal = computed(() => uploadType.value === 'local'); // 本地上传
const isHandwrite = computed(() => uploadType.value === 'handwrite'); // 手写上传
const isDefaultTask = computed(() => taskStatus.value === FORM_TASK_STATUS.default);
const isImportLoadingTask = computed(() => taskStatus.value === FORM_TASK_STATUS.importLoading);
const isImportFailedTask = computed(() => taskStatus.value === FORM_TASK_STATUS.importFailed);
const isImportSuccessTask = computed(() => taskStatus.value === FORM_TASK_STATUS.importSuccess);
const open = () => {
visible.value = true;
};
const reset = () => {
formRef.value?.resetFields?.();
uploadType.value = 'link';
taskStatus.value = FORM_TASK_STATUS.default;
form.value = cloneDeep(INITIAL_FORM);
};
const onClose = () => {
reset();
visible.value = false;
};
const onSubmit = async () => {
if (isHandwrite.value) {
if (!isSupported) {
AMessage.error('您的浏览器不支持复制,请手动复制!');
}
copy(form.value.link);
if (!copied) {
AMessage.error('复制失败,请手动复制!');
}
AMessage.success('复制成功!');
onClose();
return;
}
taskStatus.value = FORM_TASK_STATUS.importLoading;
setTimeout(() => {
taskStatus.value = FORM_TASK_STATUS.importSuccess;
}, 2000);
};
const onCancelUpload = () => {
console.log('onCancelUpload');
taskStatus.value = FORM_TASK_STATUS.default;
// onClose()
};
const handleUpload = async (file) => {
console.log('handleUpload11111', file);
taskStatus.value = FORM_TASK_STATUS.importLoading;
setTimeout(() => {
taskStatus.value = FORM_TASK_STATUS.importSuccess;
}, 2000);
};
const goEdit = () => {
console.log('goEdit 去稿件编辑页面');
};
const onDelete = (item) => {
console.log('onDelete', item);
};
const handleDownloadTemplate = async () => {
// const { code, data } = await getPlacementAccountsTemplateUrl();
// if (code === 200) {
// window.open(data.download_url, '_blank');
// }
};
const getLink = () => {
return (
<FormItem label="链接地址">
<Textarea
v-model={form.value.linkUrl}
size="large"
placeholder="请输入..."
autoSize={{ minRows: 5, maxRows: 8 }}
/>
</FormItem>
);
};
const getHandwrite = () => {
return (
<FormItem label="上传链接">
<Input v-model={form.value.link} placeholder="请输入..." size="large" />
</FormItem>
);
};
const getLocal = () => {
return (
<FormItem label="内容稿件">
<div class="flex flex-col w-full">
<Upload
ref="uploadRef"
action="/"
draggable
customRequest={handleUpload}
accept=".xlsx,.xls,.docx,.doc"
show-file-list={false}
>
{{
'upload-button': () => (
<div class="upload-box">
<icon-plus size="14" class="mb-16px" />
<span class="text mb-4px">点击或拖拽文件到此处上传</span>
<span class="tip">支持文档文本+, 视频批量上传</span>
</div>
),
}}
</Upload>
<div class="flex items-center cursor-pointer mt-8px" onClick={handleDownloadTemplate}>
<icon-download size="14" class="mr-4px !color-#6D4CFE" />
<span class="cts color-#6D4CFE">下载示例文档</span>
</div>
</div>
</FormItem>
);
};
const renderFormContent = () => {
if (isDefaultTask.value) {
if (isLink.value) {
return getLink();
}
if (isHandwrite.value) {
return getHandwrite();
}
if (isLocal.value) {
return getLocal();
}
}
if (isImportLoadingTask.value) {
return (
<div class="flex flex-col items-center justify-center rounded-8px bg-#F7F8FA h-208px">
<icon-loading size="48" class="!color-#6D4CFE mb-16px" />
<p class="tip !text-#768893">上传过程耗时可能较长请耐心等待</p>
<p class="tip !text-#768893">刷新页面将会终止本次数据的上传请谨慎操作</p>
</div>
);
}
if (isImportFailedTask.value) {
return (
<div class="flex flex-col items-center justify-center rounded-8px bg-#F7F8FA h-208px">
<img src={icon1} class="w-80px h-80px mb-16px" />
<p class="text mb-4px">上传失败</p>
<p class="tip !text-#768893">可能是网络不稳定导致请检查网络后重试</p>
</div>
);
}
if (isImportSuccessTask.value) {
return (
<div class="flex flex-col py-12px max-h-540px rounded-8px bg-#F7F8FA">
<span class="tip mb-8px px-12px fs-14px !text-left"> 30 个内容稿件</span>
<div class="flex-1 overflow-y-auto overflow-x-hidden px-12px">
{new Array(20).fill(0).map((v) => (
<div class="rounded-8px bg-#fff px-8px py-8px flex justify-between items-center mt-8px">
<div class="flex-1 overflow-hidden flex items-center mr-12px">
<img width="32" height="32" class="rounded-3px mr-8px" />
<TextOverTips
class="text"
context="挖到宝了!这个平价好物让我素颜出门都自信✨挖到宝了!这个平价好物让我素颜出门都自信✨"
/>
</div>
<icon-delete
size="16px"
class="color-#737478 cursor-pointer hover:!color-#211F24"
onClick={onDelete}
/>
</div>
))}
</div>
</div>
);
}
};
const renderFooterBtn = () => {
if (isImportLoadingTask.value) {
return (
<Button type="primary" size="medium" onClick={onCancelUpload}>
取消上传
</Button>
);
}
const getConfirmBtn = () => {
if (isDefaultTask.value) {
return (
<Button type="primary" size="medium" onClick={onSubmit}>
{isHandwrite.value ? '复制链接' : '确认'}
</Button>
);
}
if (isImportFailedTask.value) {
return (
<Button type="primary" size="medium" onClick={onClose}>
重新上传
</Button>
);
}
if (isImportSuccessTask.value) {
return (
<Button type="primary" size="medium" onClick={goEdit}>
确认
</Button>
);
}
};
return (
<>
<Button size="medium" onClick={onClose}>
取消
</Button>
{getConfirmBtn()}
</>
);
};
const getTitle = () => {
if (isDefaultTask.value) {
return '上传内容稿件';
} else if (isImportSuccessTask.value) {
return '上传内容稿件列表';
} else {
if (isLink.value) {
return '链接上传';
}
if (isLocal.value) {
return '本地批量上传';
}
}
};
expose({ open });
return () => (
<Modal
v-model:visible={visible.value}
title={getTitle()}
modal-class="upload-manuscript-modal"
width="500px"
mask-closable={false}
unmount-on-close
onClose={onClose}
footer={!(isDefaultTask.value && isLocal.value)}
v-slots={{
footer: () => renderFooterBtn(),
}}
>
<Form ref="formRef" model={form.value} layout="horizontal" auto-label-width>
{isDefaultTask.value && (
<FormItem label="上传方式">
<RadioGroup v-model={uploadType.value}>
<Radio value="link">链接上传</Radio>
<Radio value="local">本地上传</Radio>
<Radio value="handwrite">手写上传</Radio>
</RadioGroup>
</FormItem>
)}
{renderFormContent()}
</Form>
</Modal>
);
},
};
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,31 @@
.upload-manuscript-modal {
.text {
color: var(--Text-1, #211f24);
text-align: center;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.tip {
color: var(--Text-3, #737478);
text-align: center;
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.upload-box {
display: flex;
height: 120px;
padding: 0 16px;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 2px;
border: 1px dashed var(--Border-1, #d7d7d9);
background: var(--BG-200, #f2f3f5);
}
}

View File

@ -1,9 +1,60 @@
export const INITIAL_QUERY = { export const INITIAL_QUERY = {
name: '', title: '',
project_id: '', project_ids: [],
number: '', uid: '',
status: '', audit_status: '',
published_at: [], created_at: [],
sort_column: undefined, sort_column: undefined,
sort_order: undefined, sort_order: undefined,
}; };
export enum EnumCheckStatus {
All = '',
Wait = 0,
Checking = 1,
Passed = 2,
}
export enum EnumManuscriptType {
All = '',
Image = 1,
Video = 2,
}
export const CHECK_STATUS = [
{
label: '全部',
value: EnumCheckStatus.All,
},
{
label: '待审核',
value: EnumCheckStatus.Wait,
backgroundColor: '#F2F3F5',
color: '#3C4043'
},
{
label: '审核中',
value: EnumCheckStatus.Checking,
backgroundColor: '#FFF7E5',
color: '#FFAE00'
},
{
label: '已通过',
value: EnumCheckStatus.Passed,
backgroundColor: '#EBF7F2',
color: '#25C883'
},
];
export const MANUSCRIPT_TYPE = [
{
label: '全部',
value: EnumManuscriptType.All,
},
{
label: '图片',
value: EnumManuscriptType.Image,
},
{
label: '视频',
value: EnumManuscriptType.Video,
},
]

View File

@ -4,8 +4,10 @@
<div class="top flex h-64px px-24px py-10px justify-between items-center"> <div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">内容稿件列表</p> <p class="text-18px font-400 lh-26px color-#211F24 title">内容稿件列表</p>
<div class="flex items-center"> <div class="flex items-center">
<a-button type="outline" size="medium" class="mr-12px" @click="handleOpenAddProjectModal"> 分享内容稿件 </a-button> <a-button type="outline" size="medium" class="mr-12px" @click="handleOpenAddProjectModal">
<a-button type="primary" size="medium" @click="handleOpenAddProjectModal"> 分享内容稿件
</a-button>
<a-button type="primary" size="medium" @click="openUploadModal">
<template #icon> <template #icon>
<icon-plus size="16" /> <icon-plus size="16" />
</template> </template>
@ -40,6 +42,7 @@
</div> </div>
<DeleteManuscriptModal ref="deleteManuscriptModalRef" /> <DeleteManuscriptModal ref="deleteManuscriptModalRef" />
<UploadManuscriptModal ref="uploadManuscriptModalRef" />
</div> </div>
</template> </template>
<script lang="jsx" setup> <script lang="jsx" setup>
@ -48,10 +51,14 @@ import { Button } from '@arco-design/web-vue';
import FilterBlock from './components/filter-block'; import FilterBlock from './components/filter-block';
import ManuscriptTable from './components/manuscript-table'; import ManuscriptTable from './components/manuscript-table';
import DeleteManuscriptModal from './components/manuscript-table/delete-manuscript-modal.vue'; import DeleteManuscriptModal from './components/manuscript-table/delete-manuscript-modal.vue';
import UploadManuscriptModal from './components/upload-manascript-modal';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination'; import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { getProjects } from '@/api/all/propertyMarketing'; import { getProjects } from '@/api/all/propertyMarketing';
import { INITIAL_QUERY } from './constants'; import {
INITIAL_QUERY,
EnumCheckStatus,
} from '@/views/creative-generation-workshop/manuscript/manuscript-list/constants.ts';
const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } = useTableSelectionWithPagination({ const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } = useTableSelectionWithPagination({
onPageChange: () => { onPageChange: () => {
@ -64,18 +71,46 @@ const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } =
const query = ref(cloneDeep(INITIAL_QUERY)); const query = ref(cloneDeep(INITIAL_QUERY));
const addManuscriptModalRef = ref(null); const addManuscriptModalRef = ref(null);
const deleteManuscriptModalRef = ref(null); const deleteManuscriptModalRef = ref(null);
const uploadManuscriptModalRef = ref(null);
const getData = async () => { const getData = async () => {
const { page, page_size } = pageInfo.value; dataSource.value = [
const { code, data } = await getProjects({ {
...query.value, index: 1,
page, name: '内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1',
page_size, project_name: '内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1',
}); status: EnumCheckStatus.Wait,
if (code === 200) { budget: 1,
dataSource.value = data?.data ?? []; create_at: 1753682671,
pageInfo.value.total = data.total; },
} {
index: 2,
name: '内容稿件2',
project_name: '内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1',
status: EnumCheckStatus.Checking,
budget: 2,
create_at: 1753682672,
},
{
index: 3,
name: '内容稿件3',
project_name: '内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1内容稿件1',
status: EnumCheckStatus.Passed,
budget: 1,
create_at: 1753682673,
},
];
pageInfo.value.total = 3;
// const { page, page_size } = pageInfo.value;
// const { code, data } = await getProjects({
// ...query.value,
// page,
// page_size,
// });
// if (code === 200) {
// dataSource.value = data?.data ?? [];
// pageInfo.value.total = data.total;
// }
}; };
const handleSearch = () => { const handleSearch = () => {
reload(); reload();
@ -98,6 +133,9 @@ const handleSorterChange = (column, order) => {
const handleOpenAddProjectModal = () => { const handleOpenAddProjectModal = () => {
console.log('handleOpenAddProjectModal'); console.log('handleOpenAddProjectModal');
}; };
const openUploadModal = () => {
uploadManuscriptModalRef.value?.open();
};
const handleDelete = (item) => { const handleDelete = (item) => {
const { id, name } = item; const { id, name } = item;
@ -114,5 +152,5 @@ provide('update', getData);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "./style.scss" @import './style.scss';
</style> </style>

View File

@ -308,7 +308,7 @@ const handleBatchImport = async () => {
if (code === 200) { if (code === 200) {
const id = genRandomId(); const id = genRandomId();
showExportNotification(`正在导入“${file.value.name}”,请稍后...`, { id }); showExportNotification(`正在导入“${file.value.name}”,请稍后...`, { id });
emit('startQueryTaskStatus', data.id, id); emits('startQueryTaskStatus', data.id, id);
onClose(); onClose();
} else { } else {
uploadStatus.value = UploadStatus.ERROR; uploadStatus.value = UploadStatus.ERROR;