From 46a1479ccd7bd579be4bd9d16a601c19dbe1a15c Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 1 May 2025 18:50:57 +0800 Subject: [PATCH 01/41] chore: router vaild limit --- src/routes.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/routes.ts b/src/routes.ts index 17fa977..40151a9 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -53,6 +53,9 @@ const routerConfig: RouterOptions = { const router = createRouter(routerConfig); +// 上次路由的有效时间 +const VALID_DURATION = 24 * 60 * 60 * 1000; // 24小时,单位是毫秒 + router.beforeEach((to, from, next) => { // 进度条 if (typeof NProgress !== 'undefined') { @@ -60,8 +63,19 @@ router.beforeEach((to, from, next) => { } // 如果是根路径,且有上次访问的路径,则重定向到上次的路径 - if (to.path === config.routerPrefix + '/' && localStorage.getItem('lastPath')) { - next({ path: localStorage.getItem('lastPath') }); + if (to.path === config.routerPrefix + '/') { + const lastPath = localStorage.getItem('lastPath'); + const lastAccessTime = localStorage.getItem('lastPathAccessTime'); + + // 检查路径和时间戳是否存在,并且是否在有效期内 + if (lastPath && lastAccessTime && Date.now() - Number(lastAccessTime) < VALID_DURATION) { + next({ path: lastPath }); + } else { + // 如果路径无效或超时,清除存储的路径和时间戳 + localStorage.removeItem('lastPath'); + localStorage.removeItem('lastPathAccessTime'); + next(); + } } else { next(); } @@ -71,9 +85,10 @@ router.afterEach((to) => { if (typeof NProgress !== 'undefined') { NProgress.done(); } - // 保存当前路径 + // 保存当前路径和访问时间 if (to.path !== config.routerPrefix + '/') { localStorage.setItem('lastPath', to.path); + localStorage.setItem('lastPathAccessTime', Date.now().toString()); } }); From d48f858765d6cb0e7096db68f9a7fb645f043206 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 8 May 2025 00:12:55 +0800 Subject: [PATCH 02/41] feat: sso update --- src/config/index.ts | 19 +- src/hooks/common.ts | 1 - src/hooks/useBreadcrumb.tsx | 39 ++-- src/hooks/useParams.ts | 36 +++ src/pages/equipment/return.vue | 2 +- src/pages/index.vue | 349 ++++++++++++---------------- src/pages/manage/network-portal.vue | 215 +++++++++++++++++ src/pages/other/CHANGELOG.vue | 2 +- src/routes.ts | 44 +++- 9 files changed, 467 insertions(+), 240 deletions(-) create mode 100644 src/hooks/useParams.ts create mode 100644 src/pages/manage/network-portal.vue diff --git a/src/config/index.ts b/src/config/index.ts index c426827..c87719a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -92,6 +92,17 @@ const routerMap: RouteMaps = [ }, ], }, + { + label: '上网认证', + children: [ + { + key: 'network-portal', + label: '绑定列表', + icon: 'internet', + component: () => import('@pages/manage/network-portal.vue'), + }, + ], + }, { label: 'Management', children: [ @@ -295,10 +306,12 @@ export const routerPrefix = config.routerPrefix; export { routerMap, config, packageVersion }; export const VERSION = config.version; -export const VersionMode = config.versionMode; -export const SystemName = config.systemName; -export const SystemNameEn = config.systemNameEn; +export const versionMode = config.versionMode; +export const systemName = config.systemName; +export const systemNameEn = config.systemNameEn; export const pagePermissionVerify = config.pagePermissionVerify; export const menuPermissionVerify = config.menuPermissionVerify; export const useViewTransition = config.useViewTransition; export const allowHotUpdate = config.allowHotUpdate; +export const loginVerify = config.loginVerify; +export const menuUseCollapsed = config.menuUseCollapsed; diff --git a/src/hooks/common.ts b/src/hooks/common.ts index 3462fd2..60c81ee 100644 --- a/src/hooks/common.ts +++ b/src/hooks/common.ts @@ -74,7 +74,6 @@ export function getRoutePathObj( export function verifyPath(path: string, map = routerMap, deep = 0) { for (const item of map) { if (item?.key === path) { - console.warn(item); return true; } if (item?.children) { diff --git a/src/hooks/useBreadcrumb.tsx b/src/hooks/useBreadcrumb.tsx index d5c549f..771622e 100644 --- a/src/hooks/useBreadcrumb.tsx +++ b/src/hooks/useBreadcrumb.tsx @@ -1,5 +1,6 @@ import { defineComponent, ref, toRefs, watch } from 'vue'; import { routerMap } from '../config'; +import { Breadcrumb, BreadcrumbItem } from 'tdesign-vue-next'; import type { RouteMaps } from '@type/type'; export default defineComponent({ @@ -10,28 +11,22 @@ export default defineComponent({ }, }, setup(props) { - // const { value } = toRefs(props) const { value } = toRefs(props); + const componentValue = ref(value.value); watch( () => value.value, (newVal) => { - value.value = newVal; + componentValue.value = newVal; }, ); const isHidden = ref(false); - // 通过url判断当前页面 - // const getpath = () => { - // const path = window.location.hash.replace('#', '').replace(config.routerPrefix + '/', ''); - // return path; - // }; - - function getIteminMap( + const getItemInMap = ( map: RouteMaps, value: string, deep = 0, - ): { parent: string | null; current: string | null; fatherCrumb?: string | null } { + ): { parent: string | null; current: string | null; fatherCrumb?: string | null } => { for (const item of map) { if (item?.key === value) { isHidden.value = item?.hiddenBreadCrumb ?? false; @@ -42,7 +37,7 @@ export default defineComponent({ }; } if (item?.children) { - const result = getIteminMap(item.children, value, deep + 1); + const result = getItemInMap(item.children, value, deep + 1); if (result?.current) { return { parent: result?.fatherCrumb ?? result?.parent ?? item.label, @@ -52,33 +47,33 @@ export default defineComponent({ } } return { parent: null, current: null }; - } + }; - function renderCrumbItem(path: string, level = 0) { - const valuekey = getIteminMap(routerMap, path); + const renderCrumbItem = (path: string, level = 0) => { + const valueKey = getItemInMap(routerMap, path); if (level === 0) { return ( <> - 媒体部管理系统 - {renderCrumbItem(value.value, level + 1)} + 媒体部管理系统 + {renderCrumbItem(componentValue.value, level + 1)} ); } - if (!valuekey?.parent) { - return {valuekey?.current}; + if (!valueKey?.parent) { + return {valueKey?.current}; } else { return ( <> - {valuekey?.parent} - {valuekey?.current} + {valueKey?.parent} + {valueKey?.current} ); } - } + }; return () => (
- {renderCrumbItem(value.value)} + {renderCrumbItem(componentValue.value)}
); }, diff --git a/src/hooks/useParams.ts b/src/hooks/useParams.ts new file mode 100644 index 0000000..7467e4b --- /dev/null +++ b/src/hooks/useParams.ts @@ -0,0 +1,36 @@ +import { isArray } from 'lodash-es'; +import { LocationQuery } from 'vue-router'; + +export function getURLAllParams(location: Location) { + if (!location?.search) return {}; + const query = location?.search?.substring(1); + const vars = query.split('&').filter(Boolean); + const params: { [k: string]: string } = {}; + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split('=').filter(Boolean); + params[pair[0]] = pair[1]; + } + return params; +} + +export function getParams() { + return ( + routerQuery: LocationQuery, + location: Location, + key: string | string[], + ): { [key: string]: string | undefined } => { + const cachedURLQuery = getURLAllParams(location); + + if (isArray(key)) { + return key.reduce((acc, element) => { + acc[element] = routerQuery?.[element] ?? cachedURLQuery?.[element] ?? undefined; + return acc; + }, {}); + } + + const value = (routerQuery?.[key] as string) ?? cachedURLQuery?.[key]; + return { + [key]: value ?? undefined, + }; + }; +} diff --git a/src/pages/equipment/return.vue b/src/pages/equipment/return.vue index de59537..8c496a7 100644 --- a/src/pages/equipment/return.vue +++ b/src/pages/equipment/return.vue @@ -298,7 +298,7 @@ const getTime = () => { }; onMounted(() => { - var USER_INFO_STR = localStorage.getItem('user_info'); + var USER_INFO_STR = localStorage.getItem('userInfo'); try { let a = JSON.parse(USER_INFO_STR); if (a) { diff --git a/src/pages/index.vue b/src/pages/index.vue index d09c4a3..ad4a784 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -125,11 +125,11 @@ class="MainContent" :class="{ 'SideMenuShow-MainContent': SideMenu.show, - SideMenuUseCollapsed: config.menuUseCollapsed && !SideMenu.show, + SideMenuUseCollapsed: menuUseCollapsed && !SideMenu.show, }" :NoShowMenu="!TitleMenu.show" > - +
- -
@@ -172,32 +167,46 @@ import SideMenus from '../hooks/useMenu'; import BreadCrumb from '../hooks/useBreadcrumb'; import { computed, onBeforeMount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'; import { themeMode, toggleTheme } from '../utils/theme'; -import { config, routerMap, useViewTransition, allowHotUpdate, packageVersion } from '../config'; +import { + config, + routerMap, + useViewTransition, + allowHotUpdate, + packageVersion, + routerPrefix, + systemName, + loginVerify, + menuUseCollapsed, +} from '../config'; import { PoweroffIcon, ChatBubbleHelpIcon } from 'tdesign-icons-vue-next'; import { NotifyPlugin } from 'tdesign-vue-next'; import { getCurrentPage, verifyPath, getSSOURL, getLoginURL, getRoutePathObj, VerifyToken } from '../hooks/common'; import { useRequest } from '../hooks/useRequest'; import PageTooSmall from '../components/pages/PageSmall.vue'; -import router from '../routes'; +import { useRoute, useRouter } from 'vue-router'; import type { LocationQueryRaw } from 'vue-router'; +import { getParams, getURLAllParams } from '@hooks/useParams'; +import { curryRight, isEmpty, isObject } from 'lodash-es'; + +const router = useRouter(); +const route = useRoute(); watch( () => router.currentRoute.value.path, (val) => { - const v = val.replace(config.routerPrefix + '/', ''); - SideMenu.ComponentValue = v; + const v = val.replace(routerPrefix + '/', ''); SideMenu.value = v; }, ); +const getParamsData = getParams(); const componentPermissions = ref([]); const TitleMenu = reactive({ - text: config.systemName, + text: systemName, show: true, }); const SideMenu = reactive({ value: 'Content', - ComponentValue: 'Content', show: false, }); const MainContent = reactive({ @@ -205,13 +214,6 @@ const MainContent = reactive({ classOut: false, lastChoose: 'Content', ComponentValue: 'Content', - breadcrumb: { - show: true, - parent: '设备操作', - current: '借出', - changing1: false, - changing2: false, - }, AccountMenuOptions: [ // { content: "个人中心", value: 1, prefixIcon: }, { content: '遇到问题', value: 2, prefixIcon: }, @@ -221,7 +223,6 @@ const MainContent = reactive({ const login_info = reactive({ name: '-', code: '-', - class: '-', permissions: [], login_time: '', }); @@ -324,7 +325,6 @@ const getUserInfoByToken = (TOKEN) => { if (RES.errcode == 0) { login_info.code = RES.data.usercode; login_info.name = RES.data.name; - login_info.class = RES.data.class; login_info.login_time = RES.data.login_time; } }, @@ -445,17 +445,19 @@ const handleChangeComponent = (componentName: string, doNotToggleSideMenu: boole componentPermissions.value = getComponentPermissions(componentName); MainContent.lastChoose = componentName; SideMenu.value = componentName; - SideMenu.ComponentValue = componentName; // 若菜单不是Collapsed模式,则判断当前菜单是否为展开状态,若是则关闭 - if (config.menuUseCollapsed && SideMenu.show && !config.menuUseCollapsed) { + if (menuUseCollapsed && SideMenu.show && !menuUseCollapsed) { doNotToggleSideMenu ? null : ToggleSideMenu(); } // 应用动画 MainContent.classOut = true; setTimeout(() => { router.push({ - path: `${config.routerPrefix}/${componentName}`, - query: query as LocationQueryRaw, + path: `${routerPrefix}/${componentName}`, + query: { + ...route.query, + ...(query as LocationQueryRaw), + }, }); MainContent.ComponentValue = componentName; setTimeout(() => { @@ -546,193 +548,129 @@ const checkUpdate = () => { * @getUrlParam * @desc 获取参数 * @param id 参数名 + * @deprecated 已弃用,请使用 getParams() hooks */ -const getUrlParam = (variable) => { - // 先尝试从 hash 后的参数中获取 - const hashParts = window.location.hash.split('?'); - if (hashParts.length > 1) { - const hashParams = new URLSearchParams(hashParts[1]); - const value = hashParams.get(variable); - if (value) return value; - } - - // 如果在 hash 后没有找到,再尝试从 URL 参数中获取 - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get(variable); -}; - -/** - * @applyUrlParam - * @desc 获取参数 - * @param new_param 参数名 - * @param value 参数值 - */ -// const applyUrlParam = (new_param, value, location = window.location) => { -// if (!new_param || !value) { -// return false; -// } -// var regUrl = location.search == '' ? false : true; -// var regParam = location.search.indexOf(new_param) == -1 ? true : false; -// var UrlH = location.origin + location.pathname; -// var UrlS = location.href; -// if (regUrl) { -// //有参数了,追加 -// if (regParam) { -// //没有参数,直接加 -// var newurl = UrlS + '&' + new_param + '=' + value; -// window.history.pushState(null, null, newurl); -// } else { -// //有参数,替换 -// var newurl = updateUrlParam(new_param, value); -// window.history.pushState(null, null, newurl); -// } -// } else { -// //没有参数,直接加 -// var newurl = UrlH + '?' + new_param + '=' + value; -// window.history.pushState(null, null, newurl); +// const getUrlParam = (variable) => { +// // 先尝试从 hash 后的参数中获取 +// const hashParts = window.location.hash.split('?'); +// if (hashParts.length > 1) { +// const hashParams = new URLSearchParams(hashParts[1]); +// const value = hashParams.get(variable); +// if (value) return value; // } -// }; -/** - * @updateUrlParam - * @desc 获取参数 - * @param key 参数名 - * @param value 参数值 - */ -// const updateUrlParam = (key, value) => { -// var uri = window.location.href; -// if (!value) { -// return uri; -// } -// var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i'); -// var separator = uri.indexOf('?') !== -1 ? '&' : '?'; -// if (uri.match(re)) { -// return uri.replace(re, '$1' + key + '=' + value + '$2'); -// } else { -// return uri + separator + key + '=' + value; -// } -// }; - -// const removeParam = (key, sourceURL) => { -// var rtn = sourceURL.split('?')[0], -// param, -// params_arr = [], -// queryString = sourceURL.indexOf('?') !== -1 ? sourceURL.split('?')[1] : ''; -// if (queryString !== '') { -// params_arr = queryString.split('&'); -// for (var i = params_arr.length - 1; i >= 0; i -= 1) { -// param = params_arr[i].split('=')[0]; -// if (param === key) { -// params_arr.splice(i, 1); -// } -// } -// rtn = rtn + '?' + params_arr.join('&'); -// } -// return rtn; +// // 如果在 hash 后没有找到,再尝试从 URL 参数中获取 +// const urlParams = new URLSearchParams(window.location.search); +// return urlParams.get(variable); // }; onBeforeMount(() => { console.info('System Start Running!'); document.body.style.overflow = 'hidden'; - const searchParams = new URLSearchParams(window.location.search); - const actionType = searchParams.get('actionType'); - - if (actionType == 'Login_Back') { - // 登录页面返回 - const TOKEN = searchParams.get('user_token'); - if (TOKEN) { - localStorage.setItem('token', TOKEN); - const USER_INFO = { - code: searchParams.get('user_code'), - name: searchParams.get('user_name'), - class: searchParams.get('user_class'), - login_time: searchParams.get('login_time'), - }; - login_info.code = searchParams.get('user_code'); - login_info.name = searchParams.get('user_name'); - login_info.class = searchParams.get('user_class'); - login_info.login_time = searchParams.get('login_time'); - localStorage.setItem('user_info', JSON.stringify(USER_INFO)); - - // 获取当前路由路径 - const hashParams = new URLSearchParams(); - hashParams.set('user_token', TOKEN); - - // 更新 URL,只修改 search 参数 - const newUrl = new URL(window.location.href); - newUrl.search = `?${hashParams.toString()}`; - window.history.replaceState(null, '', newUrl.toString()); + + router.isReady().then(() => { + const paramsKeys = ['actionType', 'user_token', 'user_code', 'user_name', 'login_time', 'path']; + const { + actionType, + user_token: TOKEN, + user_code, + user_name, + login_time, + path: param_path, + } = getParamsData(route.query, location, paramsKeys); + const actionType_LowerCase = actionType?.toLowerCase(); + if (actionType_LowerCase == 'login_back') { + // 登录页面返回 + if (TOKEN) { + localStorage.setItem('token', TOKEN); + const USER_INFO = { + code: user_code, + name: user_name, + login_time: login_time, + }; + localStorage.setItem('userInfo', decodeURIComponent(JSON.stringify(USER_INFO))); + + login_info.code = user_code; + login_info.name = user_name; + login_info.login_time = login_time; + } } - } - // 查询Token - const VERIFY_TOKEN = localStorage.getItem('token'); - if ((VERIFY_TOKEN == null || !VERIFY_TOKEN) && config.loginVerify == true) { - // 没有登录数据,遣返登录页面 - console.warn('未登录,跳转统一认证'); - setTimeout(() => { - location.href = getLoginURL(); - }, 1500); - } else if (config.loginVerify == true) { - // 验证登录 - setTimeout(async () => { - if (await VerifyToken()) { - // 设置userInfo - const userInfo = localStorage.getItem('user_info'); - if (userInfo) { - const { code, name, class: class_name, login_time } = JSON.parse(userInfo); - login_info.code = code; - login_info.name = name; - login_info.class = class_name; - login_info.login_time = login_time; - } - // pass - var param_path = getUrlParam('path'); - if (param_path) { - console.info('检测到 Path 参数,跳转至指定页面。'); - if (VerifyPath(param_path) === true) { - handleChangeComponent(param_path, true); + const currentURLParams = getURLAllParams(location); + // 有内容时,将url的seaarch参数转成router的参数 + if (isObject(currentURLParams) && !isEmpty(currentURLParams)) { + // 只保留token参数 + const newQuery = { + ...{ + user_token: currentURLParams?.user_token, + }, + ...route.query, + } as Record; + location.replace( + `/#/system/${localStorage.getItem('lastPath') ?? MainContent.lastChoose}?${new URLSearchParams( + newQuery, + ).toString()}`, + ); + } + // url没有Token,从内存查询Token,并验证是否有效 + const VERIFY_TOKEN = localStorage.getItem('token'); + if ((VERIFY_TOKEN == null || !VERIFY_TOKEN) && loginVerify == true) { + // 没有登录数据,遣返登录页面 + console.warn('未登录,跳转统一认证'); + // setTimeout(() => { + // location.href = getLoginURL(); + // }, 1500); + } else if (loginVerify == true) { + // 验证登录 + setTimeout(async () => { + if (await VerifyToken()) { + // 设置userInfo + const userInfo = localStorage.getItem('userInfo'); + if (userInfo) { + const { code, name, login_time } = JSON.parse(userInfo); + login_info.code = code; + login_info.name = name; + login_info.login_time = login_time; + } + // pass + if (param_path) { + console.info('检测到 Path 参数,跳转至指定页面。'); + if (VerifyPath(param_path) === true) { + handleChangeComponent(param_path, true); + } } + // 下方是为了修复刷新时跳到默认页面(首页)的问题 + handleChangeComponent(localStorage.getItem('lastPath') ?? MainContent.lastChoose, true); + localStorage.setItem('token', VERIFY_TOKEN); + NotifyPlugin('success', { + title: '温馨提示', + content: () => { + return ( +
+ 欢迎使用媒体部管理系统,祝您今日工作顺利! +
+ ); + }, + duration: 5000, + }); + getUserInfoByToken(VERIFY_TOKEN); + } else { + // location.href = getLoginURL(); } - // 下方是为了修复刷新时跳到默认页面(首页)的问题 - handleChangeComponent(localStorage.getItem('lastPath') ?? MainContent.lastChoose, true); - localStorage.setItem('token', VERIFY_TOKEN); - NotifyPlugin('success', { - title: '温馨提示', - content: () => { - return ( -
- 欢迎使用媒体部管理系统,祝您今日工作顺利! -
- ); - }, - duration: 5000, - }); - getUserInfoByToken(VERIFY_TOKEN); - } else { - location.href = getLoginURL(); - } - }); - } else { - NotifyPlugin('success', { - title: '温馨提示', - content: () => { - return ( -
- 当前已关闭登录检测,请确认当前非生产环境!! -
- ); - }, - duration: 5000, - }); - // handleChangeComponent(MainContent.lastChoose,true,false) - // var param_path = getUrlParam("path"); - // if (param_path) { - // console.log("检测到 Path 参数,跳转至指定页面。"); - // if (VerifyPath(param_path) === true) { - // handleChangeComponent(param_path, true); - // } - // } - } + }); + } else { + NotifyPlugin('success', { + title: '温馨提示', + content: () => { + return ( +
+ 当前已关闭登录检测,请确认当前非生产环境!! +
+ ); + }, + duration: 5000, + }); + } + }); // update checkUpdate(); // load message @@ -740,12 +678,12 @@ onBeforeMount(() => { LoadUserPermissions(); const currentPage = getCurrentPage(); if (currentPage) { - SideMenu.ComponentValue = currentPage; SideMenu.value = currentPage; } }); onMounted(() => { + const { __DEBUG_DONTCHECKLOGINSTATUS } = getParamsData(route.query, location, '__DEBUG_DONTCHECKLOGINSTATUS'); document.body.style.overflow = ''; // document.getElementById("loading").display = "none" theme.value = ThemeMode(); @@ -755,7 +693,8 @@ onMounted(() => { window.addEventListener('resize', (e) => { GetPageWidth(e); }); - if (getUrlParam('__DEBUG_DONTCHECKLOGINSTATUS') != 'yes') { + // DEBUG-前端不检测登录状态 + if (__DEBUG_DONTCHECKLOGINSTATUS != 'yes') { // 开始检测 startCheckToken(); } diff --git a/src/pages/manage/network-portal.vue b/src/pages/manage/network-portal.vue new file mode 100644 index 0000000..3b6d77b --- /dev/null +++ b/src/pages/manage/network-portal.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/pages/other/CHANGELOG.vue b/src/pages/other/CHANGELOG.vue index 971b9c4..31d854a 100644 --- a/src/pages/other/CHANGELOG.vue +++ b/src/pages/other/CHANGELOG.vue @@ -63,7 +63,7 @@ + + diff --git a/src/pages/index.vue b/src/pages/index.vue index e3a681f..bbb34fe 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -267,6 +267,17 @@ watch( }, ); +router.afterEach(() => { + // 应用动画 + setTimeout(() => { + MainContent.classOut = false; + MainContent.classIn = true; + setTimeout(() => { + MainContent.classIn = false; + }, 280); + }, 280); +}); + const viewAllMessage = () => { handleChangeComponent('MessageList', true); }; @@ -292,10 +303,6 @@ const getMessage = () => { }, error: function (err) { console.error(err); - NotifyPlugin.error({ - title: '获取消息内容失败', - content: err, - }); }, }); }; @@ -478,13 +485,6 @@ const handleChangeComponent = (componentName: string, doNotToggleSideMenu: boole }, }); MainContent.ComponentValue = componentName; - setTimeout(() => { - MainContent.classOut = false; - MainContent.classIn = true; - setTimeout(() => { - MainContent.classIn = false; - }, 280); - }, 280); }, 200); }; @@ -724,6 +724,16 @@ export default { + + + diff --git a/src/pages/audit/type.ts b/src/pages/audit/type.ts new file mode 100644 index 0000000..b2a8ed7 --- /dev/null +++ b/src/pages/audit/type.ts @@ -0,0 +1,123 @@ +import { TdTimelineItemProps } from 'tdesign-vue-next'; + +export interface AuditItem extends Object { + // 自增id + id: number; + // 高级审批,即只有审批人可以审批 + advance_approval: number; + // 创建时间 + created_at: string; + // 当前审批阶段 + current_step: number; + // 审批详情 + details: AuditDetail; + // 审批记录 + records: AuditRecordItem[]; + // 审批状态(0待处理,1通过,2拒绝,3取消) + status: number; + // 审批步骤 + steps: AuditStepItem[]; + // 审批类型(1:设备借出, 2:上网审批, 3:任务,4:其他) + type: number; + // 更新时间 + updated_at: string; + // 申请人 + user_code: string; + // 非审批人是否可见 + visible_allow: boolean; +} + +export interface AuditRecordItem { + // 自增id + id: number; + // 操作(0待审,1同意,2拒绝) + action: number; + // 外键,关联 approval_application.id + application_id: number; + // 审批时间 + approved_at: string; + // 审批人Code + approver_user_code: string; + // 审批意见 + comment: string; + // 该记录是否废弃 + discard: boolean; + // 审批步骤 + step: number; +} + +export interface AuditStepItem { + // 自增id + id: number; + // 外键,关联 approval_application.id + application_id: number; + // 审批人Code + approver_user_code: string; + // 创建时间 + created_at: string; + // 步骤顺序 + step_order: number; + // 多人同步审批配置,or为或,and为与多人同步审批配置,or为或,and为与,mixed为混合 + required_type: 'and' | 'or' | 'mixed'; + // 审批流程混合审批逻辑 + rule_expression: string; +} + +export interface AuditDetail { + // 自增id + id: number; + // 外键,关联 approval_application.id + application_id: number; + // 审批名称 + content: string; + // 审批详情,以JSON格式存储不同类型的具体信息 + details: string; // AuditDetailItem + // 其他补充信息 + other_info: string; +} + +export type AuditDetailItem = AuditDetailOfLend | AuditDetailOfNetworkPortal | AuditDetailOfTask | AuditDetailOfOther; + +export interface AuditDetailOfLend { + equipment_code: string; + lend_time: string; + return_time: string; + lender: string; +} + +export interface AuditDetailOfNetworkPortal { + ip: string; + mac: string; + info: string; + timestamp: string; +} + +export interface AuditDetailOfTask { + content: string; + create_user: string; + equipment: string; + finally_time: string; + name: string; + place: string; + remark: string; + status: number; + type: number; + user: string; + weight: number; + work_time: string; +} + +export interface AuditDetailOfOther { + content: string; + extra: string; + key: string; + value: string; +} + +export type AuditItems = AuditItem[]; + +export interface AuditTimeLine { + total: number; + current: number; + options: TdTimelineItemProps[]; +} diff --git a/src/pages/audit/utils/constants.ts b/src/pages/audit/utils/constants.ts new file mode 100644 index 0000000..c199b85 --- /dev/null +++ b/src/pages/audit/utils/constants.ts @@ -0,0 +1,40 @@ +// 审批状态常量 +export const AUDIT_STATUS = { + PENDING: 0, // 待审批 + APPROVED: 1, // 通过 + REJECTED: 2, // 不通过 + CANCELLED: 3, // 已撤销 +} as const; + +// 审批类型常量 +export const APPROVAL_TYPE = { + AND: 'and', // 会签 + OR: 'or', // 或签 + MIXED: 'mixed', // 混合签 +} as const; + +// 审批类型常量 +export const AUDIT_TYPE = { + DEVICE: 1, // 设备借出 + INTERNET: 2, // 上网审批 + TASK: 3, // 任务 + OTHER: 4, // 其他 +} as const; + +// 主题颜色映射 +export const THEME_COLOR = { + SUCCESS: 'success', + PRIMARY: 'primary', + WARNING: 'warning', + DANGER: 'danger', + DEFAULT: 'default', +} as const; + +// 图标类型 +export const ICON_TYPE = { + PENDING: 'pending', + APPROVE: 'approve', + REJECT: 'reject', + CANCEL: 'cancel', + ERROR: 'error', +} as const; diff --git a/src/pages/audit/utils/expressionUtils.ts b/src/pages/audit/utils/expressionUtils.ts new file mode 100644 index 0000000..f7c210f --- /dev/null +++ b/src/pages/audit/utils/expressionUtils.ts @@ -0,0 +1,228 @@ +import type { ApprovalContext } from './types'; + +/** + * 解析并评估审批规则表达式 + * @param expression 规则表达式,如 "A && (B || C)" + * @param context 上下文对象,包含每个审批人的审批状态 + * @returns 表达式评估结果 + */ +export const evaluateRule = (expression: string, context: ApprovalContext): boolean => { + try { + // 验证表达式只包含合法的操作符和变量 + validateExpression(expression); + + // 替换表达式中的变量为实际值 + let jsExpression = expression; + + // 替换所有变量为其在上下文中的值 + Object.entries(context).forEach(([key, value]) => { + // 使用正则表达式确保只替换完整的变量名 + const regex = new RegExp(`\\b${key}\\b`, 'g'); + jsExpression = jsExpression.replace(regex, value ? 'true' : 'false'); + }); + + // 安全地评估表达式 + return evaluateExpression(jsExpression); + } catch (error) { + console.error('表达式解析错误:', error, { expression }); + return false; + } +}; + +/** + * 验证表达式是否只包含合法的操作符和变量 + * @param expression 规则表达式 + * @throws 如果表达式包含非法字符 + */ +const validateExpression = (expression: string): void => { + // 只允许变量名、逻辑操作符和括号 + const validExpressionRegex = /^[A-Za-z0-9_\s&|!()]+$/; + if (!validExpressionRegex.test(expression)) { + throw new Error('表达式包含非法字符'); + } + + // 检查括号是否匹配 + let bracketCount = 0; + for (const char of expression) { + if (char === '(') bracketCount++; + if (char === ')') bracketCount--; + if (bracketCount < 0) throw new Error('表达式括号不匹配'); + } + if (bracketCount !== 0) throw new Error('表达式括号不匹配'); +}; + +/** + * 安全地评估布尔表达式 + * @param expression 已替换变量的表达式 + * @returns 表达式评估结果 + */ +const evaluateExpression = (expression: string): boolean => { + // 替换所有变量名为true或false后,表达式应该只包含true、false、&&、||、!和括号 + const validResultExpressionRegex = /^[truefalse\s&|!()]+$/; + if (!validResultExpressionRegex.test(expression)) { + throw new Error('表达式包含未替换的变量'); + } + + // 使用递归下降解析器评估表达式 + const tokens = tokenize(expression); + const parser = new ExpressionParser(tokens); + return parser.parse(); +}; + +/** + * 将表达式字符串转换为标记数组 + * @param expression 表达式字符串 + * @returns 标记数组 + */ +const tokenize = (expression: string): string[] => { + const result: string[] = []; + let current = ''; + + for (const char of expression) { + if (char === ' ' || char === '\t' || char === '\n') { + if (current) { + result.push(current); + current = ''; + } + continue; + } + + if (char === '(' || char === ')' || char === '&' || char === '|' || char === '!') { + if (current) { + result.push(current); + current = ''; + } + + if (char === '&' || char === '|') { + // 处理 && 和 || + if (result.length > 0 && result[result.length - 1] === char) { + result[result.length - 1] += char; + } else { + result.push(char); + } + } else { + result.push(char); + } + } else { + current += char; + } + } + + if (current) { + result.push(current); + } + + return result; +}; + +/** + * 表达式解析器类 + */ +class ExpressionParser { + private tokens: string[]; + private position = 0; + + constructor(tokens: string[]) { + this.tokens = tokens; + } + + /** + * 解析表达式 + * @returns 表达式评估结果 + */ + parse(): boolean { + return this.parseExpression(); + } + + /** + * 解析表达式 + * @returns 表达式评估结果 + */ + private parseExpression(): boolean { + return this.parseOr(); + } + + /** + * 解析或表达式 + * @returns 表达式评估结果 + */ + private parseOr(): boolean { + let result = this.parseAnd(); + + while (this.position < this.tokens.length && this.tokens[this.position] === '||') { + this.position++; + result = result || this.parseAnd(); + } + + return result; + } + + /** + * 解析与表达式 + * @returns 表达式评估结果 + */ + private parseAnd(): boolean { + let result = this.parseNot(); + + while (this.position < this.tokens.length && this.tokens[this.position] === '&&') { + this.position++; + result = result && this.parseNot(); + } + + return result; + } + + /** + * 解析非表达式 + * @returns 表达式评估结果 + */ + private parseNot(): boolean { + if (this.position < this.tokens.length && this.tokens[this.position] === '!') { + this.position++; + return !this.parseNot(); + } + + return this.parsePrimary(); + } + + /** + * 解析基本表达式 + * @returns 表达式评估结果 + */ + private parsePrimary(): boolean { + if (this.position >= this.tokens.length) { + throw new Error('表达式不完整'); + } + + const token = this.tokens[this.position]; + this.position++; + + if (token === '(') { + const result = this.parseExpression(); + + if (this.position >= this.tokens.length || this.tokens[this.position] !== ')') { + throw new Error('缺少右括号'); + } + + this.position++; + return result; + } + + if (token === 'true') return true; + if (token === 'false') return false; + + throw new Error(`无效的标记: ${token}`); + } +} + +/** + * 检查表达式中是否包含指定的审批人 + * @param expression 规则表达式 + * @param approverCode 审批人编码 + * @returns 是否包含该审批人 + */ +export const expressionContainsApprover = (expression: string, approverCode: string): boolean => { + // 使用正则表达式检查完整的变量名 + const regex = new RegExp(`\\b${approverCode}\\b`); + return regex.test(expression); +}; diff --git a/src/pages/audit/utils/index.ts b/src/pages/audit/utils/index.ts new file mode 100644 index 0000000..74c4f54 --- /dev/null +++ b/src/pages/audit/utils/index.ts @@ -0,0 +1,481 @@ +import { TdTimelineItemProps } from 'tdesign-vue-next'; +import { AuditItem, AuditItems, AuditRecordItem, AuditStepItem } from '../type'; +import { APPROVAL_TYPE, AUDIT_TYPE } from './constants'; +import renderTimelineIcon from './renderTimelineIcon'; + +export const getStatusText = (status: number) => { + switch (status) { + case 0: + return '待审批'; + case 1: + return '通过'; + case 2: + return '不通过'; + case 3: + return '已撤销'; + default: + return '待审批'; + } +}; + +// 获取状态对应的颜色 +export const getStatusColor = (data: AuditItem, status: number) => { + const { current_step: currentStep, steps } = data; + const totalStep = steps?.length; + if (currentStep === totalStep && status === 1) { + return 'success'; + } else if (status === 1) { + return `primary`; + } + switch (status) { + case 0: + return 'warning'; + case 2: + return 'danger'; + case 3: + return 'default'; + default: + return 'warning'; + } +}; + +// 获取状态-是否审批了 +export const getStatusValue = (status: number) => { + if (!status) return null; + switch (status) { + case 1: + return true; + default: + return false; + } +}; + +export const renderStatusText = (data: AuditItem, status: number) => { + const { current_step: currentStep, steps } = data; + const totalStep = steps?.length; + if (currentStep === totalStep && status === 1) { + return '已审批 Approve'; + } else if (status === 1) { + return `流程中`; + } + switch (status) { + case 0: + return '待审批'; + case 2: + return '已拒绝'; + case 3: + return '已取消'; + default: + return '待审批'; + } +}; + +// 获取类型描述 +export const getTypeText = (type: number) => { + if (!type) return null; + switch (type) { + case 1: + return '设备借出'; + case 2: + return '上网审批'; + case 3: + return '其他'; + default: + return '其他'; + } +}; + +// 获取类型颜色 +export const getTypeColor = (type: number) => { + if (!type) return null; + switch (type) { + case 1: + return 'success'; + case 2: + return 'primary'; + case 3: + return 'default'; + default: + return 'default'; + } +}; + +// 获取该申请已经审批列表 +export const getApprovalStepRecord = (application_id: number, record: AuditRecordItem[]) => { + return record?.filter((item) => item.application_id === application_id); +}; + +export const thisUserWasApproval = (approver_user_code: string, stepData: AuditRecordItem[]) => { + return stepData + .filter((item) => item.action === 1 && !item.discard) + .map((step) => { + return step.approver_user_code; + }) + .includes(approver_user_code); +}; + +// 表达式解析器 +function evaluateRule(rule: string, context: Record): boolean { + try { + // 使用with语句注入上下文 + const fn = new Function('context', `with(context) { return (${rule}); }`); + return fn(context); + } catch (e) { + console.error('表达式解析失败:', e); + return false; + } +} + +// 时间轴项生成 +export const getTimelineItem = ( + approverUserCodes: string[], + data: AuditItem, + requiredType: 'or' | 'and' | 'mixed', + ruleExpression?: string, +) => { + // 过滤掉discard为true的记录 + const records = (data?.records || []).filter((record) => !record.discard); + + // 构建变量上下文(审批通过) + const context = Object.fromEntries( + approverUserCodes.map((code) => [code, records.some((r) => r.approver_user_code === code && r.action === 1)]), + ); + + // 判断是否拒绝 + const isAnyRejected = approverUserCodes.some((code) => + records.some((r) => r.approver_user_code === code && r.action === 2), + ); + + // 获取所有已审批的审批人及其意见 + const approvedApprovers = approverUserCodes + .filter((code) => records.some((r) => r.approver_user_code === code && r.action === 1)) + .map((code) => { + const record = records.find((r) => r.approver_user_code === code && r.action === 1); + return `${code}: ${record?.comment}`; + }); + + const approverCount = approverUserCodes.length; + const approvedCount = approvedApprovers.length; + + let label = '', + theme = 'primary', + dot = renderTimelineIcon('pending', 'primary'); + + // 混合逻辑处理 + if (requiredType === 'mixed' && ruleExpression) { + try { + const result = evaluateRule(ruleExpression, context); + if (result) { + label = `已审批(符合逻辑)`; + theme = 'success'; + dot = renderTimelineIcon('approve', 'success'); + } else { + label = `待审批`; + theme = 'primary'; + dot = renderTimelineIcon('pending', 'primary'); + } + } catch (e) { + label = `逻辑错误`; + theme = 'danger'; + dot = renderTimelineIcon('error', 'danger'); + } + } + // 或签逻辑 + else if (requiredType === 'or') { + // 优先判断是否拒绝 + if (isAnyRejected) { + const rejectReasons = approverUserCodes + .filter((code) => records.some((r) => r.approver_user_code === code && r.action === 2)) + .map((code) => { + const record = records.find((r) => r.approver_user_code === code && r.action === 2); + return `${record?.approver_user_code}: ${record?.comment}`; + }); + label = `已拒绝,${rejectReasons.join(';')}`; + theme = 'danger'; + dot = renderTimelineIcon('reject', 'danger'); + } + // 再判断是否审批通过 + else if (approvedCount > 0) { + label = `已审批,${approvedApprovers.join(';')}`; + theme = 'success'; + dot = renderTimelineIcon('approve', 'success'); + } + } + // 会签逻辑 + else if (requiredType === 'and') { + // 优先判断是否拒绝 + if (isAnyRejected) { + const rejectReasons = approverUserCodes + .filter((code) => records.some((r) => r.approver_user_code === code && r.action === 2)) + .map((code) => { + const record = records.find((r) => r.approver_user_code === code && r.action === 2); + return `${record?.approver_user_code}: ${record?.comment}`; + }); + label = `已拒绝,${rejectReasons.join(';')}`; + theme = 'danger'; + dot = renderTimelineIcon('reject', 'danger'); + } + // 再判断是否全部通过 + else if (approvedCount === approverCount) { + label = `所有人已审批,${approvedApprovers.join(';')}`; + theme = 'success'; + dot = renderTimelineIcon('approve', 'success'); + } else { + label = `${approvedCount}/${approverCount}人已审批,${approvedApprovers.join(';')}`; + theme = 'warning'; + dot = renderTimelineIcon('approve', 'warning'); + } + } + + return { label, theme, dot: () => dot }; +}; + +// 生成时间轴数据 +export const getAllStepData = (data: AuditItem) => { + // 首节点 + const first = { + content: `${data?.user_code} 提交审批`, + label: data?.details.content, + dotColor: 'success', + dot: () => renderTimelineIcon('approve', 'success'), + }; + + // 尾节点 + const last = { + content: '流程结束', + dotColor: 'primary', + dot: () => + data.current_step === data.steps.length + ? renderTimelineIcon('approve', 'success') + : renderTimelineIcon('cancel', 'primary'), + }; + + // 步骤分组(按 step_order 分组) + const stepList = data?.steps?.reduce((acc: Record, cur: AuditStepItem) => { + const key = cur.step_order.toString(); // 仅按步骤分组 + if (!acc[key]) acc[key] = []; + acc[key].push(cur); + return acc; + }, {}); + + // 生成时间轴节点 + return { + total: data?.steps?.length, + current: data?.current_step, + options: [ + first, + ...Object.entries(stepList || {}).map(([key, steps]) => { + // 获取该步骤下的所有审批人 + const approverCodes = steps.map((step) => step.approver_user_code); + + // 获取该步骤的第一个审批人的 required_type + const baseType = steps[0]?.required_type || 'or'; + + // 获取该步骤的第一个审批人的 rule_expression + const ruleExpression = steps[0]?.rule_expression; + + // 根据 required_type 生成连接符 + const separator = baseType === 'and' ? '和' : '或'; + + // 构建显示文本 + const content = ruleExpression ? `审批规则: ${ruleExpression}` : `审批人: ${approverCodes.join(separator)}`; + + // 生成时间轴项 + const timelineItem = getTimelineItem(approverCodes, data, baseType as 'or' | 'and' | 'mixed', ruleExpression); + + return { + content, + ...timelineItem, + }; + }), + last, + ], + }; +}; + +// 获取审批单类型对应的数据键值 +export const getApplicationTypeItemKeys = (type: number) => { + switch (type) { + case AUDIT_TYPE.DEVICE: + return { + eq_code: '设备Code', + lend_time: '借出时间(使用时间)', + return_time: '归还时间', + lender: '借出人', + }; + case AUDIT_TYPE.INTERNET: + return { ip: 'IP地址', mac: 'MAC地址', info: '申请信息/补充信息', timestamp: '时间戳' }; + case AUDIT_TYPE.TASK: + return { + content: '任务内容', + create_user: '创建人', + equipment: '使用设备', + finally_time: '预期完成时间', + name: '任务名称', + place: '工作地点', + remark: '备注', + status: '任务状态', + type: '任务类型', + user: '分配用户', + weight: '权重', + work_time: '工作时间', + }; + default: + return { content: '内容', extra: '其他信息', key: '键', value: '值' }; + } +}; + +export const getApplicationCommonKeys = () => { + return { + id: 'ID', + user_code: '申请人', + visible_allow: '是否可见', + create_time: '创建时间', + update_time: '更新时间', + status: '状态', + type: '类型', + details: '详情', + other_info: '其他信息', + }; +}; + +// 判断当前用户是否可以进行管理操作,判断规则:当前用户是审批单的审批人 +export const checkUserCanOperate = (userCode: string, application: AuditItem) => { + const { steps } = application; + return steps?.some((step) => step.approver_user_code === userCode); +}; + +// 判断当前用户是否可以进行审批操作,判断规则:当前用户是审批单的审批人、当前步骤到该用户审批(包括会签和签和混合签的情况) +export const checkUserCanApprove = (userCode: string, application: AuditItem) => { + const { steps, current_step, records = [] } = application; + + // 如果没有步骤或当前步骤超出范围,不能审批 + if (!steps?.length || current_step > steps.length) { + return { result: false, type: 'step_out_of_range' }; + } + + // 获取当前步骤的所有审批人 + const currentStepApprovers = steps.filter((step) => step.step_order === current_step); + + // 如果当前用户不在当前步骤的审批人列表中,不能审批 + if (!currentStepApprovers.some((step) => step.approver_user_code === userCode)) { + return { result: false, type: 'user_not_in_step' }; + } + + // 获取当前步骤的审批类型(使用第一个审批人的类型,因为同一步骤的审批类型应该相同) + const requiredType = currentStepApprovers[0]?.required_type || 'or'; + const ruleExpression = currentStepApprovers[0]?.rule_expression; + + // 过滤掉discard为true的记录 + const validRecords = records.filter((record) => !record.discard); + + // 获取当前步骤已审批的有效记录 + const currentStepRecords = validRecords.filter((record) => + currentStepApprovers.some((step) => step.approver_user_code === record.approver_user_code), + ); + + // 检查当前用户是否已经审批过 + const hasApproved = currentStepRecords.some( + (record) => record.approver_user_code === userCode && (record.action === 1 || record.action === 2), + ); + + // 如果已经审批过,不能再审批 + if (hasApproved) { + return { result: false, type: 'user_already_approved' }; + } + + // 检查是否有人已经拒绝 + const isAnyRejected = validRecords.some((record) => record.action === 2); + if (isAnyRejected) { + return { result: false, type: 'steps_has_rejected' }; + } + + // 根据不同的审批类型判断 + switch (requiredType) { + case 'and': // 会签 + // 会签情况下,只要当前用户没有审批过,就可以审批 + return { result: true, type: 'and' }; + + case 'or': // 或签 + // 或签情况下,如果没有人审批过,或者虽然有人审批过但还未通过,当前用户都可以审批 + return { result: true, type: 'or' }; + + case 'mixed': // 混合签 + if (!ruleExpression) { + return { result: false, type: 'mixed_no_rule_expression' }; + } + + // 构建当前状态的上下文 + const context = Object.fromEntries( + currentStepApprovers.map((step) => [ + step.approver_user_code, + currentStepRecords.some( + (record) => record.approver_user_code === step.approver_user_code && record.action === 1, + ), + ]), + ); + + // 使用已有的 evaluateRule 函数评估表达式 + // 如果表达式已经满足,则不需要再审批 + if (evaluateRule(ruleExpression, context)) { + return { result: false, type: 'mixed_already_approved' }; + } + // 否则当前用户可以审批 + return { result: true, type: 'mixed' }; + + default: + return { result: false, type: 'default' }; + } +}; + +// 判断当前用户是否可以进行撤销审批操作,判断规则:当前用户是审批单的审批人、当前用户审已审批过、下一审批人未审批 +export const checkUserCanRevert = (userCode: string, application: AuditItem) => { + const { steps, records = [] } = application; + + // 如果没有步骤,不能撤销 + if (!steps?.length) { + return false; + } + + // 过滤掉discard为true的记录 + const validRecords = records.filter((record) => !record.discard); + + // 1. 检查当前用户是否是审批人 + const isApprover = steps.some((step) => step.approver_user_code === userCode); + if (!isApprover) { + return false; + } + + // 2. 检查当前用户是否已审批过(忽略discard为true的记录) + const userRecord = validRecords.find( + (record) => record.approver_user_code === userCode && (record.action === 1 || record.action === 2), // 通过或拒绝的记录 + ); + if (!userRecord) { + return false; + } + + // 获取当前用户的步骤信息 + const userStep = steps.find((step) => step.approver_user_code === userCode); + if (!userStep) { + return false; + } + + // 获取下一步骤的审批人 + const nextStepOrder = userStep.step_order + 1; + const nextStepApprovers = steps.filter((step) => step.step_order === nextStepOrder); + + // 如果没有下一步,说明当前用户是最后一个审批人,可以撤销自己的审批 + if (nextStepApprovers.length === 0) { + return true; + } + + // 3. 检查下一步审批人是否已经审批(忽略discard为true的记录) + const hasNextApproved = validRecords.some( + (record) => + nextStepApprovers.some((approver) => approver.approver_user_code === record.approver_user_code) && + (record.action === 1 || record.action === 2), // 通过或拒绝的记录 + ); + + // 只有当下一步审批人未审批时才能撤销 + return !hasNextApproved; +}; diff --git a/src/pages/audit/utils/renderApprovalDetailInfo.tsx b/src/pages/audit/utils/renderApprovalDetailInfo.tsx new file mode 100644 index 0000000..45f6f77 --- /dev/null +++ b/src/pages/audit/utils/renderApprovalDetailInfo.tsx @@ -0,0 +1,112 @@ +import { Tag } from 'tdesign-vue-next'; +import { defineComponent, PropType, toRefs } from 'vue'; +import type { AuditDetail, AuditDetailItem, AuditItem } from '../type'; +import { getApplicationTypeItemKeys, getStatusColor, renderStatusText } from '.'; +import StatusTag from './renderStatusTag'; +import dayjs from 'dayjs'; + +export default defineComponent({ + name: 'AuditDetailInfo', + props: { + data: Object as PropType, + }, + setup(props) { + const { data } = toRefs(props); + return () => { + if (!data.value) return null; + + const { + type, + status, + details: detailsContent, + user_code, + created_at, + updated_at, + current_step, + advance_approval, + visible_allow, + } = data.value; + + const detailKeysInfo = getApplicationTypeItemKeys(type); + const detailKeys = Object.keys(detailKeysInfo); + const details: AuditDetailItem = JSON.parse(detailsContent?.details as string); + + const renderDetailInfo = () => { + return detailKeys.map((key) => { + const className = ['detail-info-item', `detail-info-item--${key}`]; + return ( +
+
{detailKeysInfo[key]}:
+
{details[key] ?? '-'}
+
+ ); + }); + }; + + const render = () => { + const keyList = { + status: { + content: '当前状态', + cell: ({ data, row }) => { + return ; + }, + }, + current_step: '当前审批节点', + advance_approval: { + content: '仅审批人可审批', + cell: ({ row }) => { + return row ? '是' : '否'; + }, + }, + visible_allow: { + content: '非审批人是否可见', + cell: ({ row }) => { + return row ? '是' : '否'; + }, + }, + created_at: { + content: '创建时间', + cell: ({ row }) => { + return dayjs(row).format('YYYY-MM-DD HH:mm:ss'); + }, + }, + updated_at: { + content: '修改时间', + cell: ({ row }) => { + return dayjs(row).format('YYYY-MM-DD HH:mm:ss'); + }, + }, + }; + const keys = Object.keys(keyList); + const sys_info = keys.map((key) => { + const className = ['application-info-item', `application-info-item--${key}`]; + const title = keyList[key]?.content ?? keyList[key]; + const content = keyList[key]?.cell?.({ data: data.value, row: data.value[key] }) ?? data.value[key]; + return ( +
+
{title}:
+
{content}
+
+ ); + }); + return ( +
+
+
基本信息
+
{sys_info}
+
+ +
+
+ 审批内容明细 +
+
{renderDetailInfo()}
+
+
+ ); + }; + + return render(); + }; + }, +}); diff --git a/src/pages/audit/utils/renderStatusTag.tsx b/src/pages/audit/utils/renderStatusTag.tsx new file mode 100644 index 0000000..43d6f59 --- /dev/null +++ b/src/pages/audit/utils/renderStatusTag.tsx @@ -0,0 +1,32 @@ +import { Tag } from 'tdesign-vue-next'; +import { defineComponent, PropType, toRefs } from 'vue'; +import type { AuditItem } from '../type'; +import { getStatusColor, renderStatusText } from '.'; + +export default defineComponent({ + name: 'AuditStatusTag', + props: { + status: Number, + data: Object as PropType, + size: { + type: String, + default: 'small', + }, + }, + setup(props) { + const { data, status, size } = toRefs(props); + + return () => { + if (!data.value) return null; + + const statusResultColor = getStatusColor(data.value, status.value); + const statusResultText = renderStatusText(data.value, status.value); + + return ( + + {statusResultText} + + ); + }; + }, +}); diff --git a/src/pages/audit/utils/renderTimelineIcon.tsx b/src/pages/audit/utils/renderTimelineIcon.tsx new file mode 100644 index 0000000..9812bc8 --- /dev/null +++ b/src/pages/audit/utils/renderTimelineIcon.tsx @@ -0,0 +1,41 @@ +import { ErrorCircleIcon, GestureRanslationIcon, PendingIcon, TaskChecked1Icon } from 'tdesign-icons-vue-next'; +import { ICON_TYPE, THEME_COLOR } from './constants'; +import { IconType, ThemeColor } from './types'; +import { VNode } from 'vue'; + +/** + * 主题颜色映射到CSS变量 + */ +const THEME_COLOR_MAP: Record = { + [THEME_COLOR.SUCCESS]: 'var(--td-success-color)', + [THEME_COLOR.DANGER]: 'var(--td-error-color)', + [THEME_COLOR.PRIMARY]: 'var(--td-brand-color)', + [THEME_COLOR.WARNING]: 'var(--td-warning-color)', + [THEME_COLOR.DEFAULT]: 'var(--td-text-color-primary)', +}; + +/** + * 渲染时间轴图标 + * @param status 图标类型 + * @param theme 主题颜色 + * @returns 图标组件 + */ +export const renderTimelineIcon = (status: IconType | string, theme: ThemeColor): VNode => { + const color = THEME_COLOR_MAP[theme as ThemeColor] || THEME_COLOR_MAP[THEME_COLOR.DEFAULT]; + + switch (status) { + case ICON_TYPE.APPROVE: + return ; + case ICON_TYPE.REJECT: + return ; + case ICON_TYPE.PENDING: + return ; + case ICON_TYPE.CANCEL: + return ; + case ICON_TYPE.ERROR: + default: + return ; + } +}; + +export default renderTimelineIcon; diff --git a/src/pages/audit/utils/renderTypeTag.tsx b/src/pages/audit/utils/renderTypeTag.tsx new file mode 100644 index 0000000..9d661eb --- /dev/null +++ b/src/pages/audit/utils/renderTypeTag.tsx @@ -0,0 +1,68 @@ +import { Tag } from 'tdesign-vue-next'; +import { defineComponent, toRefs } from 'vue'; +import { AUDIT_TYPE, THEME_COLOR } from './constants'; +import { isObject } from 'lodash-es'; + +/** + * 获取审批类型文本 + * @param type 审批类型 + * @returns 类型文本 + */ +export const getTypeText = (type: number): string => { + switch (type) { + case AUDIT_TYPE.DEVICE: + return '设备借出'; + case AUDIT_TYPE.INTERNET: + return '上网审批'; + case AUDIT_TYPE.TASK: + return '任务审批'; + case AUDIT_TYPE.OTHER: + return '其他'; + default: + return '未知类型'; + } +}; + +/** + * 获取审批类型颜色 + * @param type 审批类型 + * @returns 主题颜色 + */ +export const getTypeColor = (type: number): string | { t: 'diy'; color: string } => { + switch (type) { + case AUDIT_TYPE.DEVICE: + return THEME_COLOR.PRIMARY; + case AUDIT_TYPE.INTERNET: + return THEME_COLOR.SUCCESS; + case AUDIT_TYPE.TASK: + return { t: 'diy', color: 'rgb(32, 131, 168)' }; + case AUDIT_TYPE.OTHER: + return THEME_COLOR.WARNING; + default: + return THEME_COLOR.DEFAULT; + } +}; + +export default defineComponent({ + name: 'AuditTypeTag', + props: { + type: Number, + }, + setup(props) { + const { type } = toRefs(props); + + return () => { + const typeColor = getTypeColor(type.value); + const isCustomColor = isObject(typeColor) && typeColor.t === 'diy'; + return ( + + {getTypeText(type.value)} + + ); + }; + }, +}); diff --git a/src/pages/audit/utils/types.ts b/src/pages/audit/utils/types.ts new file mode 100644 index 0000000..59ce870 --- /dev/null +++ b/src/pages/audit/utils/types.ts @@ -0,0 +1,54 @@ +import { APPROVAL_TYPE, AUDIT_STATUS, THEME_COLOR, ICON_TYPE } from './constants'; +import { AuditItem, AuditRecordItem } from '../type'; +import { VNode } from 'vue'; + +// 常量类型 +export type ApprovalType = typeof APPROVAL_TYPE[keyof typeof APPROVAL_TYPE]; +export type AuditStatus = typeof AUDIT_STATUS[keyof typeof AUDIT_STATUS]; +export type ThemeColor = typeof THEME_COLOR[keyof typeof THEME_COLOR]; +export type IconType = typeof ICON_TYPE[keyof typeof ICON_TYPE]; + +// 审批上下文 +export interface ApprovalContext { + [approverCode: string]: boolean; +} + +// 时间轴项结果 +export interface TimelineItemResult { + label: string; + theme: ThemeColor; + dot: VNode; +} + +// 审批步骤分组 +export interface StepGroup { + [stepOrder: string]: { + approverCodes: string[]; + requiredType: ApprovalType; + ruleExpression?: string; + }; +} + +// 审批状态结果 +export interface ApprovalStatusResult { + text: string; + color: ThemeColor; + value: boolean | null; +} + +// 审批步骤结果 +export interface ApprovalStepResult { + isApproved: boolean; + isRejected: boolean; + approvedCount: number; + totalCount: number; + approvedApprovers: string[]; + rejectedApprovers: string[]; +} + +// 审批记录过滤条件 +export interface RecordFilter { + stepOrder?: number; + approverCode?: string; + action?: AuditStatus; +} From 88bcb8f8487ea4c9aa76194f5ee9f2edcd994a5d Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Tue, 20 May 2025 10:12:35 +0800 Subject: [PATCH 19/41] feat: auditPost 1/3 --- src/pages/audit/auditPost.vue | 518 ++++++++++++++++++ src/pages/audit/auditProgress.vue | 486 +++++++++++++++- src/pages/audit/type.ts | 42 +- src/pages/audit/utils/index.ts | 231 +++++++- .../audit/utils/renderApprovalDetailInfo.tsx | 29 +- .../audit/utils/renderPreviewTimeline.tsx | 181 ++++++ .../audit/utils/renderTimelineSelect.tsx | 30 + 7 files changed, 1484 insertions(+), 33 deletions(-) create mode 100644 src/pages/audit/auditPost.vue create mode 100644 src/pages/audit/utils/renderPreviewTimeline.tsx create mode 100644 src/pages/audit/utils/renderTimelineSelect.tsx diff --git a/src/pages/audit/auditPost.vue b/src/pages/audit/auditPost.vue new file mode 100644 index 0000000..88fc3de --- /dev/null +++ b/src/pages/audit/auditPost.vue @@ -0,0 +1,518 @@ + + + + + diff --git a/src/pages/audit/auditProgress.vue b/src/pages/audit/auditProgress.vue index 12c6703..194a2c1 100644 --- a/src/pages/audit/auditProgress.vue +++ b/src/pages/audit/auditProgress.vue @@ -1,18 +1,480 @@ - - + \ No newline at end of file diff --git a/src/pages/audit/type.ts b/src/pages/audit/type.ts index b2a8ed7..403f236 100644 --- a/src/pages/audit/type.ts +++ b/src/pages/audit/type.ts @@ -48,13 +48,13 @@ export interface AuditRecordItem { export interface AuditStepItem { // 自增id - id: number; + id?: number; // 外键,关联 approval_application.id - application_id: number; + application_id?: number; // 审批人Code approver_user_code: string; // 创建时间 - created_at: string; + created_at?: string; // 步骤顺序 step_order: number; // 多人同步审批配置,or为或,and为与多人同步审批配置,or为或,and为与,mixed为混合 @@ -79,7 +79,7 @@ export interface AuditDetail { export type AuditDetailItem = AuditDetailOfLend | AuditDetailOfNetworkPortal | AuditDetailOfTask | AuditDetailOfOther; export interface AuditDetailOfLend { - equipment_code: string; + eq_code: string; lend_time: string; return_time: string; lender: string; @@ -93,6 +93,9 @@ export interface AuditDetailOfNetworkPortal { } export interface AuditDetailOfTask { + // 操作字段 + operate_type?: 'add' | 'edit' | 'del'; + id?: number; content: string; create_user: string; equipment: string; @@ -102,9 +105,9 @@ export interface AuditDetailOfTask { remark: string; status: number; type: number; - user: string; + user: string[]; weight: number; - work_time: string; + work_time: string[]; } export interface AuditDetailOfOther { @@ -121,3 +124,30 @@ export interface AuditTimeLine { current: number; options: TdTimelineItemProps[]; } + +export interface ApprovalPreviewStepData { + approvalEquipment: { + approver: string; + eq_code: string; + eq_name: string; + }[]; + groupPosition: []; + positions: { + group_id: null | number; + id: number; + isAdmin: 0 | 1; + isOwner: 0 | 1; + name: string; + type: 0 | 1; + }[]; + rule_expression: string; + stepList: { + BZ: string[]; + GL: string[]; + JS: string[]; + }; + userPositions: { + position_id: number; + user_id: number; + }[]; +} diff --git a/src/pages/audit/utils/index.ts b/src/pages/audit/utils/index.ts index 74c4f54..3b51bfa 100644 --- a/src/pages/audit/utils/index.ts +++ b/src/pages/audit/utils/index.ts @@ -2,6 +2,10 @@ import { TdTimelineItemProps } from 'tdesign-vue-next'; import { AuditItem, AuditItems, AuditRecordItem, AuditStepItem } from '../type'; import { APPROVAL_TYPE, AUDIT_TYPE } from './constants'; import renderTimelineIcon from './renderTimelineIcon'; +import { ApprovalPreviewStepData } from '../type'; +import { merge } from 'lodash-es'; +import renderTimelineSelect from './renderTimelineSelect'; +import { ref } from 'vue'; export const getStatusText = (status: number) => { switch (status) { @@ -230,6 +234,76 @@ export const getTimelineItem = ( return { label, theme, dot: () => dot }; }; +interface AuditPreviewStepItem extends AuditStepItem { + content?: any; +} + +// 发起审批时的预览时间轴 +export const getPreviewTimeLineItem = (steps: AuditPreviewStepItem[]) => { + // 按步骤分组(按 step_order 分组) + const stepGroups = steps?.reduce((acc: Record, cur: AuditPreviewStepItem) => { + const key = cur.step_order.toString(); + if (!acc[key]) acc[key] = []; + acc[key].push(cur); + return acc; + }, {}); + + // 生成预览时间轴项 + return Object.entries(stepGroups || {}).map(([key, steps]) => { + // 检查是否为系统自动审批步骤 + const isSystemAutoStep = steps.every( + (step) => step.approver_user_code === 'SYSTEM_AUTO' || !step.approver_user_code, + ); + + // 获取该步骤下的所有审批人 + const approverCodes = steps + .map((step) => + step.approver_user_code === 'SYSTEM_AUTO' || !step.approver_user_code ? null : step.approver_user_code, + ) + .filter(Boolean); + + // 获取该步骤的第一个审批人的 required_type + const baseType = steps[0]?.required_type || 'or'; + + // 获取该步骤的第一个审批人的 rule_expression + const ruleExpression = steps[0]?.rule_expression; + + const customContent = steps[0]?.content; + + // 根据 required_type 生成连接符 + const separator = baseType === 'and' ? '和' : '或'; + + // 构建显示文本 + let content; + if (isSystemAutoStep) { + content = '系统自动审批'; + } else { + content = () => + customContent?.() ?? + (ruleExpression + ? `审批人: ${approverCodes.join('、')},规则: ${ruleExpression}` + : `审批人: ${approverCodes.join(separator)}`); + } + + // 预览状态下的显示 + if (isSystemAutoStep) { + return { + content, + label: '系统自动通过', + dorColor: 'success', + dot: () => renderTimelineIcon('approve', 'success'), + }; + } else { + return { + content, + label: '待审批', + dorColor: 'primary', + dot: () => renderTimelineIcon('pending', 'primary'), + }; + } + }); +}; + // 生成时间轴数据 export const getAllStepData = (data: AuditItem) => { // 首节点 @@ -265,8 +339,17 @@ export const getAllStepData = (data: AuditItem) => { options: [ first, ...Object.entries(stepList || {}).map(([key, steps]) => { + // 检查是否为系统自动审批步骤 + const isSystemAutoStep = steps.every( + (step) => step.approver_user_code === 'SYSTEM_AUTO' || !step.approver_user_code, + ); + // 获取该步骤下的所有审批人 - const approverCodes = steps.map((step) => step.approver_user_code); + const approverCodes = steps + .map((step) => + step.approver_user_code === 'SYSTEM_AUTO' || !step.approver_user_code ? null : step.approver_user_code, + ) + .filter(Boolean); // 获取该步骤的第一个审批人的 required_type const baseType = steps[0]?.required_type || 'or'; @@ -278,10 +361,25 @@ export const getAllStepData = (data: AuditItem) => { const separator = baseType === 'and' ? '和' : '或'; // 构建显示文本 - const content = ruleExpression ? `审批规则: ${ruleExpression}` : `审批人: ${approverCodes.join(separator)}`; + let content; + if (isSystemAutoStep) { + content = '系统自动审批'; + } else { + content = ruleExpression ? `审批规则: ${ruleExpression}` : `审批人: ${approverCodes.join(separator)}`; + } // 生成时间轴项 - const timelineItem = getTimelineItem(approverCodes, data, baseType as 'or' | 'and' | 'mixed', ruleExpression); + let timelineItem; + if (isSystemAutoStep) { + // 系统自动审批步骤始终显示为已通过 + timelineItem = { + label: '系统自动通过', + theme: 'success', + dot: () => renderTimelineIcon('approve', 'success'), + }; + } else { + timelineItem = getTimelineItem(approverCodes, data, baseType as 'or' | 'and' | 'mixed', ruleExpression); + } return { content, @@ -307,6 +405,8 @@ export const getApplicationTypeItemKeys = (type: number) => { return { ip: 'IP地址', mac: 'MAC地址', info: '申请信息/补充信息', timestamp: '时间戳' }; case AUDIT_TYPE.TASK: return { + operate_type: '操作类型', + id: '记录id', content: '任务内容', create_user: '创建人', equipment: '使用设备', @@ -357,6 +457,16 @@ export const checkUserCanApprove = (userCode: string, application: AuditItem) => // 获取当前步骤的所有审批人 const currentStepApprovers = steps.filter((step) => step.step_order === current_step); + // // 检查当前步骤是否为系统自动审批步骤 + // const isSystemAutoStep = currentStepApprovers.every( + // (step) => step.approver_user_code === 'SYSTEM_AUTO' || !step.approver_user_code, + // ); + + // // 如果是系统自动审批步骤,则不需要人工审批 + // if (isSystemAutoStep) { + // return { result: false, type: 'system_auto_approval' }; + // } + // 如果当前用户不在当前步骤的审批人列表中,不能审批 if (!currentStepApprovers.some((step) => step.approver_user_code === userCode)) { return { result: false, type: 'user_not_in_step' }; @@ -479,3 +589,118 @@ export const checkUserCanRevert = (userCode: string, application: AuditItem) => // 只有当下一步审批人未审批时才能撤销 return !hasNextApproved; }; + +export const getPreviewStepList = ( + applicationType: string, + preStepData: ApprovalPreviewStepData, + data, + userList, +): AuditStepItem[] => { + if (!preStepData) { + return []; + } + const departmentOwner = preStepData.stepList.BZ; + const departmentTech = preStepData.stepList.JS; + const departmentdmin = preStepData.stepList.GL; + // 设备列表改成key为设备code的对象,value为数组 + const approvalEquipment = preStepData.approvalEquipment.reduce((acc, cur) => { + acc[cur.eq_code] = [...(acc[cur.eq_code] || []), cur]; + return acc; + }, {}); + const userGroupAdmin = preStepData.groupPosition; + + // 合并数据并去重 + const mergeStepList = (...args: string[][]) => { + return [...new Set(args.flat())]; + }; + + // 设备借出审批,第一个审批人为部长或技术或管理,第二个审批人为物主 + if (applicationType === 'equipment') { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } else if (step_order === 2) { + const approverList = approvalEquipment[data?.eq_code]?.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver?.approver ?? '', + rule_expression: '', + }; + }); + if (!approverList) { + return { + step_order, + required_type: 'or', + approver_user_code: '[当前设备不需审批] 系统将自动通过', + rule_expression: '', + }; + } + return approverList; + } + }); + } + + // 任务审批,第一个审批人为上级组长,第二个审批人为部长或技术或管理(或签) + else if (applicationType === 'task') { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return userGroupAdmin.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } else if (step_order === 2) { + return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } + }); + } + + // 其他审批,第一个审批人默认为组长,支持自定义,第二个审批人默认为组长部长或技术或管理(或签),支持自定义 + else { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return userGroupAdmin.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + content: () => renderTimelineSelect(userGroupAdmin, userList), + rule_expression: '', + }; + }); + } else if (step_order === 2) { + const msl = ref(mergeStepList(departmentOwner, departmentTech, departmentdmin)); + return msl.value.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + content: () => + renderTimelineSelect(msl.value, userList, (value) => { + msl.value = value; + }), + rule_expression: '', + }; + }); + } + }); + } +}; diff --git a/src/pages/audit/utils/renderApprovalDetailInfo.tsx b/src/pages/audit/utils/renderApprovalDetailInfo.tsx index 45f6f77..d2c1bbe 100644 --- a/src/pages/audit/utils/renderApprovalDetailInfo.tsx +++ b/src/pages/audit/utils/renderApprovalDetailInfo.tsx @@ -1,6 +1,6 @@ import { Tag } from 'tdesign-vue-next'; import { defineComponent, PropType, toRefs } from 'vue'; -import type { AuditDetail, AuditDetailItem, AuditItem } from '../type'; +import type { AuditDetail, AuditDetailItem, AuditDetailOfTask, AuditItem } from '../type'; import { getApplicationTypeItemKeys, getStatusColor, renderStatusText } from '.'; import StatusTag from './renderStatusTag'; import dayjs from 'dayjs'; @@ -15,25 +15,30 @@ export default defineComponent({ return () => { if (!data.value) return null; - const { - type, - status, - details: detailsContent, - user_code, - created_at, - updated_at, - current_step, - advance_approval, - visible_allow, - } = data.value; + const { type, details: detailsContent } = data.value; const detailKeysInfo = getApplicationTypeItemKeys(type); const detailKeys = Object.keys(detailKeysInfo); const details: AuditDetailItem = JSON.parse(detailsContent?.details as string); const renderDetailInfo = () => { + // 忽略的字段,支持自定义配置 + const discardKeys = { + id: { + discard: (application: AuditItem) => { + if (application?.type === 3 && (details as AuditDetailOfTask)?.operate_type !== 'add') { + return true; + } + return false; + }, + }, + }; + return detailKeys.map((key) => { const className = ['detail-info-item', `detail-info-item--${key}`]; + if (discardKeys[key]?.discard?.(data.value)) { + return null; + } return (
{detailKeysInfo[key]}:
diff --git a/src/pages/audit/utils/renderPreviewTimeline.tsx b/src/pages/audit/utils/renderPreviewTimeline.tsx new file mode 100644 index 0000000..a1d4463 --- /dev/null +++ b/src/pages/audit/utils/renderPreviewTimeline.tsx @@ -0,0 +1,181 @@ +import { computed, defineComponent, PropType, ref, toRefs } from 'vue'; +import { getPreviewStepList, getPreviewTimeLineItem } from '.'; +import renderTimelineIcon from './renderTimelineIcon'; +import { ApprovalPreviewStepData, AuditStepItem } from '../type'; +import renderTimelineSelect from './renderTimelineSelect'; + +export default defineComponent({ + name: 'AuditPreviewTimeline', + props: { + application: Number, + prepareStepList: Object as PropType, + userList: Array, + data: Object, + }, + setup(props) { + const { application, prepareStepList, userList: usersList, data } = toRefs(props); + + // applicationType: string, + // preStepData: ApprovalPreviewStepData, + // data, + // userList, + + return () => { + const preStepData = computed(() => { + return prepareStepList.value; + }); + const applicationType = computed(() => { + return application.value === 1 ? 'equipment' : application.value === 3 ? 'task' : 'other'; + }); + const userList = computed(() => { + return usersList.value; + }); + + const getPreviewStepList = computed(() => { + if (!preStepData.value) { + return []; + } + const departmentOwner = preStepData.value.stepList.BZ; + const departmentTech = preStepData.value.stepList.JS; + const departmentdmin = preStepData.value.stepList.GL; + // 设备列表改成key为设备code的对象,value为数组 + const approvalEquipment = preStepData.value.approvalEquipment.reduce((acc, cur) => { + acc[cur.eq_code] = [...(acc[cur.eq_code] || []), cur]; + return acc; + }, {}); + const userGroupAdmin = preStepData.value.groupPosition; + + // 合并数据并去重 + const mergeStepList = (...args: string[][]) => { + return [...new Set(args.flat())]; + }; + + // 设备借出审批,第一个审批人为部长或技术或管理,第二个审批人为物主 + if (applicationType.value === 'equipment') { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } else if (step_order === 2) { + const approverList = approvalEquipment[data.value?.eq_code]?.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver?.approver ?? '', + rule_expression: '', + }; + }); + if (!approverList) { + return { + step_order, + required_type: 'or', + approver_user_code: '[当前设备不需审批] 系统将自动通过', + rule_expression: '', + }; + } + return approverList; + } + }); + } + + // 任务审批,第一个审批人为上级组长,第二个审批人为部长或技术或管理(或签) + else if (applicationType.value === 'task') { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return userGroupAdmin.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } else if (step_order === 2) { + return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + rule_expression: '', + }; + }); + } + }); + } + + // 其他审批,第一个审批人默认为组长,支持自定义,第二个审批人默认为组长部长或技术或管理(或签),支持自定义 + else { + return [1, 2].flatMap((step_order) => { + if (step_order === 1) { + return userGroupAdmin.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + content: () => renderTimelineSelect(userGroupAdmin, userList.value), + rule_expression: '', + }; + }); + } else if (step_order === 2) { + const msl = ref(mergeStepList(departmentOwner, departmentTech, departmentdmin)); + return msl.value.map((approver) => { + return { + step_order, + required_type: 'or', + approver_user_code: approver, + content: () => + renderTimelineSelect(msl.value, userList.value, (value) => { + msl.value = value; + }), + rule_expression: '', + }; + }); + } + }); + } + }); + + const previewStep = computed(() => { + const stepData = getPreviewStepList.value; + // ${props?.userCode}, + return [ + { + content: `提交审批`, + label: '等待中', + dotColor: 'success', + dot: () => renderTimelineIcon('pending', 'primary'), + }, + ...getPreviewTimeLineItem(stepData), + { + content: '流程结束', + dotColor: 'primary', + dot: () => renderTimelineIcon('pending', 'primary'), + }, + ]; + }); + + const renderItem = () => { + const result = []; + for (const key in previewStep.value) { + const element = previewStep.value[key]; + result.push(); + } + return result; + }; + + console.log(renderItem()); + + return ( + + {renderItem()} + + ); + }; + }, +}); diff --git a/src/pages/audit/utils/renderTimelineSelect.tsx b/src/pages/audit/utils/renderTimelineSelect.tsx new file mode 100644 index 0000000..36daa37 --- /dev/null +++ b/src/pages/audit/utils/renderTimelineSelect.tsx @@ -0,0 +1,30 @@ +import { VNode } from 'vue'; +import { Select } from 'tdesign-vue-next'; + +/** + * 渲染时间轴选择 + * @param status 图标类型 + * @returns 图标组件 + */ +export const renderTimelineSelect = (value: string[], data: any[], onSelect?: (value: string[]) => void): VNode => { + console.log(value); + const cData = data.map((item) => { + return { + label: `${item.name}[${item.code}]`, + value: item.code, + }; + }); + return ( + { - onSelect(values); - }} - /> - ); -}; - -export default renderTimelineSelect; + + return () => ( +