diff --git a/apps/web-antdv-next/src/api/core/user.ts b/apps/web-antdv-next/src/api/core/user.ts index d759e5637..ce8ea8d25 100644 --- a/apps/web-antdv-next/src/api/core/user.ts +++ b/apps/web-antdv-next/src/api/core/user.ts @@ -1,6 +1,7 @@ import type { UserInfo } from '@vben/types'; import type { SysDeptResult, SysRoleResult } from '#/api'; +import type { PaginationResult } from '#/types'; import { requestClient } from '#/api/request'; @@ -91,7 +92,10 @@ export async function getUserInfoApi() { } export async function getSysUserListApi(params: SysUserParams) { - return requestClient.get('/api/v1/sys/users', { params }); + return requestClient.get>( + '/api/v1/sys/users', + { params }, + ); } export async function createSysUserApi(data: SysAddUserParams) { diff --git a/apps/web-antdv-next/src/plugins/workflow/api/index.ts b/apps/web-antdv-next/src/plugins/workflow/api/index.ts new file mode 100644 index 000000000..3139cc090 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/api/index.ts @@ -0,0 +1,449 @@ +import type { + WorkflowDefinitionEditorValue, + WorkflowFlowConfig, + WorkflowFormConfig, +} from '#/plugins/workflow/types'; +import type { PaginationResult } from '#/types'; + +import { requestClient } from '#/api/request'; +import { + DEFAULT_WORKFLOW_FLOW_CONFIG, + DEFAULT_WORKFLOW_FORM_CONFIG, +} from '#/plugins/workflow/types'; + +export interface WorkflowDefinitionParams { + name?: string; + code?: string; + page?: number; + size?: number; +} + +export interface WorkflowCategoryParams { + name?: string; + code?: string; + page?: number; + size?: number; +} + +export interface WorkflowCategoryResult { + id: number; + name: string; + code: string; + remark?: null | string; + sort: number; + status: number; + created_time: string; + updated_time?: null | string; +} + +export interface CreateWorkflowCategoryParams { + name: string; + code: string; + remark?: null | string; + sort: number; + status: number; +} + +export type UpdateWorkflowCategoryParams = CreateWorkflowCategoryParams; + +export interface WorkflowDefinitionResult { + id: number; + category_id?: null | number; + category_name?: null | string; + name: string; + code: string; + description?: null | string; + form_config?: null | WorkflowFormConfig; + flow_config?: null | WorkflowFlowConfig; + status: number; + allow_withdraw: boolean; + allow_urge: boolean; + created_time: string; + updated_time?: null | string; +} + +export interface CreateWorkflowDefinitionParams { + category_id?: null | number; + name: string; + code: string; + description?: null | string; + form_config?: null | WorkflowFormConfig; + flow_config?: null | WorkflowFlowConfig; + status: number; + allow_withdraw: boolean; + allow_urge: boolean; +} + +export type UpdateWorkflowDefinitionParams = CreateWorkflowDefinitionParams; + +export interface WorkflowInstanceResult { + id: number; + instance_no: string; + definition_id: number; + title: string; + initiator_id: number; + status: string; + current_task_id?: null | number; + form_data?: null | Record | string; + remark?: null | string; + todo_count?: null | number; + allow_withdraw?: boolean | null; + allow_urge?: boolean | null; + messages?: WorkflowMessageResult[]; + created_time: string; + updated_time?: null | string; +} + +export interface WorkflowMessageResult { + id: number; + receiver_id: number; + instance_id?: null | number; + task_id?: null | number; + message_type: string; + title: string; + content: string; + is_read: boolean; + created_time: string; + updated_time?: null | string; +} + +export interface WorkflowPreviewFlowItem { + node_id: string; + node_type: string; + label: string; + assignee_id?: null | number; + assignee_name?: null | string; + self_select_options?: number[]; + self_select_option_labels?: Record; +} + +export interface StartWorkflowInstanceParams { + definition_id: number; + title: string; + form_data: Record; + self_select_assignees?: Record; + remark?: null | string; +} + +export interface ApproveWorkflowTaskParams { + comment?: null | string; +} + +export interface RejectWorkflowTaskParams { + comment?: null | string; +} + +function safeParseJson( + value: null | string | T | undefined, + fallback: T, +): T { + if (!value) { + return fallback; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +} + +function serializeDefinitionPayload(data: CreateWorkflowDefinitionParams) { + return { + ...data, + form_config: JSON.stringify( + data.form_config ?? DEFAULT_WORKFLOW_FORM_CONFIG, + ), + flow_config: JSON.stringify( + data.flow_config ?? DEFAULT_WORKFLOW_FLOW_CONFIG, + ), + }; +} + +function normalizeConditionNodes( + flowConfig: WorkflowFlowConfig, +): WorkflowFlowConfig { + return { + ...flowConfig, + nodes: flowConfig.nodes.map((node) => { + if (node.type !== 'CONDITION') { + return node; + } + const conditionGroup = node.data.conditionGroup ?? { + operator: 'AND', + conditions: [ + { + field: node.data.conditionField ?? '', + operator: node.data.conditionOperator ?? 'EQ', + value: node.data.conditionValue ?? '', + }, + ], + }; + return { + ...node, + data: { + ...node.data, + conditionGroup, + }, + }; + }), + }; +} + +function normalizeDefinition(result: { + allow_urge: boolean; + allow_withdraw: boolean; + category_id?: null | number; + category_name?: null | string; + code: string; + created_time: string; + description?: null | string; + flow_config?: null | string | WorkflowFlowConfig; + form_config?: null | string | WorkflowFormConfig; + id: number; + name: string; + status: number; + updated_time?: null | string; +}): WorkflowDefinitionResult { + return { + ...result, + form_config: safeParseJson( + result.form_config, + DEFAULT_WORKFLOW_FORM_CONFIG, + ), + flow_config: normalizeConditionNodes( + safeParseJson(result.flow_config, DEFAULT_WORKFLOW_FLOW_CONFIG), + ), + }; +} + +export function buildWorkflowDefinitionEditorValue( + detail?: null | Partial, +): WorkflowDefinitionEditorValue { + return { + category_id: detail?.category_id ?? null, + name: detail?.name ?? '', + code: detail?.code ?? '', + description: detail?.description ?? '', + status: detail?.status ?? 0, + allow_withdraw: detail?.allow_withdraw ?? true, + allow_urge: detail?.allow_urge ?? true, + flow_config: detail?.flow_config ?? DEFAULT_WORKFLOW_FLOW_CONFIG, + form_config: detail?.form_config ?? DEFAULT_WORKFLOW_FORM_CONFIG, + }; +} + +export async function getWorkflowDefinitionListApi( + params: WorkflowDefinitionParams, +) { + const result = await requestClient.get< + PaginationResult + >('/api/v1/workflow/definition', { params }); + return { + ...result, + items: result.items.map((item) => normalizeDefinition(item)), + }; +} + +export async function getWorkflowDefinitionAvailableApi( + params: WorkflowDefinitionParams, +) { + const result = await requestClient.get< + PaginationResult + >('/api/v1/workflow/definition/available', { params }); + return { + ...result, + items: result.items.map((item) => normalizeDefinition(item)), + }; +} + +export async function createWorkflowDefinitionApi( + data: CreateWorkflowDefinitionParams, +) { + return await requestClient.post( + '/api/v1/workflow/definition', + serializeDefinitionPayload(data), + ); +} + +export async function updateWorkflowDefinitionApi( + pk: number, + data: UpdateWorkflowDefinitionParams, +) { + return await requestClient.put( + `/api/v1/workflow/definition/${pk}`, + serializeDefinitionPayload(data), + ); +} + +export async function getWorkflowCategoryListApi( + params: WorkflowCategoryParams, +) { + return await requestClient.get>( + '/api/v1/workflow/category', + { params }, + ); +} + +export async function createWorkflowCategoryApi( + data: CreateWorkflowCategoryParams, +) { + return await requestClient.post('/api/v1/workflow/category', data); +} + +export async function updateWorkflowCategoryApi( + pk: number, + data: UpdateWorkflowCategoryParams, +) { + return await requestClient.put(`/api/v1/workflow/category/${pk}`, data); +} + +export async function getWorkflowTodoCountApi() { + return await requestClient.get( + '/api/v1/workflow/instance/todo-count', + ); +} + +export async function getWorkflowInstanceDetailApi(pk: number) { + return await requestClient.get( + `/api/v1/workflow/instance/${pk}`, + ); +} + +export async function getWorkflowDefinitionDetailApi(pk: number) { + const result = await requestClient.get( + `/api/v1/workflow/definition/${pk}`, + ); + return normalizeDefinition(result); +} + +export async function getWorkflowDefinitionAvailableDetailApi(pk: number) { + const result = await requestClient.get( + `/api/v1/workflow/definition/available/${pk}`, + ); + return normalizeDefinition(result); +} + +export async function getWorkflowCategoryAllApi() { + const result = await requestClient.get< + PaginationResult + >('/api/v1/workflow/category', { + params: { + page: 1, + size: 200, + }, + }); + return result.items; +} + +export async function getWorkflowCategoryOptionsApi() { + const items = await getWorkflowCategoryAllApi(); + return items.map((item) => ({ + label: item.name, + value: item.id, + })); +} + +export async function getWorkflowInstanceListForApplyApi(params: { + page?: number; + size?: number; +}) { + return await requestClient.get>( + '/api/v1/workflow/instance/my-apply', + { params }, + ); +} + +export async function getWorkflowInstanceListForTodoApi(params: { + page?: number; + size?: number; +}) { + return await requestClient.get>( + '/api/v1/workflow/instance/my-todo', + { params }, + ); +} + +export async function getWorkflowMyTodoListApi(params: { + page?: number; + size?: number; +}) { + return await getWorkflowInstanceListForTodoApi(params); +} + +export async function getWorkflowMyApplyListApi(params: { + page?: number; + size?: number; +}) { + return await getWorkflowInstanceListForApplyApi(params); +} + +export async function getWorkflowDefinitionAvailablePreviewFlowApi( + pk: number, + formData: Record, + selfSelectAssignees?: Record, +) { + return await requestClient.get<{ items: WorkflowPreviewFlowItem[] }>( + `/api/v1/workflow/definition/available/${pk}/preview-flow`, + { + params: { + form_data: JSON.stringify({ + ...formData, + __self_select_assignees__: selfSelectAssignees ?? {}, + }), + }, + }, + ); +} + +export async function createWorkflowInstanceApi( + data: StartWorkflowInstanceParams, +) { + return await requestClient.post('/api/v1/workflow/instance', data); +} + +export async function approveWorkflowTaskApi( + pk: number, + data: ApproveWorkflowTaskParams, +) { + return await requestClient.post(`/api/v1/workflow/task/${pk}/approve`, data); +} + +export async function rejectWorkflowTaskApi( + pk: number, + data: RejectWorkflowTaskParams, +) { + return await requestClient.post(`/api/v1/workflow/task/${pk}/reject`, data); +} + +export async function withdrawWorkflowInstanceApi(pk: number) { + return await requestClient.post( + `/api/v1/workflow/instance/${pk}/withdraw`, + ); +} + +export async function urgeWorkflowInstanceApi(pk: number) { + return await requestClient.post(`/api/v1/workflow/instance/${pk}/urge`); +} + +export async function getWorkflowMessageListApi(params: { + page?: number; + size?: number; +}) { + return await requestClient.get>( + '/api/v1/workflow/message', + { params }, + ); +} + +export async function getWorkflowUnreadCountApi() { + return await requestClient.get( + '/api/v1/workflow/message/unread-count', + ); +} + +export async function readWorkflowMessageApi(pk: number) { + return await requestClient.put(`/api/v1/workflow/message/${pk}/read`); +} diff --git a/apps/web-antdv-next/src/plugins/workflow/components/FlowDesigner/index.vue b/apps/web-antdv-next/src/plugins/workflow/components/FlowDesigner/index.vue new file mode 100644 index 000000000..300d8c6fc --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/components/FlowDesigner/index.vue @@ -0,0 +1,1669 @@ + + + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/components/WorkflowRoleSelect.vue b/apps/web-antdv-next/src/plugins/workflow/components/WorkflowRoleSelect.vue new file mode 100644 index 000000000..b020b44ba --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/components/WorkflowRoleSelect.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/components/WorkflowUserSelect.vue b/apps/web-antdv-next/src/plugins/workflow/components/WorkflowUserSelect.vue new file mode 100644 index 000000000..246af23b2 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/components/WorkflowUserSelect.vue @@ -0,0 +1,201 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/langs/en-US/workflow.json b/apps/web-antdv-next/src/plugins/workflow/langs/en-US/workflow.json new file mode 100644 index 000000000..32e20e7c1 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/langs/en-US/workflow.json @@ -0,0 +1,9 @@ +{ + "menu": "Workflow", + "category": "Categories", + "definition": "Definitions", + "start": "Start Application", + "todo": "My Todo", + "apply": "My Apply", + "message": "Messages" +} diff --git a/apps/web-antdv-next/src/plugins/workflow/langs/zh-CN/workflow.json b/apps/web-antdv-next/src/plugins/workflow/langs/zh-CN/workflow.json new file mode 100644 index 000000000..b7d96af44 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/langs/zh-CN/workflow.json @@ -0,0 +1,9 @@ +{ + "menu": "审批流", + "category": "流程分类", + "definition": "流程定义", + "start": "发起申请", + "todo": "待我审批", + "apply": "我的申请", + "message": "审批消息" +} diff --git a/apps/web-antdv-next/src/plugins/workflow/routes/index.ts b/apps/web-antdv-next/src/plugins/workflow/routes/index.ts new file mode 100644 index 000000000..669a487e6 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/routes/index.ts @@ -0,0 +1,112 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + name: 'WorkflowStartApply', + path: '/plugins/workflow/start', + component: () => import('#/plugins/workflow/views/start-apply.vue'), + meta: { + title: '发起申请', + icon: 'mdi:play-circle-outline', + }, + }, + { + name: 'WorkflowDefinition', + path: '/plugins/workflow/definition', + component: () => import('#/plugins/workflow/views/definition.vue'), + meta: { + title: $t('workflow.definition'), + icon: 'carbon:flow', + }, + }, + { + name: 'WorkflowEditor', + path: '/plugins/workflow/design/editor/:definitionId?', + component: () => import('#/plugins/workflow/views/editor.vue'), + meta: { + title: '流程设计器', + hideInMenu: true, + icon: 'carbon:flow-connection', + }, + }, + { + name: 'WorkflowFormEditor', + path: '/plugins/workflow/design/form-editor/:definitionId', + component: () => import('#/plugins/workflow/views/form-editor.vue'), + meta: { + title: '表单设计器', + hideInMenu: true, + icon: 'mdi:form-select', + }, + }, + { + name: 'WorkflowTodo', + path: '/plugins/workflow/my-todo', + component: () => import('#/plugins/workflow/views/my-todo.vue'), + meta: { + title: $t('workflow.todo'), + icon: 'mdi:clipboard-clock-outline', + }, + }, + { + name: 'WorkflowApply', + path: '/plugins/workflow/my-apply', + component: () => import('#/plugins/workflow/views/my-apply.vue'), + meta: { + title: $t('workflow.apply'), + icon: 'mdi:file-document-edit-outline', + }, + }, + { + name: 'WorkflowStart', + path: '/plugins/workflow/instance/start/:definitionId', + component: () => import('#/plugins/workflow/views/start.vue'), + meta: { + title: '发起申请', + hideInMenu: true, + icon: 'mdi:play-circle-outline', + }, + }, + { + name: 'WorkflowMessage', + path: '/plugins/workflow/message', + component: () => import('#/plugins/workflow/views/message.vue'), + meta: { + title: $t('workflow.message'), + icon: 'mdi:message-badge-outline', + }, + }, + { + name: 'WorkflowStatistics', + path: '/plugins/workflow/statistics', + component: () => import('#/plugins/workflow/views/statistics.vue'), + meta: { + title: '统计报表', + icon: 'mdi:chart-box-outline', + }, + }, + { + name: 'WorkflowDetail', + path: '/plugins/workflow/detail/:id', + component: () => import('#/plugins/workflow/views/detail.vue'), + meta: { + title: '流程详情', + hideInMenu: true, + icon: 'mdi:file-search-outline', + }, + }, + { + name: 'WorkflowInstanceDetail', + path: '/plugins/workflow/instance/detail/:id', + component: () => import('#/plugins/workflow/views/detail.vue'), + meta: { + title: '流程详情', + hideInMenu: true, + icon: 'mdi:file-search-outline', + }, + }, +]; + +export default routes; diff --git a/apps/web-antdv-next/src/plugins/workflow/types.ts b/apps/web-antdv-next/src/plugins/workflow/types.ts new file mode 100644 index 000000000..0cc804e1c --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/types.ts @@ -0,0 +1,410 @@ +export type WorkflowNodeType = + | 'APPROVER' + | 'CC' + | 'CONDITION' + | 'END' + | 'PARALLEL' + | 'START' + | 'TRIGGER'; + +export type WorkflowApproverType = + | 'DEPT_LEADER' + | 'DEPT_LEADER_UP' + | 'DESIGNATED_ROLE' + | 'DESIGNATED_USER' + | 'FORM_FIELD_USER' + | 'INITIATOR' + | 'INITIATOR_LEADER' + | 'SELF_SELECT'; + +export interface WorkflowNodePosition { + x: number; + y: number; +} + +export type WorkflowConditionOperator = + | 'CONTAINS' + | 'EQ' + | 'GT' + | 'GTE' + | 'LT' + | 'LTE' + | 'NEQ'; + +export interface WorkflowConditionRule { + field: string; + operator: WorkflowConditionOperator; + value: string; +} + +export interface WorkflowConditionGroup { + operator: 'AND' | 'OR'; + conditions: WorkflowConditionRule[]; +} + +export interface WorkflowNodeData { + label: string; + approverType?: WorkflowApproverType; + approveMode?: 'COUNTERSIGN' | 'OR_SIGN'; + countersignRatio?: number; + refuseMode?: 'BACK_TO_PREV' | 'BACK_TO_START' | 'TERMINATE'; + selfApprove?: boolean; + timeoutHours?: null | number; + timeoutAction?: 'AUTO_APPROVE' | 'AUTO_REJECT' | 'NOTIFY'; + approverIds?: number[]; + approverId?: null | number; + approverName?: string; + roleIds?: number[]; + formFieldKey?: string; + deptLevel?: number; + selfSelectOptions?: number[]; + conditionText?: string; + conditionField?: string; + conditionOperator?: WorkflowConditionOperator; + conditionValue?: string; + conditionGroup?: WorkflowConditionGroup; + ccUserIds?: number[]; +} + +export interface WorkflowEditorNode { + id: string; + type: WorkflowNodeType; + position: WorkflowNodePosition; + data: WorkflowNodeData; +} + +export interface WorkflowEditorEdge { + id: string; + source: string; + target: string; +} + +export interface WorkflowFlowConfig { + nodes: WorkflowEditorNode[]; + edges: WorkflowEditorEdge[]; +} + +export interface WorkflowFormField { + id: string; + label: string; + field: string; + type: 'input' | 'number' | 'textarea'; + required?: boolean; + placeholder?: string; +} + +export interface WorkflowFormConfig { + fields: WorkflowFormField[]; +} + +export interface WorkflowDefinitionEditorValue { + category_id?: null | number; + name: string; + code: string; + description?: null | string; + status: number; + allow_withdraw: boolean; + allow_urge: boolean; + flow_config: WorkflowFlowConfig; + form_config: WorkflowFormConfig; +} + +export const DEFAULT_WORKFLOW_FLOW_CONFIG: WorkflowFlowConfig = { + nodes: [ + { + id: 'start', + type: 'START', + position: { x: 80, y: 180 }, + data: { label: '开始' }, + }, + { + id: 'approve_1', + type: 'APPROVER', + position: { x: 280, y: 180 }, + data: { + label: '审批', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'end', + type: 'END', + position: { x: 500, y: 180 }, + data: { label: '结束' }, + }, + ], + edges: [ + { id: 'edge_start_approve_1', source: 'start', target: 'approve_1' }, + { id: 'edge_approve_1_end', source: 'approve_1', target: 'end' }, + ], +}; + +export const SERIAL_WORKFLOW_FLOW_TEMPLATE: WorkflowFlowConfig = { + nodes: [ + { + id: 'start', + type: 'START', + position: { x: 80, y: 180 }, + data: { label: '开始' }, + }, + { + id: 'approve_1', + type: 'APPROVER', + position: { x: 260, y: 180 }, + data: { + label: '一级审批', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'approve_2', + type: 'APPROVER', + position: { x: 460, y: 180 }, + data: { + label: '二级审批', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'end', + type: 'END', + position: { x: 660, y: 180 }, + data: { label: '结束' }, + }, + ], + edges: [ + { id: 'edge_start_approve_1', source: 'start', target: 'approve_1' }, + { + id: 'edge_approve_1_approve_2', + source: 'approve_1', + target: 'approve_2', + }, + { id: 'edge_approve_2_end', source: 'approve_2', target: 'end' }, + ], +}; + +export const PARALLEL_WORKFLOW_FLOW_TEMPLATE: WorkflowFlowConfig = { + nodes: [ + { + id: 'start', + type: 'START', + position: { x: 80, y: 220 }, + data: { label: '开始' }, + }, + { + id: 'parallel_1', + type: 'PARALLEL', + position: { x: 240, y: 220 }, + data: { label: '并行分支' }, + }, + { + id: 'approve_1', + type: 'APPROVER', + position: { x: 460, y: 120 }, + data: { + label: '并行审批A', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'approve_2', + type: 'APPROVER', + position: { x: 460, y: 320 }, + data: { + label: '并行审批B', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'end', + type: 'END', + position: { x: 700, y: 220 }, + data: { label: '结束' }, + }, + ], + edges: [ + { id: 'edge_start_parallel_1', source: 'start', target: 'parallel_1' }, + { + id: 'edge_parallel_1_approve_1', + source: 'parallel_1', + target: 'approve_1', + }, + { + id: 'edge_parallel_1_approve_2', + source: 'parallel_1', + target: 'approve_2', + }, + { id: 'edge_approve_1_end', source: 'approve_1', target: 'end' }, + { id: 'edge_approve_2_end', source: 'approve_2', target: 'end' }, + ], +}; + +export const CONDITION_WORKFLOW_FLOW_TEMPLATE: WorkflowFlowConfig = { + nodes: [ + { + id: 'start', + type: 'START', + position: { x: 80, y: 220 }, + data: { label: '开始' }, + }, + { + id: 'approve_1', + type: 'APPROVER', + position: { x: 250, y: 220 }, + data: { + label: '发起审批', + approverType: 'INITIATOR_LEADER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'condition_1', + type: 'CONDITION', + position: { x: 450, y: 220 }, + data: { + label: '金额判断', + conditionText: '金额 >= 1000 走经理审批,否则抄送财务', + conditionField: 'amount', + conditionOperator: 'GTE', + conditionValue: '1000', + conditionGroup: { + operator: 'AND', + conditions: [ + { + field: 'amount', + operator: 'GTE', + value: '1000', + }, + ], + }, + }, + }, + { + id: 'approve_2', + type: 'APPROVER', + position: { x: 680, y: 120 }, + data: { + label: '经理审批', + approverType: 'DEPT_LEADER_UP', + deptLevel: 1, + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'cc_1', + type: 'CC', + position: { x: 680, y: 320 }, + data: { + label: '抄送财务', + ccUserIds: [1], + }, + }, + { + id: 'end', + type: 'END', + position: { x: 920, y: 220 }, + data: { label: '结束' }, + }, + ], + edges: [ + { id: 'edge_start_approve_1', source: 'start', target: 'approve_1' }, + { + id: 'edge_approve_1_condition_1', + source: 'approve_1', + target: 'condition_1', + }, + { + id: 'edge_condition_1_approve_2', + source: 'condition_1', + target: 'approve_2', + }, + { id: 'edge_condition_1_cc_1', source: 'condition_1', target: 'cc_1' }, + { id: 'edge_approve_2_end', source: 'approve_2', target: 'end' }, + { id: 'edge_cc_1_end', source: 'cc_1', target: 'end' }, + ], +}; + +export const TRIGGER_WORKFLOW_FLOW_TEMPLATE: WorkflowFlowConfig = { + nodes: [ + { + id: 'start', + type: 'START', + position: { x: 80, y: 180 }, + data: { label: '开始' }, + }, + { + id: 'approve_1', + type: 'APPROVER', + position: { x: 270, y: 180 }, + data: { + label: '业务审批', + approverType: 'DESIGNATED_USER', + approverIds: [], + approverId: null, + approverName: '', + roleIds: [], + formFieldKey: '', + }, + }, + { + id: 'trigger_1', + type: 'TRIGGER', + position: { x: 500, y: 180 }, + data: { + label: '自动触发后续动作', + }, + }, + { + id: 'end', + type: 'END', + position: { x: 730, y: 180 }, + data: { label: '结束' }, + }, + ], + edges: [ + { id: 'edge_start_approve_1', source: 'start', target: 'approve_1' }, + { + id: 'edge_approve_1_trigger_1', + source: 'approve_1', + target: 'trigger_1', + }, + { id: 'edge_trigger_1_end', source: 'trigger_1', target: 'end' }, + ], +}; + +export const DEFAULT_WORKFLOW_FORM_CONFIG: WorkflowFormConfig = { + fields: [], +}; diff --git a/apps/web-antdv-next/src/plugins/workflow/views/category.vue b/apps/web-antdv-next/src/plugins/workflow/views/category.vue new file mode 100644 index 000000000..eb40da674 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/category.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/data.ts b/apps/web-antdv-next/src/plugins/workflow/views/data.ts new file mode 100644 index 000000000..85c5053cd --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/data.ts @@ -0,0 +1,416 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeGridProps } from '#/adapter/vxe-table'; +import type { + WorkflowCategoryResult, + WorkflowDefinitionResult, + WorkflowInstanceResult, + WorkflowMessageResult, +} from '#/plugins/workflow/api'; +import type { WorkflowNodeType } from '#/plugins/workflow/types'; + +import { $t } from '@vben/locales'; + +export const workflowDefinitionQuerySchema: VbenFormSchema[] = [ + { + component: 'Input', + fieldName: 'name', + label: '流程名称', + }, + { + component: 'Input', + fieldName: 'code', + label: '流程编码', + }, +]; + +export const workflowCategoryQuerySchema: VbenFormSchema[] = [ + { + component: 'Input', + fieldName: 'name', + label: '分类名称', + }, + { + component: 'Input', + fieldName: 'code', + label: '分类编码', + }, +]; + +export function useWorkflowCategoryColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'name', title: '分类名称' }, + { field: 'code', title: '分类编码', width: 160 }, + { field: 'sort', title: '排序', width: 100 }, + { + field: 'status', + title: '状态', + width: 120, + cellRender: { + name: 'CellTag', + options: [ + { color: 'default', label: '停用', value: 0 }, + { color: 'success', label: '启用', value: 1 }, + ], + }, + }, + { field: 'remark', title: '备注' }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 100, + cellRender: { + attrs: { onClick: onActionClick }, + name: 'CellOperation', + options: ['edit'], + }, + }, + ]; +} + +export const workflowCategorySchema: VbenFormSchema[] = [ + { + component: 'Input', + fieldName: 'name', + label: '分类名称', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'code', + label: '分类编码', + rules: 'required', + }, + { + component: 'InputNumber', + fieldName: 'sort', + label: '排序', + defaultValue: 0, + }, + { + component: 'RadioGroup', + fieldName: 'status', + label: '状态', + defaultValue: 1, + componentProps: { + buttonStyle: 'solid', + optionType: 'button', + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + }, + rules: 'required', + }, + { + component: 'Textarea', + fieldName: 'remark', + label: '备注', + }, +]; + +export const workflowDefinitionBaseSchema: VbenFormSchema[] = [ + { + component: 'Input', + fieldName: 'name', + label: '流程名称', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'code', + label: '流程编码', + rules: 'required', + }, + { + component: 'Textarea', + fieldName: 'description', + label: '描述', + }, + { + component: 'RadioGroup', + fieldName: 'status', + label: '状态', + defaultValue: 0, + componentProps: { + buttonStyle: 'solid', + optionType: 'button', + options: [ + { label: '草稿', value: 0 }, + { label: '已发布', value: 1 }, + { label: '已停用', value: 2 }, + ], + }, + rules: 'required', + }, + { + component: 'Switch', + fieldName: 'allow_withdraw', + label: '允许撤回', + defaultValue: true, + }, + { + component: 'Switch', + fieldName: 'allow_urge', + label: '允许催办', + defaultValue: true, + }, +]; + +export function createWorkflowDefinitionBaseSchema( + categoryOptions: Array<{ label: string; value: number }>, +): VbenFormSchema[] { + return [ + { + component: 'Select', + fieldName: 'category_id', + label: '流程分类', + componentProps: { + allowClear: true, + options: categoryOptions, + }, + }, + ...workflowDefinitionBaseSchema, + ]; +} + +export function useWorkflowDefinitionColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'name', title: '流程名称' }, + { field: 'code', title: '流程编码', width: 180 }, + { field: 'description', title: '描述' }, + { + field: 'status', + title: '状态', + cellRender: { + name: 'CellTag', + options: [ + { color: 'default', label: '草稿', value: 0 }, + { color: 'success', label: '已发布', value: 1 }, + { color: 'warning', label: '已停用', value: 2 }, + ], + }, + }, + { + field: 'created_time', + title: $t('common.table.created_time'), + width: 180, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 240, + cellRender: { + attrs: { onClick: onActionClick }, + name: 'CellOperation', + options: [ + { code: 'edit', text: '流程设计' }, + { code: 'form', text: '表单设计' }, + { code: 'start', text: '发起申请' }, + ], + }, + }, + ]; +} + +export function workflowNodeTypeLabel(type: WorkflowNodeType) { + return { + APPROVER: '审批', + CC: '抄送', + CONDITION: '条件', + END: '结束', + PARALLEL: '并行', + START: '开始', + TRIGGER: '触发器', + }[type]; +} + +export function workflowStatusOptions() { + return [ + { color: 'processing', label: '审批中', value: 'RUNNING' }, + { color: 'success', label: '已通过', value: 'APPROVED' }, + { color: 'error', label: '已拒绝', value: 'REJECTED' }, + { color: 'warning', label: '已撤回', value: 'WITHDRAWN' }, + ]; +} + +export function workflowFormDataText( + formData?: null | Record | string, +) { + if (!formData) { + return '-'; + } + if (typeof formData === 'string') { + return formData; + } + return JSON.stringify(formData, null, 2); +} + +export function workflowStatusLabel(status: string) { + return ( + workflowStatusOptions().find((item) => item.value === status)?.label || + status + ); +} + +export function workflowMessageTypeLabel(messageType: string) { + return ( + { + APPROVED: '审批通过', + CC_NOTIFY: '抄送通知', + PENDING_APPROVAL: '待审批', + REJECTED: '审批拒绝', + URGE_NOTIFY: '催办提醒', + WITHDRAWN: '流程撤回', + }[messageType] || messageType + ); +} + +export function useWorkflowDetailDescriptions( + instance: WorkflowInstanceResult, +) { + return [ + { label: '实例编号', value: instance.instance_no }, + { label: '标题', value: instance.title }, + { label: '流程定义ID', value: String(instance.definition_id) }, + { label: '发起人ID', value: String(instance.initiator_id) }, + { label: '状态', value: workflowStatusLabel(instance.status) }, + { + label: '当前任务ID', + value: instance.current_task_id ? String(instance.current_task_id) : '-', + }, + { + label: '待办数', + value: + instance.todo_count !== null && instance.todo_count !== undefined + ? String(instance.todo_count) + : '-', + }, + { label: '备注', value: instance.remark || '-' }, + { label: '创建时间', value: instance.created_time }, + { label: '更新时间', value: instance.updated_time || '-' }, + ]; +} + +export function buildWorkflowInstanceColumns( + onActionClick?: OnActionClickFn, + actions: Array<'approve' | 'detail' | 'reject'> = [ + 'detail', + 'approve', + 'reject', + ], +): VxeGridProps['columns'] { + const operationOptions = actions.map((action) => { + if (action === 'detail') { + return { code: 'detail', text: '详情' }; + } + if (action === 'approve') { + return { code: 'approve', text: '通过' }; + } + return { code: 'reject', text: '拒绝' }; + }); + + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'instance_no', title: '实例编号', width: 180 }, + { field: 'title', title: '标题' }, + { + field: 'status', + title: '状态', + cellRender: { + name: 'CellTag', + options: workflowStatusOptions(), + }, + }, + { + field: 'created_time', + title: $t('common.table.created_time'), + width: 180, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: actions.length > 1 ? 220 : 120, + cellRender: { + attrs: { onClick: onActionClick }, + name: 'CellOperation', + options: operationOptions, + }, + }, + ]; +} + +export function useWorkflowInstanceColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return buildWorkflowInstanceColumns(onActionClick, [ + 'detail', + 'approve', + 'reject', + ]); +} + +export function useWorkflowApplyColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return buildWorkflowInstanceColumns(onActionClick, ['detail']); +} + +export function useWorkflowMessageColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'title', title: '标题', width: 180 }, + { + field: 'message_type', + title: '消息类型', + width: 120, + formatter: ({ cellValue }) => + workflowMessageTypeLabel(String(cellValue || '')), + }, + { field: 'content', title: '内容' }, + { + field: 'is_read', + title: '已读', + cellRender: { + name: 'CellTag', + options: [ + { color: 'warning', label: '未读', value: false }, + { color: 'success', label: '已读', value: true }, + ], + }, + }, + { + field: 'created_time', + title: $t('common.table.created_time'), + width: 180, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 180, + cellRender: { + attrs: { onClick: onActionClick }, + name: 'CellOperation', + options: [ + { code: 'detail', text: '查看详情' }, + { code: 'read', text: '标记已读' }, + ], + }, + }, + ]; +} diff --git a/apps/web-antdv-next/src/plugins/workflow/views/definition.vue b/apps/web-antdv-next/src/plugins/workflow/views/definition.vue new file mode 100644 index 000000000..83cb8fc37 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/definition.vue @@ -0,0 +1,288 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/detail.vue b/apps/web-antdv-next/src/plugins/workflow/views/detail.vue new file mode 100644 index 000000000..0d610083c --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/detail.vue @@ -0,0 +1,435 @@ + + + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/editor.vue b/apps/web-antdv-next/src/plugins/workflow/views/editor.vue new file mode 100644 index 000000000..c0f33180e --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/editor.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/form-editor.vue b/apps/web-antdv-next/src/plugins/workflow/views/form-editor.vue new file mode 100644 index 000000000..79725d6f7 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/form-editor.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/message.vue b/apps/web-antdv-next/src/plugins/workflow/views/message.vue new file mode 100644 index 000000000..10e6927e6 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/message.vue @@ -0,0 +1,109 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/my-apply.vue b/apps/web-antdv-next/src/plugins/workflow/views/my-apply.vue new file mode 100644 index 000000000..a09161051 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/my-apply.vue @@ -0,0 +1,92 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/my-todo.vue b/apps/web-antdv-next/src/plugins/workflow/views/my-todo.vue new file mode 100644 index 000000000..d26edc2a0 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/my-todo.vue @@ -0,0 +1,141 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/start-apply.vue b/apps/web-antdv-next/src/plugins/workflow/views/start-apply.vue new file mode 100644 index 000000000..fbe75a743 --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/start-apply.vue @@ -0,0 +1,89 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/start.vue b/apps/web-antdv-next/src/plugins/workflow/views/start.vue new file mode 100644 index 000000000..fa02fdf7c --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/start.vue @@ -0,0 +1,274 @@ + + + diff --git a/apps/web-antdv-next/src/plugins/workflow/views/statistics.vue b/apps/web-antdv-next/src/plugins/workflow/views/statistics.vue new file mode 100644 index 000000000..a5097c9bd --- /dev/null +++ b/apps/web-antdv-next/src/plugins/workflow/views/statistics.vue @@ -0,0 +1,140 @@ + + + diff --git a/apps/web-antdv-next/src/router/guard.ts b/apps/web-antdv-next/src/router/guard.ts index cb3ec0a22..ac0141b1b 100644 --- a/apps/web-antdv-next/src/router/guard.ts +++ b/apps/web-antdv-next/src/router/guard.ts @@ -118,6 +118,12 @@ function setupAccessGuard(router: Router) { // 保存菜单信息和路由信息 accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); + for (const route of accessibleRoutes) { + const routeName = route.name as string | undefined; + if (routeName && !router.hasRoute(routeName)) { + router.addRoute(route); + } + } accessStore.setIsAccessChecked(true); const redirectPath = (from.query.redirect ?? (to.path === preferences.app.defaultHomePath diff --git a/apps/web-antdv-next/src/router/routes/index.ts b/apps/web-antdv-next/src/router/routes/index.ts index 931ccd872..ec5c2ea09 100644 --- a/apps/web-antdv-next/src/router/routes/index.ts +++ b/apps/web-antdv-next/src/router/routes/index.ts @@ -22,6 +22,13 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); /** 插件路由 */ const pluginRoutes: RouteRecordRaw[] = mergeRouteModules(pluginRouteFiles); +const hiddenPluginRoutes = pluginRoutes.filter( + (route) => route.meta?.hideInMenu, +); +const visiblePluginRoutes = pluginRoutes.filter( + (route) => !route.meta?.hideInMenu, +); + /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles); @@ -33,6 +40,7 @@ const externalRoutes: RouteRecordRaw[] = []; const routes: RouteRecordRaw[] = [ ...coreRoutes, ...externalRoutes, + ...hiddenPluginRoutes, fallbackNotFoundRoute, ]; @@ -40,7 +48,11 @@ const routes: RouteRecordRaw[] = [ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); /** 有权限校验的路由列表,包含动态路由和静态路由 */ -const accessRoutes = [...dynamicRoutes, ...pluginRoutes, ...staticRoutes]; +const accessRoutes = [ + ...dynamicRoutes, + ...visiblePluginRoutes, + ...staticRoutes, +]; const componentKeys: string[] = Object.keys({ ...import.meta.glob('../../views/**/*.vue'), diff --git a/apps/web-antdv-next/src/store/auth.ts b/apps/web-antdv-next/src/store/auth.ts index 1a1826899..41edbaa34 100644 --- a/apps/web-antdv-next/src/store/auth.ts +++ b/apps/web-antdv-next/src/store/auth.ts @@ -20,7 +20,7 @@ import { logoutApi, } from '#/api'; import { $t } from '#/locales'; -import { useDictStore, useWebSocketStore } from '#/store'; +import { useDictStore, useWebSocketStore, useWorkflowStore } from '#/store'; export const useAuthStore = defineStore('auth', () => { const accessStore = useAccessStore(); @@ -85,6 +85,8 @@ export const useAuthStore = defineStore('auth', () => { // 初始化WebSocket连接 const wsStore = useWebSocketStore(); wsStore.connect(); + const workflowStore = useWorkflowStore(); + await workflowStore.init(); if (userInfo?.nickname) { notification.success({ @@ -119,11 +121,13 @@ export const useAuthStore = defineStore('auth', () => { } async function logout(redirect: boolean = true) { + const workflowStore = useWorkflowStore(); try { await logoutApi(); } catch { // 不做任何处理 } + workflowStore.reset(); resetAllStores(); accessStore.setLoginExpired(false); diff --git a/apps/web-antdv-next/src/store/index.ts b/apps/web-antdv-next/src/store/index.ts index 369ff3592..d37dbe0d6 100644 --- a/apps/web-antdv-next/src/store/index.ts +++ b/apps/web-antdv-next/src/store/index.ts @@ -1,3 +1,4 @@ export * from './auth'; export * from './dict'; export * from './websocket'; +export * from './workflow'; diff --git a/apps/web-antdv-next/src/store/workflow.ts b/apps/web-antdv-next/src/store/workflow.ts new file mode 100644 index 000000000..f5f399f00 --- /dev/null +++ b/apps/web-antdv-next/src/store/workflow.ts @@ -0,0 +1,112 @@ +import { ref } from 'vue'; + +import { useAccessStore } from '@vben/stores'; + +import { notification } from 'antdv-next'; +import { defineStore } from 'pinia'; + +import { + getWorkflowTodoCountApi, + getWorkflowUnreadCountApi, +} from '#/plugins/workflow/api'; +import { useWebSocketStore } from '#/store'; + +export const useWorkflowStore = defineStore('workflow', () => { + const unreadCount = ref(0); + const todoCount = ref(0); + const initialized = ref(false); + let cleanup: (() => void) | null = null; + + function syncMenuBadge() { + const accessStore = useAccessStore(); + accessStore.setMenuBadgeByPath( + '/plugins/workflow/message', + unreadCount.value > 0 ? String(unreadCount.value) : undefined, + 'normal', + 'destructive', + ); + accessStore.setMenuBadgeByPath( + '/plugins/workflow/my-todo', + todoCount.value > 0 ? String(todoCount.value) : undefined, + 'normal', + 'primary', + ); + } + + async function refreshUnreadCount() { + unreadCount.value = await getWorkflowUnreadCountApi(); + syncMenuBadge(); + return unreadCount.value; + } + + async function refreshTodoCount() { + todoCount.value = await getWorkflowTodoCountApi(); + syncMenuBadge(); + return todoCount.value; + } + + async function refreshCounts() { + await Promise.all([refreshUnreadCount(), refreshTodoCount()]); + } + + function handleSocketPayload(payload: any) { + if (typeof payload?.unread_count === 'number') { + unreadCount.value = payload.unread_count; + } + if (typeof payload?.todo_count === 'number') { + todoCount.value = payload.todo_count; + } + syncMenuBadge(); + } + + function bindSocket() { + const wsStore = useWebSocketStore(); + if (cleanup) { + cleanup(); + cleanup = null; + } + cleanup = wsStore.on('workflow_message', (payload: any) => { + handleSocketPayload(payload); + if (payload?.type === 'READ') { + return; + } + if (payload?.title) { + notification.info({ + title: payload.title, + description: payload.content || '', + duration: 3, + }); + } + }); + } + + async function init() { + if (!initialized.value) { + bindSocket(); + initialized.value = true; + } + await refreshCounts(); + } + + function reset() { + if (cleanup) { + cleanup(); + cleanup = null; + } + unreadCount.value = 0; + todoCount.value = 0; + syncMenuBadge(); + initialized.value = false; + } + + return { + unreadCount, + todoCount, + initialized, + init, + refreshCounts, + refreshTodoCount, + refreshUnreadCount, + reset, + }; +}); diff --git a/packages/stores/src/modules/access.ts b/packages/stores/src/modules/access.ts index f97a8b29f..ad70aa396 100644 --- a/packages/stores/src/modules/access.ts +++ b/packages/stores/src/modules/access.ts @@ -90,6 +90,34 @@ export const useAccessStore = defineStore('core-access', { setAccessMenus(menus: MenuRecordRaw[]) { this.accessMenus = menus; }, + setMenuBadgeByPath( + path: string, + badge?: string, + badgeType: 'dot' | 'normal' = 'normal', + badgeVariants = 'destructive', + ) { + function updateMenus(menus: MenuRecordRaw[]): MenuRecordRaw[] { + return menus.map((menu) => { + const nextChildren = menu.children + ? updateMenus(menu.children) + : undefined; + if (menu.path === path) { + return { + ...menu, + badge, + badgeType: badge ? badgeType : undefined, + badgeVariants: badge ? badgeVariants : undefined, + children: nextChildren, + }; + } + return { + ...menu, + children: nextChildren, + }; + }); + } + this.accessMenus = updateMenus(this.accessMenus); + }, setAccessRoutes(routes: RouteRecordRaw[]) { this.accessRoutes = routes; },