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"
>
-
+
-
-
-
+
Copyright © 2021-2025 MTB All right reserved.
@@ -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 (
+
+ );
+ });
+ return (
+
+
+
+
+
+
{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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交审批
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
+
+
+
+
+
+
+
+
+
+ {{ getStatusText(row.status) }}
+
+
+
+
+
+ {{ getTypeText(row.type) }}
+
+
+
+
+ 查看
+
+
+
+
+
+
+
+
+ {{ getStatusText(row.status) }}
+
+
+
+
+
+ {{ getTypeText(row.type) }}
+
+
+
+
+ 查看
+
+ 审批
+
+
+
+
+
+
+
+
+
+
+
+
+
基本信息
+
+
+
+
+
审批内容
+
+
+
+
+
+
+
+
+
+
+
+
审批流程
+
+
+
+
+
+
+
+ {{ record.approver_user_name }} ({{ record.approver_user_code }})
+ {{ getActionText(record.action) }}
+
+
{{ formatDate(record.created_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 通过
+ 拒绝
+
+
+
+
+
+
+
+
+
+
-
-
+
\ 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 (
+
@@ -213,7 +207,6 @@ import {
} from './type';
import { getPreviewStepList, getPreviewTimeLineItem } from './utils';
import renderTimelineIcon from './utils/renderTimelineIcon';
-import previewTimeline from './utils/renderPreviewTimeline';
const route = useRoute();
diff --git a/src/pages/audit/utils/index.ts b/src/pages/audit/utils/index.tsx
similarity index 94%
rename from src/pages/audit/utils/index.ts
rename to src/pages/audit/utils/index.tsx
index 3b51bfa..618084e 100644
--- a/src/pages/audit/utils/index.ts
+++ b/src/pages/audit/utils/index.tsx
@@ -1,10 +1,8 @@
-import { TdTimelineItemProps } from 'tdesign-vue-next';
import { AuditItem, AuditItems, AuditRecordItem, AuditStepItem } from '../type';
-import { APPROVAL_TYPE, AUDIT_TYPE } from './constants';
+import { AUDIT_TYPE } from './constants';
import renderTimelineIcon from './renderTimelineIcon';
import { ApprovalPreviewStepData } from '../type';
-import { merge } from 'lodash-es';
-import renderTimelineSelect from './renderTimelineSelect';
+import RenderTimelineSelect from './renderTimelineSelect';
import { ref } from 'vue';
export const getStatusText = (status: number) => {
@@ -601,13 +599,13 @@ export const getPreviewStepList = (
}
const departmentOwner = preStepData.stepList.BZ;
const departmentTech = preStepData.stepList.JS;
- const departmentdmin = preStepData.stepList.GL;
+ const departmentAdmin = 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 userGroupAdmin = preStepData.groupPosition as string[];
// 合并数据并去重
const mergeStepList = (...args: string[][]) => {
@@ -618,7 +616,7 @@ export const getPreviewStepList = (
if (applicationType === 'equipment') {
return [1, 2].flatMap((step_order) => {
if (step_order === 1) {
- return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => {
+ return mergeStepList(departmentOwner, departmentTech, departmentAdmin).map((approver) => {
return {
step_order,
required_type: 'or',
@@ -661,7 +659,7 @@ export const getPreviewStepList = (
};
});
} else if (step_order === 2) {
- return mergeStepList(departmentOwner, departmentTech, departmentdmin).map((approver) => {
+ return mergeStepList(departmentOwner, departmentTech, departmentAdmin).map((approver) => {
return {
step_order,
required_type: 'or',
@@ -677,26 +675,40 @@ export const getPreviewStepList = (
else {
return [1, 2].flatMap((step_order) => {
if (step_order === 1) {
- return userGroupAdmin.map((approver) => {
+ const initialApprovers = mergeStepList(userGroupAdmin);
+ const approvers = ref([...userGroupAdmin]);
+
+ return initialApprovers.map((approver) => {
return {
step_order,
required_type: 'or',
approver_user_code: approver,
- content: () => renderTimelineSelect(userGroupAdmin, userList),
+ content: () => {
+ const handleSelect = (newValues) => {
+ approvers.value = [...newValues];
+ };
+
+ return ;
+ },
rule_expression: '',
};
});
} else if (step_order === 2) {
- const msl = ref(mergeStepList(departmentOwner, departmentTech, departmentdmin));
- return msl.value.map((approver) => {
+ const initialApprovers = mergeStepList(departmentOwner, departmentTech, departmentAdmin);
+ const approvers = ref([...initialApprovers]);
+
+ return initialApprovers.map((approver) => {
return {
step_order,
required_type: 'or',
approver_user_code: approver,
- content: () =>
- renderTimelineSelect(msl.value, userList, (value) => {
- msl.value = value;
- }),
+ content: () => {
+ const handleSelect = (newValues) => {
+ approvers.value = [...newValues];
+ };
+
+ return ;
+ },
rule_expression: '',
};
});
diff --git a/src/pages/audit/utils/renderApprovalDetailInfo.tsx b/src/pages/audit/utils/renderApprovalDetailInfo.tsx
index d2c1bbe..97f602b 100644
--- a/src/pages/audit/utils/renderApprovalDetailInfo.tsx
+++ b/src/pages/audit/utils/renderApprovalDetailInfo.tsx
@@ -1,7 +1,6 @@
-import { Tag } from 'tdesign-vue-next';
import { defineComponent, PropType, toRefs } from 'vue';
import type { AuditDetail, AuditDetailItem, AuditDetailOfTask, AuditItem } from '../type';
-import { getApplicationTypeItemKeys, getStatusColor, renderStatusText } from '.';
+import { getApplicationTypeItemKeys } from '.';
import StatusTag from './renderStatusTag';
import dayjs from 'dayjs';
diff --git a/src/pages/audit/utils/renderPreviewTimeline.tsx b/src/pages/audit/utils/renderPreviewTimeline.tsx
deleted file mode 100644
index a1d4463..0000000
--- a/src/pages/audit/utils/renderPreviewTimeline.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-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
index 36daa37..7db9c31 100644
--- a/src/pages/audit/utils/renderTimelineSelect.tsx
+++ b/src/pages/audit/utils/renderTimelineSelect.tsx
@@ -1,30 +1,71 @@
-import { VNode } from 'vue';
+import { PropType, defineComponent, ref, toRefs, watch } 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}]`,
+interface UserOption {
+ name: string;
+ code: string;
+ [key: string]: any;
+}
+
+export default defineComponent({
+ name: 'RenderTimelineSelect',
+ props: {
+ value: {
+ type: Array as PropType,
+ },
+ data: {
+ type: Array as PropType,
+ },
+ onSelect: {
+ type: Function as PropType<(value: string[]) => void>,
+ },
+ },
+ setup(props) {
+ const { value, data } = toRefs(props);
+
+ // 使用ref创建响应式状态
+ const selectedValues = ref([...value.value]);
+
+ // 监听外部value变化
+ watch(
+ () => value.value,
+ (newVal) => {
+ selectedValues.value = [...newVal];
+ },
+ { immediate: true },
+ );
+
+ const options = data.value.map((item) => ({
+ label: `${item.name} [${item.code}]`,
value: item.code,
+ }));
+
+ // 处理选择变化
+ const handleChange = (values: string[]) => {
+ selectedValues.value = [...values];
+ props?.onSelect?.([...values]);
+ };
+
+ // 处理搜索,支持模糊搜索
+ const handleSearch = (word: string, option) => {
+ return (
+ option.label.indexOf(word.toLowerCase()) !== -1 || option.label.toLowerCase().indexOf(word.toLowerCase()) !== -1
+ );
};
- });
- return (
-