feat: 内容稿件审核
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
<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,
|
||||
@ -15,11 +17,17 @@ import {
|
||||
} 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 } from './constants';
|
||||
import { getImagePreSignedUrl } from '@/api/all/common';
|
||||
|
||||
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: {
|
||||
@ -31,16 +39,24 @@ export default {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
selectedImageInfo: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
checkLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'filesChange', 'againCheck'],
|
||||
emits: ['update:modelValue', 'filesChange', 'againCheck', 'imageClick'],
|
||||
setup(props, { emit, expose }) {
|
||||
const activeTab = ref(enumTab.TEXT);
|
||||
const aiCheckLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
const uploadRef = ref(null);
|
||||
const modules = [Navigation];
|
||||
|
||||
const isTextTab = computed(() => activeTab.value === enumTab.TEXT);
|
||||
|
||||
const onAiCheck = () => {
|
||||
if (aiCheckLoading.value) return;
|
||||
@ -51,7 +67,18 @@ export default {
|
||||
}, 2000);
|
||||
};
|
||||
const onAgainCheck = () => {
|
||||
emit('againCheck');
|
||||
if (isTextTab.value) {
|
||||
emit('againCheck');
|
||||
} else {
|
||||
if (!props.modelValue.files?.length) {
|
||||
AMessage.warning('请先上传需审核图片');
|
||||
return;
|
||||
}
|
||||
emit('againCheck');
|
||||
}
|
||||
};
|
||||
const onReplaceImage = () => {
|
||||
uploadRef.value?.upload?.();
|
||||
};
|
||||
const handleTabClick = (key) => {
|
||||
activeTab.value = key;
|
||||
@ -71,6 +98,62 @@ export default {
|
||||
formRef.value?.resetFields?.();
|
||||
formRef.value?.clearValidate?.();
|
||||
};
|
||||
const getFileExtension = (filename) => {
|
||||
const match = filename.match(/\.([^.]+)$/);
|
||||
return match ? match[1].toLowerCase() : '';
|
||||
};
|
||||
const handleSelectImage = (item) => {
|
||||
emit('imageClick', item);
|
||||
};
|
||||
|
||||
const renderUpload = (UploadBtn) => {
|
||||
return (
|
||||
<Upload
|
||||
ref={uploadRef}
|
||||
action="/"
|
||||
draggable
|
||||
class="w-fit"
|
||||
custom-request={uploadImage}
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
show-file-list={false}
|
||||
multiple
|
||||
>
|
||||
{{
|
||||
'upload-button': () => <UploadBtn />,
|
||||
}}
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
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 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 _files = [
|
||||
...props.modelValue.files,
|
||||
{
|
||||
url: file_url,
|
||||
name: file_name,
|
||||
size,
|
||||
},
|
||||
];
|
||||
emits('filesChange', _files);
|
||||
};
|
||||
|
||||
const renderFooterRow = () => {
|
||||
return (
|
||||
@ -78,7 +161,7 @@ export default {
|
||||
<Button class="mr-12px" size="medium" onClick={onAgainCheck}>
|
||||
再次审核
|
||||
</Button>
|
||||
{activeTab.value === enumTab.TEXT ? (
|
||||
{isTextTab.value ? (
|
||||
<Button size="medium" type="outline" class="w-123px" onClick={onAiCheck}>
|
||||
{aiCheckLoading.value ? (
|
||||
<>
|
||||
@ -93,39 +176,122 @@ export default {
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="medium" type="outline">
|
||||
图片替换
|
||||
</Button>
|
||||
<div class="w-88px">
|
||||
{renderUpload(
|
||||
<Button size="medium" type="outline">
|
||||
图片替换
|
||||
</Button>,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderLeftBoxContent = () => {
|
||||
if (activeTab.value === enumTab.TEXT) {
|
||||
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={props.checkLoading}
|
||||
/>
|
||||
</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
|
||||
disabled={props.checkLoading}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const renderImageForm = () => {
|
||||
if (props.modelValue.files?.length > 0) {
|
||||
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={props.checkLoading}
|
||||
<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}
|
||||
class="flex items-center justify-center mb-16px"
|
||||
fit="contain"
|
||||
v-slots={{
|
||||
error: () => <img src={icon4} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
disabled={props.checkLoading}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<div class="swiper-wrap h-60px">
|
||||
<Swiper
|
||||
spaceBetween={16}
|
||||
modules={modules}
|
||||
slidesPerView="auto"
|
||||
navigation={{
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
}}
|
||||
>
|
||||
{props.modelValue.files.map((item) => (
|
||||
<SwiperSlide
|
||||
key={item.id}
|
||||
onClick={() => handleSelectImage(item)}
|
||||
class={`swiper-item !h-48px !w-48px bg-#F7F8FA cursor-pointer rounded-4px overflow-hidden !flex items-center ${
|
||||
item.id === props.selectedImageInfo.id ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<div class="group relative">
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={item.url}
|
||||
class="!rounded-4px"
|
||||
fit="cover"
|
||||
preview={false}
|
||||
v-slots={{
|
||||
error: () => <img src={icon4} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
<icon-close-circle-fill
|
||||
size={16}
|
||||
class="absolute top--8px right--8px hidden cursor-pointer color-#939499 group-hover:block z-50"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -151,9 +317,7 @@ export default {
|
||||
<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">
|
||||
{
|
||||
activeTab.value === enumTab.TEXT ? '恭喜,您的文案中没有检测出违禁词' : '恭喜,您的图片中没有检测出违禁内容'
|
||||
}
|
||||
{isTextTab.value ? '恭喜,您的文案中没有检测出违禁词' : '恭喜,您的图片中没有检测出违禁内容'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -256,7 +420,7 @@ export default {
|
||||
<TabPane key={item.value} title={item.label}></TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<div class="flex-1 px-16px">{renderLeftBoxContent()}</div>
|
||||
<div class="flex-1 px-16px">{isTextTab.value ? renderTextForm() : renderImageForm()}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">{renderFooterRow()}</div>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
.arco-tabs-nav {
|
||||
.arco-tabs-tab {
|
||||
height: 40px;
|
||||
// padding: 0 8px;
|
||||
// padding: 0 8px;
|
||||
margin: 0 16px;
|
||||
}
|
||||
&::before {
|
||||
@ -58,6 +58,62 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
.swiper-item {
|
||||
transition: all;
|
||||
&.active {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px solid var(--Brand-6, #6d4cfe);
|
||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
.s1 {
|
||||
|
||||
@ -21,7 +21,6 @@ export default {
|
||||
const route = useRoute();
|
||||
|
||||
const workIds = ref([]);
|
||||
const selectCardInfo = ref({});
|
||||
const isSaved = ref(false);
|
||||
const dataSource = ref([]);
|
||||
const remoteDataSource = ref([]);
|
||||
@ -32,6 +31,9 @@ export default {
|
||||
const checkLoading = ref(false);
|
||||
const checkResult = ref({});
|
||||
|
||||
const selectCardInfo = ref({});
|
||||
const selectedImageInfo = ref(null);
|
||||
|
||||
const onBack = () => {
|
||||
router.push({ name: 'ManuscriptCheckList' });
|
||||
};
|
||||
@ -43,6 +45,7 @@ export default {
|
||||
checkLoading.value = false;
|
||||
checkResult.value = {};
|
||||
selectCardInfo.value = cloneDeep(item);
|
||||
selectedImageInfo.value = cloneDeep(item.files?.[0] ?? {});
|
||||
};
|
||||
|
||||
const onCardClick = async (item) => {
|
||||
@ -54,11 +57,16 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const onImageClick = (item) => {
|
||||
selectedImageInfo.value = cloneDeep(item);
|
||||
};
|
||||
|
||||
const getWorkAudits = async () => {
|
||||
const { code, data } = await getWorkAuditsBatchDetail({ ids: workIds.value });
|
||||
if (code === 200) {
|
||||
dataSource.value = data ?? [];
|
||||
selectCardInfo.value = cloneDeep(data?.[0] ?? {});
|
||||
selectedImageInfo.value = data?.[0].files?.[0] ?? {};
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,12 +104,7 @@ export default {
|
||||
dataSource.value = dataSource.value.filter((v) => v.id != _id);
|
||||
|
||||
slsWithCatch('manuscriptCheckIds', workIds.value.join(','));
|
||||
if (dataSource.value.length) {
|
||||
const _target = dataSource.value[0] ?? {};
|
||||
selectCardInfo.value = cloneDeep(_target);
|
||||
} else {
|
||||
selectCardInfo.value = {};
|
||||
}
|
||||
onChangeCard(dataSource.value.length ? dataSource.value[0] : {});
|
||||
}
|
||||
};
|
||||
const onSubmit = async () => {
|
||||
@ -142,8 +145,7 @@ export default {
|
||||
);
|
||||
};
|
||||
onMounted(() => {
|
||||
const manuscriptCheckIds = glsWithCatch('manuscriptCheckIds');
|
||||
workIds.value = manuscriptCheckIds.split(',');
|
||||
workIds.value = glsWithCatch('manuscriptCheckIds')?.split(',') ?? [];
|
||||
getWorkAudits();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
@ -171,6 +173,8 @@ export default {
|
||||
onFilesChange={onFilesChange}
|
||||
onAgainCheck={onAgainCheck}
|
||||
checkLoading={checkLoading.value}
|
||||
selectedImageInfo={selectedImageInfo.value}
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user