feat: 素材中心成品库-写手端处理
This commit is contained in:
@ -0,0 +1,26 @@
|
||||
export enum AuditStatus {
|
||||
All = '0',
|
||||
Pending = '1',
|
||||
Auditing = '2',
|
||||
Passed = '3',
|
||||
}
|
||||
|
||||
export const TABS_LIST = [
|
||||
{
|
||||
label: '全部',
|
||||
value: AuditStatus.All,
|
||||
},
|
||||
{
|
||||
label: '待审核',
|
||||
value: AuditStatus.Pending,
|
||||
},
|
||||
{
|
||||
label: '审核中',
|
||||
value: AuditStatus.Auditing,
|
||||
},
|
||||
{
|
||||
label: '已通过',
|
||||
value: AuditStatus.Passed,
|
||||
},
|
||||
];
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
<script lang="tsx">
|
||||
import { Tabs, TabPane } from 'ant-design-vue';
|
||||
import ManuscriptList from './manuscript/list/index.vue';
|
||||
import ManuscriptCheckList from './manuscript/check-list/index.vue';
|
||||
import UploadManuscriptModal from '@/views/writer-material-center/components/finished-products/manuscript/components/upload-manuscript-modal/index.vue';
|
||||
|
||||
import { TABS_LIST, AuditStatus } from './constants';
|
||||
|
||||
export default defineComponent({
|
||||
setup(_, { attrs, slots, expose }) {
|
||||
const uploadManuscriptModalRef = ref(null);
|
||||
|
||||
const audit_status = ref(AuditStatus.All);
|
||||
|
||||
const showManuscriptList = computed(() => {
|
||||
return audit_status.value === AuditStatus.All;
|
||||
});
|
||||
|
||||
const openUploadModal = () => {
|
||||
uploadManuscriptModalRef.value?.open();
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div class="finished-products-wrap h-full flex flex-col">
|
||||
<div class="bg-white rounded-t-8px">
|
||||
<Tabs
|
||||
v-model:activeKey={audit_status.value}
|
||||
v-slots={{
|
||||
rightExtra: () => (
|
||||
<div class="flex items-center">
|
||||
{showManuscriptList.value && (
|
||||
<a-button
|
||||
type="primary"
|
||||
size="medium"
|
||||
class="ml-12px"
|
||||
onClick={openUploadModal}
|
||||
v-slots={{
|
||||
icon: () => <icon-plus size="16" />,
|
||||
default: () => '上传内容稿件',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{TABS_LIST.map((item) => (
|
||||
<TabPane key={item.value} tab={item.label}>
|
||||
{item.label}
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
{showManuscriptList.value ? <ManuscriptList /> : <ManuscriptCheckList audit_status={audit_status.value} />}
|
||||
|
||||
<UploadManuscriptModal ref={uploadManuscriptModalRef} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,142 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<!--
|
||||
* @Author: RenXiaoDong
|
||||
* @Date: 2025-06-25 14:02:40
|
||||
-->
|
||||
<template>
|
||||
<div class="common-filter-wrap">
|
||||
<div class="filter-row">
|
||||
<div class="filter-row-item">
|
||||
<span class="label">内容稿件标题</span>
|
||||
<a-space size="medium">
|
||||
<a-input
|
||||
v-model="query.title"
|
||||
class="!w-240px"
|
||||
placeholder="请输入内容稿件标题"
|
||||
size="medium"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="filter-row-item">
|
||||
<span class="label">序号</span>
|
||||
<a-space size="medium">
|
||||
<a-input
|
||||
v-model="query.uid"
|
||||
class="!w-160px"
|
||||
placeholder="请输入序号"
|
||||
size="medium"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="filter-row-item" v-if="query.audit_status === AuditStatus.Pending">
|
||||
<span class="label">上传时间</span>
|
||||
<a-range-picker
|
||||
v-model="created_at"
|
||||
size="medium"
|
||||
allow-clear
|
||||
format="YYYY-MM-DD"
|
||||
class="!w-280px"
|
||||
@change="(value) => onDateChange(value, 'created_at')"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="[AuditStatus.Auditing, AuditStatus.Passed].includes(query.audit_status)">
|
||||
<div class="filter-row-item">
|
||||
<span class="label">审核平台</span>
|
||||
<a-select
|
||||
v-model="query.audit_platform"
|
||||
size="medium"
|
||||
placeholder="全部"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
class="!w-160px"
|
||||
>
|
||||
<a-option v-for="(item, index) in PLATFORMS" :key="index" :value="item.value" :label="item.label">{{
|
||||
item.label
|
||||
}}</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="filter-row-item">
|
||||
<span class="label">审核时间</span>
|
||||
<a-range-picker
|
||||
v-model="audit_started_at"
|
||||
size="medium"
|
||||
allow-clear
|
||||
format="YYYY-MM-DD"
|
||||
class="!w-280px"
|
||||
@change="(value) => onDateChange(value, 'audit_started_at')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-row-item">
|
||||
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
<template #default>搜索</template>
|
||||
</a-button>
|
||||
<a-button size="medium" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, defineProps } from 'vue';
|
||||
import { PLATFORMS } from '@/views/writer-material-center/components/finished-products/manuscript/check-list/constants';
|
||||
import { AuditStatus } from '@/views/writer-material-center/components/finished-products/constants';
|
||||
|
||||
const props = defineProps({
|
||||
query: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits('search', 'reset', 'update:query');
|
||||
const created_at = ref([]);
|
||||
const audit_started_at = ref([]);
|
||||
|
||||
const handleSearch = () => {
|
||||
emits('update:query', props.query);
|
||||
nextTick(() => {
|
||||
emits('search');
|
||||
});
|
||||
};
|
||||
|
||||
const onDateChange = (value, type) => {
|
||||
if (!value) {
|
||||
props.query[type] = [];
|
||||
handleSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const [start, end] = value;
|
||||
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
|
||||
props.query[type] = [dayjs(start).startOf('day').format(FORMAT_DATE), dayjs(end).endOf('day').format(FORMAT_DATE)];
|
||||
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
created_at.value = [];
|
||||
emits('reset');
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="删除内容稿件" width="480px" @close="onClose">
|
||||
<div class="flex items-center">
|
||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
||||
<span>确认删除 {{ projectName }} 这个内容稿件吗?</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="medium" @click="onClose">取消</a-button>
|
||||
<a-button type="primary" class="ml-16px" status="danger" size="medium" @click="onDelete">确认删除</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { deleteWorkWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
|
||||
const update = inject('update');
|
||||
const route = useRoute();
|
||||
|
||||
const visible = ref(false);
|
||||
const projectId = ref(null);
|
||||
const projectName = ref('');
|
||||
|
||||
const isBatch = computed(() => Array.isArray(projectId.value));
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
projectId.value = null;
|
||||
projectName.value = '';
|
||||
}
|
||||
|
||||
const open = (record) => {
|
||||
const { id = null, name = '' } = record;
|
||||
projectId.value = id;
|
||||
projectName.value = name;
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
async function onDelete() {
|
||||
const { code } = await deleteWorkWriter(writerCode.value, projectId.value);
|
||||
if (code === 200) {
|
||||
AMessage.success('删除成功');
|
||||
update();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<a-table
|
||||
ref="tableRef"
|
||||
:data="dataSource"
|
||||
row-key="id"
|
||||
column-resizable
|
||||
:pagination="false"
|
||||
:scroll="{ x: '100%' }"
|
||||
class="flex-1 manuscript-table w-100%"
|
||||
bordered
|
||||
:row-selection="rowSelection"
|
||||
:selected-row-keys="selectedRowKeys"
|
||||
@sorter-change="handleSorterChange"
|
||||
@select="(selectedKeys, rowKeyValue, record) => emits('select', selectedKeys, rowKeyValue, record)"
|
||||
@select-all="(check) => emits('selectAll', check)"
|
||||
>
|
||||
<template #empty>
|
||||
<NoData text="暂无稿件" />
|
||||
</template>
|
||||
<template #columns>
|
||||
<a-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.dataIndex"
|
||||
:data-index="column.dataIndex"
|
||||
:fixed="column.fixed"
|
||||
:width="column.width"
|
||||
:min-width="column.minWidth"
|
||||
:sortable="column.sortable"
|
||||
:align="column.align"
|
||||
ellipsis
|
||||
tooltip
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<span class="cts mr-4px">{{ column.title }}</span>
|
||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'customer_opinion'" #cell="{ record }">
|
||||
<p
|
||||
class="h-28px px-8px flex items-center rounded-2px w-fit"
|
||||
:style="{ background: getCustomerOpinionInfo(record.customer_opinion)?.bg }"
|
||||
>
|
||||
<span class="cts" :class="getCustomerOpinionInfo(record.customer_opinion)?.color">{{
|
||||
getCustomerOpinionInfo(record.customer_opinion)?.label ?? '-'
|
||||
}}</span>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'platform'" #cell="{ record }">
|
||||
<template v-if="!PLATFORMS.find((item) => item.value === record.platform)"> - </template>
|
||||
<img
|
||||
v-else
|
||||
width="24"
|
||||
height="24"
|
||||
class="rounded-4px"
|
||||
:src="PLATFORMS.find((item) => item.value === record.platform)?.icon"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'compliance_level'" #cell="{ record }">
|
||||
<span class="cts num !color-#6D4CFE">{{
|
||||
record.ai_review?.compliance_level ? `${record.ai_review?.compliance_level}%` : '-'
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'title'" #cell="{ record }">
|
||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
||||
width="16"
|
||||
height="16"
|
||||
class="mr-4px"
|
||||
/>
|
||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
#cell="{ record }"
|
||||
v-else-if="
|
||||
['created_at', 'last_modified_at', 'audit_started_at', 'audit_passed_at'].includes(column.dataIndex)
|
||||
"
|
||||
>
|
||||
<span class="cts num">{{ exactFormatTime(record[column.dataIndex]) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
||||
<HoverImagePreview :src="record.cover">
|
||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
||||
<template #error>
|
||||
<img :src="icon4" class="w-full h-full" />
|
||||
</template>
|
||||
</a-image>
|
||||
</HoverImagePreview>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
||||
<div class="flex items-center">
|
||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
||||
<a-button type="outline" size="mini" @click="onCheck(record)" v-if="audit_status === AuditStatus.Pending"
|
||||
>审核</a-button
|
||||
>
|
||||
<a-button
|
||||
type="outline"
|
||||
size="mini"
|
||||
@click="onScan(record)"
|
||||
v-else-if="audit_status === AuditStatus.Auditing"
|
||||
>查看</a-button
|
||||
>
|
||||
<a-button type="outline" size="mini" @click="onDetail(record)" v-else>详情</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else #cell="{ record }">
|
||||
{{ formatTableField(column, record, true) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants';
|
||||
import { patchWorkAuditsAuditWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import {
|
||||
CUSTOMER_OPINION,
|
||||
PLATFORMS,
|
||||
} from '@/views/writer-material-center/components/finished-products/manuscript/check-list/constants';
|
||||
import { AuditStatus } from '@/views/writer-material-center/components/finished-products/constants';
|
||||
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import HoverImagePreview from '@/components/hover-image-preview';
|
||||
|
||||
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';
|
||||
import icon4 from '@/assets/img/error-img.png';
|
||||
|
||||
const emits = defineEmits(['sorterChange', 'delete', 'select', 'selectAll']);
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
dataSource: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tableColumns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
rowSelection: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedRowKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
audit_status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const tableRef = ref(null);
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
const handleSorterChange = (column, order) => {
|
||||
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
|
||||
};
|
||||
const onDelete = (item) => {
|
||||
emits('delete', item);
|
||||
};
|
||||
const onCheck = (item) => {
|
||||
patchWorkAuditsAuditWriter(item.id, writerCode.value);
|
||||
slsWithCatch('writerManuscriptCheckIds', [item.id]);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: writerCode.value } });
|
||||
};
|
||||
const onScan = (item) => {
|
||||
slsWithCatch('writerManuscriptCheckIds', [item.id]);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: writerCode.value } });
|
||||
};
|
||||
const onDetail = (item) => {
|
||||
router.push(
|
||||
`/writer/material-center/check-list/detail/${item.id}/${writerCode.value}?source=check&audit_status=${props.audit_status}`,
|
||||
);
|
||||
};
|
||||
const getCustomerOpinionInfo = (value) => {
|
||||
return CUSTOMER_OPINION.find((item) => item.value === value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,19 @@
|
||||
.manuscript-table {
|
||||
.cts {
|
||||
color: var(--Text-1, #211f24);
|
||||
font-family: $font-family-medium;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.num {
|
||||
font-family: $font-family-manrope-regular;
|
||||
}
|
||||
}
|
||||
:deep(.title) {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #6d4cfe;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,246 @@
|
||||
import { AuditStatus } from '@/views/writer-material-center/components/finished-products/constants';
|
||||
|
||||
export const TABLE_COLUMNS1 = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'uid',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '图片/视频',
|
||||
dataIndex: 'cover',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '内容稿件标题',
|
||||
dataIndex: 'title',
|
||||
width: 300,
|
||||
},
|
||||
// {
|
||||
// title: '客户意见',
|
||||
// dataIndex: 'customer_opinion',
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后修改时间',
|
||||
dataIndex: 'last_modified_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
export const TABLE_COLUMNS2 = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'uid',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '图片/视频',
|
||||
dataIndex: 'cover',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '内容稿件标题',
|
||||
dataIndex: 'title',
|
||||
width: 300,
|
||||
},
|
||||
// {
|
||||
// title: '客户意见',
|
||||
// dataIndex: 'customer_opinion',
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: '审核平台',
|
||||
dataIndex: 'platform',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '合规程度',
|
||||
dataIndex: 'compliance_level',
|
||||
suffix: '%',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '审核时间',
|
||||
dataIndex: 'audit_started_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后修改时间',
|
||||
dataIndex: 'last_modified_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '修改人员',
|
||||
// dataIndex: 'last_modifier',
|
||||
// width: 180,
|
||||
// },
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
export const TABLE_COLUMNS3 = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'uid',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '图片/视频',
|
||||
dataIndex: 'cover',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '内容稿件标题',
|
||||
dataIndex: 'title',
|
||||
width: 300,
|
||||
},
|
||||
// {
|
||||
// title: '客户意见',
|
||||
// dataIndex: 'customer_opinion',
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: '审核平台',
|
||||
dataIndex: 'platform',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '通过时间',
|
||||
dataIndex: 'audit_passed_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后修改时间',
|
||||
dataIndex: 'last_modified_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '修改人员',
|
||||
// dataIndex: 'last_modifier',
|
||||
// width: 180,
|
||||
// },
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
export const AUDIT_STATUS_LIST = [
|
||||
{
|
||||
label: '待审核',
|
||||
value: AuditStatus.Pending,
|
||||
tableColumns: TABLE_COLUMNS1,
|
||||
},
|
||||
{
|
||||
label: '审核中',
|
||||
value: AuditStatus.Auditing,
|
||||
tableColumns: TABLE_COLUMNS2,
|
||||
},
|
||||
{
|
||||
label: '已通过',
|
||||
value: AuditStatus.Passed,
|
||||
tableColumns: TABLE_COLUMNS3,
|
||||
},
|
||||
];
|
||||
|
||||
export const INITIAL_QUERY = {
|
||||
audit_status: AuditStatus.Pending,
|
||||
title: '',
|
||||
created_at: [],
|
||||
audit_started_at: [],
|
||||
audit_platform: '',
|
||||
sort_column: undefined,
|
||||
sort_order: undefined,
|
||||
};
|
||||
|
||||
import icon1 from '@/assets/img/platform/icon-dy.png';
|
||||
import icon2 from '@/assets/img/platform/icon-xhs.png';
|
||||
|
||||
export const PLATFORMS = [
|
||||
{
|
||||
label: '小红书',
|
||||
value: 1,
|
||||
icon: icon2,
|
||||
},
|
||||
{
|
||||
label: '抖音',
|
||||
value: 2,
|
||||
icon: icon1,
|
||||
},
|
||||
];
|
||||
|
||||
export const CUSTOMER_OPINION = [
|
||||
{
|
||||
label: '待确认',
|
||||
value: 0,
|
||||
bg: '#F2F3F5',
|
||||
color: 'color-#3C4043',
|
||||
},
|
||||
{
|
||||
label: '已确认',
|
||||
value: 1,
|
||||
bg: '#F0EDFF',
|
||||
color: '!color-#6D4CFE',
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="manuscript-check-wrap">
|
||||
<div class="filter-wrap bg-#fff rounded-b-8px mb-16px">
|
||||
<FilterBlock
|
||||
v-model:query="query"
|
||||
:audit_status="query.audit_status"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-wrap bg-#fff rounded-8px px-24px py-24px flex flex-col">
|
||||
<div
|
||||
class="flex justify-end mb-12px"
|
||||
v-if="[AuditStatus.Pending, AuditStatus.Auditing].includes(query.audit_status)"
|
||||
>
|
||||
<a-button
|
||||
type="outline"
|
||||
class="w-fit"
|
||||
size="medium"
|
||||
@click="handleBatchCheck"
|
||||
v-if="query.audit_status === AuditStatus.Pending"
|
||||
>批量审核</a-button
|
||||
>
|
||||
<a-button
|
||||
type="outline"
|
||||
class="w-fit"
|
||||
size="medium"
|
||||
@click="handleBatchView"
|
||||
v-if="query.audit_status === AuditStatus.Auditing"
|
||||
>批量查看</a-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ManuscriptCheckTable
|
||||
:key="query.audit_status"
|
||||
:tableColumns="tableColumns"
|
||||
:rowSelection="rowSelection"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
:dataSource="dataSource"
|
||||
:audit_status="query.audit_status"
|
||||
@sorterChange="handleSorterChange"
|
||||
@delete="handleDelete"
|
||||
@select="handleSelect"
|
||||
@selectAll="handleSelectAll"
|
||||
/>
|
||||
<div v-if="pageInfo.total > 0" class="pagination-row">
|
||||
<a-pagination
|
||||
:total="pageInfo.total"
|
||||
size="mini"
|
||||
show-total
|
||||
show-jumper
|
||||
show-page-size
|
||||
:current="pageInfo.page"
|
||||
:page-size="pageInfo.page_size"
|
||||
@change="onPageChange"
|
||||
@page-size-change="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteManuscriptModal ref="deleteManuscriptModalRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="jsx" setup>
|
||||
import { provide } from 'vue';
|
||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
||||
import FilterBlock from './components/filter-block';
|
||||
import ManuscriptCheckTable from './components/manuscript-check-table';
|
||||
import DeleteManuscriptModal from './components/manuscript-check-table/delete-manuscript-modal.vue';
|
||||
|
||||
import { getWorkAuditsPageWriter, patchWorkAuditsBatchAuditWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
// import { getProjects } from '@/api/all/propertyMarketing';
|
||||
import { AuditStatus } from '@/views/writer-material-center/components/finished-products/constants';
|
||||
import { INITIAL_QUERY, AUDIT_STATUS_LIST, TABLE_COLUMNS1 } from './constants';
|
||||
|
||||
const props = defineProps({
|
||||
audit_status: {
|
||||
type: String,
|
||||
default: AuditStatus.All,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
dataSource,
|
||||
pageInfo,
|
||||
rowSelection,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
resetPageInfo,
|
||||
selectedRowKeys,
|
||||
selectedRows,
|
||||
handleSelect,
|
||||
handleSelectAll,
|
||||
DEFAULT_PAGE_INFO,
|
||||
} = useTableSelectionWithPagination({
|
||||
onPageChange: () => {
|
||||
getData();
|
||||
},
|
||||
onPageSizeChange: () => {
|
||||
getData();
|
||||
},
|
||||
});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tableColumns = ref([]);
|
||||
const query = ref(cloneDeep(INITIAL_QUERY));
|
||||
|
||||
const addManuscriptModalRef = ref(null);
|
||||
const deleteManuscriptModalRef = ref(null);
|
||||
// const shareManuscriptModalRef = ref(null);
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
const getData = async () => {
|
||||
const { page, page_size } = pageInfo.value;
|
||||
const { code, data } = await getWorkAuditsPageWriter(writerCode.value, {
|
||||
...query.value,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
if (code === 200) {
|
||||
dataSource.value = data?.data ?? [];
|
||||
pageInfo.value.total = data.total;
|
||||
}
|
||||
};
|
||||
const handleSearch = () => {
|
||||
reload();
|
||||
};
|
||||
const reload = () => {
|
||||
pageInfo.value.page = 1;
|
||||
getData();
|
||||
};
|
||||
const handleReset = () => {
|
||||
resetPageInfo();
|
||||
const _audit_status = query.value.audit_status;
|
||||
query.value = cloneDeep(INITIAL_QUERY);
|
||||
query.value.audit_status = _audit_status;
|
||||
reload();
|
||||
};
|
||||
const handleSorterChange = (column, order) => {
|
||||
query.value.sort_column = column;
|
||||
query.value.sort_order = order;
|
||||
reload();
|
||||
};
|
||||
const handleBatchCheck = () => {
|
||||
if (!selectedRows.value.length) {
|
||||
AMessage.warning('请选择需审核的内容稿件');
|
||||
return;
|
||||
}
|
||||
|
||||
patchWorkAuditsBatchAuditWriter(writerCode.value, { ids: selectedRowKeys.value });
|
||||
|
||||
slsWithCatch('writerManuscriptCheckIds', selectedRowKeys.value);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: writerCode.value } });
|
||||
};
|
||||
const handleBatchView = () => {
|
||||
if (!selectedRows.value.length) {
|
||||
AMessage.warning('请选择需查看的内容稿件');
|
||||
return;
|
||||
}
|
||||
|
||||
slsWithCatch('writerManuscriptCheckIds', selectedRowKeys.value);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: writerCode.value } });
|
||||
};
|
||||
|
||||
const handleTabClick = (key) => {
|
||||
query.value = cloneDeep(INITIAL_QUERY);
|
||||
dataSource.value = [];
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
resetPageInfo();
|
||||
query.value.audit_status = key;
|
||||
tableColumns.value = AUDIT_STATUS_LIST.find((item) => item.value === key).tableColumns;
|
||||
getData();
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
const { id, title } = item;
|
||||
deleteManuscriptModalRef.value?.open({ id, name: `“${title}”` });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.audit_status,
|
||||
(newVal) => {
|
||||
handleTabClick(newVal);
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
provide('update', getData);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,35 @@
|
||||
.manuscript-check-wrap {
|
||||
// height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.filter-wrap {
|
||||
:deep(.arco-tabs) {
|
||||
.arco-tabs-tab {
|
||||
height: 56px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.arco-tabs-nav-extra {
|
||||
padding-right: 24px;
|
||||
}
|
||||
.arco-tabs-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.top {
|
||||
.title {
|
||||
font-family: $font-family-medium;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:deep(.arco-btn) {
|
||||
.arco-btn-icon {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
:title="action === 'exit' ? '退出审核' : '切换内容稿件'"
|
||||
width="480px"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
||||
<span>{{
|
||||
action === 'exit'
|
||||
? '内容已修改尚未保存,若退出编辑,本次修改将不保存。'
|
||||
: '当前内容已修改尚未保存,若切换内容稿件,本次修改将不保存。'
|
||||
}}</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">
|
||||
{{ action === 'exit' ? '确认退出' : '确认切换' }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const emit = defineEmits(['selectCard']);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
const action = ref('');
|
||||
const cardInfo = ref(null);
|
||||
|
||||
const onClose = () => {
|
||||
action.value = '';
|
||||
cardInfo.value = null;
|
||||
visible.value = false;
|
||||
};
|
||||
const onConfirm = () => {
|
||||
if (action.value === 'exit') {
|
||||
router.push({
|
||||
name: 'WriterMaterialCenterFinishedProducts',
|
||||
params: {
|
||||
writerCode: route.params.writerCode,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
emit('selectCard', cardInfo.value);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const open = (type = 'exit', card = null) => {
|
||||
action.value = type;
|
||||
cardInfo.value = card;
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="提示"
|
||||
width="480px"
|
||||
@close="onClose"
|
||||
modal-class="upload-success11-modal"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="flex items-center flex-col justify-center">
|
||||
<img :src="icon1" width="80" height="80" class="mb-16px" />
|
||||
<span class="text-18px lh-26px font-400 color-#211F24 md mb-8px">内容稿件已通过审核</span>
|
||||
<!-- <p class="text-14px lh-22px font-400 color-#737478 ld">想让内容更抓眼球、更吸流量吗?</p>
|
||||
<p class="text-14px lh-22px font-400 color-#737478 ld">试试「内容稿件分析」功能吧!</p> -->
|
||||
</div>
|
||||
<!-- <template #footer>
|
||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">内容稿件分析</a-button>
|
||||
</template> -->
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue';
|
||||
import icon1 from '@/assets/img/media-account/icon-feedback-success.png';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
const workIds = ref([]);
|
||||
let autoCloseTimer = null;
|
||||
|
||||
const onClose = () => {
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
if (workIds.value.length === 1) {
|
||||
router.push({
|
||||
name: 'WriterMaterialCenterFinishedProducts',
|
||||
params: {
|
||||
writerCode: route.params.writerCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
workIds.value = [];
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const open = (ids) => {
|
||||
workIds.value = cloneDeep(ids);
|
||||
visible.value = true;
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
autoCloseTimer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.upload-success11-modal {
|
||||
.arco-modal-header {
|
||||
border-bottom: none;
|
||||
}
|
||||
.md {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
.ld {
|
||||
font-family: $font-family-regular;
|
||||
}
|
||||
.arco-modal-footer {
|
||||
border-top: none;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,87 @@
|
||||
<script lang="jsx">
|
||||
import { Drawer, Image } from '@arco-design/web-vue';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
|
||||
import icon1 from '@/assets/img/error-img.png';
|
||||
|
||||
export default {
|
||||
emits: ['cardClick'],
|
||||
setup(props, { emit, expose }) {
|
||||
const visible = ref(false);
|
||||
const dataSource = ref([]);
|
||||
const selectCardInfo = ref({});
|
||||
|
||||
const handleCardClick = (item) => {
|
||||
emit('cardClick', item);
|
||||
onClose();
|
||||
};
|
||||
const open = (data, _selectCardInfo) => {
|
||||
dataSource.value = data;
|
||||
selectCardInfo.value = _selectCardInfo;
|
||||
visible.value = true;
|
||||
};
|
||||
const onClose = () => {
|
||||
dataSource.value = [];
|
||||
selectCardInfo.value = {};
|
||||
visible.value = false;
|
||||
};
|
||||
expose({
|
||||
open,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<Drawer
|
||||
title="审核列表"
|
||||
visible={visible.value}
|
||||
width={420}
|
||||
class="check-list-drawer-xt"
|
||||
footer={false}
|
||||
header={false}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<div class="flex justify-between items-center h-56px px-24px">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3px h-16px rounded-2px bg-#6D4CFE mr-8px"></div>
|
||||
<span class="mr-8px cts bold">批量审核列表</span>
|
||||
<span class="mr-8px cts !lh-22px !text-14px">{`共${dataSource.value.length}个`}</span>
|
||||
</div>
|
||||
<icon-menu-unfold size={16} class="color-##55585F cursor-pointer hover:color-#6D4CFE" onClick={onClose} />
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-24px">
|
||||
{dataSource.value.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleCardClick(item)}
|
||||
class={`card-item flex rounded-8px bg-#F7F8FA p-8px ${
|
||||
selectCardInfo.value.id === item.id ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
preview={false}
|
||||
src={item.cover}
|
||||
class="!rounded-4px mr-8px"
|
||||
fit="cover"
|
||||
v-slots={{
|
||||
error: () => <img src={icon1} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
<div class="flex-1 overflow-hidden flex flex-col items-start">
|
||||
<TextOverTips context={item.title} class={`cts !color-#211F24 title mb-4px !text-14px`} />
|
||||
<p class="cts !text-14px">{`合规程度:${
|
||||
item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'
|
||||
}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,45 @@
|
||||
.check-list-drawer-xt {
|
||||
.arco-drawer-mask {
|
||||
background-color: transparent;
|
||||
}
|
||||
.arco-drawer {
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
|
||||
.arco-drawer-body {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 0 24px;
|
||||
.cts {
|
||||
color: var(--Text-1, #939499);
|
||||
|
||||
font-family: $font-family-regular;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
&.bold {
|
||||
color: var(--Text-1, #211f24);
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.card-item {
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all;
|
||||
&:hover {
|
||||
background-color: #e6e6e8;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
&.active {
|
||||
border-color: #6d4cfe;
|
||||
background-color: #f0edff;
|
||||
:deep(.overflow-text) {
|
||||
font-family: $font-family-medium !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
export const escapeRegExp = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
export const FORM_RULES = {
|
||||
title: [{ required: true, message: '请输入标题' }],
|
||||
};
|
||||
export const enumTab = {
|
||||
TEXT: 0,
|
||||
IMAGE: 1,
|
||||
};
|
||||
export const TAB_LIST = [
|
||||
{
|
||||
label: '文本',
|
||||
value: enumTab.TEXT,
|
||||
},
|
||||
// {
|
||||
// label: '图片',
|
||||
// value: enumTab.IMAGE,
|
||||
// },
|
||||
];
|
||||
|
||||
export enum Enum_Level {
|
||||
LOW = 0,
|
||||
MEDIUM = 1,
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
export const LEVEL_MAP = new Map([
|
||||
[Enum_Level.LOW, { label: '低风险', value: 'low_risk_number', color: '#6d4cfe' }],
|
||||
[Enum_Level.MEDIUM, { label: '中风险', value: 'medium_risk_number', color: '#FFAE00' }],
|
||||
[Enum_Level.HIGH, { label: '高风险', value: 'high_risk_number', color: '#F64B31' }],
|
||||
]);
|
||||
|
||||
export const RESULT_LIST = [
|
||||
{
|
||||
label: '合规程度',
|
||||
value: 'compliance_level',
|
||||
color: LEVEL_MAP.get(Enum_Level.LOW)?.color,
|
||||
suffix: '%',
|
||||
},
|
||||
{
|
||||
label: '检验项',
|
||||
value: 'inspection_count',
|
||||
color: '#211F24',
|
||||
},
|
||||
{
|
||||
label: '高风险',
|
||||
value: 'high_risk_number',
|
||||
color: LEVEL_MAP.get(Enum_Level.HIGH)?.color,
|
||||
},
|
||||
{
|
||||
label: '中风险',
|
||||
value: 'medium_risk_number',
|
||||
color: LEVEL_MAP.get(Enum_Level.MEDIUM)?.color,
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<a-textarea
|
||||
ref="textareaWrapRef"
|
||||
v-model="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
:max-length="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@focus="() => (focus = true)"
|
||||
@blur="() => (focus = false)"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="textarea-highlight"
|
||||
:class="{ focus: focus }"
|
||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||
import { escapeRegExp } from './constants';
|
||||
|
||||
// 定义Props类型
|
||||
interface ViolationItem {
|
||||
word: string;
|
||||
risk_level: number;
|
||||
}
|
||||
|
||||
interface LevelMapItem {
|
||||
color: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
prohibitedWords?: ViolationItem[];
|
||||
levelMap?: Map<number, LevelMapItem>;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
// 内部状态管理
|
||||
const inputValue = ref(props.modelValue || '');
|
||||
const focus = ref(false);
|
||||
const textareaWrapRef = ref();
|
||||
let nativeTextarea: HTMLTextAreaElement | null = null;
|
||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||
|
||||
// 监听外部modelValue变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal !== inputValue.value) {
|
||||
inputValue.value = (newVal || '').slice(0, 1000);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 处理输入事件
|
||||
const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
};
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
if (!isString(str)) return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const generateHighlightedHtml = (): string => {
|
||||
if (!inputValue.value) return '';
|
||||
|
||||
// 获取违禁词列表并按长度倒序排序(避免短词匹配长词)
|
||||
const words = (props.prohibitedWords || [])
|
||||
.filter((item) => item.word && item.risk_level !== undefined)
|
||||
.sort((a, b) => b.word.length - a.word.length);
|
||||
|
||||
if (words.length === 0) {
|
||||
return escapeHtml(inputValue.value);
|
||||
}
|
||||
|
||||
// 创建匹配正则表达式
|
||||
const pattern = new RegExp(`(${words.map((item) => escapeRegExp(item.word)).join('|')})`, 'gi');
|
||||
|
||||
// 替换匹配的违禁词为带样式的span标签
|
||||
return inputValue.value.replace(pattern, (match) => {
|
||||
// 找到对应的违禁词信息
|
||||
const wordInfo = words.find((item) => item.word.toLowerCase() === match.toLowerCase());
|
||||
|
||||
if (!wordInfo) return match;
|
||||
|
||||
// 获取风险等级对应的样式
|
||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
||||
const color = levelStyle?.color || '#F64B31';
|
||||
|
||||
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nativeTextarea =
|
||||
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
||||
document.querySelector('.textarea-input .arco-textarea');
|
||||
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.addEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.removeEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.removeEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTextareaScroll = (e: Event) => {
|
||||
const _scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
|
||||
|
||||
const highlightElement = document.querySelector('.textarea-highlight');
|
||||
if (highlightElement) {
|
||||
highlightElement.scrollTop = _scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompositionUpdate = () => {
|
||||
if (!nativeTextarea) return;
|
||||
// 使用 rAF 等待浏览器把最新字符写入 textarea.value 再读取
|
||||
requestAnimationFrame(() => {
|
||||
if (!nativeTextarea) return;
|
||||
const latest = nativeTextarea.value.slice(0, 1000);
|
||||
if (latest !== inputValue.value) {
|
||||
inputValue.value = latest;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.highlight-textarea-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@mixin textarea-padding {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@mixin textarea-style {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400px;
|
||||
border: 1px solid #d7d7d9;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
.textarea-highlight {
|
||||
@include textarea-style;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
@include textarea-padding;
|
||||
|
||||
&.focus {
|
||||
border-color: rgb(var(--primary-6)) !important;
|
||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
@include textarea-style;
|
||||
|
||||
.arco-textarea {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: #211f24 !important;
|
||||
resize: none;
|
||||
@include textarea-padding;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.arco-textarea-word-limit {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.arco-textarea-disabled {
|
||||
.arco-textarea {
|
||||
background: #f2f3f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
||||
:deep(.textarea-input.composing .arco-textarea) {
|
||||
color: #211f24 !important;
|
||||
-webkit-text-fill-color: #211f24 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,475 @@
|
||||
<script lang="jsx">
|
||||
import axios from 'axios';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import { IconLoading } from '@arco-design/web-vue/es/icon';
|
||||
import {
|
||||
Image,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Textarea,
|
||||
Button,
|
||||
Tabs,
|
||||
Upload,
|
||||
TabPane,
|
||||
Spin,
|
||||
Message as AMessage,
|
||||
} from '@arco-design/web-vue';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import { Navigation } from 'swiper/modules';
|
||||
import { FORM_RULES, enumTab, TAB_LIST, RESULT_LIST, LEVEL_MAP, escapeRegExp } from './constants';
|
||||
import { getImagePreSignedUrl } from '@/api/all/common';
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants';
|
||||
|
||||
import icon1 from '@/assets/img/creative-generation-workshop/icon-magic.png';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-line.png';
|
||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-success.png';
|
||||
import icon4 from '@/assets/img/error-img.png';
|
||||
import icon5 from '@/assets/img/creative-generation-workshop/icon-lf2.png';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
selectedImageInfo: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
checkLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
getDataLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'filesChange', 'selectImage', 'againCheck', 'startCheck'],
|
||||
setup(props, { emit, expose }) {
|
||||
const activeTab = ref(enumTab.TEXT);
|
||||
const aiReplaceLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
const uploadRef = ref(null);
|
||||
const modules = [Navigation];
|
||||
|
||||
const isTextTab = computed(() => activeTab.value === enumTab.TEXT);
|
||||
const aiReview = computed(() => props.modelValue.ai_review);
|
||||
const isDisabled = computed(() => props.checkLoading || aiReplaceLoading.value);
|
||||
|
||||
const onAiReplace = () => {
|
||||
if (aiReplaceLoading.value) return;
|
||||
|
||||
aiReplaceLoading.value = true;
|
||||
setTimeout(() => {
|
||||
const content = props.modelValue.content;
|
||||
const rules = aiReview.value?.violation_items ?? [];
|
||||
const sortedRules = [...rules].sort((a, b) => b.word.length - a.word.length);
|
||||
|
||||
const replacedContent = sortedRules.reduce((result, rule) => {
|
||||
if (!rule.word) return result;
|
||||
|
||||
const escapedWord = escapeRegExp(rule.word);
|
||||
const regex = new RegExp(escapedWord, 'g');
|
||||
|
||||
return result.replace(regex, rule.replace_word);
|
||||
}, content);
|
||||
|
||||
props.modelValue.content = replacedContent;
|
||||
aiReplaceLoading.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onAgainCheck = () => {
|
||||
if (!isTextTab.value && !props.modelValue.files?.length) {
|
||||
AMessage.warning('请先上传需审核图片');
|
||||
return;
|
||||
}
|
||||
emit('againCheck');
|
||||
};
|
||||
const onReplaceImage = () => {
|
||||
uploadRef.value?.upload?.();
|
||||
};
|
||||
const handleTabClick = (key) => {
|
||||
activeTab.value = key;
|
||||
};
|
||||
const validate = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (errors) {
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const reset = () => {
|
||||
formRef.value?.resetFields?.();
|
||||
formRef.value?.clearValidate?.();
|
||||
aiReplaceLoading.value = false;
|
||||
};
|
||||
const getFileExtension = (filename) => {
|
||||
const match = filename.match(/\.([^.]+)$/);
|
||||
return match ? match[1].toLowerCase() : '';
|
||||
};
|
||||
const handleSelectImage = (item) => {
|
||||
emit('selectImage', item);
|
||||
};
|
||||
const onDeleteImage = (e, item, index) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const _newFiles = cloneDeep(props.modelValue.files);
|
||||
_newFiles.splice(index, 1);
|
||||
|
||||
if (item.id === props.selectedImageInfo.id) {
|
||||
emit('selectImage', _newFiles.length ? _newFiles[0] : {});
|
||||
}
|
||||
|
||||
emit('filesChange', _newFiles);
|
||||
};
|
||||
|
||||
const renderUpload = (UploadBtn, action = 'upload') => {
|
||||
return (
|
||||
<Upload
|
||||
ref={uploadRef}
|
||||
action="/"
|
||||
draggable
|
||||
class="w-fit"
|
||||
custom-request={(option) => uploadImage(option, action)}
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
show-file-list={false}
|
||||
multiple
|
||||
>
|
||||
{{
|
||||
'upload-button': () => <UploadBtn />,
|
||||
}}
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadImage = async (option, action = 'upload') => {
|
||||
const {
|
||||
fileItem: { file },
|
||||
} = option;
|
||||
|
||||
const { name, size, type } = file;
|
||||
const response = await getImagePreSignedUrl({ suffix: getFileExtension(name) });
|
||||
const { file_name, upload_url, file_url } = response?.data;
|
||||
|
||||
const blob = new Blob([file], { type });
|
||||
await axios.put(upload_url, blob, {
|
||||
headers: { 'Content-Type': type },
|
||||
});
|
||||
|
||||
const _file = {
|
||||
url: file_url,
|
||||
name: file_name,
|
||||
size,
|
||||
};
|
||||
|
||||
const _newFiles =
|
||||
action === 'replaceImage'
|
||||
? props.modelValue.files.map((item) => {
|
||||
if (item.url === props.selectedImageInfo.url) {
|
||||
return _file;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
: [...props.modelValue.files, _file];
|
||||
|
||||
emit('filesChange', _newFiles);
|
||||
emit('selectImage', _file);
|
||||
};
|
||||
|
||||
const renderFooterRow = () => {
|
||||
return (
|
||||
<>
|
||||
<Button class="mr-12px" size="medium" onClick={onAgainCheck} disabled={isDisabled.value}>
|
||||
再次审核
|
||||
</Button>
|
||||
{isTextTab.value ? (
|
||||
<Button size="medium" type="outline" class="w-123px check-btn" onClick={onAiReplace} disabled={isDisabled.value}>
|
||||
{aiReplaceLoading.value ? (
|
||||
<>
|
||||
<IconLoading size={14} />
|
||||
<span class="ml-8px check-text">AI生成中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<img src={icon1} width={14} height={14} />
|
||||
<span class="ml-8px check-text">替换违禁词</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<div class="w-88px">
|
||||
{renderUpload(
|
||||
<Button size="medium" type="outline">
|
||||
图片替换
|
||||
</Button>,
|
||||
'replaceImage',
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderTextForm = () => {
|
||||
return (
|
||||
<Form ref={formRef} model={props.modelValue} rules={FORM_RULES} layout="vertical" auto-label-width>
|
||||
<FormItem label="标题" field="title" required>
|
||||
<Input
|
||||
v-model={props.modelValue.title}
|
||||
placeholder="请输入标题"
|
||||
size="large"
|
||||
maxLength={30}
|
||||
show-word-limit
|
||||
disabled={isDisabled.value}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="作品描述" field="content" class="flex-1 content-form-item">
|
||||
<Textarea
|
||||
v-model={props.modelValue.content}
|
||||
placeholder="请输入作品描述"
|
||||
size="large"
|
||||
show-word-limit
|
||||
maxLength={1000}
|
||||
disabled={isDisabled.value}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const renderImageForm = () => {
|
||||
if (props.modelValue.files?.length > 0) {
|
||||
return (
|
||||
<div class="w-full h-full py-16px flex justify-center">
|
||||
<div class="w-380px flex flex-col justify-center">
|
||||
<Image
|
||||
src={props.selectedImageInfo.url}
|
||||
width={370}
|
||||
height={370}
|
||||
preview={false}
|
||||
class="flex items-center justify-center mb-8px"
|
||||
fit="contain"
|
||||
v-slots={{
|
||||
error: () => <img src={icon4} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="swiper-wrap h-78px">
|
||||
<Swiper
|
||||
spaceBetween={16}
|
||||
modules={modules}
|
||||
slidesPerView="auto"
|
||||
navigation={{
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
}}
|
||||
>
|
||||
{props.modelValue.files.map((item, index) => (
|
||||
<SwiperSlide
|
||||
key={item.id}
|
||||
onClick={() => handleSelectImage(item)}
|
||||
class={`!h-48px !w-48px !relative bg-#F7F8FA cursor-pointer !flex items-center ${
|
||||
item.id === props.selectedImageInfo.id ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<div class="group relative w-full h-full rounded-5px">
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
src={item.url}
|
||||
class="!rounded-4px"
|
||||
fit="contain"
|
||||
preview={false}
|
||||
v-slots={{
|
||||
error: () => <img src={icon4} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<icon-close-circle-fill
|
||||
size={16}
|
||||
class="close-icon absolute top--8px right--8px hidden cursor-pointer color-#737478 hover:!color-#211F24 z-50"
|
||||
onClick={(e) => onDeleteImage(e, item, index)}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
<div class="swiper-box swiper-button-prev">
|
||||
<img src={icon5} class="w-8px h-17px" />
|
||||
</div>
|
||||
<div class="swiper-box swiper-button-next">
|
||||
<img src={icon5} class="w-8px h-17px rotate-180" />
|
||||
</div>
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
||||
<div class="flex justify-center mb-16px">
|
||||
{renderUpload(
|
||||
<div class="upload-box">
|
||||
<icon-plus size="14" class="mb-16px color-#3C4043" />
|
||||
<span class="cts !color-#211F24">上传图片</span>
|
||||
</div>,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span class="cts">上传要审核的图片素材</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderCheckSuccessBox = () => {
|
||||
if (!aiReview.value?.violation_items?.length) {
|
||||
return (
|
||||
<div>
|
||||
<div class="flex items-center mb-16px">
|
||||
{RESULT_LIST.map((item, index) => (
|
||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
||||
item.suffix || ''
|
||||
}`}</span>
|
||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
||||
<div class="mb-24px relative w-fit">
|
||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center h-138px justify-center">
|
||||
<img src={icon3} width={72} height={72} class="mb-12px" />
|
||||
<span class="cts !color-#25C883">
|
||||
{isTextTab.value ? '恭喜,您的文案中没有检测出违禁词' : '恭喜,您的图片中没有检测出违禁内容'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class="flex items-center mb-16px">
|
||||
{RESULT_LIST.map((item, index) => (
|
||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
||||
item.suffix || ''
|
||||
}`}</span>
|
||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
||||
<div class=" mb-8px relative w-fit">
|
||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
||||
</div>
|
||||
{aiReview.value?.suggestion?.map((item, index) => (
|
||||
<p class="cts !color-#55585F !text-12px !lh-20px" key={index}>{`${index + 1}. ${item}`}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="forbid-word-box flex-1 flex">
|
||||
<div class="left mr-32px w-56px">
|
||||
<p class="mb-12px cts !text-12px">违禁词</p>
|
||||
{aiReview.value?.violation_items?.map((item, index) => (
|
||||
<TextOverTips
|
||||
context={item.word}
|
||||
class="mb-12px cts"
|
||||
style={{ color: LEVEL_MAP.get(item.risk_level)?.color }}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div class="right flex-1 overflow-hidden">
|
||||
<p class="mb-12px cts !text-12px">解释</p>
|
||||
{aiReview.value?.violation_items?.map((item, index) => (
|
||||
<TextOverTips context={item.reason} class="mb-12px" key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRightBox = () => {
|
||||
if (props.checkLoading) {
|
||||
return (
|
||||
<div class="right-box h-210px">
|
||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
||||
<Spin
|
||||
loading={true}
|
||||
tip={`${isTextTab.value ? '文本' : '图片'}检测中`}
|
||||
size={72}
|
||||
class="h-298px !flex flex-col justify-center items-center"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div class="right-box">
|
||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
||||
{props.getDataLoading ? (
|
||||
<Spin loading={true} size={72} class="h-298px !flex justify-center items-center" />
|
||||
) : (
|
||||
renderCheckSuccessBox()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
expose({
|
||||
validate,
|
||||
reset,
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="h-full w-full px-24px pt-16px pb-24px content-wrap flex">
|
||||
<div class="flex-2 left-box mr-24px flex flex-col">
|
||||
<div class="flex-1 mb-12px rounded-8px border-1px pt-8px flex flex-col pb-16px bg-#F7F8FA border-#E6E6E8 border-solid">
|
||||
<Tabs v-model={activeTab.value} onTabClick={handleTabClick} class="mb-16px">
|
||||
{TAB_LIST.map((item) => (
|
||||
<TabPane
|
||||
key={item.value}
|
||||
v-slots={{
|
||||
title: () => (
|
||||
<div class="flex items-center relative">
|
||||
<span>{item.label}</span>
|
||||
{
|
||||
// activeTab.value === item.value && aiReview.value?.violation_items.length > 0 && (
|
||||
// <icon-exclamation-circle-fill size={14} class="color-#F64B31 absolute right--10px top-0" />
|
||||
// )
|
||||
}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<div class="flex-1 px-16px">{isTextTab.value ? renderTextForm() : renderImageForm()}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">{renderFooterRow()}</div>
|
||||
</div>
|
||||
{renderRightBox()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,202 @@
|
||||
.content-wrap {
|
||||
.cts {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.check-btn {
|
||||
.check-text {
|
||||
background: linear-gradient(84deg, #266cff 4.57%, #a15af0 84.93%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.left-box {
|
||||
:deep(.arco-tabs) {
|
||||
.arco-tabs-nav {
|
||||
.arco-tabs-tab {
|
||||
height: 40px;
|
||||
// padding: 0 8px;
|
||||
margin: 0 16px;
|
||||
}
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.arco-tabs-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.arco-form) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.arco-form-item {
|
||||
margin-bottom: 24px;
|
||||
.arco-form-item-label-col {
|
||||
.arco-form-item-label {
|
||||
color: #939499;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-form-item {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.arco-form-item-wrapper-col {
|
||||
flex: 1;
|
||||
.arco-form-item-content-wrapper,
|
||||
.arco-form-item-content,
|
||||
.arco-textarea-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.upload-box {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
||||
background: var(--BG-200, #f2f3f5);
|
||||
&:hover {
|
||||
background: var(--Primary-1, #e6e6e8);
|
||||
}
|
||||
}
|
||||
.swiper-wrap {
|
||||
:deep(.swiper) {
|
||||
height: 100%;
|
||||
.swiper-wrapper {
|
||||
align-items: center;
|
||||
.swiper-slide {
|
||||
transition: all;
|
||||
&.active {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
.group {
|
||||
border: 2px solid var(--Brand-6, #6d4cfe);
|
||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.close-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
position: absolute;
|
||||
margin-top: 0 !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
transition: all;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
&.swiper-button-prev {
|
||||
left: 16px;
|
||||
}
|
||||
&.swiper-button-next {
|
||||
right: 16px;
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
&.swiper-button-disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-box {
|
||||
border: 1px solid #E6E6E8;
|
||||
flex: 1;
|
||||
border-radius: 8px; padding: 16px; display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
height: fit-content;
|
||||
max-height: 100%;
|
||||
.s1 {
|
||||
font-family: $font-family-manrope-medium;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 32px; /* 133.333% */
|
||||
}
|
||||
.result-item {
|
||||
&:first-child {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: var(--Border-1, #d7d7d9);
|
||||
}
|
||||
}
|
||||
}
|
||||
.suggestion-box {
|
||||
.ai-text {
|
||||
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-family: $font-family-medium;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
:deep(.overflow-text) {
|
||||
color: #211f24;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
.forbid-word-box {
|
||||
:deep(.overflow-text) {
|
||||
&.level0 {
|
||||
color: #6d4cfe;
|
||||
}
|
||||
&.level2 {
|
||||
color: #f64b31;
|
||||
}
|
||||
&.level1 {
|
||||
color: #ffae00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
<script lang="jsx">
|
||||
import { Image } from '@arco-design/web-vue';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import { Navigation } from 'swiper/modules';
|
||||
import { PLATFORMS } from '@/views/writer-material-center/components/finished-products/manuscript/check-list/constants';
|
||||
|
||||
import icon1 from '@/assets/img/error-img.png';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-lf.png';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
dataSource: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectCardInfo: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
emits: ['cardClick', 'platformChange'],
|
||||
setup(props, { emit, expose }) {
|
||||
const modules = [Navigation];
|
||||
const handleCardClick = (item) => {
|
||||
// emit('update:modelValue', item);
|
||||
emit('cardClick', item);
|
||||
};
|
||||
return () => {
|
||||
return (
|
||||
<header class="header-wrap">
|
||||
{props.dataSource.length > 1 && (
|
||||
<div class="swiper-wrap pt-16px h-80px px-24px">
|
||||
<Swiper
|
||||
spaceBetween={16}
|
||||
modules={modules}
|
||||
slidesPerView="auto"
|
||||
navigation={{
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
}}
|
||||
>
|
||||
{props.dataSource.map((item) => (
|
||||
<SwiperSlide
|
||||
key={item.id}
|
||||
onClick={() => handleCardClick(item)}
|
||||
class={`swiper-item !h-64px !w-280px bg-#F7F8FA border-1px cursor-pointer border-solid border-transparent rounded-8px p-8px overflow-hidden !flex items-center ${
|
||||
item.id === props.selectCardInfo.id ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
preview={false}
|
||||
src={item.cover}
|
||||
class="!rounded-4px mr-8px"
|
||||
fit="cover"
|
||||
v-slots={{
|
||||
error: () => <img src={icon1} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
<div class="flex-1 overflow-hidden flex flex-col items-start">
|
||||
<TextOverTips context={item.title} class={`cts !color-#211F24 title mb-4px`} />
|
||||
<p class="cts">{`合规程度:${item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'}`}</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
|
||||
<div class="swiper-box swiper-button-prev">
|
||||
<div class="swiper-button">
|
||||
<img src={icon2} class="w-16px h-16px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-box swiper-button-next">
|
||||
<div class="swiper-button">
|
||||
<img src={icon2} class="w-16px h-16px rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
</Swiper>
|
||||
</div>
|
||||
)}
|
||||
<div class="platform-row py-16px flex items-center px-24px">
|
||||
<span class="mr-16px cts !color-#211F24">审核平台选择</span>
|
||||
<div class="flex items-center">
|
||||
{PLATFORMS.map((item) => (
|
||||
<div
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
emit('platformChange', item.value);
|
||||
}}
|
||||
class={`w-100px flex items-center mr-16px py-8px px-12px flex border-1px border-solid border-transparent transition-all
|
||||
items-center rounded-8px cursor-pointer bg-#F2F3F5 hover:bg-#E6E6E8 ${
|
||||
props.selectCardInfo.platform === item.value ? '!bg-#F0EDFF !border-#6D4CFE' : ''
|
||||
}`}
|
||||
>
|
||||
<img src={item.icon} alt="" width={20} height={20} class="mr-4px" />
|
||||
<span class={`cts !color-#211F24 ${props.selectCardInfo.platform === item.value ? 'bold' : ''}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,74 @@
|
||||
.header-wrap {
|
||||
.cts {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.swiper-wrap {
|
||||
.swiper-item {
|
||||
transition: all;
|
||||
&:hover {
|
||||
background-color: #e6e6e8;
|
||||
}
|
||||
&.active {
|
||||
background-color: #f0edff;
|
||||
border-color: #6d4cfe;
|
||||
:deep(.overflow-text) {
|
||||
font-family: $font-family-medium !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper-box {
|
||||
width: 100px;
|
||||
height: 64px;
|
||||
position: absolute;
|
||||
&.swiper-button-prev {
|
||||
background: linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, #fff 43.06%);
|
||||
margin-top: 0 !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
justify-content: flex-start;
|
||||
padding-left: 8px;
|
||||
}
|
||||
&.swiper-button-next {
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #fff 43.06%);
|
||||
margin-top: 0 !important;
|
||||
top: 0;
|
||||
right: 0;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
.swiper-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--Border-1, #d7d7d9);
|
||||
&:hover {
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
&.click {
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
}
|
||||
&.swiper-button-disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.platform-row {
|
||||
border-bottom: 1px solid var(--Border-2, #e6e6e8);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,269 @@
|
||||
<script lang="jsx">
|
||||
import { Button, Message as AMessage, Spin } from '@arco-design/web-vue';
|
||||
import CancelCheckModal from './cancel-check-modal.vue';
|
||||
import CheckSuccessModal from './check-success-modal.vue';
|
||||
import HeaderCard from './components/header-card';
|
||||
import ContentCard from './components/content-card';
|
||||
import CheckListDrawer from './components/check-list-drawer';
|
||||
|
||||
import { slsWithCatch, rlsWithCatch, glsWithCatch } from '@/utils/stroage.ts';
|
||||
import useGetAiReviewResult from '@/hooks/useGetAiReviewResult.ts';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
import {
|
||||
getWorkAuditsBatchDetailWriter,
|
||||
putWorkAuditsUpdateWriter,
|
||||
postWorkAuditsAiReviewWriter,
|
||||
getWorkAuditsAiReviewResultWriter,
|
||||
putWorkAuditsAuditPassWriter,
|
||||
} from '@/api/all/generationWorkshop-writer.ts';
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const sidebarStore = useSidebarStore();
|
||||
|
||||
const workIds = ref([]);
|
||||
const isSaved = ref(false);
|
||||
const dataSource = ref([]);
|
||||
const remoteDataSource = ref([]);
|
||||
const cancelCheckModalRef = ref(null);
|
||||
const checkSuccessModalRef = ref(null);
|
||||
const submitLoading = ref(false);
|
||||
const contentCardRef = ref(null);
|
||||
const checkListDrawerRef = ref(null);
|
||||
const getDataLoading = ref(false);
|
||||
|
||||
const selectCardInfo = ref({});
|
||||
const selectedImageInfo = ref(null);
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
const collapsed = computed(() => {
|
||||
return sidebarStore.menuCollapse;
|
||||
});
|
||||
|
||||
const { handleStartCheck, handleAgainCheck, ticket, checkLoading, resetAiReviewInfo } = useGetAiReviewResult({
|
||||
cardInfo: selectCardInfo,
|
||||
startAiReviewFn: postWorkAuditsAiReviewWriter,
|
||||
getAiReviewResultFn: getWorkAuditsAiReviewResultWriter,
|
||||
updateAiReview(ai_review) {
|
||||
selectCardInfo.value.ai_review = ai_review;
|
||||
},
|
||||
});
|
||||
|
||||
const onBack = () => {
|
||||
router.push({
|
||||
name: 'WriterMaterialCenterFinishedProducts',
|
||||
params: {
|
||||
writerCode: route.params.writerCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeCard = (item) => {
|
||||
contentCardRef.value.reset();
|
||||
isSaved.value = false;
|
||||
submitLoading.value = false;
|
||||
getDataLoading.value = false;
|
||||
checkLoading.value = false;
|
||||
resetAiReviewInfo();
|
||||
|
||||
const { files = [], ai_review } = item;
|
||||
selectCardInfo.value = cloneDeep(item);
|
||||
selectedImageInfo.value = cloneDeep(files?.[0] ?? {});
|
||||
|
||||
if (isEmpty(ai_review)) {
|
||||
handleStartCheck();
|
||||
}
|
||||
};
|
||||
|
||||
const onCardClick = async (item) => {
|
||||
const isModified = await isSelectCardModified();
|
||||
if (isModified) {
|
||||
cancelCheckModalRef.value?.open('toggle', item);
|
||||
} else {
|
||||
onChangeCard(item);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectImage = (item) => {
|
||||
selectedImageInfo.value = cloneDeep(item);
|
||||
};
|
||||
|
||||
const getWorkAudits = async () => {
|
||||
try {
|
||||
getDataLoading.value = true;
|
||||
const { code, data } = await getWorkAuditsBatchDetailWriter(writerCode.value, { ids: workIds.value });
|
||||
if (code === 200) {
|
||||
const _data = (data ?? []).map((item) => ({
|
||||
...item,
|
||||
platform: item.platform === 0 ? 1 : item.platform,
|
||||
}));
|
||||
|
||||
dataSource.value = _data;
|
||||
remoteDataSource.value = cloneDeep(_data);
|
||||
|
||||
const _firstCard = _data?.[0] ?? {};
|
||||
const { id, ai_review } = _firstCard;
|
||||
|
||||
selectCardInfo.value = cloneDeep(_firstCard);
|
||||
selectedImageInfo.value = cloneDeep(_firstCard.files?.[0] ?? {});
|
||||
|
||||
if (isEmpty(ai_review)) {
|
||||
handleStartCheck();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
getDataLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const isSelectCardModified = () => {
|
||||
return new Promise((resolve) => {
|
||||
const _item = remoteDataSource.value.find((item) => item.id === selectCardInfo.value.id);
|
||||
resolve(!isEqual(selectCardInfo.value, _item) && !isSaved.value);
|
||||
});
|
||||
};
|
||||
const onPlatformChange = (platform) => {
|
||||
selectCardInfo.value.platform = platform;
|
||||
};
|
||||
|
||||
const onExit = async () => {
|
||||
const isModified = await isSelectCardModified();
|
||||
if (isModified) {
|
||||
cancelCheckModalRef.value?.open();
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
const onSave = async () => {
|
||||
if (!selectCardInfo.value.title) {
|
||||
AMessage.warning('标题不能为空');
|
||||
}
|
||||
|
||||
contentCardRef.value?.validate().then(async () => {
|
||||
const { code, data } = await putWorkAuditsUpdateWriter(writerCode.value, selectCardInfo.value);
|
||||
if (code === 200) {
|
||||
isSaved.value = true;
|
||||
AMessage.success('当前内容稿件已保存');
|
||||
}
|
||||
});
|
||||
};
|
||||
const onCheckSuccess = () => {
|
||||
checkSuccessModalRef.value?.open(workIds.value);
|
||||
|
||||
if (workIds.value.length > 1) {
|
||||
const _id = selectCardInfo.value.id;
|
||||
workIds.value = workIds.value.filter((v) => v != _id);
|
||||
dataSource.value = dataSource.value.filter((v) => v.id != _id);
|
||||
|
||||
slsWithCatch('writerManuscriptCheckIds', workIds.value.join(','));
|
||||
onChangeCard(dataSource.value.length ? dataSource.value[0] : {});
|
||||
}
|
||||
};
|
||||
const onSubmit = async () => {
|
||||
contentCardRef.value?.validate().then(async () => {
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
const { code, data } = await putWorkAuditsAuditPassWriter(writerCode.value, selectCardInfo.value);
|
||||
if (code === 200) {
|
||||
onCheckSuccess();
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
const onFilesChange = (files) => {
|
||||
selectCardInfo.value.files = cloneDeep(files);
|
||||
};
|
||||
const onAgainCheck = () => {
|
||||
handleAgainCheck();
|
||||
};
|
||||
|
||||
const renderFooterRow = () => {
|
||||
return (
|
||||
<>
|
||||
<Button size="medium" type="outline" class="mr-12px" onClick={onExit}>
|
||||
退出
|
||||
</Button>
|
||||
<Button size="medium" type="outline" class="mr-12px" onClick={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={onSubmit} loading={submitLoading.value}>
|
||||
{submitLoading.value ? '通过审核中...' : '通过审核'}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
onMounted(() => {
|
||||
workIds.value = glsWithCatch('writerManuscriptCheckIds')?.split(',') ?? [];
|
||||
getWorkAudits();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
rlsWithCatch('writerManuscriptCheckIds');
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="manuscript-check-wrap flex flex-col">
|
||||
<div class="flex items-center mb-10px">
|
||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onExit}>
|
||||
成品库
|
||||
</span>
|
||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
||||
<span class="cts bold !color-#1D2129">{`${workIds.value.length > 0 ? '批量' : ''}审核内容稿件`}</span>
|
||||
</div>
|
||||
{dataSource.value.length > 1 && (
|
||||
<div
|
||||
class="check-list-icon"
|
||||
onClick={() => checkListDrawerRef.value.open(dataSource.value, selectCardInfo.value)}
|
||||
>
|
||||
<icon-menu-fold size={16} class="color-#55585F mr-4px icon" />
|
||||
<span class="cts !color-#211F24">审核列表</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex-1 flex flex-col overflow-hidden bg-#fff rounded-8px ">
|
||||
<HeaderCard
|
||||
dataSource={dataSource.value}
|
||||
selectCardInfo={selectCardInfo.value}
|
||||
onCardClick={onCardClick}
|
||||
onPlatformChange={onPlatformChange}
|
||||
/>
|
||||
<section class="flex-1 overflow-hidden">
|
||||
<ContentCard
|
||||
ref={contentCardRef}
|
||||
v-model={selectCardInfo.value}
|
||||
selectCardInfo={selectCardInfo.value}
|
||||
onFilesChange={onFilesChange}
|
||||
selectedImageInfo={selectedImageInfo.value}
|
||||
onSelectImage={onSelectImage}
|
||||
checkLoading={checkLoading.value}
|
||||
getDataLoading={getDataLoading.value}
|
||||
onAgainCheck={onAgainCheck}
|
||||
onStartCheck={handleStartCheck}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
||||
collapsed.value ? 'collapsed' : ''
|
||||
}`}
|
||||
>
|
||||
{renderFooterRow()}
|
||||
</footer>
|
||||
|
||||
<CancelCheckModal ref={cancelCheckModalRef} onSelectCard={onChangeCard} />
|
||||
<CheckSuccessModal ref={checkSuccessModalRef} />
|
||||
<CheckListDrawer ref={checkListDrawerRef} onCardClick={onCardClick} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,48 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-check-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
||||
.cts {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.check-list-icon {
|
||||
// width: 92px;
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 30px 0 0 30px;
|
||||
border: 1px solid var(--Border-1, #d7d7d9);
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc($navbar-height + 8px);
|
||||
&:hover {
|
||||
.icon,
|
||||
.cts {
|
||||
color: #6d4cfe !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer-row {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: $sidebar-width;
|
||||
width: calc(100% - $sidebar-width);
|
||||
height: $footer-height;
|
||||
&.collapsed {
|
||||
left: $sidebar-width-collapse;
|
||||
width: calc(100% - $sidebar-width-collapse);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<a-form-item field="files">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="cts !color-#211F24 mr-4px">图片</span>
|
||||
<span class="cts mr-8px !color-#939499">{{ `(${files.length ?? 0}/18)` }}</span>
|
||||
<span class="cts !color-#939499">第一张为首图,支持拖拽排序</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center">
|
||||
<VueDraggable v-model="files" class="grid grid-cols-7 gap-y-8px" @end="handleChange" draggable=".group">
|
||||
<div
|
||||
v-for="(file, index) in files"
|
||||
:key="file.url"
|
||||
class="group relative cursor-move overflow-visible py-8px pr-8px"
|
||||
>
|
||||
<div class="group-container relative rounded-8px w-100px h-100px">
|
||||
<img :src="file.url" class="object-cover w-full h-full border-1px border-#E6E6E8 rounded-8px" />
|
||||
<icon-close-circle-fill
|
||||
:size="16"
|
||||
class="absolute top--8px right--8px cursor-pointer hidden color-#939499 hidden group-hover:block z-50"
|
||||
@click="() => handleDeleteFile(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a-upload
|
||||
v-if="files.length < 18"
|
||||
ref="uploadRef"
|
||||
action="/"
|
||||
draggable
|
||||
:custom-request="(option) => emit('upload', option)"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
:show-file-list="false"
|
||||
multiple
|
||||
class="!flex !items-center"
|
||||
:limit="18 - files.length"
|
||||
>
|
||||
<template #upload-button>
|
||||
<div class="upload-box">
|
||||
<icon-plus size="14" class="mb-16px color-#3C4043" />
|
||||
<span class="cts !color-#211F24">上传图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const files = ref([]);
|
||||
const uploadRef = ref(null);
|
||||
|
||||
const emit = defineEmits(['change', 'delete', 'upload']);
|
||||
|
||||
const handleChange = () => {
|
||||
emit('change', files.value);
|
||||
};
|
||||
|
||||
const handleDeleteFile = (index) => {
|
||||
emit('delete', index);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.files,
|
||||
(newVal) => {
|
||||
files.value = newVal;
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-box {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
||||
background: var(--BG-200, #f2f3f5);
|
||||
&:hover {
|
||||
background: var(--Primary-1, #e6e6e8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,376 @@
|
||||
<script lang="jsx">
|
||||
import axios from 'axios';
|
||||
import { Form, FormItem, Input, Textarea, Upload, Message as AMessage, Button } from '@arco-design/web-vue';
|
||||
import CommonSelect from '@/components/common-select';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import ImgBox from './img-box';
|
||||
|
||||
import { formatFileSize, getVideoInfo, formatDuration, formatUploadSpeed } from '@/utils/tools';
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts';
|
||||
import { getVideoPreSignedUrlWriter, getImagePreSignedUrlWriter } from '@/api/all/generationWorkshop-writer';
|
||||
|
||||
// import icon1 from '@/assets/img/creative-generation-workshop/icon-close.png';
|
||||
|
||||
// 表单验证规则
|
||||
const FORM_RULES = {
|
||||
title: [{ required: true, message: '请输入标题' }],
|
||||
};
|
||||
export const ENUM_UPLOAD_STATUS = {
|
||||
DEFAULT: 'default',
|
||||
UPLOADING: 'uploading',
|
||||
END: 'end',
|
||||
};
|
||||
|
||||
export const INITIAL_VIDEO_INFO = {
|
||||
name: '',
|
||||
size: '',
|
||||
percent: 0,
|
||||
duration: 0,
|
||||
time: '',
|
||||
uploadSpeed: '0 KB/s',
|
||||
startTime: 0,
|
||||
lastTime: 0,
|
||||
lastLoaded: 0,
|
||||
estimatedTime: 0,
|
||||
poster: '',
|
||||
uploadStatus: ENUM_UPLOAD_STATUS.DEFAULT,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ManuscriptForm',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => FORM_RULES,
|
||||
},
|
||||
formData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['reValidate', 'change', 'update:modelValue', 'updateVideoInfo'],
|
||||
setup(props, { emit, expose }) {
|
||||
const route = useRoute();
|
||||
const formRef = ref(null);
|
||||
const formData = ref({});
|
||||
const uploadRef = ref(null);
|
||||
|
||||
function getFileExtension(filename) {
|
||||
const match = filename.match(/\.([^.]+)$/);
|
||||
return match ? match[1].toLowerCase() : '';
|
||||
}
|
||||
const isVideo = computed(() => formData.value.type === EnumManuscriptType.Video);
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
const setVideoInfo = (file) => {
|
||||
formData.value.videoInfo.percent = 0;
|
||||
formData.value.videoInfo.name = file.name;
|
||||
formData.value.videoInfo.size = formatFileSize(file.size);
|
||||
formData.value.videoInfo.startTime = Date.now();
|
||||
formData.value.videoInfo.lastTime = Date.now();
|
||||
formData.value.videoInfo.lastLoaded = 0;
|
||||
formData.value.videoInfo.uploadSpeed = '0 KB/s';
|
||||
emit('updateVideoInfo', formData.value.videoInfo);
|
||||
|
||||
getVideoInfo(file)
|
||||
.then(({ duration, firstFrame }) => {
|
||||
formData.value.videoInfo.poster = firstFrame;
|
||||
formData.value.videoInfo.duration = Math.floor(duration);
|
||||
formData.value.videoInfo.time = formatDuration(duration);
|
||||
emit('updateVideoInfo', formData.value.videoInfo);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取视频时长失败:', error);
|
||||
});
|
||||
};
|
||||
const handleUploadProgress = (progressEvent) => {
|
||||
const percentCompleted = Math.round(progressEvent.progress * 100);
|
||||
formData.value.videoInfo.percent = percentCompleted;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const currentLoaded = progressEvent.loaded;
|
||||
const totalSize = progressEvent.total;
|
||||
|
||||
if (formData.value.videoInfo.lastLoaded === 0) {
|
||||
formData.value.videoInfo.lastLoaded = currentLoaded;
|
||||
formData.value.videoInfo.lastTime = currentTime;
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDiff = (currentTime - formData.value.videoInfo.lastTime) / 1000;
|
||||
const bytesDiff = currentLoaded - formData.value.videoInfo.lastLoaded;
|
||||
|
||||
// 避免频繁更新,至少间隔200ms计算一次速率
|
||||
if (timeDiff >= 0.2) {
|
||||
const bytesPerSecond = bytesDiff / timeDiff;
|
||||
formData.value.videoInfo.uploadSpeed = formatUploadSpeed(bytesPerSecond);
|
||||
formData.value.videoInfo.lastLoaded = currentLoaded;
|
||||
formData.value.videoInfo.lastTime = currentTime;
|
||||
|
||||
// 计算预估剩余时间
|
||||
if (totalSize && bytesPerSecond > 0) {
|
||||
const remainingBytes = totalSize - currentLoaded;
|
||||
const remainingSeconds = remainingBytes / bytesPerSecond;
|
||||
formData.value.videoInfo.estimatedTime = formatDuration(remainingSeconds);
|
||||
} else {
|
||||
formData.value.videoInfo.estimatedTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
emit('updateVideoInfo', formData.value.videoInfo);
|
||||
};
|
||||
|
||||
const uploadVideo = async (option) => {
|
||||
try {
|
||||
formData.value.videoInfo.uploadStatus = ENUM_UPLOAD_STATUS.UPLOADING;
|
||||
emit('updateVideoInfo', formData.value.videoInfo);
|
||||
|
||||
const {
|
||||
fileItem: { file },
|
||||
} = option;
|
||||
setVideoInfo(file);
|
||||
|
||||
const response = await getVideoPreSignedUrlWriter(writerCode.value, { suffix: getFileExtension(file.name) });
|
||||
const { file_name, upload_url, file_url } = response?.data;
|
||||
if (!upload_url) {
|
||||
throw new Error('未能获取有效的预签名上传地址');
|
||||
}
|
||||
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
await axios.put(upload_url, blob, {
|
||||
headers: { 'Content-Type': file.type },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
handleUploadProgress(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
const { name, duration, size } = formData.value.videoInfo;
|
||||
formData.value.files.push({ url: file_url, name, duration, size });
|
||||
onChange();
|
||||
} finally {
|
||||
formData.value.videoInfo.uploadStatus = ENUM_UPLOAD_STATUS.END;
|
||||
emit('updateVideoInfo', formData.value.videoInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = () => {
|
||||
emit('change', formData.value);
|
||||
};
|
||||
// 文件上传处理
|
||||
const uploadImage = async (option) => {
|
||||
const {
|
||||
fileItem: { file },
|
||||
} = option;
|
||||
|
||||
// 验证文件数量
|
||||
if (formData.value.files?.length >= 18) {
|
||||
AMessage.error('最多只能上传18张图片!');
|
||||
return;
|
||||
}
|
||||
const { name, size, type } = file;
|
||||
const response = await getImagePreSignedUrlWriter(writerCode.value, { suffix: getFileExtension(name) });
|
||||
const { file_name, upload_url, file_url } = response?.data;
|
||||
|
||||
const blob = new Blob([file], { type });
|
||||
await axios.put(upload_url, blob, {
|
||||
headers: { 'Content-Type': type },
|
||||
});
|
||||
|
||||
formData.value.files.push({ url: file_url, name, size });
|
||||
onChange();
|
||||
};
|
||||
|
||||
const handleDeleteFile = (index) => {
|
||||
formData.value.files.splice(index, 1);
|
||||
onChange();
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (errors) {
|
||||
reject(formData.value);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {};
|
||||
formRef.value?.resetFields?.();
|
||||
formRef.value?.clearValidate?.();
|
||||
};
|
||||
|
||||
const renderVideoUpload = () => {
|
||||
return (
|
||||
<Upload
|
||||
ref={uploadRef}
|
||||
action="/"
|
||||
draggable
|
||||
custom-request={uploadVideo}
|
||||
accept=".mp4,.mov,.avi,.flv,.wmv"
|
||||
show-file-list={false}
|
||||
>
|
||||
{{
|
||||
'upload-button': () => {
|
||||
if (formData.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.DEFAULT) {
|
||||
return (
|
||||
<div class="upload-box">
|
||||
<icon-plus size="14" class="mb-16px color-#3C4043" />
|
||||
<span class="cts !color-#211F24">上传视频</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <Button type="text">替换视频</Button>;
|
||||
}
|
||||
},
|
||||
}}
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
const renderVideo = () => {
|
||||
const isUploading = formData.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING;
|
||||
const isEnd = formData.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.END;
|
||||
return (
|
||||
<FormItem
|
||||
field="files"
|
||||
v-slots={{
|
||||
label: () => (
|
||||
<div class="flex items-center">
|
||||
<span class="cts !color-#211F24 mr-8px">视频</span>
|
||||
<span class="cts !color-#939499">截取视频第一帧为首图</span>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{formData.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.DEFAULT ? (
|
||||
renderVideoUpload()
|
||||
) : (
|
||||
<div class="flex items-center justify-between p-12px rounded-8px bg-#F7F8FA w-784px">
|
||||
<div class="flex items-center mr-12px">
|
||||
{isUploading ? (
|
||||
<div class="w-80px h-80px flex items-center justify-center bg-#fff rounded-8px mr-16px">
|
||||
<icon-loading size="24" class="color-#B1B2B5" />
|
||||
</div>
|
||||
) : (
|
||||
<img src={formData.value.videoInfo.poster} class="w-80 h-80 object-cover mr-16px rounded-8px" />
|
||||
)}
|
||||
<div class="flex flex-col">
|
||||
<TextOverTips
|
||||
context={formData.value.videoInfo.name}
|
||||
class="mb-4px cts !text-14px !lh-22px color-#211F24"
|
||||
/>
|
||||
{isEnd ? (
|
||||
<p>
|
||||
<span class="cts color-#939499 mr-24px">视频大小:{formData.value.videoInfo.size}</span>
|
||||
<span class="cts color-#939499">视频时长:{formData.value.videoInfo.time}</span>
|
||||
</p>
|
||||
) : (
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center mr-24px w-100px">
|
||||
<icon-loading size="16" class="color-#6D4CFE mr-8px" />
|
||||
<span class="cts !color-#6D4CFE mr-4px">上传中</span>
|
||||
<span class="cts !color-#6D4CFE ">{formData.value.videoInfo.percent}%</span>
|
||||
</div>
|
||||
<div class="flex items-center w-160px mr-24px">
|
||||
<span class="cts color-#939499">上传速度:{formData.value.videoInfo.uploadSpeed}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="cts color-#939499">预估剩余时间:{formData.value.videoInfo.estimatedTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{renderVideoUpload()}</div>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
const handleImagesChange = (files) => {
|
||||
formData.value.files = cloneDeep(files);
|
||||
onChange();
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
expose({
|
||||
validate,
|
||||
resetForm,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formData,
|
||||
(val) => {
|
||||
formData.value = val;
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
return () => (
|
||||
<Form ref={formRef} model={formData.value} rules={props.rules} layout="vertical" auto-label-width>
|
||||
<FormItem label="标题" field="title" required>
|
||||
<Input
|
||||
v-model={formData.value.title}
|
||||
onInput={() => {
|
||||
onChange();
|
||||
emit('reValidate');
|
||||
}}
|
||||
placeholder="请输入标题"
|
||||
size="large"
|
||||
class="!w-500px"
|
||||
maxLength={30}
|
||||
show-word-limit
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="作品描述" field="content">
|
||||
<Textarea
|
||||
v-model={formData.value.content}
|
||||
onInput={onChange}
|
||||
placeholder="请输入作品描述"
|
||||
size="large"
|
||||
class="h-300px !w-784px"
|
||||
show-word-limit
|
||||
max-length={1000}
|
||||
auto-size={{ minRows: 7, maxRows: 12 }}
|
||||
/>
|
||||
</FormItem>
|
||||
{isVideo.value ? (
|
||||
renderVideo()
|
||||
) : (
|
||||
<ImgBox
|
||||
files={formData.value.files}
|
||||
onChange={handleImagesChange}
|
||||
onDelete={handleDeleteFile}
|
||||
onUpload={uploadImage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <FormItem label="所属项目" field="project_ids">
|
||||
<CommonSelect
|
||||
v-model={formData.value.project_ids}
|
||||
onChange={() => emit('change')}
|
||||
options={projects.value}
|
||||
placeholder="请选择所属项目"
|
||||
size="large"
|
||||
class="!w-280px"
|
||||
/>
|
||||
</FormItem>*/}
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
.cts {
|
||||
font-family: $font-family-regular;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
.upload-box {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
||||
background: var(--BG-200, #f2f3f5);
|
||||
&:hover {
|
||||
background: var(--Primary-1, #e6e6e8);
|
||||
}
|
||||
}
|
||||
.group {
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
.group-container {
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
|
||||
url(<path-to-image>) lightgray 0px -40.771px / 100% 149.766% no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,379 @@
|
||||
<script lang="jsx">
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Upload,
|
||||
Button,
|
||||
Message as AMessage,
|
||||
Textarea,
|
||||
} from '@arco-design/web-vue';
|
||||
import {
|
||||
getTemplateUrlWriter,
|
||||
postWorksByLinkWriter,
|
||||
postWorksByFileWriter,
|
||||
} from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { getTemplateUrl } from '@/api/all/generationWorkshop';
|
||||
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import icon1 from '@/assets/img/media-account/icon-feedback-fail.png';
|
||||
|
||||
// 状态枚举
|
||||
const TASK_STATUS = {
|
||||
DEFAULT: 1,
|
||||
LOADING: 2,
|
||||
FAILED: 3,
|
||||
SUCCESS: 4,
|
||||
};
|
||||
|
||||
const UPLOAD_TYPE = {
|
||||
LINK: 'link',
|
||||
LOCAL: 'local',
|
||||
};
|
||||
|
||||
// 初始表单数据
|
||||
const INITIAL_FORM = {
|
||||
link: '',
|
||||
writerLink: '',
|
||||
};
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const update = inject('update');
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 响应式状态
|
||||
const visible = ref(false);
|
||||
const formRef = ref(null);
|
||||
const uploadType = ref(UPLOAD_TYPE.LOCAL);
|
||||
const taskStatus = ref(TASK_STATUS.DEFAULT);
|
||||
const form = ref(cloneDeep(INITIAL_FORM));
|
||||
const works = ref([]);
|
||||
const uploadingFiles = ref([]); // 上传中
|
||||
const uploadSuccessFiles = ref([]); // 上传成功
|
||||
|
||||
// 生成自增 id(基于当前列表中最大的 id)
|
||||
const getNextWorkId = () => {
|
||||
const currentMax = works.value.reduce((max, item) => {
|
||||
const numericId = Number(item?.id);
|
||||
return Number.isFinite(numericId) ? Math.max(max, numericId) : max;
|
||||
}, 0);
|
||||
return currentMax + 1;
|
||||
};
|
||||
|
||||
const isLink = computed(() => uploadType.value === UPLOAD_TYPE.LINK);
|
||||
const isLocal = computed(() => uploadType.value === UPLOAD_TYPE.LOCAL);
|
||||
const isDefault = computed(() => taskStatus.value === TASK_STATUS.DEFAULT);
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
// 模态框标题
|
||||
const getTitle = () => {
|
||||
const titleMap = {
|
||||
[TASK_STATUS.DEFAULT]: '上传内容稿件',
|
||||
[TASK_STATUS.LOADING]: isLink.value ? '链接上传' : isLocal.value ? '本地批量上传' : '上传内容稿件',
|
||||
[TASK_STATUS.FAILED]: isLink.value ? '链接上传' : isLocal.value ? '本地批量上传' : '上传内容稿件',
|
||||
[TASK_STATUS.SUCCESS]: '上传内容稿件列表',
|
||||
};
|
||||
return titleMap[taskStatus.value];
|
||||
};
|
||||
|
||||
// 重置状态
|
||||
const reset = () => {
|
||||
formRef.value?.resetFields?.();
|
||||
uploadType.value = UPLOAD_TYPE.LINK;
|
||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
||||
form.value = cloneDeep(INITIAL_FORM);
|
||||
works.value = [];
|
||||
uploadingFiles.value = [];
|
||||
uploadSuccessFiles.value = [];
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
reset();
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 防抖提交
|
||||
const debouncedSubmit = debounce(async () => {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
taskStatus.value = TASK_STATUS.LOADING;
|
||||
const { link } = form.value;
|
||||
const { code, data } = await postWorksByLinkWriter(writerCode.value, { link });
|
||||
if (code === 200) {
|
||||
taskStatus.value = TASK_STATUS.SUCCESS;
|
||||
works.value = data ? [data] : [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 提交处理
|
||||
const onSubmit = () => {
|
||||
debouncedSubmit();
|
||||
};
|
||||
|
||||
// 取消上传
|
||||
const onCancelUpload = () => {
|
||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
||||
AMessage.info('已取消上传');
|
||||
};
|
||||
|
||||
// 文件上传处理
|
||||
const handleUpload = async (option) => {
|
||||
taskStatus.value = TASK_STATUS.LOADING;
|
||||
|
||||
const {
|
||||
fileItem: { file },
|
||||
} = option;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
uploadingFiles.value.push(file);
|
||||
|
||||
const { code, data } = await postWorksByFileWriter(formData, {
|
||||
timeout: 0,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'writer-code': writerCode.value,
|
||||
},
|
||||
});
|
||||
if (code === 200) {
|
||||
if (data) {
|
||||
const id = data.id ?? getNextWorkId();
|
||||
const _data = { ...data, id };
|
||||
works.value.push(_data);
|
||||
uploadSuccessFiles.value.push(_data);
|
||||
}
|
||||
|
||||
if (uploadingFiles.value.length === uploadSuccessFiles.value.length) {
|
||||
taskStatus.value = TASK_STATUS.SUCCESS;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转编辑
|
||||
const goUpload = () => {
|
||||
slsWithCatch('writerWaitUploadWorks', JSON.stringify(works.value));
|
||||
router.push({
|
||||
name: 'WriterManuscriptUpload',
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 删除项目
|
||||
const onDelete = (index) => {
|
||||
works.value.splice(index, 1);
|
||||
if (!works.value.length) {
|
||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
||||
}
|
||||
// AMessage.success('删除成功');
|
||||
};
|
||||
|
||||
// 上传方式切换
|
||||
const onUploadTypeChange = (val) => {
|
||||
uploadType.value = val;
|
||||
formRef.value?.clearValidate?.();
|
||||
};
|
||||
|
||||
// 下载模板
|
||||
const handleDownloadTemplate = async () => {
|
||||
const { code, data } = await getTemplateUrlWriter(writerCode.value);
|
||||
if (code === 200) {
|
||||
window.open(data.download_url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染链接上传表单
|
||||
const renderLinkForm = () => (
|
||||
<FormItem label="链接地址" field="link" required>
|
||||
<Textarea
|
||||
v-model={form.value.link}
|
||||
size="large"
|
||||
placeholder="请输入飞书链接地址"
|
||||
autoSize={{ minRows: 5, maxRows: 8 }}
|
||||
/>
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
// 渲染本地上传表单
|
||||
const renderLocalForm = () => (
|
||||
<FormItem label="内容稿件">
|
||||
<div class="flex flex-col w-full">
|
||||
<Upload
|
||||
action="/"
|
||||
draggable
|
||||
multiple
|
||||
customRequest={handleUpload}
|
||||
accept=".xlsx,.xls,.docx,.doc,.mp4,.mov,.avi,.flv,.wmv,.m4v"
|
||||
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 renderLoadingState = () => (
|
||||
<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>
|
||||
);
|
||||
|
||||
// 渲染失败状态
|
||||
const renderFailedState = () => (
|
||||
<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>
|
||||
);
|
||||
|
||||
// 渲染成功状态
|
||||
const renderSuccessState = () => (
|
||||
<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">共 {works.value.length} 个内容稿件</span>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden px-12px">
|
||||
{works.value.map((item, index) => (
|
||||
<div key={item.id} 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 src={item.cover} width="32" height="32" class="rounded-3px mr-8px" />
|
||||
<TextOverTips class="text !text-left" context={item.title} />
|
||||
</div>
|
||||
<icon-delete
|
||||
size="16px"
|
||||
class="color-#737478 cursor-pointer hover:!color-#211F24"
|
||||
onClick={() => onDelete(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染表单内容
|
||||
const renderFormContent = () => {
|
||||
const contentMap = {
|
||||
[TASK_STATUS.DEFAULT]: () => {
|
||||
const formMap = {
|
||||
[UPLOAD_TYPE.LINK]: renderLinkForm,
|
||||
[UPLOAD_TYPE.LOCAL]: renderLocalForm,
|
||||
};
|
||||
return formMap[uploadType.value]?.();
|
||||
},
|
||||
[TASK_STATUS.LOADING]: renderLoadingState,
|
||||
[TASK_STATUS.FAILED]: renderFailedState,
|
||||
[TASK_STATUS.SUCCESS]: renderSuccessState,
|
||||
};
|
||||
|
||||
return contentMap[taskStatus.value]?.();
|
||||
};
|
||||
|
||||
// 渲染底部按钮
|
||||
const renderFooterButtons = () => {
|
||||
const buttonMap = {
|
||||
[TASK_STATUS.LOADING]: () => (
|
||||
<Button type="primary" size="medium" onClick={onCancelUpload}>
|
||||
取消上传
|
||||
</Button>
|
||||
),
|
||||
[TASK_STATUS.DEFAULT]: () => (
|
||||
<>
|
||||
<Button size="medium" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={onSubmit}>
|
||||
确认
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
[TASK_STATUS.FAILED]: () => (
|
||||
<>
|
||||
<Button size="medium" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={onClose}>
|
||||
重新上传
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
[TASK_STATUS.SUCCESS]: () => (
|
||||
<>
|
||||
<Button size="medium" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={goUpload}>
|
||||
确认
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
return buttonMap[taskStatus.value]?.();
|
||||
};
|
||||
|
||||
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={!(isDefault.value && isLocal.value)}
|
||||
v-slots={{
|
||||
footer: () => renderFooterButtons(),
|
||||
}}
|
||||
>
|
||||
<Form ref={formRef} model={form.value} layout="horizontal" auto-label-width>
|
||||
{/* {isDefault.value && (
|
||||
<FormItem label="上传方式">
|
||||
<RadioGroup v-model={uploadType.value} onChange={onUploadTypeChange}>
|
||||
<Radio value={UPLOAD_TYPE.LINK}>外部链接上传</Radio>
|
||||
<Radio value={UPLOAD_TYPE.LOCAL}>本地上传</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
)} */}
|
||||
|
||||
{renderFormContent()}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,222 @@
|
||||
<script lang="jsx">
|
||||
import { Button, Message as AMessage, Spin } from '@arco-design/web-vue';
|
||||
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { AuditStatus } from '@/views/writer-material-center/components/finished-products/constants';
|
||||
import { getWorksDetailWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts';
|
||||
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
|
||||
const DEFAULT_SOURCE_INFO = {
|
||||
title: '成品库',
|
||||
routeName: 'WriterMaterialCenterFinishedProducts',
|
||||
};
|
||||
const SOURCE_MAP = new Map([['check', { title: '成品库', routeName: 'WriterMaterialCenterFinishedProducts' }]]);
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const sidebarStore = useSidebarStore();
|
||||
const workId = ref(route.params.id);
|
||||
const { source, audit_status } = route.query;
|
||||
|
||||
// 视频播放相关状态
|
||||
const isPlaying = ref(false);
|
||||
const videoRef = ref(null);
|
||||
const videoUrl = ref('');
|
||||
const coverImageUrl = ref('');
|
||||
const isVideoLoaded = ref(false);
|
||||
const dataSource = ref({});
|
||||
const images = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isVideo = computed(() => dataSource.value.type === EnumManuscriptType.Video);
|
||||
const sourceInfo = computed(() => SOURCE_MAP.get(source) ?? DEFAULT_SOURCE_INFO);
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
const collapsed = computed(() => {
|
||||
return sidebarStore.menuCollapse;
|
||||
});
|
||||
|
||||
const onBack = () => {
|
||||
router.push({
|
||||
name: sourceInfo.value.routeName,
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
const [fileOne, ...fileOthers] = dataSource.value.files ?? [];
|
||||
if (isVideo.value) {
|
||||
videoUrl.value = fileOne.url;
|
||||
coverImageUrl.value = convertVideoUrlToCoverUrl(fileOne.url);
|
||||
} else {
|
||||
coverImageUrl.value = fileOne.url;
|
||||
images.value = fileOthers;
|
||||
}
|
||||
};
|
||||
|
||||
const getData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { code, data } = await getWorksDetailWriter(writerCode.value, workId.value);
|
||||
if (code === 200) {
|
||||
dataSource.value = data;
|
||||
initData();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderMainImg = () => {
|
||||
if (!coverImageUrl.value) return null;
|
||||
|
||||
if (isVideo.value) {
|
||||
return (
|
||||
<div class="main-video-box mb-16px relative overflow-hidden cursor-pointer" onClick={togglePlay}>
|
||||
<video ref={videoRef} class="w-100% h-100% object-contain" onEnded={onVideoEnded}></video>
|
||||
{!isPlaying.value && (
|
||||
<>
|
||||
<img src={coverImageUrl.value} class="w-100% h-100% object-contain absolute z-0 top-0 left-0" />
|
||||
<div v-show={!isPlaying.value} class="play-icon"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div class="main-img-box mb-16px relative overflow-hidden cursor-pointer">
|
||||
<img src={coverImageUrl.value} class="w-100% h-100% object-contain" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlay = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
if (isPlaying.value) {
|
||||
videoRef.value.pause();
|
||||
} else {
|
||||
if (!isVideoLoaded.value) {
|
||||
videoRef.value.src = videoUrl.value;
|
||||
isVideoLoaded.value = true;
|
||||
}
|
||||
videoRef.value.play();
|
||||
}
|
||||
isPlaying.value = !isPlaying.value;
|
||||
};
|
||||
|
||||
const onVideoEnded = () => {
|
||||
isPlaying.value = false;
|
||||
};
|
||||
|
||||
const renderFooterRow = () => {
|
||||
const _fn = () => {
|
||||
slsWithCatch('writerManuscriptCheckIds', [workId.value]);
|
||||
router.push({
|
||||
name: 'WriterManuscriptCheck',
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="medium" type="outline" class="mr-12px" onClick={onBack}>
|
||||
退出
|
||||
</Button>
|
||||
<Button
|
||||
size="medium"
|
||||
type="outline"
|
||||
class="mr-12px"
|
||||
onClick={() => {
|
||||
router.push({
|
||||
name: 'WriterManuscriptEdit',
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
id: workId.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{audit_status !== AuditStatus.Passed && (
|
||||
<Button type="primary" size="medium" onClick={_fn}>
|
||||
去审核
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
workId && getData();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.pause();
|
||||
videoRef.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
return () => (
|
||||
<Spin loading={loading.value} class="manuscript-detail-wrap" size={50}>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<div class="flex items-center mb-8px">
|
||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onBack}>
|
||||
{sourceInfo.value.title}
|
||||
</span>
|
||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
||||
<span class="cts bold !color-#1D2129">内容稿件详情</span>
|
||||
</div>
|
||||
<div class="flex-1 bg-#fff rounded-8px py-32px">
|
||||
<div class="w-684px mx-auto flex flex-col items-center">
|
||||
<div class="flex justify-start flex-col w-full">
|
||||
<p class="mb-8px cts bold !text-28px !lh-40px !color-#211F24">{dataSource.value.title}</p>
|
||||
<p class="cts !text-12px !color-#737478 mb-32px w-full text-left">
|
||||
{exactFormatTime(dataSource.value.update_time)}修改
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{renderMainImg()}
|
||||
<div class="w-full">
|
||||
<p class="cts !color-#211F24 whitespace-pre-line">{dataSource.value.content}</p>
|
||||
</div>
|
||||
|
||||
{/* 仅图片类型显示图片列表 */}
|
||||
{!isVideo.value && (
|
||||
<div class="desc-img-wrap mt-40px">
|
||||
{images.value.map((item, index) => (
|
||||
<div class="desc-img-box" key={index}>
|
||||
<img src={item.url} class="w-100% h-100% object-contain" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
||||
collapsed.value ? 'collapsed' : ''
|
||||
}`}
|
||||
>
|
||||
{renderFooterRow()}
|
||||
</footer>
|
||||
</Spin>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,69 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-detail-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
||||
.cts {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.main-video-box {
|
||||
width: 320px;
|
||||
height: 472px;
|
||||
background: #333;
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
.main-img-box {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
max-height: 472px;
|
||||
background: #fff;
|
||||
}
|
||||
.desc-img-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
.desc-img-box {
|
||||
width: 212px;
|
||||
height: 283px;
|
||||
background: #fff;
|
||||
object-fit: contain;
|
||||
aspect-ratio: 3/4;
|
||||
}
|
||||
}
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 222;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: background-image 0.3s ease;
|
||||
}
|
||||
|
||||
.play-icon:hover {
|
||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play-hover.png');
|
||||
}
|
||||
}
|
||||
.footer-row {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: $sidebar-width;
|
||||
width: calc(100% - $sidebar-width);
|
||||
height: $footer-height;
|
||||
&.collapsed {
|
||||
left: $sidebar-width-collapse;
|
||||
width: calc(100% - $sidebar-width-collapse);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="退出编辑" width="480px" @close="onClose">
|
||||
<div class="flex items-center">
|
||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
||||
<span>内容已修改尚未保存,若退出编辑,本次修改将不保存。</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">确认退出</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
|
||||
const onClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
const onConfirm = () => {
|
||||
onClose();
|
||||
router.push({ name: 'WriterMaterialCenterFinishedProducts', params: { writerCode: route.params.writerCode } });
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,143 @@
|
||||
<script lang="jsx">
|
||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
||||
import EditForm, { ENUM_UPLOAD_STATUS, INITIAL_VIDEO_INFO } from '../components/edit-form';
|
||||
import CancelEditModal from './cancel-edit-modal.vue';
|
||||
|
||||
import { getWorksDetailWriter, putWorksUpdateWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts';
|
||||
import { formatDuration, formatFileSize, convertVideoUrlToCoverUrl } from '@/utils/tools';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditForm,
|
||||
},
|
||||
setup(props, { emit, expose }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const sidebarStore = useSidebarStore();
|
||||
|
||||
const formRef = ref(null);
|
||||
const cancelEditModal = ref(null);
|
||||
const dataSource = ref({});
|
||||
const remoteDataSource = ref({});
|
||||
const uploadLoading = ref(false);
|
||||
const isSaved = ref(false);
|
||||
|
||||
const workId = ref(route.params.id);
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
const collapsed = computed(() => {
|
||||
return sidebarStore.menuCollapse;
|
||||
});
|
||||
|
||||
const onCancel = () => {
|
||||
const isModified = !isEqual(dataSource.value, remoteDataSource.value);
|
||||
if (isModified && !isSaved.value) {
|
||||
cancelEditModal.value?.open();
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async (check = false) => {
|
||||
formRef.value?.validate().then(async () => {
|
||||
if (dataSource.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING) {
|
||||
AMessage.warning('有视频正在上传中,请等待上传完成后再提交');
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredWorks = omit(dataSource.value, 'videoInfo');
|
||||
const { code, data } = await putWorksUpdateWriter(writerCode.value, { id: workId.value, ...filteredWorks });
|
||||
if (code === 200) {
|
||||
AMessage.success('保存成功');
|
||||
isSaved.value = true;
|
||||
|
||||
if (check) {
|
||||
slsWithCatch('writerManuscriptCheckIds', [workId.value]);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: writerCode.value } });
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const getData = async () => {
|
||||
const { code, data } = await getWorksDetailWriter(writerCode.value, workId.value);
|
||||
if (code === 200) {
|
||||
const { type, files } = data;
|
||||
const _data = { ...data, videoInfo: cloneDeep(INITIAL_VIDEO_INFO) };
|
||||
|
||||
// 初始化视频数据
|
||||
if (type === EnumManuscriptType.Video && files.length) {
|
||||
_data.videoInfo.uploadStatus = 'end';
|
||||
const { name, size, duration, url } = files[0];
|
||||
_data.videoInfo.name = name;
|
||||
_data.videoInfo.size = formatFileSize(size);
|
||||
_data.videoInfo.time = formatDuration(duration);
|
||||
_data.videoInfo.poster = convertVideoUrlToCoverUrl(url);
|
||||
}
|
||||
|
||||
dataSource.value = cloneDeep(_data);
|
||||
remoteDataSource.value = cloneDeep(_data);
|
||||
}
|
||||
};
|
||||
const onChange = (val) => {
|
||||
dataSource.value = val;
|
||||
};
|
||||
const onUpdateVideoInfo = (newVideoInfo) => {
|
||||
dataSource.value.videoInfo = { ...dataSource.value.videoInfo, ...newVideoInfo };
|
||||
};
|
||||
const onBack = () => {
|
||||
router.push({ name: 'WriterMaterialCenterFinishedProducts', params: { writerCode: writerCode.value } });
|
||||
};
|
||||
onMounted(() => {
|
||||
workId && getData();
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="manuscript-edit-wrap">
|
||||
<div class="flex items-center mb-8px">
|
||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onCancel}>
|
||||
成品库
|
||||
</span>
|
||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
||||
<span class="cts bold !color-#1D2129">编辑内容稿件</span>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-24px bg-#fff rounded-8px ">
|
||||
<EditForm
|
||||
ref={formRef}
|
||||
formData={dataSource.value}
|
||||
onChange={onChange}
|
||||
onUpdateVideoInfo={onUpdateVideoInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
||||
collapsed.value ? 'collapsed' : ''
|
||||
}`}
|
||||
>
|
||||
{' '}
|
||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
||||
退出
|
||||
</Button>
|
||||
<Button size="medium" type="outline" onClick={() => onSave()} class="mr-12px">
|
||||
保存
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={() => onSave(true)} loading={uploadLoading.value}>
|
||||
保存并审核
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
<CancelEditModal ref={cancelEditModal} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,28 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-edit-wrap {
|
||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.cts {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer-row {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: $sidebar-width;
|
||||
width: calc(100% - $sidebar-width);
|
||||
height: $footer-height;
|
||||
&.collapsed {
|
||||
left: $sidebar-width-collapse;
|
||||
width: calc(100% - $sidebar-width-collapse);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="common-filter-wrap">
|
||||
<div class="filter-row">
|
||||
<div class="filter-row-item">
|
||||
<span class="label">内容稿件标题</span>
|
||||
<a-input
|
||||
v-model="query.title"
|
||||
class="!w-240px"
|
||||
placeholder="请输入内容稿件标题"
|
||||
size="medium"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<!-- <div class="filter-row-item">
|
||||
<span class="label">所属项目</span>
|
||||
<CommonSelect
|
||||
placeholder="请选择所属项目"
|
||||
v-model="query.project_id"
|
||||
:options="projects"
|
||||
class="!w-166px"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</div> -->
|
||||
<div class="filter-row-item">
|
||||
<span class="label">序号</span>
|
||||
<a-space size="medium">
|
||||
<a-input
|
||||
v-model="query.uid"
|
||||
class="!w-160px"
|
||||
placeholder="请输入序号"
|
||||
size="medium"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="filter-row-item">
|
||||
<span class="label">审核状态</span>
|
||||
<CommonSelect
|
||||
placeholder="全部"
|
||||
:options="CHECK_STATUS"
|
||||
v-model="query.audit_status"
|
||||
class="!w-160px"
|
||||
:multiple="false"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-row-item">
|
||||
<span class="label">上传时间</span>
|
||||
<a-range-picker
|
||||
v-model="created_at"
|
||||
size="medium"
|
||||
allow-clear
|
||||
format="YYYY-MM-DD"
|
||||
class="!w-280px"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-row-item">
|
||||
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
<template #default>搜索</template>
|
||||
</a-button>
|
||||
<a-button size="medium" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, defineProps } from 'vue';
|
||||
import { CHECK_STATUS } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
||||
import CommonSelect from '@/components/common-select';
|
||||
// import { getProjectList } from '@/api/all/propertyMarketing';
|
||||
|
||||
const props = defineProps({
|
||||
query: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits('search', 'reset', 'update:query');
|
||||
|
||||
const created_at = ref([]);
|
||||
// const projects = ref([]);
|
||||
|
||||
const handleSearch = () => {
|
||||
emits('update:query', props.query);
|
||||
nextTick(() => {
|
||||
emits('search');
|
||||
});
|
||||
};
|
||||
|
||||
const onDateChange = (value) => {
|
||||
if (!value) {
|
||||
props.query.created_at = [];
|
||||
handleSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const [start, end] = value;
|
||||
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
|
||||
props.query.created_at = [
|
||||
dayjs(start).startOf('day').format(FORMAT_DATE),
|
||||
dayjs(end).endOf('day').format(FORMAT_DATE),
|
||||
];
|
||||
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 获取项目列表
|
||||
// const getProjects = async () => {
|
||||
// try {
|
||||
// const { code, data } = await getProjectList();
|
||||
// if (code === 200) {
|
||||
// projects.value = data;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('获取项目列表失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleReset = () => {
|
||||
created_at.value = [];
|
||||
// projects.value = [];
|
||||
emits('reset');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// getProjects();
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,68 @@
|
||||
export const TABLE_COLUMNS = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'uid',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '图片/视频',
|
||||
dataIndex: 'cover',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '内容稿件标题',
|
||||
dataIndex: 'title',
|
||||
width: 240,
|
||||
},
|
||||
// {
|
||||
// title: '所属项目',
|
||||
// dataIndex: 'projects',
|
||||
// width: 240,
|
||||
// },
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'audit_status',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '上传人员',
|
||||
// dataIndex: 'uploader',
|
||||
// width: 180,
|
||||
// },
|
||||
{
|
||||
title: '最后修改时间',
|
||||
dataIndex: 'last_modified_at',
|
||||
width: 180,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '最后修改人员',
|
||||
// dataIndex: 'last_modifier',
|
||||
// width: 180,
|
||||
// },
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="删除稿件" width="480px" @close="onClose">
|
||||
<div class="flex items-center">
|
||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
||||
<span>确认删除 {{ projectName }} 这个稿件吗?</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="large" @click="onClose">取消</a-button>
|
||||
<a-button type="primary" class="ml-16px" status="danger" size="large" @click="onDelete">确认删除</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { deleteWorkWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
|
||||
const update = inject('update');
|
||||
const route = useRoute();
|
||||
|
||||
const visible = ref(false);
|
||||
const projectId = ref(null);
|
||||
const projectName = ref('');
|
||||
|
||||
const isBatch = computed(() => Array.isArray(projectId.value));
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
projectId.value = null;
|
||||
projectName.value = '';
|
||||
}
|
||||
|
||||
const open = (record) => {
|
||||
const { id = null, name = '' } = record;
|
||||
projectId.value = id;
|
||||
projectName.value = name;
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
async function onDelete() {
|
||||
const { code } = await deleteWorkWriter(writerCode.value, projectId.value);
|
||||
if (code === 200) {
|
||||
AMessage.success('删除成功');
|
||||
update();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<a-table
|
||||
ref="tableRef"
|
||||
:data="dataSource"
|
||||
row-key="id"
|
||||
column-resizable
|
||||
:pagination="false"
|
||||
:scroll="{ x: '100%' }"
|
||||
class="manuscript-table w-100%"
|
||||
bordered
|
||||
@sorter-change="handleSorterChange"
|
||||
>
|
||||
<template #empty>
|
||||
<NoData text="暂无稿件" />
|
||||
</template>
|
||||
<template #columns>
|
||||
<a-table-column
|
||||
v-for="column in TABLE_COLUMNS"
|
||||
:key="column.dataIndex"
|
||||
:data-index="column.dataIndex"
|
||||
:fixed="column.fixed"
|
||||
:width="column.width"
|
||||
:min-width="column.minWidth"
|
||||
:sortable="column.sortable"
|
||||
:align="column.align"
|
||||
ellipsis
|
||||
tooltip
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<span class="cts mr-4px">{{ column.title }}</span>
|
||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'create_at'" #cell="{ record }">
|
||||
{{ exactFormatTime(record.create_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'title'" #cell="{ record }">
|
||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'audit_status'" #cell="{ record }">
|
||||
<div
|
||||
class="flex items-center w-fit h-28px px-8px rounded-2px"
|
||||
:style="{ backgroundColor: getStatusInfo(record.audit_status).backgroundColor }"
|
||||
>
|
||||
<span class="cts s1" :style="{ color: getStatusInfo(record.audit_status).color }">{{
|
||||
getStatusInfo(record.audit_status).name
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
||||
width="16"
|
||||
height="16"
|
||||
class="mr-4px"
|
||||
/>
|
||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="['uploader', 'last_modifier'].includes(column.dataIndex)" #cell="{ record }">
|
||||
{{ record[column.dataIndex].name || record[column.dataIndex].mobile }}
|
||||
</template>
|
||||
<template v-else-if="['created_at', 'last_modified_at'].includes(column.dataIndex)" #cell="{ record }">
|
||||
{{ exactFormatTime(record[column.dataIndex]) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
||||
<HoverImagePreview :src="record.cover">
|
||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
||||
<template #error>
|
||||
<img :src="icon4" class="w-full h-full" />
|
||||
</template>
|
||||
</a-image>
|
||||
</HoverImagePreview>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
||||
<div class="flex items-center">
|
||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else #cell="{ record }">
|
||||
{{ formatTableField(column, record, true) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
||||
import { TABLE_COLUMNS } from './constants';
|
||||
import { CHECK_STATUS, EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants';
|
||||
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import HoverImagePreview from '@/components/hover-image-preview';
|
||||
|
||||
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';
|
||||
import icon4 from '@/assets/img/error-img.png';
|
||||
|
||||
const emits = defineEmits(['edit', 'sorterChange', 'delete']);
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
dataSource: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const tableRef = ref(null);
|
||||
const route = useRoute();
|
||||
|
||||
const handleSorterChange = (column, order) => {
|
||||
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
|
||||
};
|
||||
const onDelete = (item) => {
|
||||
emits('delete', item);
|
||||
};
|
||||
const onEdit = (item) => {
|
||||
router.push({
|
||||
name: 'WriterManuscriptEdit',
|
||||
params: {
|
||||
writerCode: route.params.writerCode,
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onDetail = (item) => {
|
||||
router.push({
|
||||
name: 'WriterManuscriptDetail',
|
||||
params: {
|
||||
writerCode: route.params.writerCode,
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusInfo = (audit_status) => {
|
||||
return CHECK_STATUS.find((v) => v.id === audit_status) ?? {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,16 @@
|
||||
.manuscript-table {
|
||||
.cts {
|
||||
color: var(--Text-1, #211f24);
|
||||
font-family: $font-family-medium;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
:deep(.title) {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #6d4cfe;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
export const INITIAL_QUERY = {
|
||||
title: '',
|
||||
// project_ids: [],
|
||||
uid: '',
|
||||
audit_status: '',
|
||||
created_at: [],
|
||||
sort_column: undefined,
|
||||
sort_order: undefined,
|
||||
};
|
||||
|
||||
export enum EnumCheckStatus {
|
||||
All = '',
|
||||
Wait = 1,
|
||||
Checking = 2,
|
||||
Passed = 3,
|
||||
}
|
||||
export enum EnumManuscriptType {
|
||||
All = '',
|
||||
Image = 0,
|
||||
Video = 1,
|
||||
}
|
||||
|
||||
export const CHECK_STATUS = [
|
||||
{
|
||||
name: '待审核',
|
||||
id: EnumCheckStatus.Wait,
|
||||
backgroundColor: '#F2F3F5',
|
||||
color: '#3C4043'
|
||||
},
|
||||
{
|
||||
name: '审核中',
|
||||
id: EnumCheckStatus.Checking,
|
||||
backgroundColor: '#FFF7E5',
|
||||
color: '#FFAE00'
|
||||
},
|
||||
{
|
||||
name: '已通过',
|
||||
id: EnumCheckStatus.Passed,
|
||||
backgroundColor: '#EBF7F2',
|
||||
color: '#25C883'
|
||||
},
|
||||
];
|
||||
export const MANUSCRIPT_TYPE = [
|
||||
{
|
||||
label: '全部',
|
||||
value: EnumManuscriptType.All,
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: EnumManuscriptType.Image,
|
||||
},
|
||||
{
|
||||
label: '视频',
|
||||
value: EnumManuscriptType.Video,
|
||||
},
|
||||
]
|
||||
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="manuscript-list-wrap">
|
||||
<div class="filter-wrap bg-#fff rounded-b-8px mb-16px">
|
||||
<FilterBlock v-model:query="query" @search="handleSearch" @reset="handleReset" />
|
||||
</div>
|
||||
<div class="table-wrap bg-#fff rounded-8px px-24px py-24px flex flex-col">
|
||||
<ManuscriptTable :dataSource="dataSource" @sorterChange="handleSorterChange" @delete="handleDelete" />
|
||||
<div v-if="pageInfo.total > 0" class="pagination-row">
|
||||
<a-pagination
|
||||
:total="pageInfo.total"
|
||||
size="mini"
|
||||
show-total
|
||||
show-jumper
|
||||
show-page-size
|
||||
:current="pageInfo.page"
|
||||
:page-size="pageInfo.page_size"
|
||||
@change="onPageChange"
|
||||
@page-size-change="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteManuscriptModal ref="deleteManuscriptModalRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="jsx" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import { Button } from '@arco-design/web-vue';
|
||||
import FilterBlock from './components/filter-block';
|
||||
import ManuscriptTable from './components/manuscript-table';
|
||||
import DeleteManuscriptModal from './components/manuscript-table/delete-manuscript-modal.vue';
|
||||
|
||||
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
|
||||
import { getWorksPageWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import {
|
||||
INITIAL_QUERY,
|
||||
EnumCheckStatus,
|
||||
} from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts';
|
||||
|
||||
const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } = useTableSelectionWithPagination({
|
||||
onPageChange: () => {
|
||||
getData();
|
||||
},
|
||||
onPageSizeChange: () => {
|
||||
getData();
|
||||
},
|
||||
});
|
||||
const route = useRoute();
|
||||
const query = ref(cloneDeep(INITIAL_QUERY));
|
||||
const addManuscriptModalRef = ref(null);
|
||||
const deleteManuscriptModalRef = ref(null);
|
||||
|
||||
const getData = async () => {
|
||||
const { page, page_size } = pageInfo.value;
|
||||
const { code, data } = await getWorksPageWriter(route.params.writerCode, {
|
||||
...query.value,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
if (code === 200) {
|
||||
dataSource.value = data?.data ?? [];
|
||||
pageInfo.value.total = data.total;
|
||||
}
|
||||
};
|
||||
const handleSearch = () => {
|
||||
reload();
|
||||
};
|
||||
const reload = () => {
|
||||
pageInfo.value.page = 1;
|
||||
getData();
|
||||
};
|
||||
const handleReset = () => {
|
||||
resetPageInfo();
|
||||
query.value = cloneDeep(INITIAL_QUERY);
|
||||
reload();
|
||||
};
|
||||
const handleSorterChange = (column, order) => {
|
||||
query.value.sort_column = column;
|
||||
query.value.sort_order = order;
|
||||
reload();
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
const { id, title } = item;
|
||||
deleteManuscriptModalRef.value?.open({ id, name: `“${title}”` });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
provide('update', getData);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,22 @@
|
||||
.manuscript-list-wrap {
|
||||
// height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.filter-wrap {
|
||||
.top {
|
||||
.title {
|
||||
font-family: $font-family-medium;
|
||||
font-style: normal;
|
||||
}
|
||||
:deep(.arco-btn) {
|
||||
.arco-btn-icon {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="确认提示" width="480px" @close="onClose">
|
||||
<div class="flex items-center">
|
||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
||||
<span>确认取消上传这 {{ num }} 个文件吗?此操作不可恢复。</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">确认取消</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
const num = ref('');
|
||||
|
||||
const onClose = () => {
|
||||
num.value = '';
|
||||
visible.value = false;
|
||||
};
|
||||
const onConfirm = () => {
|
||||
onClose();
|
||||
router.push({ name: 'WriterMaterialCenterFinishedProducts', params: { writerCode: route.params.writerCode } });
|
||||
};
|
||||
|
||||
const open = (manusNum) => {
|
||||
num.value = manusNum;
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,334 @@
|
||||
<script lang="jsx">
|
||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import EditForm, { ENUM_UPLOAD_STATUS, INITIAL_VIDEO_INFO } from '../components/edit-form';
|
||||
import CancelUploadModal from './cancel-upload-modal.vue';
|
||||
import UploadSuccessModal from './upload-success-modal.vue';
|
||||
|
||||
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants';
|
||||
import { postWorksBatchWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { glsWithCatch, rlsWithCatch, slsWithCatch } from '@/utils/stroage.ts';
|
||||
import { formatDuration, formatFileSize, convertVideoUrlToCoverUrl } from '@/utils/tools';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
|
||||
import icon1 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditForm,
|
||||
},
|
||||
setup(props, { emit, expose }) {
|
||||
const formRef = ref(null);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const sidebarStore = useSidebarStore();
|
||||
|
||||
const cancelUploadModal = ref(null);
|
||||
const uploadSuccessModal = ref(null);
|
||||
const works = ref([]);
|
||||
const selectCardInfo = ref({});
|
||||
const errorDataCards = ref([]);
|
||||
const uploadLoading = ref(false);
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
const collapsed = computed(() => {
|
||||
return sidebarStore.menuCollapse;
|
||||
});
|
||||
|
||||
const onCancel = () => {
|
||||
cancelUploadModal.value?.open(works.value.length);
|
||||
};
|
||||
|
||||
const validateDataSource = () => {
|
||||
return new Promise((resolve) => {
|
||||
const uploadingVideos = works.value.filter(
|
||||
(item) => item.videoInfo?.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING,
|
||||
);
|
||||
if (uploadingVideos.length > 0) {
|
||||
AMessage.warning(`有 ${uploadingVideos.length} 个视频正在上传中,请等待上传完成后再提交`);
|
||||
return;
|
||||
}
|
||||
|
||||
works.value.forEach((item) => {
|
||||
if (!item.title?.trim()) {
|
||||
errorDataCards.value.push(item);
|
||||
}
|
||||
});
|
||||
if (!errorDataCards.value.length) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (errorDataCards.value.length > 0) {
|
||||
AMessage.warning(`有 ${errorDataCards.value.length} 个必填信息未填写,请检查`);
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`card-${errorDataCards.value[0]?.id}`);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (action) => {
|
||||
uploadLoading.value = true;
|
||||
const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo'));
|
||||
const { code, data } = await postWorksBatchWriter({ works: filteredWorks }, writerCode.value);
|
||||
if (code === 200) {
|
||||
uploadLoading.value = false;
|
||||
if (action === 'batchUpload') {
|
||||
uploadSuccessModal.value?.open(data);
|
||||
} else {
|
||||
if (action === 'uploadAndCheck') {
|
||||
slsWithCatch('writerManuscriptCheckIds', [data]);
|
||||
router.push({
|
||||
name: 'WriterManuscriptCheck',
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
},
|
||||
});
|
||||
|
||||
} else {
|
||||
router.push({
|
||||
name: 'WriterMaterialCenterFinishedProducts',
|
||||
params: {
|
||||
writerCode: writerCode.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onUpload = async (action) => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
return validateDataSource();
|
||||
})
|
||||
.then(() => {
|
||||
onSubmit(action);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('catch');
|
||||
errorDataCards.value.push(err);
|
||||
});
|
||||
};
|
||||
|
||||
const syncLocalStorage = () => {
|
||||
slsWithCatch('writerWaitUploadWorks', JSON.stringify(works.value ?? []));
|
||||
};
|
||||
|
||||
const handleReValidate = () => {
|
||||
formRef.value?.validate().then(() => {
|
||||
deleteErrorCard(selectCardInfo.value);
|
||||
});
|
||||
};
|
||||
const onSelect = (item) => {
|
||||
formRef.value.resetForm();
|
||||
selectCardInfo.value = cloneDeep(item);
|
||||
};
|
||||
|
||||
const deleteErrorCard = (item) => {
|
||||
const errorIndex = errorDataCards.value.findIndex((v) => v.id === item.id);
|
||||
errorIndex > -1 && errorDataCards.value.splice(errorIndex, 1);
|
||||
};
|
||||
|
||||
const onDelete = (e, item, index) => {
|
||||
e.stopPropagation();
|
||||
works.value.splice(index, 1);
|
||||
deleteErrorCard(item);
|
||||
|
||||
if (item.id === selectCardInfo.value.id) {
|
||||
if (works.value.length) {
|
||||
const _target = works.value[0] ?? {};
|
||||
selectCardInfo.value = cloneDeep(_target);
|
||||
} else {
|
||||
selectCardInfo.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
syncLocalStorage();
|
||||
};
|
||||
|
||||
const renderFooterRow = () => {
|
||||
if (works.value.length > 1) {
|
||||
return (
|
||||
<>
|
||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
||||
取消上传
|
||||
</Button>
|
||||
<Button type="primary" size="medium" onClick={() => onUpload('batchUpload')} loading={uploadLoading.value}>
|
||||
{uploadLoading.value ? '批量上传' : '批量上传'}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
||||
取消上传
|
||||
</Button>
|
||||
<Button
|
||||
size="medium"
|
||||
type="outline"
|
||||
onClick={() => onUpload('singleUpload')}
|
||||
class="mr-12px"
|
||||
loading={uploadLoading.value}
|
||||
>
|
||||
上传
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="medium"
|
||||
onClick={() => onUpload('uploadAndCheck')}
|
||||
loading={uploadLoading.value}
|
||||
>
|
||||
上传并审核
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
const getData = () => {
|
||||
const _works = JSON.parse(glsWithCatch('writerWaitUploadWorks') ?? '[]')?.map((item) => {
|
||||
const { type, files } = item;
|
||||
|
||||
const _data = { ...item, videoInfo: cloneDeep(INITIAL_VIDEO_INFO) };
|
||||
|
||||
// 初始化视频数据
|
||||
if (type === EnumManuscriptType.Video && files.length) {
|
||||
_data.videoInfo.uploadStatus = 'end';
|
||||
const { name, size, duration, url } = files[0];
|
||||
_data.videoInfo.name = name;
|
||||
_data.videoInfo.size = formatFileSize(size);
|
||||
_data.videoInfo.time = formatDuration(duration);
|
||||
_data.videoInfo.poster = convertVideoUrlToCoverUrl(url);
|
||||
}
|
||||
|
||||
return _data;
|
||||
});
|
||||
|
||||
works.value = _works;
|
||||
selectCardInfo.value = cloneDeep(_works[0] ?? {});
|
||||
};
|
||||
const getCardClass = (item) => {
|
||||
if (selectCardInfo.value.id === item.id) {
|
||||
return '!border-#6D4CFE !bg-#F0EDFF';
|
||||
}
|
||||
if (errorDataCards.value.find((v) => v.id === item.id)) {
|
||||
return '!border-#F64B31';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const onChange = (val) => {
|
||||
const index = works.value.findIndex((v) => v.id === selectCardInfo.value.id);
|
||||
if (index !== -1) {
|
||||
works.value[index] = cloneDeep(val);
|
||||
}
|
||||
selectCardInfo.value = val;
|
||||
|
||||
syncLocalStorage();
|
||||
};
|
||||
const onUpdateVideoInfo = (newVideoInfo) => {
|
||||
const index = works.value.findIndex((v) => v.id === selectCardInfo.value.id);
|
||||
if (index !== -1) {
|
||||
works.value[index].videoInfo = cloneDeep(newVideoInfo);
|
||||
}
|
||||
selectCardInfo.value.videoInfo = { ...selectCardInfo.value.videoInfo, ...newVideoInfo };
|
||||
|
||||
syncLocalStorage();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
rlsWithCatch('writerWaitUploadWorks');
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="manuscript-upload-wrap bg-#fff rounded-8px flex">
|
||||
<div class="left flex-1 overflow-y-auto p-24px">
|
||||
<EditForm
|
||||
ref={formRef}
|
||||
formData={selectCardInfo.value}
|
||||
onReValidate={handleReValidate}
|
||||
onChange={onChange}
|
||||
onUpdateVideoInfo={onUpdateVideoInfo}
|
||||
/>
|
||||
</div>
|
||||
{works.value.length > 1 && (
|
||||
<div class="right pt-24px pb-12px flex flex-col">
|
||||
<div class="title flex items-center px-24px">
|
||||
<div class="w-3px h-16px bg-#6D4CFE rounded-2px mr-8px"></div>
|
||||
<p class="cts bold !color-#211F24 !text-16px !lh-24px mr-8px">上传内容稿件列表</p>
|
||||
<p class="cts">{`共${works.value.length}个`}</p>
|
||||
</div>
|
||||
<div class="flex-1 px-24px overflow-y-auto pt-24px">
|
||||
{works.value.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
id={`card-${item.id}`}
|
||||
class={`group h-66px relative mb-12px px-12px py-8px flex flex-col rounded-8px bg-#F7F8FA border-1px border-solid border-transparent transition-all duration-300 cursor-pointer hover:bg-#E6E6E8 ${getCardClass(
|
||||
item,
|
||||
)}`}
|
||||
onClick={() => onSelect(item)}
|
||||
>
|
||||
<icon-close-circle-fill
|
||||
size={16}
|
||||
class="absolute top--8px right--8px color-#737478 hover:color-#211F24 hidden group-hover:block"
|
||||
onClick={(e) => onDelete(e, item, index)}
|
||||
/>
|
||||
<TextOverTips
|
||||
context={item.title || '-'}
|
||||
line={1}
|
||||
class={`cts !color-#211F24 mb-8px ${selectCardInfo.value.id === item.id ? 'bold' : ''}`}
|
||||
/>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center ">
|
||||
<img
|
||||
src={item.type === EnumManuscriptType.Image ? icon1 : icon2}
|
||||
width="14"
|
||||
height="14"
|
||||
class="mr-4px"
|
||||
/>
|
||||
<span
|
||||
class={`cts !text-12px ${
|
||||
item.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'
|
||||
}`}
|
||||
>
|
||||
{item.type === EnumManuscriptType.Image ? '图文' : '视频'}
|
||||
</span>
|
||||
</div>
|
||||
{errorDataCards.value.find((v) => v.id === item.id) && (
|
||||
<p class="cts !text-12px !color-#F64B31">必填项未填</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<footer
|
||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
||||
collapsed.value ? 'collapsed' : ''
|
||||
}`}
|
||||
>
|
||||
{renderFooterRow()}
|
||||
</footer>
|
||||
<CancelUploadModal ref={cancelUploadModal} />
|
||||
<UploadSuccessModal ref={uploadSuccessModal} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-upload-wrap {
|
||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
||||
.cts,
|
||||
:deep(.overflow-text) {
|
||||
color: #939499;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
border-left: 1px solid var(--Border-2, #e6e6e8);
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.footer-row {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: $sidebar-width;
|
||||
width: calc(100% - $sidebar-width);
|
||||
height: $footer-height;
|
||||
&.collapsed {
|
||||
left: $sidebar-width-collapse;
|
||||
width: calc(100% - $sidebar-width-collapse);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="提示" width="480px" @close="onClose" modal-class="upload-success11-modal">
|
||||
<div class="flex items-center flex-col justify-center">
|
||||
<img :src="icon1" width="80" height="80" class="mb-16px" />
|
||||
<span class="text-18px lh-26px font-400 color-#211F24 md">上传成功</span>
|
||||
<p class="text-14px lh-22px font-400 color-#737478 ld">为确保内容合规,建议您立即进行审核</p>
|
||||
<p class="text-14px lh-22px font-400 color-#737478 ld">检测是否存在违规内容</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button size="medium" @click="onBack">回到列表</a-button>
|
||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">批量审核</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
import icon1 from '@/assets/img/media-account/icon-feedback-success.png';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
const workIds = ref([]);
|
||||
|
||||
const onClose = () => {
|
||||
workIds.value = [];
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
onClose();
|
||||
router.push({ name: 'WriterMaterialCenterFinishedProducts', params: { writerCode: route.params.writerCode } });
|
||||
};
|
||||
const onConfirm = () => {
|
||||
visible.value = false;
|
||||
slsWithCatch('writerManuscriptCheckIds', workIds.value);
|
||||
router.push({ name: 'WriterManuscriptCheck', params: { writerCode: route.params.writerCode } });
|
||||
};
|
||||
|
||||
const open = (_workIds) => {
|
||||
workIds.value = _workIds;
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.upload-success11-modal {
|
||||
.arco-modal-header {
|
||||
border-bottom: none;
|
||||
}
|
||||
.md {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
.ld {
|
||||
font-family: $font-family-regular;
|
||||
}
|
||||
.arco-modal-footer {
|
||||
border-top: none;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,37 @@
|
||||
.finished-products-wrap {
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
padding: 0 24px;
|
||||
.ant-tabs-nav-wrap {
|
||||
height: 56px;
|
||||
.ant-tabs-nav-list {
|
||||
.ant-tabs-tab {
|
||||
.ant-tabs-tab-btn {
|
||||
color: var(--Text-2, #55585f);
|
||||
font-family: $font-family-regular;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
&.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: var(--Brand-6, #6d4cfe);
|
||||
font-weight: 500;
|
||||
font-family: $font-family-medium;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs-ink-bar {
|
||||
background: #6D4CFE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="tsx">
|
||||
export default defineComponent({
|
||||
setup(_, { attrs, slots, expose }) {
|
||||
|
||||
return () => (
|
||||
<div class="raw-material-wrap h-full flex flex-col">
|
||||
原材料库
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,3 @@
|
||||
.raw-material-wrap {
|
||||
|
||||
}
|
||||
54
src/views/writer-material-center/index.vue
Normal file
54
src/views/writer-material-center/index.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script lang="tsx">
|
||||
import FinishedProducts from './components/finished-products/index.vue';
|
||||
import RawMaterial from './components/raw-material/index.vue';
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
label: '成品库',
|
||||
key: '1',
|
||||
routeName: 'WriterMaterialCenterFinishedProducts',
|
||||
},
|
||||
// {
|
||||
// label: '原料库',
|
||||
// key: '2',
|
||||
// routeName: 'WriterMaterialCenterRawMaterial',
|
||||
// },
|
||||
];
|
||||
export default defineComponent({
|
||||
setup(_, { attrs, slots, expose }) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const activeKey = ref('1');
|
||||
|
||||
onMounted(() => {
|
||||
activeKey.value = TABS.find((item) => item.routeName === route.name)?.key;
|
||||
});
|
||||
return () => (
|
||||
<div class="material-center-wrap h-full flex flex-col">
|
||||
<header class="py-16px px-24px rounded-8px bg-#fff flex items-center mb-16px">
|
||||
{TABS.map((item) => (
|
||||
<p
|
||||
key={item.key}
|
||||
class={`font-family-medium color-#737478 font-400 text-16px lh-24px cursor-pointer mr-32px ${
|
||||
item.key === activeKey.value ? '!color-#6D4CFE font-500' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
activeKey.value = item.key;
|
||||
router.push({ name: item.routeName });
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
))}
|
||||
</header>
|
||||
{activeKey.value === '1' ? <FinishedProducts /> : <RawMaterial />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
2
src/views/writer-material-center/style.scss
Normal file
2
src/views/writer-material-center/style.scss
Normal file
@ -0,0 +1,2 @@
|
||||
.material-center-wrap {
|
||||
}
|
||||
Reference in New Issue
Block a user