feat: 文本检测

This commit is contained in:
rd
2025-08-06 12:00:05 +08:00
parent a9b2cfc7d1
commit e73414c46c
11 changed files with 428 additions and 52 deletions

View File

@ -104,7 +104,7 @@
v-else-if="audit_status === AuditStatus.Pending"
>审核</a-button
>
<a-button type="outline" size="mini" @click="onCheck(record)" v-else>查看</a-button>
<a-button type="outline" size="mini" @click="onScan(record)" v-else>查看</a-button>
</div>
</template>
<template v-else #cell="{ record }">
@ -121,6 +121,7 @@
import { ref } from 'vue';
import { formatTableField, exactFormatTime } from '@/utils/tools';
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
import { patchWorkAuditsAudit } from '@/api/all/generationWorkshop';
import {
AuditStatus,
CUSTOMER_OPINION,
@ -173,6 +174,10 @@ const onShare = (item) => {
shareModalRef.value?.open([item.id]);
};
const onCheck = (item) => {
patchWorkAuditsAudit(item.id);
router.push(`/manuscript/check/${item.id}`);
};
const onScan = (item) => {
router.push(`/manuscript/check/${item.id}`);
};
const onDetail = (item) => {

View File

@ -152,7 +152,7 @@ export const TABLE_COLUMNS3 = [
{
title: '客户意见',
dataIndex: 'title',
width: 120,
width: 200,
},
{
title: '审核平台',

View File

@ -17,7 +17,10 @@
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<div class="flex justify-end mb-12px" v-if="[AuditStatus.Pending, AuditStatus.Auditing].includes(query.audit_status)">
<div
class="flex justify-end mb-12px"
v-if="[AuditStatus.Pending, AuditStatus.Auditing].includes(query.audit_status)"
>
<a-button
type="outline"
class="w-fit"
@ -76,9 +79,9 @@ import ManuscriptCheckTable from './components/manuscript-check-table';
import DeleteManuscriptModal from './components/manuscript-check-table/delete-manuscript-modal.vue';
import ShareManuscriptModal from '@/views/creative-generation-workshop/manuscript/components/share-manuscript-modal';
import { getWorkAuditsPage } from '@/api/all/generationWorkshop.ts';
import { getWorkAuditsPage, patchWorkAuditsBatchAudit } from '@/api/all/generationWorkshop.ts';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { getProjects } from '@/api/all/propertyMarketing';
// import { getProjects } from '@/api/all/propertyMarketing';
import {
AuditStatus,
INITIAL_QUERY,
@ -151,14 +154,19 @@ const handleBatchCheck = () => {
return;
}
patchWorkAuditsBatchAudit({ ids: selectedRowKeys.value });
const ids = selectedRowKeys.value.join(',');
router.push(`/manuscript/check/${ids}`);
};
const handleBatchView = () => {
if (!selectedRows.value.length) {
if (!selectedRows.value.length) {
AMessage.warning('请选择需查看的内容稿件');
return;
}
const ids = selectedRowKeys.value.join(',');
router.push(`/manuscript/check/${ids}`);
};
const handleTabClick = (key) => {
query.value = cloneDeep(INITIAL_QUERY);

View File

@ -0,0 +1,42 @@
const FORM_RULES = {
title: [{ required: true, message: '请输入标题' }],
};
const enumTab = {
TEXT: 0,
IMAGE: 1,
};
const TAB_LIST = [
{
label: '文本',
value: enumTab.TEXT,
},
{
label: '图片',
value: enumTab.IMAGE,
},
];
export const RESULT_LIST = [
{
label: '合规程度',
value: 'compliance_degree',
class: '!color-#6d4cfe',
},
{
label: '检验项',
value: 'compliance',
class: '!color-#211F24',
},
{
label: '高风险',
value: 'high_risk',
class: '!color-#F64B31',
},
{
label: '中风险',
value: 'medium_risk',
class: '!color-#FFAE00',
},
];
export { FORM_RULES, enumTab, TAB_LIST };

View File

@ -1,12 +1,187 @@
<script lang="jsx">
import axios from 'axios';
import { IconLoading } from '@arco-design/web-vue/es/icon';
import {
Form,
FormItem,
Input,
Textarea,
Button,
Tabs,
Upload,
TabPane,
Message as AMessage,
} from '@arco-design/web-vue';
import { FORM_RULES, enumTab, TAB_LIST, RESULT_LIST } from './constants';
import icon1 from '@/assets/img/creative-generation-workshop/icon-magic.png';
export default {
props: {
modelValue: {
type: Object,
default: {},
},
checkResult: {
type: Object,
default: {},
},
},
emits: ['update:modelValue', 'filesChange', 'againCheck'],
setup(props, { emit, expose }) {
const activeTab = ref(enumTab.TEXT);
const aiCheckLoading = ref(false);
const formRef = ref(null);
const onAiCheck = () => {
if (aiCheckLoading.value) return;
aiCheckLoading.value = true;
setTimeout(() => {
aiCheckLoading.value = false;
}, 2000);
};
const onAgainCheck = () => {
emit('againCheck');
};
const handleTabClick = (key) => {
activeTab.value = key;
};
const validate = () => {
return new Promise((resolve, reject) => {
formRef.value?.validate((errors) => {
if (errors) {
reject();
} else {
resolve();
}
});
});
};
const resetForm = () => {
formRef.value?.resetFields?.();
formRef.value?.clearValidate?.();
};
const renderFooterRow = () => {
return (
<>
<Button class="mr-12px" size="medium" onClick={onAgainCheck}>
再次审核
</Button>
{activeTab.value === enumTab.TEXT ? (
<Button size="medium" type="outline" class="w-123px" onClick={onAiCheck}>
{aiCheckLoading.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>
) : (
<Button size="medium" type="outline">
图片替换
</Button>
)}
</>
);
};
const renderContent = () => {
if (activeTab.value === enumTab.TEXT) {
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
/>
</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}
show-word-limit
/>
</FormItem>
</Form>
);
}
};
expose({
validate,
resetForm,
});
const descs = [
'删除 “效果炸裂”“医院都做不到” 等夸大对比的表述;',
'去掉 “顽固体质逆袭”“腰围小 10cm” 等具体效果承诺(食品 / 非药品不得宣称减肥等功效);',
'严禁提及 “孕妇能吃”,明确标注不适宜人群;',
'“进口纯天然” 需有依据,避免绝对化,可改为 “选用进口原料,成分温和”;',
'去掉 “当糖吃” 等误导性表述,强调正常食用量。',
]
const forbidWords = [
{
label: '纯天然',
desc: '涉嫌虚假内容相关词语'
},
{
label: '安全',
desc: '涉嫌绝对化承诺保证'
},
{
label: '副作用',
desc: '涉嫌夸大词语'
},
]
return () => {
return (
<div class="content-wrap">
<div class="content-left">
<span class="cts bold !color-#1D2129">批量审核内容稿件</span>
<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} title={item.label}></TabPane>
))}
</Tabs>
<div class="flex-1 px-16px">{renderContent()}</div>
</div>
<div class="flex items-center justify-end">{renderFooterRow()}</div>
</div>
<div class="right-box flex-1 rounded-8px border-1px border-#E6E6E8 border-solid p-16px flex flex-col overflow-y-auto">
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
<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 ${item.class}`}>30</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 max-h-220px overflow-y-auto">
<img src={null} class="mb-8px w-84px h-24px"/>
{
descs.map((item, index) => (
<p class="cts !color-#55585F !lh-20px" key={index}>{`${index + 1}. ${item}`}</p>
))
}
</div>
<div class="forbid-word-box flex">
</div>
</div>
</div>
);
@ -17,4 +192,4 @@ export default {
<style lang="scss" scoped>
@import './style.scss';
</style>
</style>

View File

@ -0,0 +1,90 @@
.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-text {
background: linear-gradient(84deg, #266cff 4.57%, #a15af0 84.93%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.left-box {
:deep(.arco-tabs) {
.arco-tabs-nav {
.arco-tabs-tab {
height: 40px;
padding: 0;
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%;
}
}
}
}
}
.right-box {
.s1 {
color: var(--Brand-6, #6d4cfe);
font-family: $font-family-manrope-regular;
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);
}
}
}
.forbid-word-box{
}
}
}

View File

@ -21,13 +21,17 @@ export default {
type: Object,
default: {},
},
selectCardInfo: {
type: Object,
default: {},
},
},
emits: ['update:modelValue', 'cardClick'],
emits: ['update:modelValue', 'cardClick'],
setup(props, { emit, expose }) {
const selectedPlatform = ref(2);
const selectedPlatform = ref(1);
const modules = [Navigation];
const handleCardClick = (item) => {
// emit('update:modelValue', item);
// emit('update:modelValue', item);
emit('cardClick', item);
};
return () => {
@ -38,6 +42,7 @@ export default {
<Swiper
spaceBetween={16}
modules={modules}
slidesPerView="auto"
navigation={{
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
@ -48,7 +53,7 @@ export default {
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.modelValue.id ? 'active' : ''
item.id === props.selectCardInfo.id ? 'active' : ''
}`}
>
<Image

View File

@ -1,5 +1,5 @@
<script lang="jsx">
import { Button, Message as AMessage } from '@arco-design/web-vue';
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';
@ -11,38 +11,49 @@ import {
putWorkAuditsUpdate,
putWorkAuditsAuditPass,
getWorkAuditsDetail,
getWorkAuditsBatchDetail,
} from '@/api/all/generationWorkshop.ts';
const generateMockData = (count = 20) =>
Array.from({ length: count }, (_, i) => ({
id: `${i + 1}`,
title: `挖到宝了!这个平价好物让我素颜出门都自信✨挖到宝了!这个平价好物让我素颜出门都自信✨${i + 1}`,
content: '挖到宝了!这个平价好物让我素颜出门都自信✨挖到宝了!这个平价好物让我素颜出门都自信✨',
platform: 1,
files: [],
}));
// const generateMockData = (count = 20) =>
// Array.from({ length: count }, (_, i) => ({
// id: `${i + 1}`,
// title: `挖到宝了!这个平价好物让我素颜出门都自信✨挖到宝了!这个平价好物让我素颜出门都自信✨${i + 1}`,
// content: '挖到宝了!这个平价好物让我素颜出门都自信✨挖到宝了!这个平价好物让我素颜出门都自信✨',
// platform: 1,
// files: [],
// }));
export default {
setup(props, { emit, expose }) {
const router = useRouter();
const route = useRoute();
const workId = ref(route.params.id);
// const workIds = ref(route.params.id);
const selectCardInfo = 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 checkLoading = ref(false);
const checkResult = ref({});
const workIds = computed(() => {
return route.params.id.split(',');
});
const onBack = () => {
router.push({ name: 'ManuscriptCheckList' });
};
const onChangeCard = (item) => {
contentCardRef.value.resetForm();
isSaved.value = false;
submitLoading.value = false;
checkLoading.value = false;
checkResult.value = {};
selectCardInfo.value = cloneDeep(item);
};
@ -55,9 +66,14 @@ export default {
}
};
const getData = () => {
dataSource.value = generateMockData();
selectCardInfo.value = cloneDeep(dataSource.value[0] ?? {});
const getData = async () => {
const { code, data } = await getWorkAuditsBatchDetail({ ids: workIds.value });
if (code === 200) {
dataSource.value = data ?? [];
selectCardInfo.value = cloneDeep(data?.[0] ?? {});
}
// dataSource.value = generateMockData();
// selectCardInfo.value = cloneDeep(dataSource.value[0] ?? {});
};
const isSelectCardModified = () => {
@ -77,22 +93,35 @@ export default {
}
};
const onSave = async () => {
const { code, data } = await putWorkAuditsUpdate(selectCardInfo.value);
if (code === 200) {
isSaved.value = true;
AMessage.success('当前内容稿件已保存');
}
contentCardRef.value?.validate().then(async () => {
const { code, data } = await putWorkAuditsUpdate(selectCardInfo.value);
if (code === 200) {
isSaved.value = true;
AMessage.success('当前内容稿件已保存');
}
});
};
const onSubmit = async () => {
try {
checkLoading.value = true;
const { code, data } = await putWorkAuditsAuditPass(selectCardInfo.value);
if (code === 200) {
checkSuccessModalRef.value?.open(selectCardInfo.value.id);
contentCardRef.value?.validate().then(async () => {
try {
submitLoading.value = true;
const { code, data } = await putWorkAuditsAuditPass(selectCardInfo.value);
if (code === 200) {
checkSuccessModalRef.value?.open(selectCardInfo.value.id);
}
} finally {
submitLoading.value = false;
}
} finally {
});
};
const onFilesChange = (files) => {
selectCardInfo.value.files = cloneDeep(files);
};
const onAgainCheck = () => {
checkLoading.value = true;
setTimeout(() => {
checkLoading.value = false;
}
}, 2000);
};
const renderFooterRow = () => {
return (
@ -103,8 +132,8 @@ export default {
<Button size="medium" type="outline" class="mr-12px" onClick={onSave}>
保存
</Button>
<Button type="primary" size="medium" onClick={onSubmit} loading={checkLoading.value}>
{checkLoading.value ? '通过审核中...' : '通过审核'}
<Button type="primary" size="medium" onClick={onSubmit} loading={submitLoading.value}>
{submitLoading.value ? '通过审核中...' : '通过审核'}
</Button>
</>
);
@ -114,8 +143,8 @@ export default {
});
return () => (
<>
<div class="manuscript-check-wrap">
<Spin loading={checkLoading.value} tip="文本检测中" class="manuscript-check-wrap" size={100}>
<div class="h-full w-full flex flex-col">
<div class="flex items-center mb-8px">
<span class="cts color-#4E5969 cursor-pointer" onClick={onExit}>
内容稿件审核
@ -124,9 +153,16 @@ export default {
<span class="cts bold !color-#1D2129">批量审核内容稿件</span>
</div>
<div class="flex-1 flex flex-col overflow-hidden bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<HeaderCard dataSource={dataSource.value} v-model={selectCardInfo.value} onCardClick={onCardClick} />
<HeaderCard dataSource={dataSource.value} selectCardInfo={selectCardInfo.value} onCardClick={onCardClick} />
<section class="flex-1">
<ContentCard />
<ContentCard
ref={contentCardRef}
v-model={selectCardInfo.value}
selectCardInfo={selectCardInfo.value}
checkResult={checkResult.value}
onFilesChange={onFilesChange}
onAgainCheck={onAgainCheck}
/>
</section>
</div>
</div>
@ -136,7 +172,7 @@ export default {
<CancelCheckModal ref={cancelCheckModalRef} onSelectCard={onChangeCard} />
<CheckSuccessModal ref={checkSuccessModalRef} />
</>
</Spin>
);
},
};

View File

@ -1,8 +1,7 @@
$footer-height: 68px;
.manuscript-check-wrap {
width: 100%;
height: calc(100% - 72px);
display: flex;
flex-direction: column;
.cts {
color: #939499;
font-family: $font-family-regular;