Merge remote-tracking branch 'origin/feature/v1.2灵机空间-内容上传审核_rxd' into test
# Conflicts: # pnpm-lock.yaml # src/components/text-over-tips/index.vue # src/layouts/Basic.vue # src/layouts/Page.vue # src/main.ts # src/router/constants.ts # src/router/index.ts # src/router/typeings.d.ts # src/utils/tools.ts
@ -16,7 +16,21 @@ export function configAutoImport() {
|
||||
'@vueuse/core',
|
||||
{
|
||||
dayjs: [['default', 'dayjs']],
|
||||
'lodash-es': ['cloneDeep', 'omit', 'pick', 'union', 'uniq', 'isNumber', 'uniqBy', 'isEmpty', 'merge', 'debounce'],
|
||||
'lodash-es': [
|
||||
'cloneDeep',
|
||||
'omit',
|
||||
'pick',
|
||||
'union',
|
||||
'map',
|
||||
'uniq',
|
||||
'isNumber',
|
||||
'uniqBy',
|
||||
'isEmpty',
|
||||
'merge',
|
||||
'debounce',
|
||||
'isEqual',
|
||||
'isString'
|
||||
],
|
||||
'@/hooks': ['useModal'],
|
||||
},
|
||||
],
|
||||
|
||||
5
env.d.ts
vendored
@ -1 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const vueComponent: DefineComponent<{}, {}, any>;
|
||||
export default vueComponent;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN" size="small" :theme="redTheme">
|
||||
<router-view v-if="$route.path === '/login'" />
|
||||
<router-view v-if="$route.path === '/login' || ['ExploreList', 'ExploreDetail'].includes($route.name)" />
|
||||
<LayoutBasic v-else />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
@ -61,4 +61,14 @@ export const postBatchDownload = (params = {}) => {
|
||||
// 任务中心-批量查询任务状态
|
||||
export const batchQueryTaskStatus = (params = {}) => {
|
||||
return Http.get(`/v1/tasks/batch-query-status`, params);
|
||||
};
|
||||
};
|
||||
|
||||
// 获取图片上传地址
|
||||
export const getImagePreSignedUrl = (params = {}) => {
|
||||
return Http.get('/v1/oss/image-pre-signed-url', params);
|
||||
};
|
||||
|
||||
// 获取视频上传地址
|
||||
export const getVideoPreSignedUrl = (params = {}) => {
|
||||
return Http.get('/v1/oss/video-pre-signed-url', params);
|
||||
};
|
||||
|
||||
160
src/api/all/generationWorkshop-writer.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 写手端接口
|
||||
*/
|
||||
|
||||
import Http from '@/api';
|
||||
|
||||
// 内容稿件-批量添加(写手)
|
||||
export const postWorksBatchWriter = (params = {}, writerCode: string) => {
|
||||
return Http.post('/v1/writer/works/batch', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-分页(写手)
|
||||
export const getWorksPageWriter = (writerCode: string, params = {}) => {
|
||||
return Http.get('/v1/writer/works', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-详情(写手)
|
||||
export const getWorksDetailWriter = (writerCode: string, id: string) => {
|
||||
return Http.get(
|
||||
`/v1/writer/works/${id}`,
|
||||
{},
|
||||
{
|
||||
headers: { 'writer-code': writerCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-修改(写手)
|
||||
export const putWorksUpdateWriter = (writerCode: string, params = {}) => {
|
||||
const { id, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/writer/works/${id}`, rest, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-删除(写手)
|
||||
export const deleteWorkWriter = (writerCode: string, id: string) => {
|
||||
return Http.delete(`/v1/writer/works/${id}`, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-获取模板(写手)
|
||||
export const getTemplateUrlWriter = (writerCode: string) => {
|
||||
return Http.get(
|
||||
'/v1/writer/works/template',
|
||||
{},
|
||||
{
|
||||
headers: { 'writer-code': writerCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件审核-分页(写手)
|
||||
export const getWorkAuditsPageWriter = (writerCode: string, params = {}) => {
|
||||
return Http.get('/v1/writer/work-audits', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件审核-详情(写手)
|
||||
export const getWorkAuditsDetailWriter = (writerCode: string, id: string) => {
|
||||
return Http.get(
|
||||
`/v1/writer/work-audits/${id}`,
|
||||
{},
|
||||
{
|
||||
headers: { 'writer-code': writerCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件审核-多个详情(写手)
|
||||
export const getWorkAuditsBatchDetailWriter = (writerCode: string, params = {}) => {
|
||||
return Http.get('/v1/writer/work-audits/list', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-审核(写手)
|
||||
export const patchWorkAuditsAuditWriter = (id: string, writerCode: string) => {
|
||||
return Http.patch(
|
||||
`/v1/writer/work-audits/${id}/audit`,
|
||||
{},
|
||||
{
|
||||
headers: { 'writer-code': writerCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-批量审核(写手)
|
||||
export const patchWorkAuditsBatchAuditWriter = (writerCode: string, params: {}) => {
|
||||
return Http.patch('/v1/writer/work-audits/batch-audit', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件审核-修改(写手)
|
||||
export const putWorkAuditsUpdateWriter = (writerCode: string, params = {}) => {
|
||||
const { id: auditId, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/writer/work-audits/${auditId}`, rest, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件审核-审核通过(写手)
|
||||
export const putWorkAuditsAuditPassWriter = (writerCode: string, params = {}) => {
|
||||
const { id: auditId, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/writer/work-audits/${auditId}/audit-pass`, rest, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件审核-AI审查(写手)
|
||||
export const postWorkAuditsAiReviewWriter = (params = {}) => {
|
||||
const { id: auditId, writerCode, ...rest } = params as { id: string; writerCode: string; [key: string]: any };
|
||||
return Http.post(`/v1/writer/work-audits/${auditId}/ai-review`, rest, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件审核-获取AI审查结果(写手)
|
||||
export const getWorkAuditsAiReviewResultWriter = (id: string, ticket: string, writerCode: string) => {
|
||||
return Http.get(
|
||||
`/v1/writer/work-audits/${id}/ai-review/${ticket}`,
|
||||
{},
|
||||
{
|
||||
headers: { 'writer-code': writerCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-通过链接获取稿件
|
||||
export const postWorksByLinkWriter = (writerCode: string, params = {}) => {
|
||||
return Http.post('/v1/writer/works/by-link', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-通过文档获取稿件
|
||||
export const postWorksByFileWriter = (params = {}, config = {}) => {
|
||||
return Http.post('/v1/writer/works/by-file', params, config);
|
||||
};
|
||||
|
||||
// 获取图片上传地址
|
||||
export const getImagePreSignedUrlWriter = (writerCode: string,params = {}) => {
|
||||
return Http.get('/v1/writer/oss/image-pre-signed-url', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 获取视频上传地址
|
||||
export const getVideoPreSignedUrlWriter = (writerCode: string,params = {}) => {
|
||||
return Http.get('/v1/writer/oss/video-pre-signed-url', params, {
|
||||
headers: { 'writer-code': writerCode },
|
||||
});
|
||||
};
|
||||
152
src/api/all/generationWorkshop.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import Http from '@/api';
|
||||
|
||||
// 内容稿件-列表
|
||||
export const getWorksList = (params = {}) => {
|
||||
return Http.get('/v1/works/list', params);
|
||||
};
|
||||
|
||||
// 内容稿件-获取模板
|
||||
export const getTemplateUrl = (params = {}) => {
|
||||
return Http.get('/v1/works/template', params);
|
||||
};
|
||||
|
||||
// 内容稿件-通过链接获取稿件
|
||||
export const postWorksByLink = (params = {}) => {
|
||||
return Http.post('/v1/works/by-link', params);
|
||||
};
|
||||
|
||||
// 内容稿件-通过文档获取稿件
|
||||
export const postWorksByFile = (params = {}, config = {}) => {
|
||||
return Http.post('/v1/works/by-file', params, config);
|
||||
};
|
||||
|
||||
// 内容稿件-批量添加
|
||||
export const postWorksBatch = (params = {}) => {
|
||||
return Http.post('/v1/works/batch', params);
|
||||
};
|
||||
|
||||
// 生成分享链接
|
||||
export const postShareLinksGenerate = (params = {}) => {
|
||||
return Http.post('/v1/share-links/generate', params);
|
||||
};
|
||||
|
||||
// 生成写手链接
|
||||
export const getWriterLinksGenerate = () => {
|
||||
return Http.get('/v1/writer-links/generate');
|
||||
};
|
||||
|
||||
// 内容稿件-修改
|
||||
export const putWorksUpdate = (params = {}) => {
|
||||
const { id, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/works/${id}`, rest);
|
||||
};
|
||||
|
||||
// 内容稿件-删除
|
||||
export const deleteWork = (id: string) => {
|
||||
return Http.delete(`/v1/works/${id}`);
|
||||
};
|
||||
|
||||
// 内容稿件-分页
|
||||
export const getWorksPage = (params = {}) => {
|
||||
return Http.get('/v1/works', params);
|
||||
};
|
||||
|
||||
// 内容稿件-详情
|
||||
export const getWorksDetail = (id: string) => {
|
||||
return Http.get(`/v1/works/${id}`);
|
||||
};
|
||||
|
||||
// 内容稿件审核-分页
|
||||
export const getWorkAuditsPage = (params = {}) => {
|
||||
return Http.get('/v1/work-audits', params);
|
||||
};
|
||||
|
||||
// 内容稿件审核-详情
|
||||
export const getWorkAuditsDetail = (id: string) => {
|
||||
return Http.get(`/v1/work-audits/${id}`);
|
||||
};
|
||||
|
||||
// 内容稿件审核-多个详情
|
||||
export const getWorkAuditsBatchDetail = (params = {}) => {
|
||||
return Http.get('/v1/work-audits/list', params);
|
||||
};
|
||||
|
||||
// 内容稿件-审核
|
||||
export const patchWorkAuditsAudit = (id: string, params = {}) => {
|
||||
return Http.patch(`/v1/work-audits/${id}/audit`, params);
|
||||
};
|
||||
|
||||
// 内容稿件-批量审核
|
||||
export const patchWorkAuditsBatchAudit = (params = {}) => {
|
||||
return Http.patch('/v1/work-audits/batch-audit', params);
|
||||
};
|
||||
|
||||
// 内容稿件审核-修改
|
||||
export const putWorkAuditsUpdate = (params = {}) => {
|
||||
const { id: auditId, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/work-audits/${auditId}`, rest);
|
||||
};
|
||||
|
||||
// 内容稿件审核-审核通过
|
||||
export const putWorkAuditsAuditPass = (params = {}) => {
|
||||
const { id: auditId, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.put(`/v1/work-audits/${auditId}/audit-pass`, rest);
|
||||
};
|
||||
|
||||
// 内容稿件审核-AI审查
|
||||
export const postWorkAuditsAiReview = (params = {}) => {
|
||||
const { id, ...rest } = params as { id: string; [key: string]: any };
|
||||
return Http.post(`/v1/work-audits/${id}/ai-review`, rest);
|
||||
};
|
||||
|
||||
// 内容稿件审核-获取AI审查结果
|
||||
export const getWorkAuditsAiReviewResult = (id: string, ticket: string) => {
|
||||
return Http.get(`/v1/work-audits/${id}/ai-review/${ticket}`);
|
||||
};
|
||||
|
||||
// 内容稿件-列表(客户)
|
||||
export const getShareWorksList = (shareCode: string) => {
|
||||
return Http.get(
|
||||
'/v1/share/works/list',
|
||||
{},
|
||||
{
|
||||
headers: { 'share-code': shareCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-详情(客户)
|
||||
export const getShareWorksDetail = (id: string, shareCode: string) => {
|
||||
return Http.get(
|
||||
`/v1/share/works/${id}`,
|
||||
{},
|
||||
{
|
||||
headers: { 'share-code': shareCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-确认(客户)
|
||||
export const patchShareWorksConfirm = (id: string, shareCode: string) => {
|
||||
return Http.patch(
|
||||
`/v1/share/works/${id}/confirm`,
|
||||
{},
|
||||
{
|
||||
headers: { 'share-code': shareCode },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 内容稿件-评论(客户)
|
||||
export const postShareWorksComments = (id: string, shareCode: string, params = {}) => {
|
||||
return Http.post(`/v1/share/works/${id}/comments`, params, {
|
||||
headers: { 'share-code': shareCode },
|
||||
});
|
||||
};
|
||||
|
||||
// 内容稿件-删除评论(客户)
|
||||
export const deleteShareWorksComments = (id: string, commentId: string, shareCode: string) => {
|
||||
return Http.delete(`/v1/share/works/${id}/comments/${commentId}`, {
|
||||
headers: { 'share-code': shareCode },
|
||||
});
|
||||
};
|
||||
@ -10,6 +10,7 @@ import axios from 'axios';
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { handleUserLogout, goUserLogin } from '@/utils/user';
|
||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||
import { glsWithCatch } from '@/utils/stroage';
|
||||
import pinia from '@/stores';
|
||||
|
||||
const contentType = 'application/json';
|
||||
@ -43,7 +44,7 @@ export class Request {
|
||||
(config: AxiosRequestConfig) => {
|
||||
const store = useEnterpriseStore(pinia);
|
||||
|
||||
const token = localStorage.getItem('accessToken') as string;
|
||||
const token = glsWithCatch('accessToken');
|
||||
config.headers!.Authorization = token;
|
||||
|
||||
if (store.enterpriseInfo) {
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-close.png
Normal file
|
After Width: | Height: | Size: 604 B |
BIN
src/assets/img/creative-generation-workshop/icon-confirm.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-lf.png
Normal file
|
After Width: | Height: | Size: 346 B |
BIN
src/assets/img/creative-generation-workshop/icon-lf2.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
src/assets/img/creative-generation-workshop/icon-line.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-magic.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-photo.png
Normal file
|
After Width: | Height: | Size: 543 B |
BIN
src/assets/img/creative-generation-workshop/icon-play-hover.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-play.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-success.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-upload-fail.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/img/creative-generation-workshop/icon-video.png
Normal file
|
After Width: | Height: | Size: 513 B |
BIN
src/assets/img/error-img.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
6
src/assets/svg/svg-comment.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M4.99935 6.33398C5.36739 6.33416 5.66602 6.63257 5.66602 7.00065C5.66602 7.36873 5.36739 7.66714 4.99935 7.66732H4.33333C3.96514 7.66732 3.66667 7.36884 3.66667 7.00065C3.66667 6.63246 3.96514 6.33398 4.33333 6.33398H4.99935Z" fill="#55585F"/>
|
||||
<path d="M8.33333 6.33398C8.70152 6.33398 9 6.63246 9 7.00065C9 7.36884 8.70152 7.66732 8.33333 7.66732H7.66602C7.29783 7.66732 6.99935 7.36884 6.99935 7.00065C6.99935 6.63246 7.29783 6.33398 7.66602 6.33398H8.33333Z" fill="#55585F"/>
|
||||
<path d="M11.666 6.33398C12.0341 6.33416 12.3327 6.63257 12.3327 7.00065C12.3327 7.36873 12.0341 7.66714 11.666 7.66732H11C10.6318 7.66732 10.3333 7.36884 10.3333 7.00065C10.3333 6.63246 10.6318 6.33398 11 6.33398H11.666Z" fill="#55585F"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.666 1.33398C15.0342 1.33398 15.3327 1.63246 15.3327 2.00065V12.0007C15.3327 12.3688 15.0342 12.6673 14.666 12.6673H9.94206L8.4707 14.1387C8.21035 14.399 7.78834 14.399 7.528 14.1387L6.05664 12.6673H1.33268C0.964492 12.6673 0.666016 12.3688 0.666016 12.0007V2.00065C0.666016 1.63246 0.964492 1.33398 1.33268 1.33398H14.666ZM1.99935 11.334H6.33268C6.50949 11.334 6.67901 11.4043 6.80404 11.5293L7.99935 12.7246L9.19466 11.5293L9.24349 11.485C9.36213 11.3878 9.51129 11.334 9.66602 11.334H13.9993V2.66732H1.99935V11.334Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
src/assets/svg/svg-contentManuscript.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3997 1.33398C11.2944 1.33398 11.9999 2.0689 12 2.95052V6.66732H13.3333C14.0697 6.66732 14.6667 7.26426 14.6667 8.00065V13.6673C14.6667 14.5878 13.9205 15.334 13 15.334H2.93359C2.09483 15.334 1.42261 14.6881 1.3418 13.8809L1.33333 13.7174V2.95052C1.3334 2.06889 2.03891 1.33398 2.93359 1.33398H10.3997ZM12 14.0007H13C13.1841 14.0007 13.3333 13.8514 13.3333 13.6673V8.00065H12V14.0007ZM4 5.66732C3.63181 5.66732 3.33333 5.96579 3.33333 6.33398C3.33333 6.70217 3.63181 7.00065 4 7.00065H8C8.36819 7.00065 8.66667 6.70217 8.66667 6.33398C8.66667 5.96579 8.36819 5.66732 8 5.66732H4ZM4 3.33398C3.63181 3.33398 3.33333 3.63246 3.33333 4.00065C3.33333 4.36884 3.63181 4.66732 4 4.66732H6.66667C7.03486 4.66732 7.33333 4.36884 7.33333 4.00065C7.33333 3.63246 7.03486 3.33398 6.66667 3.33398H4Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 966 B |
@ -88,30 +88,20 @@ export default defineComponent({
|
||||
// 跳过没有 name 的菜单项,防止 key 报错
|
||||
if (!element?.name) return;
|
||||
|
||||
// const icon element?.meta?.icon
|
||||
const icon = element?.meta?.icon
|
||||
? (() => {
|
||||
if (typeof element.meta.icon === 'string') {
|
||||
return h(
|
||||
'svg',
|
||||
{
|
||||
style: {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('use', {
|
||||
'xlink:href': element.meta.icon,
|
||||
}),
|
||||
],
|
||||
return (
|
||||
<svg class="w-16px h-16px">
|
||||
<use xlinkHref={element.meta.icon} />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
// 如果是对象,按原来的方式渲染
|
||||
return h(element.meta.icon as object);
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
|
||||
if (element.children && element.children.length > 0) {
|
||||
nodes.push(
|
||||
<a-sub-menu
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
* @Date: 2025-06-19 01:45:53
|
||||
*/
|
||||
import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
|
||||
|
||||
// import { useAppStore } from '@/stores';
|
||||
import { appRoutes } from '@/router/routes';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
|
||||
export default function useMenuTree() {
|
||||
// const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const appRoutes = router.options?.routes ?? [];
|
||||
|
||||
const sidebarStore = useSidebarStore();
|
||||
const appRoute = computed(() => {
|
||||
const _filterRoutes = appRoutes.filter((v) => v.meta?.id === sidebarStore.activeMenuId);
|
||||
|
||||
@ -33,9 +33,11 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
import router from '@/router';
|
||||
// import router from '@/router';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const sidebarStore = useSidebarStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="navbar-wrap">
|
||||
<div class="left-wrap">
|
||||
<a-space class="cursor-pointer" @click="router.push('/')">
|
||||
<div class="h-full flex items-center cursor-pointer" @click="handleUserHome">
|
||||
<img src="@/assets/LOGO.svg" alt="" />
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<NavbarMenu v-if="!isAgentRoute"/>
|
||||
@ -16,6 +16,7 @@
|
||||
import NavbarMenu from './components/navbar-menu';
|
||||
import RightSide from './components/right-side';
|
||||
|
||||
import { handleUserHome } from '@/utils/user.ts';
|
||||
import router from '@/router';
|
||||
|
||||
const route = useRoute();
|
||||
@ -34,7 +35,7 @@ const isAgentRoute = computed(() => {
|
||||
.left-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.arco-dropdown-option-suffix {
|
||||
display: none;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
:multiple="multiple"
|
||||
size="medium"
|
||||
:placeholder="placeholder"
|
||||
allow-clear
|
||||
:allow-clear="allClear"
|
||||
:max-tag-count="maxTagCount"
|
||||
@change="handleChange"
|
||||
>
|
||||
@ -42,6 +42,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
allClear: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-16 11:58:01
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-16 16:56:27
|
||||
* @Description: 二次确认框
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-popconfirm
|
||||
:content="content"
|
||||
:position="position"
|
||||
:ok-text="okText"
|
||||
:cancel-text="cancelText"
|
||||
:type="popupType"
|
||||
@ok="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<slot></slot>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue-demi';
|
||||
|
||||
type Position = 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
|
||||
|
||||
type PopupType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '是否确认?',
|
||||
},
|
||||
position: {
|
||||
type: String as PropType<Position>,
|
||||
default: 'top',
|
||||
},
|
||||
okText: {
|
||||
type: String,
|
||||
default: '确定',
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
popupType: {
|
||||
type: String as PropType<PopupType>,
|
||||
default: 'info',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirmEmit', 'cancelEmit']);
|
||||
|
||||
/**
|
||||
* 确定事件
|
||||
*/
|
||||
function handleConfirm() {
|
||||
emit('confirmEmit');
|
||||
}
|
||||
/**
|
||||
* 确定事件
|
||||
*/
|
||||
function handleCancel() {
|
||||
emit('cancelEmit');
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
93
src/components/hover-image-preview/index.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<!--
|
||||
* @Author: RenXiaoDong
|
||||
* @Date: 2025-08-11 22:15:35
|
||||
-->
|
||||
<template>
|
||||
<a-popover
|
||||
:trigger="'hover'"
|
||||
class="hover-big-image-preview-popover"
|
||||
:position="props.position"
|
||||
:mouse-enter-delay="props.enterDelay"
|
||||
:mouse-leave-delay="props.leaveDelay"
|
||||
:disabled="!props.src"
|
||||
>
|
||||
<template #content>
|
||||
<div class="preview-container" :style="containerStyle">
|
||||
<img :src="props.src" alt="preview" class="preview-image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import type { ImageOrientation } from '@/utils/tools';
|
||||
import { getImageOrientationByUrl } from '@/utils/tools';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
position?: 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
|
||||
enterDelay?: number;
|
||||
leaveDelay?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
position: 'right',
|
||||
enterDelay: 100,
|
||||
leaveDelay: 200,
|
||||
});
|
||||
|
||||
const orientation = ref<ImageOrientation>('landscape');
|
||||
|
||||
const resolveOrientation = async () => {
|
||||
if (!props.src) {
|
||||
orientation.value = 'landscape';
|
||||
return;
|
||||
}
|
||||
const o = await getImageOrientationByUrl(props.src);
|
||||
orientation.value = o === 'square' ? 'landscape' : o;
|
||||
};
|
||||
|
||||
onMounted(resolveOrientation);
|
||||
watch(() => props.src, resolveOrientation);
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
// 竖图: 306x400;横图: 400x251
|
||||
const isPortrait = orientation.value === 'portrait';
|
||||
const width = isPortrait ? 306 : 400;
|
||||
const height = isPortrait ? 400 : 251;
|
||||
return {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
} as Record<string, string>;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hover-big-image-preview-popover {
|
||||
.arco-popover-popup-content {
|
||||
padding: 16px !important;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -3,22 +3,20 @@
|
||||
<template #content>
|
||||
<div :style="contentStyle" class="tip-content">{{ props.context }}</div>
|
||||
</template>
|
||||
<div class="overflow-hidden">
|
||||
<div v-bind="$attrs" ref="Text" :class="`${isShow ? '' : `line-${props.line}`} `" class="overflow-text">
|
||||
{{ props.context }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isShowBtn && !disabled"
|
||||
class="color-#8C8C8C flex items-center cursor-pointer mt-2px"
|
||||
@click="
|
||||
() => {
|
||||
isShow = !isShow;
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ isShow ? '收起' : '展开' }}
|
||||
<icon-up size="16" :class="{ active: isShow }" class="ml-2px color-#8C8C8C" />
|
||||
</div>
|
||||
<div v-bind="$attrs" ref="Text" :class="`${isShow ? '' : `line-${props.line}`} `" class="overflow-text">
|
||||
{{ props.context }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isShowBtn && !disabled"
|
||||
class="color-#8C8C8C flex items-center cursor-pointer mt-2px"
|
||||
@click="
|
||||
() => {
|
||||
isShow = !isShow;
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ isShow ? '收起' : '展开' }}
|
||||
<icon-up size="16" :class="{ active: isShow }" class="ml-2px color-#8C8C8C" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
# 动态配置form表单
|
||||
示例见 views/components/form
|
||||
参数 fieldList:配置项,包括arco.design Form.Item和Input、Select以及自定义属性component等
|
||||
参数 model: 传默认值
|
||||
通过ref获取实例,调用子组件实例updateFieldsList方法更新配置项,调用setModel方法更新数据,调用setForm方法更新Form属性,自定义事件change处理逻辑
|
||||
@ -1,40 +0,0 @@
|
||||
// form.item的属性名称集合
|
||||
export const formItemKeys = [
|
||||
'field',
|
||||
'label',
|
||||
'tooltip',
|
||||
'showColon',
|
||||
'noStyle',
|
||||
'disabled',
|
||||
'help',
|
||||
'extra',
|
||||
'required',
|
||||
'asteriskPosition',
|
||||
'rules',
|
||||
'validateStatus',
|
||||
'validateTrigger',
|
||||
'wrapperColProps',
|
||||
'hideLabel',
|
||||
'hideAsterisk',
|
||||
'labelColStyle',
|
||||
'wrapperColStyle',
|
||||
'rowProps',
|
||||
'rowClass',
|
||||
'contentClass',
|
||||
'contentFlex',
|
||||
'labelColFlex',
|
||||
'feedback',
|
||||
'labelComponent',
|
||||
'labelAttrs',
|
||||
];
|
||||
// 自定义属性名称集合
|
||||
export const customKeys = ['component', 'lists'];
|
||||
// 响应式栅格默认配置
|
||||
export const COL_PROPS = {
|
||||
xs: 12,
|
||||
sm: 12,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
xxl: 6,
|
||||
};
|
||||
@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<slot name="header"></slot>
|
||||
<a-form v-bind="_options" ref="formRef" :model="model" @submit.prevent>
|
||||
<a-row v-bind="rowProps" :gutter="20">
|
||||
<template
|
||||
v-for="{ field, component, formItemProps, componentProps, lists, colProps } in newFieldList"
|
||||
:key="field"
|
||||
>
|
||||
<!-- 单选框 -->
|
||||
<a-col v-if="component === 'radio'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-radio-group v-bind="componentProps" v-model="model[field]">
|
||||
<a-radio v-for="val in lists" :key="val['value']" :label="val['value']" size="large">
|
||||
{{ val['label'] }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 复选框 -->
|
||||
<a-col v-if="component === 'checkbox'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-checkbox-group v-bind="componentProps" v-model="model[field]">
|
||||
<a-checkbox v-for="c in lists" :key="c['value']" :label="c['value']">{{ c['label'] }}</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 下拉框 -->
|
||||
<a-col v-if="component === 'select'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-select v-bind="componentProps" v-model="model[field]">
|
||||
<a-option v-for="s in lists" :key="s['value']" :label="s['label']" :value="s['value']" />
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 文本域 -->
|
||||
<a-col v-if="component === 'textarea'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-textarea v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 时间选择器 -->
|
||||
<a-col v-if="component === 'time'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-time-picker v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 日期选择器 -->
|
||||
<a-col v-if="component === 'date'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-date-picker v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 日期范围选择器 -->
|
||||
<a-col v-if="component === 'rangeDate'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-range-picker v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 级联选择器 -->
|
||||
<a-col v-if="component === 'cascader'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-cascader v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 数字输入框 -->
|
||||
<a-col v-if="component === 'inputNumber'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-input-number v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 输入框 -->
|
||||
<a-col v-if="component === 'input'" v-bind="colProps">
|
||||
<a-form-item v-bind="formItemProps">
|
||||
<a-input v-bind="componentProps" v-model="model[field]" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 标题模块 -->
|
||||
<a-col v-if="component === 'title'" :span="24">
|
||||
<div class="title">
|
||||
<div class="bar"></div>
|
||||
<h4 class="text">{{ formItemProps.label }}</h4>
|
||||
</div>
|
||||
</a-col>
|
||||
<!-- 自定义插槽slot -->
|
||||
<a-col v-if="component === 'slot'" :span="24">
|
||||
<slot :name="field"></slot>
|
||||
</a-col>
|
||||
</template>
|
||||
<a-col :span="24">
|
||||
<a-form-item>
|
||||
<slot name="buttons" :model="model" :formRef="formRef">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onSubmit(formRef)">{{ _options.submitButtonText }}</a-button>
|
||||
<a-button v-if="_options.showResetButton" @click="resetForm(formRef)">
|
||||
{{ _options.resetButtonText }}
|
||||
</a-button>
|
||||
<a-button v-if="_options.showCancelButton" @click="emit('cancel')">
|
||||
{{ _options.cancelButtonText }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</slot>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, RowProps, ValidatedError } from '@arco-design/web-vue';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import type { Form } from './interface';
|
||||
import { formItemKeys, customKeys, COL_PROPS } from './constants';
|
||||
import { changeFormList } from './utils';
|
||||
import type { FieldData } from '@arco-design/web-vue/es/form/interface';
|
||||
// 父组件传递的值
|
||||
interface Props {
|
||||
fieldList: Form.FieldItem[];
|
||||
model?: Record<string, any>;
|
||||
options?: Form.Options;
|
||||
rowProps?: RowProps;
|
||||
}
|
||||
interface EmitEvent {
|
||||
(e: 'submit' | 'change', params: any): void;
|
||||
(e: 'reset' | 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<EmitEvent>();
|
||||
// 表单的数据
|
||||
let model = ref<Record<string, any>>({});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 初始化处理Form组件属性options
|
||||
const _options = ref<Record<string, any>>({});
|
||||
const initOptions = () => {
|
||||
const option = {
|
||||
layout: 'vertical',
|
||||
disabled: false,
|
||||
submitButtonText: '提交',
|
||||
resetButtonText: '重置',
|
||||
cancelButtonText: '取消',
|
||||
showResetButton: true,
|
||||
};
|
||||
Object.assign(option, props?.options);
|
||||
_options.value = option;
|
||||
};
|
||||
initOptions();
|
||||
// 初始化处理model
|
||||
const initFormModel = () => {
|
||||
props.fieldList.forEach((item: Form.FieldItem) => {
|
||||
// 如果类型为checkbox,默认值需要设置一个空数组
|
||||
const value = item.component === 'checkbox' ? [] : '';
|
||||
const { field, component } = item;
|
||||
if (component !== 'slot' && component !== 'title') {
|
||||
model.value[item.field] = props?.model?.[field] || value;
|
||||
}
|
||||
});
|
||||
};
|
||||
initFormModel();
|
||||
// 初始化处理fieldList
|
||||
const newFieldList: any = ref(null);
|
||||
const initFieldList = () => {
|
||||
const list = props?.fieldList.map((item: Form.FieldItem) => {
|
||||
const customProps = pick(item, customKeys);
|
||||
const formItemProps = pick(item, formItemKeys);
|
||||
const componentProps = omit(item, [...formItemKeys, ...customKeys, 'field', 'colProps']);
|
||||
const { colProps = {}, field, placeholder, component = 'input', label } = item;
|
||||
componentProps.onChange = (val: any) => onChange(field, val);
|
||||
const newColProps = {
|
||||
...colProps,
|
||||
...COL_PROPS,
|
||||
};
|
||||
const obj = {
|
||||
field,
|
||||
colProps: newColProps,
|
||||
...customProps,
|
||||
formItemProps,
|
||||
componentProps,
|
||||
};
|
||||
if ((component === 'input' || component === 'textarea') && !placeholder) {
|
||||
componentProps.placeholder = `请输入${label}`;
|
||||
}
|
||||
if (component === 'select' && !placeholder) {
|
||||
componentProps.placeholder = `请选择${label}`;
|
||||
}
|
||||
if (component === 'rangeDate') {
|
||||
componentProps.value = [null, null];
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
newFieldList.value = list;
|
||||
return list;
|
||||
};
|
||||
initFieldList();
|
||||
// 提交
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
let flag = false;
|
||||
await formEl.validate((errors: undefined | Record<string, ValidatedError>) => {
|
||||
if (!errors) {
|
||||
emit('submit', model.value);
|
||||
flag = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return (flag && model.value) || null;
|
||||
};
|
||||
// 提交--父组件调用
|
||||
const submit = () => {
|
||||
return onSubmit(formRef.value);
|
||||
};
|
||||
// 重置
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
};
|
||||
// 表单变化
|
||||
const onChange = (key: string, val: any) => {
|
||||
emit('change', { key, val });
|
||||
};
|
||||
// 设置
|
||||
const setModel = (data: Record<string, FieldData>) => {
|
||||
const newData = {
|
||||
...model.value,
|
||||
...data,
|
||||
};
|
||||
model.value = newData;
|
||||
};
|
||||
// 设置Form
|
||||
const setForm = (data: Form.Options) => {
|
||||
const options = {
|
||||
..._options.value,
|
||||
...data,
|
||||
};
|
||||
_options.value = options;
|
||||
};
|
||||
// 更新配置项
|
||||
const updateFieldsList = (updateList: any[]) => {
|
||||
const list = changeFormList(newFieldList.value, updateList);
|
||||
newFieldList.value = list;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
submit,
|
||||
setModel,
|
||||
setForm,
|
||||
updateFieldsList,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-picker) {
|
||||
width: 100%;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.bar {
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
border-radius: 1px;
|
||||
margin-right: 8px;
|
||||
background-color: rgb(var(--primary-6));
|
||||
}
|
||||
.text {
|
||||
font-size: 16px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/components/wyg-form/interface.d.ts
vendored
@ -1,95 +0,0 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue/es/form/interface';
|
||||
import { Size, ColProps, CascaderOption, InputProps } from '@arco-design/web-vue';
|
||||
console.log(InputProps, 'InputProps=====');
|
||||
|
||||
export namespace Form {
|
||||
/** 表单项自身Props */
|
||||
interface FormItem<T = string> {
|
||||
field: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
showColon?: boolean;
|
||||
noStyle?: boolean;
|
||||
disabled?: boolean;
|
||||
help?: string;
|
||||
extra?: string;
|
||||
required?: boolean;
|
||||
asteriskPosition?: 'start' | 'end';
|
||||
rules?: FieldRule | FieldRule[];
|
||||
validateStatus?: 'success' | 'warning' | 'error' | 'validating';
|
||||
validateTrigger?: 'change' | 'input' | 'focus' | 'blur';
|
||||
labelColProps?: object;
|
||||
wrapperColProps?: object;
|
||||
hideLabel?: boolean;
|
||||
hideAsterisk?: boolean;
|
||||
labelColStyle?: object;
|
||||
wrapperColStyle?: object;
|
||||
rowProps?: object;
|
||||
rowClass?: string | Array<T> | object;
|
||||
contentClass?: string | Array<T> | object;
|
||||
contentFlex?: boolean;
|
||||
labelColFlex?: number | string;
|
||||
feedback?: boolean;
|
||||
labelComponent?: string;
|
||||
labelAttrs?: object;
|
||||
}
|
||||
// 当前 fieldItem 的类型 默认值'input'
|
||||
type ComponentType =
|
||||
| 'input'
|
||||
| 'textarea'
|
||||
| 'radio'
|
||||
| 'checkbox'
|
||||
| 'select'
|
||||
| 'time'
|
||||
| 'date'
|
||||
| 'rangeDate'
|
||||
| 'inputNumber'
|
||||
| 'cascader'
|
||||
| 'title'
|
||||
| 'slot';
|
||||
/** 自定义Props */
|
||||
interface CustomProps {
|
||||
component?: ComponentType;
|
||||
lists?: object; // 如果 type='checkbox' / 'radio' / 'select'时,需传入此配置项。格式参考FieldItemOptions配置项
|
||||
}
|
||||
/** Input、Select组件等的Props */
|
||||
interface ComponentProps {
|
||||
placeholder?: string; // 输入框占位文本
|
||||
readonly?: boolean; // 是否只读 false
|
||||
allowClear?: boolean; // 是否可清空 false
|
||||
onChange?: Function;
|
||||
options?: CascaderOption[];
|
||||
}
|
||||
/** 每一项配置项的属性 */
|
||||
interface FieldItem extends FormItem, CustomProps, ComponentProps {
|
||||
colProps?: ColProps;
|
||||
}
|
||||
/** 处理后的配置项属性 */
|
||||
interface NewFieldItem extends CustomProps {
|
||||
formItemProps: FormItem;
|
||||
componentProps: ComponentProps;
|
||||
field: string;
|
||||
colProps?: ColProps;
|
||||
}
|
||||
interface FieldItemOptions {
|
||||
label: string | number;
|
||||
value: string | number;
|
||||
}
|
||||
/** 表单Form自身Props */
|
||||
interface Options {
|
||||
layout?: 'horizontal' | 'vertical' | 'inline'; // 表单的布局方式,包括水平、垂直、多列
|
||||
size?: Size; // 用于控制该表单内组件的尺寸
|
||||
labelColProps?: object; // 标签元素布局选项。参数同 <col> 组件一致,默认值span: 5, offset: 0
|
||||
wrapperColProps?: object; // 表单控件布局选项。参数同 <col> 组件一致,默认值span: 19, offset: 0
|
||||
labelAlign?: 'left' | 'right'; // 标签的对齐方向,默认值'right'
|
||||
disabled?: boolean; // 是否禁用表单
|
||||
rules?: Record<string, FieldRule | FieldRule[]>; // 表单项校验规则
|
||||
autoLabelWidth?: boolean; // 是否开启自动标签宽度,仅在 layout="horizontal" 下生效。默认值false
|
||||
|
||||
showResetButton?: boolean; // 是否展示重置按钮
|
||||
showCancelButton?: boolean; // 是否展示取消按钮
|
||||
submitButtonText?: string;
|
||||
resetButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { formItemKeys, customKeys, COL_PROPS } from './constants';
|
||||
import type { Form } from './interface';
|
||||
// fieldList更新
|
||||
export const changeFormList = (formList: Form.NewFieldItem[], updateList: Form.FieldItem[]): Form.NewFieldItem[] => {
|
||||
let list: any = formList;
|
||||
list.forEach((item: any, index: string | number) => {
|
||||
updateList.forEach((ele: any) => {
|
||||
if (item.field === ele.field) {
|
||||
list[index] = { ...item, ...ele };
|
||||
const keys: string[] = Object.keys(ele);
|
||||
keys.forEach((key: string) => {
|
||||
const val = ele[key];
|
||||
if (formItemKeys.includes(key)) {
|
||||
list[index].formItemProps[key] = val;
|
||||
} else if (customKeys.includes(key) || key === 'colProps') {
|
||||
list[index][key] = val;
|
||||
} else {
|
||||
list[index].componentProps[key] = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return list;
|
||||
};
|
||||
@ -1,73 +0,0 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-16 14:40:38
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-16 16:37:52
|
||||
* @Description: table公用封装
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-table
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
:data="tableData"
|
||||
:bordered="{ cell: borderCell }"
|
||||
:pagination="setPagination"
|
||||
page-position="br"
|
||||
v-bind="propsRes"
|
||||
v-on="propsEvent"
|
||||
>
|
||||
<template #columns>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationProps, TableData } from '@arco-design/web-vue';
|
||||
import type { PropType } from 'vue-demi';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
|
||||
type Size = 'mini' | 'small' | 'medium' | 'large';
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String as PropType<Size>,
|
||||
default: 'large',
|
||||
},
|
||||
tableData: {
|
||||
type: Array as PropType<TableData[]>,
|
||||
default: () => [],
|
||||
},
|
||||
borderCell: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
pagination: {
|
||||
type: Object as PropType<PaginationProps>,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const loading = ref(false);
|
||||
|
||||
const setPagination = computed(() => {
|
||||
const defaultPagination: PaginationProps = {
|
||||
showPageSize: true,
|
||||
showTotal: true,
|
||||
showMore: true,
|
||||
size: 'large',
|
||||
};
|
||||
return Object.assign(defaultPagination, props.pagination);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.tableData.length === 0) {
|
||||
loading.value = true;
|
||||
} else {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
80
src/hooks/useGetAiReviewResult.ts
Normal file
@ -0,0 +1,80 @@
|
||||
export default function useGetAiReviewResult({
|
||||
cardInfo,
|
||||
updateAiReview,
|
||||
startAiReviewFn,
|
||||
getAiReviewResultFn,
|
||||
}: {
|
||||
cardInfo: any;
|
||||
updateAiReview: (ai_review: any) => void;
|
||||
startAiReviewFn: (params: {
|
||||
id: string;
|
||||
platform: string;
|
||||
content: string;
|
||||
writerCode: string | undefined;
|
||||
}) => Promise<any>;
|
||||
getAiReviewResultFn: (id: string, ticket: string, writerCode: string | undefined) => Promise<any>;
|
||||
}) {
|
||||
const route = useRoute();
|
||||
const statusPollingTimer = ref<number | null>(null);
|
||||
const ticket = ref('');
|
||||
const checkLoading = ref(false);
|
||||
const checkResult = ref<any>({});
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
const handleStartCheck = async () => {
|
||||
checkLoading.value = true;
|
||||
const { id, platform, content } = cardInfo.value;
|
||||
const { code, data } = await startAiReviewFn({
|
||||
id,
|
||||
platform,
|
||||
content,
|
||||
writerCode: writerCode.value as string | undefined,
|
||||
});
|
||||
if (code === 200) {
|
||||
ticket.value = data.ticket;
|
||||
startStatusPolling();
|
||||
}
|
||||
};
|
||||
const handleAgainCheck = async () => {
|
||||
checkResult.value = {};
|
||||
ticket.value = '';
|
||||
clearStatusPollingTimer();
|
||||
handleStartCheck();
|
||||
};
|
||||
const startStatusPolling = () => {
|
||||
clearStatusPollingTimer();
|
||||
statusPollingTimer.value = setInterval(async () => {
|
||||
const { code, data } = await getAiReviewResultFn(
|
||||
cardInfo.value.id,
|
||||
ticket.value,
|
||||
writerCode.value as string | undefined,
|
||||
);
|
||||
if (code === 200 && data.status === 1) {
|
||||
checkResult.value = data.ai_review;
|
||||
updateAiReview?.(data.ai_review);
|
||||
checkLoading.value = false;
|
||||
clearStatusPollingTimer();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const clearStatusPollingTimer = () => {
|
||||
if (statusPollingTimer.value) {
|
||||
clearInterval(statusPollingTimer.value);
|
||||
statusPollingTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
clearStatusPollingTimer();
|
||||
});
|
||||
|
||||
return {
|
||||
handleStartCheck,
|
||||
handleAgainCheck,
|
||||
checkResult,
|
||||
checkLoading,
|
||||
ticket,
|
||||
};
|
||||
}
|
||||
@ -45,6 +45,7 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
|
||||
|
||||
// 全选/取消全选
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
console.log('handleSelectAll', checked)
|
||||
const currentPageRows = dataSource.value;
|
||||
const currentPageKeys = currentPageRows.map((v) => v[rowKey]);
|
||||
|
||||
|
||||
@ -108,16 +108,19 @@ provide('toggleDrawerMenu', () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$nav-size-height: 72px;
|
||||
$layout-max-width: 1100px;
|
||||
|
||||
.layout {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
}
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: $nav-size-height;
|
||||
height: $navbar-height;
|
||||
}
|
||||
.layout-sider {
|
||||
position: fixed;
|
||||
|
||||
12
src/main.ts
@ -7,8 +7,8 @@ import router from './router';
|
||||
import store from './stores';
|
||||
import * as directives from '@/directives';
|
||||
|
||||
import NoData from '@/components/no-data';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import NoData from '@/components/no-data/index.vue';
|
||||
import SvgIcon from '@/components/svg-icon/index.vue';
|
||||
|
||||
import '@/api/index';
|
||||
import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式
|
||||
@ -24,10 +24,6 @@ const app = createApp(App);
|
||||
|
||||
app.component('NoData', NoData);
|
||||
app.component('SvgIcon', SvgIcon);
|
||||
(Object.keys(directives) as Array<keyof typeof directives>).forEach((k) => app.use(directives[k])); // 注册指令
|
||||
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
|
||||
Object.keys(directives).forEach((k) => app.use(directives[k])); // 注册指令
|
||||
|
||||
app.mount('#app');
|
||||
app.use(store).use(router).mount('#app');
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* @Author: RenXiaoDong
|
||||
* @Date: 2025-06-19 01:45:53
|
||||
*/
|
||||
import { appRoutes } from '../routes';
|
||||
|
||||
const mixinRoutes = [...appRoutes];
|
||||
|
||||
const appClientMenus = mixinRoutes.map((el) => {
|
||||
const { name, path, meta, redirect, children } = el;
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
meta,
|
||||
redirect,
|
||||
children,
|
||||
};
|
||||
});
|
||||
|
||||
export default mixinRoutes;
|
||||
@ -23,8 +23,10 @@ export const DEFAULT_ROUTE = {
|
||||
|
||||
export const MENU_GROUP_IDS = {
|
||||
DATA_ENGINE_ID: 1, // 全域数据分析
|
||||
MANAGEMENT_ID: -1, // 管理中心
|
||||
PROPERTY_ID: 10, // 资产营销平台
|
||||
WORK_BENCH_ID: -99, // 工作台
|
||||
AGENT: 2, // 智能体
|
||||
MANAGEMENT_ID: 3, // 管理中心
|
||||
PROPERTY_ID: 4, // 资产营销平台
|
||||
WORK_BENCH_ID: 5, // 工作台
|
||||
WRITER_CREATIVE_GENERATION_WORKSHOP_ID: 6, // 内容稿件-写手侧
|
||||
CREATIVE_GENERATION_WORKSHOP_ID: 7, // 创意生成工坊
|
||||
};
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
*/
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { appRoutes } from './routes';
|
||||
import { NOT_FOUND_ROUTE } from './routes/base';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import { MENU_GROUP_IDS } from './constants';
|
||||
@ -18,7 +17,7 @@ export const router = createRouter({
|
||||
{
|
||||
path: '/login',
|
||||
name: 'UserLogin',
|
||||
component: () => import('@/views/components/login'),
|
||||
component: () => import('@/views/components/login/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
@ -27,7 +26,7 @@ export const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/components/workplace'),
|
||||
component: () => import('@/views/components/workplace/index.vue'),
|
||||
meta: {
|
||||
hideSidebar: true,
|
||||
requiresAuth: false,
|
||||
@ -35,8 +34,18 @@ export const router = createRouter({
|
||||
id: MENU_GROUP_IDS.WORK_BENCH_ID,
|
||||
},
|
||||
},
|
||||
|
||||
...appRoutes,
|
||||
NOT_FOUND_ROUTE,
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: () => import('@/layouts/NotFound.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
hideInMenu: true,
|
||||
hideSidebar: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 };
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||
|
||||
// export const REDIRECT_MAIN: RouteRecordRaw = {
|
||||
// path: '/redirect',
|
||||
// name: 'redirect',
|
||||
// meta: {
|
||||
// requiresAuth: false,
|
||||
// requireLogin: false,
|
||||
// hideInMenu: true,
|
||||
// },
|
||||
// children: [
|
||||
// {
|
||||
// path: '/redirect/:path',
|
||||
// name: REDIRECT_ROUTE_NAME,
|
||||
// component: () => import('@/layouts/Basic.vue'),
|
||||
// meta: {
|
||||
// requiresAuth: false,
|
||||
// requireLogin: false,
|
||||
// hideInMenu: true,
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
export const NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: () => import('@/layouts/NotFound.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
hideInMenu: true,
|
||||
hideSidebar: true,
|
||||
},
|
||||
};
|
||||
138
src/router/routes/modules/creativeGenerationWorkshop.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
||||
|
||||
import IconContentManuscript from '@/assets/svg/svg-contentManuscript.svg';
|
||||
|
||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/manuscript',
|
||||
name: 'Manuscript',
|
||||
redirect: 'manuscript/list',
|
||||
meta: {
|
||||
locale: '内容稿件',
|
||||
icon: IconContentManuscript,
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
id: MENU_GROUP_IDS.CREATIVE_GENERATION_WORKSHOP_ID,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'ManuscriptList',
|
||||
meta: {
|
||||
locale: '内容稿件列表',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/list/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
name: 'ManuscriptUpload',
|
||||
meta: {
|
||||
locale: '稿件上传',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'ManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/upload/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'edit/:id',
|
||||
name: 'ManuscriptEdit',
|
||||
meta: {
|
||||
locale: '账号详情',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'ManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/edit/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
name: 'ManuscriptDetail',
|
||||
meta: {
|
||||
locale: '稿件详情',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'ManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check-list',
|
||||
name: 'ManuscriptCheckList',
|
||||
meta: {
|
||||
locale: '内容稿件审核',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/check-list/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check-list/detail/:id',
|
||||
name: 'ManuscriptCheckListDetail',
|
||||
meta: {
|
||||
locale: '内容稿件审核详情',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
hideFooter: true,
|
||||
hideInMenu: true,
|
||||
roles: ['*'],
|
||||
activeMenu: 'ManuscriptCheckList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check',
|
||||
name: 'ManuscriptCheck',
|
||||
meta: {
|
||||
locale: '稿件审核',
|
||||
requiresAuth: true,
|
||||
requireLogin: true,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'ManuscriptCheckList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript/check/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/explore/list/:shareCode',
|
||||
name: 'ExploreList',
|
||||
meta: {
|
||||
locale: '分享链接列表',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/explore/list/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/explore/detail/:shareCode/:id',
|
||||
name: 'ExploreDetail',
|
||||
meta: {
|
||||
locale: '分享链接详情',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/explore/detail/index.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
export default COMPONENTS;
|
||||
@ -19,7 +19,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: 'person',
|
||||
name: 'ManagementPerson',
|
||||
component: () => import('@/views/components/management/person'),
|
||||
component: () => import('@/views/components/management/person/index.vue'),
|
||||
meta: {
|
||||
locale: '个人信息',
|
||||
requiresAuth: false,
|
||||
@ -30,7 +30,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: 'enterprise',
|
||||
name: 'ManagementEnterprise',
|
||||
component: () => import('@/views/components/management/enterprise'),
|
||||
component: () => import('@/views/components/management/enterprise/index.vue'),
|
||||
meta: {
|
||||
locale: '企业信息',
|
||||
requiresAuth: false,
|
||||
@ -41,7 +41,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: 'account',
|
||||
name: 'ManagementAccount',
|
||||
component: () => import('@/views/components/management/account'),
|
||||
component: () => import('@/views/components/management/account/index.vue'),
|
||||
meta: {
|
||||
locale: '账号管理',
|
||||
requiresAuth: false,
|
||||
|
||||
115
src/router/routes/modules/manuscript-writer.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import IconContentManuscript from '@/assets/svg/svg-contentManuscript.svg';
|
||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
||||
|
||||
// 内容稿件-写手端
|
||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/writer/manuscript',
|
||||
name: 'WriterManuscript',
|
||||
redirect: 'writer/manuscript/list',
|
||||
meta: {
|
||||
locale: '内容稿件',
|
||||
icon: IconContentManuscript,
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
roles: ['*'],
|
||||
id: MENU_GROUP_IDS.WRITER_CREATIVE_GENERATION_WORKSHOP_ID,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list/:writerCode',
|
||||
name: 'WriterManuscriptList',
|
||||
meta: {
|
||||
locale: '内容稿件列表',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/list/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'upload/:writerCode',
|
||||
name: 'WriterManuscriptUpload',
|
||||
meta: {
|
||||
locale: '稿件上传',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'WriterManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/upload/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'edit/:writerCode/:id',
|
||||
name: 'WriterManuscriptEdit',
|
||||
meta: {
|
||||
locale: '账号详情',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'WriterManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/edit/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'detail/:writerCode/:id',
|
||||
name: 'WriterManuscriptDetail',
|
||||
meta: {
|
||||
locale: '稿件详情',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'ManuscriptList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check-list/:writerCode',
|
||||
name: 'WriterManuscriptCheckList',
|
||||
meta: {
|
||||
locale: '内容稿件审核',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/check-list/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check-list/detail/:id/:writerCode',
|
||||
name: 'WriterManuscriptCheckListDetail',
|
||||
meta: {
|
||||
locale: '内容稿件审核详情',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
hideFooter: true,
|
||||
hideInMenu: true,
|
||||
roles: ['*'],
|
||||
activeMenu: 'WriterManuscriptCheckList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'check/:writerCode',
|
||||
name: 'WriterManuscriptCheck',
|
||||
meta: {
|
||||
locale: '稿件审核',
|
||||
requiresAuth: false,
|
||||
requireLogin: false,
|
||||
hideFooter: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
activeMenu: 'WriterManuscriptCheckList',
|
||||
},
|
||||
component: () => import('@/views/creative-generation-workshop/manuscript-writer/check/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export default COMPONENTS;
|
||||
@ -60,7 +60,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/media-account/account-manage'),
|
||||
component: () => import('@/views/property-marketing/media-account/account-manage/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
@ -71,7 +71,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/media-account/account-dashboard'),
|
||||
component: () => import('@/views/property-marketing/media-account/account-dashboard/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
@ -84,7 +84,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
activeMenu: 'MediaAccountAccountDashboard',
|
||||
},
|
||||
component: () => import('@/views/property-marketing/media-account/account-detail'),
|
||||
component: () => import('@/views/property-marketing/media-account/account-detail/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -110,7 +110,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/put-account/account-manage'),
|
||||
component: () => import('@/views/property-marketing/put-account/account-manage/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'data',
|
||||
@ -121,7 +121,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/put-account/account-data'),
|
||||
component: () => import('@/views/property-marketing/put-account/account-data/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'account-dashboard',
|
||||
@ -132,7 +132,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/put-account/account-dashboard'),
|
||||
component: () => import('@/views/property-marketing/put-account/account-dashboard/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'investmentGuidelines',
|
||||
@ -143,7 +143,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines'),
|
||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
@ -155,7 +155,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
roles: ['*'],
|
||||
activeMenu: 'PutAccountInvestmentGuidelines',
|
||||
},
|
||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines/detail'),
|
||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines/detail.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -181,7 +181,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
// requireLogin: true,
|
||||
// roles: ['*'],
|
||||
// },
|
||||
// component: () => import('@/views/property-marketing/intelligent-solution/businessAnalysisReport'),
|
||||
// component: () => import('@/views/property-marketing/intelligent-solution/businessAnalysisReport.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: 'competitiveProductAnalysisReport',
|
||||
@ -192,7 +192,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
// requireLogin: true,
|
||||
// roles: ['*'],
|
||||
// },
|
||||
// component: () => import('@/views/property-marketing/intelligent-solution/competitiveProductAnalysisReport'),
|
||||
// component: () => import('@/views/property-marketing/intelligent-solution/competitiveProductAnalysisReport.vue'),
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
@ -218,7 +218,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
||||
requireLogin: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
component: () => import('@/views/property-marketing/project-manage/project-list'),
|
||||
component: () => import('@/views/property-marketing/project-manage/project-list/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
1
src/router/typeings.d.ts
vendored
@ -15,6 +15,7 @@ declare module 'vue-router' {
|
||||
ignoreCache?: boolean; // if set true, the page will not be cached
|
||||
hideSidebar?: boolean;
|
||||
isAgentRoute?:boolean;
|
||||
hideFooter?: boolean;
|
||||
requireLogin?: boolean; // 是否需要登陆才能访问
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,27 @@ export const MENU_LIST = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: MENU_GROUP_IDS.CREATIVE_GENERATION_WORKSHOP_ID,
|
||||
name: '创意生成工坊',
|
||||
permissionKey: '',
|
||||
requiresAuth: true,
|
||||
children: [
|
||||
{
|
||||
name: '内容稿件',
|
||||
routeName: 'ManuscriptList',
|
||||
includeRouteNames: [
|
||||
'ManuscriptList',
|
||||
'ManuscriptUpload',
|
||||
'ManuscriptEdit',
|
||||
'ManuscriptDetail',
|
||||
'ManuscriptCheckList',
|
||||
'ManuscriptCheckListDetail',
|
||||
'ManuscriptCheck',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: MENU_GROUP_IDS.PROPERTY_ID,
|
||||
name: '营销资产中台',
|
||||
@ -88,9 +109,7 @@ export const MENU_LIST = [
|
||||
{
|
||||
name: '项目管理',
|
||||
routeName: 'ProjectList',
|
||||
includeRouteNames: [
|
||||
'ProjectList',
|
||||
],
|
||||
includeRouteNames: ['ProjectList'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
|
||||
import router from '@/router';
|
||||
import { defineStore } from 'pinia';
|
||||
import { fetchProfileInfo } from '@/api/all/login';
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
import router from '@/router';
|
||||
import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage';
|
||||
|
||||
interface UserInfo {
|
||||
|
||||
@ -21,15 +21,17 @@
|
||||
|
||||
.arco-btn-primary {
|
||||
background-color: $color-primary !important;
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
color: #fff !important;
|
||||
&.arco-btn-disabled {
|
||||
color: #fff !important;
|
||||
border-color: transparent !important;
|
||||
background-color: $color-primary-3 !important;
|
||||
}
|
||||
&:not(.arco-btn-disabled) {
|
||||
&:hover {
|
||||
color: #fff !important;
|
||||
border-color: transparent !important;
|
||||
background-color: $color-primary-5 !important;
|
||||
}
|
||||
}
|
||||
@ -134,14 +136,16 @@
|
||||
|
||||
.arco-btn-text {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
color: $color-primary !important;
|
||||
&.arco-btn-disabled {
|
||||
color: $color-primary-2 !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
&:not(.arco-btn-disabled) {
|
||||
&:hover {
|
||||
color: $color-primary-5 !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
.arco-form {
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px !important;
|
||||
margin-bottom: 0;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
.arco-form-item-label-col {
|
||||
padding-right: 12px !important;
|
||||
.arco-form-item-label {
|
||||
|
||||
@ -15,4 +15,8 @@
|
||||
&.arco-input-disabled {
|
||||
background-color: var(--BG-200, #F2F3F5) !important;
|
||||
}
|
||||
.arco-input-prefix {
|
||||
padding-right: 0 !important;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
}
|
||||
.arco-table-container {
|
||||
border: none !important;
|
||||
height: 100%;
|
||||
.arco-table-element {
|
||||
thead {
|
||||
.arco-table-tr {
|
||||
|
||||
@ -23,3 +23,23 @@ p {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
height: 10px;
|
||||
background-color: #C9CDD4;
|
||||
border-radius: 99px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
|
||||
$navbar-height: 72px; // 头部高度
|
||||
$sidebar-width: 220px; // 侧边栏菜单宽度
|
||||
|
||||
// 汉字字体
|
||||
$font-family-regular: 'PingFangSC-Regular', 'Microsoft Yahei', Arial, sans-serif;
|
||||
$font-family-medium: 'PingFangSC-Medium', 'Microsoft Yahei', Arial, sans-serif;
|
||||
|
||||
@ -74,3 +74,27 @@
|
||||
.arco-link {
|
||||
--color-primary-6: var(--arco-primary-6) !important;
|
||||
}
|
||||
|
||||
.common-filter-wrap {
|
||||
padding: 24px 24px 8px;
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.filter-row-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 24px 16px 0;
|
||||
.label {
|
||||
margin-right: 8px;
|
||||
color: #211f24;
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,235 @@ export function genRandomId() {
|
||||
return `id_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 修改函数以同时获取视频时长和首帧
|
||||
export function getVideoInfo(file: File): Promise<{ duration: number; firstFrame: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.crossOrigin = 'anonymous'; // 避免跨域问题
|
||||
video.muted = true; // 静音,避免意外播放声音
|
||||
video.style.position = 'fixed'; // 确保视频元素在DOM中
|
||||
video.style.top = '-1000px'; // 但不可见
|
||||
document.body.appendChild(video); // 添加到DOM
|
||||
|
||||
let hasResolved = false;
|
||||
|
||||
// 先获取元数据(时长)
|
||||
video.onloadedmetadata = function () {
|
||||
// 视频时长
|
||||
const duration = video.duration;
|
||||
|
||||
// 尝试将视频定位到非常小的时间点,确保有帧可捕获
|
||||
if (duration > 0) {
|
||||
video.currentTime = Math.min(0.1, duration / 2);
|
||||
}
|
||||
};
|
||||
|
||||
// 当视频定位完成后尝试捕获首帧
|
||||
video.onseeked = function () {
|
||||
if (hasResolved) return;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// 清理
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
document.body.removeChild(video);
|
||||
|
||||
// 返回结果
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
duration: video.duration,
|
||||
firstFrame: canvas.toDataURL('image/jpeg', 0.9), // 提高质量
|
||||
});
|
||||
};
|
||||
|
||||
// 作为备选方案,监听loadeddata事件
|
||||
video.onloadeddata = function () {
|
||||
if (hasResolved) return;
|
||||
|
||||
// 尝试捕获帧
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// 检查是否捕获到有效帧(非全黑)
|
||||
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height);
|
||||
if (imageData) {
|
||||
let isAllBlack = true;
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
if (imageData.data[i] > 10 || imageData.data[i + 1] > 10 || imageData.data[i + 2] > 10) {
|
||||
isAllBlack = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllBlack) {
|
||||
// 清理
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
document.body.removeChild(video);
|
||||
|
||||
// 返回结果
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
duration: video.duration,
|
||||
firstFrame: canvas.toDataURL('image/jpeg', 0.9),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是全黑帧,尝试定位到0.1秒
|
||||
if (video.duration > 0) {
|
||||
video.currentTime = 0.1;
|
||||
}
|
||||
};
|
||||
|
||||
// 设置视频源以触发加载
|
||||
video.src = URL.createObjectURL(file);
|
||||
|
||||
// 设置超时,防止长时间无响应
|
||||
setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
document.body.removeChild(video);
|
||||
resolve({
|
||||
duration: 0,
|
||||
firstFrame: '',
|
||||
});
|
||||
}
|
||||
}, 5000); // 5秒超时
|
||||
});
|
||||
}
|
||||
|
||||
export const formatDuration = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const remainingSecondsAfterHours = seconds % 3600;
|
||||
const minutes = Math.floor(remainingSecondsAfterHours / 60);
|
||||
const remainingSeconds = Math.floor(remainingSecondsAfterHours % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
if (remainingSecondsAfterHours === 0) {
|
||||
return `${hours}小时`;
|
||||
}
|
||||
if (remainingSeconds === 0) {
|
||||
return `${hours}小时${minutes}分`;
|
||||
}
|
||||
return `${hours}小时${minutes}分${remainingSeconds}秒`;
|
||||
} else if (minutes > 0) {
|
||||
if (remainingSeconds === 0) {
|
||||
return `${minutes}分`;
|
||||
}
|
||||
return `${minutes}分${remainingSeconds}秒`;
|
||||
} else {
|
||||
return `${remainingSeconds}秒`;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatUploadSpeed = (bytesPerSecond: number): string => {
|
||||
if (bytesPerSecond < 1024) {
|
||||
return `${bytesPerSecond.toFixed(2)} B/s`;
|
||||
} else if (bytesPerSecond < 1024 * 1024) {
|
||||
return `${(bytesPerSecond / 1024).toFixed(2)} KB/s`;
|
||||
} else {
|
||||
return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} MB/s`;
|
||||
}
|
||||
};
|
||||
|
||||
export function convertVideoUrlToCoverUrl(videoUrl: string): string {
|
||||
if (!videoUrl || typeof videoUrl !== 'string') {
|
||||
console.error('Invalid video URL');
|
||||
return '';
|
||||
}
|
||||
|
||||
const urlWithCovers = videoUrl.replace('/videos/', '/covers/');
|
||||
|
||||
const lastDotIndex = urlWithCovers.lastIndexOf('.');
|
||||
if (lastDotIndex !== -1) {
|
||||
return urlWithCovers.substring(0, lastDotIndex) + '.jpg';
|
||||
}
|
||||
|
||||
return urlWithCovers + '.jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成包含协议、域名和参数的完整URL
|
||||
*/
|
||||
export const generateFullUrl = (pathTemplate: string, params: Record<string, string | number> = {}): string => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port ? `:${window.location.port}` : '';
|
||||
const baseUrl = `${protocol}//${hostname}${port}`;
|
||||
|
||||
let path = pathTemplate;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
path = path.replace(`:${key}`, String(value));
|
||||
});
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
};
|
||||
|
||||
/** 图片朝向类型 */
|
||||
export type ImageOrientation = 'landscape' | 'portrait' | 'square';
|
||||
|
||||
/**
|
||||
* 根据宽高判断图片是横图/竖图/正方形
|
||||
*/
|
||||
export const getImageOrientationBySize = (width: number, height: number): ImageOrientation => {
|
||||
if (!width || !height) return 'square';
|
||||
if (width > height) return 'landscape';
|
||||
if (height > width) return 'portrait';
|
||||
return 'square';
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载图片自然尺寸(跨域下需服务端允许匿名访问)
|
||||
*/
|
||||
export const getImageNaturalSize = (url: string): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
};
|
||||
// 若是跨域资源并允许匿名访问,可尝试设置
|
||||
try {
|
||||
img.crossOrigin = 'anonymous';
|
||||
} catch (_) {}
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 基于图片 URL 判断图片横竖
|
||||
*/
|
||||
export const getImageOrientationByUrl = async (url: string): Promise<ImageOrientation> => {
|
||||
const { width, height } = await getImageNaturalSize(url);
|
||||
return getImageOrientationBySize(width, height);
|
||||
};
|
||||
|
||||
export function getImageMainColor(imageUrl: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@ -168,7 +397,7 @@ export function getImageMainColor(imageUrl: string): Promise<string> {
|
||||
const avgColor = {
|
||||
r: Math.round(maxGroup.sumR / maxGroup.count),
|
||||
g: Math.round(maxGroup.sumG / maxGroup.count),
|
||||
b: Math.round(maxGroup.sumB / maxGroup.count)
|
||||
b: Math.round(maxGroup.sumB / maxGroup.count),
|
||||
};
|
||||
|
||||
resolve(`rgb(${avgColor.r},${avgColor.g},${avgColor.b})`);
|
||||
@ -198,28 +427,38 @@ function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
if (a < 128) continue;
|
||||
|
||||
colors.push({
|
||||
r, g, b,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
count: 1,
|
||||
sumR: r, sumG: g, sumB: b
|
||||
sumR: r,
|
||||
sumG: g,
|
||||
sumB: b,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有颜色数据,返回默认白色
|
||||
if (colors.length === 0) {
|
||||
return [{
|
||||
count: 1,
|
||||
sumR: 255, sumG: 255, sumB: 255
|
||||
}];
|
||||
return [
|
||||
{
|
||||
count: 1,
|
||||
sumR: 255,
|
||||
sumG: 255,
|
||||
sumB: 255,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 开始中位数切分
|
||||
let colorGroups = [{
|
||||
colors,
|
||||
count: colors.length,
|
||||
sumR: colors.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: colors.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: colors.reduce((sum, c) => sum + c.b, 0)
|
||||
}];
|
||||
let colorGroups = [
|
||||
{
|
||||
colors,
|
||||
count: colors.length,
|
||||
sumR: colors.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: colors.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: colors.reduce((sum, c) => sum + c.b, 0),
|
||||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < levels; i++) {
|
||||
const newGroups = [];
|
||||
@ -231,12 +470,12 @@ function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
}
|
||||
|
||||
// 找出颜色范围最大的通道
|
||||
const rMin = Math.min(...group.colors.map(c => c.r));
|
||||
const rMax = Math.max(...group.colors.map(c => c.r));
|
||||
const gMin = Math.min(...group.colors.map(c => c.g));
|
||||
const gMax = Math.max(...group.colors.map(c => c.g));
|
||||
const bMin = Math.min(...group.colors.map(c => c.b));
|
||||
const bMax = Math.max(...group.colors.map(c => c.b));
|
||||
const rMin = Math.min(...group.colors.map((c) => c.r));
|
||||
const rMax = Math.max(...group.colors.map((c) => c.r));
|
||||
const gMin = Math.min(...group.colors.map((c) => c.g));
|
||||
const gMax = Math.max(...group.colors.map((c) => c.g));
|
||||
const bMin = Math.min(...group.colors.map((c) => c.b));
|
||||
const bMax = Math.max(...group.colors.map((c) => c.b));
|
||||
|
||||
const rRange = rMax - rMin;
|
||||
const gRange = gMax - gMin;
|
||||
@ -250,7 +489,9 @@ function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
}
|
||||
|
||||
// 按最大范围通道排序
|
||||
group.colors.sort((a, b) => a[sortChannel] - b[sortChannel]);
|
||||
type ColorChannel = 'r' | 'g' | 'b';
|
||||
const safeSortChannel = sortChannel as ColorChannel;
|
||||
group.colors.sort((a, b) => a[safeSortChannel] - b[safeSortChannel]);
|
||||
|
||||
// 切分中位数
|
||||
const mid = Math.floor(group.colors.length / 2);
|
||||
@ -263,7 +504,7 @@ function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
count: group1.length,
|
||||
sumR: group1.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: group1.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: group1.reduce((sum, c) => sum + c.b, 0)
|
||||
sumB: group1.reduce((sum, c) => sum + c.b, 0),
|
||||
});
|
||||
|
||||
newGroups.push({
|
||||
@ -271,7 +512,7 @@ function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
count: group2.length,
|
||||
sumR: group2.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: group2.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: group2.reduce((sum, c) => sum + c.b, 0)
|
||||
sumB: group2.reduce((sum, c) => sum + c.b, 0),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
import type { Form } from '@/components/wyg-form/interface';
|
||||
export const options = [
|
||||
{
|
||||
value: 'beijing',
|
||||
label: 'Beijing',
|
||||
children: [
|
||||
{
|
||||
value: 'chaoyang',
|
||||
label: 'ChaoYang',
|
||||
children: [
|
||||
{
|
||||
value: 'datunli',
|
||||
label: 'Datunli',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'haidian',
|
||||
label: 'Haidian',
|
||||
},
|
||||
{
|
||||
value: 'dongcheng',
|
||||
label: 'Dongcheng',
|
||||
},
|
||||
{
|
||||
value: 'xicheng',
|
||||
label: 'Xicheng',
|
||||
children: [
|
||||
{
|
||||
value: 'jinrongjie',
|
||||
label: 'Jinrongjie',
|
||||
},
|
||||
{
|
||||
value: 'tianqiao',
|
||||
label: 'Tianqiao',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'shanghai',
|
||||
label: 'Shanghai',
|
||||
children: [
|
||||
{
|
||||
value: 'huangpu',
|
||||
label: 'Huangpu',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const formConfig: Form.FieldItem[] = [
|
||||
{
|
||||
field: 'basic',
|
||||
label: '基本信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
required: true,
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
colProps: {
|
||||
span: 20,
|
||||
offset: 100,
|
||||
},
|
||||
rules: [{ required: true, message: 'name is required' }],
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
label: '年龄',
|
||||
required: true,
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
},
|
||||
{
|
||||
field: 'hobby',
|
||||
label: '爱好',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'school',
|
||||
label: '学校',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
required: true,
|
||||
component: 'select',
|
||||
lists: [
|
||||
{
|
||||
label: '男',
|
||||
value: 'M',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 'F',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'birthTime',
|
||||
label: '时间',
|
||||
component: 'time',
|
||||
},
|
||||
{
|
||||
field: 'birthDate',
|
||||
label: '出生日期',
|
||||
component: 'date',
|
||||
},
|
||||
{
|
||||
field: 'rangeDate',
|
||||
label: '日期范围',
|
||||
component: 'rangeDate',
|
||||
},
|
||||
{
|
||||
field: 'address',
|
||||
label: '地址',
|
||||
component: 'cascader',
|
||||
options,
|
||||
},
|
||||
{
|
||||
field: 'desc',
|
||||
label: '自我介绍',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'contact',
|
||||
label: '联系信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'tel',
|
||||
label: '联系电话',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'relation',
|
||||
label: '关系信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'relationTable',
|
||||
label: '关系信息',
|
||||
component: 'slot',
|
||||
},
|
||||
];
|
||||
@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<a-card class="mb-5">
|
||||
<eo-form ref="formRef" :fieldList="fieldList" :model="model" @submit="handleSubmit" @change="handleChange">
|
||||
<!-- <template #header> 基本表单header </template> -->
|
||||
<!-- <template #footer>基本表单footer</template> -->
|
||||
|
||||
<!-- <template #buttons="{ model, formRef }">
|
||||
<a-button @click="handleSubmit(model, formRef)">提交</a-button>
|
||||
</template> -->
|
||||
|
||||
<!-- <template #relationTable>
|
||||
<div>关系人slot</div>
|
||||
</template> -->
|
||||
</eo-form>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onSubmit">父组件提交</a-button>
|
||||
<a-button @click="onSwith">启用/禁用</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import EoForm from '@/components/wyg-form/index.vue';
|
||||
import { formConfig } from './config';
|
||||
import type { Form } from '@/components/wyg-form/interface';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
let fieldList: Form.FieldItem[] = formConfig;
|
||||
const model = ref<Record<string, any>>({
|
||||
name: '张三',
|
||||
age: 18,
|
||||
});
|
||||
// 提交按钮在子组件
|
||||
const handleSubmit = (model: Record<string, any>) => {
|
||||
console.log(model, 'model====');
|
||||
};
|
||||
// 提交按钮在父组件
|
||||
const onSubmit = async () => {
|
||||
const res = await formRef?.value?.submit();
|
||||
console.log(formRef, 'formRef====');
|
||||
console.log(res, 'res====');
|
||||
};
|
||||
// 修改Form组件相关属性
|
||||
const status = ref(false);
|
||||
const onSwith = () => {
|
||||
status.value = !status.value;
|
||||
formRef?.value?.setForm({ disabled: !status.value });
|
||||
};
|
||||
|
||||
const handleChange = (params: any) => {
|
||||
const { key, val } = params;
|
||||
if (key === 'sex') {
|
||||
const required = val === 'F';
|
||||
// 更新form配置
|
||||
const updateList: any = [
|
||||
{
|
||||
field: 'school',
|
||||
required: required,
|
||||
disabled: !required,
|
||||
},
|
||||
];
|
||||
formRef?.value?.updateFieldsList(updateList);
|
||||
formRef?.value?.setModel({ age: 26 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-20 14:00:07
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-20 14:00:08
|
||||
* @Description:
|
||||
*/
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
|
||||
export const columnsData: TableColumnData[] = [
|
||||
{
|
||||
title: '测试表头1',
|
||||
dataIndex: 'column1',
|
||||
},
|
||||
{
|
||||
title: '测试表头2',
|
||||
dataIndex: 'column2',
|
||||
},
|
||||
{
|
||||
title: '测试表头3',
|
||||
dataIndex: 'column3',
|
||||
},
|
||||
{
|
||||
title: '测试表头4',
|
||||
dataIndex: 'column4',
|
||||
},
|
||||
{
|
||||
title: '测试表头5',
|
||||
dataIndex: 'column5',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: '',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
slotName: 'opeartion',
|
||||
},
|
||||
];
|
||||
@ -1,132 +0,0 @@
|
||||
import type { Form } from '../wyg-form/interface';
|
||||
export const options = [
|
||||
{
|
||||
value: 'beijing',
|
||||
label: 'Beijing',
|
||||
children: [
|
||||
{
|
||||
value: 'chaoyang',
|
||||
label: 'ChaoYang',
|
||||
children: [
|
||||
{
|
||||
value: 'datunli',
|
||||
label: 'Datunli',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'haidian',
|
||||
label: 'Haidian',
|
||||
},
|
||||
{
|
||||
value: 'dongcheng',
|
||||
label: 'Dongcheng',
|
||||
},
|
||||
{
|
||||
value: 'xicheng',
|
||||
label: 'Xicheng',
|
||||
children: [
|
||||
{
|
||||
value: 'jinrongjie',
|
||||
label: 'Jinrongjie',
|
||||
},
|
||||
{
|
||||
value: 'tianqiao',
|
||||
label: 'Tianqiao',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'shanghai',
|
||||
label: 'Shanghai',
|
||||
children: [
|
||||
{
|
||||
value: 'huangpu',
|
||||
label: 'Huangpu',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const formConfig: Form.FieldItem[] = [
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
required: true,
|
||||
value: '波波',
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
colProps: {
|
||||
span: 20,
|
||||
offset: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
label: '年龄',
|
||||
required: true,
|
||||
value: '18',
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
},
|
||||
{
|
||||
field: 'hobby',
|
||||
label: '爱好',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'school',
|
||||
label: '学校',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
required: true,
|
||||
component: 'select',
|
||||
lists: [
|
||||
{
|
||||
label: '男',
|
||||
value: 'M',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 'F',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'birthTime',
|
||||
label: '时间',
|
||||
component: 'time',
|
||||
},
|
||||
{
|
||||
field: 'birthDate',
|
||||
label: '出生日期',
|
||||
component: 'date',
|
||||
},
|
||||
{
|
||||
field: 'rangeDate',
|
||||
label: '日期范围',
|
||||
component: 'rangeDate',
|
||||
},
|
||||
{
|
||||
field: 'address',
|
||||
label: '地址',
|
||||
component: 'cascader',
|
||||
options,
|
||||
},
|
||||
{
|
||||
field: 'desc',
|
||||
label: '自我介绍',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
];
|
||||
@ -1,27 +0,0 @@
|
||||
# Table 说明
|
||||
|
||||
## 说明
|
||||
|
||||
`<a-table v-bind="propsRes" v-on="propsEvent" ></a-table>`
|
||||
|
||||
`复写a-table中原始的v-bind及v-on属性,propRes负责处理属性,propsEvent负责处理a-table的事件`
|
||||
|
||||
`setProps:设置a-table的属性,类型为:IDefaultProps`
|
||||
|
||||
`setColumns:设置table中columns的属性,类型为:TableColumnData[]`
|
||||
|
||||
`setLoadListParams: 设置列表数据接口的请求参数,类型为自定义`
|
||||
|
||||
`loadTableData:获取列表数据,返回列表参数`
|
||||
|
||||
## 使用方式
|
||||
|
||||
`<a-table v-bind="propsRes" v-on="propsEvent">`
|
||||
<br>
|
||||
`<template #xx></template> // 自定义插槽名称,在setColumns的传入值中设置`
|
||||
<br>
|
||||
`</a-table>`
|
||||
|
||||
## 示例
|
||||
|
||||
`具体参照table文件夹的示例`
|
||||
@ -1,52 +0,0 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-20 13:58:48
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-28 12:05:03
|
||||
* @Description: table示例
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-table v-bind="propsRes" v-on="propsEvent">
|
||||
<template #opeartion="{ record }">
|
||||
<div flex>
|
||||
<a-link>编辑</a-link>
|
||||
<ConfirmButton content="确定要删除该条数据吗?" popupType="error" @confirm-emit="deleteItem(record)">
|
||||
<a-link>删除</a-link>
|
||||
</ConfirmButton>
|
||||
<a-link>下载</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchTableData, type IExample } from '@/api/example';
|
||||
import useTableProps from '@/hooks/table-hooks';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { columnsData } from './columns';
|
||||
|
||||
const { propsRes, propsEvent, setProps, setColumns, setLoadListParams, loadTableData } =
|
||||
useTableProps<IExample.ITableResponse>(fetchTableData);
|
||||
setProps({
|
||||
'row-key': 'id',
|
||||
'row-selection': {
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true,
|
||||
},
|
||||
'selected-keys': [1, 2, 3],
|
||||
});
|
||||
setColumns(columnsData);
|
||||
onMounted(async () => {
|
||||
await loadTableData();
|
||||
});
|
||||
|
||||
function add() {
|
||||
Message.info('=sa');
|
||||
}
|
||||
|
||||
function deleteItem(record) {}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@ -24,23 +24,26 @@
|
||||
<div class="footer flex arco-row-justify-start">
|
||||
<a-button
|
||||
v-if="props.product.status === Status.Enable || props.product.status === Status.ON_TRIAL"
|
||||
class="primary-button"
|
||||
type="primary"
|
||||
size="mini"
|
||||
class="mr-8px"
|
||||
@click="gotoModule(props.product.id)"
|
||||
>
|
||||
进入模块
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="props.product.status === Status.TRIAL_ENDS || props.product.status === Status.EXPIRED"
|
||||
class="primary-button"
|
||||
size="mini"
|
||||
type="primary"
|
||||
class="mr-8px"
|
||||
@click="visible = true"
|
||||
>
|
||||
立即购买
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="props.product.status === Status.ON_TRIAL"
|
||||
class="outline-button"
|
||||
class="mr-8px"
|
||||
size="mini"
|
||||
type="outline"
|
||||
@click="visible = true"
|
||||
>
|
||||
@ -48,16 +51,15 @@
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="props.product.status === Status.TRIAL_ENDS || props.product.status === Status.EXPIRED"
|
||||
class="outline-button"
|
||||
class="mr-8px"
|
||||
size="mini"
|
||||
type="outline"
|
||||
@click="visible = true"
|
||||
>
|
||||
联系客服
|
||||
</a-button>
|
||||
<a-popconfirm focusLock title="试用产品" content="确定试用该产品吗?" @ok="handleTrial(props.product.id)">
|
||||
<a-button v-if="props.product.status === Status.Disable" class="outline-button" type="outline">
|
||||
免费试用7天
|
||||
</a-button>
|
||||
<a-button v-if="props.product.status === Status.Disable" size="mini" type="outline"> 免费试用7天 </a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<CustomerServiceModal v-model:visible="visible" />
|
||||
@ -69,7 +71,6 @@ import { now } from '@vueuse/core';
|
||||
import { trialProduct } from '@/api/all';
|
||||
import { useRouter } from 'vue-router';
|
||||
import CustomerServiceModal from '@/components/customer-service-modal.vue';
|
||||
import { appRoutes } from '@/router/routes';
|
||||
|
||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||
@ -119,7 +120,6 @@ const gotoModule = (menuId: number) => {
|
||||
'1': 'DataEngineHotTranslation',
|
||||
'2': 'RepositoryBrandMaterials',
|
||||
};
|
||||
console.log(routeMap[menuId]);
|
||||
router.push({ name: routeMap[menuId] });
|
||||
};
|
||||
</script>
|
||||
@ -221,38 +221,6 @@ const gotoModule = (menuId: number) => {
|
||||
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
|
||||
.primary-button {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
gap: 8px;
|
||||
padding: 2px 12px;
|
||||
background-color: rgba(109, 76, 254, 1) !important;
|
||||
font-family: $font-family-medium;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.outline-button {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
gap: 8px;
|
||||
padding: 2px 12px;
|
||||
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
|
||||
font-family: $font-family-medium;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
color: rgba(109, 76, 254, 1);
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" title="确定删除评论?" width="400px" @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="large" @click="onClose">取消</a-button>
|
||||
<a-button type="primary" class="ml-16px !border-none" size="large" @click="onDelete">删除</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
||||
|
||||
const emits = defineEmits(['delete', 'close']);
|
||||
|
||||
const visible = ref(false);
|
||||
const commentId = ref('');
|
||||
|
||||
const onClose = () => {
|
||||
commentId.value = ''
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const open = (id) => {
|
||||
commentId.value = id;
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
emits('delete', commentId.value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@ -0,0 +1,313 @@
|
||||
<script lang="jsx">
|
||||
import { Image, Spin, Button, Input, Textarea } from '@arco-design/web-vue';
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import SvgIcon from '@/components/svg-icon/index.vue';
|
||||
import DeleteCommentModal from './delete-comment-modal.vue';
|
||||
|
||||
import { RESULT_LIST } from '@/views/creative-generation-workshop/manuscript/check/components/content-card/constants.ts';
|
||||
import { ENUM_OPINION, formatRelativeTime } from '../../constants';
|
||||
import { postShareWorksComments, deleteShareWorksComments } from '@/api/all/generationWorkshop.ts';
|
||||
import { exactFormatTime } from '@/utils/tools.ts';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
import icon1 from '@/assets/img/creative-generation-workshop/icon-line.png';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-avatar-default.png';
|
||||
import icon3 from '@/assets/img/error-img.png';
|
||||
|
||||
const _iconMap = new Map([
|
||||
// [3, { icon: <icon-check-circle-fill size={16} class="color-#25C883 flex-shrink-0" /> }],
|
||||
[2, { icon: <icon-exclamation-circle-fill size={16} class="color-#F64B31 flex-shrink-0" /> }],
|
||||
[1, { icon: <icon-exclamation-circle-fill size={16} class="color-#FFAE00 flex-shrink-0" /> }],
|
||||
[0, { icon: <icon-check-circle-fill size={16} class="color-#25C883 flex-shrink-0" /> }],
|
||||
]);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isExpand: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
dataSource: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['toggle', 'updateComment', 'deleteComment'],
|
||||
setup(props, { emit, expose }) {
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isCollapse = ref(false);
|
||||
const comment = ref('');
|
||||
const isReplay = ref(false);
|
||||
const replayTarget = ref({});
|
||||
const deleteCommentModalRef = ref(null);
|
||||
const textAreaRef = ref(null);
|
||||
|
||||
const aiReview = computed(() => props.dataSource.ai_review);
|
||||
const violationItems = computed(() => props.dataSource?.ai_review?.violation_items ?? []);
|
||||
|
||||
const closeReplay = () => {
|
||||
isReplay.value = false;
|
||||
replayTarget.value = {};
|
||||
};
|
||||
|
||||
const onReplay = (item) => {
|
||||
isReplay.value = true;
|
||||
replayTarget.value = item;
|
||||
};
|
||||
|
||||
const onComment = async () => {
|
||||
console.log(textAreaRef.value.focus());
|
||||
const { code, data } = await postShareWorksComments(props.dataSource.id, route.params.shareCode, {
|
||||
content: comment.value,
|
||||
comment_id: replayTarget.value.id,
|
||||
});
|
||||
if (code === 200) {
|
||||
emit('updateComment');
|
||||
comment.value = '';
|
||||
textAreaRef.value.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onClearComment = () => {
|
||||
isReplay.value = false;
|
||||
replayTarget.value = {};
|
||||
comment.value = '';
|
||||
};
|
||||
|
||||
const renderDeleteBtn = (item) => {
|
||||
const { userInfo, isLogin } = userStore;
|
||||
const { commenter_id, id, commenter } = item;
|
||||
|
||||
let showBtn = true;
|
||||
if (isLogin) {
|
||||
showBtn = commenter?.id === userInfo.id;
|
||||
} else {
|
||||
showBtn = commenter_id === 1 ? false : !commenter;
|
||||
}
|
||||
|
||||
if (!showBtn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<icon-delete
|
||||
class="ml-12px cursor-pointer color-#55585F hover:color-#6D4CFE"
|
||||
size={16}
|
||||
onClick={() => deleteCommentModalRef.value?.open(id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const deleteComment = async (comment_id) => {
|
||||
emit('deleteComment', comment_id);
|
||||
|
||||
deleteShareWorksComments(props.dataSource.id, comment_id, route.params.shareCode);
|
||||
};
|
||||
|
||||
const renderTextareaBox = () => {
|
||||
return (
|
||||
<div class="sticky bottom-0 left-0 w-full z-22 px-24px">
|
||||
<div class="relative">
|
||||
{isReplay.value && (
|
||||
<div class="px-12px pt-8px absolute top-0 left-0 z-2 mb-8px w-full">
|
||||
<div class="rounded-4px bg-#F2F3F5 h-30px px-8px flex justify-between items-center ">
|
||||
<div class="flex items-center mr-12px flex-1 overflow-hidden">
|
||||
<span class="mr-4px cts !color-#737478 flex-shrink-0">回复</span>
|
||||
<TextOverTips
|
||||
context={`${getCommentName(replayTarget.value)}:${replayTarget.value.content}`}
|
||||
class="cts !color-#737478"
|
||||
/>
|
||||
</div>
|
||||
<icon-close size={16} class="color-#737478 cursor-pointer flex-shrink-0" onClick={closeReplay} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
ref={textAreaRef}
|
||||
auto-size
|
||||
class={`max-h-220px overflow-y-auto ${isReplay.value ? 'pt-38px' : ''}`}
|
||||
size="large"
|
||||
placeholder="输入评论"
|
||||
v-model={comment.value}
|
||||
onPressEnter={onComment}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{comment.value && (
|
||||
<div class="flex justify-end mt-12px">
|
||||
<Button type="outline" class="mr-12px rounded-8px" size="medium" onClick={onClearComment}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" class="rounded-8px" size="medium" onClick={onComment}>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCommentBox = () => {
|
||||
return (
|
||||
<div class="comment-box">
|
||||
<p class="mb-16px">
|
||||
<span class="cts bold cm !text-16px !lh-24px mr-8px">评论</span>
|
||||
{props.dataSource.comments?.length > 0 && (
|
||||
<span class="cts !text-16px !lh-24px bold">{props.dataSource.comments?.length}</span>
|
||||
)}
|
||||
</p>
|
||||
{props.dataSource.comments?.length > 0 && (
|
||||
<div class="comment-list flex flex-col my-16px rounded-8px">
|
||||
{props.dataSource.comments?.map((item) => (
|
||||
<div class="comment-item flex px-12px py-8px group" key={item.id}>
|
||||
<Image
|
||||
src={item.commenter_id === 0 ? icon2 : item.commenter?.head_image}
|
||||
width={40}
|
||||
height={40}
|
||||
preview={false}
|
||||
fit="cover"
|
||||
class="rounded-50% mr-13px"
|
||||
v-slots={{
|
||||
error: () => <img src={icon3} class="w-40 h-40 rounded-50%" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col flex-1 overflow-hidden">
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4px">
|
||||
<span class="cts !color-#211F24 mr-8px">{getCommentName(item)}</span>
|
||||
<span class="cts !color-#939499">{formatRelativeTime(item.created_at)}</span>
|
||||
</p>
|
||||
<div class="items-center hidden group-hover:flex">
|
||||
<SvgIcon
|
||||
onClick={() => onReplay(item)}
|
||||
name="svg-comment"
|
||||
size={16}
|
||||
class="color-#55585F cursor-pointer hover:color-#6D4CFE"
|
||||
/>
|
||||
{renderDeleteBtn(item)}
|
||||
</div>
|
||||
</div>
|
||||
{item.reply_comment && (
|
||||
<div class="flex items-center">
|
||||
<div class="w-2px h-12px bg-#B1B2B5"></div>
|
||||
<span class="mx-4px cts !color-#939499 flex-shrink-0">回复</span>
|
||||
<TextOverTips
|
||||
context={`${getCommentName(item.reply_comment)}:${item.reply_comment.content}`}
|
||||
class="cts !color-#939499"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p class="cts !color-#211F24">{item.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAiSuggest = () => {
|
||||
if (isEmpty(aiReview.value)) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="result-box p-16px rounded-8px">
|
||||
<div class="flex items-center justify-between mb-16px">
|
||||
<p class="cts bold !color-#000 !text-16px">审核结果</p>
|
||||
<Button
|
||||
type="text"
|
||||
class="!color-#6D4CFE hover:!color-#8A70FE"
|
||||
onClick={() => (isCollapse.value = !isCollapse.value)}
|
||||
>
|
||||
{isCollapse.value ? '展开详情' : '收起详情'}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
{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>
|
||||
<div class={`collapse-box mb-16px overflow-hidden ${isCollapse.value ? 'h-0 ' : 'h-auto'}`}>
|
||||
{violationItems.value.length > 0 && (
|
||||
<div class="result-box p-16px rounded-8px mt-16px">
|
||||
<p class="cts bold !color-#000 !text-16px mb-16px">敏感词检测</p>
|
||||
<div class="grid grid-cols-3 gap-x-24px gap-y-8px">
|
||||
{violationItems.value.map((item, index) => (
|
||||
<div class="audit-item" key={index}>
|
||||
<div class="flex items-center h-20px">
|
||||
{_iconMap.get(item.risk_level)?.icon}
|
||||
<TextOverTips context={item.word} class="cts ml-4px !color-#000" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getCommentName = (item) => {
|
||||
if (item.commenter_id === 0) {
|
||||
return '佚名';
|
||||
}
|
||||
// 姓名脱敏:保留首尾字符,中间拼接6个****
|
||||
const maskName = (name) => {
|
||||
if (!name || name.length <= 1) return name; // 单字符不脱敏
|
||||
return name[0] + '****' + name[name.length - 1];
|
||||
};
|
||||
// 手机号脱敏:保留前3位和后4位,中间4位替换为****
|
||||
const maskMobile = (mobile) => mobile?.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
|
||||
|
||||
const maskedName = maskName(item.commenter?.name);
|
||||
const maskedMobile = maskMobile(item.commenter?.mobile);
|
||||
return maskedName || maskedMobile;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<section class="py-16px absolute right-16px w-440px h-full overflow-hidden">
|
||||
<div class="ai-suggest-box relative py-24px flex flex-col">
|
||||
{!isEmpty(aiReview.value) && (
|
||||
<div class="relative w-fit ml-24px mb-16px">
|
||||
<span class="ai-text">AI 智能审核</span>
|
||||
<img src={icon1} class="w-80px h-10.8px absolute bottom-1px left--9px" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<icon-menu-unfold
|
||||
size={20}
|
||||
class="color-#55585F cursor-pointer hover:color-#6D4CFE absolute top-24px right-24px"
|
||||
onClick={() => emit('toggle', false)}
|
||||
/>
|
||||
|
||||
{/**主体 */}
|
||||
<div class="flex-1 overflow-y-auto px-24px">
|
||||
{/* AI审核结果 */}
|
||||
{renderAiSuggest()}
|
||||
{/* 评论与回复 */}
|
||||
{renderCommentBox()}
|
||||
</div>
|
||||
|
||||
{renderTextareaBox()}
|
||||
</div>
|
||||
<DeleteCommentModal ref={deleteCommentModalRef} onDelete={deleteComment} />
|
||||
</section>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,93 @@
|
||||
.ai-suggest-box {
|
||||
width: 440px;
|
||||
height: fit-content;
|
||||
max-height: 100%;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(126deg, #eef2fd 8.36%, #f5ebfe 49.44%, #fdebf3 90.52%);
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.ai-text {
|
||||
font-family: $font-family-medium;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
border-color: transparent !important;
|
||||
align-items: center;
|
||||
border-radius: 8px !important;
|
||||
background-color: #fff;
|
||||
color: #211f24 !important;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
&.arco-textarea-focus {
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
}
|
||||
.result-box {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
.result-item {
|
||||
.s1 {
|
||||
color: var(--Brand-6, #6d4cfe);
|
||||
font-family: $font-family-manrope-regular;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
}
|
||||
&:first-child {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: var(--Border-1, #d7d7d9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.collapse-box {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.comment-box {
|
||||
.cm {
|
||||
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
backdrop-filter: blur(4px);
|
||||
.comment-item {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
export const ENUM_OPINION = {
|
||||
wait: 0, // 待确认
|
||||
confirm: 1, // 已确认
|
||||
};
|
||||
|
||||
export const formatRelativeTime = (date: number): string => {
|
||||
const target = dayjs(date * 1000);
|
||||
|
||||
if (!target.isValid()) return '';
|
||||
|
||||
const now = dayjs();
|
||||
// 处理未来时间
|
||||
if (target.isAfter(now)) return '刚刚';
|
||||
|
||||
const diffInMinutes = now.diff(target, 'minute');
|
||||
const diffInHours = now.diff(target, 'hour');
|
||||
const diffInDays = now.diff(target, 'day');
|
||||
const diffInYears = now.diff(target, 'year');
|
||||
|
||||
// 1分钟内
|
||||
if (diffInMinutes < 1) {
|
||||
return '刚刚';
|
||||
}
|
||||
// 1分钟 ~ 1小时
|
||||
else if (diffInMinutes < 60) {
|
||||
return `${diffInMinutes}分钟前`;
|
||||
}
|
||||
// 1小时 ~ 24小时
|
||||
else if (diffInHours < 24) {
|
||||
return `${diffInHours}小时前`;
|
||||
}
|
||||
// 1天 ~ 30天
|
||||
else if (diffInDays < 30) {
|
||||
return `${diffInDays}天前`;
|
||||
}
|
||||
// 超过30天但不到1年
|
||||
else if (diffInYears < 1) {
|
||||
return target.format('MM-DD HH:mm');
|
||||
}
|
||||
// 超过1年
|
||||
else {
|
||||
return target.format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
};
|
||||
275
src/views/creative-generation-workshop/explore/detail/index.vue
Normal file
@ -0,0 +1,275 @@
|
||||
<script lang="jsx">
|
||||
import { Image, Spin, Button } from '@arco-design/web-vue';
|
||||
import AiSuggest from './components/ai-suggest/';
|
||||
|
||||
import { getShareWorksList, getShareWorksDetail, patchShareWorksConfirm } from '@/api/all/generationWorkshop.ts';
|
||||
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
|
||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
|
||||
import { ENUM_OPINION } from './constants';
|
||||
import { handleUserHome } from '@/utils/user.ts';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
import icon1 from '@/assets/logo.svg';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-confirm.png';
|
||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-line.png';
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const dataSource = ref({});
|
||||
const shareWorks = ref({});
|
||||
const isExpand = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const videoRef = ref(null);
|
||||
const videoUrl = ref('');
|
||||
const coverImageUrl = ref('');
|
||||
const isVideoLoaded = ref(false);
|
||||
const images = ref([]);
|
||||
|
||||
const isVideo = computed(() => dataSource.value.type === EnumManuscriptType.Video);
|
||||
const isMultiWork = computed(() => shareWorks.value.works?.length > 1);
|
||||
const shareCode = computed(() => route.params.shareCode);
|
||||
const hasPrevBtn = computed(() => dataSource.value.prev);
|
||||
const hasNextBtn = computed(() => dataSource.value.next);
|
||||
|
||||
const initData = () => {
|
||||
videoUrl.value = '';
|
||||
coverImageUrl.value = '';
|
||||
images.value = [];
|
||||
if (!dataSource.value.files.length) return;
|
||||
|
||||
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 getDetail = async () => {
|
||||
try {
|
||||
dataSource.value = {};
|
||||
loading.value = true;
|
||||
|
||||
const { id } = route.params;
|
||||
const { code, data } = await getShareWorksDetail(id, shareCode.value);
|
||||
if (code === 200) {
|
||||
dataSource.value = data;
|
||||
initData();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateCommentList = async () => {
|
||||
const { id } = route.params;
|
||||
const { code, data } = await getShareWorksDetail(id, shareCode.value);
|
||||
if (code === 200) {
|
||||
dataSource.value.comments = data.comments;
|
||||
}
|
||||
};
|
||||
|
||||
const getShareWorks = async () => {
|
||||
const { code, data } = await getShareWorksList(shareCode.value);
|
||||
if (code === 200) {
|
||||
shareWorks.value = data;
|
||||
}
|
||||
};
|
||||
|
||||
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-cover" onEnded={onVideoEnded}></video>
|
||||
{!isPlaying.value && (
|
||||
<>
|
||||
<img src={coverImageUrl.value} class="w-100% h-100% object-cover 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-cover absolute z-0 top-0 left-0" />
|
||||
</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 onBackList = () => {
|
||||
router.push({
|
||||
path: `/explore/list/${shareCode.value}`,
|
||||
});
|
||||
};
|
||||
const onPrevWork = async () => {
|
||||
await router.push({
|
||||
path: `/explore/detail/${shareCode.value}/${dataSource.value.prev.id}`,
|
||||
});
|
||||
getDetail();
|
||||
};
|
||||
const onNextWork = async () => {
|
||||
await router.push({
|
||||
path: `/explore/detail/${shareCode.value}/${dataSource.value.next.id}`,
|
||||
});
|
||||
getDetail();
|
||||
};
|
||||
const handleConfirm = async () => {
|
||||
const { code, data } = await patchShareWorksConfirm(dataSource.value.id, shareCode.value);
|
||||
if (code === 200) {
|
||||
dataSource.value.customer_opinion = ENUM_OPINION.confirm;
|
||||
}
|
||||
};
|
||||
const onUpdateComment = () => {
|
||||
updateCommentList();
|
||||
};
|
||||
const onDeleteComment = (id) => {
|
||||
const index = dataSource.value.comments.findIndex((item) => item.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
console.log({ index });
|
||||
dataSource.value.comments.splice(index, 1);
|
||||
};
|
||||
const renderConfirmBtn = () => {
|
||||
if (userStore.isLogin) return null;
|
||||
if (dataSource.value.customer_opinion === ENUM_OPINION.confirm) return null;
|
||||
|
||||
return (
|
||||
<Button type="primary" size="large" onClick={handleConfirm}>
|
||||
确认内容稿件
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
const renderActionRow = () => {
|
||||
if (loading.value) return null;
|
||||
|
||||
if (isMultiWork.value) {
|
||||
return (
|
||||
<>
|
||||
{hasPrevBtn.value && (
|
||||
<Button type="text" size="large" class="mr-12px" onClick={onPrevWork}>
|
||||
上一条
|
||||
</Button>
|
||||
)}
|
||||
{hasNextBtn.value && (
|
||||
<Button type="text" size="large" class="mr-12px" onClick={onNextWork}>
|
||||
下一条
|
||||
</Button>
|
||||
)}
|
||||
<Button type="outline" size="large" class="mr-12px" onClick={onBackList}>
|
||||
返回列表
|
||||
</Button>
|
||||
{renderConfirmBtn()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return renderConfirmBtn();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDetail();
|
||||
getShareWorks();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.pause();
|
||||
videoRef.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="explore-page">
|
||||
<header class="page-header">
|
||||
<div class="content w-full px-24px flex items-center bg-#fff justify-between">
|
||||
<div class="h-full flex items-center cursor-pointer" onClick={handleUserHome}>
|
||||
<img src={icon1} alt="" width={130} />
|
||||
</div>
|
||||
<div class="flex items-center">{renderActionRow()}</div>
|
||||
</div>
|
||||
</header>
|
||||
{loading.value ? (
|
||||
<Spin spinning={loading.value} class="flex-1 w-full flex justify-center items-center" size={60} />
|
||||
) : (
|
||||
<section class={`page-wrap relative ${isExpand.value ? 'expand' : ''}`}>
|
||||
<div class="fold-box cursor-pointer" onClick={() => (isExpand.value = true)}>
|
||||
<icon-menu-fold size={20} class="color-#55585F hover:color-#6D4CFE" />
|
||||
</div>
|
||||
<section class="explore-detail-wrap pt-32px pb-52px flex flex-col items-center">
|
||||
<div class="flex justify-start flex-col w-full relative">
|
||||
<p class="title mb-8px">{dataSource.value.title}</p>
|
||||
<p class="cts color-#737478 mb-32px">
|
||||
{exactFormatTime(dataSource.value.last_modified_at, 'YYYY年MM月DD日')}修改
|
||||
</p>
|
||||
{dataSource.value.customer_opinion === ENUM_OPINION.confirm && (
|
||||
<img src={icon2} width={92} height={92} class="absolute right-0 bottom-0" />
|
||||
)}
|
||||
</div>
|
||||
{renderMainImg()}
|
||||
<div class="w-full">
|
||||
<p class="cts !color-#211F24">{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-cover" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{isExpand.value && (
|
||||
<AiSuggest
|
||||
isExpand={isExpand.value}
|
||||
dataSource={dataSource.value}
|
||||
onToggle={(expand) => (isExpand.value = expand)}
|
||||
onUpdateComment={onUpdateComment}
|
||||
onDeleteComment={onDeleteComment}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
106
src/views/creative-generation-workshop/explore/detail/style.scss
Normal file
@ -0,0 +1,106 @@
|
||||
.explore-page {
|
||||
position: relative;
|
||||
padding-top: $navbar-height;
|
||||
min-width: 1200px;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.page-header {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
min-width: 1200px;
|
||||
.content {
|
||||
height: $navbar-height;
|
||||
border-bottom: 1px solid var(--Border-1, #d7d7d9);
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
.explore-detail-wrap {
|
||||
min-height: 500px;
|
||||
width: 684px;
|
||||
.title {
|
||||
color: var(--Text-1, #211f24);
|
||||
font-family: $font-family-medium;
|
||||
font-size: 28px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 40px; /* 142.857% */
|
||||
}
|
||||
}
|
||||
.fold-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 30px;
|
||||
border: 1px solid var(--Border-1, #d7d7d9);
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.main-video-box {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
}
|
||||
.main-img-box {
|
||||
width: 347px;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
aspect-ratio: 3/4;
|
||||
}
|
||||
.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/views/creative-generation-workshop/explore/list/index.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script lang="jsx">
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import { Image, Spin } from '@arco-design/web-vue';
|
||||
|
||||
import { exactFormatTime } from '@/utils/tools';
|
||||
import { handleUserHome } from '@/utils/user.ts';
|
||||
import { getShareWorksList } from '@/api/all/generationWorkshop';
|
||||
import { ENUM_OPINION } from '../detail/constants';
|
||||
|
||||
import icon1 from '@/assets/img/error-img.png';
|
||||
import icon2 from '@/assets/logo.svg';
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const dataSource = ref({});
|
||||
const loading = ref(false);
|
||||
const works = computed(() => dataSource.value.works ?? []);
|
||||
const shareCode = computed(() => route.params.shareCode);
|
||||
|
||||
const onClickItem = (item) => {
|
||||
router.push({
|
||||
path: `/explore/detail/${shareCode.value}/${item.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const getShareWorks = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { code, data } = await getShareWorksList(shareCode.value);
|
||||
if (code === 200) {
|
||||
dataSource.value = data;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getShareWorks();
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="explore-page">
|
||||
<header class="page-header">
|
||||
<div class="content w-full px-24px flex items-center bg-#fff">
|
||||
<div class="h-full flex items-center cursor-pointer" onClick={handleUserHome}>
|
||||
<img src={icon2} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="page-wrapper flex justify-center">
|
||||
{loading.value ? (
|
||||
<Spin spinning={loading.value} class="w-full flex justify-center items-center" size={60} />
|
||||
) : (
|
||||
<div class="explore-container">
|
||||
<div class="explore-list-wrap pt-24px pb-28px">
|
||||
<TextOverTips context={`${works.value[0]?.title}等${works.value.length}个文件`} class="mb-8px" />
|
||||
<p class="cts !color-#939499 mb-24px">
|
||||
{`分享时间:${exactFormatTime(dataSource.value.created_at, 'YYYY-MM-DD HH:mm:ss')} 有效期${
|
||||
dataSource.value.days
|
||||
}天`}
|
||||
</p>
|
||||
<div class="card-container">
|
||||
{works.value.map((item) => {
|
||||
return (
|
||||
<div
|
||||
class="card-item rounded-8px overflow-hidden"
|
||||
key={item.id}
|
||||
onClick={() => onClickItem(item)}
|
||||
>
|
||||
<Image
|
||||
src={item.cover}
|
||||
width={'100%'}
|
||||
height={300}
|
||||
preview={false}
|
||||
fit="cover"
|
||||
v-slots={{
|
||||
error: () => <img src={icon1} class="w-full h-full" />,
|
||||
}}
|
||||
/>
|
||||
<div class="p-16px">
|
||||
<TextOverTips context={item.title} class=" !lh-24px !text-16px mb-8px bold" />
|
||||
<div class="flex justify-between">
|
||||
<p class="cts color-#737478">
|
||||
{exactFormatTime(item.last_modified_at, 'YYYY年MM月DD日')}修改
|
||||
</p>
|
||||
{item.customer_opinion === ENUM_OPINION.confirm && (
|
||||
<div class="h-24px px-8px flex justify-center items-center rounded-2px bg-#EBF7F2">
|
||||
<span class="cts bold color-#25C883">已确认</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,63 @@
|
||||
.explore-page {
|
||||
position: relative;
|
||||
padding-top: $navbar-height;
|
||||
min-width: 1200px;
|
||||
background: #fff;
|
||||
.cts {
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.page-header {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
min-width: 1200px;
|
||||
.content {
|
||||
height: $navbar-height;
|
||||
border-bottom: 1px solid var(--Border-1, #d7d7d9);
|
||||
}
|
||||
}
|
||||
.page-wrapper {
|
||||
background: #fff;
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
.explore-container {
|
||||
width: 1200px;
|
||||
.explore-list-wrap {
|
||||
:deep(.overflow-text) {
|
||||
color: var(--Text-1, #211f24);
|
||||
font-family: $font-family-regular;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
&.bold {
|
||||
font-family: $font-family-medium;
|
||||
}
|
||||
}
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
.card-item {
|
||||
border: 1px solid var(--Border-1, #d7d7d9);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
|
||||
border: 1.01px solid var(--Border-1, #d7d7d9);
|
||||
border-radius: 8.08px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
<!-- 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, AuditStatus } from '@/views/creative-generation-workshop/manuscript/check-list/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,192 @@
|
||||
<template>
|
||||
<a-table
|
||||
ref="tableRef"
|
||||
:data="dataSource"
|
||||
row-key="id"
|
||||
column-resizable
|
||||
:pagination="false"
|
||||
:scroll="{ x: '100%' }"
|
||||
class="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" :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="
|
||||
['updated_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 }">
|
||||
<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>
|
||||
</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/creative-generation-workshop/manuscript/list/constants';
|
||||
import { patchWorkAuditsAuditWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import {
|
||||
AuditStatus,
|
||||
CUSTOMER_OPINION,
|
||||
PLATFORMS,
|
||||
} from '@/views/creative-generation-workshop/manuscript/check-list/constants';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
|
||||
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', '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({ path: `/writer/manuscript/check/${writerCode.value}` });
|
||||
};
|
||||
const onScan = (item) => {
|
||||
slsWithCatch('writerManuscriptCheckIds', [item.id]);
|
||||
router.push({ path: `/writer/manuscript/check/${writerCode.value}` });
|
||||
};
|
||||
const onDetail = (item) => {
|
||||
router.push(
|
||||
`/writer/manuscript/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,244 @@
|
||||
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: 'type',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'updated_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: 220,
|
||||
},
|
||||
{
|
||||
title: '审核平台',
|
||||
dataIndex: 'platform',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '合规程度',
|
||||
dataIndex: 'compliance_level',
|
||||
suffix: '%',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
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: 200,
|
||||
},
|
||||
{
|
||||
title: '审核平台',
|
||||
dataIndex: 'platform',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '稿件类型',
|
||||
dataIndex: 'type',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
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 enum AuditStatus {
|
||||
Pending = '1',
|
||||
Auditing = '2',
|
||||
Passed = '3',
|
||||
}
|
||||
|
||||
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/media-account/icon-dy.png';
|
||||
import icon2 from '@/assets/img/media-account/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,208 @@
|
||||
<template>
|
||||
<div class="manuscript-check-wrap">
|
||||
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid mb-16px">
|
||||
<a-tabs v-model="query.audit_status" @tab-click="handleTabClick">
|
||||
<a-tab-pane :title="item.label" v-for="item in AUDIT_STATUS_LIST" :key="item.value"></a-tab-pane>
|
||||
<!-- <template #extra>
|
||||
<a-button type="outline" size="medium" @click="handleShareModal">分享内容稿件</a-button>
|
||||
</template> -->
|
||||
</a-tabs>
|
||||
<FilterBlock
|
||||
v-model:query="query"
|
||||
:audit_status="query.audit_status"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
</div>
|
||||
<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)"
|
||||
>
|
||||
<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"
|
||||
@edit="handleEdit"
|
||||
@select="handleSelect"
|
||||
@selectAll="handleSelectAll"
|
||||
/>
|
||||
<div v-if="pageInfo.total > 0" class="pagination-box">
|
||||
<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, 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,
|
||||
INITIAL_QUERY,
|
||||
AUDIT_STATUS_LIST,
|
||||
TABLE_COLUMNS1,
|
||||
TABLE_COLUMNS2,
|
||||
TABLE_COLUMNS3,
|
||||
} from '@/views/creative-generation-workshop/manuscript/check-list/constants';
|
||||
|
||||
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({ path: `/writer/manuscript/check/${writerCode.value}` });
|
||||
};
|
||||
const handleBatchView = () => {
|
||||
if (!selectedRows.value.length) {
|
||||
AMessage.warning('请选择需查看的内容稿件');
|
||||
return;
|
||||
}
|
||||
|
||||
slsWithCatch('writerManuscriptCheckIds', selectedRowKeys.value);
|
||||
router.push({ path: `/writer/manuscript/check/${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 handleShareModal = () => {
|
||||
// shareManuscriptModalRef.value.open();
|
||||
// };
|
||||
|
||||
const handleDelete = (item) => {
|
||||
const { id, title } = item;
|
||||
deleteManuscriptModalRef.value?.open({ id, name: `“${title}”` });
|
||||
};
|
||||
const handleEdit = (item) => {
|
||||
// addManuscriptModalRef.value?.open(item.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
tableColumns.value = TABLE_COLUMNS1;
|
||||
getData();
|
||||
});
|
||||
provide('update', getData);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,42 @@
|
||||
.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;
|
||||
.pagination-box {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
<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({ path: `/writer/manuscript/check-list/${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,62 @@
|
||||
<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 } 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([]);
|
||||
|
||||
const onClose = () => {
|
||||
if (workIds.value.length === 1) {
|
||||
router.push({ path: `/writer/manuscript/check-list/${route.params.writerCode}` });
|
||||
}
|
||||
workIds.value = [];
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const open = (ids) => {
|
||||
workIds.value = cloneDeep(ids);
|
||||
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,78 @@
|
||||
<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 {
|
||||
setup(props, { emit, expose }) {
|
||||
const visible = ref(false);
|
||||
const dataSource = ref([]);
|
||||
const selectCardInfo = ref({});
|
||||
|
||||
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}
|
||||
>
|
||||
<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">{`共${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
|
||||
class={`card-item flex rounded-8px bg-#F7F8FA p-8px ${
|
||||
selectCardInfo.value.id === item.id ? 'active' : ''
|
||||
}`}
|
||||
key={item.id}
|
||||
>
|
||||
<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">{`合规程度:${90}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,31 @@
|
||||
.check-list-drawer-xt {
|
||||
.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 {
|
||||
border: 1px solid transparent;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
&.active {
|
||||
border-color: #6d4cfe;
|
||||
background-color: #f0edff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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_items',
|
||||
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,179 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<!-- 透明输入层 -->
|
||||
<a-textarea
|
||||
v-model="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
maxlength="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@scroll="syncScroll"
|
||||
:style="textareaStyle"
|
||||
/>
|
||||
|
||||
<!-- 高亮显示层 -->
|
||||
<div
|
||||
class="textarea-highlight"
|
||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml"
|
||||
@scroll="syncScroll"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineProps, defineEmits } from 'vue';
|
||||
import { isString } from '@/utils/is';
|
||||
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>;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
maxLength?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
// 内部状态管理
|
||||
const inputValue = ref(props.modelValue || '');
|
||||
const scrollTop = ref(0);
|
||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||
|
||||
// 监听外部modelValue变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal !== inputValue.value) {
|
||||
inputValue.value = newVal || '';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 处理输入事件
|
||||
const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
};
|
||||
|
||||
// 同步滚动位置
|
||||
const syncScroll = (e: Event) => {
|
||||
console.log('syncScroll');
|
||||
scrollTop.value = (e.target as HTMLTextAreaElement).scrollTop;
|
||||
};
|
||||
|
||||
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="s1" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.highlight-textarea-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.textarea-input,
|
||||
.textarea-highlight {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
// font-family: inherit;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.s1 {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400px;
|
||||
}
|
||||
.textarea-input {
|
||||
:deep(.arco-textarea) {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: #211f24 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 字数统计样式 */
|
||||
.word-count {
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,457 @@
|
||||
<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/creative-generation-workshop/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,
|
||||
},
|
||||
},
|
||||
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 || !rule.replace_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" 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 renderRightBox = () => {
|
||||
if (props.checkLoading) {
|
||||
return (
|
||||
<div class="right-box flex-1 h-210px 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>
|
||||
<Spin loading={true} tip={`${isTextTab.value ? '文本' : '图片'}检测中`} size={72} class="" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (!aiReview.value?.violation_items?.length) {
|
||||
return (
|
||||
<div class="right-box flex-1 h-372px 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" 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">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="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" 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">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>
|
||||
</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,190 @@
|
||||
.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 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 {
|
||||
.s1 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
.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/creative-generation-workshop/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">{`合规程度:${90}%`}</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,248 @@
|
||||
<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 {
|
||||
getWorkAuditsBatchDetailWriter,
|
||||
putWorkAuditsUpdateWriter,
|
||||
postWorkAuditsAiReviewWriter,
|
||||
getWorkAuditsAiReviewResultWriter,
|
||||
putWorkAuditsAuditPassWriter,
|
||||
} from '@/api/all/generationWorkshop-writer.ts';
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
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 selectCardInfo = ref({});
|
||||
const selectedImageInfo = ref(null);
|
||||
|
||||
const writerCode = computed(() => route.params.writerCode);
|
||||
|
||||
const { handleStartCheck, handleAgainCheck, ticket, checkLoading } = useGetAiReviewResult({
|
||||
cardInfo: selectCardInfo,
|
||||
startAiReviewFn: postWorkAuditsAiReviewWriter,
|
||||
getAiReviewResultFn: getWorkAuditsAiReviewResultWriter,
|
||||
updateAiReview(ai_review) {
|
||||
selectCardInfo.value.ai_review = ai_review;
|
||||
},
|
||||
});
|
||||
|
||||
const onBack = () => {
|
||||
router.push({ path: `/writer/manuscript/check-list/${writerCode.value}` });
|
||||
};
|
||||
|
||||
const onChangeCard = (item) => {
|
||||
contentCardRef.value.reset();
|
||||
isSaved.value = false;
|
||||
submitLoading.value = false;
|
||||
checkLoading.value = false;
|
||||
ticket.value = '';
|
||||
|
||||
const { files = [], ai_review } = item;
|
||||
selectCardInfo.value = cloneDeep(item);
|
||||
selectedImageInfo.value = cloneDeep(files?.[0] ?? {});
|
||||
|
||||
console.log({ ai_review });
|
||||
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 () => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 hover:color-#6D4CFE" />
|
||||
<span class="cts !color-#211F24">审核列表</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}
|
||||
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}
|
||||
onAgainCheck={onAgainCheck}
|
||||
onStartCheck={handleStartCheck}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex justify-end items-center px-16px py-20px w-full bg-#fff footer-row">
|
||||
{renderFooterRow()}
|
||||
</footer>
|
||||
|
||||
<CancelCheckModal ref={cancelCheckModalRef} onSelectCard={onChangeCard} />
|
||||
<CheckSuccessModal ref={checkSuccessModalRef} />
|
||||
<CheckListDrawer ref={checkListDrawerRef} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,39 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-check-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 72px);
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.footer-row {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: $sidebar-width;
|
||||
width: calc(100% - $sidebar-width);
|
||||
border-top: 1px solid #e6e6e8;
|
||||
height: $footer-height;
|
||||
}
|
||||
@ -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/creative-generation-workshop/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,193 @@
|
||||
<script lang="jsx">
|
||||
import { Button, Message as AMessage, Spin } from '@arco-design/web-vue';
|
||||
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { AuditStatus } from '@/views/creative-generation-workshop/manuscript/check-list/constants';
|
||||
import { getWorksDetailWriter } from '@/api/all/generationWorkshop-writer.ts';
|
||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
|
||||
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
|
||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
||||
|
||||
const DEFAULT_SOURCE_INFO = {
|
||||
title: '内容稿件列表',
|
||||
routePath: '/writer/manuscript/list',
|
||||
};
|
||||
const SOURCE_MAP = new Map([['check', { title: '内容稿件审核', routePath: '/writer/manuscript/check-list' }]]);
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
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 onBack = () => {
|
||||
router.push({ path: `${sourceInfo.value.routePath}/${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 absolute z-0 top-0 left-0" />
|
||||
</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({ path: `/writer/manuscript/check/${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(`/writer/manuscript/edit/${writerCode.value}/${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 overflow-y-auto bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid 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 ">{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>
|
||||
<div class="flex justify-end items-center px-16px py-20px w-full bg-#fff footer-row">{renderFooterRow()}</div>
|
||||
</Spin>
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
@ -0,0 +1,65 @@
|
||||
$footer-height: 68px;
|
||||
.manuscript-detail-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 72px);
|
||||
.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: auto;
|
||||
background: #fff;
|
||||
}
|
||||
.main-img-box {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
aspect-ratio: 3/4;
|
||||
}
|
||||
.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);
|
||||
border-top: 1px solid #e6e6e8;
|
||||
height: $footer-height;
|
||||
}
|
||||