@robot-admin/naive-ui-components 0.3.1 โ 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/C_ActionBar-nnfbZCea.css.map +1 -0
- package/dist/C_ActionBar2.js +2 -2
- package/dist/C_ActionBar2.js.map +1 -1
- package/dist/C_AntV-DGjscTWa.css.map +1 -0
- package/dist/C_AntV2.js +6 -6
- package/dist/C_AntV2.js.map +1 -1
- package/dist/C_Barcode-DjTmDkbQ.css.map +1 -0
- package/dist/C_Barcode2.js +1 -1
- package/dist/C_Barcode2.js.map +1 -1
- package/dist/C_Captcha-Ccq3DMrR.css.map +1 -0
- package/dist/C_Captcha2.js +1 -1
- package/dist/C_Captcha2.js.map +1 -1
- package/dist/C_Cascade-IUUHIh7r.css.map +1 -0
- package/dist/C_Cascade2.js +2 -2
- package/dist/C_Cascade2.js.map +1 -1
- package/dist/C_City-Cv5BESaN.css.map +1 -0
- package/dist/C_City2.js +2 -2
- package/dist/C_City2.js.map +1 -1
- package/dist/C_Code-DPZlNSxL.css.map +1 -0
- package/dist/C_Code2.js +8 -7
- package/dist/C_Code2.js.map +1 -1
- package/dist/C_CollapsePanel-Fap-lv_5.css.map +1 -0
- package/dist/C_CollapsePanel2.js +1 -1
- package/dist/C_CollapsePanel2.js.map +1 -1
- package/dist/C_Cron-C0-8b5af.css.map +1 -0
- package/dist/C_Cron2.js +15 -14
- package/dist/C_Cron2.js.map +1 -1
- package/dist/C_Date2.js +1 -1
- package/dist/C_Date2.js.map +1 -1
- package/dist/C_Draggable-Bq6o0qXn.css.map +1 -0
- package/dist/C_Draggable2.js +1 -1
- package/dist/C_Draggable2.js.map +1 -1
- package/dist/C_Editor-OlxIF9-5.css.map +1 -0
- package/dist/C_Editor2.js +1 -1
- package/dist/C_Editor2.js.map +1 -1
- package/dist/C_FilePreview-B4XgTv-h.css.map +1 -0
- package/dist/C_FilePreview.cjs +1 -0
- package/dist/C_FilePreview.js +1 -0
- package/dist/C_FilePreview2.js +10 -9
- package/dist/C_FilePreview2.js.map +1 -1
- package/dist/C_Form-Cr9oX037.css.map +1 -0
- package/dist/C_Form.cjs +1 -0
- package/dist/C_Form.js +1 -0
- package/dist/C_Form2.js +69 -72
- package/dist/C_Form2.js.map +1 -1
- package/dist/C_FormSearch-DlIEoh7X.css.map +1 -0
- package/dist/C_FormSearch2.js +2 -2
- package/dist/C_FormSearch2.js.map +1 -1
- package/dist/C_FormulaEditor-Cm0CokN5.css.map +1 -0
- package/dist/C_FormulaEditor2.js +6 -6
- package/dist/C_FormulaEditor2.js.map +1 -1
- package/dist/C_FullCalendar-BULCIlVK.css.map +1 -0
- package/dist/C_FullCalendar2.js +2 -2
- package/dist/C_FullCalendar2.js.map +1 -1
- package/dist/C_Guide2.js +1 -1
- package/dist/C_Guide2.js.map +1 -1
- package/dist/C_Icon2.js.map +1 -1
- package/dist/C_ImageCropper-DrmUlaLi.css.map +1 -0
- package/dist/C_ImageCropper2.js +4 -4
- package/dist/C_ImageCropper2.js.map +1 -1
- package/dist/C_Language2.js +1 -1
- package/dist/C_Language2.js.map +1 -1
- package/dist/C_Map-WUMXSAfy.css.map +1 -0
- package/dist/C_Map2.js +2 -2
- package/dist/C_Map2.js.map +1 -1
- package/dist/C_Markdown-Dmv8yaM4.css.map +1 -0
- package/dist/C_Markdown2.js +4 -4
- package/dist/C_Markdown2.js.map +1 -1
- package/dist/C_NotificationCenter-DbgBiyqB.css.map +1 -0
- package/dist/C_NotificationCenter2.js +21 -20
- package/dist/C_NotificationCenter2.js.map +1 -1
- package/dist/C_Progress2.js +1 -1
- package/dist/C_Progress2.js.map +1 -1
- package/dist/C_QRCode-G7fiAkm4.css.map +1 -0
- package/dist/C_QRCode2.js +1 -1
- package/dist/C_QRCode2.js.map +1 -1
- package/dist/C_Signature-es-ZNPzr.css.map +1 -0
- package/dist/C_Signature2.js +2 -2
- package/dist/C_Signature2.js.map +1 -1
- package/dist/C_SplitPane-Br2eK8IG.css.map +1 -0
- package/dist/C_SplitPane2.js +1 -1
- package/dist/C_SplitPane2.js.map +1 -1
- package/dist/C_Steps-P9Qj9iDd.css.map +1 -0
- package/dist/C_Steps2.js +1 -1
- package/dist/C_Steps2.js.map +1 -1
- package/dist/C_Table-DAwAxr72.css.map +1 -0
- package/dist/C_Table.cjs +1 -0
- package/dist/C_Table.js +1 -0
- package/dist/C_Table2.js +3 -3
- package/dist/C_Table2.js.map +1 -1
- package/dist/C_Theme2.js +1 -1
- package/dist/C_Theme2.js.map +1 -1
- package/dist/C_Time-Bd_e1YDN.css.map +1 -0
- package/dist/C_Time2.js +2 -2
- package/dist/C_Time2.js.map +1 -1
- package/dist/C_Tree-DnGc_MPb.css.map +1 -0
- package/dist/C_Tree2.js +2 -2
- package/dist/C_Tree2.js.map +1 -1
- package/dist/C_Upload-i8LB_29U.css.map +1 -0
- package/dist/C_Upload2.js +5 -5
- package/dist/C_Upload2.js.map +1 -1
- package/dist/C_VideoPlayer-rm0MODUv.css.map +1 -0
- package/dist/C_VideoPlayer2.js +21 -20
- package/dist/C_VideoPlayer2.js.map +1 -1
- package/dist/C_VtableGantt-BpY-Rng3.css.map +1 -0
- package/dist/C_VtableGantt2.js +2 -2
- package/dist/C_VtableGantt2.js.map +1 -1
- package/dist/C_WaterFall-HWB-gPON.css.map +1 -0
- package/dist/C_WaterFall2.js +2 -2
- package/dist/C_WaterFall2.js.map +1 -1
- package/dist/C_WorkFlow-CSO86Cuc.css.map +1 -0
- package/dist/C_WorkFlow2.js +6 -6
- package/dist/C_WorkFlow2.js.map +1 -1
- package/dist/constants.d.ts +4 -4
- package/dist/constants2.d.ts +4 -4
- package/dist/constants3.d.ts +4 -4
- package/dist/constants4.d.ts +5 -5
- package/dist/constants5.d.ts +2 -2
- package/dist/data.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.vue.d.ts +1 -1
- package/dist/index10.vue.d.ts +7 -7
- package/dist/index11.vue.d.ts +2 -2
- package/dist/index12.vue.d.ts +5 -5
- package/dist/index12.vue.d.ts.map +1 -1
- package/dist/index13.vue.d.ts +1 -1
- package/dist/index14.vue.d.ts +1 -1
- package/dist/index16.vue.d.ts +1 -1
- package/dist/index2.vue.d.ts +6 -6
- package/dist/index3.vue.d.ts +2 -2
- package/dist/index4.vue.d.ts +2 -2
- package/dist/index5.vue.d.ts +2 -2
- package/dist/index6.vue.d.ts +1 -1
- package/dist/index8.vue.d.ts +3 -3
- package/dist/resolver.js.map +1 -1
- package/dist/style.css +1555 -1555
- package/dist/useCalendarEvents.d.ts +2 -2
- package/dist/useCollapsePanel.d.ts +2 -2
- package/dist/useCropperCore.d.ts +3 -3
- package/dist/useDraggableLayout.d.ts +8 -8
- package/dist/useDynamicFormState.d.ts +111 -111
- package/dist/useDynamicFormState.d.ts.map +1 -1
- package/dist/useEdgeInteraction.d.ts +2 -2
- package/dist/useInfiniteScroll.d.ts +1 -1
- package/dist/useModalEdit.d.ts +4 -4
- package/dist/useModalEdit.d.ts.map +1 -1
- package/dist/useQRCode.d.ts +4 -4
- package/dist/useQRCode.d.ts.map +1 -1
- package/dist/useSearchState.d.ts +2 -2
- package/dist/useSignatureHistory.d.ts +4 -4
- package/dist/useSplitResize.d.ts +6 -6
- package/dist/useSplitResize.d.ts.map +1 -1
- package/dist/useTimeSelection.d.ts +2 -2
- package/dist/useTreeOperations.d.ts +6 -6
- package/dist/useWorkflowValidation.d.ts +5 -5
- package/package.json +2 -1
- package/dist/C_ActionBar-DWN-woTc.css.map +0 -1
- package/dist/C_AntV-AFKyK6hH.css.map +0 -1
- package/dist/C_Barcode-P_EFj8dC.css.map +0 -1
- package/dist/C_Captcha-C-ef41xw.css.map +0 -1
- package/dist/C_Cascade-D9kNsjsV.css.map +0 -1
- package/dist/C_City-BCQ4ipiK.css.map +0 -1
- package/dist/C_Code-C9kvvEmO.css.map +0 -1
- package/dist/C_CollapsePanel-BUJHuYcU.css.map +0 -1
- package/dist/C_Cron-yx2Ob4Jl.css.map +0 -1
- package/dist/C_Draggable-C483syRC.css.map +0 -1
- package/dist/C_Editor-Bp0SyIEw.css.map +0 -1
- package/dist/C_FilePreview-CPqvhoCy.css.map +0 -1
- package/dist/C_Form-Jx7PY3sT.css.map +0 -1
- package/dist/C_FormSearch-DvRgxlRn.css.map +0 -1
- package/dist/C_FormulaEditor-DtGkt4T_.css.map +0 -1
- package/dist/C_FullCalendar-BF7H0YIx.css.map +0 -1
- package/dist/C_ImageCropper-BVJfUufl.css.map +0 -1
- package/dist/C_Map-DpzeuWdX.css.map +0 -1
- package/dist/C_Markdown-BEjxknqd.css.map +0 -1
- package/dist/C_NotificationCenter-0l3TY2Gn.css.map +0 -1
- package/dist/C_QRCode-DbdiAIPg.css.map +0 -1
- package/dist/C_Signature-zhHCbra9.css.map +0 -1
- package/dist/C_SplitPane-C6sBsfKY.css.map +0 -1
- package/dist/C_Steps-CODHN5Hs.css.map +0 -1
- package/dist/C_Table-DSNsntmT.css.map +0 -1
- package/dist/C_Time-BvZLYraL.css.map +0 -1
- package/dist/C_Tree-0GDv--jX.css.map +0 -1
- package/dist/C_Upload-BXd3YYLx.css.map +0 -1
- package/dist/C_VideoPlayer-DYG3RL0Q.css.map +0 -1
- package/dist/C_VtableGantt-fhItIiHE.css.map +0 -1
- package/dist/C_WaterFall-8sQDFXKg.css.map +0 -1
- package/dist/C_WorkFlow-J-dyIuh9.css.map +0 -1
package/dist/C_WorkFlow2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_WorkFlow2.js","names":["data","data","data","data","StartNode","ApprovalNode","CopyNode","ConditionNode","users","departments"],"sources":["../src/components/C_WorkFlow/data.ts","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/composables/useWorkflowNodes.ts","../src/components/C_WorkFlow/composables/useWorkflowValidation.ts","../src/components/C_WorkFlow/composables/useWorkflowPreview.ts","../src/components/C_WorkFlow/index.vue","../src/components/C_WorkFlow/index.vue","../src/components/C_WorkFlow/index.vue"],"sourcesContent":["/*\r\n * @Description: ๅทฅไฝ๏ผๅฎกๆนๆต๏ผๆต็ปไปถ - ๆฐๆฎๅฑ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n */\r\nimport type { NodeType, WorkflowNode } from \"./types\";\r\n\r\n/* ่็น็ฑปๅ้้กน */\r\nexport const NODE_TYPE_OPTIONS = [\r\n {\r\n type: \"approval\" as NodeType,\r\n label: \"ๅฎกๆนไบบ\",\r\n icon: \"mdi:account-check\",\r\n iconClass: \"approval-icon\",\r\n },\r\n {\r\n type: \"copy\" as NodeType,\r\n label: \"ๆ้ไบบ\",\r\n icon: \"mdi:email-outline\",\r\n iconClass: \"copy-icon\",\r\n },\r\n {\r\n type: \"condition\" as NodeType,\r\n label: \"ๆกไปถๅๆฏ\",\r\n icon: \"mdi:source-branch\",\r\n iconClass: \"condition-icon\",\r\n },\r\n];\r\n\r\n/* ๅฎกๆนๆจกๅผ้้กน */\r\nexport const APPROVAL_MODES = [\r\n { value: \"any\", label: \"ๆ็ญพ\", desc: \"ไปปๆไธไบบๅๆๅณๅฏ้่ฟ\" },\r\n { value: \"all\", label: \"ไผ็ญพ\", desc: \"ๆๆไบบ้ฝๅๆๆ่ฝ้่ฟ\" },\r\n { value: \"sequence\", label: \"้กบๅบๅฎกๆน\", desc: \"ๆ้ๆฉ้กบๅบไพๆฌกๅฎกๆน\" },\r\n];\r\n\r\n/* ๅญๆฎต้้กน */\r\nexport const FIELD_OPTIONS = [\r\n { label: \"็ณ่ฏท้้ข\", value: \"amount\" },\r\n { label: \"็ณ่ฏทไบบ้จ้จ\", value: \"department\" },\r\n { label: \"็ณ่ฏท็ฑปๅ\", value: \"type\" },\r\n { label: \"็ดงๆฅ็จๅบฆ\", value: \"priority\" },\r\n];\r\n\r\n/* ๆไฝ็ฌฆ้้กน */\r\nexport const OPERATOR_OPTIONS = [\r\n { label: \"็ญไบ\", value: \"equals\" },\r\n { label: \"ๅคงไบ\", value: \"greater_than\" },\r\n { label: \"ๅฐไบ\", value: \"less_than\" },\r\n { label: \"ๅ
ๅซ\", value: \"contains\" },\r\n { label: \"ไธ็ญไบ\", value: \"not_equals\" },\r\n];\r\n\r\n/* ่็นๆ ้ขๆ ๅฐ */\r\nexport const NODE_TITLES: Record<NodeType, string> = {\r\n start: \"ๅ่ตทไบบ\",\r\n approval: \"ๅฎกๆนไบบ\",\r\n copy: \"ๆ้ไบบ\",\r\n condition: \"ๆกไปถๅๆฏ\",\r\n};\r\n\r\n/* ้
็ฝฎๆ ้ขๆ ๅฐ */\r\nexport const CONFIG_TITLES = {\r\n approval: \"ๅฎกๆนไบบ่ฎพ็ฝฎ\",\r\n copy: \"ๆ้ไบบ่ฎพ็ฝฎ\",\r\n condition: \"ๆกไปถๅๆฏ่ฎพ็ฝฎ\",\r\n};\r\n\r\n/* ๅญๆฎตๆพ็คบๅ็งฐๆ ๅฐ */\r\nexport const FIELD_DISPLAY_NAMES: Record<string, string> = {\r\n approvers: \"ๅฎกๆนไบบ\",\r\n conditions: \"ๆกไปถ้
็ฝฎ\",\r\n connection: \"่็น่ฟๆฅ\",\r\n copyUsers: \"ๆ้ไบบ\",\r\n};\r\n\r\n/* ้่ฏฏ็ฑปๅๆๆฌๆ ๅฐ */\r\nexport const ERROR_TYPE_TEXTS: Record<string, string> = {\r\n required: \"ๅฟ
ๅกซ\",\r\n incomplete: \"ไธๅฎๆด\",\r\n warning: \"่ญฆๅ\",\r\n error: \"้่ฏฏ\",\r\n};\r\n\r\n/** ่็นไน้ด็ Y ่ฝด้ด่ท๏ผpx๏ผ */\r\nexport const NODE_Y_GAP = 180;\r\n\r\n/* ๅๅง่็นๆฐๆฎ */\r\nexport const INITIAL_NODE: WorkflowNode = {\r\n id: \"start-1\",\r\n type: \"start\",\r\n position: { x: 150, y: 100 },\r\n data: { title: \"ๅ่ตทไบบ\", status: \"active\", initiators: [] },\r\n};\r\n\r\n/* ้ป่ฎคๅคดๅ็ๆๅฝๆฐ */\r\nexport const getDefaultAvatar = (name: string): string =>\r\n `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n\r\n/* ็ๆๆกไปถID็ๅฝๆฐ */\r\nexport const generateConditionId = (): string => `condition-${Date.now()}`;\r\n\r\n/* ็ๆ่พนID็ๅฝๆฐ */\r\nexport const generateEdgeId = (sourceId: string, targetId: string): string =>\r\n `edge-${sourceId}-${targetId}`;\r\n\r\n/* ้ป่ฎคๆกไปถๅฏน่ฑกๅทฅๅๅฝๆฐ */\r\nexport const createDefaultCondition = () => ({\r\n id: generateConditionId(),\r\n name: \"\",\r\n field: \"\",\r\n operator: \"equals\" as const,\r\n value: \"\",\r\n});\r\n","<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","/**\r\n * ๅทฅไฝๆต่็น็ฎก็ Composable\r\n * ๅฐ่ฃ
่็น CRUDใprovide/injectใ่พน้่ฟใ็ปๅธๆไฝ็ญ้ป่พ\r\n */\r\n\r\nimport {\r\n ref,\r\n computed,\r\n provide,\r\n watch,\r\n onMounted,\r\n nextTick,\r\n markRaw,\r\n} from \"vue\";\r\nimport type { Component, Ref } from \"vue\";\r\nimport { useMessage } from \"naive-ui\";\r\nimport type {\r\n WorkflowNode,\r\n WorkflowEdge,\r\n WorkflowData,\r\n WorkflowProps,\r\n NodeType,\r\n MenuPosition,\r\n} from \"../types\";\r\nimport { NODE_TITLES, INITIAL_NODE, NODE_Y_GAP, generateEdgeId } from \"../data\";\r\n\r\nimport StartNode from \"../nodes/StartNode.vue\";\r\nimport ApprovalNode from \"../nodes/ApprovalNode.vue\";\r\nimport CopyNode from \"../nodes/CopyNode.vue\";\r\nimport ConditionNode from \"../nodes/ConditionNode.vue\";\r\n\r\n/** ่็น็ปไปถๆ ๅฐ๏ผๆจกๅ็บงๅธธ้๏ผไป
ๅๅงๅไธๆฌก๏ผ */\r\nconst NODE_COMPONENT_MAP: Record<string, Component> = {\r\n start: markRaw(StartNode),\r\n approval: markRaw(ApprovalNode),\r\n copy: markRaw(CopyNode),\r\n condition: markRaw(ConditionNode),\r\n};\r\n\r\ntype EmitFn = (event: string, ...args: any[]) => void;\r\n\r\n/** ๅทฅไฝๆต่็น็ฎก็ โโ ๅฐ่ฃ
่็น CRUDใprovide/injectใ่พน้่ฟใ็ปๅธๆไฝ็ญ้ป่พ */\r\nexport function useWorkflowNodes(\r\n props: WorkflowProps,\r\n emit: EmitFn,\r\n vueFlowRef: Ref,\r\n) {\r\n const message = useMessage();\r\n\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const nodes = ref<WorkflowNode[]>([{ ...INITIAL_NODE }]);\r\n const edges = ref<WorkflowEdge[]>([]);\r\n const showAddMenu = ref(false);\r\n const menuPosition = ref<MenuPosition>({ x: 0, y: 0 });\r\n const showNodeConfig = ref(false);\r\n const currentNode = ref<WorkflowNode | null>(null);\r\n const currentAddNodeId = ref<string | null>(null);\r\n\r\n /* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const nodeTypes = computed(() => NODE_COMPONENT_MAP);\r\n\r\n const workflowStats = computed(() => {\r\n const stats = {\r\n totalNodes: nodes.value.length,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n };\r\n nodes.value.forEach((n) => {\r\n if (n.type === \"approval\") stats.approvalNodes++;\r\n else if (n.type === \"copy\") stats.copyNodes++;\r\n else if (n.type === \"condition\") stats.conditionNodes++;\r\n });\r\n return stats;\r\n });\r\n\r\n /* โโโ ๆฐๆฎๆไฝ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ่ทๅๅฝๅๅทฅไฝๆตๅฎๆดๆฐๆฎ */\r\n const getCurrentWorkflowData = (): WorkflowData => ({\r\n nodes: nodes.value,\r\n edges: edges.value,\r\n config: { version: \"1.0\", createdAt: new Date().toISOString() },\r\n });\r\n\r\n /** ่งฆๅๆฐๆฎๅๆดไบไปถ */\r\n const emitChange = (): void => {\r\n const data = getCurrentWorkflowData();\r\n emit(\"update:modelValue\", data);\r\n emit(\"change\", data);\r\n };\r\n\r\n /** ๅปถ่ฟ้ๅบ็ปๅธ่งๅพ */\r\n const deferFitView = (padding = 60, duration = 400): void => {\r\n nextTick(() => {\r\n setTimeout(() => {\r\n vueFlowRef.value?.fitView?.({ padding, duration });\r\n }, 100);\r\n });\r\n };\r\n\r\n /* โโโ ๆทปๅ ่ๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const handleShowAddMenu = (position: MenuPosition, nodeId?: string): void => {\r\n menuPosition.value = {\r\n x: typeof position.x === \"number\" ? position.x : 0,\r\n y: typeof position.y === \"number\" ? position.y : 0,\r\n };\r\n currentAddNodeId.value = nodeId || null;\r\n showAddMenu.value = true;\r\n };\r\n\r\n const closeAddMenu = (): void => {\r\n showAddMenu.value = false;\r\n };\r\n\r\n /* โโโ ่็นๅ ้ค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const deleteNode = (nodeId: string): void => {\r\n if (nodeId === \"start-1\") {\r\n message.warning(\"ไธ่ฝๅ ้คๅผๅง่็น\");\r\n return;\r\n }\r\n\r\n const nodeIndex = nodes.value.findIndex((n) => n.id === nodeId);\r\n if (nodeIndex === -1) return;\r\n\r\n /* ๆถ้ไธไธๆธธ่พน๏ผ็จไบ้่ฟ */\r\n const incomingEdges = edges.value.filter((e) => e.target === nodeId);\r\n const outgoingEdges = edges.value.filter((e) => e.source === nodeId);\r\n\r\n /* ็งป้ค่็นๅๅ
ณ่่พน */\r\n nodes.value.splice(nodeIndex, 1);\r\n edges.value = edges.value.filter(\r\n (e) => e.source !== nodeId && e.target !== nodeId,\r\n );\r\n\r\n /* ้่ฟไธไธๆธธ */\r\n incomingEdges.forEach((inEdge) => {\r\n outgoingEdges.forEach((outEdge) => {\r\n edges.value.push({\r\n id: generateEdgeId(inEdge.source, outEdge.target),\r\n source: inEdge.source,\r\n target: outEdge.target,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n });\r\n });\r\n\r\n /* ไธ็งปๅ็ปญ่็นไฝ็ฝฎ */\r\n nodes.value.forEach((node, i) => {\r\n if (i >= nodeIndex) node.position.y -= NODE_Y_GAP;\r\n });\r\n\r\n emitChange();\r\n message.success(\"่็นๅทฒๅ ้ค\");\r\n deferFitView();\r\n };\r\n\r\n /* โโโ provide ๆณจๅ
ฅ็ปๅญ่็น็ปไปถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n provide(\"showAddMenu\", handleShowAddMenu);\r\n provide(\"deleteNode\", deleteNode);\r\n\r\n /* โโโ ่็นๆทปๅ ๏ผๅ
้จ่พ
ๅฉ๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const getTargetNodeInfo = () => {\r\n let targetNodeIndex = nodes.value.length - 1;\r\n let targetNode = nodes.value[targetNodeIndex];\r\n\r\n if (currentAddNodeId.value) {\r\n const foundIndex = nodes.value.findIndex(\r\n (n) => n.id === currentAddNodeId.value,\r\n );\r\n if (foundIndex !== -1) {\r\n targetNodeIndex = foundIndex;\r\n targetNode = nodes.value[targetNodeIndex];\r\n }\r\n }\r\n return { targetNodeIndex, targetNode };\r\n };\r\n\r\n const createNewNode = (\r\n type: NodeType,\r\n targetNode: WorkflowNode | null,\r\n ): WorkflowNode => ({\r\n id: `${type}-${Date.now()}`,\r\n type,\r\n position: {\r\n x: targetNode?.position.x || 150,\r\n y: (targetNode?.position.y || 130) + NODE_Y_GAP,\r\n },\r\n data: {\r\n title: NODE_TITLES[type],\r\n status: \"pending\",\r\n ...(type === \"approval\" && { approvers: [], approvalMode: \"any\" }),\r\n ...(type === \"copy\" && { copyUsers: [] }),\r\n ...(type === \"condition\" && { conditions: [] }),\r\n },\r\n });\r\n\r\n /** ๅฐๆฐ่็นๆๅ
ฅๅฐ targetNode ไนๅ๏ผ้่ฟๆๆ่พน */\r\n const reconnectEdges = (\r\n targetNode: WorkflowNode,\r\n newNode: WorkflowNode,\r\n ): void => {\r\n const outgoing = edges.value.filter((e) => e.source === targetNode.id);\r\n edges.value = edges.value.filter((e) => e.source !== targetNode.id);\r\n\r\n /* ๅ่็น โ ๆฐ่็น */\r\n edges.value.push({\r\n id: generateEdgeId(targetNode.id, newNode.id),\r\n source: targetNode.id,\r\n target: newNode.id,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n\r\n /* ๆฐ่็น โ ๅไธๆธธ */\r\n outgoing.forEach((edge) => {\r\n edges.value.push({\r\n id: generateEdgeId(newNode.id, edge.target),\r\n source: newNode.id,\r\n target: edge.target,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n });\r\n };\r\n\r\n /* โโโ ่็นๆทปๅ ๏ผๅ
ฌๅผ๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const addNode = (type: NodeType): void => {\r\n try {\r\n const { targetNodeIndex, targetNode } = getTargetNodeInfo();\r\n const newNode = createNewNode(type, targetNode);\r\n\r\n nodes.value.splice(targetNodeIndex + 1, 0, newNode);\r\n\r\n /* ไธๆจๅ็ปญ่็น */\r\n for (let i = targetNodeIndex + 2; i < nodes.value.length; i++) {\r\n nodes.value[i].position.y += NODE_Y_GAP;\r\n }\r\n\r\n if (targetNode) reconnectEdges(targetNode, newNode);\r\n\r\n showAddMenu.value = false;\r\n currentAddNodeId.value = null;\r\n emitChange();\r\n deferFitView();\r\n } catch (error) {\r\n console.error(\"Error adding node:\", error);\r\n message.error(\"ๆทปๅ ่็นๅคฑ่ดฅ๏ผ่ฏท้่ฏ\");\r\n }\r\n };\r\n\r\n /* โโโ ่็นไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const onNodeClick = (event: { node: WorkflowNode }): void => {\r\n const { node } = event;\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n emit(\"node-click\", node);\r\n };\r\n\r\n const handleConfigSave = (configData: Record<string, unknown>): void => {\r\n if (!currentNode.value) return;\r\n\r\n const nodeIndex = nodes.value.findIndex(\r\n (n) => n.id === currentNode.value!.id,\r\n );\r\n if (nodeIndex !== -1) {\r\n const updatedNode = {\r\n ...nodes.value[nodeIndex],\r\n data: { ...nodes.value[nodeIndex].data, ...configData },\r\n };\r\n nodes.value.splice(nodeIndex, 1, updatedNode);\r\n currentNode.value = updatedNode;\r\n }\r\n\r\n emitChange();\r\n showNodeConfig.value = false;\r\n message.success(\"่็น้
็ฝฎๅทฒไฟๅญ\");\r\n };\r\n\r\n /* โโโ ็ปๅธๆไฝ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const fitView = (): void => {\r\n if (!vueFlowRef.value?.fitView) {\r\n message.warning(\"็ปๅธๆชๅๅคๅฐฑ็ปช๏ผ่ฏท็จๅ้่ฏ\");\r\n return;\r\n }\r\n nextTick(() => {\r\n vueFlowRef.value.fitView({\r\n padding: 50,\r\n includeHiddenNodes: false,\r\n minZoom: 0.5,\r\n maxZoom: 1.5,\r\n duration: 800,\r\n });\r\n });\r\n message.success(\"ๅทฒ้ๅบ็ชๅฃๅคงๅฐ\");\r\n };\r\n\r\n /** ้็ฝฎ่็นๅ่พน๏ผไธๆถๅ้ช่ฏ็ถๆ๏ผ */\r\n const resetNodes = (): void => {\r\n nodes.value = [{ ...INITIAL_NODE }];\r\n edges.value = [];\r\n emitChange();\r\n deferFitView(80, 600);\r\n message.success(\"็ปๅธๅทฒๆธ
็ฉบ\");\r\n };\r\n\r\n /* โโโ ็ๅฝๅจๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n watch(\r\n () => props.modelValue,\r\n (newValue) => {\r\n if (newValue && newValue !== getCurrentWorkflowData()) {\r\n nodes.value = newValue.nodes || [];\r\n edges.value = newValue.edges || [];\r\n }\r\n },\r\n { deep: true },\r\n );\r\n\r\n onMounted(() => {\r\n nextTick(() => {\r\n setTimeout(() => {\r\n vueFlowRef.value?.fitView?.({\r\n padding: 80,\r\n includeHiddenNodes: false,\r\n minZoom: 0.8,\r\n maxZoom: 1.2,\r\n duration: 600,\r\n });\r\n }, 300);\r\n });\r\n });\r\n\r\n return {\r\n /* ็ถๆ */\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n /* ่ฎก็ฎๅฑๆง */\r\n nodeTypes,\r\n workflowStats,\r\n /* ๆนๆณ */\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n };\r\n}\r\n","/**\r\n * ๅทฅไฝๆต้ช่ฏ Composable\r\n * ๅฐ่ฃ
ๆต็จๆ ก้ช้ป่พใ้่ฏฏๅฑ็คบๅ่็นๅฎไฝ\r\n */\r\n\r\nimport { ref } from \"vue\";\r\nimport type { Ref } from \"vue\";\r\nimport { useMessage } from \"naive-ui\";\r\nimport type { WorkflowNode, WorkflowEdge, ValidationError } from \"../types\";\r\nimport { FIELD_DISPLAY_NAMES, ERROR_TYPE_TEXTS } from \"../data\";\r\n\r\nexport interface WorkflowValidationOptions {\r\n /** ๅฎไฝ้่ฏฏ่็นๅๆๅผ้
็ฝฎๅผน็ช็ๅ่ฐ */\r\n onShowNodeConfig?: (node: WorkflowNode) => void;\r\n /** ้ช่ฏๅคฑ่ดฅๆถ็ๅค้จๅ่ฐ๏ผ้ๅธธ็จไบ emit๏ผ */\r\n onValidateError?: (errors: ValidationError[]) => void;\r\n}\r\n\r\n/** ๅทฅไฝๆต้ช่ฏ โโ ๅฐ่ฃ
ๆต็จๆ ก้ช้ป่พใ้่ฏฏๅฑ็คบๅ่็นๅฎไฝ */\r\nexport function useWorkflowValidation(\r\n nodes: Ref<WorkflowNode[]>,\r\n edges: Ref<WorkflowEdge[]>,\r\n vueFlowRef: Ref,\r\n options?: WorkflowValidationOptions,\r\n) {\r\n const message = useMessage();\r\n\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const validationErrors = ref<ValidationError[]>([]);\r\n const showValidationErrors = ref(false);\r\n\r\n /* โโโ ๆ ธๅฟ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆง่กๅทฅไฝๆต้ช่ฏ๏ผ่ฟๅ้่ฏฏๅ่กจ๏ผ็บฏ้ป่พ๏ผไธๆไฝ UI๏ผ */\r\n const validateWorkflow = (): ValidationError[] => {\r\n const errors: ValidationError[] = [];\r\n\r\n nodes.value.forEach((node) => {\r\n /* ๅฎกๆน่็น๏ผๅฟ
้กปๆๅฎกๆนไบบ */\r\n if (node.type === \"approval\") {\r\n const approvers = (node.data as any).approvers || [];\r\n if (approvers.length === 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"approvers\",\r\n message: \"ๅฎกๆน่็นๅฟ
้กป่ฎพ็ฝฎ่ณๅฐไธไธชๅฎกๆนไบบ๏ผๅฆๅๆต็จๆ ๆณๆญฃๅธธ่ฟ่ก\",\r\n type: \"required\",\r\n });\r\n }\r\n }\r\n\r\n /* ๆกไปถ่็น๏ผๅฟ
้กปๆๅๆฏไธ้
็ฝฎๅฎๆด */\r\n if (node.type === \"condition\") {\r\n const conditions = (node.data as any).conditions || [];\r\n if (conditions.length === 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"conditions\",\r\n message:\r\n \"ๆกไปถๅๆฏ่็นๅฟ
้กป้
็ฝฎ่ณๅฐไธไธชๅๆฏๆกไปถ๏ผๅฆๅๆ ๆณ่ฟ่กๆกไปถๅคๆญ\",\r\n type: \"required\",\r\n });\r\n } else {\r\n const incomplete = conditions.filter(\r\n (c: any) => !c.name || !c.field || !c.operator || !c.value,\r\n );\r\n if (incomplete.length > 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"conditions\",\r\n message: `ๆ ${incomplete.length} ไธชๆกไปถๅๆฏ้
็ฝฎไธๅฎๆด๏ผ่ฏทๅฎๅๆๆๅฟ
ๅกซๅญๆฎต`,\r\n type: \"incomplete\",\r\n });\r\n }\r\n }\r\n }\r\n });\r\n\r\n /* ๆฃๆฅๆญ่ฟ่็น */\r\n const connectedNodes = new Set<string>();\r\n edges.value.forEach((edge) => {\r\n connectedNodes.add(edge.source);\r\n connectedNodes.add(edge.target);\r\n });\r\n\r\n nodes.value.forEach((node) => {\r\n if (node.type !== \"start\" && !connectedNodes.has(node.id)) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"connection\",\r\n message: \"ๆญค่็นๆชไธๅ
ถไป่็น่ฟๆฅ๏ผๅฏ่ฝๅฏผ่ดๆต็จไธญๆญ\",\r\n type: \"warning\",\r\n });\r\n }\r\n });\r\n\r\n return errors;\r\n };\r\n\r\n /* โโโ UI ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆง่ก้ช่ฏๅนถๆดๆฐ UI ็ถๆ๏ผๆถๆฏๆ็คบ + ๆฝๅฑ๏ผ */\r\n const validateCurrentWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n validationErrors.value = errors;\r\n\r\n if (errors.length === 0) {\r\n message.success(\"ๅทฅไฝๆต้ช่ฏ้่ฟ๏ผๆๆ่็น้
็ฝฎๆญฃ็กฎ\");\r\n showValidationErrors.value = false;\r\n } else {\r\n message.error(`ๅ็ฐ ${errors.length} ไธช้ฎ้ข๏ผ่ฏทๆฅ็่ฏฆ็ป้่ฏฏ`);\r\n showValidationErrors.value = true;\r\n options?.onValidateError?.(errors);\r\n }\r\n };\r\n\r\n /** ๅฎไฝๅฐ้่ฏฏ่็นๅนถๆๅผ้
็ฝฎๅผน็ช */\r\n const jumpToErrorNode = (nodeId: string): void => {\r\n const node = nodes.value.find((n) => n.id === nodeId);\r\n if (!node || !vueFlowRef.value) return;\r\n\r\n vueFlowRef.value.setCenter(node.position.x, node.position.y, {\r\n zoom: 1.2,\r\n duration: 800,\r\n });\r\n\r\n setTimeout(() => {\r\n options?.onShowNodeConfig?.(node);\r\n showValidationErrors.value = false;\r\n }, 900);\r\n\r\n message.info(`ๅทฒๅฎไฝๅฐ่็น๏ผ${node.data.title}`);\r\n };\r\n\r\n /* โโโ ่พ
ๅฉๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const getFieldDisplayName = (field: string): string =>\r\n FIELD_DISPLAY_NAMES[field] || field;\r\n\r\n const getErrorTypeText = (type: string): string =>\r\n ERROR_TYPE_TEXTS[type] || type;\r\n\r\n /** ้็ฝฎ้ช่ฏ็ถๆ */\r\n const resetValidation = (): void => {\r\n validationErrors.value = [];\r\n showValidationErrors.value = false;\r\n };\r\n\r\n return {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n };\r\n}\r\n","/**\r\n * ๅทฅไฝๆต้ข่ง Composable\r\n * ๅฐ่ฃ
ๆต็จ้ข่ง้ป่พใ่็นๆๅบๅๆญฅ้ชคๅฑ็คบ\r\n */\r\n\r\nimport { ref } from \"vue\";\r\nimport type { Ref } from \"vue\";\r\nimport type { WorkflowNode, WorkflowEdge } from \"../types\";\r\n\r\n/** ่ฏฆๆ
้กน็ฑปๅ */\r\nexport type DetailItemType = \"text\" | \"mode\" | \"users\" | \"warning\";\r\n\r\n/** ็ปๆๅ่ฏฆๆ
้กน */\r\nexport interface DetailItem {\r\n type: DetailItemType;\r\n label: string;\r\n value?: string;\r\n users?: { name: string; department?: string }[];\r\n modeKey?: string;\r\n}\r\n\r\n/** ๆต็จๆญฅ้ชค๏ผๅทฒๆๅบ๏ผ */\r\nexport interface FlowStep {\r\n order: number;\r\n node: WorkflowNode;\r\n nodeTypeLabel: string;\r\n icon: string;\r\n colorClass: string;\r\n details: DetailItem[];\r\n children?: FlowStep[];\r\n}\r\n\r\n/** ้ข่ง็ป่ฎกไฟกๆฏ */\r\nexport interface PreviewStats {\r\n totalNodes: number;\r\n approvalNodes: number;\r\n copyNodes: number;\r\n conditionNodes: number;\r\n totalEdges: number;\r\n}\r\n\r\nconst NODE_TYPE_META: Record<\r\n string,\r\n { label: string; icon: string; colorClass: string }\r\n> = {\r\n start: { label: \"ๅ่ตทไบบ\", icon: \"mdi:play-circle\", colorClass: \"start\" },\r\n approval: {\r\n label: \"ๅฎกๆน่็น\",\r\n icon: \"mdi:account-check\",\r\n colorClass: \"approval\",\r\n },\r\n copy: { label: \"ๆ้่็น\", icon: \"mdi:email-outline\", colorClass: \"copy\" },\r\n condition: {\r\n label: \"ๆกไปถๅๆฏ\",\r\n icon: \"mdi:source-branch\",\r\n colorClass: \"condition\",\r\n },\r\n};\r\n\r\nconst APPROVAL_MODE_LABELS: Record<string, string> = {\r\n any: \"ๆ็ญพ๏ผไปปไธๅฎกๆนไบบ้่ฟๅณๅฏ๏ผ\",\r\n all: \"ไผ็ญพ๏ผๆๆๅฎกๆนไบบๅฟ
้กป้่ฟ๏ผ\",\r\n sequence: \"้กบๅบๅฎกๆน๏ผๆ้กบๅบไพๆฌกๅฎกๆน๏ผ\",\r\n};\r\n\r\nconst OPERATOR_LABELS: Record<string, string> = {\r\n equals: \"็ญไบ\",\r\n not_equals: \"ไธ็ญไบ\",\r\n greater_than: \"ๅคงไบ\",\r\n less_than: \"ๅฐไบ\",\r\n contains: \"ๅ
ๅซ\",\r\n};\r\n\r\n/** ๅทฅไฝๆต้ข่ง โโ ๅฐ่ฃ
ๆต็จ้ข่ง้ป่พใ่็นๆๅบๅๆญฅ้ชคๅฑ็คบ */\r\nexport function useWorkflowPreview(\r\n nodes: Ref<WorkflowNode[]>,\r\n edges: Ref<WorkflowEdge[]>,\r\n) {\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const showPreview = ref(false);\r\n const previewSteps = ref<FlowStep[]>([]);\r\n const previewStats = ref<PreviewStats>({\r\n totalNodes: 0,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n totalEdges: 0,\r\n });\r\n\r\n /* โโโ ่็น่ฏฆๆ
ๆๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅๅผๅง่็น่ฏฆๆ
*/\r\n const extractStartDetails = (data: any): DetailItem[] => {\r\n const initiators = data.initiators || [];\r\n return initiators.length > 0\r\n ? [{ type: \"users\", label: \"ๅ่ตทไบบ\", users: initiators }]\r\n : [{ type: \"text\", label: \"ๅ่ตทไบบ\", value: \"ๆๆไบบ\" }];\r\n };\r\n\r\n /** ๆๅๅฎกๆน่็น่ฏฆๆ
*/\r\n const extractApprovalDetails = (data: any): DetailItem[] => {\r\n const mode = data.approvalMode || \"any\";\r\n const approvers = data.approvers || [];\r\n const items: DetailItem[] = [\r\n {\r\n type: \"mode\",\r\n label: \"ๅฎกๆนๆนๅผ\",\r\n value: APPROVAL_MODE_LABELS[mode] || mode,\r\n modeKey: mode,\r\n },\r\n ];\r\n if (approvers.length > 0) {\r\n items.push({ type: \"users\", label: \"ๅฎกๆนไบบ\", users: approvers });\r\n } else {\r\n items.push({ type: \"warning\", label: \"ๅฎกๆนไบบ\", value: \"ๆช่ฎพ็ฝฎ\" });\r\n }\r\n return items;\r\n };\r\n\r\n /** ๆๅๆ้่็น่ฏฆๆ
*/\r\n const extractCopyDetails = (data: any): DetailItem[] => {\r\n const copyUsers = data.copyUsers || [];\r\n return copyUsers.length > 0\r\n ? [{ type: \"users\", label: \"ๆ้ไบบ\", users: copyUsers }]\r\n : [{ type: \"text\", label: \"ๆ้ไบบ\", value: \"ๆช่ฎพ็ฝฎ\" }];\r\n };\r\n\r\n /** ๆๅๆกไปถ่็น่ฏฆๆ
*/\r\n const extractConditionDetails = (data: any): DetailItem[] => {\r\n const conditions = data.conditions || [];\r\n if (conditions.length === 0) {\r\n return [{ type: \"warning\", label: \"ๆกไปถๅๆฏ\", value: \"ๆช้
็ฝฎ\" }];\r\n }\r\n return conditions.map((cond: any, i: number) => {\r\n const op = OPERATOR_LABELS[cond.operator] || cond.operator;\r\n return {\r\n type: \"text\" as const,\r\n label: `ๅๆฏ ${i + 1}`,\r\n value: `${cond.name || \"ๆชๅฝๅ\"} โ ${cond.field || \"?\"} ${op} ${cond.value || \"?\"}`,\r\n };\r\n });\r\n };\r\n\r\n /** ่็น่ฏฆๆ
ๆๅ็ญ็ฅ */\r\n const detailExtractors: Record<string, (data: any) => DetailItem[]> = {\r\n start: extractStartDetails,\r\n approval: extractApprovalDetails,\r\n copy: extractCopyDetails,\r\n condition: extractConditionDetails,\r\n };\r\n\r\n /** ไป่็นๆฐๆฎไธญๆๅๅฏ่ฏป็้
็ฝฎ่ฏฆๆ
*/\r\n const extractNodeDetails = (node: WorkflowNode): DetailItem[] => {\r\n const extractor = detailExtractors[node.type || \"start\"];\r\n return extractor ? extractor(node.data) : [];\r\n };\r\n\r\n /* โโโ ๆๆๆๅบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๅบไบ BFS ๅฏนๆต็จ่็น่ฟ่กๆๆๆๅบ */\r\n const sortNodesByFlow = (): WorkflowNode[] => {\r\n const nodeMap = new Map<string, WorkflowNode>();\r\n nodes.value.forEach((n) => nodeMap.set(n.id, n));\r\n\r\n const adjacency = new Map<string, string[]>();\r\n const inDegree = new Map<string, number>();\r\n nodes.value.forEach((n) => {\r\n adjacency.set(n.id, []);\r\n inDegree.set(n.id, 0);\r\n });\r\n\r\n edges.value.forEach((e) => {\r\n adjacency.get(e.source)?.push(e.target);\r\n inDegree.set(e.target, (inDegree.get(e.target) || 0) + 1);\r\n });\r\n\r\n /* ไปๅ
ฅๅบฆไธบ 0 ็่็น๏ผstart๏ผๅผๅง BFS */\r\n const queue: string[] = [];\r\n inDegree.forEach((deg, id) => {\r\n if (deg === 0) queue.push(id);\r\n });\r\n\r\n const sorted: WorkflowNode[] = [];\r\n const visited = new Set<string>();\r\n\r\n while (queue.length > 0) {\r\n const id = queue.shift()!;\r\n if (visited.has(id)) continue;\r\n visited.add(id);\r\n\r\n const node = nodeMap.get(id);\r\n if (node) sorted.push(node);\r\n\r\n const neighbors = adjacency.get(id) || [];\r\n for (const next of neighbors) {\r\n const newDeg = (inDegree.get(next) || 1) - 1;\r\n inDegree.set(next, newDeg);\r\n if (newDeg <= 0 && !visited.has(next)) {\r\n queue.push(next);\r\n }\r\n }\r\n }\r\n\r\n /* ่ฟฝๅ ๆช่ฟๆฅ็ๅญค็ซ่็น */\r\n nodes.value.forEach((n) => {\r\n if (!visited.has(n.id)) sorted.push(n);\r\n });\r\n\r\n return sorted;\r\n };\r\n\r\n /* โโโ ็ๆๆต็จๆญฅ้ชค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅปบ้ข่งๆญฅ้ชคๅ่กจ */\r\n const buildFlowSteps = (): FlowStep[] => {\r\n const sorted = sortNodesByFlow();\r\n return sorted.map((node, index) => {\r\n const meta = NODE_TYPE_META[node.type || \"start\"] || NODE_TYPE_META.start;\r\n return {\r\n order: index + 1,\r\n node,\r\n nodeTypeLabel: meta.label,\r\n icon: meta.icon,\r\n colorClass: meta.colorClass,\r\n details: extractNodeDetails(node),\r\n };\r\n });\r\n };\r\n\r\n /* โโโ ็ป่ฎกไฟกๆฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const computeStats = (): PreviewStats => {\r\n const stats: PreviewStats = {\r\n totalNodes: nodes.value.length,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n totalEdges: edges.value.length,\r\n };\r\n nodes.value.forEach((n) => {\r\n if (n.type === \"approval\") stats.approvalNodes++;\r\n else if (n.type === \"copy\") stats.copyNodes++;\r\n else if (n.type === \"condition\") stats.conditionNodes++;\r\n });\r\n return stats;\r\n };\r\n\r\n /* โโโ ๅ
ฌๅผๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅผ้ข่ง้ขๆฟ */\r\n const openPreview = (): void => {\r\n previewSteps.value = buildFlowSteps();\r\n previewStats.value = computeStats();\r\n showPreview.value = true;\r\n };\r\n\r\n /** ๅ
ณ้ญ้ข่ง้ขๆฟ */\r\n const closePreview = (): void => {\r\n showPreview.value = false;\r\n };\r\n\r\n return {\r\n showPreview,\r\n previewSteps,\r\n previewStats,\r\n openPreview,\r\n closePreview,\r\n };\r\n}\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_WorkFlow2.js","names":["data","data","data","data","StartNode","ApprovalNode","CopyNode","ConditionNode","users","departments"],"sources":["../src/components/C_WorkFlow/data.ts","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/NodeConfigModal.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/StartNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/ApprovalNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/CopyNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/nodes/ConditionNode.vue","../src/components/C_WorkFlow/composables/useWorkflowNodes.ts","../src/components/C_WorkFlow/composables/useWorkflowValidation.ts","../src/components/C_WorkFlow/composables/useWorkflowPreview.ts","../src/components/C_WorkFlow/index.vue","../src/components/C_WorkFlow/index.vue","../src/components/C_WorkFlow/index.vue"],"sourcesContent":["/*\r\n * @Description: ๅทฅไฝ๏ผๅฎกๆนๆต๏ผๆต็ปไปถ - ๆฐๆฎๅฑ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n */\r\nimport type { NodeType, WorkflowNode } from \"./types\";\r\n\r\n/* ่็น็ฑปๅ้้กน */\r\nexport const NODE_TYPE_OPTIONS = [\r\n {\r\n type: \"approval\" as NodeType,\r\n label: \"ๅฎกๆนไบบ\",\r\n icon: \"mdi:account-check\",\r\n iconClass: \"approval-icon\",\r\n },\r\n {\r\n type: \"copy\" as NodeType,\r\n label: \"ๆ้ไบบ\",\r\n icon: \"mdi:email-outline\",\r\n iconClass: \"copy-icon\",\r\n },\r\n {\r\n type: \"condition\" as NodeType,\r\n label: \"ๆกไปถๅๆฏ\",\r\n icon: \"mdi:source-branch\",\r\n iconClass: \"condition-icon\",\r\n },\r\n];\r\n\r\n/* ๅฎกๆนๆจกๅผ้้กน */\r\nexport const APPROVAL_MODES = [\r\n { value: \"any\", label: \"ๆ็ญพ\", desc: \"ไปปๆไธไบบๅๆๅณๅฏ้่ฟ\" },\r\n { value: \"all\", label: \"ไผ็ญพ\", desc: \"ๆๆไบบ้ฝๅๆๆ่ฝ้่ฟ\" },\r\n { value: \"sequence\", label: \"้กบๅบๅฎกๆน\", desc: \"ๆ้ๆฉ้กบๅบไพๆฌกๅฎกๆน\" },\r\n];\r\n\r\n/* ๅญๆฎต้้กน */\r\nexport const FIELD_OPTIONS = [\r\n { label: \"็ณ่ฏท้้ข\", value: \"amount\" },\r\n { label: \"็ณ่ฏทไบบ้จ้จ\", value: \"department\" },\r\n { label: \"็ณ่ฏท็ฑปๅ\", value: \"type\" },\r\n { label: \"็ดงๆฅ็จๅบฆ\", value: \"priority\" },\r\n];\r\n\r\n/* ๆไฝ็ฌฆ้้กน */\r\nexport const OPERATOR_OPTIONS = [\r\n { label: \"็ญไบ\", value: \"equals\" },\r\n { label: \"ๅคงไบ\", value: \"greater_than\" },\r\n { label: \"ๅฐไบ\", value: \"less_than\" },\r\n { label: \"ๅ
ๅซ\", value: \"contains\" },\r\n { label: \"ไธ็ญไบ\", value: \"not_equals\" },\r\n];\r\n\r\n/* ่็นๆ ้ขๆ ๅฐ */\r\nexport const NODE_TITLES: Record<NodeType, string> = {\r\n start: \"ๅ่ตทไบบ\",\r\n approval: \"ๅฎกๆนไบบ\",\r\n copy: \"ๆ้ไบบ\",\r\n condition: \"ๆกไปถๅๆฏ\",\r\n};\r\n\r\n/* ้
็ฝฎๆ ้ขๆ ๅฐ */\r\nexport const CONFIG_TITLES = {\r\n approval: \"ๅฎกๆนไบบ่ฎพ็ฝฎ\",\r\n copy: \"ๆ้ไบบ่ฎพ็ฝฎ\",\r\n condition: \"ๆกไปถๅๆฏ่ฎพ็ฝฎ\",\r\n};\r\n\r\n/* ๅญๆฎตๆพ็คบๅ็งฐๆ ๅฐ */\r\nexport const FIELD_DISPLAY_NAMES: Record<string, string> = {\r\n approvers: \"ๅฎกๆนไบบ\",\r\n conditions: \"ๆกไปถ้
็ฝฎ\",\r\n connection: \"่็น่ฟๆฅ\",\r\n copyUsers: \"ๆ้ไบบ\",\r\n};\r\n\r\n/* ้่ฏฏ็ฑปๅๆๆฌๆ ๅฐ */\r\nexport const ERROR_TYPE_TEXTS: Record<string, string> = {\r\n required: \"ๅฟ
ๅกซ\",\r\n incomplete: \"ไธๅฎๆด\",\r\n warning: \"่ญฆๅ\",\r\n error: \"้่ฏฏ\",\r\n};\r\n\r\n/** ่็นไน้ด็ Y ่ฝด้ด่ท๏ผpx๏ผ */\r\nexport const NODE_Y_GAP = 180;\r\n\r\n/* ๅๅง่็นๆฐๆฎ */\r\nexport const INITIAL_NODE: WorkflowNode = {\r\n id: \"start-1\",\r\n type: \"start\",\r\n position: { x: 150, y: 100 },\r\n data: { title: \"ๅ่ตทไบบ\", status: \"active\", initiators: [] },\r\n};\r\n\r\n/* ้ป่ฎคๅคดๅ็ๆๅฝๆฐ */\r\nexport const getDefaultAvatar = (name: string): string =>\r\n `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n\r\n/* ็ๆๆกไปถID็ๅฝๆฐ */\r\nexport const generateConditionId = (): string => `condition-${Date.now()}`;\r\n\r\n/* ็ๆ่พนID็ๅฝๆฐ */\r\nexport const generateEdgeId = (sourceId: string, targetId: string): string =>\r\n `edge-${sourceId}-${targetId}`;\r\n\r\n/* ้ป่ฎคๆกไปถๅฏน่ฑกๅทฅๅๅฝๆฐ */\r\nexport const createDefaultCondition = () => ({\r\n id: generateConditionId(),\r\n name: \"\",\r\n field: \"\",\r\n operator: \"equals\" as const,\r\n value: \"\",\r\n});\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","<!--\r\n * @Description: ่็น้
็ฝฎๅผน็ช็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <NModal\r\n v-model:show=\"visible\"\r\n style=\"width: 900px\"\r\n :mask-closable=\"false\"\r\n preset=\"dialog\"\r\n :title=\"configTitle\"\r\n positive-text=\"็กฎๅฎ\"\r\n negative-text=\"ๅๆถ\"\r\n :loading=\"configLoading\"\r\n @positive-click=\"saveNodeConfig\"\r\n @negative-click=\"handleCancel\"\r\n >\r\n <!-- ๅ
ฌๅ
ฑ้ๆฉ้ขๆฟ -->\r\n <template v-if=\"isUserSelectNode\">\r\n <div class=\"max-h-60vh overflow-y-auto\">\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon\r\n :name=\"userSelectConfig.icon\"\r\n :size=\"16\"\r\n v-if=\"userSelectConfig.icon\"\r\n />\r\n {{ userSelectConfig.title }}\r\n </h4>\r\n\r\n <NInput\r\n v-model:value=\"searchKeyword\"\r\n placeholder=\"ๆ็ดข็จๆทๅงๅๆ้จ้จ\"\r\n clearable\r\n class=\"mb-4\"\r\n >\r\n <template #prefix>\r\n <C_Icon name=\"mdi:magnify\" :size=\"16\" />\r\n </template>\r\n </NInput>\r\n\r\n <div\r\n class=\"border rounded-lg p-3 mb-4 max-h-50 overflow-y-auto\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <NTree\r\n :data=\"departmentUserTree\"\r\n :checked-keys=\"userSelectConfig.checkedKeys\"\r\n :selectable=\"false\"\r\n checkable\r\n cascade\r\n :virtual-scroll=\"true\"\r\n style=\"max-height: 300px\"\r\n @update:checked-keys=\"userSelectConfig.onSelect\"\r\n />\r\n </div>\r\n\r\n <div\r\n v-if=\"userSelectConfig.selectedUsers.length > 0\"\r\n class=\"rounded-lg p-4 mb-4 border\"\r\n :style=\"{\r\n borderColor: 'var(--n-border-color)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >\r\n <h5\r\n class=\"mb-2 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n {{ userSelectConfig.selectedLabel }}\r\n ({{ userSelectConfig.selectedUsers.length }})\r\n </h5>\r\n <div class=\"flex flex-wrap gap-3\">\r\n <NTag\r\n v-for=\"user in userSelectConfig.selectedUsers\"\r\n :key=\"user.id\"\r\n closable\r\n :type=\"userSelectConfig.tagType\"\r\n @close=\"userSelectConfig.onRemove(user.id)\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <NAvatar\r\n :src=\"user.avatar\"\r\n :fallback-src=\"getDefaultAvatar(user.name)\"\r\n size=\"small\"\r\n />\r\n <span class=\"font-medium text-sm\">{{ user.name }}</span>\r\n <span\r\n class=\"text-xs px-1.5 py-0.5 rounded\"\r\n :style=\"{\r\n color: 'var(--n-text-color-3)',\r\n background: 'var(--n-color-embedded)',\r\n }\"\r\n >{{ user.department }}</span\r\n >\r\n </div>\r\n </NTag>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- ๅฎกๆน่็นๆถ้ๅธฆๅฎกๆนๆจกๅผ -->\r\n <template v-if=\"props.currentNode?.type === 'approval'\">\r\n <div class=\"mt-4\">\r\n <h5\r\n class=\"mb-3 text-sm font-medium\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n ๅฎกๆนๆจกๅผ\r\n </h5>\r\n <NRadioGroup v-model:value=\"approvalMode\">\r\n <NSpace vertical>\r\n <NRadio\r\n v-for=\"mode in APPROVAL_MODES\"\r\n :key=\"mode.value\"\r\n :value=\"mode.value\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <strong class=\"text-sm\">{{ mode.label }}</strong>\r\n <span\r\n class=\"text-xs\"\r\n :style=\"{ color: 'var(--n-text-color-3)' }\"\r\n >{{ mode.desc }}</span\r\n >\r\n </div>\r\n </NRadio>\r\n </NSpace>\r\n </NRadioGroup>\r\n </div>\r\n </template>\r\n </div>\r\n </template>\r\n\r\n <!-- ๆกไปถ่็น้
็ฝฎ -->\r\n <div\r\n v-else-if=\"props.currentNode?.type === 'condition'\"\r\n class=\"max-h-60vh overflow-y-auto\"\r\n >\r\n <div class=\"config-section\">\r\n <h4\r\n class=\"flex items-center gap-2 mb-4 text-base font-semibold\"\r\n :style=\"{ color: 'var(--n-text-color-1)' }\"\r\n >\r\n <C_Icon name=\"mdi:source-branch\" :size=\"16\" />\r\n ๆกไปถๅๆฏ่ฎพ็ฝฎ\r\n </h4>\r\n <div class=\"space-y-3\">\r\n <div\r\n v-for=\"(condition, index) in conditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-item\"\r\n >\r\n <NCard\r\n size=\"small\"\r\n class=\"border border-gray-200 hover:border-gray-300 transition-colors\"\r\n >\r\n <div class=\"flex items-center gap-3 flex-nowrap min-h-10 p-2\">\r\n <NInput\r\n v-model:value=\"condition.name\"\r\n placeholder=\"ๅๆฏๅ็งฐ\"\r\n style=\"width: 150px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.field\"\r\n placeholder=\"้ๆฉๅญๆฎต\"\r\n :options=\"FIELD_OPTIONS\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NSelect\r\n v-model:value=\"condition.operator\"\r\n placeholder=\"ๆไฝ็ฌฆ\"\r\n :options=\"OPERATOR_OPTIONS\"\r\n style=\"width: 100px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NInput\r\n v-model:value=\"condition.value\"\r\n placeholder=\"ๅผ\"\r\n style=\"width: 120px\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <NButton\r\n quaternary\r\n type=\"error\"\r\n @click=\"removeCondition(index)\"\r\n class=\"flex-shrink-0 ml-auto\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </NCard>\r\n </div>\r\n <NButton dashed block @click=\"addCondition\" class=\"w-full\">\r\n <template #icon><C_Icon name=\"mdi:plus\" :size=\"16\" /></template>\r\n ๆทปๅ ๆกไปถ\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n </NModal>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport {\r\n NModal,\r\n NInput,\r\n NTree,\r\n NTag,\r\n NAvatar,\r\n NRadioGroup,\r\n NRadio,\r\n NSpace,\r\n NCard,\r\n NSelect,\r\n NButton,\r\n useMessage,\r\n} from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowNode, User, Department, Condition } from \"./types\";\r\nimport {\r\n APPROVAL_MODES,\r\n FIELD_OPTIONS,\r\n OPERATOR_OPTIONS,\r\n CONFIG_TITLES,\r\n getDefaultAvatar,\r\n createDefaultCondition,\r\n} from \"./data\";\r\n\r\ninterface Props {\r\n show: boolean;\r\n currentNode: WorkflowNode | null;\r\n users: User[];\r\n departments: Department[];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n show: false,\r\n currentNode: null,\r\n users: () => [],\r\n departments: () => [],\r\n});\r\nconst emit = defineEmits([\"update:show\", \"save\", \"cancel\"]);\r\nconst message = useMessage();\r\n\r\nconst searchKeyword = ref(\"\");\r\nconst selectedUsers = ref<string[]>([]);\r\nconst selectedCopyUsers = ref<string[]>([]);\r\nconst approvalMode = ref<\"any\" | \"all\" | \"sequence\">(\"any\");\r\nconst configLoading = ref(false);\r\nconst conditions = ref<Condition[]>([]);\r\n\r\nconst visible = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit(\"update:show\", value),\r\n});\r\nconst configTitle = computed(() => {\r\n if (props.currentNode?.type === \"start\") return \"ๅ่ตทไบบ่ฎพ็ฝฎ\";\r\n const type = props.currentNode?.type as keyof typeof CONFIG_TITLES;\r\n return CONFIG_TITLES[type] || \"่็น่ฎพ็ฝฎ\";\r\n});\r\n\r\n/* ็จๆทๆ */\r\nconst departmentUserTree = computed(() => {\r\n const tree: any[] = [];\r\n const deptMap = new Map();\r\n props.departments?.forEach((dept) => {\r\n if (!deptMap.has(dept.id)) {\r\n deptMap.set(dept.id, {\r\n key: `dept-${dept.id}`,\r\n label: `${dept.name} ${dept.manager ? `(่ด่ดฃไบบ: ${dept.manager})` : \"\"}`,\r\n children: [],\r\n isLeaf: false,\r\n disabled: true,\r\n });\r\n }\r\n });\r\n const filteredUsers =\r\n props.users?.filter(\r\n (user) =>\r\n !searchKeyword.value ||\r\n user.name.includes(searchKeyword.value) ||\r\n user.department.includes(searchKeyword.value),\r\n ) || [];\r\n\r\n filteredUsers.forEach((user) => {\r\n const dept = props.departments?.find((d) => d.name === user.department);\r\n if (dept && deptMap.has(dept.id)) {\r\n deptMap.get(dept.id).children.push({\r\n key: user.id,\r\n label: `${user.name}${user.role ? `(${user.role})` : \"\"}`,\r\n isLeaf: true,\r\n user,\r\n });\r\n }\r\n });\r\n\r\n deptMap.forEach((dept) => {\r\n if (dept.children.length > 0) {\r\n tree.push(dept);\r\n }\r\n });\r\n\r\n return tree;\r\n});\r\n\r\nconst selectedApprovers = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedCopyUserList = computed(\r\n () =>\r\n props.users?.filter((u) => selectedCopyUsers.value.includes(u.id)) || [],\r\n);\r\nconst selectedInitiators = computed(\r\n () => props.users?.filter((u) => selectedUsers.value.includes(u.id)) || [],\r\n);\r\n\r\nconst handleUserSelect = (keys: string[]) => {\r\n selectedUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst handleCopyUserSelect = (keys: string[]) => {\r\n selectedCopyUsers.value = keys.filter((key) => !key.startsWith(\"dept-\"));\r\n};\r\nconst removeApprover = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\nconst removeCopyUser = (userId: string) => {\r\n selectedCopyUsers.value = selectedCopyUsers.value.filter(\r\n (id) => id !== userId,\r\n );\r\n};\r\nconst removeInitiator = (userId: string) => {\r\n selectedUsers.value = selectedUsers.value.filter((id) => id !== userId);\r\n};\r\n\r\nconst addCondition = () => conditions.value.push(createDefaultCondition());\r\nconst removeCondition = (index: number) => conditions.value.splice(index, 1);\r\n\r\nconst configureStartNode = (node: WorkflowNode) => {\r\n const { initiators } = node.data as any;\r\n selectedUsers.value = initiators ? initiators.map((u: User) => u.id) : [];\r\n};\r\nconst configureApprovalNode = (node: WorkflowNode) => {\r\n const approvers = (node.data as any).approvers || [];\r\n selectedUsers.value = approvers.map((u: User) => u.id);\r\n approvalMode.value = (node.data as any).approvalMode || \"any\";\r\n};\r\nconst configureCopyNode = (node: WorkflowNode) => {\r\n const copyUsers = (node.data as any).copyUsers || [];\r\n selectedCopyUsers.value = copyUsers.map((u: User) => u.id);\r\n};\r\nconst configureConditionNode = (node: WorkflowNode) => {\r\n conditions.value = (node.data as any).conditions || [];\r\n};\r\n\r\nconst saveStartNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท้ๆฉๅ่ตทไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedInitiators.value;\r\n emit(\"save\", { initiators: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅ่ตทไบบ`);\r\n return true;\r\n};\r\nconst saveApprovalNodeConfig = async (): Promise<boolean> => {\r\n if (selectedUsers.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐ้ๆฉไธไธชๅฎกๆนไบบ\");\r\n return false;\r\n }\r\n const selectedUserObjs = selectedApprovers.value;\r\n emit(\"save\", {\r\n approvers: selectedUserObjs,\r\n approvalMode: approvalMode.value,\r\n });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๅฎกๆนไบบ`);\r\n return true;\r\n};\r\nconst saveCopyNodeConfig = async (): Promise<boolean> => {\r\n const selectedUserObjs = selectedCopyUserList.value;\r\n emit(\"save\", { copyUsers: selectedUserObjs });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${selectedUserObjs.length} ไธชๆ้ไบบ`);\r\n return true;\r\n};\r\nconst saveConditionNodeConfig = async (): Promise<boolean> => {\r\n if (conditions.value.length === 0) {\r\n message.error(\"่ฏท่ณๅฐๆทปๅ ไธไธชๆกไปถๅๆฏ\");\r\n return false;\r\n }\r\n const validConditions = conditions.value.filter(\r\n (c) => c.name && c.field && c.operator && c.value,\r\n );\r\n if (validConditions.length === 0) {\r\n message.error(\"่ฏทๅฎๅๆกไปถ้
็ฝฎ\");\r\n return false;\r\n }\r\n emit(\"save\", { conditions: validConditions });\r\n message.success(`ๅทฒ่ฎพ็ฝฎ ${validConditions.length} ไธชๆกไปถๅๆฏ`);\r\n return true;\r\n};\r\nconst saveNodeConfig = async (): Promise<boolean> => {\r\n if (!props.currentNode) return false;\r\n configLoading.value = true;\r\n try {\r\n const nodeSavers = {\r\n start: saveStartNodeConfig,\r\n approval: saveApprovalNodeConfig,\r\n copy: saveCopyNodeConfig,\r\n condition: saveConditionNodeConfig,\r\n };\r\n const saver = nodeSavers[props.currentNode.type as keyof typeof nodeSavers];\r\n const success = saver ? await saver() : false;\r\n return success;\r\n } catch (error) {\r\n message.error(\"ไฟๅญ้
็ฝฎๅคฑ่ดฅ\");\r\n console.error(\"Save node config error:\", error);\r\n return false;\r\n } finally {\r\n configLoading.value = false;\r\n }\r\n};\r\nconst handleCancel = () => {\r\n emit(\"cancel\");\r\n};\r\nwatch(\r\n () => props.currentNode,\r\n (newNode) => {\r\n if (newNode) {\r\n searchKeyword.value = \"\";\r\n const nodeConfigurators = {\r\n start: configureStartNode,\r\n approval: configureApprovalNode,\r\n copy: configureCopyNode,\r\n condition: configureConditionNode,\r\n };\r\n const configurator =\r\n nodeConfigurators[newNode.type as keyof typeof nodeConfigurators];\r\n if (configurator) {\r\n configurator(newNode);\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n\r\nconst isUserSelectNode = computed(() =>\r\n [\"start\", \"approval\", \"copy\"].includes(props.currentNode?.type || \"\"),\r\n);\r\n\r\nconst userSelectConfig = computed(() => {\r\n const type = props.currentNode?.type;\r\n if (type === \"start\") {\r\n return {\r\n icon: \"mdi:account-star\",\r\n title: \"้ๆฉๅ่ตทไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedInitiators.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅ่ตทไบบ\",\r\n tagType: \"primary\" as const,\r\n onRemove: removeInitiator,\r\n };\r\n } else if (type === \"approval\") {\r\n return {\r\n icon: \"mdi:account-check\",\r\n title: \"้ๆฉๅฎกๆนไบบ\",\r\n checkedKeys: selectedUsers.value,\r\n onSelect: handleUserSelect,\r\n selectedUsers: selectedApprovers.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๅฎกๆนไบบ\",\r\n tagType: \"info\" as const,\r\n onRemove: removeApprover,\r\n };\r\n } else if (type === \"copy\") {\r\n return {\r\n icon: \"mdi:email-outline\",\r\n title: \"้ๆฉๆ้ไบบ\",\r\n checkedKeys: selectedCopyUsers.value,\r\n onSelect: handleCopyUserSelect,\r\n selectedUsers: selectedCopyUserList.value,\r\n selectedLabel: \"ๅทฒ้ๆฉๆ้ไบบ\",\r\n tagType: \"success\" as const,\r\n onRemove: removeCopyUser,\r\n };\r\n }\r\n /* ๅฎๅ
จๅ
ๅบ */\r\n return {\r\n icon: \"\",\r\n title: \"\",\r\n checkedKeys: [] as string[],\r\n onSelect: (_keys: string[]) => {},\r\n selectedUsers: [] as User[],\r\n selectedLabel: \"\",\r\n tagType: \"default\" as const,\r\n onRemove: (_id: string) => {},\r\n };\r\n});\r\n</script>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅผๅง่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"start-node\">\r\n <!-- ไธป่ฆๅ
ๅฎนๅบๅ -->\r\n <div class=\"node-content\">\r\n <div class=\"node-icon\">๐</div>\r\n <div class=\"node-info\">\r\n <div class=\"node-text\">{{ data.title }}</div>\r\n <div v-if=\"initiatorNames\" class=\"initiator-name\">\r\n {{ initiatorNames }}\r\n </div>\r\n <div v-else class=\"placeholder-text\">็นๅป่ฎพ็ฝฎๅ่ตทไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆทปๅ ่็นๆ้ฎ -->\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { inject, computed } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface Initiator {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n status?: string;\r\n initiators?: Initiator[];\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\n/* ๅชๆณจๅ
ฅ้่ฆ็ๆนๆณ */\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\n/* ่ฎก็ฎๅ่ตทไบบๅ็งฐ */\r\nconst initiatorNames = computed(() => {\r\n const { initiators } = props.data;\r\n if (!initiators || !Array.isArray(initiators) || initiators.length === 0) {\r\n return \"\";\r\n }\r\n return initiators.map((user) => user?.name || \"ๆช็ฅ็จๆท\").join(\"ใ\");\r\n});\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.start-node {\r\n position: relative;\r\n}\r\n\r\n.node-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 16px 20px;\r\n border-radius: 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 12px;\r\n min-width: 160px;\r\n box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);\r\n }\r\n}\r\n\r\n.node-icon {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.node-info {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n text-align: center;\r\n}\r\n\r\n.node-text {\r\n font-weight: 600;\r\n font-size: 14px;\r\n}\r\n\r\n.initiator-name {\r\n font-size: 12px;\r\n opacity: 0.9;\r\n background: rgba(255, 255, 255, 0.2);\r\n padding: 2px 8px;\r\n border-radius: 8px;\r\n}\r\n\r\n.placeholder-text {\r\n font-size: 11px;\r\n opacity: 0.7;\r\n font-style: italic;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๅฎกๆน่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">\r\n <C_Icon name=\"mdi:account\" :size=\"12\" color=\"white\" />\r\n </div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"approverCount > 0\">\r\n {{ approverCount }}\r\n </div>\r\n <div class=\"approval-mode-badge\" v-if=\"data.approvalMode\">\r\n {{ getApprovalModeText(data.approvalMode) }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"approverCount > 0\" class=\"approvers-list\">\r\n <div\r\n v-for=\"approver in displayApprovers\"\r\n :key=\"approver.id\"\r\n class=\"approver-tag\"\r\n >\r\n <div class=\"approver-info\">\r\n <div class=\"approver-avatar\">\r\n <NAvatar\r\n :src=\"approver.avatar\"\r\n :fallback-src=\"getDefaultAvatar(approver.name)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n <div class=\"approver-details\">\r\n <span class=\"approver-name\">{{ approver.name }}</span>\r\n <span class=\"approver-dept\">{{ approver.department }}</span>\r\n <span class=\"approver-role\">{{ approver.role }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">\r\n <C_Icon name=\"mdi:account-plus\" :size=\"24\" color=\"#9ca3af\" />\r\n <span>่ฏท่ฎพ็ฝฎๅฎกๆนไบบ</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { NAvatar } from \"naive-ui\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๅฎกๆนไบบ็ฑปๅ */\r\ninterface Approver {\r\n id: string;\r\n name: string;\r\n avatar?: string;\r\n department: string;\r\n role: string;\r\n email?: string;\r\n phone?: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n approvers?: Approver[];\r\n approvalMode?: \"any\" | \"all\" | \"sequence\";\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst approvers = computed(() => props.data.approvers ?? []);\r\nconst approverCount = computed(() => approvers.value.length);\r\nconst displayApprovers = computed(() => approvers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, approverCount.value - 3));\r\n\r\nconst getApprovalModeText = (mode?: string) => {\r\n const modeMap = {\r\n any: \"ๆ็ญพ\",\r\n all: \"ไผ็ญพ\",\r\n sequence: \"้กบๅบ\",\r\n };\r\n return modeMap[mode as keyof typeof modeMap] || \"\";\r\n};\r\n\r\nconst getDefaultAvatar = (name: string) => {\r\n return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;\r\n};\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.approval-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.approved {\r\n background: #52c41a;\r\n }\r\n &.rejected {\r\n background: #ff4d4f;\r\n }\r\n &.pending {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100; /* ็กฎไฟๅจๆๆๅ
็ด ไนไธ */\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.approval-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 240px;\r\n max-width: 300px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #1890ff;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border-bottom: 1px solid #91d5ff;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #1890ff;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: var(--n-text-color-1);\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #1890ff;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.approval-mode-badge {\r\n background: #f0f0f0;\r\n color: #666;\r\n padding: 2px 6px;\r\n border-radius: 8px;\r\n font-size: 10px;\r\n font-weight: 500;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.approvers-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n.approver-tag {\r\n background: linear-gradient(135deg, #e6f7ff, #f0f9ff);\r\n border: 1px solid #91d5ff;\r\n border-radius: 8px;\r\n padding: 8px 12px;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n transform: translateX(2px);\r\n box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\r\n }\r\n\r\n .approver-info {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .approver-avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n .approver-details {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n\r\n .approver-name {\r\n font-weight: 600;\r\n color: #1890ff;\r\n font-size: 13px;\r\n }\r\n\r\n .approver-dept {\r\n font-size: 11px;\r\n color: #666;\r\n background: rgba(0, 0, 0, 0.05);\r\n padding: 1px 4px;\r\n border-radius: 4px;\r\n display: inline-block;\r\n }\r\n\r\n .approver-role {\r\n font-size: 10px;\r\n color: #999;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 6px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n border: 1px dashed #d9d9d9;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 20px 0;\r\n border: 1px dashed #d9d9d9;\r\n border-radius: 8px;\r\n background: #fafafa;\r\n\r\n span {\r\n font-weight: 500;\r\n }\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #1890ff, #722ed1);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆ้่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"copy-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\" @click=\"handleNodeClick\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"copyCount > 0\">{{ copyCount }}</div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"copyCount > 0\" class=\"copy-users-list\">\r\n <div\r\n v-for=\"user in displayCopyUsers\"\r\n :key=\"user.id\"\r\n class=\"copy-user-tag\"\r\n >\r\n {{ user.name }}\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๆ้ไบบ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\ninterface User {\r\n id: string;\r\n name: string;\r\n department: string;\r\n role: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n copyUsers?: User[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ็ปไธไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผ */\r\nconst copyUsers = computed(() => props.data.copyUsers ?? []);\r\nconst copyCount = computed(() => copyUsers.value.length);\r\nconst displayCopyUsers = computed(() => copyUsers.value.slice(0, 3));\r\nconst moreCount = computed(() => Math.max(0, copyCount.value - 3));\r\n\r\nconst handleNodeClick = () => {\r\n /* ไธ้ปๆญขไบไปถๅๆณก๏ผ่ฎฉ VueFlow ็ node-click ไบไปถ่ช็ถ่งฆๅ */\r\n};\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.copy-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #1890ff;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.copy-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 200px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #52c41a;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #f6ffed;\r\n border-bottom: 1px solid #d9f7be;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #52c41a;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #52c41a;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.copy-users-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n.copy-user-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #f6ffed;\r\n color: #52c41a;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n border: 1px solid #b7eb8f;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #52c41a, #389e0d);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Description: ๆกไปถ่็น็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"condition-node\">\r\n <div class=\"status-indicator\" :class=\"data.status\"></div>\r\n\r\n <!-- ๅ ้คๆ้ฎ -->\r\n <div class=\"delete-btn\" @click=\"handleDelete\" title=\"ๅ ้ค่็น\">\r\n <C_Icon name=\"mdi:close\" :size=\"12\" />\r\n </div>\r\n\r\n <div class=\"node-card\">\r\n <div class=\"node-header\">\r\n <div class=\"node-icon\">๐</div>\r\n <span class=\"node-title\">{{ data.title }}</span>\r\n <div class=\"node-badge\" v-if=\"conditionCount > 0\">\r\n {{ conditionCount }}\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <div v-if=\"conditionCount > 0\" class=\"conditions-list\">\r\n <div\r\n v-for=\"condition in displayConditions\"\r\n :key=\"condition.id\"\r\n class=\"condition-tag\"\r\n >\r\n <span class=\"condition-icon\">โ</span>\r\n <span>{{\r\n condition.name ||\r\n \"ๆกไปถ\" + (displayConditions.indexOf(condition) + 1)\r\n }}</span>\r\n </div>\r\n <div v-if=\"moreCount > 0\" class=\"more-count\">+{{ moreCount }}</div>\r\n </div>\r\n <div v-else class=\"placeholder\">่ฏท่ฎพ็ฝฎๅๆฏๆกไปถ</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"add-node-btn\" @click=\"showAddMenu\" title=\"ๆทปๅ ไธไธไธช่็น\">\r\n <C_Icon name=\"mdi:plus\" :size=\"16\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject } from \"vue\";\r\nimport { C_Icon } from \"../../C_Icon\";\r\n\r\n/* ๅฎไนๆกไปถ็ฑปๅ */\r\ninterface Condition {\r\n id: string;\r\n name: string;\r\n operator: \"equals\" | \"not_equals\" | \"greater_than\" | \"less_than\" | \"contains\";\r\n value: string;\r\n}\r\n\r\ninterface Props {\r\n id: string;\r\n data: {\r\n title: string;\r\n conditions?: Condition[];\r\n status?: string;\r\n };\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst showAddMenuFn = inject(\"showAddMenu\") as\r\n | ((position: { x: number; y: number }, nodeId?: string) => void)\r\n | undefined;\r\n\r\nconst deleteNodeFn = inject(\"deleteNode\") as\r\n | ((nodeId: string) => void)\r\n | undefined;\r\n\r\n/* ไฝฟ็จๅฎๅ
จ็่ฎฟ้ฎๆนๅผๅ้ป่ฎคๅผ */\r\nconst conditions = computed(() => props.data.conditions ?? []);\r\nconst conditionCount = computed(() => conditions.value.length);\r\nconst displayConditions = computed(() => conditions.value.slice(0, 2));\r\nconst moreCount = computed(() => Math.max(0, conditionCount.value - 2));\r\n\r\nconst showAddMenu = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\r\n\r\n if (showAddMenuFn) {\r\n showAddMenuFn(\r\n {\r\n x: rect.left + rect.width / 2,\r\n y: rect.bottom + 10,\r\n },\r\n props.id,\r\n );\r\n }\r\n};\r\n\r\nconst handleDelete = (event: MouseEvent) => {\r\n event.stopPropagation();\r\n if (deleteNodeFn) {\r\n deleteNodeFn(props.id);\r\n }\r\n};\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.condition-node {\r\n position: relative;\r\n}\r\n\r\n.status-indicator {\r\n position: absolute;\r\n top: -4px;\r\n right: -4px;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n border: 2px solid white;\r\n z-index: 2;\r\n background: #faad14;\r\n\r\n &.completed {\r\n background: #52c41a;\r\n }\r\n &.active {\r\n background: #fa8c16;\r\n }\r\n}\r\n\r\n.delete-btn {\r\n position: absolute;\r\n top: -10px;\r\n right: -10px;\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #ff4d4f;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n z-index: 100;\r\n opacity: 0;\r\n transform: scale(0.8);\r\n transition: all 0.2s ease;\r\n border: 2px solid white;\r\n box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);\r\n\r\n &:hover {\r\n transform: scale(1);\r\n background: #ff7875;\r\n box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);\r\n }\r\n\r\n &:active {\r\n transform: scale(0.95);\r\n }\r\n}\r\n\r\n.condition-node:hover .delete-btn {\r\n opacity: 1;\r\n transform: scale(1);\r\n}\r\n\r\n.node-card {\r\n background: white;\r\n border-radius: 12px;\r\n min-width: 220px;\r\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n border: 2px solid transparent;\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n overflow: hidden;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n transform: translateY(-2px);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\r\n border-color: #fa8c16;\r\n }\r\n}\r\n\r\n.node-header {\r\n padding: 12px 16px;\r\n background: #fff7e6;\r\n border-bottom: 1px solid #ffe7ba;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.node-icon {\r\n width: 24px;\r\n height: 24px;\r\n border-radius: 50%;\r\n background: #fa8c16;\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 12px;\r\n}\r\n\r\n.node-title {\r\n font-weight: 600;\r\n color: #262626;\r\n font-size: 14px;\r\n flex: 1;\r\n}\r\n\r\n.node-badge {\r\n background: #fa8c16;\r\n color: white;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n font-size: 11px;\r\n font-weight: 600;\r\n min-width: 16px;\r\n text-align: center;\r\n}\r\n\r\n.node-content {\r\n padding: 16px;\r\n}\r\n\r\n.conditions-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.condition-tag {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n background: #fff7e6;\r\n color: #fa8c16;\r\n padding: 6px 10px;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n border: 1px solid #ffd591;\r\n transition: all 0.2s ease;\r\n\r\n &:hover {\r\n background: #ffe7ba;\r\n transform: translateX(2px);\r\n }\r\n}\r\n\r\n.condition-icon {\r\n font-weight: bold;\r\n color: #d48806;\r\n}\r\n\r\n.more-count {\r\n background: #f0f0f0;\r\n color: #8c8c8c;\r\n padding: 4px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-align: center;\r\n margin-top: 4px;\r\n}\r\n\r\n.placeholder {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #8c8c8c;\r\n font-size: 12px;\r\n padding: 8px 0;\r\n}\r\n\r\n.add-node-btn {\r\n position: absolute;\r\n left: 50%;\r\n bottom: -20px;\r\n transform: translateX(-50%);\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 50%;\r\n background: linear-gradient(135deg, #fa8c16, #d48806);\r\n color: white;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);\r\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n font-size: 16px;\r\n font-weight: bold;\r\n z-index: 10;\r\n\r\n &:hover {\r\n transform: translateX(-50%) scale(1.1);\r\n box-shadow: 0 6px 20px rgba(250, 140, 22, 0.4);\r\n }\r\n}\r\n</style>\r\n","/**\r\n * ๅทฅไฝๆต่็น็ฎก็ Composable\r\n * ๅฐ่ฃ
่็น CRUDใprovide/injectใ่พน้่ฟใ็ปๅธๆไฝ็ญ้ป่พ\r\n */\r\n\r\nimport {\r\n ref,\r\n computed,\r\n provide,\r\n watch,\r\n onMounted,\r\n nextTick,\r\n markRaw,\r\n} from \"vue\";\r\nimport type { Component, Ref } from \"vue\";\r\nimport { useMessage } from \"naive-ui\";\r\nimport type {\r\n WorkflowNode,\r\n WorkflowEdge,\r\n WorkflowData,\r\n WorkflowProps,\r\n NodeType,\r\n MenuPosition,\r\n} from \"../types\";\r\nimport { NODE_TITLES, INITIAL_NODE, NODE_Y_GAP, generateEdgeId } from \"../data\";\r\n\r\nimport StartNode from \"../nodes/StartNode.vue\";\r\nimport ApprovalNode from \"../nodes/ApprovalNode.vue\";\r\nimport CopyNode from \"../nodes/CopyNode.vue\";\r\nimport ConditionNode from \"../nodes/ConditionNode.vue\";\r\n\r\n/** ่็น็ปไปถๆ ๅฐ๏ผๆจกๅ็บงๅธธ้๏ผไป
ๅๅงๅไธๆฌก๏ผ */\r\nconst NODE_COMPONENT_MAP: Record<string, Component> = {\r\n start: markRaw(StartNode),\r\n approval: markRaw(ApprovalNode),\r\n copy: markRaw(CopyNode),\r\n condition: markRaw(ConditionNode),\r\n};\r\n\r\ntype EmitFn = (event: string, ...args: any[]) => void;\r\n\r\n/** ๅทฅไฝๆต่็น็ฎก็ โโ ๅฐ่ฃ
่็น CRUDใprovide/injectใ่พน้่ฟใ็ปๅธๆไฝ็ญ้ป่พ */\r\nexport function useWorkflowNodes(\r\n props: WorkflowProps,\r\n emit: EmitFn,\r\n vueFlowRef: Ref,\r\n) {\r\n const message = useMessage();\r\n\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const nodes = ref<WorkflowNode[]>([{ ...INITIAL_NODE }]);\r\n const edges = ref<WorkflowEdge[]>([]);\r\n const showAddMenu = ref(false);\r\n const menuPosition = ref<MenuPosition>({ x: 0, y: 0 });\r\n const showNodeConfig = ref(false);\r\n const currentNode = ref<WorkflowNode | null>(null);\r\n const currentAddNodeId = ref<string | null>(null);\r\n\r\n /* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const nodeTypes = computed(() => NODE_COMPONENT_MAP);\r\n\r\n const workflowStats = computed(() => {\r\n const stats = {\r\n totalNodes: nodes.value.length,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n };\r\n nodes.value.forEach((n) => {\r\n if (n.type === \"approval\") stats.approvalNodes++;\r\n else if (n.type === \"copy\") stats.copyNodes++;\r\n else if (n.type === \"condition\") stats.conditionNodes++;\r\n });\r\n return stats;\r\n });\r\n\r\n /* โโโ ๆฐๆฎๆไฝ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ่ทๅๅฝๅๅทฅไฝๆตๅฎๆดๆฐๆฎ */\r\n const getCurrentWorkflowData = (): WorkflowData => ({\r\n nodes: nodes.value,\r\n edges: edges.value,\r\n config: { version: \"1.0\", createdAt: new Date().toISOString() },\r\n });\r\n\r\n /** ่งฆๅๆฐๆฎๅๆดไบไปถ */\r\n const emitChange = (): void => {\r\n const data = getCurrentWorkflowData();\r\n emit(\"update:modelValue\", data);\r\n emit(\"change\", data);\r\n };\r\n\r\n /** ๅปถ่ฟ้ๅบ็ปๅธ่งๅพ */\r\n const deferFitView = (padding = 60, duration = 400): void => {\r\n nextTick(() => {\r\n setTimeout(() => {\r\n vueFlowRef.value?.fitView?.({ padding, duration });\r\n }, 100);\r\n });\r\n };\r\n\r\n /* โโโ ๆทปๅ ่ๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const handleShowAddMenu = (position: MenuPosition, nodeId?: string): void => {\r\n menuPosition.value = {\r\n x: typeof position.x === \"number\" ? position.x : 0,\r\n y: typeof position.y === \"number\" ? position.y : 0,\r\n };\r\n currentAddNodeId.value = nodeId || null;\r\n showAddMenu.value = true;\r\n };\r\n\r\n const closeAddMenu = (): void => {\r\n showAddMenu.value = false;\r\n };\r\n\r\n /* โโโ ่็นๅ ้ค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const deleteNode = (nodeId: string): void => {\r\n if (nodeId === \"start-1\") {\r\n message.warning(\"ไธ่ฝๅ ้คๅผๅง่็น\");\r\n return;\r\n }\r\n\r\n const nodeIndex = nodes.value.findIndex((n) => n.id === nodeId);\r\n if (nodeIndex === -1) return;\r\n\r\n /* ๆถ้ไธไธๆธธ่พน๏ผ็จไบ้่ฟ */\r\n const incomingEdges = edges.value.filter((e) => e.target === nodeId);\r\n const outgoingEdges = edges.value.filter((e) => e.source === nodeId);\r\n\r\n /* ็งป้ค่็นๅๅ
ณ่่พน */\r\n nodes.value.splice(nodeIndex, 1);\r\n edges.value = edges.value.filter(\r\n (e) => e.source !== nodeId && e.target !== nodeId,\r\n );\r\n\r\n /* ้่ฟไธไธๆธธ */\r\n incomingEdges.forEach((inEdge) => {\r\n outgoingEdges.forEach((outEdge) => {\r\n edges.value.push({\r\n id: generateEdgeId(inEdge.source, outEdge.target),\r\n source: inEdge.source,\r\n target: outEdge.target,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n });\r\n });\r\n\r\n /* ไธ็งปๅ็ปญ่็นไฝ็ฝฎ */\r\n nodes.value.forEach((node, i) => {\r\n if (i >= nodeIndex) node.position.y -= NODE_Y_GAP;\r\n });\r\n\r\n emitChange();\r\n message.success(\"่็นๅทฒๅ ้ค\");\r\n deferFitView();\r\n };\r\n\r\n /* โโโ provide ๆณจๅ
ฅ็ปๅญ่็น็ปไปถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n provide(\"showAddMenu\", handleShowAddMenu);\r\n provide(\"deleteNode\", deleteNode);\r\n\r\n /* โโโ ่็นๆทปๅ ๏ผๅ
้จ่พ
ๅฉ๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const getTargetNodeInfo = () => {\r\n let targetNodeIndex = nodes.value.length - 1;\r\n let targetNode = nodes.value[targetNodeIndex];\r\n\r\n if (currentAddNodeId.value) {\r\n const foundIndex = nodes.value.findIndex(\r\n (n) => n.id === currentAddNodeId.value,\r\n );\r\n if (foundIndex !== -1) {\r\n targetNodeIndex = foundIndex;\r\n targetNode = nodes.value[targetNodeIndex];\r\n }\r\n }\r\n return { targetNodeIndex, targetNode };\r\n };\r\n\r\n const createNewNode = (\r\n type: NodeType,\r\n targetNode: WorkflowNode | null,\r\n ): WorkflowNode => ({\r\n id: `${type}-${Date.now()}`,\r\n type,\r\n position: {\r\n x: targetNode?.position.x || 150,\r\n y: (targetNode?.position.y || 130) + NODE_Y_GAP,\r\n },\r\n data: {\r\n title: NODE_TITLES[type],\r\n status: \"pending\",\r\n ...(type === \"approval\" && { approvers: [], approvalMode: \"any\" }),\r\n ...(type === \"copy\" && { copyUsers: [] }),\r\n ...(type === \"condition\" && { conditions: [] }),\r\n },\r\n });\r\n\r\n /** ๅฐๆฐ่็นๆๅ
ฅๅฐ targetNode ไนๅ๏ผ้่ฟๆๆ่พน */\r\n const reconnectEdges = (\r\n targetNode: WorkflowNode,\r\n newNode: WorkflowNode,\r\n ): void => {\r\n const outgoing = edges.value.filter((e) => e.source === targetNode.id);\r\n edges.value = edges.value.filter((e) => e.source !== targetNode.id);\r\n\r\n /* ๅ่็น โ ๆฐ่็น */\r\n edges.value.push({\r\n id: generateEdgeId(targetNode.id, newNode.id),\r\n source: targetNode.id,\r\n target: newNode.id,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n\r\n /* ๆฐ่็น โ ๅไธๆธธ */\r\n outgoing.forEach((edge) => {\r\n edges.value.push({\r\n id: generateEdgeId(newNode.id, edge.target),\r\n source: newNode.id,\r\n target: edge.target,\r\n animated: true,\r\n type: \"default\",\r\n });\r\n });\r\n };\r\n\r\n /* โโโ ่็นๆทปๅ ๏ผๅ
ฌๅผ๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const addNode = (type: NodeType): void => {\r\n try {\r\n const { targetNodeIndex, targetNode } = getTargetNodeInfo();\r\n const newNode = createNewNode(type, targetNode);\r\n\r\n nodes.value.splice(targetNodeIndex + 1, 0, newNode);\r\n\r\n /* ไธๆจๅ็ปญ่็น */\r\n for (let i = targetNodeIndex + 2; i < nodes.value.length; i++) {\r\n nodes.value[i].position.y += NODE_Y_GAP;\r\n }\r\n\r\n if (targetNode) reconnectEdges(targetNode, newNode);\r\n\r\n showAddMenu.value = false;\r\n currentAddNodeId.value = null;\r\n emitChange();\r\n deferFitView();\r\n } catch (error) {\r\n console.error(\"Error adding node:\", error);\r\n message.error(\"ๆทปๅ ่็นๅคฑ่ดฅ๏ผ่ฏท้่ฏ\");\r\n }\r\n };\r\n\r\n /* โโโ ่็นไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const onNodeClick = (event: { node: WorkflowNode }): void => {\r\n const { node } = event;\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n emit(\"node-click\", node);\r\n };\r\n\r\n const handleConfigSave = (configData: Record<string, unknown>): void => {\r\n if (!currentNode.value) return;\r\n\r\n const nodeIndex = nodes.value.findIndex(\r\n (n) => n.id === currentNode.value!.id,\r\n );\r\n if (nodeIndex !== -1) {\r\n const updatedNode = {\r\n ...nodes.value[nodeIndex],\r\n data: { ...nodes.value[nodeIndex].data, ...configData },\r\n };\r\n nodes.value.splice(nodeIndex, 1, updatedNode);\r\n currentNode.value = updatedNode;\r\n }\r\n\r\n emitChange();\r\n showNodeConfig.value = false;\r\n message.success(\"่็น้
็ฝฎๅทฒไฟๅญ\");\r\n };\r\n\r\n /* โโโ ็ปๅธๆไฝ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const fitView = (): void => {\r\n if (!vueFlowRef.value?.fitView) {\r\n message.warning(\"็ปๅธๆชๅๅคๅฐฑ็ปช๏ผ่ฏท็จๅ้่ฏ\");\r\n return;\r\n }\r\n nextTick(() => {\r\n vueFlowRef.value.fitView({\r\n padding: 50,\r\n includeHiddenNodes: false,\r\n minZoom: 0.5,\r\n maxZoom: 1.5,\r\n duration: 800,\r\n });\r\n });\r\n message.success(\"ๅทฒ้ๅบ็ชๅฃๅคงๅฐ\");\r\n };\r\n\r\n /** ้็ฝฎ่็นๅ่พน๏ผไธๆถๅ้ช่ฏ็ถๆ๏ผ */\r\n const resetNodes = (): void => {\r\n nodes.value = [{ ...INITIAL_NODE }];\r\n edges.value = [];\r\n emitChange();\r\n deferFitView(80, 600);\r\n message.success(\"็ปๅธๅทฒๆธ
็ฉบ\");\r\n };\r\n\r\n /* โโโ ็ๅฝๅจๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n watch(\r\n () => props.modelValue,\r\n (newValue) => {\r\n if (newValue && newValue !== getCurrentWorkflowData()) {\r\n nodes.value = newValue.nodes || [];\r\n edges.value = newValue.edges || [];\r\n }\r\n },\r\n { deep: true },\r\n );\r\n\r\n onMounted(() => {\r\n nextTick(() => {\r\n setTimeout(() => {\r\n vueFlowRef.value?.fitView?.({\r\n padding: 80,\r\n includeHiddenNodes: false,\r\n minZoom: 0.8,\r\n maxZoom: 1.2,\r\n duration: 600,\r\n });\r\n }, 300);\r\n });\r\n });\r\n\r\n return {\r\n /* ็ถๆ */\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n /* ่ฎก็ฎๅฑๆง */\r\n nodeTypes,\r\n workflowStats,\r\n /* ๆนๆณ */\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n };\r\n}\r\n","/**\r\n * ๅทฅไฝๆต้ช่ฏ Composable\r\n * ๅฐ่ฃ
ๆต็จๆ ก้ช้ป่พใ้่ฏฏๅฑ็คบๅ่็นๅฎไฝ\r\n */\r\n\r\nimport { ref } from \"vue\";\r\nimport type { Ref } from \"vue\";\r\nimport { useMessage } from \"naive-ui\";\r\nimport type { WorkflowNode, WorkflowEdge, ValidationError } from \"../types\";\r\nimport { FIELD_DISPLAY_NAMES, ERROR_TYPE_TEXTS } from \"../data\";\r\n\r\nexport interface WorkflowValidationOptions {\r\n /** ๅฎไฝ้่ฏฏ่็นๅๆๅผ้
็ฝฎๅผน็ช็ๅ่ฐ */\r\n onShowNodeConfig?: (node: WorkflowNode) => void;\r\n /** ้ช่ฏๅคฑ่ดฅๆถ็ๅค้จๅ่ฐ๏ผ้ๅธธ็จไบ emit๏ผ */\r\n onValidateError?: (errors: ValidationError[]) => void;\r\n}\r\n\r\n/** ๅทฅไฝๆต้ช่ฏ โโ ๅฐ่ฃ
ๆต็จๆ ก้ช้ป่พใ้่ฏฏๅฑ็คบๅ่็นๅฎไฝ */\r\nexport function useWorkflowValidation(\r\n nodes: Ref<WorkflowNode[]>,\r\n edges: Ref<WorkflowEdge[]>,\r\n vueFlowRef: Ref,\r\n options?: WorkflowValidationOptions,\r\n) {\r\n const message = useMessage();\r\n\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const validationErrors = ref<ValidationError[]>([]);\r\n const showValidationErrors = ref(false);\r\n\r\n /* โโโ ๆ ธๅฟ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆง่กๅทฅไฝๆต้ช่ฏ๏ผ่ฟๅ้่ฏฏๅ่กจ๏ผ็บฏ้ป่พ๏ผไธๆไฝ UI๏ผ */\r\n const validateWorkflow = (): ValidationError[] => {\r\n const errors: ValidationError[] = [];\r\n\r\n nodes.value.forEach((node) => {\r\n /* ๅฎกๆน่็น๏ผๅฟ
้กปๆๅฎกๆนไบบ */\r\n if (node.type === \"approval\") {\r\n const approvers = (node.data as any).approvers || [];\r\n if (approvers.length === 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"approvers\",\r\n message: \"ๅฎกๆน่็นๅฟ
้กป่ฎพ็ฝฎ่ณๅฐไธไธชๅฎกๆนไบบ๏ผๅฆๅๆต็จๆ ๆณๆญฃๅธธ่ฟ่ก\",\r\n type: \"required\",\r\n });\r\n }\r\n }\r\n\r\n /* ๆกไปถ่็น๏ผๅฟ
้กปๆๅๆฏไธ้
็ฝฎๅฎๆด */\r\n if (node.type === \"condition\") {\r\n const conditions = (node.data as any).conditions || [];\r\n if (conditions.length === 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"conditions\",\r\n message:\r\n \"ๆกไปถๅๆฏ่็นๅฟ
้กป้
็ฝฎ่ณๅฐไธไธชๅๆฏๆกไปถ๏ผๅฆๅๆ ๆณ่ฟ่กๆกไปถๅคๆญ\",\r\n type: \"required\",\r\n });\r\n } else {\r\n const incomplete = conditions.filter(\r\n (c: any) => !c.name || !c.field || !c.operator || !c.value,\r\n );\r\n if (incomplete.length > 0) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"conditions\",\r\n message: `ๆ ${incomplete.length} ไธชๆกไปถๅๆฏ้
็ฝฎไธๅฎๆด๏ผ่ฏทๅฎๅๆๆๅฟ
ๅกซๅญๆฎต`,\r\n type: \"incomplete\",\r\n });\r\n }\r\n }\r\n }\r\n });\r\n\r\n /* ๆฃๆฅๆญ่ฟ่็น */\r\n const connectedNodes = new Set<string>();\r\n edges.value.forEach((edge) => {\r\n connectedNodes.add(edge.source);\r\n connectedNodes.add(edge.target);\r\n });\r\n\r\n nodes.value.forEach((node) => {\r\n if (node.type !== \"start\" && !connectedNodes.has(node.id)) {\r\n errors.push({\r\n nodeId: node.id,\r\n nodeName: node.data.title,\r\n field: \"connection\",\r\n message: \"ๆญค่็นๆชไธๅ
ถไป่็น่ฟๆฅ๏ผๅฏ่ฝๅฏผ่ดๆต็จไธญๆญ\",\r\n type: \"warning\",\r\n });\r\n }\r\n });\r\n\r\n return errors;\r\n };\r\n\r\n /* โโโ UI ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆง่ก้ช่ฏๅนถๆดๆฐ UI ็ถๆ๏ผๆถๆฏๆ็คบ + ๆฝๅฑ๏ผ */\r\n const validateCurrentWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n validationErrors.value = errors;\r\n\r\n if (errors.length === 0) {\r\n message.success(\"ๅทฅไฝๆต้ช่ฏ้่ฟ๏ผๆๆ่็น้
็ฝฎๆญฃ็กฎ\");\r\n showValidationErrors.value = false;\r\n } else {\r\n message.error(`ๅ็ฐ ${errors.length} ไธช้ฎ้ข๏ผ่ฏทๆฅ็่ฏฆ็ป้่ฏฏ`);\r\n showValidationErrors.value = true;\r\n options?.onValidateError?.(errors);\r\n }\r\n };\r\n\r\n /** ๅฎไฝๅฐ้่ฏฏ่็นๅนถๆๅผ้
็ฝฎๅผน็ช */\r\n const jumpToErrorNode = (nodeId: string): void => {\r\n const node = nodes.value.find((n) => n.id === nodeId);\r\n if (!node || !vueFlowRef.value) return;\r\n\r\n vueFlowRef.value.setCenter(node.position.x, node.position.y, {\r\n zoom: 1.2,\r\n duration: 800,\r\n });\r\n\r\n setTimeout(() => {\r\n options?.onShowNodeConfig?.(node);\r\n showValidationErrors.value = false;\r\n }, 900);\r\n\r\n message.info(`ๅทฒๅฎไฝๅฐ่็น๏ผ${node.data.title}`);\r\n };\r\n\r\n /* โโโ ่พ
ๅฉๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const getFieldDisplayName = (field: string): string =>\r\n FIELD_DISPLAY_NAMES[field] || field;\r\n\r\n const getErrorTypeText = (type: string): string =>\r\n ERROR_TYPE_TEXTS[type] || type;\r\n\r\n /** ้็ฝฎ้ช่ฏ็ถๆ */\r\n const resetValidation = (): void => {\r\n validationErrors.value = [];\r\n showValidationErrors.value = false;\r\n };\r\n\r\n return {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n };\r\n}\r\n","/**\r\n * ๅทฅไฝๆต้ข่ง Composable\r\n * ๅฐ่ฃ
ๆต็จ้ข่ง้ป่พใ่็นๆๅบๅๆญฅ้ชคๅฑ็คบ\r\n */\r\n\r\nimport { ref } from \"vue\";\r\nimport type { Ref } from \"vue\";\r\nimport type { WorkflowNode, WorkflowEdge } from \"../types\";\r\n\r\n/** ่ฏฆๆ
้กน็ฑปๅ */\r\nexport type DetailItemType = \"text\" | \"mode\" | \"users\" | \"warning\";\r\n\r\n/** ็ปๆๅ่ฏฆๆ
้กน */\r\nexport interface DetailItem {\r\n type: DetailItemType;\r\n label: string;\r\n value?: string;\r\n users?: { name: string; department?: string }[];\r\n modeKey?: string;\r\n}\r\n\r\n/** ๆต็จๆญฅ้ชค๏ผๅทฒๆๅบ๏ผ */\r\nexport interface FlowStep {\r\n order: number;\r\n node: WorkflowNode;\r\n nodeTypeLabel: string;\r\n icon: string;\r\n colorClass: string;\r\n details: DetailItem[];\r\n children?: FlowStep[];\r\n}\r\n\r\n/** ้ข่ง็ป่ฎกไฟกๆฏ */\r\nexport interface PreviewStats {\r\n totalNodes: number;\r\n approvalNodes: number;\r\n copyNodes: number;\r\n conditionNodes: number;\r\n totalEdges: number;\r\n}\r\n\r\nconst NODE_TYPE_META: Record<\r\n string,\r\n { label: string; icon: string; colorClass: string }\r\n> = {\r\n start: { label: \"ๅ่ตทไบบ\", icon: \"mdi:play-circle\", colorClass: \"start\" },\r\n approval: {\r\n label: \"ๅฎกๆน่็น\",\r\n icon: \"mdi:account-check\",\r\n colorClass: \"approval\",\r\n },\r\n copy: { label: \"ๆ้่็น\", icon: \"mdi:email-outline\", colorClass: \"copy\" },\r\n condition: {\r\n label: \"ๆกไปถๅๆฏ\",\r\n icon: \"mdi:source-branch\",\r\n colorClass: \"condition\",\r\n },\r\n};\r\n\r\nconst APPROVAL_MODE_LABELS: Record<string, string> = {\r\n any: \"ๆ็ญพ๏ผไปปไธๅฎกๆนไบบ้่ฟๅณๅฏ๏ผ\",\r\n all: \"ไผ็ญพ๏ผๆๆๅฎกๆนไบบๅฟ
้กป้่ฟ๏ผ\",\r\n sequence: \"้กบๅบๅฎกๆน๏ผๆ้กบๅบไพๆฌกๅฎกๆน๏ผ\",\r\n};\r\n\r\nconst OPERATOR_LABELS: Record<string, string> = {\r\n equals: \"็ญไบ\",\r\n not_equals: \"ไธ็ญไบ\",\r\n greater_than: \"ๅคงไบ\",\r\n less_than: \"ๅฐไบ\",\r\n contains: \"ๅ
ๅซ\",\r\n};\r\n\r\n/** ๅทฅไฝๆต้ข่ง โโ ๅฐ่ฃ
ๆต็จ้ข่ง้ป่พใ่็นๆๅบๅๆญฅ้ชคๅฑ็คบ */\r\nexport function useWorkflowPreview(\r\n nodes: Ref<WorkflowNode[]>,\r\n edges: Ref<WorkflowEdge[]>,\r\n) {\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const showPreview = ref(false);\r\n const previewSteps = ref<FlowStep[]>([]);\r\n const previewStats = ref<PreviewStats>({\r\n totalNodes: 0,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n totalEdges: 0,\r\n });\r\n\r\n /* โโโ ่็น่ฏฆๆ
ๆๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅๅผๅง่็น่ฏฆๆ
*/\r\n const extractStartDetails = (data: any): DetailItem[] => {\r\n const initiators = data.initiators || [];\r\n return initiators.length > 0\r\n ? [{ type: \"users\", label: \"ๅ่ตทไบบ\", users: initiators }]\r\n : [{ type: \"text\", label: \"ๅ่ตทไบบ\", value: \"ๆๆไบบ\" }];\r\n };\r\n\r\n /** ๆๅๅฎกๆน่็น่ฏฆๆ
*/\r\n const extractApprovalDetails = (data: any): DetailItem[] => {\r\n const mode = data.approvalMode || \"any\";\r\n const approvers = data.approvers || [];\r\n const items: DetailItem[] = [\r\n {\r\n type: \"mode\",\r\n label: \"ๅฎกๆนๆนๅผ\",\r\n value: APPROVAL_MODE_LABELS[mode] || mode,\r\n modeKey: mode,\r\n },\r\n ];\r\n if (approvers.length > 0) {\r\n items.push({ type: \"users\", label: \"ๅฎกๆนไบบ\", users: approvers });\r\n } else {\r\n items.push({ type: \"warning\", label: \"ๅฎกๆนไบบ\", value: \"ๆช่ฎพ็ฝฎ\" });\r\n }\r\n return items;\r\n };\r\n\r\n /** ๆๅๆ้่็น่ฏฆๆ
*/\r\n const extractCopyDetails = (data: any): DetailItem[] => {\r\n const copyUsers = data.copyUsers || [];\r\n return copyUsers.length > 0\r\n ? [{ type: \"users\", label: \"ๆ้ไบบ\", users: copyUsers }]\r\n : [{ type: \"text\", label: \"ๆ้ไบบ\", value: \"ๆช่ฎพ็ฝฎ\" }];\r\n };\r\n\r\n /** ๆๅๆกไปถ่็น่ฏฆๆ
*/\r\n const extractConditionDetails = (data: any): DetailItem[] => {\r\n const conditions = data.conditions || [];\r\n if (conditions.length === 0) {\r\n return [{ type: \"warning\", label: \"ๆกไปถๅๆฏ\", value: \"ๆช้
็ฝฎ\" }];\r\n }\r\n return conditions.map((cond: any, i: number) => {\r\n const op = OPERATOR_LABELS[cond.operator] || cond.operator;\r\n return {\r\n type: \"text\" as const,\r\n label: `ๅๆฏ ${i + 1}`,\r\n value: `${cond.name || \"ๆชๅฝๅ\"} โ ${cond.field || \"?\"} ${op} ${cond.value || \"?\"}`,\r\n };\r\n });\r\n };\r\n\r\n /** ่็น่ฏฆๆ
ๆๅ็ญ็ฅ */\r\n const detailExtractors: Record<string, (data: any) => DetailItem[]> = {\r\n start: extractStartDetails,\r\n approval: extractApprovalDetails,\r\n copy: extractCopyDetails,\r\n condition: extractConditionDetails,\r\n };\r\n\r\n /** ไป่็นๆฐๆฎไธญๆๅๅฏ่ฏป็้
็ฝฎ่ฏฆๆ
*/\r\n const extractNodeDetails = (node: WorkflowNode): DetailItem[] => {\r\n const extractor = detailExtractors[node.type || \"start\"];\r\n return extractor ? extractor(node.data) : [];\r\n };\r\n\r\n /* โโโ ๆๆๆๅบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๅบไบ BFS ๅฏนๆต็จ่็น่ฟ่กๆๆๆๅบ */\r\n const sortNodesByFlow = (): WorkflowNode[] => {\r\n const nodeMap = new Map<string, WorkflowNode>();\r\n nodes.value.forEach((n) => nodeMap.set(n.id, n));\r\n\r\n const adjacency = new Map<string, string[]>();\r\n const inDegree = new Map<string, number>();\r\n nodes.value.forEach((n) => {\r\n adjacency.set(n.id, []);\r\n inDegree.set(n.id, 0);\r\n });\r\n\r\n edges.value.forEach((e) => {\r\n adjacency.get(e.source)?.push(e.target);\r\n inDegree.set(e.target, (inDegree.get(e.target) || 0) + 1);\r\n });\r\n\r\n /* ไปๅ
ฅๅบฆไธบ 0 ็่็น๏ผstart๏ผๅผๅง BFS */\r\n const queue: string[] = [];\r\n inDegree.forEach((deg, id) => {\r\n if (deg === 0) queue.push(id);\r\n });\r\n\r\n const sorted: WorkflowNode[] = [];\r\n const visited = new Set<string>();\r\n\r\n while (queue.length > 0) {\r\n const id = queue.shift()!;\r\n if (visited.has(id)) continue;\r\n visited.add(id);\r\n\r\n const node = nodeMap.get(id);\r\n if (node) sorted.push(node);\r\n\r\n const neighbors = adjacency.get(id) || [];\r\n for (const next of neighbors) {\r\n const newDeg = (inDegree.get(next) || 1) - 1;\r\n inDegree.set(next, newDeg);\r\n if (newDeg <= 0 && !visited.has(next)) {\r\n queue.push(next);\r\n }\r\n }\r\n }\r\n\r\n /* ่ฟฝๅ ๆช่ฟๆฅ็ๅญค็ซ่็น */\r\n nodes.value.forEach((n) => {\r\n if (!visited.has(n.id)) sorted.push(n);\r\n });\r\n\r\n return sorted;\r\n };\r\n\r\n /* โโโ ็ๆๆต็จๆญฅ้ชค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅปบ้ข่งๆญฅ้ชคๅ่กจ */\r\n const buildFlowSteps = (): FlowStep[] => {\r\n const sorted = sortNodesByFlow();\r\n return sorted.map((node, index) => {\r\n const meta = NODE_TYPE_META[node.type || \"start\"] || NODE_TYPE_META.start;\r\n return {\r\n order: index + 1,\r\n node,\r\n nodeTypeLabel: meta.label,\r\n icon: meta.icon,\r\n colorClass: meta.colorClass,\r\n details: extractNodeDetails(node),\r\n };\r\n });\r\n };\r\n\r\n /* โโโ ็ป่ฎกไฟกๆฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n const computeStats = (): PreviewStats => {\r\n const stats: PreviewStats = {\r\n totalNodes: nodes.value.length,\r\n approvalNodes: 0,\r\n copyNodes: 0,\r\n conditionNodes: 0,\r\n totalEdges: edges.value.length,\r\n };\r\n nodes.value.forEach((n) => {\r\n if (n.type === \"approval\") stats.approvalNodes++;\r\n else if (n.type === \"copy\") stats.copyNodes++;\r\n else if (n.type === \"condition\") stats.conditionNodes++;\r\n });\r\n return stats;\r\n };\r\n\r\n /* โโโ ๅ
ฌๅผๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n /** ๆๅผ้ข่ง้ขๆฟ */\r\n const openPreview = (): void => {\r\n previewSteps.value = buildFlowSteps();\r\n previewStats.value = computeStats();\r\n showPreview.value = true;\r\n };\r\n\r\n /** ๅ
ณ้ญ้ข่ง้ขๆฟ */\r\n const closePreview = (): void => {\r\n showPreview.value = false;\r\n };\r\n\r\n return {\r\n showPreview,\r\n previewSteps,\r\n previewStats,\r\n openPreview,\r\n closePreview,\r\n };\r\n}\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-08-01\r\n * @Description: ๅทฅไฝๆต/ๅฎกๆนๆต็ปไปถ๏ผๅบไบ Vue Flow๏ผ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"approval-workflow-container\">\r\n <!-- ๆตฎๅจๅทฅๅ
ทๆ -->\r\n <div class=\"floating-toolbar\">\r\n <NButton size=\"small\" type=\"primary\" @click=\"saveWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ไฟๅญ\r\n </NButton>\r\n <NButton size=\"small\" @click=\"previewWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:eye\" :size=\"16\" />\r\n </template>\r\n ้ข่ง\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n @click=\"validateCurrentWorkflow\"\r\n title=\"ๆฃๆฅๅทฅไฝๆต้
็ฝฎๆฏๅฆๆญฃ็กฎ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-circle\" :size=\"16\" />\r\n </template>\r\n ้ช่ฏๆต็จ\r\n </NButton>\r\n <div class=\"toolbar-divider\"></div>\r\n <NButton size=\"small\" @click=\"fitView\" title=\"้ๅบ็ชๅฃ\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:fit-to-screen\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n <NButton\r\n size=\"small\"\r\n type=\"error\"\r\n @click=\"clearWorkflow\"\r\n title=\"ๆธ
็ฉบ็ปๅธ\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n\r\n <!-- Vue Flow ็ปๅธ -->\r\n <VueFlow\r\n ref=\"vueFlowRef\"\r\n v-model:nodes=\"nodes\"\r\n v-model:edges=\"edges\"\r\n :node-types=\"nodeTypes\"\r\n class=\"workflow-canvas\"\r\n :default-viewport=\"{ zoom: 1, x: 0, y: 0 }\"\r\n :min-zoom=\"0.5\"\r\n :max-zoom=\"2\"\r\n :fit-view-on-init=\"true\"\r\n :nodes-draggable=\"true\"\r\n :elements-selectable=\"true\"\r\n @node-click=\"onNodeClick\"\r\n @pane-click=\"closeAddMenu\"\r\n />\r\n\r\n <!-- ่็นๆทปๅ ่ๅ -->\r\n <Teleport to=\"body\">\r\n <div\r\n v-show=\"showAddMenu\"\r\n class=\"add-node-menu\"\r\n :style=\"{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }\"\r\n >\r\n <div class=\"add-menu-content\">\r\n <div\r\n v-for=\"nodeType in NODE_TYPE_OPTIONS\"\r\n :key=\"nodeType.type\"\r\n class=\"add-menu-item\"\r\n @click=\"addNode(nodeType.type)\"\r\n >\r\n <div class=\"menu-icon\" :class=\"nodeType.iconClass\">\r\n <C_Icon :name=\"nodeType.icon\" :size=\"16\" />\r\n </div>\r\n <span class=\"menu-text\">{{ nodeType.label }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </Teleport>\r\n\r\n <!-- ่็น้
็ฝฎๅผน็ช - ๆๅๅฐ็ฌ็ซ็ปไปถ -->\r\n <NodeConfigModal\r\n v-model:show=\"showNodeConfig\"\r\n :current-node=\"currentNode\"\r\n :users=\"users\"\r\n :departments=\"departments\"\r\n @save=\"handleConfigSave\"\r\n @cancel=\"showNodeConfig = false\"\r\n />\r\n\r\n <!-- ้ช่ฏ้่ฏฏๆฅๅฟๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showValidationErrors\" :width=\"450\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ช่ฏ็ปๆ\" closable>\r\n <div v-if=\"validationErrors.length === 0\" class=\"validation-success\">\r\n <div class=\"success-icon\">\r\n <C_Icon name=\"mdi:check-circle\" :size=\"32\" color=\"#52c41a\" />\r\n </div>\r\n <h3>้ช่ฏ้่ฟ</h3>\r\n <p>ๅทฅไฝๆต้
็ฝฎๆญฃ็กฎ๏ผๆๆ่็น้ฝๅทฒๆญฃ็กฎ่ฎพ็ฝฎ๏ผ</p>\r\n </div>\r\n\r\n <div v-else class=\"validation-errors\">\r\n <div class=\"error-summary\">\r\n <div class=\"error-icon\">\r\n <C_Icon name=\"mdi:alert-circle\" :size=\"24\" color=\"#ff4d4f\" />\r\n </div>\r\n <h3>ๅ็ฐ {{ validationErrors.length }} ไธช้ฎ้ข</h3>\r\n <p>่ฏทไฟฎๅคไปฅไธ้ฎ้ขๅ้ๆฐ้ช่ฏ๏ผ</p>\r\n </div>\r\n\r\n <div class=\"error-list\">\r\n <div\r\n v-for=\"(error, index) in validationErrors\"\r\n :key=\"error.nodeId\"\r\n class=\"error-item\"\r\n >\r\n <div class=\"error-header\">\r\n <span class=\"error-number\">{{ index + 1 }}</span>\r\n <div class=\"error-info\">\r\n <strong class=\"error-node\">{{ error.nodeName }}</strong>\r\n <span class=\"error-field\">{{\r\n getFieldDisplayName(error.field)\r\n }}</span>\r\n </div>\r\n <div class=\"error-type\" :class=\"error.type\">\r\n {{ getErrorTypeText(error.type) }}\r\n </div>\r\n </div>\r\n <div class=\"error-message\">{{ error.message }}</div>\r\n <div class=\"error-actions\">\r\n <NButton\r\n size=\"small\"\r\n type=\"primary\"\r\n @click=\"jumpToErrorNode(error.nodeId)\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:target\" :size=\"16\" />\r\n </template>\r\n ๅฎไฝ่็น\r\n </NButton>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"validation-tips\">\r\n <h4>๐ก ๅธธ่ง้ฎ้ข่งฃๅณๆนๆก๏ผ</h4>\r\n <ul>\r\n <li>\r\n <strong>ๅฎกๆนไบบๆช่ฎพ็ฝฎ๏ผ</strong\r\n >็นๅปๅฎกๆน่็น๏ผๅจๅผน็ชไธญ้ๆฉๅฎกๆนไบบๅ\r\n </li>\r\n <li>\r\n <strong>ๆกไปถๅๆฏๆช้
็ฝฎ๏ผ</strong\r\n >็นๅปๆกไปถ่็น๏ผๆทปๅ ่ณๅฐไธไธชๆกไปถๅๆฏ\r\n </li>\r\n <li>\r\n <strong>่็น่ฟๆฅๆญๅผ๏ผ</strong> ๆฃๆฅ่็นไน้ด็่ฟ็บฟๆฏๅฆๆญฃ็กฎ\r\n </li>\r\n </ul>\r\n </div>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"validation-footer\">\r\n <NButton @click=\"showValidationErrors = false\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"validateCurrentWorkflow\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:refresh\" :size=\"16\" />\r\n </template>\r\n ้ๆฐ้ช่ฏ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n\r\n <!-- ๆต็จ้ข่งๆฝๅฑ -->\r\n <NDrawer v-model:show=\"showPreview\" :width=\"520\" placement=\"right\">\r\n <NDrawerContent title=\"ๆต็จ้ข่ง\" closable>\r\n <!-- ็ป่ฎกๆฆ่ง -->\r\n <div class=\"preview-stats\">\r\n <div class=\"stat-item\">\r\n <span class=\"stat-value\">{{ previewStats.totalNodes }}</span>\r\n <span class=\"stat-label\">ๆป่็น</span>\r\n </div>\r\n <div class=\"stat-item approval\">\r\n <span class=\"stat-value\">{{ previewStats.approvalNodes }}</span>\r\n <span class=\"stat-label\">ๅฎกๆน</span>\r\n </div>\r\n <div class=\"stat-item copy\">\r\n <span class=\"stat-value\">{{ previewStats.copyNodes }}</span>\r\n <span class=\"stat-label\">ๆ้</span>\r\n </div>\r\n <div class=\"stat-item condition\">\r\n <span class=\"stat-value\">{{ previewStats.conditionNodes }}</span>\r\n <span class=\"stat-label\">ๆกไปถ</span>\r\n </div>\r\n <div class=\"stat-item edge\">\r\n <span class=\"stat-value\">{{ previewStats.totalEdges }}</span>\r\n <span class=\"stat-label\">่ฟ็บฟ</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆต็จๆญฅ้ชคๆถ้ด็บฟ -->\r\n <div class=\"preview-timeline\">\r\n <div\r\n v-for=\"(step, index) in previewSteps\"\r\n :key=\"step.node.id\"\r\n class=\"preview-step\"\r\n :class=\"step.colorClass\"\r\n >\r\n <!-- ๆถ้ด็บฟ่ฟๆฅ็บฟ -->\r\n <div class=\"step-connector\">\r\n <div class=\"step-dot\">\r\n <C_Icon :name=\"step.icon\" :size=\"16\" />\r\n </div>\r\n <div\r\n v-if=\"index < previewSteps.length - 1\"\r\n class=\"step-line\"\r\n ></div>\r\n </div>\r\n\r\n <!-- ๆญฅ้ชคๅ
ๅฎน -->\r\n <div class=\"step-content\">\r\n <div class=\"step-header\">\r\n <span class=\"step-order\">ๆญฅ้ชค {{ step.order }}</span>\r\n <span class=\"step-type-badge\">{{ step.nodeTypeLabel }}</span>\r\n </div>\r\n <div class=\"step-title\">{{ step.node.data.title }}</div>\r\n <div v-if=\"step.details.length > 0\" class=\"step-details\">\r\n <div\r\n v-for=\"(detail, dIdx) in step.details\"\r\n :key=\"dIdx\"\r\n class=\"step-detail-item\"\r\n :class=\"{ 'is-warning': detail.type === 'warning' }\"\r\n >\r\n <span class=\"detail-label\">{{ detail.label }}๏ผ</span>\r\n\r\n <!-- ๅฎกๆนๆนๅผ badge -->\r\n <span\r\n v-if=\"detail.type === 'mode'\"\r\n class=\"mode-badge\"\r\n :class=\"detail.modeKey\"\r\n >\r\n {{ detail.value }}\r\n </span>\r\n\r\n <!-- ไบบๅ tags -->\r\n <template v-else-if=\"detail.type === 'users'\">\r\n <NTag\r\n v-for=\"user in detail.users\"\r\n :key=\"user.name\"\r\n size=\"small\"\r\n :bordered=\"false\"\r\n round\r\n class=\"user-tag\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:account\" :size=\"12\" />\r\n </template>\r\n {{ user.name }}\r\n </NTag>\r\n </template>\r\n\r\n <!-- ่ญฆๅๆๆก -->\r\n <span\r\n v-else-if=\"detail.type === 'warning'\"\r\n class=\"warning-text\"\r\n >\r\n โ ๏ธ {{ detail.value }}\r\n </span>\r\n\r\n <!-- ๆฎ้ๆๆฌ -->\r\n <span v-else>{{ detail.value }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-if=\"previewSteps.length === 0\" class=\"preview-empty\">\r\n <C_Icon name=\"mdi:file-document-outline\" :size=\"48\" color=\"#d1d5db\" />\r\n <p>ๆๆ ๆต็จ่็น</p>\r\n </div>\r\n\r\n <template #footer>\r\n <div class=\"preview-footer\">\r\n <NButton @click=\"closePreview\">ๅ
ณ้ญ</NButton>\r\n <NButton type=\"primary\" @click=\"confirmAndSave\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:content-save\" :size=\"16\" />\r\n </template>\r\n ็กฎ่ฎคๅนถไฟๅญ\r\n </NButton>\r\n </div>\r\n </template>\r\n </NDrawerContent>\r\n </NDrawer>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { VueFlow } from \"@vue-flow/core\";\r\nimport { NButton, NDrawer, NDrawerContent, NTag, useMessage } from \"naive-ui\";\r\nimport { C_Icon } from \"../C_Icon\";\r\nimport type { WorkflowProps, WorkflowEmits } from \"./types\";\r\nimport { NODE_TYPE_OPTIONS } from \"./data\";\r\nimport NodeConfigModal from \"./NodeConfigModal.vue\";\r\nimport { useWorkflowNodes } from \"./composables/useWorkflowNodes\";\r\nimport { useWorkflowValidation } from \"./composables/useWorkflowValidation\";\r\nimport { useWorkflowPreview } from \"./composables/useWorkflowPreview\";\r\n\r\ndefineOptions({ name: \"C_WorkFlow\" });\r\n\r\n/* Props & Emits */\r\nconst props = withDefaults(defineProps<WorkflowProps>(), {\r\n users: () => [],\r\n roles: () => [],\r\n departments: () => [],\r\n readonly: false,\r\n theme: \"light\",\r\n});\r\n\r\nconst emit = defineEmits<WorkflowEmits>();\r\nconst message = useMessage();\r\nconst vueFlowRef = ref();\r\n\r\n/* โโโ ่็น็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n nodes,\r\n edges,\r\n showAddMenu,\r\n menuPosition,\r\n showNodeConfig,\r\n currentNode,\r\n nodeTypes,\r\n workflowStats,\r\n addNode,\r\n onNodeClick,\r\n closeAddMenu,\r\n handleConfigSave,\r\n resetNodes,\r\n getCurrentWorkflowData,\r\n fitView,\r\n deleteNode,\r\n} = useWorkflowNodes(props, emit, vueFlowRef);\r\n\r\n/* โโโ ๆต็จ้ช่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst {\r\n validationErrors,\r\n showValidationErrors,\r\n validateWorkflow,\r\n validateCurrentWorkflow,\r\n jumpToErrorNode,\r\n getFieldDisplayName,\r\n getErrorTypeText,\r\n resetValidation,\r\n} = useWorkflowValidation(nodes, edges, vueFlowRef, {\r\n onShowNodeConfig: (node) => {\r\n currentNode.value = node;\r\n showNodeConfig.value = true;\r\n },\r\n onValidateError: (errors) => emit(\"validate-error\", errors),\r\n});\r\n\r\n/* โโโ ๆต็จ้ข่ง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\nconst { showPreview, previewSteps, previewStats, openPreview, closePreview } =\r\n useWorkflowPreview(nodes, edges);\r\n\r\n/* โโโ ็ผๆๆนๆณ๏ผ่ทจ composable ๅไฝ๏ผ โโโโโโโโโโโโโโโโโโโโโโ */\r\nconst saveWorkflow = (): void => {\r\n const errors = validateWorkflow();\r\n if (errors.length > 0) {\r\n message.error(`ๅทฅไฝๆต้ช่ฏๅคฑ่ดฅ: ${errors[0].message}`);\r\n showValidationErrors.value = true;\r\n return;\r\n }\r\n const data = getCurrentWorkflowData();\r\n emit(\"save\", data);\r\n message.success(\"ๅทฅไฝๆตไฟๅญๆๅ\");\r\n};\r\n\r\nconst previewWorkflow = (): void => {\r\n openPreview();\r\n};\r\n\r\nconst confirmAndSave = (): void => {\r\n closePreview();\r\n saveWorkflow();\r\n};\r\n\r\nconst clearWorkflow = (): void => {\r\n resetNodes();\r\n resetValidation();\r\n};\r\n\r\n/* โโโ ๆด้ฒๆนๆณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\ndefineExpose({\r\n validateWorkflow,\r\n getCurrentWorkflowData,\r\n saveWorkflow,\r\n previewWorkflow,\r\n deleteNode,\r\n stats: workflowStats,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n"],"mappings":"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"}
|