Merge remote-tracking branch 'origin/feature/v1.3_主agent_rxd' into test
This commit is contained in:
@ -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;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: RenXiaoDong
|
|
||||||
* @Date: 2025-06-24 16:50:35
|
|
||||||
*/
|
|
||||||
export const WHITE_LIST = [
|
|
||||||
{ name: 'notFound', children: [] },
|
|
||||||
{ name: 'login', children: [] },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const NOT_FOUND = {
|
|
||||||
name: 'notFound',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const REDIRECT_ROUTE_NAME = 'Redirect';
|
|
||||||
|
|
||||||
export const DEFAULT_ROUTE_NAME = 'main';
|
|
||||||
|
|
||||||
export const DEFAULT_ROUTE = {
|
|
||||||
title: '首页',
|
|
||||||
name: DEFAULT_ROUTE_NAME,
|
|
||||||
fullPath: '/',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MENU_GROUP_IDS = {
|
|
||||||
DATA_ENGINE_ID: 1, // 全域数据分析
|
|
||||||
MANAGEMENT_ID: -1, // 管理中心
|
|
||||||
PROPERTY_ID: 10, // 资产营销平台
|
|
||||||
WORK_BENCH_ID: -99, // 工作台
|
|
||||||
AGENT: 2, // 智能体
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: 田鑫
|
|
||||||
* @Date: 2023-03-05 18:14:17
|
|
||||||
* @LastEditors: Please set LastEditors
|
|
||||||
* @LastEditTime: 2025-06-23 04:10:41
|
|
||||||
* @Description:
|
|
||||||
*/
|
|
||||||
import type { Router } from 'vue-router';
|
|
||||||
import { setRouteEmitter } from '@/utils/route-listener';
|
|
||||||
import setupUserLoginInfoGuard from './userLoginInfo';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
// import setupPermissionGuard from './permission';
|
|
||||||
|
|
||||||
function setupPageGuard(router: Router) {
|
|
||||||
router.beforeEach(async (to) => {
|
|
||||||
// emit route change
|
|
||||||
setRouteEmitter(to);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function createRouteGuard(router: Router) {
|
|
||||||
|
|
||||||
setupPageGuard(router);
|
|
||||||
setupUserLoginInfoGuard(router);
|
|
||||||
// setupPermissionGuard(router);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: 田鑫
|
|
||||||
* @Date: 2023-03-05 14:46:43
|
|
||||||
* @LastEditors: Please set LastEditors
|
|
||||||
* @LastEditTime: 2025-06-23 04:07:43
|
|
||||||
* @Description: 路由权限守卫
|
|
||||||
*/
|
|
||||||
// import type { Router, RouteRecordNormalized } from 'vue-router';
|
|
||||||
// import NProgress from 'nprogress'; // progress bar
|
|
||||||
// import { useAppStore } from '@/stores';
|
|
||||||
|
|
||||||
// export default function setupPermissionGuard(router: Router) {
|
|
||||||
// router.beforeEach(async (to, from, next) => {
|
|
||||||
// console.log('access permission router guard');
|
|
||||||
// const appStore = useAppStore();
|
|
||||||
// //* 菜单是否为服务端渲染
|
|
||||||
// if (appStore.menuFromServer) {
|
|
||||||
// //* 没有服务端渲染的菜单
|
|
||||||
// if (!appStore.appAsyncMenus) {
|
|
||||||
// // todo 请求服务端渲染菜单的接口,当前为mock数据
|
|
||||||
// // await appStore.fetchServerMenuConfig();
|
|
||||||
// }
|
|
||||||
// next();
|
|
||||||
// } else {
|
|
||||||
// next();
|
|
||||||
// }
|
|
||||||
// NProgress.done();
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: RenXiaoDong
|
|
||||||
* @Date: 2025-06-22 22:59:16
|
|
||||||
*/
|
|
||||||
import type { Router } from 'vue-router';
|
|
||||||
import NProgress from 'nprogress';
|
|
||||||
import { goUserLogin } from '@/utils/user';
|
|
||||||
// import router from '@/router';
|
|
||||||
// import { checkRoutePermission } from '@/permission/permission';
|
|
||||||
|
|
||||||
import { useUserStore } from '@/stores/modules/user';
|
|
||||||
|
|
||||||
export default function setupUserLoginInfoGuard(router: Router) {
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
NProgress.start();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const routeName = to?.name as string;
|
|
||||||
const requiresAuth = to?.meta?.requiresAuth || false;
|
|
||||||
const requireLogin = to?.meta?.requireLogin || false;
|
|
||||||
const query = to?.query ?? {};
|
|
||||||
|
|
||||||
if (requireLogin && !userStore.isLogin) {
|
|
||||||
goUserLogin(query);
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (requiresAuth) {
|
|
||||||
// const hasPermission = checkRoutePermission(routeName);
|
|
||||||
// if (!hasPermission) {
|
|
||||||
// AMessage.error('您没有权限访问该页面');
|
|
||||||
// next('/');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// next();
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
router.afterEach((to) => {
|
|
||||||
NProgress.done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: RenXiaoDong
|
|
||||||
* @Date: 2025-06-22 22:59:16
|
|
||||||
*/
|
|
||||||
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';
|
|
||||||
import createRouteGuard from './guard';
|
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
|
||||||
|
|
||||||
export const router = createRouter({
|
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'UserLogin',
|
|
||||||
component: () => import('@/views/components/login'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'Home',
|
|
||||||
component: () => import('@/views/components/workplace'),
|
|
||||||
meta: {
|
|
||||||
hideSidebar: true,
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
id: MENU_GROUP_IDS.WORK_BENCH_ID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...appRoutes,
|
|
||||||
NOT_FOUND_ROUTE,
|
|
||||||
],
|
|
||||||
scrollBehavior() {
|
|
||||||
return { top: 0 };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createRouteGuard(router);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import type { RouteRecordNormalized } from 'vue-router';
|
|
||||||
// import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './base';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
|
|
||||||
const modules = import.meta.glob('./modules/*.ts', { eager: true });
|
|
||||||
// const externalModules = import.meta.glob('./externalModules/*.ts', {
|
|
||||||
// eager: true,
|
|
||||||
// });
|
|
||||||
|
|
||||||
function formatModules(_modules: any, result: RouteRecordNormalized[]) {
|
|
||||||
Object.keys(_modules).forEach((key) => {
|
|
||||||
const defaultModule = _modules[key].default;
|
|
||||||
if (!defaultModule) return;
|
|
||||||
const moduleList = Array.isArray(defaultModule) ? [...defaultModule] : [defaultModule];
|
|
||||||
result.push(...moduleList);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []);
|
|
||||||
|
|
||||||
// export const appExternalRoutes: RouteRecordNormalized[] = formatModules(externalModules, []);
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import type { AppRouteRecordRaw } from '../types';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
|
|
||||||
import IconRepository from '@/assets/svg/svg-agent.svg';
|
|
||||||
|
|
||||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/agent',
|
|
||||||
name: 'Agent',
|
|
||||||
redirect: 'agent/index',
|
|
||||||
meta: {
|
|
||||||
locale: '灵机ai',
|
|
||||||
icon: IconRepository,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.AGENT,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'index',
|
|
||||||
name: 'AgentIndex',
|
|
||||||
component: () => import('@/views/agent/index'),
|
|
||||||
meta: {
|
|
||||||
locale:'智能体应用',
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
hideFooter: true,
|
|
||||||
isAgentRoute: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'chat',
|
|
||||||
name: 'AgentChat',
|
|
||||||
component: () => import('@/views/agent/chat'),
|
|
||||||
meta: {
|
|
||||||
hideSidebar: true,
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
hideFooter: true,
|
|
||||||
id: MENU_GROUP_IDS.AGENT,
|
|
||||||
isAgentRoute: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: 'workFlow',
|
|
||||||
name: 'AgentWorkFlow',
|
|
||||||
component: () => import('@/views/agent/work-flow'),
|
|
||||||
meta: {
|
|
||||||
hideSidebar: true,
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
hideFooter: true,
|
|
||||||
id: MENU_GROUP_IDS.AGENT,
|
|
||||||
isAgentRoute: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default COMPONENTS;
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { IconBookmark } from '@arco-design/web-vue/es/icon';
|
|
||||||
import type { AppRouteRecordRaw } from '../types';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
|
|
||||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/dataEngine',
|
|
||||||
name: 'DataEngine',
|
|
||||||
redirect: 'dataEngine/hotTranslation',
|
|
||||||
meta: {
|
|
||||||
locale: '全域数据引擎',
|
|
||||||
icon: IconBookmark,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.DATA_ENGINE_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'hotTranslation',
|
|
||||||
name: 'DataEngineHotTranslation',
|
|
||||||
meta: {
|
|
||||||
locale: '行业热门话题洞察',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/hotTranslation.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'hotCloud',
|
|
||||||
name: 'DataEngineHotCloud',
|
|
||||||
meta: {
|
|
||||||
locale: '行业词云',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/hotCloud.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'keyWord',
|
|
||||||
name: 'DataEngineKeyWord',
|
|
||||||
meta: {
|
|
||||||
locale: '行业关键词动向',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/keyWord.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'userPainPoints',
|
|
||||||
name: 'DataEngineUserPainPoints',
|
|
||||||
meta: {
|
|
||||||
locale: '用户痛点观察',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/userPainPoints.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'keyBrandMovement',
|
|
||||||
name: 'DataEngineKeyBrandMovement',
|
|
||||||
meta: {
|
|
||||||
locale: '重点品牌动向',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/keyBrandMovement.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'userPersona',
|
|
||||||
name: 'DataEngineUserPersona',
|
|
||||||
meta: {
|
|
||||||
locale: '用户画像',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/components/dataEngine/userPersona.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default COMPONENTS;
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { IconBookmark } from '@arco-design/web-vue/es/icon';
|
|
||||||
import type { AppRouteRecordRaw } from '../types';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
|
|
||||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/management',
|
|
||||||
name: 'Management',
|
|
||||||
redirect: 'management/person',
|
|
||||||
meta: {
|
|
||||||
locale: '管理中心',
|
|
||||||
icon: IconBookmark,
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.MANAGEMENT_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'person',
|
|
||||||
name: 'ManagementPerson',
|
|
||||||
component: () => import('@/views/components/management/person'),
|
|
||||||
meta: {
|
|
||||||
locale: '个人信息',
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'enterprise',
|
|
||||||
name: 'ManagementEnterprise',
|
|
||||||
component: () => import('@/views/components/management/enterprise'),
|
|
||||||
meta: {
|
|
||||||
locale: '企业信息',
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'account',
|
|
||||||
name: 'ManagementAccount',
|
|
||||||
component: () => import('@/views/components/management/account'),
|
|
||||||
meta: {
|
|
||||||
locale: '账号管理',
|
|
||||||
requiresAuth: false,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default COMPONENTS;
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
/**
|
|
||||||
* 资产营销平台
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { AppRouteRecordRaw } from '../types';
|
|
||||||
import { MENU_GROUP_IDS } from '@/router/constants';
|
|
||||||
|
|
||||||
import IconRepository from '@/assets/svg/svg-repository.svg';
|
|
||||||
import IconMediaAccount from '@/assets/svg/svg-mediaAccount.svg';
|
|
||||||
import IconPutAccount from '@/assets/svg/svg-putAccount.svg';
|
|
||||||
import IconIntelligentSolution from '@/assets/svg/svg-intelligentSolution.svg';
|
|
||||||
import IconProjectManagement from '@/assets/svg/svg-projectManagement.svg';
|
|
||||||
|
|
||||||
const COMPONENTS: AppRouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/repository',
|
|
||||||
name: 'Repository',
|
|
||||||
redirect: 'repository/brandMaterials',
|
|
||||||
meta: {
|
|
||||||
locale: '品牌资产管理',
|
|
||||||
icon: IconRepository,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.PROPERTY_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'brandMaterials',
|
|
||||||
name: 'RepositoryBrandMaterials',
|
|
||||||
meta: {
|
|
||||||
locale: '品牌信息',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/brands/brand-materials/index.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/media-account',
|
|
||||||
name: 'MediaAccount',
|
|
||||||
redirect: 'media-account/accountManagement',
|
|
||||||
meta: {
|
|
||||||
locale: '账号资源中心',
|
|
||||||
icon: IconMediaAccount,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.PROPERTY_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'manage',
|
|
||||||
name: 'MediaAccountAccountManagement',
|
|
||||||
meta: {
|
|
||||||
locale: '账号管理',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/media-account/account-manage'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'dashboard',
|
|
||||||
name: 'MediaAccountAccountDashboard',
|
|
||||||
meta: {
|
|
||||||
locale: '账号数据看板',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/media-account/account-dashboard'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'detail/:id',
|
|
||||||
name: 'MediaAccountAccountDetails',
|
|
||||||
meta: {
|
|
||||||
locale: '账号详情',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'MediaAccountAccountDashboard',
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/media-account/account-detail'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/put-account',
|
|
||||||
name: 'PutAccount',
|
|
||||||
redirect: 'put-account/accountManagement',
|
|
||||||
meta: {
|
|
||||||
locale: '投放资源中心',
|
|
||||||
icon: IconPutAccount,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.PROPERTY_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'manage',
|
|
||||||
name: 'PutAccountAccountManagement',
|
|
||||||
meta: {
|
|
||||||
locale: '账户管理',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/put-account/account-manage'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'data',
|
|
||||||
name: 'PutAccountAccountData',
|
|
||||||
meta: {
|
|
||||||
locale: '账户数据',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/put-account/account-data'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'account-dashboard',
|
|
||||||
name: 'PutAccountAccountDashboard',
|
|
||||||
meta: {
|
|
||||||
locale: '投放表现分析',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/put-account/account-dashboard'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'investmentGuidelines',
|
|
||||||
name: 'PutAccountInvestmentGuidelines',
|
|
||||||
meta: {
|
|
||||||
locale: '投放指南',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'detail/:id',
|
|
||||||
name: 'PutAccountInvestmentGuidelinesDetail',
|
|
||||||
meta: {
|
|
||||||
locale: '投放指南详情',
|
|
||||||
requiresAuth: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
roles: ['*'],
|
|
||||||
activeMenu: 'PutAccountInvestmentGuidelines',
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/put-account/investment-guidelines/detail'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: '/intelligent-solution',
|
|
||||||
// name: 'IntelligentSolution',
|
|
||||||
// redirect: 'intelligent-solution/businessAnalysisReport',
|
|
||||||
// meta: {
|
|
||||||
// locale: '智能方案管理',
|
|
||||||
// icon: IconIntelligentSolution,
|
|
||||||
// requiresAuth: true,
|
|
||||||
// requireLogin: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// id: MENU_GROUP_IDS.PROPERTY_ID,
|
|
||||||
// },
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// path: 'businessAnalysisReport',
|
|
||||||
// name: 'IntelligentSolutionBusinessAnalysisReport',
|
|
||||||
// meta: {
|
|
||||||
// locale: '业务洞察报告',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// requireLogin: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/property-marketing/intelligent-solution/businessAnalysisReport'),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'competitiveProductAnalysisReport',
|
|
||||||
// name: 'IntelligentSolutionCompetitiveProductAnalysisReport',
|
|
||||||
// meta: {
|
|
||||||
// locale: '竟品对比报告',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// requireLogin: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/property-marketing/intelligent-solution/competitiveProductAnalysisReport'),
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: '/project-manage',
|
|
||||||
name: 'ProjectManagement',
|
|
||||||
redirect: 'project-manage/project-list',
|
|
||||||
meta: {
|
|
||||||
locale: '项目管理',
|
|
||||||
icon: IconProjectManagement,
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
id: MENU_GROUP_IDS.PROPERTY_ID,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'project-list',
|
|
||||||
name: 'ProjectList',
|
|
||||||
meta: {
|
|
||||||
locale: '项目列表',
|
|
||||||
requiresAuth: true,
|
|
||||||
requireLogin: true,
|
|
||||||
roles: ['*'],
|
|
||||||
},
|
|
||||||
component: () => import('@/views/property-marketing/project-manage/project-list'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default COMPONENTS;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: RenXiaoDong
|
|
||||||
* @Date: 2025-06-19 01:45:53
|
|
||||||
*/
|
|
||||||
import type { RouteMeta, NavigationGuard, RouteComponent } from 'vue-router';
|
|
||||||
|
|
||||||
export interface AppRouteRecordRaw {
|
|
||||||
id?: number;
|
|
||||||
path: string;
|
|
||||||
name?: string | symbol;
|
|
||||||
meta?: RouteMeta;
|
|
||||||
redirect?: string;
|
|
||||||
component?: RouteComponent;
|
|
||||||
children?: AppRouteRecordRaw[];
|
|
||||||
alias?: string | string[];
|
|
||||||
props?: Record<string, any>;
|
|
||||||
beforeEnter?: NavigationGuard | NavigationGuard[];
|
|
||||||
fullPath?: string;
|
|
||||||
}
|
|
||||||
20
src/router copy/typeings.d.ts
vendored
20
src/router copy/typeings.d.ts
vendored
@ -1,20 +0,0 @@
|
|||||||
import { RouteComponent } from 'vue-router';
|
|
||||||
|
|
||||||
declare module 'vue-router' {
|
|
||||||
interface RouteMeta {
|
|
||||||
roles?: string[]; // Controls roles that have access to the page
|
|
||||||
requiresAuth?: boolean; // Whether login is required to access the current page (every route must declare)
|
|
||||||
icon?: RouteComponent | string; // The icon show in the side menu
|
|
||||||
locale?: string; // The locale name show in side menu and breadcrumb
|
|
||||||
needNavigate?: boolean; // if set true, the breadcrumb will support navigate
|
|
||||||
hideInMenu?: boolean; // If true, it is not displayed in the side menu
|
|
||||||
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
|
|
||||||
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
|
|
||||||
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
|
|
||||||
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
|
|
||||||
ignoreCache?: boolean; // if set true, the page will not be cached
|
|
||||||
hideSidebar?: boolean;
|
|
||||||
isAgentRoute?:boolean;
|
|
||||||
requireLogin?: boolean; // 是否需要登陆才能访问
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
// 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/material-center/components/finished-products/manuscript/list/index.vue'),
|
|
||||||
// // },
|
|
||||||
// {
|
|
||||||
// path: 'upload',
|
|
||||||
// name: 'ManuscriptUpload',
|
|
||||||
// meta: {
|
|
||||||
// locale: '稿件上传',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// requireLogin: true,
|
|
||||||
// hideFooter: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// hideInMenu: true,
|
|
||||||
// activeMenu: 'ManuscriptList',
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/material-center/components/finished-products/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/material-center/components/finished-products/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/material-center/components/finished-products/manuscript/detail/index.vue'),
|
|
||||||
// },
|
|
||||||
// // {
|
|
||||||
// // path: 'check-list',
|
|
||||||
// // name: 'ManuscriptCheckList',
|
|
||||||
// // meta: {
|
|
||||||
// // locale: '内容稿件审核',
|
|
||||||
// // requiresAuth: true,
|
|
||||||
// // requireLogin: true,
|
|
||||||
// // roles: ['*'],
|
|
||||||
// // },
|
|
||||||
// // component: () => import('@/views/material-center/components/finished-products/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/material-center/components/finished-products/manuscript/detail/index.vue'),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'check',
|
|
||||||
// name: 'ManuscriptCheck',
|
|
||||||
// meta: {
|
|
||||||
// locale: '稿件审核',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// requireLogin: true,
|
|
||||||
// hideFooter: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// hideInMenu: true,
|
|
||||||
// activeMenu: 'ManuscriptCheckList',
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/material-center/components/finished-products/manuscript/check/index.vue'),
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: '/explore/list/:shareCode',
|
|
||||||
// name: 'ExploreList',
|
|
||||||
// meta: {
|
|
||||||
// locale: '分享链接列表',
|
|
||||||
// requiresAuth: false,
|
|
||||||
// requireLogin: false,
|
|
||||||
// hideFooter: true,
|
|
||||||
// hideSidebar: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/creative-generation-workshop/explore/list/index.vue'),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: '/explore/detail/:shareCode/:id',
|
|
||||||
// name: 'ExploreDetail',
|
|
||||||
// meta: {
|
|
||||||
// locale: '分享链接详情',
|
|
||||||
// requiresAuth: false,
|
|
||||||
// requireLogin: false,
|
|
||||||
// hideFooter: true,
|
|
||||||
// hideSidebar: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// },
|
|
||||||
// component: () => import('@/views/creative-generation-workshop/explore/detail/index.vue'),
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// export default COMPONENTS;
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,320 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Image, Spin, Button, Input, Textarea, Affix } 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';
|
|
||||||
import icon4 from '@/assets/img/creative-generation-workshop/icon-avatar-default-v2.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 inspectionItems = computed(() => props.dataSource?.ai_review?.inspection_items ?? []);
|
|
||||||
|
|
||||||
const closeReplay = () => {
|
|
||||||
isReplay.value = false;
|
|
||||||
replayTarget.value = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReplay = (item) => {
|
|
||||||
isReplay.value = true;
|
|
||||||
replayTarget.value = item;
|
|
||||||
textAreaRef.value.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onComment = async () => {
|
|
||||||
const { code, data } = await postShareWorksComments(props.dataSource.id, route.params.shareCode, {
|
|
||||||
content: comment.value,
|
|
||||||
comment_id: replayTarget.value.id,
|
|
||||||
});
|
|
||||||
if (code === 200) {
|
|
||||||
emit('updateComment');
|
|
||||||
onClearComment();
|
|
||||||
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-8px 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 || icon4}
|
|
||||||
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 ">
|
|
||||||
<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 break-all">{item.content}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAiSuggest = () => {
|
|
||||||
if (isEmpty(aiReview.value)) return null;
|
|
||||||
const hasInspectionItems = inspectionItems.value.length > 0;
|
|
||||||
|
|
||||||
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>
|
|
||||||
{hasInspectionItems && (
|
|
||||||
<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'}`}>
|
|
||||||
{hasInspectionItems &&
|
|
||||||
inspectionItems.value.map((parentItem, parentIndex) => (
|
|
||||||
<div class="result-box p-16px rounded-8px mt-16px" key={parentIndex}>
|
|
||||||
<p class="cts bold !color-#000 !text-16px mb-16px">{parentItem.name}</p>
|
|
||||||
<div class="grid grid-cols-3 gap-x-24px gap-y-8px">
|
|
||||||
{parentItem.items.map((item, index) => (
|
|
||||||
<div class="audit-item" key={index}>
|
|
||||||
<div class="flex items-center h-20px">
|
|
||||||
{_iconMap.get(item.level)?.icon}
|
|
||||||
<TextOverTips context={item.name} class="cts ml-4px !color-#000" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCommentName = (item) => {
|
|
||||||
// 姓名脱敏:保留首尾字符,中间拼接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');
|
|
||||||
|
|
||||||
if (item.commenter_id === 0) {
|
|
||||||
return item.commenter?.name ? maskName(item.commenter?.name) : '佚名';
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskedName = maskName(item.commenter?.name);
|
|
||||||
const maskedMobile = maskMobile(item.commenter?.mobile);
|
|
||||||
return maskedName || maskedMobile;
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<section class="ai-suggest-wrap py-16px fixed z-3 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 relative z-2">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 main-box">
|
|
||||||
{/* AI审核结果 */}
|
|
||||||
{renderAiSuggest()}
|
|
||||||
{/* 评论与回复 */}
|
|
||||||
{renderCommentBox()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderTextareaBox()}
|
|
||||||
</div>
|
|
||||||
<DeleteCommentModal ref={deleteCommentModalRef} onDelete={deleteComment} />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
.ai-suggest-wrap {
|
|
||||||
top: $navbar-height;
|
|
||||||
height: calc(100% - ($navbar-height + 12px));
|
|
||||||
.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;
|
|
||||||
.arco-textarea-mirror,
|
|
||||||
.arco-textarea {
|
|
||||||
padding: 8px 16px !important;
|
|
||||||
}
|
|
||||||
&: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-medium;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.main-box {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,275 +0,0 @@
|
|||||||
<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/img/icon-logo.png';
|
|
||||||
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-contain" onEnded={onVideoEnded}></video>
|
|
||||||
{!isPlaying.value && (
|
|
||||||
<>
|
|
||||||
<img src={coverImageUrl.value} class="w-100% h-100% object-contain absolute z-0 top-0 left-0" />
|
|
||||||
<div v-show={!isPlaying.value} class="play-icon"></div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="main-img-box mb-16px relative overflow-hidden cursor-pointer">
|
|
||||||
<img src={coverImageUrl.value} class="w-100% h-100% object-contain" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (!videoRef.value) return;
|
|
||||||
|
|
||||||
if (isPlaying.value) {
|
|
||||||
videoRef.value.pause();
|
|
||||||
} else {
|
|
||||||
if (!isVideoLoaded.value) {
|
|
||||||
videoRef.value.src = videoUrl.value;
|
|
||||||
isVideoLoaded.value = true;
|
|
||||||
}
|
|
||||||
videoRef.value.play();
|
|
||||||
}
|
|
||||||
isPlaying.value = !isPlaying.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onVideoEnded = () => {
|
|
||||||
isPlaying.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const 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">
|
|
||||||
<img src={icon1} alt="" width="96" height="24"/>
|
|
||||||
</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 whitespace-pre-line">{dataSource.value.content}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 仅图片类型显示图片列表 */}
|
|
||||||
{!isVideo.value && (
|
|
||||||
<div class="desc-img-wrap mt-40px">
|
|
||||||
{images.value.map((item, index) => (
|
|
||||||
<div class="desc-img-box" key={index}>
|
|
||||||
<img src={item.url} class="w-100% h-100% object-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>
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
.explore-page {
|
|
||||||
position: relative;
|
|
||||||
min-width: $layout-min-width;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #fff;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
.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: fixed;
|
|
||||||
right: 16px;
|
|
||||||
top: calc($navbar-height + 32px);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.page-header {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
min-width: $layout-min-width;
|
|
||||||
.content {
|
|
||||||
height: $navbar-height;
|
|
||||||
// border-bottom: 1px solid var(--Border-1, #d7d7d9);
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: url('@/assets/img/icon-app-header-bg.png') center top no-repeat !important;
|
|
||||||
background-size: cover !important;
|
|
||||||
bottom: 0;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: -998;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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 {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background: #fff;
|
|
||||||
&.expand {
|
|
||||||
width: calc(100% - 456px);
|
|
||||||
}
|
|
||||||
.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% */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-video-box {
|
|
||||||
width: 320px;
|
|
||||||
height: 472px;
|
|
||||||
background: #333;
|
|
||||||
aspect-ratio: 3 / 4;
|
|
||||||
}
|
|
||||||
.main-img-box {
|
|
||||||
width: 320px;
|
|
||||||
height: auto;
|
|
||||||
max-height: 472px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.desc-img-wrap {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
.desc-img-box {
|
|
||||||
width: 212px;
|
|
||||||
height: 283px;
|
|
||||||
background: #fff;
|
|
||||||
object-fit: contain;
|
|
||||||
aspect-ratio: 3/4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.play-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 222;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play.png');
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
transition: background-image 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon:hover {
|
|
||||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play-hover.png');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
<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/img/icon-logo.png';
|
|
||||||
|
|
||||||
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">
|
|
||||||
<img src={icon2} alt="" width="96" height="24" />
|
|
||||||
</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">
|
|
||||||
<div class="mb-8px flex items-center w-fit">
|
|
||||||
<TextOverTips context={`${works.value[0]?.title?.slice(0,10)}...`} class="!w-fit mr-4px" />
|
|
||||||
<span class="cts color-#211F24">{`等${works.value.length}个文件`}</span>
|
|
||||||
</div>
|
|
||||||
{/* <TextOverTips context={`${works.value[0]?.title}等${works.value.length}个文件`} /> */}
|
|
||||||
<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>
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
.explore-page {
|
|
||||||
position: relative;
|
|
||||||
padding-top: $navbar-height;
|
|
||||||
min-width: $layout-min-width;
|
|
||||||
.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: $layout-min-width;
|
|
||||||
.content {
|
|
||||||
height: $navbar-height;
|
|
||||||
// border-bottom: 1px solid var(--Border-1, #d7d7d9);
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: url('@/assets/img/icon-app-header-bg.png') center top no-repeat !important;
|
|
||||||
background-size: cover !important;
|
|
||||||
bottom: 0;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: -998;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.page-wrapper {
|
|
||||||
min-height: calc(100vh - $navbar-height);
|
|
||||||
.explore-container {
|
|
||||||
width: $layout-min-width;
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
<!-- 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-writer/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>
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-table
|
|
||||||
ref="tableRef"
|
|
||||||
:data="dataSource"
|
|
||||||
row-key="id"
|
|
||||||
column-resizable
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ x: '100%' }"
|
|
||||||
class="flex-1 manuscript-table w-100%"
|
|
||||||
bordered
|
|
||||||
:row-selection="rowSelection"
|
|
||||||
:selected-row-keys="selectedRowKeys"
|
|
||||||
@sorter-change="handleSorterChange"
|
|
||||||
@select="(selectedKeys, rowKeyValue, record) => emits('select', selectedKeys, rowKeyValue, record)"
|
|
||||||
@select-all="(check) => emits('selectAll', check)"
|
|
||||||
>
|
|
||||||
<template #empty>
|
|
||||||
<NoData text="暂无稿件" />
|
|
||||||
</template>
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column
|
|
||||||
v-for="column in tableColumns"
|
|
||||||
:key="column.dataIndex"
|
|
||||||
:data-index="column.dataIndex"
|
|
||||||
:fixed="column.fixed"
|
|
||||||
:width="column.width"
|
|
||||||
:min-width="column.minWidth"
|
|
||||||
:sortable="column.sortable"
|
|
||||||
:align="column.align"
|
|
||||||
ellipsis
|
|
||||||
tooltip
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px">{{ column.title }}</span>
|
|
||||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
|
||||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="column.dataIndex === 'customer_opinion'" #cell="{ record }">
|
|
||||||
<p
|
|
||||||
class="h-28px px-8px flex items-center rounded-2px w-fit"
|
|
||||||
:style="{ background: getCustomerOpinionInfo(record.customer_opinion)?.bg }"
|
|
||||||
>
|
|
||||||
<span class="cts" :class="getCustomerOpinionInfo(record.customer_opinion)?.color">{{
|
|
||||||
getCustomerOpinionInfo(record.customer_opinion)?.label ?? '-'
|
|
||||||
}}</span>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'platform'" #cell="{ record }">
|
|
||||||
<template v-if="!PLATFORMS.find((item) => item.value === record.platform)"> - </template>
|
|
||||||
<img v-else width="24" height="24" class="rounded-4px" :src="PLATFORMS.find((item) => item.value === record.platform)?.icon" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'compliance_level'" #cell="{ record }">
|
|
||||||
<span class="cts num !color-#6D4CFE">{{
|
|
||||||
record.ai_review?.compliance_level ? `${record.ai_review?.compliance_level}%` : '-'
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'title'" #cell="{ record }">
|
|
||||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
|
||||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
#cell="{ record }"
|
|
||||||
v-else-if="
|
|
||||||
['created_at', 'last_modified_at', 'audit_started_at', 'audit_passed_at'].includes(column.dataIndex)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="cts num">{{ exactFormatTime(record[column.dataIndex]) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
|
||||||
<HoverImagePreview :src="record.cover">
|
|
||||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
|
||||||
<template #error>
|
|
||||||
<img :src="icon4" class="w-full h-full" />
|
|
||||||
</template>
|
|
||||||
</a-image>
|
|
||||||
</HoverImagePreview>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
|
||||||
<a-button type="outline" size="mini" @click="onCheck(record)" v-if="audit_status === AuditStatus.Pending"
|
|
||||||
>审核</a-button
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
size="mini"
|
|
||||||
@click="onScan(record)"
|
|
||||||
v-else-if="audit_status === AuditStatus.Auditing"
|
|
||||||
>查看</a-button
|
|
||||||
>
|
|
||||||
<a-button type="outline" size="mini" @click="onDetail(record)" v-else>详情</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else #cell="{ record }">
|
|
||||||
{{ formatTableField(column, record, true) }}
|
|
||||||
</template>
|
|
||||||
</a-table-column>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript-writer/list/constants';
|
|
||||||
import { patchWorkAuditsAuditWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import {
|
|
||||||
AuditStatus,
|
|
||||||
CUSTOMER_OPINION,
|
|
||||||
PLATFORMS,
|
|
||||||
} from '@/views/creative-generation-workshop/manuscript-writer/check-list/constants';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import HoverImagePreview from '@/components/hover-image-preview';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-delete.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
import icon4 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
const emits = defineEmits(['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>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,249 +0,0 @@
|
|||||||
export const TABLE_COLUMNS1 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '客户意见',
|
|
||||||
// dataIndex: 'customer_opinion',
|
|
||||||
// width: 120,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传时间',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export const TABLE_COLUMNS2 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '客户意见',
|
|
||||||
// dataIndex: 'customer_opinion',
|
|
||||||
// width: 120,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '审核平台',
|
|
||||||
dataIndex: 'platform',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '合规程度',
|
|
||||||
dataIndex: 'compliance_level',
|
|
||||||
suffix: '%',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核时间',
|
|
||||||
dataIndex: 'audit_started_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '修改人员',
|
|
||||||
// dataIndex: 'last_modifier',
|
|
||||||
// width: 180,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export const TABLE_COLUMNS3 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '客户意见',
|
|
||||||
// dataIndex: 'customer_opinion',
|
|
||||||
// width: 120,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '审核平台',
|
|
||||||
dataIndex: 'platform',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '通过时间',
|
|
||||||
dataIndex: 'audit_passed_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '修改人员',
|
|
||||||
// dataIndex: 'last_modifier',
|
|
||||||
// width: 180,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export 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/platform/icon-dy.png';
|
|
||||||
import icon2 from '@/assets/img/platform/icon-xhs.png';
|
|
||||||
|
|
||||||
export const PLATFORMS = [
|
|
||||||
{
|
|
||||||
label: '小红书',
|
|
||||||
value: 1,
|
|
||||||
icon: icon2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '抖音',
|
|
||||||
value: 2,
|
|
||||||
icon: icon1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CUSTOMER_OPINION = [
|
|
||||||
{
|
|
||||||
label: '待确认',
|
|
||||||
value: 0,
|
|
||||||
bg: '#F2F3F5',
|
|
||||||
color: 'color-#3C4043',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '已确认',
|
|
||||||
value: 1,
|
|
||||||
bg: '#F0EDFF',
|
|
||||||
color: '!color-#6D4CFE',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="manuscript-check-wrap">
|
|
||||||
<div class="filter-wrap bg-#fff rounded-8px 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 px-24px py-24px flex flex-col">
|
|
||||||
<div
|
|
||||||
class="flex justify-end mb-12px"
|
|
||||||
v-if="[AuditStatus.Pending, AuditStatus.Auditing].includes(query.audit_status)"
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
class="w-fit"
|
|
||||||
size="medium"
|
|
||||||
@click="handleBatchCheck"
|
|
||||||
v-if="query.audit_status === AuditStatus.Pending"
|
|
||||||
>批量审核</a-button
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
class="w-fit"
|
|
||||||
size="medium"
|
|
||||||
@click="handleBatchView"
|
|
||||||
v-if="query.audit_status === AuditStatus.Auditing"
|
|
||||||
>批量查看</a-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ManuscriptCheckTable
|
|
||||||
:key="query.audit_status"
|
|
||||||
:tableColumns="tableColumns"
|
|
||||||
:rowSelection="rowSelection"
|
|
||||||
:selectedRowKeys="selectedRowKeys"
|
|
||||||
:dataSource="dataSource"
|
|
||||||
:audit_status="query.audit_status"
|
|
||||||
@sorterChange="handleSorterChange"
|
|
||||||
@delete="handleDelete"
|
|
||||||
@edit="handleEdit"
|
|
||||||
@select="handleSelect"
|
|
||||||
@selectAll="handleSelectAll"
|
|
||||||
/>
|
|
||||||
<div v-if="pageInfo.total > 0" class="pagination-row">
|
|
||||||
<a-pagination
|
|
||||||
:total="pageInfo.total"
|
|
||||||
size="mini"
|
|
||||||
show-total
|
|
||||||
show-jumper
|
|
||||||
show-page-size
|
|
||||||
:current="pageInfo.page"
|
|
||||||
:page-size="pageInfo.page_size"
|
|
||||||
@change="onPageChange"
|
|
||||||
@page-size-change="onPageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeleteManuscriptModal ref="deleteManuscriptModalRef" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="jsx" setup>
|
|
||||||
import { 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,
|
|
||||||
} from '@/views/creative-generation-workshop/manuscript-writer/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>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal
|
|
||||||
v-model:visible="visible"
|
|
||||||
title="提示"
|
|
||||||
width="480px"
|
|
||||||
@close="onClose"
|
|
||||||
modal-class="upload-success11-modal"
|
|
||||||
:footer="null"
|
|
||||||
>
|
|
||||||
<div class="flex items-center flex-col justify-center">
|
|
||||||
<img :src="icon1" width="80" height="80" class="mb-16px" />
|
|
||||||
<span class="text-18px lh-26px font-400 color-#211F24 md mb-8px">内容稿件已通过审核</span>
|
|
||||||
<!-- <p class="text-14px lh-22px font-400 color-#737478 ld">想让内容更抓眼球、更吸流量吗?</p>
|
|
||||||
<p class="text-14px lh-22px font-400 color-#737478 ld">试试「内容稿件分析」功能吧!</p> -->
|
|
||||||
</div>
|
|
||||||
<!-- <template #footer>
|
|
||||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">内容稿件分析</a-button>
|
|
||||||
</template> -->
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onUnmounted } from 'vue';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-feedback-success.png';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const visible = ref(false);
|
|
||||||
const workIds = ref([]);
|
|
||||||
let autoCloseTimer = null;
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
if (autoCloseTimer) {
|
|
||||||
clearTimeout(autoCloseTimer);
|
|
||||||
autoCloseTimer = null;
|
|
||||||
}
|
|
||||||
if (workIds.value.length === 1) {
|
|
||||||
router.push({ path: `/writer/manuscript/check-list/${route.params.writerCode}` });
|
|
||||||
}
|
|
||||||
workIds.value = [];
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = (ids) => {
|
|
||||||
workIds.value = cloneDeep(ids);
|
|
||||||
visible.value = true;
|
|
||||||
if (autoCloseTimer) {
|
|
||||||
clearTimeout(autoCloseTimer);
|
|
||||||
autoCloseTimer = null;
|
|
||||||
}
|
|
||||||
autoCloseTimer = setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (autoCloseTimer) {
|
|
||||||
clearTimeout(autoCloseTimer);
|
|
||||||
autoCloseTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
<style lang="scss">
|
|
||||||
.upload-success11-modal {
|
|
||||||
.arco-modal-header {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.md {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
.ld {
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
}
|
|
||||||
.arco-modal-footer {
|
|
||||||
border-top: none;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Drawer, Image } from '@arco-design/web-vue';
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['cardClick'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const visible = ref(false);
|
|
||||||
const dataSource = ref([]);
|
|
||||||
const selectCardInfo = ref({});
|
|
||||||
|
|
||||||
const handleCardClick = (item) => {
|
|
||||||
emit('cardClick', item);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
const open = (data, _selectCardInfo) => {
|
|
||||||
dataSource.value = data;
|
|
||||||
selectCardInfo.value = _selectCardInfo;
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
const onClose = () => {
|
|
||||||
dataSource.value = [];
|
|
||||||
selectCardInfo.value = {};
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
expose({
|
|
||||||
open,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<Drawer
|
|
||||||
title="审核列表"
|
|
||||||
visible={visible.value}
|
|
||||||
width={420}
|
|
||||||
class="check-list-drawer-xt"
|
|
||||||
footer={false}
|
|
||||||
header={false}
|
|
||||||
onCancel={onClose}
|
|
||||||
>
|
|
||||||
<div class="flex justify-between items-center h-56px px-24px">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-3px h-16px rounded-2px bg-#6D4CFE mr-8px"></div>
|
|
||||||
<span class="mr-8px cts bold">批量审核列表</span>
|
|
||||||
<span class="mr-8px cts !lh-22px !text-14px">{`共${dataSource.value.length}个`}</span>
|
|
||||||
</div>
|
|
||||||
<icon-menu-unfold size={16} class="color-##55585F cursor-pointer hover:color-#6D4CFE" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-y-auto px-24px">
|
|
||||||
{dataSource.value.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => handleCardClick(item)}
|
|
||||||
class={`card-item flex rounded-8px bg-#F7F8FA p-8px ${
|
|
||||||
selectCardInfo.value.id === item.id ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
preview={false}
|
|
||||||
src={item.cover}
|
|
||||||
class="!rounded-4px mr-8px"
|
|
||||||
fit="cover"
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon1} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="flex-1 overflow-hidden flex flex-col items-start">
|
|
||||||
<TextOverTips context={item.title} class={`cts !color-#211F24 title mb-4px !text-14px`} />
|
|
||||||
<p class="cts !text-14px">{`合规程度:${
|
|
||||||
item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'
|
|
||||||
}`}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
.check-list-drawer-xt {
|
|
||||||
.arco-drawer-mask {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.arco-drawer {
|
|
||||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
|
|
||||||
.arco-drawer-body {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 0 24px;
|
|
||||||
.cts {
|
|
||||||
color: var(--Text-1, #939499);
|
|
||||||
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
&.bold {
|
|
||||||
color: var(--Text-1, #211f24);
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-item {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: all;
|
|
||||||
&:hover {
|
|
||||||
background-color: #e6e6e8;
|
|
||||||
}
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
border-color: #6d4cfe;
|
|
||||||
background-color: #f0edff;
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
font-family: $font-family-medium !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
export const escapeRegExp = (str: string) => {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FORM_RULES = {
|
|
||||||
title: [{ required: true, message: '请输入标题' }],
|
|
||||||
};
|
|
||||||
export const enumTab = {
|
|
||||||
TEXT: 0,
|
|
||||||
IMAGE: 1,
|
|
||||||
};
|
|
||||||
export const TAB_LIST = [
|
|
||||||
{
|
|
||||||
label: '文本',
|
|
||||||
value: enumTab.TEXT,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// label: '图片',
|
|
||||||
// value: enumTab.IMAGE,
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
export enum Enum_Level {
|
|
||||||
LOW = 0,
|
|
||||||
MEDIUM = 1,
|
|
||||||
HIGH = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LEVEL_MAP = new Map([
|
|
||||||
[Enum_Level.LOW, { label: '低风险', value: 'low_risk_number', color: '#6d4cfe' }],
|
|
||||||
[Enum_Level.MEDIUM, { label: '中风险', value: 'medium_risk_number', color: '#FFAE00' }],
|
|
||||||
[Enum_Level.HIGH, { label: '高风险', value: 'high_risk_number', color: '#F64B31' }],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const RESULT_LIST = [
|
|
||||||
{
|
|
||||||
label: '合规程度',
|
|
||||||
value: 'compliance_level',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.LOW)?.color,
|
|
||||||
suffix: '%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '检验项',
|
|
||||||
value: 'inspection_count',
|
|
||||||
color: '#211F24',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '高风险',
|
|
||||||
value: 'high_risk_number',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.HIGH)?.color,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中风险',
|
|
||||||
value: 'medium_risk_number',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.MEDIUM)?.color,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,233 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="highlight-textarea-container">
|
|
||||||
<a-textarea
|
|
||||||
ref="textareaWrapRef"
|
|
||||||
v-model="inputValue"
|
|
||||||
placeholder="请输入作品描述"
|
|
||||||
:disabled="disabled"
|
|
||||||
show-word-limit
|
|
||||||
:max-length="1000"
|
|
||||||
size="large"
|
|
||||||
class="textarea-input h-full w-full"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="() => (focus = true)"
|
|
||||||
@blur="() => (focus = false)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="textarea-highlight"
|
|
||||||
:class="{ focus: focus }"
|
|
||||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
|
||||||
v-html="highlightedHtml"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
|
||||||
import { escapeRegExp } from './constants';
|
|
||||||
|
|
||||||
// 定义Props类型
|
|
||||||
interface ViolationItem {
|
|
||||||
word: string;
|
|
||||||
risk_level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LevelMapItem {
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: string;
|
|
||||||
prohibitedWords?: ViolationItem[];
|
|
||||||
levelMap?: Map<number, LevelMapItem>;
|
|
||||||
disabled?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// 内部状态管理
|
|
||||||
const inputValue = ref(props.modelValue || '');
|
|
||||||
const focus = ref(false);
|
|
||||||
const textareaWrapRef = ref();
|
|
||||||
let nativeTextarea: HTMLTextAreaElement | null = null;
|
|
||||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
|
||||||
|
|
||||||
// 监听外部modelValue变化
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal !== inputValue.value) {
|
|
||||||
inputValue.value = (newVal || '').slice(0, 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理输入事件
|
|
||||||
const handleInput = (value: string) => {
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = (str: string): string => {
|
|
||||||
if (!isString(str)) return '';
|
|
||||||
return str
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateHighlightedHtml = (): string => {
|
|
||||||
if (!inputValue.value) return '';
|
|
||||||
|
|
||||||
// 获取违禁词列表并按长度倒序排序(避免短词匹配长词)
|
|
||||||
const words = (props.prohibitedWords || [])
|
|
||||||
.filter((item) => item.word && item.risk_level !== undefined)
|
|
||||||
.sort((a, b) => b.word.length - a.word.length);
|
|
||||||
|
|
||||||
if (words.length === 0) {
|
|
||||||
return escapeHtml(inputValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建匹配正则表达式
|
|
||||||
const pattern = new RegExp(`(${words.map((item) => escapeRegExp(item.word)).join('|')})`, 'gi');
|
|
||||||
|
|
||||||
// 替换匹配的违禁词为带样式的span标签
|
|
||||||
return inputValue.value.replace(pattern, (match) => {
|
|
||||||
// 找到对应的违禁词信息
|
|
||||||
const wordInfo = words.find((item) => item.word.toLowerCase() === match.toLowerCase());
|
|
||||||
|
|
||||||
if (!wordInfo) return match;
|
|
||||||
|
|
||||||
// 获取风险等级对应的样式
|
|
||||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
|
||||||
const color = levelStyle?.color || '#F64B31';
|
|
||||||
|
|
||||||
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nativeTextarea =
|
|
||||||
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
|
||||||
document.querySelector('.textarea-input .arco-textarea');
|
|
||||||
|
|
||||||
if (nativeTextarea) {
|
|
||||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
|
||||||
nativeTextarea.addEventListener('compositionstart', handleCompositionUpdate);
|
|
||||||
nativeTextarea.addEventListener('compositionupdate', handleCompositionUpdate);
|
|
||||||
nativeTextarea.addEventListener('compositionend', handleCompositionUpdate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (nativeTextarea) {
|
|
||||||
nativeTextarea.removeEventListener('scroll', handleTextareaScroll);
|
|
||||||
nativeTextarea.removeEventListener('compositionstart', handleCompositionUpdate);
|
|
||||||
nativeTextarea.removeEventListener('compositionupdate', handleCompositionUpdate);
|
|
||||||
nativeTextarea.removeEventListener('compositionend', handleCompositionUpdate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTextareaScroll = (e: Event) => {
|
|
||||||
const _scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
|
|
||||||
|
|
||||||
const highlightElement = document.querySelector('.textarea-highlight');
|
|
||||||
if (highlightElement) {
|
|
||||||
highlightElement.scrollTop = _scrollTop;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompositionUpdate = () => {
|
|
||||||
if (!nativeTextarea) return;
|
|
||||||
// 使用 rAF 等待浏览器把最新字符写入 textarea.value 再读取
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!nativeTextarea) return;
|
|
||||||
const latest = nativeTextarea.value.slice(0, 1000);
|
|
||||||
if (latest !== inputValue.value) {
|
|
||||||
inputValue.value = latest;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.highlight-textarea-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
@mixin textarea-padding {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin textarea-style {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: 400px;
|
|
||||||
border: 1px solid #d7d7d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
resize: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
z-index: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-highlight {
|
|
||||||
@include textarea-style;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
background: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
@include textarea-padding;
|
|
||||||
|
|
||||||
&.focus {
|
|
||||||
border-color: rgb(var(--primary-6)) !important;
|
|
||||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.arco-textarea-wrapper) {
|
|
||||||
@include textarea-style;
|
|
||||||
|
|
||||||
.arco-textarea {
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
color: transparent;
|
|
||||||
caret-color: #211f24 !important;
|
|
||||||
resize: none;
|
|
||||||
@include textarea-padding;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arco-textarea-word-limit {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arco-textarea-disabled {
|
|
||||||
.arco-textarea {
|
|
||||||
background: #f2f3f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
|
||||||
:deep(.textarea-input.composing .arco-textarea) {
|
|
||||||
color: #211f24 !important;
|
|
||||||
-webkit-text-fill-color: #211f24 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,475 +0,0 @@
|
|||||||
<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-writer/list/constants';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/creative-generation-workshop/icon-magic.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-line.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-success.png';
|
|
||||||
import icon4 from '@/assets/img/error-img.png';
|
|
||||||
import icon5 from '@/assets/img/creative-generation-workshop/icon-lf2.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
default: {},
|
|
||||||
},
|
|
||||||
selectedImageInfo: {
|
|
||||||
type: Object,
|
|
||||||
default: {},
|
|
||||||
},
|
|
||||||
checkLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
getDataLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'filesChange', 'selectImage', 'againCheck', 'startCheck'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const activeTab = ref(enumTab.TEXT);
|
|
||||||
const aiReplaceLoading = ref(false);
|
|
||||||
const formRef = ref(null);
|
|
||||||
const uploadRef = ref(null);
|
|
||||||
const modules = [Navigation];
|
|
||||||
|
|
||||||
const isTextTab = computed(() => activeTab.value === enumTab.TEXT);
|
|
||||||
const aiReview = computed(() => props.modelValue.ai_review);
|
|
||||||
const isDisabled = computed(() => props.checkLoading || aiReplaceLoading.value);
|
|
||||||
|
|
||||||
const onAiReplace = () => {
|
|
||||||
if (aiReplaceLoading.value) return;
|
|
||||||
|
|
||||||
aiReplaceLoading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
const content = props.modelValue.content;
|
|
||||||
const rules = aiReview.value?.violation_items ?? [];
|
|
||||||
const sortedRules = [...rules].sort((a, b) => b.word.length - a.word.length);
|
|
||||||
|
|
||||||
const replacedContent = sortedRules.reduce((result, rule) => {
|
|
||||||
if (!rule.word) return result;
|
|
||||||
|
|
||||||
const escapedWord = escapeRegExp(rule.word);
|
|
||||||
const regex = new RegExp(escapedWord, 'g');
|
|
||||||
|
|
||||||
return result.replace(regex, rule.replace_word);
|
|
||||||
}, content);
|
|
||||||
|
|
||||||
props.modelValue.content = replacedContent;
|
|
||||||
aiReplaceLoading.value = false;
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAgainCheck = () => {
|
|
||||||
if (!isTextTab.value && !props.modelValue.files?.length) {
|
|
||||||
AMessage.warning('请先上传需审核图片');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit('againCheck');
|
|
||||||
};
|
|
||||||
const onReplaceImage = () => {
|
|
||||||
uploadRef.value?.upload?.();
|
|
||||||
};
|
|
||||||
const handleTabClick = (key) => {
|
|
||||||
activeTab.value = key;
|
|
||||||
};
|
|
||||||
const validate = () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
formRef.value?.validate((errors) => {
|
|
||||||
if (errors) {
|
|
||||||
reject();
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const reset = () => {
|
|
||||||
formRef.value?.resetFields?.();
|
|
||||||
formRef.value?.clearValidate?.();
|
|
||||||
aiReplaceLoading.value = false;
|
|
||||||
};
|
|
||||||
const getFileExtension = (filename) => {
|
|
||||||
const match = filename.match(/\.([^.]+)$/);
|
|
||||||
return match ? match[1].toLowerCase() : '';
|
|
||||||
};
|
|
||||||
const handleSelectImage = (item) => {
|
|
||||||
emit('selectImage', item);
|
|
||||||
};
|
|
||||||
const onDeleteImage = (e, item, index) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const _newFiles = cloneDeep(props.modelValue.files);
|
|
||||||
_newFiles.splice(index, 1);
|
|
||||||
|
|
||||||
if (item.id === props.selectedImageInfo.id) {
|
|
||||||
emit('selectImage', _newFiles.length ? _newFiles[0] : {});
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('filesChange', _newFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderUpload = (UploadBtn, action = 'upload') => {
|
|
||||||
return (
|
|
||||||
<Upload
|
|
||||||
ref={uploadRef}
|
|
||||||
action="/"
|
|
||||||
draggable
|
|
||||||
class="w-fit"
|
|
||||||
custom-request={(option) => uploadImage(option, action)}
|
|
||||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
|
||||||
show-file-list={false}
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
'upload-button': () => <UploadBtn />,
|
|
||||||
}}
|
|
||||||
</Upload>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadImage = async (option, action = 'upload') => {
|
|
||||||
const {
|
|
||||||
fileItem: { file },
|
|
||||||
} = option;
|
|
||||||
|
|
||||||
const { name, size, type } = file;
|
|
||||||
const response = await getImagePreSignedUrl({ suffix: getFileExtension(name) });
|
|
||||||
const { file_name, upload_url, file_url } = response?.data;
|
|
||||||
|
|
||||||
const blob = new Blob([file], { type });
|
|
||||||
await axios.put(upload_url, blob, {
|
|
||||||
headers: { 'Content-Type': type },
|
|
||||||
});
|
|
||||||
|
|
||||||
const _file = {
|
|
||||||
url: file_url,
|
|
||||||
name: file_name,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _newFiles =
|
|
||||||
action === 'replaceImage'
|
|
||||||
? props.modelValue.files.map((item) => {
|
|
||||||
if (item.url === props.selectedImageInfo.url) {
|
|
||||||
return _file;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
: [...props.modelValue.files, _file];
|
|
||||||
|
|
||||||
emit('filesChange', _newFiles);
|
|
||||||
emit('selectImage', _file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooterRow = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button class="mr-12px" size="medium" onClick={onAgainCheck} disabled={isDisabled.value}>
|
|
||||||
再次审核
|
|
||||||
</Button>
|
|
||||||
{isTextTab.value ? (
|
|
||||||
<Button size="medium" type="outline" class="w-123px check-btn" onClick={onAiReplace} disabled={isDisabled.value}>
|
|
||||||
{aiReplaceLoading.value ? (
|
|
||||||
<>
|
|
||||||
<IconLoading size={14} />
|
|
||||||
<span class="ml-8px check-text">AI生成中</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<img src={icon1} width={14} height={14} />
|
|
||||||
<span class="ml-8px check-text">替换违禁词</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div class="w-88px">
|
|
||||||
{renderUpload(
|
|
||||||
<Button size="medium" type="outline">
|
|
||||||
图片替换
|
|
||||||
</Button>,
|
|
||||||
'replaceImage',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderTextForm = () => {
|
|
||||||
return (
|
|
||||||
<Form ref={formRef} model={props.modelValue} rules={FORM_RULES} layout="vertical" auto-label-width>
|
|
||||||
<FormItem label="标题" field="title" required>
|
|
||||||
<Input
|
|
||||||
v-model={props.modelValue.title}
|
|
||||||
placeholder="请输入标题"
|
|
||||||
size="large"
|
|
||||||
maxLength={30}
|
|
||||||
show-word-limit
|
|
||||||
disabled={isDisabled.value}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="作品描述" field="content" class="flex-1 content-form-item">
|
|
||||||
<Textarea
|
|
||||||
v-model={props.modelValue.content}
|
|
||||||
placeholder="请输入作品描述"
|
|
||||||
size="large"
|
|
||||||
show-word-limit
|
|
||||||
maxLength={1000}
|
|
||||||
disabled={isDisabled.value}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderImageForm = () => {
|
|
||||||
if (props.modelValue.files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div class="w-full h-full py-16px flex justify-center">
|
|
||||||
<div class="w-380px flex flex-col justify-center">
|
|
||||||
<Image
|
|
||||||
src={props.selectedImageInfo.url}
|
|
||||||
width={370}
|
|
||||||
height={370}
|
|
||||||
preview={false}
|
|
||||||
class="flex items-center justify-center mb-8px"
|
|
||||||
fit="contain"
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon4} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="swiper-wrap h-78px">
|
|
||||||
<Swiper
|
|
||||||
spaceBetween={16}
|
|
||||||
modules={modules}
|
|
||||||
slidesPerView="auto"
|
|
||||||
navigation={{
|
|
||||||
nextEl: '.swiper-button-next',
|
|
||||||
prevEl: '.swiper-button-prev',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.modelValue.files.map((item, index) => (
|
|
||||||
<SwiperSlide
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => handleSelectImage(item)}
|
|
||||||
class={`!h-48px !w-48px !relative bg-#F7F8FA cursor-pointer !flex items-center ${
|
|
||||||
item.id === props.selectedImageInfo.id ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div class="group relative w-full h-full rounded-5px">
|
|
||||||
<Image
|
|
||||||
width={'100%'}
|
|
||||||
height={'100%'}
|
|
||||||
src={item.url}
|
|
||||||
class="!rounded-4px"
|
|
||||||
fit="contain"
|
|
||||||
preview={false}
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon4} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<icon-close-circle-fill
|
|
||||||
size={16}
|
|
||||||
class="close-icon absolute top--8px right--8px hidden cursor-pointer color-#737478 hover:!color-#211F24 z-50"
|
|
||||||
onClick={(e) => onDeleteImage(e, item, index)}
|
|
||||||
/>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
<div class="swiper-box swiper-button-prev">
|
|
||||||
<img src={icon5} class="w-8px h-17px" />
|
|
||||||
</div>
|
|
||||||
<div class="swiper-box swiper-button-next">
|
|
||||||
<img src={icon5} class="w-8px h-17px rotate-180" />
|
|
||||||
</div>
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
|
||||||
<div class="flex justify-center mb-16px">
|
|
||||||
{renderUpload(
|
|
||||||
<div class="upload-box">
|
|
||||||
<icon-plus size="14" class="mb-16px color-#3C4043" />
|
|
||||||
<span class="cts !color-#211F24">上传图片</span>
|
|
||||||
</div>,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="cts">上传要审核的图片素材</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const renderCheckSuccessBox = () => {
|
|
||||||
if (!aiReview.value?.violation_items?.length) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center mb-16px">
|
|
||||||
{RESULT_LIST.map((item, index) => (
|
|
||||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
|
||||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
|
||||||
item.suffix || ''
|
|
||||||
}`}</span>
|
|
||||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
|
||||||
<div class="mb-24px relative w-fit">
|
|
||||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
|
||||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center h-138px justify-center">
|
|
||||||
<img src={icon3} width={72} height={72} class="mb-12px" />
|
|
||||||
<span class="cts !color-#25C883">
|
|
||||||
{isTextTab.value ? '恭喜,您的文案中没有检测出违禁词' : '恭喜,您的图片中没有检测出违禁内容'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="flex items-center mb-16px">
|
|
||||||
{RESULT_LIST.map((item, index) => (
|
|
||||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
|
||||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
|
||||||
item.suffix || ''
|
|
||||||
}`}</span>
|
|
||||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
|
||||||
<div class=" mb-8px relative w-fit">
|
|
||||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
|
||||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
|
||||||
</div>
|
|
||||||
{aiReview.value?.suggestion?.map((item, index) => (
|
|
||||||
<p class="cts !color-#55585F !text-12px !lh-20px" key={index}>{`${index + 1}. ${item}`}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="forbid-word-box flex-1 flex">
|
|
||||||
<div class="left mr-32px w-56px">
|
|
||||||
<p class="mb-12px cts !text-12px">违禁词</p>
|
|
||||||
{aiReview.value?.violation_items?.map((item, index) => (
|
|
||||||
<TextOverTips
|
|
||||||
context={item.word}
|
|
||||||
class="mb-12px cts"
|
|
||||||
style={{ color: LEVEL_MAP.get(item.risk_level)?.color }}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="right flex-1 overflow-hidden">
|
|
||||||
<p class="mb-12px cts !text-12px">解释</p>
|
|
||||||
{aiReview.value?.violation_items?.map((item, index) => (
|
|
||||||
<TextOverTips context={item.reason} class="mb-12px" key={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRightBox = () => {
|
|
||||||
if (props.checkLoading) {
|
|
||||||
return (
|
|
||||||
<div class="right-box h-210px">
|
|
||||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
|
||||||
<Spin
|
|
||||||
loading={true}
|
|
||||||
tip={`${isTextTab.value ? '文本' : '图片'}检测中`}
|
|
||||||
size={72}
|
|
||||||
class="h-298px !flex flex-col justify-center items-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="right-box">
|
|
||||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
|
||||||
{props.getDataLoading ? (
|
|
||||||
<Spin loading={true} size={72} class="h-298px !flex justify-center items-center" />
|
|
||||||
) : (
|
|
||||||
renderCheckSuccessBox()
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expose({
|
|
||||||
validate,
|
|
||||||
reset,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<div class="h-full w-full px-24px pt-16px pb-24px content-wrap flex">
|
|
||||||
<div class="flex-2 left-box mr-24px flex flex-col">
|
|
||||||
<div class="flex-1 mb-12px rounded-8px border-1px pt-8px flex flex-col pb-16px bg-#F7F8FA border-#E6E6E8 border-solid">
|
|
||||||
<Tabs v-model={activeTab.value} onTabClick={handleTabClick} class="mb-16px">
|
|
||||||
{TAB_LIST.map((item) => (
|
|
||||||
<TabPane
|
|
||||||
key={item.value}
|
|
||||||
v-slots={{
|
|
||||||
title: () => (
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<span>{item.label}</span>
|
|
||||||
{
|
|
||||||
// activeTab.value === item.value && aiReview.value?.violation_items.length > 0 && (
|
|
||||||
// <icon-exclamation-circle-fill size={14} class="color-#F64B31 absolute right--10px top-0" />
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
<div class="flex-1 px-16px">{isTextTab.value ? renderTextForm() : renderImageForm()}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-end">{renderFooterRow()}</div>
|
|
||||||
</div>
|
|
||||||
{renderRightBox()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
.content-wrap {
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.check-btn {
|
|
||||||
.check-text {
|
|
||||||
background: linear-gradient(84deg, #266cff 4.57%, #a15af0 84.93%);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.left-box {
|
|
||||||
:deep(.arco-tabs) {
|
|
||||||
.arco-tabs-nav {
|
|
||||||
.arco-tabs-tab {
|
|
||||||
height: 40px;
|
|
||||||
// padding: 0 8px;
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-tabs-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.arco-form) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.arco-form-item {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
.arco-form-item-label-col {
|
|
||||||
.arco-form-item-label {
|
|
||||||
color: #939499;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content-form-item {
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.arco-form-item-wrapper-col {
|
|
||||||
flex: 1;
|
|
||||||
.arco-form-item-content-wrapper,
|
|
||||||
.arco-form-item-content,
|
|
||||||
.arco-textarea-wrapper {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.upload-box {
|
|
||||||
display: flex;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
|
||||||
background: var(--BG-200, #f2f3f5);
|
|
||||||
&:hover {
|
|
||||||
background: var(--Primary-1, #e6e6e8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.swiper-wrap {
|
|
||||||
:deep(.swiper) {
|
|
||||||
height: 100%;
|
|
||||||
.swiper-wrapper {
|
|
||||||
align-items: center;
|
|
||||||
.swiper-slide {
|
|
||||||
transition: all;
|
|
||||||
&.active {
|
|
||||||
width: 60px !important;
|
|
||||||
height: 60px !important;
|
|
||||||
.group {
|
|
||||||
border: 2px solid var(--Brand-6, #6d4cfe);
|
|
||||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.close-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-box {
|
|
||||||
position: absolute;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
transition: all;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
&.swiper-button-prev {
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
&.swiper-button-next {
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.swiper-button-disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.right-box {
|
|
||||||
border: 1px solid #E6E6E8;
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 8px; padding: 16px; display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 100%;
|
|
||||||
.s1 {
|
|
||||||
font-family: $font-family-manrope-medium;
|
|
||||||
font-size: 24px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 32px; /* 133.333% */
|
|
||||||
}
|
|
||||||
.result-item {
|
|
||||||
&:first-child {
|
|
||||||
position: relative;
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
right: 0;
|
|
||||||
width: 1px;
|
|
||||||
height: 32px;
|
|
||||||
background: var(--Border-1, #d7d7d9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.suggestion-box {
|
|
||||||
.ai-text {
|
|
||||||
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
color: #211f24;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
.forbid-word-box {
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
&.level0 {
|
|
||||||
color: #6d4cfe;
|
|
||||||
}
|
|
||||||
&.level2 {
|
|
||||||
color: #f64b31;
|
|
||||||
}
|
|
||||||
&.level1 {
|
|
||||||
color: #ffae00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
<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-writer/check-list/constants';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/error-img.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-lf.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
dataSource: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
selectCardInfo: {
|
|
||||||
type: Object,
|
|
||||||
default: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['cardClick', 'platformChange'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const modules = [Navigation];
|
|
||||||
const handleCardClick = (item) => {
|
|
||||||
// emit('update:modelValue', item);
|
|
||||||
emit('cardClick', item);
|
|
||||||
};
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<header class="header-wrap">
|
|
||||||
{props.dataSource.length > 1 && (
|
|
||||||
<div class="swiper-wrap pt-16px h-80px px-24px">
|
|
||||||
<Swiper
|
|
||||||
spaceBetween={16}
|
|
||||||
modules={modules}
|
|
||||||
slidesPerView="auto"
|
|
||||||
navigation={{
|
|
||||||
nextEl: '.swiper-button-next',
|
|
||||||
prevEl: '.swiper-button-prev',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.dataSource.map((item) => (
|
|
||||||
<SwiperSlide
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => handleCardClick(item)}
|
|
||||||
class={`swiper-item !h-64px !w-280px bg-#F7F8FA border-1px cursor-pointer border-solid border-transparent rounded-8px p-8px overflow-hidden !flex items-center ${
|
|
||||||
item.id === props.selectCardInfo.id ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
preview={false}
|
|
||||||
src={item.cover}
|
|
||||||
class="!rounded-4px mr-8px"
|
|
||||||
fit="cover"
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon1} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="flex-1 overflow-hidden flex flex-col items-start">
|
|
||||||
<TextOverTips context={item.title} class={`cts !color-#211F24 title mb-4px`} />
|
|
||||||
<p class="cts">{`合规程度:${item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'}`}</p>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div class="swiper-box swiper-button-prev">
|
|
||||||
<div class="swiper-button">
|
|
||||||
<img src={icon2} class="w-16px h-16px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-box swiper-button-next">
|
|
||||||
<div class="swiper-button">
|
|
||||||
<img src={icon2} class="w-16px h-16px rotate-180" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class="platform-row py-16px flex items-center px-24px">
|
|
||||||
<span class="mr-16px cts !color-#211F24">审核平台选择</span>
|
|
||||||
<div class="flex items-center">
|
|
||||||
{PLATFORMS.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
onClick={() => {
|
|
||||||
emit('platformChange', item.value);
|
|
||||||
}}
|
|
||||||
class={`w-100px flex items-center mr-16px py-8px px-12px flex border-1px border-solid border-transparent transition-all
|
|
||||||
items-center rounded-8px cursor-pointer bg-#F2F3F5 hover:bg-#E6E6E8 ${
|
|
||||||
props.selectCardInfo.platform === item.value ? '!bg-#F0EDFF !border-#6D4CFE' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img src={item.icon} alt="" width={20} height={20} class="mr-4px" />
|
|
||||||
<span class={`cts !color-#211F24 ${props.selectCardInfo.platform === item.value ? 'bold' : ''}`}>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,264 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Button, Message as AMessage, Spin } from '@arco-design/web-vue';
|
|
||||||
import CancelCheckModal from './cancel-check-modal.vue';
|
|
||||||
import CheckSuccessModal from './check-success-modal.vue';
|
|
||||||
import HeaderCard from './components/header-card';
|
|
||||||
import ContentCard from './components/content-card';
|
|
||||||
import CheckListDrawer from './components/check-list-drawer';
|
|
||||||
|
|
||||||
import { slsWithCatch, rlsWithCatch, glsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import useGetAiReviewResult from '@/hooks/useGetAiReviewResult.ts';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
import {
|
|
||||||
getWorkAuditsBatchDetailWriter,
|
|
||||||
putWorkAuditsUpdateWriter,
|
|
||||||
postWorkAuditsAiReviewWriter,
|
|
||||||
getWorkAuditsAiReviewResultWriter,
|
|
||||||
putWorkAuditsAuditPassWriter,
|
|
||||||
} from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
|
|
||||||
const workIds = ref([]);
|
|
||||||
const isSaved = ref(false);
|
|
||||||
const dataSource = ref([]);
|
|
||||||
const remoteDataSource = ref([]);
|
|
||||||
const cancelCheckModalRef = ref(null);
|
|
||||||
const checkSuccessModalRef = ref(null);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const contentCardRef = ref(null);
|
|
||||||
const checkListDrawerRef = ref(null);
|
|
||||||
const getDataLoading = ref(false);
|
|
||||||
|
|
||||||
const selectCardInfo = ref({});
|
|
||||||
const selectedImageInfo = ref(null);
|
|
||||||
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleStartCheck, handleAgainCheck, ticket, checkLoading, resetAiReviewInfo } = useGetAiReviewResult({
|
|
||||||
cardInfo: selectCardInfo,
|
|
||||||
startAiReviewFn: postWorkAuditsAiReviewWriter,
|
|
||||||
getAiReviewResultFn: getWorkAuditsAiReviewResultWriter,
|
|
||||||
updateAiReview(ai_review) {
|
|
||||||
selectCardInfo.value.ai_review = ai_review;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ path: `/writer/manuscript/check-list/${writerCode.value}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeCard = (item) => {
|
|
||||||
contentCardRef.value.reset();
|
|
||||||
isSaved.value = false;
|
|
||||||
submitLoading.value = false;
|
|
||||||
getDataLoading.value = false;
|
|
||||||
checkLoading.value = false;
|
|
||||||
resetAiReviewInfo();
|
|
||||||
|
|
||||||
const { files = [], ai_review } = item;
|
|
||||||
selectCardInfo.value = cloneDeep(item);
|
|
||||||
selectedImageInfo.value = cloneDeep(files?.[0] ?? {});
|
|
||||||
|
|
||||||
if (isEmpty(ai_review)) {
|
|
||||||
handleStartCheck();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCardClick = async (item) => {
|
|
||||||
const isModified = await isSelectCardModified();
|
|
||||||
if (isModified) {
|
|
||||||
cancelCheckModalRef.value?.open('toggle', item);
|
|
||||||
} else {
|
|
||||||
onChangeCard(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectImage = (item) => {
|
|
||||||
selectedImageInfo.value = cloneDeep(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWorkAudits = async () => {
|
|
||||||
try {
|
|
||||||
getDataLoading.value = true;
|
|
||||||
const { code, data } = await getWorkAuditsBatchDetailWriter(writerCode.value, { ids: workIds.value });
|
|
||||||
if (code === 200) {
|
|
||||||
const _data = (data ?? []).map((item) => ({
|
|
||||||
...item,
|
|
||||||
platform: item.platform === 0 ? 1 : item.platform,
|
|
||||||
}));
|
|
||||||
|
|
||||||
dataSource.value = _data;
|
|
||||||
remoteDataSource.value = cloneDeep(_data);
|
|
||||||
|
|
||||||
const _firstCard = _data?.[0] ?? {};
|
|
||||||
const { id, ai_review } = _firstCard;
|
|
||||||
|
|
||||||
selectCardInfo.value = cloneDeep(_firstCard);
|
|
||||||
selectedImageInfo.value = cloneDeep(_firstCard.files?.[0] ?? {});
|
|
||||||
|
|
||||||
if (isEmpty(ai_review)) {
|
|
||||||
handleStartCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
getDataLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectCardModified = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const _item = remoteDataSource.value.find((item) => item.id === selectCardInfo.value.id);
|
|
||||||
resolve(!isEqual(selectCardInfo.value, _item) && !isSaved.value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onPlatformChange = (platform) => {
|
|
||||||
selectCardInfo.value.platform = platform;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExit = async () => {
|
|
||||||
const isModified = await isSelectCardModified();
|
|
||||||
if (isModified) {
|
|
||||||
cancelCheckModalRef.value?.open();
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onSave = async () => {
|
|
||||||
if (!selectCardInfo.value.title) {
|
|
||||||
AMessage.warning('标题不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
contentCardRef.value?.validate().then(async () => {
|
|
||||||
const { code, data } = await putWorkAuditsUpdateWriter(writerCode.value, selectCardInfo.value);
|
|
||||||
if (code === 200) {
|
|
||||||
isSaved.value = true;
|
|
||||||
AMessage.success('当前内容稿件已保存');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onCheckSuccess = () => {
|
|
||||||
checkSuccessModalRef.value?.open(workIds.value);
|
|
||||||
|
|
||||||
if (workIds.value.length > 1) {
|
|
||||||
const _id = selectCardInfo.value.id;
|
|
||||||
workIds.value = workIds.value.filter((v) => v != _id);
|
|
||||||
dataSource.value = dataSource.value.filter((v) => v.id != _id);
|
|
||||||
|
|
||||||
slsWithCatch('writerManuscriptCheckIds', workIds.value.join(','));
|
|
||||||
onChangeCard(dataSource.value.length ? dataSource.value[0] : {});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onSubmit = async () => {
|
|
||||||
contentCardRef.value?.validate().then(async () => {
|
|
||||||
try {
|
|
||||||
submitLoading.value = true;
|
|
||||||
const { code, data } = await putWorkAuditsAuditPassWriter(writerCode.value, selectCardInfo.value);
|
|
||||||
if (code === 200) {
|
|
||||||
onCheckSuccess();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onFilesChange = (files) => {
|
|
||||||
selectCardInfo.value.files = cloneDeep(files);
|
|
||||||
};
|
|
||||||
const onAgainCheck = () => {
|
|
||||||
handleAgainCheck();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooterRow = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button size="medium" type="outline" class="mr-12px" onClick={onExit}>
|
|
||||||
退出
|
|
||||||
</Button>
|
|
||||||
<Button size="medium" type="outline" class="mr-12px" onClick={onSave}>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={onSubmit} loading={submitLoading.value}>
|
|
||||||
{submitLoading.value ? '通过审核中...' : '通过审核'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
workIds.value = glsWithCatch('writerManuscriptCheckIds')?.split(',') ?? [];
|
|
||||||
getWorkAudits();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
rlsWithCatch('writerManuscriptCheckIds');
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<div class="manuscript-check-wrap flex flex-col">
|
|
||||||
<div class="flex items-center mb-10px">
|
|
||||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onExit}>
|
|
||||||
内容稿件审核
|
|
||||||
</span>
|
|
||||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
|
||||||
<span class="cts bold !color-#1D2129">{`${workIds.value.length > 0 ? '批量' : ''}审核内容稿件`}</span>
|
|
||||||
</div>
|
|
||||||
{dataSource.value.length > 1 && (
|
|
||||||
<div
|
|
||||||
class="check-list-icon"
|
|
||||||
onClick={() => checkListDrawerRef.value.open(dataSource.value, selectCardInfo.value)}
|
|
||||||
>
|
|
||||||
<icon-menu-fold size={16} class="color-#55585F mr-4px icon" />
|
|
||||||
<span class="cts !color-#211F24">审核列表</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden bg-#fff rounded-8px ">
|
|
||||||
<HeaderCard
|
|
||||||
dataSource={dataSource.value}
|
|
||||||
selectCardInfo={selectCardInfo.value}
|
|
||||||
onCardClick={onCardClick}
|
|
||||||
onPlatformChange={onPlatformChange}
|
|
||||||
/>
|
|
||||||
<section class="flex-1 overflow-hidden">
|
|
||||||
<ContentCard
|
|
||||||
ref={contentCardRef}
|
|
||||||
v-model={selectCardInfo.value}
|
|
||||||
selectCardInfo={selectCardInfo.value}
|
|
||||||
onFilesChange={onFilesChange}
|
|
||||||
selectedImageInfo={selectedImageInfo.value}
|
|
||||||
onSelectImage={onSelectImage}
|
|
||||||
checkLoading={checkLoading.value}
|
|
||||||
getDataLoading={getDataLoading.value}
|
|
||||||
onAgainCheck={onAgainCheck}
|
|
||||||
onStartCheck={handleStartCheck}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderFooterRow()}
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<CancelCheckModal ref={cancelCheckModalRef} onSelectCard={onChangeCard} />
|
|
||||||
<CheckSuccessModal ref={checkSuccessModalRef} />
|
|
||||||
<CheckListDrawer ref={checkListDrawerRef} onCardClick={onCardClick} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-check-wrap {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.check-list-icon {
|
|
||||||
// width: 92px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
padding: 8px 12px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 30px 0 0 30px;
|
|
||||||
border: 1px solid var(--Border-1, #d7d7d9);
|
|
||||||
background: #fff;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: calc($navbar-height + 8px);
|
|
||||||
&:hover {
|
|
||||||
.icon,
|
|
||||||
.cts {
|
|
||||||
color: #6d4cfe !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
<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-writer/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>
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
<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-writer/check-list/constants';
|
|
||||||
import { getWorksDetailWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript-writer/list/constants.ts';
|
|
||||||
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
|
|
||||||
const DEFAULT_SOURCE_INFO = {
|
|
||||||
title: '内容稿件列表',
|
|
||||||
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 sidebarStore = useSidebarStore();
|
|
||||||
const workId = ref(route.params.id);
|
|
||||||
const { source, audit_status } = route.query;
|
|
||||||
|
|
||||||
// 视频播放相关状态
|
|
||||||
const isPlaying = ref(false);
|
|
||||||
const videoRef = ref(null);
|
|
||||||
const videoUrl = ref('');
|
|
||||||
const coverImageUrl = ref('');
|
|
||||||
const isVideoLoaded = ref(false);
|
|
||||||
const dataSource = ref({});
|
|
||||||
const images = ref([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const isVideo = computed(() => dataSource.value.type === EnumManuscriptType.Video);
|
|
||||||
const sourceInfo = computed(() => SOURCE_MAP.get(source) ?? DEFAULT_SOURCE_INFO);
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ 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" />
|
|
||||||
</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 bg-#fff rounded-8px py-32px">
|
|
||||||
<div class="w-684px mx-auto flex flex-col items-center">
|
|
||||||
<div class="flex justify-start flex-col w-full">
|
|
||||||
<p class="mb-8px cts bold !text-28px !lh-40px !color-#211F24">{dataSource.value.title}</p>
|
|
||||||
<p class="cts !text-12px !color-#737478 mb-32px w-full text-left">
|
|
||||||
{exactFormatTime(dataSource.value.update_time)}修改
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderMainImg()}
|
|
||||||
<div class="w-full">
|
|
||||||
<p class="cts !color-#211F24 whitespace-pre-line">{dataSource.value.content}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 仅图片类型显示图片列表 */}
|
|
||||||
{!isVideo.value && (
|
|
||||||
<div class="desc-img-wrap mt-40px">
|
|
||||||
{images.value.map((item, index) => (
|
|
||||||
<div class="desc-img-box" key={index}>
|
|
||||||
<img src={item.url} class="w-100% h-100% object-contain" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderFooterRow()}
|
|
||||||
</footer>
|
|
||||||
</Spin>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-detail-wrap {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.main-video-box {
|
|
||||||
width: 320px;
|
|
||||||
height: 472px;
|
|
||||||
background: #333;
|
|
||||||
aspect-ratio: 3 / 4;
|
|
||||||
}
|
|
||||||
.main-img-box {
|
|
||||||
width: 320px;
|
|
||||||
height: auto;
|
|
||||||
max-height: 472px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.desc-img-wrap {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
.desc-img-box {
|
|
||||||
width: 212px;
|
|
||||||
height: 283px;
|
|
||||||
background: #fff;
|
|
||||||
object-fit: contain;
|
|
||||||
aspect-ratio: 3/4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.play-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 222;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play.png');
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
transition: background-image 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon:hover {
|
|
||||||
background-image: url('@/assets/img/creative-generation-workshop/icon-play-hover.png');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:visible="visible" title="退出编辑" width="480px" @close="onClose">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
|
||||||
<span>内容已修改尚未保存,若退出编辑,本次修改将不保存。</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
|
||||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">确认退出</a-button>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
const onConfirm = () => {
|
|
||||||
onClose();
|
|
||||||
router.push({ path: `/writer/manuscript/list/${route.params.writerCode}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
|
||||||
import EditForm, { ENUM_UPLOAD_STATUS, INITIAL_VIDEO_INFO } from '../components/edit-form';
|
|
||||||
import CancelEditModal from './cancel-edit-modal.vue';
|
|
||||||
|
|
||||||
import { getWorksDetailWriter, putWorksUpdateWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript-writer/list/constants.ts';
|
|
||||||
import { formatDuration, formatFileSize, convertVideoUrlToCoverUrl } from '@/utils/tools';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditForm,
|
|
||||||
},
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
|
||||||
const cancelEditModal = ref(null);
|
|
||||||
const dataSource = ref({});
|
|
||||||
const remoteDataSource = ref({});
|
|
||||||
const uploadLoading = ref(false);
|
|
||||||
const isSaved = ref(false);
|
|
||||||
|
|
||||||
const workId = ref(route.params.id);
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
const isModified = !isEqual(dataSource.value, remoteDataSource.value);
|
|
||||||
if (isModified && !isSaved.value) {
|
|
||||||
cancelEditModal.value?.open();
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSave = async (check = false) => {
|
|
||||||
formRef.value?.validate().then(async () => {
|
|
||||||
if (dataSource.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING) {
|
|
||||||
AMessage.warning('有视频正在上传中,请等待上传完成后再提交');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredWorks = omit(dataSource.value, 'videoInfo');
|
|
||||||
const { code, data } = await putWorksUpdateWriter(writerCode.value, { id: workId.value, ...filteredWorks });
|
|
||||||
if (code === 200) {
|
|
||||||
AMessage.success('保存成功');
|
|
||||||
isSaved.value = true;
|
|
||||||
|
|
||||||
if (check) {
|
|
||||||
slsWithCatch('writerManuscriptCheckIds', [workId.value]);
|
|
||||||
router.push({ path: `/writer/manuscript/check/${writerCode.value}` });
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const getData = async () => {
|
|
||||||
const { code, data } = await getWorksDetailWriter(writerCode.value, workId.value);
|
|
||||||
if (code === 200) {
|
|
||||||
const { type, files } = data;
|
|
||||||
const _data = { ...data, videoInfo: cloneDeep(INITIAL_VIDEO_INFO) };
|
|
||||||
|
|
||||||
// 初始化视频数据
|
|
||||||
if (type === EnumManuscriptType.Video && files.length) {
|
|
||||||
_data.videoInfo.uploadStatus = 'end';
|
|
||||||
const { name, size, duration, url } = files[0];
|
|
||||||
_data.videoInfo.name = name;
|
|
||||||
_data.videoInfo.size = formatFileSize(size);
|
|
||||||
_data.videoInfo.time = formatDuration(duration);
|
|
||||||
_data.videoInfo.poster = convertVideoUrlToCoverUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource.value = cloneDeep(_data);
|
|
||||||
remoteDataSource.value = cloneDeep(_data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onChange = (val) => {
|
|
||||||
dataSource.value = val;
|
|
||||||
};
|
|
||||||
const onUpdateVideoInfo = (newVideoInfo) => {
|
|
||||||
dataSource.value.videoInfo = { ...dataSource.value.videoInfo, ...newVideoInfo };
|
|
||||||
};
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ path: `/writer/manuscript/list/${writerCode.value}` });
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
workId && getData();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<div class="manuscript-edit-wrap">
|
|
||||||
<div class="flex items-center mb-8px">
|
|
||||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onCancel}>
|
|
||||||
内容稿件列表
|
|
||||||
</span>
|
|
||||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
|
||||||
<span class="cts bold !color-#1D2129">编辑内容稿件</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-y-auto p-24px bg-#fff rounded-8px ">
|
|
||||||
<EditForm
|
|
||||||
ref={formRef}
|
|
||||||
formData={dataSource.value}
|
|
||||||
onChange={onChange}
|
|
||||||
onUpdateVideoInfo={onUpdateVideoInfo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{' '}
|
|
||||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
|
||||||
退出
|
|
||||||
</Button>
|
|
||||||
<Button size="medium" type="outline" onClick={() => onSave()} class="mr-12px">
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={() => onSave(true)} loading={uploadLoading.value}>
|
|
||||||
保存并审核
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<CancelEditModal ref={cancelEditModal} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-edit-wrap {
|
|
||||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="common-filter-wrap">
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">内容稿件标题</span>
|
|
||||||
<a-input
|
|
||||||
v-model="query.title"
|
|
||||||
class="!w-240px"
|
|
||||||
placeholder="请输入内容稿件标题"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="filter-row-item">
|
|
||||||
<span class="label">所属项目</span>
|
|
||||||
<CommonSelect
|
|
||||||
placeholder="请选择所属项目"
|
|
||||||
v-model="query.project_id"
|
|
||||||
:options="projects"
|
|
||||||
class="!w-166px"
|
|
||||||
@change="handleSearch"
|
|
||||||
/>
|
|
||||||
</div> -->
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">序号</span>
|
|
||||||
<a-space size="medium">
|
|
||||||
<a-input
|
|
||||||
v-model="query.uid"
|
|
||||||
class="!w-160px"
|
|
||||||
placeholder="请输入序号"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">审核状态</span>
|
|
||||||
<CommonSelect
|
|
||||||
placeholder="全部"
|
|
||||||
:options="CHECK_STATUS"
|
|
||||||
v-model="query.audit_status"
|
|
||||||
class="!w-160px"
|
|
||||||
:multiple="false"
|
|
||||||
@change="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">上传时间</span>
|
|
||||||
<a-range-picker
|
|
||||||
v-model="created_at"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
format="YYYY-MM-DD"
|
|
||||||
class="!w-280px"
|
|
||||||
@change="onDateChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
|
|
||||||
<template #icon>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
<template #default>搜索</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button size="medium" @click="handleReset">
|
|
||||||
<template #icon>
|
|
||||||
<icon-refresh />
|
|
||||||
</template>
|
|
||||||
<template #default>重置</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { defineEmits, defineProps } from 'vue';
|
|
||||||
import { CHECK_STATUS } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
|
||||||
import CommonSelect from '@/components/common-select';
|
|
||||||
// import { getProjectList } from '@/api/all/propertyMarketing';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
query: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits('search', 'reset', 'update:query');
|
|
||||||
|
|
||||||
const created_at = ref([]);
|
|
||||||
// const projects = ref([]);
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
emits('update:query', props.query);
|
|
||||||
nextTick(() => {
|
|
||||||
emits('search');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDateChange = (value) => {
|
|
||||||
if (!value) {
|
|
||||||
props.query.created_at = [];
|
|
||||||
handleSearch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [start, end] = value;
|
|
||||||
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
props.query.created_at = [
|
|
||||||
dayjs(start).startOf('day').format(FORMAT_DATE),
|
|
||||||
dayjs(end).endOf('day').format(FORMAT_DATE),
|
|
||||||
];
|
|
||||||
|
|
||||||
handleSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取项目列表
|
|
||||||
// const getProjects = async () => {
|
|
||||||
// try {
|
|
||||||
// const { code, data } = await getProjectList();
|
|
||||||
// if (code === 200) {
|
|
||||||
// projects.value = data;
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('获取项目列表失败:', error);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
created_at.value = [];
|
|
||||||
// projects.value = [];
|
|
||||||
emits('reset');
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// getProjects();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
export const TABLE_COLUMNS = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 240,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '所属项目',
|
|
||||||
// dataIndex: 'projects',
|
|
||||||
// width: 240,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核状态',
|
|
||||||
dataIndex: 'audit_status',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传时间',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '上传人员',
|
|
||||||
// dataIndex: 'uploader',
|
|
||||||
// width: 180,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '最后修改人员',
|
|
||||||
// dataIndex: 'last_modifier',
|
|
||||||
// width: 180,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 180,
|
|
||||||
fixed: 'right'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:visible="visible" title="删除稿件" width="480px" @close="onClose">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
|
||||||
<span>确认删除 {{ projectName }} 这个稿件吗?</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="large" @click="onClose">取消</a-button>
|
|
||||||
<a-button type="primary" class="ml-16px" status="danger" size="large" @click="onDelete">确认删除</a-button>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { deleteWorkWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const update = inject('update');
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const projectId = ref(null);
|
|
||||||
const projectName = ref('');
|
|
||||||
|
|
||||||
const isBatch = computed(() => Array.isArray(projectId.value));
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
|
|
||||||
function onClose() {
|
|
||||||
visible.value = false;
|
|
||||||
projectId.value = null;
|
|
||||||
projectName.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = (record) => {
|
|
||||||
const { id = null, name = '' } = record;
|
|
||||||
projectId.value = id;
|
|
||||||
projectName.value = name;
|
|
||||||
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function onDelete() {
|
|
||||||
const { code } = await deleteWorkWriter(writerCode.value, projectId.value);
|
|
||||||
if (code === 200) {
|
|
||||||
AMessage.success('删除成功');
|
|
||||||
update();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-table
|
|
||||||
ref="tableRef"
|
|
||||||
:data="dataSource"
|
|
||||||
row-key="id"
|
|
||||||
column-resizable
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ x: '100%' }"
|
|
||||||
class="manuscript-table w-100%"
|
|
||||||
bordered
|
|
||||||
@sorter-change="handleSorterChange"
|
|
||||||
>
|
|
||||||
<template #empty>
|
|
||||||
<NoData text="暂无稿件" />
|
|
||||||
</template>
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column
|
|
||||||
v-for="column in TABLE_COLUMNS"
|
|
||||||
:key="column.dataIndex"
|
|
||||||
:data-index="column.dataIndex"
|
|
||||||
:fixed="column.fixed"
|
|
||||||
:width="column.width"
|
|
||||||
:min-width="column.minWidth"
|
|
||||||
:sortable="column.sortable"
|
|
||||||
:align="column.align"
|
|
||||||
ellipsis
|
|
||||||
tooltip
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px">{{ column.title }}</span>
|
|
||||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
|
||||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="column.dataIndex === 'create_at'" #cell="{ record }">
|
|
||||||
{{ exactFormatTime(record.create_at) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'title'" #cell="{ record }">
|
|
||||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'audit_status'" #cell="{ record }">
|
|
||||||
<div
|
|
||||||
class="flex items-center w-fit h-28px px-8px rounded-2px"
|
|
||||||
:style="{ backgroundColor: getStatusInfo(record.audit_status).backgroundColor }"
|
|
||||||
>
|
|
||||||
<span class="cts s1" :style="{ color: getStatusInfo(record.audit_status).color }">{{
|
|
||||||
getStatusInfo(record.audit_status).name
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
|
||||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="['uploader', 'last_modifier'].includes(column.dataIndex)" #cell="{ record }">
|
|
||||||
{{ record[column.dataIndex].name || record[column.dataIndex].mobile }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="['created_at', 'last_modified_at'].includes(column.dataIndex)" #cell="{ record }">
|
|
||||||
{{ exactFormatTime(record[column.dataIndex]) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
|
||||||
<HoverImagePreview :src="record.cover">
|
|
||||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
|
||||||
<template #error>
|
|
||||||
<img :src="icon4" class="w-full h-full" />
|
|
||||||
</template>
|
|
||||||
</a-image>
|
|
||||||
</HoverImagePreview>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
|
||||||
<a-button type="outline" size="mini" class="mr-8px" @click="onEdit(record)">编辑</a-button>
|
|
||||||
<a-button type="outline" size="mini" @click="onDetail(record)">详情</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else #cell="{ record }">
|
|
||||||
{{ formatTableField(column, record, true) }}
|
|
||||||
</template>
|
|
||||||
</a-table-column>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
|
||||||
import { TABLE_COLUMNS } from './constants';
|
|
||||||
import { CHECK_STATUS, EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript-writer/list/constants';
|
|
||||||
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import HoverImagePreview from '@/components/hover-image-preview';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-delete.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
import icon4 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
const emits = defineEmits(['edit', 'sorterChange', 'delete']);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dataSource: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableRef = ref(null);
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const handleSorterChange = (column, order) => {
|
|
||||||
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
|
|
||||||
};
|
|
||||||
const onDelete = (item) => {
|
|
||||||
emits('delete', item);
|
|
||||||
};
|
|
||||||
const onEdit = (item) => {
|
|
||||||
router.push(`/writer/manuscript/edit/${route.params.writerCode}/${item.id}`);
|
|
||||||
};
|
|
||||||
const onDetail = (item) => {
|
|
||||||
router.push(`/writer/manuscript/detail/${route.params.writerCode}/${item.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusInfo = (audit_status) => {
|
|
||||||
return CHECK_STATUS.find((v) => v.id === audit_status) ?? {};
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
.manuscript-table {
|
|
||||||
.cts {
|
|
||||||
color: var(--Text-1, #211f24);
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
:deep(.title) {
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: #6d4cfe;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,374 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
Input,
|
|
||||||
RadioGroup,
|
|
||||||
Radio,
|
|
||||||
Upload,
|
|
||||||
Button,
|
|
||||||
Message as AMessage,
|
|
||||||
Textarea,
|
|
||||||
} from '@arco-design/web-vue';
|
|
||||||
import {
|
|
||||||
getTemplateUrlWriter,
|
|
||||||
postWorksByLinkWriter,
|
|
||||||
postWorksByFileWriter,
|
|
||||||
} from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import { getTemplateUrl } from '@/api/all/generationWorkshop';
|
|
||||||
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-feedback-fail.png';
|
|
||||||
|
|
||||||
// 状态枚举
|
|
||||||
const TASK_STATUS = {
|
|
||||||
DEFAULT: 1,
|
|
||||||
LOADING: 2,
|
|
||||||
FAILED: 3,
|
|
||||||
SUCCESS: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const UPLOAD_TYPE = {
|
|
||||||
LINK: 'link',
|
|
||||||
LOCAL: 'local',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始表单数据
|
|
||||||
const INITIAL_FORM = {
|
|
||||||
link: '',
|
|
||||||
writerLink: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const update = inject('update');
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
// 响应式状态
|
|
||||||
const visible = ref(false);
|
|
||||||
const formRef = ref(null);
|
|
||||||
const uploadType = ref(UPLOAD_TYPE.LOCAL);
|
|
||||||
const taskStatus = ref(TASK_STATUS.DEFAULT);
|
|
||||||
const form = ref(cloneDeep(INITIAL_FORM));
|
|
||||||
const works = ref([]);
|
|
||||||
const uploadingFiles = ref([]); // 上传中
|
|
||||||
const uploadSuccessFiles = ref([]); // 上传成功
|
|
||||||
|
|
||||||
// 生成自增 id(基于当前列表中最大的 id)
|
|
||||||
const getNextWorkId = () => {
|
|
||||||
const currentMax = works.value.reduce((max, item) => {
|
|
||||||
const numericId = Number(item?.id);
|
|
||||||
return Number.isFinite(numericId) ? Math.max(max, numericId) : max;
|
|
||||||
}, 0);
|
|
||||||
return currentMax + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLink = computed(() => uploadType.value === UPLOAD_TYPE.LINK);
|
|
||||||
const isLocal = computed(() => uploadType.value === UPLOAD_TYPE.LOCAL);
|
|
||||||
const isDefault = computed(() => taskStatus.value === TASK_STATUS.DEFAULT);
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
|
|
||||||
// 模态框标题
|
|
||||||
const getTitle = () => {
|
|
||||||
const titleMap = {
|
|
||||||
[TASK_STATUS.DEFAULT]: '上传内容稿件',
|
|
||||||
[TASK_STATUS.LOADING]: isLink.value ? '链接上传' : isLocal.value ? '本地批量上传' : '上传内容稿件',
|
|
||||||
[TASK_STATUS.FAILED]: isLink.value ? '链接上传' : isLocal.value ? '本地批量上传' : '上传内容稿件',
|
|
||||||
[TASK_STATUS.SUCCESS]: '上传内容稿件列表',
|
|
||||||
};
|
|
||||||
return titleMap[taskStatus.value];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
const reset = () => {
|
|
||||||
formRef.value?.resetFields?.();
|
|
||||||
uploadType.value = UPLOAD_TYPE.LINK;
|
|
||||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
|
||||||
form.value = cloneDeep(INITIAL_FORM);
|
|
||||||
works.value = [];
|
|
||||||
uploadingFiles.value = [];
|
|
||||||
uploadSuccessFiles.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
reset();
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 防抖提交
|
|
||||||
const debouncedSubmit = debounce(async () => {
|
|
||||||
formRef.value?.validate(async (errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
taskStatus.value = TASK_STATUS.LOADING;
|
|
||||||
const { link } = form.value;
|
|
||||||
const { code, data } = await postWorksByLinkWriter(writerCode.value, { link });
|
|
||||||
if (code === 200) {
|
|
||||||
taskStatus.value = TASK_STATUS.SUCCESS;
|
|
||||||
works.value = data ? [data] : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// 提交处理
|
|
||||||
const onSubmit = () => {
|
|
||||||
debouncedSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 取消上传
|
|
||||||
const onCancelUpload = () => {
|
|
||||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
|
||||||
AMessage.info('已取消上传');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件上传处理
|
|
||||||
const handleUpload = async (option) => {
|
|
||||||
taskStatus.value = TASK_STATUS.LOADING;
|
|
||||||
|
|
||||||
const {
|
|
||||||
fileItem: { file },
|
|
||||||
} = option;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
uploadingFiles.value.push(file);
|
|
||||||
|
|
||||||
const { code, data } = await postWorksByFileWriter(formData, {
|
|
||||||
timeout: 0,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
'writer-code': writerCode.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (code === 200) {
|
|
||||||
if (data) {
|
|
||||||
const id = data.id ?? getNextWorkId();
|
|
||||||
const _data = { ...data, id };
|
|
||||||
works.value.push(_data);
|
|
||||||
uploadSuccessFiles.value.push(_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadingFiles.value.length === uploadSuccessFiles.value.length) {
|
|
||||||
taskStatus.value = TASK_STATUS.SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳转编辑
|
|
||||||
const goUpload = () => {
|
|
||||||
slsWithCatch('writerWaitUploadWorks', JSON.stringify(works.value));
|
|
||||||
router.push(`/writer/manuscript/upload/${writerCode.value}`);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除项目
|
|
||||||
const onDelete = (index) => {
|
|
||||||
works.value.splice(index, 1);
|
|
||||||
if (!works.value.length) {
|
|
||||||
taskStatus.value = TASK_STATUS.DEFAULT;
|
|
||||||
}
|
|
||||||
// AMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传方式切换
|
|
||||||
const onUploadTypeChange = (val) => {
|
|
||||||
uploadType.value = val;
|
|
||||||
formRef.value?.clearValidate?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载模板
|
|
||||||
const handleDownloadTemplate = async () => {
|
|
||||||
const { code, data } = await getTemplateUrlWriter(writerCode.value);
|
|
||||||
if (code === 200) {
|
|
||||||
window.open(data.download_url, '_blank');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染链接上传表单
|
|
||||||
const renderLinkForm = () => (
|
|
||||||
<FormItem label="链接地址" field="link" required>
|
|
||||||
<Textarea
|
|
||||||
v-model={form.value.link}
|
|
||||||
size="large"
|
|
||||||
placeholder="请输入飞书链接地址"
|
|
||||||
autoSize={{ minRows: 5, maxRows: 8 }}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染本地上传表单
|
|
||||||
const renderLocalForm = () => (
|
|
||||||
<FormItem label="内容稿件">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<Upload
|
|
||||||
action="/"
|
|
||||||
draggable
|
|
||||||
multiple
|
|
||||||
customRequest={handleUpload}
|
|
||||||
accept=".xlsx,.xls,.docx,.doc,.mp4,.mov,.avi,.flv,.wmv,.m4v"
|
|
||||||
show-file-list={false}
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
'upload-button': () => (
|
|
||||||
<div class="upload-box">
|
|
||||||
<icon-plus size="14" class="mb-16px" />
|
|
||||||
<span class="text mb-4px">点击或拖拽文件到此处上传</span>
|
|
||||||
<span class="tip">支持文档(文本+图), 视频批量上传</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Upload>
|
|
||||||
<div class="flex items-center cursor-pointer mt-8px" onClick={handleDownloadTemplate}>
|
|
||||||
<icon-download size="14" class="mr-4px !color-#6D4CFE" />
|
|
||||||
<span class="cts color-#6D4CFE">下载示例文档</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染加载状态
|
|
||||||
const renderLoadingState = () => (
|
|
||||||
<div class="flex flex-col items-center justify-center rounded-8px bg-#F7F8FA h-208px">
|
|
||||||
<icon-loading size="48" class="!color-#6D4CFE mb-16px" />
|
|
||||||
<p class="tip !text-#768893">上传过程耗时可能较长,请耐心等待;</p>
|
|
||||||
<p class="tip !text-#768893">刷新页面将会终止本次数据的上传,请谨慎操作</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染失败状态
|
|
||||||
const renderFailedState = () => (
|
|
||||||
<div class="flex flex-col items-center justify-center rounded-8px bg-#F7F8FA h-208px">
|
|
||||||
<img src={icon1} class="w-80px h-80px mb-16px" />
|
|
||||||
<p class="text mb-4px">上传失败</p>
|
|
||||||
<p class="tip !text-#768893">可能是网络不稳定导致,请检查网络后重试~</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染成功状态
|
|
||||||
const renderSuccessState = () => (
|
|
||||||
<div class="flex flex-col py-12px max-h-540px rounded-8px bg-#F7F8FA">
|
|
||||||
<span class="tip mb-8px px-12px fs-14px !text-left">共 {works.value.length} 个内容稿件</span>
|
|
||||||
<div class="flex-1 overflow-y-auto overflow-x-hidden px-12px">
|
|
||||||
{works.value.map((item, index) => (
|
|
||||||
<div key={item.id} class="rounded-8px bg-#fff px-8px py-8px flex justify-between items-center mt-8px">
|
|
||||||
<div class="flex-1 overflow-hidden flex items-center mr-12px">
|
|
||||||
<img src={item.cover} width="32" height="32" class="rounded-3px mr-8px" />
|
|
||||||
<TextOverTips class="text !text-left" context={item.title} />
|
|
||||||
</div>
|
|
||||||
<icon-delete
|
|
||||||
size="16px"
|
|
||||||
class="color-#737478 cursor-pointer hover:!color-#211F24"
|
|
||||||
onClick={() => onDelete(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染表单内容
|
|
||||||
const renderFormContent = () => {
|
|
||||||
const contentMap = {
|
|
||||||
[TASK_STATUS.DEFAULT]: () => {
|
|
||||||
const formMap = {
|
|
||||||
[UPLOAD_TYPE.LINK]: renderLinkForm,
|
|
||||||
[UPLOAD_TYPE.LOCAL]: renderLocalForm,
|
|
||||||
};
|
|
||||||
return formMap[uploadType.value]?.();
|
|
||||||
},
|
|
||||||
[TASK_STATUS.LOADING]: renderLoadingState,
|
|
||||||
[TASK_STATUS.FAILED]: renderFailedState,
|
|
||||||
[TASK_STATUS.SUCCESS]: renderSuccessState,
|
|
||||||
};
|
|
||||||
|
|
||||||
return contentMap[taskStatus.value]?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染底部按钮
|
|
||||||
const renderFooterButtons = () => {
|
|
||||||
const buttonMap = {
|
|
||||||
[TASK_STATUS.LOADING]: () => (
|
|
||||||
<Button type="primary" size="medium" onClick={onCancelUpload}>
|
|
||||||
取消上传
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
[TASK_STATUS.DEFAULT]: () => (
|
|
||||||
<>
|
|
||||||
<Button size="medium" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={onSubmit}>
|
|
||||||
确认
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
[TASK_STATUS.FAILED]: () => (
|
|
||||||
<>
|
|
||||||
<Button size="medium" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={onClose}>
|
|
||||||
重新上传
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
[TASK_STATUS.SUCCESS]: () => (
|
|
||||||
<>
|
|
||||||
<Button size="medium" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={goUpload}>
|
|
||||||
确认
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return buttonMap[taskStatus.value]?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
expose({ open });
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<Modal
|
|
||||||
v-model:visible={visible.value}
|
|
||||||
title={getTitle()}
|
|
||||||
modal-class="upload-manuscript-modal"
|
|
||||||
width="500px"
|
|
||||||
mask-closable={false}
|
|
||||||
unmount-on-close
|
|
||||||
onClose={onClose}
|
|
||||||
footer={!(isDefault.value && isLocal.value)}
|
|
||||||
v-slots={{
|
|
||||||
footer: () => renderFooterButtons(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form ref={formRef} model={form.value} layout="horizontal" auto-label-width>
|
|
||||||
{/* {isDefault.value && (
|
|
||||||
<FormItem label="上传方式">
|
|
||||||
<RadioGroup v-model={uploadType.value} onChange={onUploadTypeChange}>
|
|
||||||
<Radio value={UPLOAD_TYPE.LINK}>外部链接上传</Radio>
|
|
||||||
<Radio value={UPLOAD_TYPE.LOCAL}>本地上传</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormItem>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{renderFormContent()}
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
.upload-manuscript-modal {
|
|
||||||
.text {
|
|
||||||
color: var(--Text-1, #211f24);
|
|
||||||
text-align: center;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
.tip {
|
|
||||||
color: var(--Text-3, #737478);
|
|
||||||
text-align: center;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.upload-box {
|
|
||||||
display: flex;
|
|
||||||
height: 120px;
|
|
||||||
padding: 0 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
|
||||||
background: var(--BG-200, #f2f3f5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
export const INITIAL_QUERY = {
|
|
||||||
title: '',
|
|
||||||
// project_ids: [],
|
|
||||||
uid: '',
|
|
||||||
audit_status: '',
|
|
||||||
created_at: [],
|
|
||||||
sort_column: undefined,
|
|
||||||
sort_order: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum EnumCheckStatus {
|
|
||||||
All = '',
|
|
||||||
Wait = 1,
|
|
||||||
Checking = 2,
|
|
||||||
Passed = 3,
|
|
||||||
}
|
|
||||||
export enum EnumManuscriptType {
|
|
||||||
All = '',
|
|
||||||
Image = 0,
|
|
||||||
Video = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHECK_STATUS = [
|
|
||||||
{
|
|
||||||
name: '待审核',
|
|
||||||
id: EnumCheckStatus.Wait,
|
|
||||||
backgroundColor: '#F2F3F5',
|
|
||||||
color: '#3C4043'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '审核中',
|
|
||||||
id: EnumCheckStatus.Checking,
|
|
||||||
backgroundColor: '#FFF7E5',
|
|
||||||
color: '#FFAE00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '已通过',
|
|
||||||
id: EnumCheckStatus.Passed,
|
|
||||||
backgroundColor: '#EBF7F2',
|
|
||||||
color: '#25C883'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export const MANUSCRIPT_TYPE = [
|
|
||||||
{
|
|
||||||
label: '全部',
|
|
||||||
value: EnumManuscriptType.All,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '图片',
|
|
||||||
value: EnumManuscriptType.Image,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '视频',
|
|
||||||
value: EnumManuscriptType.Video,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="manuscript-list-wrap">
|
|
||||||
<div class="filter-wrap bg-#fff rounded-8px mb-16px">
|
|
||||||
<div class="top flex h-64px px-24px py-10px justify-between items-center">
|
|
||||||
<p class="text-18px font-400 lh-26px color-#211F24 title">内容稿件列表</p>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<a-button type="primary" size="medium" @click="openUploadModal">
|
|
||||||
<template #icon>
|
|
||||||
<icon-plus size="16" />
|
|
||||||
</template>
|
|
||||||
<template #default>上传内容稿件</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FilterBlock v-model:query="query" @search="handleSearch" @reset="handleReset" />
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap bg-#fff rounded-8px px-24px py-24px flex flex-col">
|
|
||||||
<ManuscriptTable :dataSource="dataSource" @sorterChange="handleSorterChange" @delete="handleDelete" />
|
|
||||||
<div v-if="pageInfo.total > 0" class="pagination-row">
|
|
||||||
<a-pagination
|
|
||||||
:total="pageInfo.total"
|
|
||||||
size="mini"
|
|
||||||
show-total
|
|
||||||
show-jumper
|
|
||||||
show-page-size
|
|
||||||
:current="pageInfo.page"
|
|
||||||
:page-size="pageInfo.page_size"
|
|
||||||
@change="onPageChange"
|
|
||||||
@page-size-change="onPageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeleteManuscriptModal ref="deleteManuscriptModalRef" />
|
|
||||||
<UploadManuscriptModal ref="uploadManuscriptModalRef" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="jsx" setup>
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { Button } from '@arco-design/web-vue';
|
|
||||||
import FilterBlock from './components/filter-block';
|
|
||||||
import ManuscriptTable from './components/manuscript-table';
|
|
||||||
import DeleteManuscriptModal from './components/manuscript-table/delete-manuscript-modal.vue';
|
|
||||||
import UploadManuscriptModal from './components/upload-manuscript-modal';
|
|
||||||
|
|
||||||
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
|
|
||||||
import { getWorksPageWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import {
|
|
||||||
INITIAL_QUERY,
|
|
||||||
EnumCheckStatus,
|
|
||||||
} from '@/views/creative-generation-workshop/manuscript-writer/list/constants.ts';
|
|
||||||
|
|
||||||
const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } = useTableSelectionWithPagination({
|
|
||||||
onPageChange: () => {
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
onPageSizeChange: () => {
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const route = useRoute();
|
|
||||||
const query = ref(cloneDeep(INITIAL_QUERY));
|
|
||||||
const addManuscriptModalRef = ref(null);
|
|
||||||
const deleteManuscriptModalRef = ref(null);
|
|
||||||
const uploadManuscriptModalRef = ref(null);
|
|
||||||
|
|
||||||
const getData = async () => {
|
|
||||||
const { page, page_size } = pageInfo.value;
|
|
||||||
const { code, data } = await getWorksPageWriter(route.params.writerCode, {
|
|
||||||
...query.value,
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
});
|
|
||||||
if (code === 200) {
|
|
||||||
dataSource.value = data?.data ?? [];
|
|
||||||
pageInfo.value.total = data.total;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleSearch = () => {
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
const reload = () => {
|
|
||||||
pageInfo.value.page = 1;
|
|
||||||
getData();
|
|
||||||
};
|
|
||||||
const handleReset = () => {
|
|
||||||
resetPageInfo();
|
|
||||||
query.value = cloneDeep(INITIAL_QUERY);
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
const handleSorterChange = (column, order) => {
|
|
||||||
query.value.sort_column = column;
|
|
||||||
query.value.sort_order = order;
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openUploadModal = () => {
|
|
||||||
uploadManuscriptModalRef.value?.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (item) => {
|
|
||||||
const { id, title } = item;
|
|
||||||
deleteManuscriptModalRef.value?.open({ id, name: `“${title}”` });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getData();
|
|
||||||
});
|
|
||||||
provide('update', getData);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
.manuscript-list-wrap {
|
|
||||||
// height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.filter-wrap {
|
|
||||||
.top {
|
|
||||||
.title {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
:deep(.arco-btn) {
|
|
||||||
.arco-btn-icon {
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.table-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:visible="visible" title="确认提示" width="480px" @close="onClose">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
|
||||||
<span>确认取消上传这 {{ num }} 个文件吗?此操作不可恢复。</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
|
||||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">确认取消</a-button>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const visible = ref(false);
|
|
||||||
const num = ref('');
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
num.value = '';
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
const onConfirm = () => {
|
|
||||||
onClose();
|
|
||||||
router.push({ path: `/writer/manuscript/list/${route.params.writerCode}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = (manusNum) => {
|
|
||||||
num.value = manusNum;
|
|
||||||
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,323 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import EditForm, { ENUM_UPLOAD_STATUS, INITIAL_VIDEO_INFO } from '../components/edit-form';
|
|
||||||
import CancelUploadModal from './cancel-upload-modal.vue';
|
|
||||||
import UploadSuccessModal from './upload-success-modal.vue';
|
|
||||||
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript-writer/list/constants';
|
|
||||||
import { postWorksBatchWriter } from '@/api/all/generationWorkshop-writer.ts';
|
|
||||||
import { glsWithCatch, rlsWithCatch, slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import { formatDuration, formatFileSize, convertVideoUrlToCoverUrl } from '@/utils/tools';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditForm,
|
|
||||||
},
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const formRef = ref(null);
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
|
|
||||||
const cancelUploadModal = ref(null);
|
|
||||||
const uploadSuccessModal = ref(null);
|
|
||||||
const works = ref([]);
|
|
||||||
const selectCardInfo = ref({});
|
|
||||||
const errorDataCards = ref([]);
|
|
||||||
const uploadLoading = ref(false);
|
|
||||||
|
|
||||||
const writerCode = computed(() => route.params.writerCode);
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
cancelUploadModal.value?.open(works.value.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateDataSource = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const uploadingVideos = works.value.filter(
|
|
||||||
(item) => item.videoInfo?.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING,
|
|
||||||
);
|
|
||||||
if (uploadingVideos.length > 0) {
|
|
||||||
AMessage.warning(`有 ${uploadingVideos.length} 个视频正在上传中,请等待上传完成后再提交`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
works.value.forEach((item) => {
|
|
||||||
if (!item.title?.trim()) {
|
|
||||||
errorDataCards.value.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!errorDataCards.value.length) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorDataCards.value.length > 0) {
|
|
||||||
AMessage.warning(`有 ${errorDataCards.value.length} 个必填信息未填写,请检查`);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const el = document.getElementById(`card-${errorDataCards.value[0]?.id}`);
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (action) => {
|
|
||||||
uploadLoading.value = true;
|
|
||||||
const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo'));
|
|
||||||
const { code, data } = await postWorksBatchWriter({ works: filteredWorks }, writerCode.value);
|
|
||||||
if (code === 200) {
|
|
||||||
uploadLoading.value = false;
|
|
||||||
if (action === 'batchUpload') {
|
|
||||||
uploadSuccessModal.value?.open(data);
|
|
||||||
} else {
|
|
||||||
if (action === 'uploadAndCheck') {
|
|
||||||
slsWithCatch('writerManuscriptCheckIds', [data]);
|
|
||||||
router.push({ path: `/writer/manuscript/check/${writerCode.value}` });
|
|
||||||
} else {
|
|
||||||
router.push({ path: `/writer/manuscript/list/${writerCode.value}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpload = async (action) => {
|
|
||||||
formRef.value
|
|
||||||
?.validate()
|
|
||||||
.then(() => {
|
|
||||||
return validateDataSource();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
onSubmit(action);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('catch');
|
|
||||||
errorDataCards.value.push(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncLocalStorage = () => {
|
|
||||||
slsWithCatch('writerWaitUploadWorks', JSON.stringify(works.value ?? []));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReValidate = () => {
|
|
||||||
formRef.value?.validate().then(() => {
|
|
||||||
deleteErrorCard(selectCardInfo.value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onSelect = (item) => {
|
|
||||||
formRef.value.resetForm();
|
|
||||||
selectCardInfo.value = cloneDeep(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteErrorCard = (item) => {
|
|
||||||
const errorIndex = errorDataCards.value.findIndex((v) => v.id === item.id);
|
|
||||||
errorIndex > -1 && errorDataCards.value.splice(errorIndex, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = (e, item, index) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
works.value.splice(index, 1);
|
|
||||||
deleteErrorCard(item);
|
|
||||||
|
|
||||||
if (item.id === selectCardInfo.value.id) {
|
|
||||||
if (works.value.length) {
|
|
||||||
const _target = works.value[0] ?? {};
|
|
||||||
selectCardInfo.value = cloneDeep(_target);
|
|
||||||
} else {
|
|
||||||
selectCardInfo.value = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncLocalStorage();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooterRow = () => {
|
|
||||||
if (works.value.length > 1) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
|
||||||
取消上传
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={() => onUpload('batchUpload')} loading={uploadLoading.value}>
|
|
||||||
{uploadLoading.value ? '批量上传' : '批量上传'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
|
||||||
取消上传
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="medium"
|
|
||||||
type="outline"
|
|
||||||
onClick={() => onUpload('singleUpload')}
|
|
||||||
class="mr-12px"
|
|
||||||
loading={uploadLoading.value}
|
|
||||||
>
|
|
||||||
上传
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="medium"
|
|
||||||
onClick={() => onUpload('uploadAndCheck')}
|
|
||||||
loading={uploadLoading.value}
|
|
||||||
>
|
|
||||||
上传并审核
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getData = () => {
|
|
||||||
const _works = JSON.parse(glsWithCatch('writerWaitUploadWorks') ?? '[]')?.map((item) => {
|
|
||||||
const { type, files } = item;
|
|
||||||
|
|
||||||
const _data = { ...item, videoInfo: cloneDeep(INITIAL_VIDEO_INFO) };
|
|
||||||
|
|
||||||
// 初始化视频数据
|
|
||||||
if (type === EnumManuscriptType.Video && files.length) {
|
|
||||||
_data.videoInfo.uploadStatus = 'end';
|
|
||||||
const { name, size, duration, url } = files[0];
|
|
||||||
_data.videoInfo.name = name;
|
|
||||||
_data.videoInfo.size = formatFileSize(size);
|
|
||||||
_data.videoInfo.time = formatDuration(duration);
|
|
||||||
_data.videoInfo.poster = convertVideoUrlToCoverUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _data;
|
|
||||||
});
|
|
||||||
|
|
||||||
works.value = _works;
|
|
||||||
selectCardInfo.value = cloneDeep(_works[0] ?? {});
|
|
||||||
};
|
|
||||||
const getCardClass = (item) => {
|
|
||||||
if (selectCardInfo.value.id === item.id) {
|
|
||||||
return '!border-#6D4CFE !bg-#F0EDFF';
|
|
||||||
}
|
|
||||||
if (errorDataCards.value.find((v) => v.id === item.id)) {
|
|
||||||
return '!border-#F64B31';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChange = (val) => {
|
|
||||||
const index = works.value.findIndex((v) => v.id === selectCardInfo.value.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
works.value[index] = cloneDeep(val);
|
|
||||||
}
|
|
||||||
selectCardInfo.value = val;
|
|
||||||
|
|
||||||
syncLocalStorage();
|
|
||||||
};
|
|
||||||
const onUpdateVideoInfo = (newVideoInfo) => {
|
|
||||||
const index = works.value.findIndex((v) => v.id === selectCardInfo.value.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
works.value[index].videoInfo = cloneDeep(newVideoInfo);
|
|
||||||
}
|
|
||||||
selectCardInfo.value.videoInfo = { ...selectCardInfo.value.videoInfo, ...newVideoInfo };
|
|
||||||
|
|
||||||
syncLocalStorage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getData();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
rlsWithCatch('writerWaitUploadWorks');
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<div class="manuscript-upload-wrap bg-#fff rounded-8px flex">
|
|
||||||
<div class="left flex-1 overflow-y-auto p-24px">
|
|
||||||
<EditForm
|
|
||||||
ref={formRef}
|
|
||||||
formData={selectCardInfo.value}
|
|
||||||
onReValidate={handleReValidate}
|
|
||||||
onChange={onChange}
|
|
||||||
onUpdateVideoInfo={onUpdateVideoInfo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{works.value.length > 1 && (
|
|
||||||
<div class="right pt-24px pb-12px flex flex-col">
|
|
||||||
<div class="title flex items-center px-24px">
|
|
||||||
<div class="w-3px h-16px bg-#6D4CFE rounded-2px mr-8px"></div>
|
|
||||||
<p class="cts bold !color-#211F24 !text-16px !lh-24px mr-8px">上传内容稿件列表</p>
|
|
||||||
<p class="cts">{`共${works.value.length}个`}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 px-24px overflow-y-auto pt-24px">
|
|
||||||
{works.value.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
id={`card-${item.id}`}
|
|
||||||
class={`group h-66px relative mb-12px px-12px py-8px flex flex-col rounded-8px bg-#F7F8FA border-1px border-solid border-transparent transition-all duration-300 cursor-pointer hover:bg-#E6E6E8 ${getCardClass(
|
|
||||||
item,
|
|
||||||
)}`}
|
|
||||||
onClick={() => onSelect(item)}
|
|
||||||
>
|
|
||||||
<icon-close-circle-fill
|
|
||||||
size={16}
|
|
||||||
class="absolute top--8px right--8px color-#737478 hover:color-#211F24 hidden group-hover:block"
|
|
||||||
onClick={(e) => onDelete(e, item, index)}
|
|
||||||
/>
|
|
||||||
<TextOverTips
|
|
||||||
context={item.title || '-'}
|
|
||||||
line={1}
|
|
||||||
class={`cts !color-#211F24 mb-8px ${selectCardInfo.value.id === item.id ? 'bold' : ''}`}
|
|
||||||
/>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center ">
|
|
||||||
<img
|
|
||||||
src={item.type === EnumManuscriptType.Image ? icon1 : icon2}
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class={`cts !text-12px ${
|
|
||||||
item.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.type === EnumManuscriptType.Image ? '图文' : '视频'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{errorDataCards.value.find((v) => v.id === item.id) && (
|
|
||||||
<p class="cts !text-12px !color-#F64B31">必填项未填</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderFooterRow()}
|
|
||||||
</footer>
|
|
||||||
<CancelUploadModal ref={cancelUploadModal} />
|
|
||||||
<UploadSuccessModal ref={uploadSuccessModal} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-upload-wrap {
|
|
||||||
height: calc(100% - ($footer-height - $layout-padding-bottom));
|
|
||||||
.cts,
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
border-left: 1px solid var(--Border-2, #e6e6e8);
|
|
||||||
width: 320px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:visible="visible" title="提示" width="480px" @close="onClose" modal-class="upload-success11-modal">
|
|
||||||
<div class="flex items-center flex-col justify-center">
|
|
||||||
<img :src="icon1" width="80" height="80" class="mb-16px" />
|
|
||||||
<span class="text-18px lh-26px font-400 color-#211F24 md">上传成功</span>
|
|
||||||
<p class="text-14px lh-22px font-400 color-#737478 ld">为确保内容合规,建议您立即进行审核</p>
|
|
||||||
<p class="text-14px lh-22px font-400 color-#737478 ld">检测是否存在违规内容</p>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="medium" @click="onBack">回到列表</a-button>
|
|
||||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">批量审核</a-button>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-feedback-success.png';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const visible = ref(false);
|
|
||||||
const workIds = ref([]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
workIds.value = [];
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBack = () => {
|
|
||||||
onClose();
|
|
||||||
router.push({ path: `/writer/manuscript/list/${route.params.writerCode}` });
|
|
||||||
};
|
|
||||||
const onConfirm = () => {
|
|
||||||
visible.value = false;
|
|
||||||
slsWithCatch('writerManuscriptCheckIds', workIds.value);
|
|
||||||
router.push({ path: `/writer/manuscript/check/${route.params.writerCode}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = (_workIds) => {
|
|
||||||
workIds.value = _workIds;
|
|
||||||
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
<style lang="scss">
|
|
||||||
.upload-success11-modal {
|
|
||||||
.arco-modal-header {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.md {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
.ld {
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
}
|
|
||||||
.arco-modal-footer {
|
|
||||||
border-top: none;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<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 { deleteWork } from '@/api/all/generationWorkshop';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const update = inject('update');
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const projectId = ref(null);
|
|
||||||
const projectName = ref('');
|
|
||||||
|
|
||||||
const isBatch = computed(() => Array.isArray(projectId.value));
|
|
||||||
|
|
||||||
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 deleteWork(projectId.value);
|
|
||||||
if (code === 200) {
|
|
||||||
AMessage.success('删除成功');
|
|
||||||
update()
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-table
|
|
||||||
ref="tableRef"
|
|
||||||
:data="dataSource"
|
|
||||||
row-key="id"
|
|
||||||
column-resizable
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ x: '100%' }"
|
|
||||||
class="flex-1 manuscript-table w-100%"
|
|
||||||
bordered
|
|
||||||
:row-selection="rowSelection"
|
|
||||||
:selected-row-keys="selectedRowKeys"
|
|
||||||
@sorter-change="handleSorterChange"
|
|
||||||
@select="(selectedKeys, rowKeyValue, record) => emits('select', selectedKeys, rowKeyValue, record)"
|
|
||||||
@select-all="(check) => emits('selectAll', check)"
|
|
||||||
>
|
|
||||||
<template #empty>
|
|
||||||
<NoData text="暂无稿件" />
|
|
||||||
</template>
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column
|
|
||||||
v-for="column in tableColumns"
|
|
||||||
:key="column.dataIndex"
|
|
||||||
:data-index="column.dataIndex"
|
|
||||||
:fixed="column.fixed"
|
|
||||||
:width="column.width"
|
|
||||||
:min-width="column.minWidth"
|
|
||||||
:sortable="column.sortable"
|
|
||||||
:align="column.align"
|
|
||||||
ellipsis
|
|
||||||
tooltip
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px">{{ column.title }}</span>
|
|
||||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
|
||||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="column.dataIndex === 'customer_opinion'" #cell="{ record }">
|
|
||||||
<p
|
|
||||||
class="h-28px px-8px flex items-center rounded-2px w-fit"
|
|
||||||
:style="{ background: getCustomerOpinionInfo(record.customer_opinion)?.bg }"
|
|
||||||
>
|
|
||||||
<span class="cts" :class="getCustomerOpinionInfo(record.customer_opinion)?.color">{{
|
|
||||||
getCustomerOpinionInfo(record.customer_opinion)?.label ?? '-'
|
|
||||||
}}</span>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'platform'" #cell="{ record }">
|
|
||||||
<template v-if="!PLATFORMS.find((item) => item.value === record.platform)"> - </template>
|
|
||||||
<img v-else width="24" height="24" class="rounded-4px" :src="PLATFORMS.find((item) => item.value === record.platform)?.icon" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'compliance_level'" #cell="{ record }">
|
|
||||||
<span class="cts num !color-#6D4CFE">{{
|
|
||||||
record.ai_review?.compliance_level ? `${record.ai_review?.compliance_level}%` : '-'
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'title'" #cell="{ record }">
|
|
||||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
|
||||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="['uploader', 'last_modifier'].includes(column.dataIndex)" #cell="{ record }">
|
|
||||||
{{ record[column.dataIndex].name || record[column.dataIndex].mobile }}
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
#cell="{ record }"
|
|
||||||
v-else-if="
|
|
||||||
['created_at', 'last_modified_at', 'audit_started_at', 'audit_passed_at'].includes(column.dataIndex)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="cts num">{{ exactFormatTime(record[column.dataIndex]) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
|
||||||
<HoverImagePreview :src="record.cover">
|
|
||||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
|
||||||
<template #error>
|
|
||||||
<img :src="icon4" class="w-full h-full" />
|
|
||||||
</template>
|
|
||||||
</a-image>
|
|
||||||
</HoverImagePreview>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
|
||||||
<a-button type="outline" size="mini" @click="onShare(record)" v-if="audit_status === AuditStatus.Passed"
|
|
||||||
>分享</a-button
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
size="mini"
|
|
||||||
@click="onCheck(record)"
|
|
||||||
v-else-if="audit_status === AuditStatus.Pending"
|
|
||||||
>审核</a-button
|
|
||||||
>
|
|
||||||
<a-button type="outline" size="mini" @click="onScan(record)" v-else>查看</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else #cell="{ record }">
|
|
||||||
{{ formatTableField(column, record, true) }}
|
|
||||||
</template>
|
|
||||||
</a-table-column>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
|
|
||||||
<ShareModal ref="shareModalRef" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
|
||||||
import { patchWorkAuditsAudit } from '@/api/all/generationWorkshop';
|
|
||||||
import {
|
|
||||||
AuditStatus,
|
|
||||||
CUSTOMER_OPINION,
|
|
||||||
PLATFORMS,
|
|
||||||
} from '@/views/creative-generation-workshop/manuscript/check-list/constants';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import ShareModal from '@/views/creative-generation-workshop/manuscript/components/share-manuscript-modal/share-modal.vue';
|
|
||||||
import HoverImagePreview from '@/components/hover-image-preview';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-delete.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
import icon4 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
const emits = defineEmits(['edit', 'sorterChange', 'delete', '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 tableRef = ref(null);
|
|
||||||
const shareModalRef = ref(null);
|
|
||||||
|
|
||||||
const handleSorterChange = (column, order) => {
|
|
||||||
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
|
|
||||||
};
|
|
||||||
const onDelete = (item) => {
|
|
||||||
emits('delete', item);
|
|
||||||
};
|
|
||||||
const onShare = (item) => {
|
|
||||||
shareModalRef.value?.open([item.id]);
|
|
||||||
};
|
|
||||||
const onCheck = (item) => {
|
|
||||||
patchWorkAuditsAudit(item.id);
|
|
||||||
slsWithCatch('manuscriptCheckIds', [item.id]);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
};
|
|
||||||
const onScan = (item) => {
|
|
||||||
slsWithCatch('manuscriptCheckIds', [item.id]);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
};
|
|
||||||
const onDetail = (item) => {
|
|
||||||
router.push(`/manuscript/check-list/detail/${item.id}?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>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,259 +0,0 @@
|
|||||||
export const TABLE_COLUMNS1 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户意见',
|
|
||||||
dataIndex: 'customer_opinion',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传时间',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传人员',
|
|
||||||
dataIndex: 'uploader',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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_COLUMNS2 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户意见',
|
|
||||||
dataIndex: 'customer_opinion',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核平台',
|
|
||||||
dataIndex: 'platform',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '合规程度',
|
|
||||||
dataIndex: 'compliance_level',
|
|
||||||
suffix: '%',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核时间',
|
|
||||||
dataIndex: 'audit_started_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '修改人员',
|
|
||||||
dataIndex: 'last_modifier',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export const TABLE_COLUMNS3 = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户意见',
|
|
||||||
dataIndex: 'customer_opinion',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核平台',
|
|
||||||
dataIndex: 'platform',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '通过时间',
|
|
||||||
dataIndex: 'audit_passed_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '修改人员',
|
|
||||||
dataIndex: 'last_modifier',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export 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/platform/icon-dy.png';
|
|
||||||
import icon2 from '@/assets/img/platform/icon-xhs.png';
|
|
||||||
|
|
||||||
export const PLATFORMS = [
|
|
||||||
{
|
|
||||||
label: '小红书',
|
|
||||||
value: 1,
|
|
||||||
icon: icon2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '抖音',
|
|
||||||
value: 2,
|
|
||||||
icon: icon1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CUSTOMER_OPINION = [
|
|
||||||
{
|
|
||||||
label: '待确认',
|
|
||||||
value: 0,
|
|
||||||
bg: '#F2F3F5',
|
|
||||||
color: 'color-#3C4043',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '已确认',
|
|
||||||
value: 1,
|
|
||||||
bg: '#F0EDFF',
|
|
||||||
color: '!color-#6D4CFE',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,201 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="manuscript-check-wrap">
|
|
||||||
<div class="filter-wrap bg-#fff rounded-8px 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 px-24px py-24px flex flex-col">
|
|
||||||
<div
|
|
||||||
class="flex justify-end mb-12px"
|
|
||||||
v-if="[AuditStatus.Pending, AuditStatus.Auditing].includes(query.audit_status)"
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
class="w-fit"
|
|
||||||
size="medium"
|
|
||||||
@click="handleBatchCheck"
|
|
||||||
v-if="query.audit_status === AuditStatus.Pending"
|
|
||||||
>批量审核</a-button
|
|
||||||
>
|
|
||||||
<a-button
|
|
||||||
type="outline"
|
|
||||||
class="w-fit"
|
|
||||||
size="medium"
|
|
||||||
@click="handleBatchView"
|
|
||||||
v-if="query.audit_status === AuditStatus.Auditing"
|
|
||||||
>批量查看</a-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ManuscriptCheckTable
|
|
||||||
:key="query.audit_status"
|
|
||||||
:tableColumns="tableColumns"
|
|
||||||
:rowSelection="rowSelection"
|
|
||||||
:selectedRowKeys="selectedRowKeys"
|
|
||||||
:dataSource="dataSource"
|
|
||||||
:audit_status="query.audit_status"
|
|
||||||
@sorterChange="handleSorterChange"
|
|
||||||
@delete="handleDelete"
|
|
||||||
@edit="handleEdit"
|
|
||||||
@select="handleSelect"
|
|
||||||
@selectAll="handleSelectAll"
|
|
||||||
/>
|
|
||||||
<div v-if="pageInfo.total > 0" class="pagination-row">
|
|
||||||
<a-pagination
|
|
||||||
:total="pageInfo.total"
|
|
||||||
size="mini"
|
|
||||||
show-total
|
|
||||||
show-jumper
|
|
||||||
show-page-size
|
|
||||||
:current="pageInfo.page"
|
|
||||||
:page-size="pageInfo.page_size"
|
|
||||||
@change="onPageChange"
|
|
||||||
@page-size-change="onPageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeleteManuscriptModal ref="deleteManuscriptModalRef" />
|
|
||||||
<ShareManuscriptModal ref="shareManuscriptModalRef" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="jsx" setup>
|
|
||||||
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 ShareManuscriptModal from '@/views/creative-generation-workshop/manuscript/components/share-manuscript-modal';
|
|
||||||
|
|
||||||
import { getWorkAuditsPage, patchWorkAuditsBatchAudit } from '@/api/all/generationWorkshop.ts';
|
|
||||||
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import {
|
|
||||||
AuditStatus,
|
|
||||||
INITIAL_QUERY,
|
|
||||||
AUDIT_STATUS_LIST,
|
|
||||||
TABLE_COLUMNS1,
|
|
||||||
} 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 tableColumns = ref([]);
|
|
||||||
const query = ref(cloneDeep(INITIAL_QUERY));
|
|
||||||
|
|
||||||
const addManuscriptModalRef = ref(null);
|
|
||||||
const deleteManuscriptModalRef = ref(null);
|
|
||||||
const shareManuscriptModalRef = ref(null);
|
|
||||||
|
|
||||||
const getData = async () => {
|
|
||||||
const { page, page_size } = pageInfo.value;
|
|
||||||
const { code, data } = await getWorkAuditsPage({
|
|
||||||
...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;
|
|
||||||
}
|
|
||||||
|
|
||||||
patchWorkAuditsBatchAudit({ ids: selectedRowKeys.value });
|
|
||||||
|
|
||||||
slsWithCatch('manuscriptCheckIds', selectedRowKeys.value);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
};
|
|
||||||
const handleBatchView = () => {
|
|
||||||
if (!selectedRows.value.length) {
|
|
||||||
AMessage.warning('请选择需查看的内容稿件');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
slsWithCatch('manuscriptCheckIds', selectedRowKeys.value);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
<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 visible = ref(false);
|
|
||||||
const action = ref('');
|
|
||||||
const cardInfo = ref(null);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
action.value = '';
|
|
||||||
cardInfo.value = null;
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
const onConfirm = () => {
|
|
||||||
if (action.value === 'exit') {
|
|
||||||
router.push({ name: 'ManuscriptCheckList' });
|
|
||||||
} else {
|
|
||||||
emit('selectCard', cardInfo.value);
|
|
||||||
}
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = (type = 'exit', card = null) => {
|
|
||||||
action.value = type;
|
|
||||||
cardInfo.value = card;
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
<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 visible = ref(false);
|
|
||||||
const workIds = ref([]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
if (workIds.value.length === 1) {
|
|
||||||
router.push({ name: 'ManuscriptCheckList' });
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Drawer, Image } from '@arco-design/web-vue';
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['cardClick'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const visible = ref(false);
|
|
||||||
const dataSource = ref([]);
|
|
||||||
const selectCardInfo = ref({});
|
|
||||||
|
|
||||||
const handleCardClick = (item) => {
|
|
||||||
emit('cardClick', item);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
const open = (data, _selectCardInfo) => {
|
|
||||||
dataSource.value = data;
|
|
||||||
selectCardInfo.value = _selectCardInfo;
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
const onClose = () => {
|
|
||||||
dataSource.value = [];
|
|
||||||
selectCardInfo.value = {};
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
expose({
|
|
||||||
open,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<Drawer
|
|
||||||
title="审核列表"
|
|
||||||
visible={visible.value}
|
|
||||||
width={420}
|
|
||||||
class="check-list-drawer-xt"
|
|
||||||
footer={false}
|
|
||||||
header={false}
|
|
||||||
onCancel={onClose}
|
|
||||||
>
|
|
||||||
<div class="flex justify-between items-center h-56px px-24px">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-3px h-16px rounded-2px bg-#6D4CFE mr-8px"></div>
|
|
||||||
<span class="mr-8px cts bold">批量审核列表</span>
|
|
||||||
<span class="mr-8px cts !lh-22px !text-14px">{`共${dataSource.value.length}个`}</span>
|
|
||||||
</div>
|
|
||||||
<icon-menu-unfold size={16} class="color-##55585F cursor-pointer hover:color-#6D4CFE" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-y-auto px-24px">
|
|
||||||
{dataSource.value.map((item) => (
|
|
||||||
<div
|
|
||||||
onClick={() => handleCardClick(item)}
|
|
||||||
key={item.id}
|
|
||||||
class={`card-item flex rounded-8px bg-#F7F8FA p-8px ${
|
|
||||||
selectCardInfo.value.id === item.id ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
preview={false}
|
|
||||||
src={item.cover}
|
|
||||||
class="!rounded-4px mr-8px"
|
|
||||||
fit="cover"
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon1} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="flex-1 overflow-hidden flex flex-col items-start">
|
|
||||||
<TextOverTips context={item.title} class={`cts !color-#211F24 title mb-4px !text-14px`} />
|
|
||||||
<p class="cts !text-14px">{`合规程度:${
|
|
||||||
item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'
|
|
||||||
}`}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
.check-list-drawer-xt {
|
|
||||||
.arco-drawer-mask {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.arco-drawer {
|
|
||||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
|
|
||||||
.arco-drawer-body {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 0 24px;
|
|
||||||
.cts {
|
|
||||||
color: var(--Text-1, #939499);
|
|
||||||
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
&.bold {
|
|
||||||
color: var(--Text-1, #211f24);
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-item {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: all;
|
|
||||||
&:hover {
|
|
||||||
background-color: #e6e6e8;
|
|
||||||
}
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
border-color: #6d4cfe;
|
|
||||||
background-color: #f0edff;
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
font-family: $font-family-medium !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
export const escapeRegExp = (str: string) => {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FORM_RULES = {
|
|
||||||
title: [{ required: true, message: '请输入标题' }],
|
|
||||||
};
|
|
||||||
export const enumTab = {
|
|
||||||
TEXT: 0,
|
|
||||||
IMAGE: 1,
|
|
||||||
};
|
|
||||||
export const TAB_LIST = [
|
|
||||||
{
|
|
||||||
label: '文本',
|
|
||||||
value: enumTab.TEXT,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// label: '图片',
|
|
||||||
// value: enumTab.IMAGE,
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
export enum Enum_Level {
|
|
||||||
LOW = 0,
|
|
||||||
MEDIUM = 1,
|
|
||||||
HIGH = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LEVEL_MAP = new Map([
|
|
||||||
[Enum_Level.LOW, { label: '低风险', value: 'low_risk_number', color: '#6d4cfe' }],
|
|
||||||
[Enum_Level.MEDIUM, { label: '中风险', value: 'medium_risk_number', color: '#FFAE00' }],
|
|
||||||
[Enum_Level.HIGH, { label: '高风险', value: 'high_risk_number', color: '#F64B31' }],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const RESULT_LIST = [
|
|
||||||
{
|
|
||||||
label: '合规程度',
|
|
||||||
value: 'compliance_level',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.LOW)?.color,
|
|
||||||
suffix: '%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '检验项',
|
|
||||||
value: 'inspection_count',
|
|
||||||
color: '#211F24',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '高风险',
|
|
||||||
value: 'high_risk_number',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.HIGH)?.color,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中风险',
|
|
||||||
value: 'medium_risk_number',
|
|
||||||
color: LEVEL_MAP.get(Enum_Level.MEDIUM)?.color,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="highlight-textarea-container">
|
|
||||||
<a-textarea
|
|
||||||
ref="textareaWrapRef"
|
|
||||||
v-model="inputValue"
|
|
||||||
placeholder="请输入作品描述"
|
|
||||||
:disabled="disabled"
|
|
||||||
show-word-limit
|
|
||||||
:max-length="1000"
|
|
||||||
size="large"
|
|
||||||
class="textarea-input h-full w-full"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="() => (focus = true)"
|
|
||||||
@blur="() => (focus = false)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="textarea-highlight"
|
|
||||||
:class="{ focus: focus }"
|
|
||||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
|
||||||
v-html="highlightedHtml"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
|
||||||
import { escapeRegExp } from './constants';
|
|
||||||
|
|
||||||
// 定义Props类型
|
|
||||||
interface ViolationItem {
|
|
||||||
word: string;
|
|
||||||
risk_level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LevelMapItem {
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: string;
|
|
||||||
prohibitedWords?: ViolationItem[];
|
|
||||||
levelMap?: Map<number, LevelMapItem>;
|
|
||||||
disabled?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// 内部状态管理
|
|
||||||
const inputValue = ref(props.modelValue || '');
|
|
||||||
const scrollTop = ref(0);
|
|
||||||
const focus = ref(false);
|
|
||||||
const textareaWrapRef = ref();
|
|
||||||
let nativeTextarea: HTMLTextAreaElement | null = null;
|
|
||||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
|
||||||
|
|
||||||
// 监听外部modelValue变化
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal !== inputValue.value) {
|
|
||||||
inputValue.value = (newVal || '').slice(0, 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理输入事件
|
|
||||||
const handleInput = (value: string) => {
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = (str: string): string => {
|
|
||||||
if (!isString(str)) return '';
|
|
||||||
return str
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateHighlightedHtml = (): string => {
|
|
||||||
if (!inputValue.value) return '';
|
|
||||||
|
|
||||||
// 获取违禁词列表并按长度倒序排序(避免短词匹配长词)
|
|
||||||
const words = (props.prohibitedWords || [])
|
|
||||||
.filter((item) => item.word && item.risk_level !== undefined)
|
|
||||||
.sort((a, b) => b.word.length - a.word.length);
|
|
||||||
|
|
||||||
if (words.length === 0) {
|
|
||||||
return escapeHtml(inputValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建匹配正则表达式
|
|
||||||
const pattern = new RegExp(`(${words.map((item) => escapeRegExp(item.word)).join('|')})`, 'gi');
|
|
||||||
|
|
||||||
// 替换匹配的违禁词为带样式的span标签
|
|
||||||
return inputValue.value.replace(pattern, (match) => {
|
|
||||||
// 找到对应的违禁词信息
|
|
||||||
const wordInfo = words.find((item) => item.word.toLowerCase() === match.toLowerCase());
|
|
||||||
|
|
||||||
if (!wordInfo) return match;
|
|
||||||
|
|
||||||
// 获取风险等级对应的样式
|
|
||||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
|
||||||
const color = levelStyle?.color || '#F64B31';
|
|
||||||
|
|
||||||
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nativeTextarea =
|
|
||||||
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
|
||||||
document.querySelector('.textarea-input .arco-textarea');
|
|
||||||
|
|
||||||
if (nativeTextarea) {
|
|
||||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
|
||||||
nativeTextarea.addEventListener('compositionstart', handleCompositionUpdate);
|
|
||||||
nativeTextarea.addEventListener('compositionupdate', handleCompositionUpdate);
|
|
||||||
nativeTextarea.addEventListener('compositionend', handleCompositionUpdate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (nativeTextarea) {
|
|
||||||
nativeTextarea.removeEventListener('scroll', handleTextareaScroll);
|
|
||||||
nativeTextarea.removeEventListener('compositionstart', handleCompositionUpdate);
|
|
||||||
nativeTextarea.removeEventListener('compositionupdate', handleCompositionUpdate);
|
|
||||||
nativeTextarea.removeEventListener('compositionend', handleCompositionUpdate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTextareaScroll = (e: Event) => {
|
|
||||||
const _scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
|
|
||||||
|
|
||||||
const highlightElement = document.querySelector('.textarea-highlight');
|
|
||||||
if (highlightElement) {
|
|
||||||
highlightElement.scrollTop = _scrollTop;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompositionUpdate = () => {
|
|
||||||
if (!nativeTextarea) return;
|
|
||||||
// 使用 rAF 等待浏览器把最新字符写入 textarea.value 再读取
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!nativeTextarea) return;
|
|
||||||
const latest = nativeTextarea.value.slice(0, 1000);
|
|
||||||
if (latest !== inputValue.value) {
|
|
||||||
inputValue.value = latest;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.highlight-textarea-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
@mixin textarea-padding {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin textarea-style {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: 400px;
|
|
||||||
border: 1px solid #d7d7d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
resize: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
z-index: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-highlight {
|
|
||||||
@include textarea-style;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
background: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
@include textarea-padding;
|
|
||||||
|
|
||||||
&.focus {
|
|
||||||
border-color: rgb(var(--primary-6)) !important;
|
|
||||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.arco-textarea-wrapper) {
|
|
||||||
@include textarea-style;
|
|
||||||
|
|
||||||
.arco-textarea {
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
color: transparent;
|
|
||||||
caret-color: #211f24 !important;
|
|
||||||
resize: none;
|
|
||||||
@include textarea-padding;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arco-textarea-word-limit {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arco-textarea-disabled {
|
|
||||||
.arco-textarea {
|
|
||||||
background: #f2f3f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
|
||||||
:deep(.textarea-input.composing .arco-textarea) {
|
|
||||||
color: #211f24 !important;
|
|
||||||
-webkit-text-fill-color: #211f24 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,476 +0,0 @@
|
|||||||
<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 HighlightTextarea from './highlight-textarea';
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
getDataLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'filesChange', 'selectImage', 'againCheck', 'startCheck'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const activeTab = ref(enumTab.TEXT);
|
|
||||||
const aiReplaceLoading = ref(false);
|
|
||||||
const formRef = ref(null);
|
|
||||||
const uploadRef = ref(null);
|
|
||||||
const modules = [Navigation];
|
|
||||||
|
|
||||||
const isTextTab = computed(() => activeTab.value === enumTab.TEXT);
|
|
||||||
const aiReview = computed(() => props.modelValue.ai_review);
|
|
||||||
const isDisabled = computed(() => props.checkLoading || aiReplaceLoading.value);
|
|
||||||
|
|
||||||
const onAiReplace = () => {
|
|
||||||
if (aiReplaceLoading.value) return;
|
|
||||||
|
|
||||||
aiReplaceLoading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
const content = props.modelValue.content;
|
|
||||||
const rules = aiReview.value?.violation_items ?? [];
|
|
||||||
const sortedRules = [...rules].sort((a, b) => b.word.length - a.word.length);
|
|
||||||
|
|
||||||
const replacedContent = sortedRules.reduce((result, rule) => {
|
|
||||||
if (!rule.word) return result;
|
|
||||||
|
|
||||||
const escapedWord = escapeRegExp(rule.word);
|
|
||||||
const regex = new RegExp(escapedWord, 'g');
|
|
||||||
|
|
||||||
return result.replace(regex, rule.replace_word);
|
|
||||||
}, content);
|
|
||||||
|
|
||||||
props.modelValue.content = replacedContent;
|
|
||||||
aiReplaceLoading.value = false;
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAgainCheck = () => {
|
|
||||||
if (!isTextTab.value && !props.modelValue.files?.length) {
|
|
||||||
AMessage.warning('请先上传需审核图片');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit('againCheck');
|
|
||||||
};
|
|
||||||
const onReplaceImage = () => {
|
|
||||||
uploadRef.value?.upload?.();
|
|
||||||
};
|
|
||||||
const handleTabClick = (key) => {
|
|
||||||
activeTab.value = key;
|
|
||||||
};
|
|
||||||
const validate = () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
formRef.value?.validate((errors) => {
|
|
||||||
if (errors) {
|
|
||||||
reject();
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const reset = () => {
|
|
||||||
formRef.value?.resetFields?.();
|
|
||||||
formRef.value?.clearValidate?.();
|
|
||||||
aiReplaceLoading.value = false;
|
|
||||||
};
|
|
||||||
const getFileExtension = (filename) => {
|
|
||||||
const match = filename.match(/\.([^.]+)$/);
|
|
||||||
return match ? match[1].toLowerCase() : '';
|
|
||||||
};
|
|
||||||
const handleSelectImage = (item) => {
|
|
||||||
emit('selectImage', item);
|
|
||||||
};
|
|
||||||
const onDeleteImage = (e, item, index) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const _newFiles = cloneDeep(props.modelValue.files);
|
|
||||||
_newFiles.splice(index, 1);
|
|
||||||
|
|
||||||
if (item.id === props.selectedImageInfo.id) {
|
|
||||||
emit('selectImage', _newFiles.length ? _newFiles[0] : {});
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('filesChange', _newFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderUpload = (UploadBtn, action = 'upload') => {
|
|
||||||
return (
|
|
||||||
<Upload
|
|
||||||
ref={uploadRef}
|
|
||||||
action="/"
|
|
||||||
draggable
|
|
||||||
class="w-fit"
|
|
||||||
custom-request={(option) => uploadImage(option, action)}
|
|
||||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
|
||||||
show-file-list={false}
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
'upload-button': () => <UploadBtn />,
|
|
||||||
}}
|
|
||||||
</Upload>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadImage = async (option, action = 'upload') => {
|
|
||||||
const {
|
|
||||||
fileItem: { file },
|
|
||||||
} = option;
|
|
||||||
|
|
||||||
const { name, size, type } = file;
|
|
||||||
const response = await getImagePreSignedUrl({ suffix: getFileExtension(name) });
|
|
||||||
const { file_name, upload_url, file_url } = response?.data;
|
|
||||||
|
|
||||||
const blob = new Blob([file], { type });
|
|
||||||
await axios.put(upload_url, blob, {
|
|
||||||
headers: { 'Content-Type': type },
|
|
||||||
});
|
|
||||||
|
|
||||||
const _file = {
|
|
||||||
url: file_url,
|
|
||||||
name: file_name,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _newFiles =
|
|
||||||
action === 'replaceImage'
|
|
||||||
? props.modelValue.files.map((item) => {
|
|
||||||
if (item.url === props.selectedImageInfo.url) {
|
|
||||||
return _file;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
: [...props.modelValue.files, _file];
|
|
||||||
|
|
||||||
emit('filesChange', _newFiles);
|
|
||||||
emit('selectImage', _file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooterRow = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button class="mr-12px" size="medium" onClick={onAgainCheck} disabled={isDisabled.value}>
|
|
||||||
再次审核
|
|
||||||
</Button>
|
|
||||||
{isTextTab.value ? (
|
|
||||||
<Button size="medium" type="outline" class="w-123px check-btn" onClick={onAiReplace} disabled={isDisabled.value}>
|
|
||||||
{aiReplaceLoading.value ? (
|
|
||||||
<>
|
|
||||||
<IconLoading size={14} />
|
|
||||||
<span class="ml-8px check-text">AI生成中</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<img src={icon1} width={14} height={14} />
|
|
||||||
<span class="ml-8px check-text">替换违禁词</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div class="w-88px">
|
|
||||||
{renderUpload(
|
|
||||||
<Button size="medium" type="outline">
|
|
||||||
图片替换
|
|
||||||
</Button>,
|
|
||||||
'replaceImage',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderTextForm = () => {
|
|
||||||
return (
|
|
||||||
<Form ref={formRef} model={props.modelValue} rules={FORM_RULES} layout="vertical" auto-label-width>
|
|
||||||
<FormItem label="标题" field="title" required>
|
|
||||||
<Input
|
|
||||||
v-model={props.modelValue.title}
|
|
||||||
placeholder="请输入标题"
|
|
||||||
size="large"
|
|
||||||
maxLength={30}
|
|
||||||
show-word-limit
|
|
||||||
disabled={isDisabled.value}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="作品描述" field="content" class="flex-1 content-form-item">
|
|
||||||
<HighlightTextarea
|
|
||||||
v-model={props.modelValue.content}
|
|
||||||
disabled={isDisabled.value}
|
|
||||||
prohibitedWords={aiReview.value?.violation_items ?? []}
|
|
||||||
levelMap={LEVEL_MAP}
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderImageForm = () => {
|
|
||||||
if (props.modelValue.files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div class="w-full h-full py-16px flex justify-center">
|
|
||||||
<div class="w-380px flex flex-col justify-center">
|
|
||||||
<Image
|
|
||||||
src={props.selectedImageInfo.url}
|
|
||||||
width={370}
|
|
||||||
height={370}
|
|
||||||
preview={false}
|
|
||||||
class="flex items-center justify-center mb-8px"
|
|
||||||
fit="contain"
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon4} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="swiper-wrap h-78px">
|
|
||||||
<Swiper
|
|
||||||
spaceBetween={16}
|
|
||||||
modules={modules}
|
|
||||||
slidesPerView="auto"
|
|
||||||
navigation={{
|
|
||||||
nextEl: '.swiper-button-next',
|
|
||||||
prevEl: '.swiper-button-prev',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.modelValue.files.map((item, index) => (
|
|
||||||
<SwiperSlide
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => handleSelectImage(item)}
|
|
||||||
class={`!h-48px !w-48px !relative bg-#F7F8FA cursor-pointer !flex items-center ${
|
|
||||||
item.id === props.selectedImageInfo.id ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div class="group relative w-full h-full rounded-5px">
|
|
||||||
<Image
|
|
||||||
width={'100%'}
|
|
||||||
height={'100%'}
|
|
||||||
src={item.url}
|
|
||||||
class="!rounded-4px"
|
|
||||||
fit="contain"
|
|
||||||
preview={false}
|
|
||||||
v-slots={{
|
|
||||||
error: () => <img src={icon4} class="w-full h-full" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<icon-close-circle-fill
|
|
||||||
size={16}
|
|
||||||
class="close-icon absolute top--8px right--8px hidden cursor-pointer color-#737478 hover:!color-#211F24 z-50"
|
|
||||||
onClick={(e) => onDeleteImage(e, item, index)}
|
|
||||||
/>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
<div class="swiper-box swiper-button-prev">
|
|
||||||
<img src={icon5} class="w-8px h-17px" />
|
|
||||||
</div>
|
|
||||||
<div class="swiper-box swiper-button-next">
|
|
||||||
<img src={icon5} class="w-8px h-17px rotate-180" />
|
|
||||||
</div>
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
|
||||||
<div class="flex justify-center mb-16px">
|
|
||||||
{renderUpload(
|
|
||||||
<div class="upload-box">
|
|
||||||
<icon-plus size="14" class="mb-16px color-#3C4043" />
|
|
||||||
<span class="cts !color-#211F24">上传图片</span>
|
|
||||||
</div>,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="cts">上传要审核的图片素材</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCheckSuccessBox = () => {
|
|
||||||
if (!aiReview.value?.violation_items?.length) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center mb-16px">
|
|
||||||
{RESULT_LIST.map((item, index) => (
|
|
||||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
|
||||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
|
||||||
item.suffix || ''
|
|
||||||
}`}</span>
|
|
||||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
|
||||||
<div class="mb-24px relative w-fit">
|
|
||||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
|
||||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center h-138px justify-center">
|
|
||||||
<img src={icon3} width={72} height={72} class="mb-12px" />
|
|
||||||
<span class="cts !color-#25C883">
|
|
||||||
{isTextTab.value ? '恭喜,您的文案中没有检测出违禁词' : '恭喜,您的图片中没有检测出违禁内容'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="flex items-center mb-16px">
|
|
||||||
{RESULT_LIST.map((item, index) => (
|
|
||||||
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
|
|
||||||
<span class="s1" style={{ color: item.color }}>{`${aiReview.value?.[item.value] ?? '-'}${
|
|
||||||
item.suffix || ''
|
|
||||||
}`}</span>
|
|
||||||
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
|
|
||||||
<div class=" mb-8px relative w-fit">
|
|
||||||
<span class="ai-text relative z-2">AI 审核建议</span>
|
|
||||||
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
|
|
||||||
</div>
|
|
||||||
{aiReview.value?.suggestion?.map((item, index) => (
|
|
||||||
<p class="cts !color-#55585F !text-12px !lh-20px" key={index}>{`${index + 1}. ${item}`}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="forbid-word-box flex-1 flex">
|
|
||||||
<div class="left mr-32px w-56px">
|
|
||||||
<p class="mb-12px cts !text-12px">违禁词</p>
|
|
||||||
{aiReview.value?.violation_items?.map((item, index) => (
|
|
||||||
<TextOverTips
|
|
||||||
context={item.word}
|
|
||||||
class="mb-12px cts"
|
|
||||||
style={{ color: LEVEL_MAP.get(item.risk_level)?.color }}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="right flex-1 overflow-hidden">
|
|
||||||
<p class="mb-12px cts !text-12px">解释</p>
|
|
||||||
{aiReview.value?.violation_items?.map((item, index) => (
|
|
||||||
<TextOverTips context={item.reason} class="mb-12px" key={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRightBox = () => {
|
|
||||||
if (props.checkLoading) {
|
|
||||||
return (
|
|
||||||
<div class="right-box">
|
|
||||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
|
||||||
<Spin
|
|
||||||
loading={true}
|
|
||||||
tip={`${isTextTab.value ? '文本' : '图片'}检测中`}
|
|
||||||
size={72}
|
|
||||||
class="h-298px !flex flex-col justify-center items-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="right-box">
|
|
||||||
<p class="cts bold !text-16px !lh-24px !color-#211F24 mb-16px">审核结果</p>
|
|
||||||
{props.getDataLoading ? (
|
|
||||||
<Spin loading={true} size={72} class="h-298px !flex justify-center items-center" />
|
|
||||||
) : (
|
|
||||||
renderCheckSuccessBox()
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expose({
|
|
||||||
validate,
|
|
||||||
reset,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<div class="h-full w-full px-24px pt-16px pb-24px content-wrap flex">
|
|
||||||
<div class="flex-2 left-box mr-24px flex flex-col">
|
|
||||||
<div class="flex-1 mb-12px rounded-8px border-1px pt-8px flex flex-col pb-16px bg-#F7F8FA border-#E6E6E8 border-solid">
|
|
||||||
<Tabs v-model={activeTab.value} onTabClick={handleTabClick} class="mb-16px">
|
|
||||||
{TAB_LIST.map((item) => (
|
|
||||||
<TabPane
|
|
||||||
key={item.value}
|
|
||||||
v-slots={{
|
|
||||||
title: () => (
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<span>{item.label}</span>
|
|
||||||
{
|
|
||||||
// activeTab.value === item.value && aiReview.value?.violation_items.length > 0 && (
|
|
||||||
// <icon-exclamation-circle-fill size={14} class="color-#F64B31 absolute right--10px top-0" />
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
<div class="flex-1 px-16px">{isTextTab.value ? renderTextForm() : renderImageForm()}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-end">{renderFooterRow()}</div>
|
|
||||||
</div>
|
|
||||||
{renderRightBox()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
.content-wrap {
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.check-btn {
|
|
||||||
.check-text {
|
|
||||||
background: linear-gradient(84deg, #266cff 4.57%, #a15af0 84.93%);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-box {
|
|
||||||
:deep(.arco-tabs) {
|
|
||||||
.arco-tabs-nav {
|
|
||||||
.arco-tabs-tab {
|
|
||||||
height: 40px;
|
|
||||||
// padding: 0 8px;
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-tabs-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.arco-form) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.arco-form-item {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
.arco-form-item-label-col {
|
|
||||||
.arco-form-item-label {
|
|
||||||
color: #939499;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content-form-item {
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.arco-form-item-wrapper-col {
|
|
||||||
flex: 1;
|
|
||||||
.arco-form-item-content-wrapper,
|
|
||||||
.arco-form-item-content,
|
|
||||||
.arco-textarea-wrapper {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.upload-box {
|
|
||||||
display: flex;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px dashed var(--Border-1, #d7d7d9);
|
|
||||||
background: var(--BG-200, #f2f3f5);
|
|
||||||
&:hover {
|
|
||||||
background: var(--Primary-1, #e6e6e8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.swiper-wrap {
|
|
||||||
:deep(.swiper) {
|
|
||||||
height: 100%;
|
|
||||||
.swiper-wrapper {
|
|
||||||
align-items: center;
|
|
||||||
.swiper-slide {
|
|
||||||
transition: all;
|
|
||||||
&.active {
|
|
||||||
width: 60px !important;
|
|
||||||
height: 60px !important;
|
|
||||||
.group {
|
|
||||||
border: 2px solid var(--Brand-6, #6d4cfe);
|
|
||||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.close-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-box {
|
|
||||||
position: absolute;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
transition: all;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
&.swiper-button-prev {
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
&.swiper-button-next {
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.swiper-button-disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.right-box {
|
|
||||||
border: 1px solid #e6e6e8;
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 100%;
|
|
||||||
.s1 {
|
|
||||||
font-family: $font-family-manrope-medium;
|
|
||||||
font-size: 24px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 32px; /* 133.333% */
|
|
||||||
}
|
|
||||||
.result-item {
|
|
||||||
&:first-child {
|
|
||||||
position: relative;
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
right: 0;
|
|
||||||
width: 1px;
|
|
||||||
height: 32px;
|
|
||||||
background: var(--Border-1, #d7d7d9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.suggestion-box {
|
|
||||||
.ai-text {
|
|
||||||
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
color: #211f24;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
.forbid-word-box {
|
|
||||||
:deep(.overflow-text) {
|
|
||||||
&.level0 {
|
|
||||||
color: #6d4cfe;
|
|
||||||
}
|
|
||||||
&.level2 {
|
|
||||||
color: #f64b31;
|
|
||||||
}
|
|
||||||
&.level1 {
|
|
||||||
color: #ffae00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
<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">{`合规程度:${item.ai_review?.compliance_level ? `${item.ai_review?.compliance_level}%` : '-'}`}</p>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div class="swiper-box swiper-button-prev">
|
|
||||||
<div class="swiper-button">
|
|
||||||
<img src={icon2} class="w-16px h-16px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-box swiper-button-next">
|
|
||||||
<div class="swiper-button">
|
|
||||||
<img src={icon2} class="w-16px h-16px rotate-180" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class="platform-row py-16px flex items-center px-24px">
|
|
||||||
<span class="mr-16px cts !color-#211F24">审核平台选择</span>
|
|
||||||
<div class="flex items-center">
|
|
||||||
{PLATFORMS.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
onClick={() => {
|
|
||||||
emit('platformChange', item.value);
|
|
||||||
}}
|
|
||||||
class={`w-100px flex items-center mr-16px py-8px px-12px flex border-1px border-solid border-transparent transition-all
|
|
||||||
items-center rounded-8px cursor-pointer bg-#F2F3F5 hover:bg-#E6E6E8 ${
|
|
||||||
props.selectCardInfo.platform === item.value ? '!bg-#F0EDFF !border-#6D4CFE' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img src={item.icon} alt="" width={20} height={20} class="mr-4px" />
|
|
||||||
<span class={`cts !color-#211F24 ${props.selectCardInfo.platform === item.value ? 'bold' : ''}`}>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,266 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Button, Message as AMessage, Spin } from '@arco-design/web-vue';
|
|
||||||
import CancelCheckModal from './cancel-check-modal.vue';
|
|
||||||
import CheckSuccessModal from './check-success-modal.vue';
|
|
||||||
import HeaderCard from './components/header-card';
|
|
||||||
import ContentCard from './components/content-card';
|
|
||||||
import CheckListDrawer from './components/check-list-drawer';
|
|
||||||
|
|
||||||
import { slsWithCatch, rlsWithCatch, glsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import useGetAiReviewResult from '@/hooks/useGetAiReviewResult.ts';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
import {
|
|
||||||
patchWorkAuditsAudit,
|
|
||||||
patchWorkAuditsBatchAudit,
|
|
||||||
putWorkAuditsUpdate,
|
|
||||||
putWorkAuditsAuditPass,
|
|
||||||
getWorkAuditsDetail,
|
|
||||||
getWorkAuditsBatchDetail,
|
|
||||||
postWorkAuditsAiReview,
|
|
||||||
getWorkAuditsAiReviewResult,
|
|
||||||
} from '@/api/all/generationWorkshop.ts';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
|
|
||||||
const workIds = ref([]);
|
|
||||||
const isSaved = ref(false);
|
|
||||||
const dataSource = ref([]);
|
|
||||||
const remoteDataSource = ref([]);
|
|
||||||
const cancelCheckModalRef = ref(null);
|
|
||||||
const checkSuccessModalRef = ref(null);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const contentCardRef = ref(null);
|
|
||||||
const checkListDrawerRef = ref(null);
|
|
||||||
const getDataLoading = ref(false);
|
|
||||||
|
|
||||||
const selectCardInfo = ref({});
|
|
||||||
const selectedImageInfo = ref(null);
|
|
||||||
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleStartCheck, handleAgainCheck, ticket, checkLoading, resetAiReviewInfo } = useGetAiReviewResult({
|
|
||||||
cardInfo: selectCardInfo,
|
|
||||||
startAiReviewFn: postWorkAuditsAiReview,
|
|
||||||
getAiReviewResultFn: getWorkAuditsAiReviewResult,
|
|
||||||
updateAiReview(ai_review) {
|
|
||||||
selectCardInfo.value.ai_review = ai_review;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ name: 'ManuscriptCheckList' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeCard = (item) => {
|
|
||||||
contentCardRef.value.reset();
|
|
||||||
isSaved.value = false;
|
|
||||||
submitLoading.value = false;
|
|
||||||
getDataLoading.value = false;
|
|
||||||
checkLoading.value = false;
|
|
||||||
resetAiReviewInfo();
|
|
||||||
|
|
||||||
const { files = [], ai_review } = item;
|
|
||||||
selectCardInfo.value = cloneDeep(item);
|
|
||||||
selectedImageInfo.value = cloneDeep(files?.[0] ?? {});
|
|
||||||
|
|
||||||
if (isEmpty(ai_review)) {
|
|
||||||
handleStartCheck();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCardClick = async (item) => {
|
|
||||||
const isModified = await isSelectCardModified();
|
|
||||||
if (isModified) {
|
|
||||||
cancelCheckModalRef.value?.open('toggle', item);
|
|
||||||
} else {
|
|
||||||
onChangeCard(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectImage = (item) => {
|
|
||||||
selectedImageInfo.value = cloneDeep(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWorkAudits = async () => {
|
|
||||||
try {
|
|
||||||
getDataLoading.value = true;
|
|
||||||
const { code, data } = await getWorkAuditsBatchDetail({ ids: workIds.value });
|
|
||||||
if (code === 200) {
|
|
||||||
const _data = (data ?? []).map((item) => ({
|
|
||||||
...item,
|
|
||||||
platform: item.platform === 0 ? 1 : item.platform,
|
|
||||||
}));
|
|
||||||
|
|
||||||
dataSource.value = _data;
|
|
||||||
remoteDataSource.value = cloneDeep(_data);
|
|
||||||
|
|
||||||
const _firstCard = _data?.[0] ?? {};
|
|
||||||
const { id, ai_review } = _firstCard;
|
|
||||||
|
|
||||||
selectCardInfo.value = cloneDeep(_firstCard);
|
|
||||||
selectedImageInfo.value = cloneDeep(_firstCard.files?.[0] ?? {});
|
|
||||||
|
|
||||||
if (isEmpty(ai_review)) {
|
|
||||||
handleStartCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
getDataLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectCardModified = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const _item = remoteDataSource.value.find((item) => item.id === selectCardInfo.value.id);
|
|
||||||
resolve(!isEqual(selectCardInfo.value, _item) && !isSaved.value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onPlatformChange = (platform) => {
|
|
||||||
selectCardInfo.value.platform = platform;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExit = async () => {
|
|
||||||
const isModified = await isSelectCardModified();
|
|
||||||
if (isModified) {
|
|
||||||
cancelCheckModalRef.value?.open();
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onSave = async () => {
|
|
||||||
if (!selectCardInfo.value.title) {
|
|
||||||
AMessage.warning('标题不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
contentCardRef.value?.validate().then(async () => {
|
|
||||||
const { code, data } = await putWorkAuditsUpdate(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('manuscriptCheckIds', 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 putWorkAuditsAuditPass(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('manuscriptCheckIds')?.split(',') ?? [];
|
|
||||||
getWorkAudits();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
rlsWithCatch('manuscriptCheckIds');
|
|
||||||
});
|
|
||||||
|
|
||||||
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 icon mr-4px" />
|
|
||||||
<span class="cts !color-#211F24">审核列表</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden bg-#fff rounded-8px ">
|
|
||||||
<HeaderCard
|
|
||||||
dataSource={dataSource.value}
|
|
||||||
selectCardInfo={selectCardInfo.value}
|
|
||||||
onCardClick={onCardClick}
|
|
||||||
onPlatformChange={onPlatformChange}
|
|
||||||
/>
|
|
||||||
<section class="flex-1 overflow-hidden">
|
|
||||||
<ContentCard
|
|
||||||
ref={contentCardRef}
|
|
||||||
v-model={selectCardInfo.value}
|
|
||||||
selectCardInfo={selectCardInfo.value}
|
|
||||||
onFilesChange={onFilesChange}
|
|
||||||
selectedImageInfo={selectedImageInfo.value}
|
|
||||||
onSelectImage={onSelectImage}
|
|
||||||
checkLoading={checkLoading.value}
|
|
||||||
getDataLoading={getDataLoading.value}
|
|
||||||
onAgainCheck={onAgainCheck}
|
|
||||||
onStartCheck={handleStartCheck}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderFooterRow()}
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<CancelCheckModal ref={cancelCheckModalRef} onSelectCard={onChangeCard} />
|
|
||||||
<CheckSuccessModal ref={checkSuccessModalRef} />
|
|
||||||
<CheckListDrawer ref={checkListDrawerRef} onCardClick={onCardClick} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
$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);
|
|
||||||
&:hover {
|
|
||||||
.icon,
|
|
||||||
.cts {
|
|
||||||
color: #6d4cfe !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,373 +0,0 @@
|
|||||||
<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 { getProjectList } from '@/api/all/propertyMarketing';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
|
|
||||||
import { getImagePreSignedUrl, getVideoPreSignedUrl } from '@/api/all/common';
|
|
||||||
|
|
||||||
// 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 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 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 getVideoPreSignedUrl({ 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 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 },
|
|
||||||
});
|
|
||||||
|
|
||||||
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="textarea-box !w-784px"
|
|
||||||
show-word-limit
|
|
||||||
max-length={1000}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textarea-box {
|
|
||||||
:deep(.arco-textarea) {
|
|
||||||
height: 140px;
|
|
||||||
max-height: 298px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
export const INITIAL_FORM = {
|
|
||||||
audit_status: '',
|
|
||||||
sort_column: undefined,
|
|
||||||
sort_order: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TABLE_COLUMNS = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 220,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核状态',
|
|
||||||
dataIndex: 'audit_status',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 160,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,296 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
Table,
|
|
||||||
Modal,
|
|
||||||
TableColumn,
|
|
||||||
Checkbox,
|
|
||||||
Pagination,
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
Notification,
|
|
||||||
} from '@arco-design/web-vue';
|
|
||||||
import CommonSelect from '@/components/common-select';
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import ShareModal from '@/views/creative-generation-workshop/manuscript/components/share-manuscript-modal/share-modal';
|
|
||||||
|
|
||||||
import { INITIAL_FORM, TABLE_COLUMNS } from './constants';
|
|
||||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
|
||||||
import { CHECK_STATUS, EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
|
||||||
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
|
|
||||||
import { getWorksPage, getWriterLinksGenerate } from '@/api/all/generationWorkshop.ts';
|
|
||||||
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const {
|
|
||||||
selectedRowKeys,
|
|
||||||
selectedRows,
|
|
||||||
dataSource,
|
|
||||||
pageInfo,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
rowSelection,
|
|
||||||
handleSelect,
|
|
||||||
handleSelectAll,
|
|
||||||
DEFAULT_PAGE_INFO,
|
|
||||||
} = useTableSelectionWithPagination({
|
|
||||||
onPageChange: () => {
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
onPageSizeChange: () => {
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const visible = ref(false);
|
|
||||||
const query = ref(cloneDeep(INITIAL_FORM));
|
|
||||||
const tableRef = ref(null);
|
|
||||||
const shareModalRef = ref(null);
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
query.value = cloneDeep(INITIAL_FORM);
|
|
||||||
pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO);
|
|
||||||
selectedRowKeys.value = [];
|
|
||||||
selectedRows.value = [];
|
|
||||||
dataSource.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = async () => {
|
|
||||||
const { page, page_size } = pageInfo.value;
|
|
||||||
const { code, data } = await getWorksPage({
|
|
||||||
...query.value,
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
});
|
|
||||||
if (code === 200) {
|
|
||||||
dataSource.value = data?.data ?? [];
|
|
||||||
pageInfo.value.total = data.total;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleSearch = (value) => {
|
|
||||||
query.value.audit_status = value;
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
const reload = () => {
|
|
||||||
pageInfo.value.page = 1;
|
|
||||||
getData();
|
|
||||||
};
|
|
||||||
const open = () => {
|
|
||||||
getData();
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
const onClose = () => {
|
|
||||||
visible.value = false;
|
|
||||||
reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderColumn = () => {
|
|
||||||
return TABLE_COLUMNS.map((column) => (
|
|
||||||
<TableColumn
|
|
||||||
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
|
|
||||||
v-slots={{
|
|
||||||
title: () => (
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px">{column.title}</span>
|
|
||||||
{column.tooltip && (
|
|
||||||
<Tooltip content={column.tooltip} position="top">
|
|
||||||
<IconQuestionCircle class="tooltip-icon color-#737478" size={16} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ record }) => renderCell(record),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShare = () => {
|
|
||||||
shareModalRef.value?.open(selectedRowKeys.value);
|
|
||||||
};
|
|
||||||
const handleSorterChange = (column, order) => {
|
|
||||||
query.value.sort_column = column;
|
|
||||||
query.value.sort_order = order === 'ascend' ? 'asc' : 'desc';
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
const getStatusInfo = (audit_status) => {
|
|
||||||
return CHECK_STATUS.find((v) => v.id === audit_status) ?? {};
|
|
||||||
};
|
|
||||||
expose({
|
|
||||||
open,
|
|
||||||
});
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
v-model:visible={visible.value}
|
|
||||||
title="分享内容稿件"
|
|
||||||
width="920px"
|
|
||||||
onClose={onClose}
|
|
||||||
unmount-on-close
|
|
||||||
modal-class="share-manuscript-modal"
|
|
||||||
v-slots={{
|
|
||||||
footer: () => (
|
|
||||||
<div class="flex justify-between w-full items-center">
|
|
||||||
<p class="cts color-#737478">
|
|
||||||
已选择 <span class="cts color-#211F24 bold">{selectedRows.value.length}</span> 个
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button size="medium" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
class="ml-16px"
|
|
||||||
size="medium"
|
|
||||||
onClick={onShare}
|
|
||||||
disabled={!selectedRows.value.length}
|
|
||||||
>
|
|
||||||
分享
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col h-100%">
|
|
||||||
<div class="filter-row-item mb-16px">
|
|
||||||
<span class="cts text-#211f24 !text-14px mr-12px">审核状态</span>
|
|
||||||
<CommonSelect
|
|
||||||
placeholder="全部"
|
|
||||||
options={CHECK_STATUS}
|
|
||||||
v-model={query.value.audit_status}
|
|
||||||
class="!w-200px"
|
|
||||||
multiple={false}
|
|
||||||
onChange={handleSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Table
|
|
||||||
ref={tableRef}
|
|
||||||
data={dataSource.value}
|
|
||||||
row-key="id"
|
|
||||||
column-resizable
|
|
||||||
row-selection={rowSelection.value}
|
|
||||||
selected-keys={selectedRowKeys.value}
|
|
||||||
pagination={false}
|
|
||||||
scroll={{ x: '100%', y: '100%' }}
|
|
||||||
class="overflow-hidden"
|
|
||||||
bordered
|
|
||||||
onSorterChange={handleSorterChange}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
onSelectAll={handleSelectAll}
|
|
||||||
v-slots={{
|
|
||||||
empty: () => <NoData />,
|
|
||||||
columns: () => (
|
|
||||||
<>
|
|
||||||
{TABLE_COLUMNS.map((column) => (
|
|
||||||
<TableColumn
|
|
||||||
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
|
|
||||||
v-slots={{
|
|
||||||
title: () => (
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px bold color-#211F24">{column.title}</span>
|
|
||||||
{column.tooltip && (
|
|
||||||
<Tooltip content={column.tooltip} position="top">
|
|
||||||
<IconQuestionCircle class="tooltip-icon color-#737478" size={16} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ record }) => {
|
|
||||||
if (column.dataIndex === 'audit_status') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex items-center w-fit h-24px px-8px rounded-2px"
|
|
||||||
style={{ backgroundColor: getStatusInfo(record.audit_status).backgroundColor }}
|
|
||||||
>
|
|
||||||
<span class="cts s1 bold" style={{ color: getStatusInfo(record.audit_status).color }}>
|
|
||||||
{getStatusInfo(record.audit_status).name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (column.dataIndex === 'title') {
|
|
||||||
return <TextOverTips context={record.title} />;
|
|
||||||
} else if (column.dataIndex === 'type') {
|
|
||||||
return (
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
src={record.type === EnumManuscriptType.Image ? icon2 : icon3}
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class={`cts !text-14px !lh-22px ${
|
|
||||||
record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{record.type === EnumManuscriptType.Image ? '图文' : '视频'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (column.dataIndex === 'last_modified_at') {
|
|
||||||
return (
|
|
||||||
<span class="cts num">
|
|
||||||
{exactFormatTime(
|
|
||||||
record.last_modified_at,
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return formatTableField(column, record, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{pageInfo.value.total > 0 && (
|
|
||||||
<div class="flex justify-end mt-16px">
|
|
||||||
<Pagination
|
|
||||||
total={pageInfo.value.total}
|
|
||||||
size="mini"
|
|
||||||
show-total
|
|
||||||
show-jumper
|
|
||||||
show-page-size
|
|
||||||
current={pageInfo.value.page}
|
|
||||||
page-size={pageInfo.value.page_size}
|
|
||||||
onChange={onPageChange}
|
|
||||||
onPageSizeChange={onPageSizeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<ShareModal ref={shareModalRef} onClose={onClose} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Modal, Form, FormItem, Input, Button, Message as AMessage } from '@arco-design/web-vue';
|
|
||||||
import CommonSelect from '@/components/common-select';
|
|
||||||
|
|
||||||
import { useClipboard } from '@vueuse/core';
|
|
||||||
import { postShareLinksGenerate } from '@/api/all/generationWorkshop';
|
|
||||||
import { generateFullUrl } from '@/utils/tools';
|
|
||||||
|
|
||||||
const INITIAL_FORM = {
|
|
||||||
work_ids: [],
|
|
||||||
days: 1,
|
|
||||||
receiver: '',
|
|
||||||
};
|
|
||||||
const OPTIONS = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: '1天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '3天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '7天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '15天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: '30天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: '60天',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['close'],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const visible = ref(false);
|
|
||||||
const formRef = ref(null);
|
|
||||||
const formData = ref(cloneDeep(INITIAL_FORM));
|
|
||||||
const loading = ref(false);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { copy } = useClipboard({ source: formData.value.link });
|
|
||||||
const rules = {
|
|
||||||
receiver: [{ required: true, message: '请输入分享对象' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
loading.value = false;
|
|
||||||
formData.value = cloneDeep(INITIAL_FORM);
|
|
||||||
formRef.value?.resetFields?.();
|
|
||||||
formRef.value?.clearValidate?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
visible.value = false;
|
|
||||||
reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGenerateLink = () => {
|
|
||||||
formRef.value.validate().then(async (errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const { code, data } = await postShareLinksGenerate(formData.value);
|
|
||||||
if (code === 200) {
|
|
||||||
onClose();
|
|
||||||
|
|
||||||
const url = router.resolve({
|
|
||||||
path: `/explore/list/${data.code}`,
|
|
||||||
}).href;
|
|
||||||
copy(generateFullUrl(url));
|
|
||||||
AMessage.success('链接已复制!');
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const open = (workIds) => {
|
|
||||||
formData.value.work_ids = workIds;
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
expose({
|
|
||||||
open,
|
|
||||||
});
|
|
||||||
return () => (
|
|
||||||
<Modal
|
|
||||||
v-model:visible={visible.value}
|
|
||||||
title="分享内容稿件"
|
|
||||||
width="480px"
|
|
||||||
onClose={onClose}
|
|
||||||
unmount-on-close
|
|
||||||
auto-label-width
|
|
||||||
v-slots={{
|
|
||||||
footer: () => (
|
|
||||||
<>
|
|
||||||
<Button size="medium" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" class="ml-16px" size="medium" onClick={onGenerateLink} disabled={loading.value}>
|
|
||||||
{loading.value ? '生成中...' : '生成链接'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form ref={formRef} rules={rules} model={formData.value} auto-label-width>
|
|
||||||
<FormItem label="有效期" prop="days" row-class="!items-center">
|
|
||||||
<CommonSelect
|
|
||||||
v-model={formData.value.days}
|
|
||||||
options={OPTIONS}
|
|
||||||
multiple={false}
|
|
||||||
placeholder="请选择有效期"
|
|
||||||
size="large"
|
|
||||||
class="!w-240px"
|
|
||||||
allClear={false}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="分享对象"
|
|
||||||
prop="receiver"
|
|
||||||
row-class="!items-center"
|
|
||||||
v-slots={{
|
|
||||||
label: () => (
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span>分享对象</span>
|
|
||||||
<a-tooltip content="可填写客户名称、昵称等,非必填" position="top">
|
|
||||||
<icon-question-circle class="tooltip-icon color-#737478 ml-4px" size="14" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input v-model={formData.value.receiver} class="!w-240px" size="large" placeholder="请输入分享对象" />
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
.share-manuscript-modal {
|
|
||||||
.cts {
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
&.num {
|
|
||||||
font-family: $font-family-manrope-regular;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter-row-item {
|
|
||||||
.label {
|
|
||||||
color: var(--Text-1, #211f24);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-modal-body {
|
|
||||||
height: 464px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
.arco-scrollbar-track {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.arco-table {
|
|
||||||
.arco-table-container {
|
|
||||||
.arco-table-element {
|
|
||||||
thead {
|
|
||||||
.arco-table-tr {
|
|
||||||
.arco-table-th {
|
|
||||||
.arco-table-cell {
|
|
||||||
padding: 10px 16px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
.arco-table-tr {
|
|
||||||
.arco-table-td {
|
|
||||||
.arco-table-cell {
|
|
||||||
padding: 6px 16px;
|
|
||||||
.arco-table-cell-content,
|
|
||||||
.arco-table-td-content {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-pagination {
|
|
||||||
.arco-pagination-total,
|
|
||||||
.arco-pagination-jumper-prepend {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.arco-pagination-jumper-prepend {
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
<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 { getWorksDetail } from '@/api/all/generationWorkshop';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
|
|
||||||
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
|
|
||||||
const DEFAULT_SOURCE_INFO = {
|
|
||||||
title: '内容稿件列表',
|
|
||||||
routeName: 'ManuscriptList',
|
|
||||||
};
|
|
||||||
const SOURCE_MAP = new Map([['check', { title: '内容稿件审核', routeName: 'ManuscriptCheckList' }]]);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
const workId = ref(route.params.id);
|
|
||||||
const { source, audit_status } = route.query;
|
|
||||||
|
|
||||||
// 视频播放相关状态
|
|
||||||
const isPlaying = ref(false);
|
|
||||||
const videoRef = ref(null);
|
|
||||||
const videoUrl = ref('');
|
|
||||||
const coverImageUrl = ref('');
|
|
||||||
const isVideoLoaded = ref(false);
|
|
||||||
const dataSource = ref({});
|
|
||||||
const images = ref([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const isVideo = computed(() => dataSource.value.type === EnumManuscriptType.Video);
|
|
||||||
const sourceInfo = computed(() => SOURCE_MAP.get(source) ?? DEFAULT_SOURCE_INFO);
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ name: sourceInfo.value.routeName });
|
|
||||||
};
|
|
||||||
|
|
||||||
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 getWorksDetail(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 overflow-hidden cursor-pointer">
|
|
||||||
<img src={coverImageUrl.value} class="w-100% h-100% object-contain" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (!videoRef.value) return;
|
|
||||||
|
|
||||||
if (isPlaying.value) {
|
|
||||||
videoRef.value.pause();
|
|
||||||
} else {
|
|
||||||
if (!isVideoLoaded.value) {
|
|
||||||
videoRef.value.src = videoUrl.value;
|
|
||||||
isVideoLoaded.value = true;
|
|
||||||
}
|
|
||||||
videoRef.value.play();
|
|
||||||
}
|
|
||||||
isPlaying.value = !isPlaying.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onVideoEnded = () => {
|
|
||||||
isPlaying.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooterRow = () => {
|
|
||||||
const _fn = () => {
|
|
||||||
slsWithCatch('manuscriptCheckIds', [workId.value]);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button size="medium" type="outline" class="mr-12px" onClick={onBack}>
|
|
||||||
退出
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="medium"
|
|
||||||
type="outline"
|
|
||||||
class="mr-12px"
|
|
||||||
onClick={() => router.push(`/manuscript/edit/${workId.value}`)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
{audit_status !== AuditStatus.Passed && (
|
|
||||||
<Button type="primary" size="medium" onClick={_fn}>
|
|
||||||
去审核
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
workId && getData();
|
|
||||||
});
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (videoRef.value) {
|
|
||||||
videoRef.value.pause();
|
|
||||||
videoRef.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<Spin loading={loading.value} class="manuscript-detail-wrap" size={50}>
|
|
||||||
<div class="h-full w-full flex flex-col">
|
|
||||||
<div class="flex items-center mb-8px">
|
|
||||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onBack}>
|
|
||||||
{sourceInfo.value.title}
|
|
||||||
</span>
|
|
||||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
|
||||||
<span class="cts bold !color-#1D2129">内容稿件详情</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 bg-#fff rounded-8px py-32px">
|
|
||||||
<div class="w-684px mx-auto flex flex-col items-center">
|
|
||||||
<div class="flex justify-start flex-col w-full">
|
|
||||||
<p class="mb-8px cts bold !text-28px !lh-40px !color-#211F24">{dataSource.value.title}</p>
|
|
||||||
<p class="cts !text-12px !color-#737478 mb-32px w-full text-left">
|
|
||||||
{exactFormatTime(dataSource.value.update_time)}修改
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderMainImg()}
|
|
||||||
<div class="w-full">
|
|
||||||
<p class="cts !color-#211F24 whitespace-pre-line">{dataSource.value.content}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 仅图片类型显示图片列表 */}
|
|
||||||
{!isVideo.value && (
|
|
||||||
<div class="desc-img-wrap mt-40px">
|
|
||||||
{images.value.map((item, index) => (
|
|
||||||
<div class="desc-img-box" key={index}>
|
|
||||||
<img src={item.url} class="w-100% h-100% object-contain" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderFooterRow()}
|
|
||||||
</footer>
|
|
||||||
</Spin>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-detail-wrap {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 72px);
|
|
||||||
margin-bottom: 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: 472px;
|
|
||||||
background: #333;
|
|
||||||
aspect-ratio: 3 / 4;
|
|
||||||
}
|
|
||||||
.main-img-box {
|
|
||||||
width: 320px;
|
|
||||||
height: auto;
|
|
||||||
max-height: 472px;
|
|
||||||
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);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:visible="visible" title="退出编辑" width="480px" @close="onClose">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
|
||||||
<span>内容已修改尚未保存,若退出编辑,本次修改将不保存。</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="medium" @click="onClose">继续编辑</a-button>
|
|
||||||
<a-button type="primary" class="ml-8px" size="medium" @click="onConfirm">确认退出</a-button>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
const onConfirm = () => {
|
|
||||||
onClose();
|
|
||||||
router.push({ name: 'ManuscriptList' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
<script lang="jsx">
|
|
||||||
import { Button, Message as AMessage } from '@arco-design/web-vue';
|
|
||||||
import EditForm, { ENUM_UPLOAD_STATUS, INITIAL_VIDEO_INFO } from '../components/edit-form';
|
|
||||||
import CancelEditModal from './cancel-edit-modal.vue';
|
|
||||||
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
import { getWorksDetail, putWorksUpdate } from '@/api/all/generationWorkshop';
|
|
||||||
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
|
|
||||||
import { formatDuration, formatFileSize, convertVideoUrlToCoverUrl } from '@/utils/tools';
|
|
||||||
import { slsWithCatch } from '@/utils/stroage.ts';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditForm,
|
|
||||||
},
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
|
||||||
const cancelEditModal = ref(null);
|
|
||||||
const dataSource = ref({});
|
|
||||||
const remoteDataSource = ref({});
|
|
||||||
const uploadLoading = ref(false);
|
|
||||||
const isSaved = ref(false);
|
|
||||||
|
|
||||||
const workId = ref(route.params.id);
|
|
||||||
|
|
||||||
const collapsed = computed(() => {
|
|
||||||
return sidebarStore.menuCollapse;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
const isModified = !isEqual(dataSource.value, remoteDataSource.value);
|
|
||||||
if (isModified && !isSaved.value) {
|
|
||||||
cancelEditModal.value?.open();
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSave = async (check = false) => {
|
|
||||||
formRef.value?.validate().then(async () => {
|
|
||||||
if (dataSource.value.videoInfo.uploadStatus === ENUM_UPLOAD_STATUS.UPLOADING) {
|
|
||||||
AMessage.warning('有视频正在上传中,请等待上传完成后再提交');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredWorks = omit(dataSource.value, 'videoInfo');
|
|
||||||
const { code, data } = await putWorksUpdate({ id: workId.value, ...filteredWorks });
|
|
||||||
if (code === 200) {
|
|
||||||
AMessage.success('保存成功');
|
|
||||||
isSaved.value = true;
|
|
||||||
|
|
||||||
if (check) {
|
|
||||||
slsWithCatch('manuscriptCheckIds', [workId.value]);
|
|
||||||
router.push({ name: 'ManuscriptCheck' });
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const getData = async () => {
|
|
||||||
const { code, data } = await getWorksDetail(workId.value);
|
|
||||||
if (code === 200) {
|
|
||||||
const { type, files } = data;
|
|
||||||
const _data = { ...data, videoInfo: cloneDeep(INITIAL_VIDEO_INFO) };
|
|
||||||
|
|
||||||
// 初始化视频数据
|
|
||||||
if (type === EnumManuscriptType.Video && files.length) {
|
|
||||||
_data.videoInfo.uploadStatus = 'end';
|
|
||||||
const { name, size, duration, url } = files[0];
|
|
||||||
_data.videoInfo.name = name;
|
|
||||||
_data.videoInfo.size = formatFileSize(size);
|
|
||||||
_data.videoInfo.time = formatDuration(duration);
|
|
||||||
_data.videoInfo.poster = convertVideoUrlToCoverUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource.value = cloneDeep(_data);
|
|
||||||
remoteDataSource.value = cloneDeep(_data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onChange = (val) => {
|
|
||||||
dataSource.value = val;
|
|
||||||
};
|
|
||||||
const onUpdateVideoInfo = (newVideoInfo) => {
|
|
||||||
dataSource.value.videoInfo = { ...dataSource.value.videoInfo, ...newVideoInfo };
|
|
||||||
};
|
|
||||||
const onBack = () => {
|
|
||||||
router.push({ name: 'ManuscriptList' });
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
workId && getData();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<div class="manuscript-edit-wrap">
|
|
||||||
<div class="flex items-center mb-8px">
|
|
||||||
<span class="cts color-#4E5969 cursor-pointer" onClick={onCancel}>
|
|
||||||
内容稿件列表
|
|
||||||
</span>
|
|
||||||
<icon-oblique-line size="12" class="color-#C9CDD4 mx-4px" />
|
|
||||||
<span class="cts bold !color-#1D2129">编辑内容稿件</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-y-auto p-24px bg-#fff rounded-8px ">
|
|
||||||
<EditForm
|
|
||||||
ref={formRef}
|
|
||||||
formData={dataSource.value}
|
|
||||||
onChange={onChange}
|
|
||||||
onUpdateVideoInfo={onUpdateVideoInfo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
class={`flex justify-end items-center px-16px py-16px w-full bg-#F6F5FC footer-row ${
|
|
||||||
collapsed.value ? 'collapsed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Button size="medium" type="outline" onClick={onCancel} class="mr-12px">
|
|
||||||
退出
|
|
||||||
</Button>
|
|
||||||
<Button size="medium" type="outline" onClick={() => onSave()} class="mr-12px">
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" size="medium" onClick={() => onSave(true)} loading={uploadLoading.value}>
|
|
||||||
保存并审核
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<CancelEditModal ref={cancelEditModal} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
$footer-height: 68px;
|
|
||||||
.manuscript-edit-wrap {
|
|
||||||
height: calc(100% - 72px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.cts {
|
|
||||||
color: #939499;
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
&.bold {
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer-row {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: $sidebar-width;
|
|
||||||
width: calc(100% - $sidebar-width);
|
|
||||||
height: $footer-height;
|
|
||||||
&.collapsed {
|
|
||||||
left: $sidebar-width-collapse;
|
|
||||||
width: calc(100% - $sidebar-width-collapse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="common-filter-wrap">
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">内容稿件标题</span>
|
|
||||||
<a-input
|
|
||||||
v-model="query.title"
|
|
||||||
class="!w-240px"
|
|
||||||
placeholder="请输入内容稿件标题"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="filter-row-item">
|
|
||||||
<span class="label">所属项目</span>
|
|
||||||
<CommonSelect
|
|
||||||
placeholder="请选择所属项目"
|
|
||||||
v-model="query.project_id"
|
|
||||||
:options="projects"
|
|
||||||
class="!w-166px"
|
|
||||||
@change="handleSearch"
|
|
||||||
/>
|
|
||||||
</div> -->
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">序号</span>
|
|
||||||
<a-space size="medium">
|
|
||||||
<a-input
|
|
||||||
v-model="query.uid"
|
|
||||||
class="!w-160px"
|
|
||||||
placeholder="请输入序号"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">审核状态</span>
|
|
||||||
<CommonSelect
|
|
||||||
placeholder="全部"
|
|
||||||
:options="CHECK_STATUS"
|
|
||||||
v-model="query.audit_status"
|
|
||||||
class="!w-160px"
|
|
||||||
:multiple="false"
|
|
||||||
@change="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<span class="label">上传时间</span>
|
|
||||||
<a-range-picker
|
|
||||||
v-model="created_at"
|
|
||||||
size="medium"
|
|
||||||
allow-clear
|
|
||||||
format="YYYY-MM-DD"
|
|
||||||
class="w-280px"
|
|
||||||
@change="onDateChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row-item">
|
|
||||||
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
|
|
||||||
<template #icon>
|
|
||||||
<icon-search />
|
|
||||||
</template>
|
|
||||||
<template #default>搜索</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button size="medium" @click="handleReset">
|
|
||||||
<template #icon>
|
|
||||||
<icon-refresh />
|
|
||||||
</template>
|
|
||||||
<template #default>重置</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { defineEmits, defineProps } from 'vue';
|
|
||||||
import { CHECK_STATUS } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
|
||||||
import CommonSelect from '@/components/common-select';
|
|
||||||
// import { getProjectList } from '@/api/all/propertyMarketing';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
query: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits('search', 'reset', 'update:query');
|
|
||||||
|
|
||||||
const created_at = ref([]);
|
|
||||||
// const projects = ref([]);
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
emits('update:query', props.query);
|
|
||||||
nextTick(() => {
|
|
||||||
emits('search');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDateChange = (value) => {
|
|
||||||
if (!value) {
|
|
||||||
props.query.created_at = [];
|
|
||||||
handleSearch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [start, end] = value;
|
|
||||||
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
props.query.created_at = [
|
|
||||||
dayjs(start).startOf('day').format(FORMAT_DATE),
|
|
||||||
dayjs(end).endOf('day').format(FORMAT_DATE),
|
|
||||||
];
|
|
||||||
|
|
||||||
handleSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取项目列表
|
|
||||||
// const getProjects = async () => {
|
|
||||||
// try {
|
|
||||||
// const { code, data } = await getProjectList();
|
|
||||||
// if (code === 200) {
|
|
||||||
// projects.value = data;
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('获取项目列表失败:', error);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
created_at.value = [];
|
|
||||||
// projects.value = [];
|
|
||||||
emits('reset');
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// getProjects();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
export const TABLE_COLUMNS = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
dataIndex: 'uid',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'left',
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片/视频',
|
|
||||||
dataIndex: 'cover',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容稿件标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 240,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户意见',
|
|
||||||
dataIndex: 'customer_opinion',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '所属项目',
|
|
||||||
// dataIndex: 'projects',
|
|
||||||
// width: 240,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '稿件类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核状态',
|
|
||||||
dataIndex: 'audit_status',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传时间',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上传人员',
|
|
||||||
dataIndex: 'uploader',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改时间',
|
|
||||||
dataIndex: 'last_modified_at',
|
|
||||||
width: 180,
|
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后修改人员',
|
|
||||||
dataIndex: 'last_modifier',
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 180,
|
|
||||||
fixed: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal
|
|
||||||
v-model:visible="visible"
|
|
||||||
title="删除稿件"
|
|
||||||
width="480px"
|
|
||||||
@close="onClose"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img :src="icon1" width="20" height="20" class="mr-12px" />
|
|
||||||
<span>确认删除 {{ projectName }} 这个稿件吗?</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<a-button size="large" @click="onClose">取消</a-button>
|
|
||||||
<a-button type="primary" class="ml-16px" status="danger" size="large" @click="onDelete"
|
|
||||||
>确认删除</a-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { deleteWork } from '@/api/all/generationWorkshop';
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
|
|
||||||
|
|
||||||
const update = inject('update');
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const projectId = ref(null);
|
|
||||||
const projectName = ref('');
|
|
||||||
|
|
||||||
const isBatch = computed(() => Array.isArray(projectId.value));
|
|
||||||
|
|
||||||
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 deleteWork(projectId.value);
|
|
||||||
if (code === 200) {
|
|
||||||
AMessage.success('删除成功');
|
|
||||||
update()
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-table
|
|
||||||
ref="tableRef"
|
|
||||||
:data="dataSource"
|
|
||||||
row-key="id"
|
|
||||||
column-resizable
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ x: '100%' }"
|
|
||||||
class="manuscript-table w-100% flex-1"
|
|
||||||
bordered
|
|
||||||
@sorter-change="handleSorterChange"
|
|
||||||
>
|
|
||||||
<template #empty>
|
|
||||||
<NoData text="暂无稿件" />
|
|
||||||
</template>
|
|
||||||
<template #columns>
|
|
||||||
<a-table-column
|
|
||||||
v-for="column in TABLE_COLUMNS"
|
|
||||||
:key="column.dataIndex"
|
|
||||||
:data-index="column.dataIndex"
|
|
||||||
:fixed="column.fixed"
|
|
||||||
:width="column.width"
|
|
||||||
:min-width="column.minWidth"
|
|
||||||
:sortable="column.sortable"
|
|
||||||
:align="column.align"
|
|
||||||
ellipsis
|
|
||||||
tooltip
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="cts mr-4px">{{ column.title }}</span>
|
|
||||||
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
|
|
||||||
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="column.dataIndex === 'create_at'" #cell="{ record }">
|
|
||||||
{{ exactFormatTime(record.create_at) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === '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 === 'title'" #cell="{ record }">
|
|
||||||
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'audit_status'" #cell="{ record }">
|
|
||||||
<div
|
|
||||||
class="flex items-center w-fit h-28px px-8px rounded-2px"
|
|
||||||
:style="{ backgroundColor: getStatusInfo(record.audit_status).backgroundColor }"
|
|
||||||
>
|
|
||||||
<span class="cts s1" :style="{ color: getStatusInfo(record.audit_status).color }">{{
|
|
||||||
getStatusInfo(record.audit_status).name
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'type'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="record.type === EnumManuscriptType.Image ? icon2 : icon3"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="mr-4px"
|
|
||||||
/>
|
|
||||||
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
|
|
||||||
record.type === EnumManuscriptType.Image ? '图文' : '视频'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="['uploader', 'last_modifier'].includes(column.dataIndex)" #cell="{ record }">
|
|
||||||
{{ record[column.dataIndex].name || record[column.dataIndex].mobile }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="['created_at', 'last_modified_at'].includes(column.dataIndex)" #cell="{ record }">
|
|
||||||
{{ exactFormatTime(record[column.dataIndex]) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
|
||||||
<HoverImagePreview :src="record.cover">
|
|
||||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
|
||||||
<template #error>
|
|
||||||
<img :src="icon4" class="w-full h-full" />
|
|
||||||
</template>
|
|
||||||
</a-image>
|
|
||||||
</HoverImagePreview>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
|
|
||||||
<a-button type="outline" size="mini" class="mr-8px" @click="onEdit(record)">编辑</a-button>
|
|
||||||
<a-button type="outline" size="mini" @click="onDetail(record)">详情</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else #cell="{ record }">
|
|
||||||
{{ formatTableField(column, record, true) }}
|
|
||||||
</template>
|
|
||||||
</a-table-column>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { formatTableField, exactFormatTime } from '@/utils/tools';
|
|
||||||
import { TABLE_COLUMNS } from './constants';
|
|
||||||
import { CHECK_STATUS, EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
|
||||||
import { CUSTOMER_OPINION } from '@/views/creative-generation-workshop/manuscript/check-list/constants';
|
|
||||||
|
|
||||||
import TextOverTips from '@/components/text-over-tips';
|
|
||||||
import HoverImagePreview from '@/components/hover-image-preview';
|
|
||||||
|
|
||||||
import icon1 from '@/assets/img/media-account/icon-delete.png';
|
|
||||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
|
||||||
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
|
|
||||||
import icon4 from '@/assets/img/error-img.png';
|
|
||||||
|
|
||||||
const emits = defineEmits(['edit', 'sorterChange', 'delete']);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dataSource: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableRef = ref(null);
|
|
||||||
|
|
||||||
const handleSorterChange = (column, order) => {
|
|
||||||
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
|
|
||||||
};
|
|
||||||
const onDelete = (item) => {
|
|
||||||
emits('delete', item);
|
|
||||||
};
|
|
||||||
const onEdit = (item) => {
|
|
||||||
router.push(`/manuscript/edit/${item.id}`);
|
|
||||||
};
|
|
||||||
const onDetail = (item) => {
|
|
||||||
router.push(`/manuscript/detail/${item.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusInfo = (audit_status) => {
|
|
||||||
return CHECK_STATUS.find((v) => v.id === audit_status) ?? {};
|
|
||||||
};
|
|
||||||
const getCustomerOpinionInfo = (value) => {
|
|
||||||
return CUSTOMER_OPINION.find((item) => item.value === value);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user