@robot-admin/naive-ui-components 0.3.2 โ 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_Form2.js +48 -48
- 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_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/constants2.d.ts +2 -2
- package/dist/constants3.d.ts +2 -2
- package/dist/constants4.d.ts +5 -5
- package/dist/constants5.d.ts +2 -2
- package/dist/data.d.ts +1 -1
- package/dist/index.vue.d.ts +1 -1
- package/dist/index10.vue.d.ts +5 -5
- package/dist/index12.vue.d.ts +1 -1
- package/dist/index12.vue.d.ts.map +1 -1
- package/dist/index13.vue.d.ts +1 -1
- package/dist/index16.vue.d.ts +3 -3
- package/dist/index16.vue.d.ts.map +1 -1
- package/dist/index2.vue.d.ts +5 -5
- package/dist/index3.vue.d.ts +4 -4
- package/dist/index6.vue.d.ts +3 -3
- package/dist/index8.vue.d.ts +2 -2
- package/dist/style.css +1555 -1555
- package/dist/useCropperCore.d.ts +7 -7
- package/dist/useDraggableLayout.d.ts +9 -9
- package/dist/useDynamicFormState.d.ts +9 -9
- package/dist/useDynamicFormState.d.ts.map +1 -1
- package/dist/useEdgeInteraction.d.ts +2 -2
- package/dist/useInfiniteScroll.d.ts +2 -2
- package/dist/useModalEdit.d.ts +2 -2
- package/dist/useModalEdit.d.ts.map +1 -1
- package/dist/useQRCode.d.ts +2 -2
- package/dist/useSearchState.d.ts +2 -2
- package/dist/useSignatureHistory.d.ts +3 -3
- package/dist/useSplitResize.d.ts +6 -6
- package/dist/useSplitResize.d.ts.map +1 -1
- package/dist/useTimeSelection.d.ts +1 -1
- package/dist/useTreeOperations.d.ts +3 -3
- package/dist/useWorkflowValidation.d.ts +1 -1
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_NotificationCenter2.js","names":["$emit","count","hasUrgent","message","$emit","activeCategory","$emit","messages","scrollHeight","loading","hasMore","$emit","message","scrollHeight"],"sources":["../src/components/C_NotificationCenter/constants.ts","../src/components/C_NotificationCenter/composables/useNotificationWS.ts","../src/components/C_NotificationCenter/composables/useNotificationCore.ts","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/composables/useNotificationFormat.ts","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/index.vue","../src/components/C_NotificationCenter/index.vue","../src/components/C_NotificationCenter/index.vue"],"sourcesContent":["/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟๅธธ้้
็ฝฎ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport type { NotificationCategory, NotificationPriority } from \"./types\";\r\n\r\n/* โโโ ้ป่ฎค้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ้ป่ฎค่งๆ ๆๅคงๆพ็คบๆฐ */\r\nexport const DEFAULT_MAX_BADGE_COUNT = 99;\r\n\r\n/** ้ป่ฎคๆฏ้กตๆกๆฐ */\r\nexport const DEFAULT_PAGE_SIZE = 20;\r\n\r\n/** ้ป่ฎค่ฝฎ่ฏข้ด้๏ผms๏ผ๏ผ60 ็ง */\r\nexport const DEFAULT_POLLING_INTERVAL = 60_000;\r\n\r\n/** ้ป่ฎค็ผๅญ key */\r\nexport const DEFAULT_STORAGE_KEY = \"notification_center\";\r\n\r\n/** WebSocket ้ป่ฎค้่ฟ้ด้๏ผms๏ผ */\r\nexport const DEFAULT_WS_RECONNECT_INTERVAL = 3_000;\r\n\r\n/** WebSocket ้ป่ฎคๆๅคง้่ฟๆฌกๆฐ */\r\nexport const DEFAULT_WS_MAX_RECONNECT = 5;\r\n\r\n/** WebSocket ้ป่ฎคๅฟ่ทณ้ด้๏ผms๏ผ */\r\nexport const DEFAULT_WS_HEARTBEAT_INTERVAL = 30_000;\r\n\r\n/** WebSocket ้ป่ฎคๅฟ่ทณๆถๆฏ */\r\nexport const DEFAULT_WS_HEARTBEAT_MESSAGE = \"ping\";\r\n\r\n/* โโโ ๅ็ฑป้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๅ็ฑปๆ ็ญพๆ ๅฐ */\r\nexport const CATEGORY_MAP: Record<\r\n NotificationCategory,\r\n {\r\n label: string;\r\n icon: string;\r\n color: string;\r\n }\r\n> = {\r\n system: {\r\n label: \"็ณป็ป้็ฅ\",\r\n icon: \"mdi:cog-outline\",\r\n color: \"info\",\r\n },\r\n business: {\r\n label: \"ไธๅกๆถๆฏ\",\r\n icon: \"mdi:briefcase-outline\",\r\n color: \"success\",\r\n },\r\n alarm: {\r\n label: \"ๅ่ญฆ้ข่ญฆ\",\r\n icon: \"mdi:alert-outline\",\r\n color: \"warning\",\r\n },\r\n};\r\n\r\n/** ๅ็ฑป Tab ๅ่กจ */\r\nexport const CATEGORY_TABS: {\r\n key: NotificationCategory | \"all\";\r\n label: string;\r\n}[] = [\r\n { key: \"all\", label: \"ๅ
จ้จ\" },\r\n { key: \"system\", label: \"็ณป็ป้็ฅ\" },\r\n { key: \"business\", label: \"ไธๅกๆถๆฏ\" },\r\n { key: \"alarm\", label: \"ๅ่ญฆ้ข่ญฆ\" },\r\n];\r\n\r\n/* โโโ ไผๅ
็บง้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ไผๅ
็บงๆ ็ญพๆ ๅฐ */\r\nexport const PRIORITY_MAP: Record<\r\n NotificationPriority,\r\n {\r\n label: string;\r\n type: \"default\" | \"info\" | \"success\" | \"warning\" | \"error\";\r\n }\r\n> = {\r\n low: { label: \"ไฝ\", type: \"default\" },\r\n normal: { label: \"ๆฎ้\", type: \"info\" },\r\n high: { label: \"้่ฆ\", type: \"warning\" },\r\n urgent: { label: \"็ดงๆฅ\", type: \"error\" },\r\n};\r\n\r\n/* โโโ ๆจกๆๆฐๆฎ๏ผๅผๅ็ฏๅขไฝฟ็จ๏ผ โโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆจกๆๆถๆฏๅ่กจ */\r\nexport const MOCK_MESSAGES = [\r\n {\r\n id: \"msg-001\",\r\n title: \"็ณป็ป็ปดๆค้็ฅ\",\r\n summary:\r\n \"็ณป็ปๅฐไบไปๆ 22:00-23:00 ่ฟ่กไพ่ก็ปดๆคๅ็บง๏ผๅฑๆถ้จๅๅ่ฝๅฏ่ฝๆๆถไธๅฏ็จใ\",\r\n content:\r\n \"<p>ๅฐๆฌ็็จๆท๏ผ</p><p>ไธบไบๆๅ็ณป็ปๆง่ฝๅ็จณๅฎๆง๏ผๆไปฌ่ฎกๅไบ <strong>2026ๅนด2ๆ27ๆฅ 22:00-23:00</strong> ๅฏน็ณป็ป่ฟ่กไพ่ก็ปดๆคๅ็บงใ</p><p>็ปดๆคๆ้ด๏ผไปฅไธๅ่ฝๅฏ่ฝๆๆถไธๅฏ็จ๏ผ</p><ul><li>ๆฅ่กจๅฏผๅบ</li><li>ๆฐๆฎๅๆญฅ</li><li>ๅฎๆถไปปๅก</li></ul><p>่ฏทๆๅไฟๅญๅฅฝๆชๅฎๆ็ๅทฅไฝ๏ผๆ่ฐขๆจ็็่งฃไธๆฏๆ๏ผ</p>\",\r\n category: \"system\" as const,\r\n priority: \"high\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 15 * 60_000).toISOString(),\r\n sender: { name: \"็ณป็ป็ฎก็ๅ\", avatar: \"\" },\r\n },\r\n {\r\n id: \"msg-002\",\r\n title: \"ๅฎกๆนๆต็จๅพ
ๅค็\",\r\n summary: \"ๅผ ไธๆไบค็ใQ1 ๅญฃๅบฆ้ข็ฎ็ณ่ฏทใ้่ฆๆจๅฎกๆน๏ผ่ฏทๅฐฝๅฟซๅค็ใ\",\r\n category: \"business\" as const,\r\n priority: \"urgent\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 30 * 60_000).toISOString(),\r\n sender: {\r\n name: \"ๅผ ไธ\",\r\n avatar:\r\n \"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png\",\r\n },\r\n actionUrl: \"/workflow/approval\",\r\n actionText: \"ๅปๅฎกๆน\",\r\n },\r\n {\r\n id: \"msg-003\",\r\n title: \"ๆๅกๅจ CPU ไฝฟ็จ็ๅ่ญฆ\",\r\n summary:\r\n \"็ไบง็ฏๅข่็น prod-node-03 CPU ไฝฟ็จ็ๆ็ปญ่ถ
่ฟ 90%๏ผๅทฒ่งฆๅๅ่ญฆ้ๅผใ\",\r\n category: \"alarm\" as const,\r\n priority: \"urgent\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 2 * 60_000).toISOString(),\r\n sender: { name: \"็ๆงไธญๅฟ\" },\r\n actionUrl: \"/monitor/server\",\r\n actionText: \"ๆฅ็่ฏฆๆ
\",\r\n },\r\n {\r\n id: \"msg-004\",\r\n title: \"ๆฐ็ๆฌๅๅธ v1.14.0\",\r\n summary: \"็ณป็ปๅทฒๅ็บง่ณ v1.14.0๏ผๆฐๅข้็ฅไธญๅฟใๅทฅไฝๆตๅขๅผบ็ญๅค้กนๅ่ฝใ\",\r\n category: \"system\" as const,\r\n priority: \"normal\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 3 * 3_600_000).toISOString(),\r\n sender: { name: \"็ณป็ป็ฎก็ๅ\" },\r\n },\r\n {\r\n id: \"msg-005\",\r\n title: \"้กน็ฎไบคไปๆ้\",\r\n summary: \"ใRobot Admin ไบๆใ้กน็ฎไบคไปๆชๆญขๆฅๆไธบ 3ๆ15ๆฅ๏ผๅฝๅ่ฟๅบฆ 78%ใ\",\r\n category: \"business\" as const,\r\n priority: \"high\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 8 * 3_600_000).toISOString(),\r\n sender: { name: \"้กน็ฎ็ฎก็็ณป็ป\" },\r\n },\r\n {\r\n id: \"msg-006\",\r\n title: \"ๆฐๆฎๅบ่ฟๆฅๆฑ ๅ่ญฆ\",\r\n summary: \"ๆฐๆฎๅบ่ฟๆฅๆฑ ไฝฟ็จ็่พพๅฐ 85%๏ผๅปบ่ฎฎๅ
ณๆณจๅนถ้ๆถๆฉๅฎนใ\",\r\n category: \"alarm\" as const,\r\n priority: \"high\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 12 * 3_600_000).toISOString(),\r\n sender: { name: \"็ๆงไธญๅฟ\" },\r\n },\r\n {\r\n id: \"msg-007\",\r\n title: \"ๅจๆฅๆไบคๆ้\",\r\n summary: \"ๆฌๅจๅจๆฅๆไบคๆชๆญขๆถ้ดไธบๅจไบ 18:00๏ผ่ฏทๅๆถๆไบคใ\",\r\n category: \"business\" as const,\r\n priority: \"normal\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 24 * 3_600_000).toISOString(),\r\n sender: { name: \"OA ็ณป็ป\" },\r\n },\r\n {\r\n id: \"msg-008\",\r\n title: \"่ดฆๅทๅฎๅ
จๆ้\",\r\n summary: \"ๆฃๆตๅฐๆจ็่ดฆๅทๅจๆฐ่ฎพๅคไธ็ปๅฝ๏ผๅฆ้ๆฌไบบๆไฝ่ฏทๅๆถไฟฎๆนๅฏ็ ใ\",\r\n category: \"system\" as const,\r\n priority: \"high\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 45 * 60_000).toISOString(),\r\n sender: { name: \"ๅฎๅ
จไธญๅฟ\" },\r\n },\r\n];\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: WebSocket ่ฟๆฅ็ฎก็\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, readonly, onBeforeUnmount } from \"vue\";\r\nimport type {\r\n NotificationWSConfig,\r\n WSConnectionStatus,\r\n WSNotificationPayload,\r\n} from \"../types\";\r\nimport {\r\n DEFAULT_WS_RECONNECT_INTERVAL,\r\n DEFAULT_WS_MAX_RECONNECT,\r\n DEFAULT_WS_HEARTBEAT_INTERVAL,\r\n DEFAULT_WS_HEARTBEAT_MESSAGE,\r\n} from \"../constants\";\r\n\r\n/**\r\n * WebSocket ่ฟๆฅ็ฎก็\r\n *\r\n * ็ฎก็ WebSocket ็ๅปบ่ฟใ้่ฟใๅฟ่ทณไฟๆดปใๆถๆฏ่งฃๆใ\r\n * ๆฏๆ่ชๅจ้่ฟใ้ดๆ tokenใๅฟ่ทณๆฃๆตใ\r\n */\r\nexport function useNotificationWS(\r\n onMessage: (payload: WSNotificationPayload) => void,\r\n onStatusChange?: (status: WSConnectionStatus) => void,\r\n) {\r\n /** ่ฟๆฅ็ถๆ */\r\n const status = ref<WSConnectionStatus>(\"disconnected\");\r\n\r\n /** WebSocket ๅฎไพ */\r\n let ws: WebSocket | null = null;\r\n /** ้่ฟ่ฎกๆฐๅจ */\r\n let reconnectCount = 0;\r\n /** ้่ฟๅฎๆถๅจ */\r\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n /** ๅฟ่ทณๅฎๆถๅจ */\r\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\r\n /** ๅฝๅ้
็ฝฎ */\r\n let currentConfig: NotificationWSConfig | null = null;\r\n\r\n /**\r\n * ๆดๆฐ่ฟๆฅ็ถๆ\r\n */\r\n function setStatus(s: WSConnectionStatus) {\r\n status.value = s;\r\n onStatusChange?.(s);\r\n }\r\n\r\n /**\r\n * ๅปบ็ซ่ฟๆฅ\r\n */\r\n function connect(config: NotificationWSConfig) {\r\n currentConfig = config;\r\n reconnectCount = 0;\r\n createConnection(config);\r\n }\r\n\r\n /**\r\n * ๅๅปบ WebSocket ่ฟๆฅ\r\n */\r\n function createConnection(config: NotificationWSConfig) {\r\n cleanup();\r\n\r\n const url = buildUrl(config);\r\n setStatus(\"connecting\");\r\n\r\n try {\r\n ws = new WebSocket(url);\r\n } catch {\r\n setStatus(\"disconnected\");\r\n scheduleReconnect(config);\r\n return;\r\n }\r\n\r\n ws.addEventListener(\"open\", () => {\r\n setStatus(\"connected\");\r\n reconnectCount = 0;\r\n startHeartbeat(config);\r\n });\r\n\r\n ws.addEventListener(\"message\", (event) => {\r\n handleMessage(event.data);\r\n });\r\n\r\n ws.addEventListener(\"close\", () => {\r\n stopHeartbeat();\r\n setStatus(\"disconnected\");\r\n\r\n if (config.autoReconnect !== false) {\r\n scheduleReconnect(config);\r\n }\r\n });\r\n\r\n ws.addEventListener(\"error\", () => {\r\n /* error ไบไปถๅ้ๅธธ็ดงๆฅ close ไบไปถ๏ผไบค็ฑ close ๅค็้่ฟ */\r\n });\r\n }\r\n\r\n /**\r\n * ๆๅปบๅธฆ token ็ WebSocket URL\r\n */\r\n function buildUrl(config: NotificationWSConfig): string {\r\n const token = config.getToken?.();\r\n if (token) {\r\n const separator = config.url.includes(\"?\") ? \"&\" : \"?\";\r\n return `${config.url}${separator}token=${encodeURIComponent(token)}`;\r\n }\r\n return config.url;\r\n }\r\n\r\n /**\r\n * ๅค็ๆฅๆถๅฐ็ๆถๆฏ\r\n */\r\n function handleMessage(raw: string | ArrayBuffer | Blob) {\r\n if (typeof raw !== \"string\") return;\r\n\r\n /* ่ฟๆปคๅฟ่ทณๅๅบ */\r\n if (raw === \"pong\") return;\r\n\r\n try {\r\n const payload = JSON.parse(raw) as WSNotificationPayload;\r\n onMessage(payload);\r\n } catch {\r\n /* ้ๆ ๅ JSON ๆถๆฏ๏ผๅฟฝ็ฅ */\r\n }\r\n }\r\n\r\n /**\r\n * ่ฐๅบฆ้่ฟ\r\n */\r\n function scheduleReconnect(config: NotificationWSConfig) {\r\n const maxAttempts = config.maxReconnectAttempts ?? DEFAULT_WS_MAX_RECONNECT;\r\n if (reconnectCount >= maxAttempts) return;\r\n\r\n reconnectCount++;\r\n setStatus(\"reconnecting\");\r\n\r\n const interval = config.reconnectInterval ?? DEFAULT_WS_RECONNECT_INTERVAL;\r\n reconnectTimer = setTimeout(() => {\r\n createConnection(config);\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅฏๅจๅฟ่ทณ\r\n */\r\n function startHeartbeat(config: NotificationWSConfig) {\r\n const interval = config.heartbeatInterval ?? DEFAULT_WS_HEARTBEAT_INTERVAL;\r\n if (interval <= 0) return;\r\n\r\n const message = config.heartbeatMessage ?? DEFAULT_WS_HEARTBEAT_MESSAGE;\r\n heartbeatTimer = setInterval(() => {\r\n if (ws?.readyState === WebSocket.OPEN) {\r\n ws.send(message);\r\n }\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅๆญขๅฟ่ทณ\r\n */\r\n function stopHeartbeat() {\r\n if (heartbeatTimer) {\r\n clearInterval(heartbeatTimer);\r\n heartbeatTimer = null;\r\n }\r\n }\r\n\r\n /**\r\n * ๆธ
็่ตๆบ\r\n */\r\n function cleanup() {\r\n stopHeartbeat();\r\n if (reconnectTimer) {\r\n clearTimeout(reconnectTimer);\r\n reconnectTimer = null;\r\n }\r\n if (ws) {\r\n ws.close();\r\n ws = null;\r\n }\r\n }\r\n\r\n /**\r\n * ๆญๅผ่ฟๆฅ\r\n */\r\n function disconnect() {\r\n currentConfig = null;\r\n reconnectCount = Infinity;\r\n cleanup();\r\n setStatus(\"disconnected\");\r\n }\r\n\r\n /**\r\n * ๆๅจ้ๆฐ่ฟๆฅ\r\n */\r\n function reconnect() {\r\n if (currentConfig) {\r\n reconnectCount = 0;\r\n createConnection(currentConfig);\r\n }\r\n }\r\n\r\n /* ็ปไปถๅธ่ฝฝๆถ่ชๅจๆธ
็ */\r\n onBeforeUnmount(cleanup);\r\n\r\n return {\r\n /** WebSocket ่ฟๆฅ็ถๆ */\r\n status: readonly(status),\r\n connect,\r\n disconnect,\r\n reconnect,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟๆ ธๅฟ็ถๆ็ฎก็\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, computed, onMounted, onBeforeUnmount } from \"vue\";\r\nimport type {\r\n NotificationMessage,\r\n NotificationCategory,\r\n NotificationCenterProps,\r\n WSNotificationPayload,\r\n WSConnectionStatus,\r\n} from \"../types\";\r\nimport {\r\n DEFAULT_PAGE_SIZE,\r\n DEFAULT_POLLING_INTERVAL,\r\n DEFAULT_STORAGE_KEY,\r\n MOCK_MESSAGES,\r\n} from \"../constants\";\r\nimport { useNotificationWS } from \"./useNotificationWS\";\r\nimport { setItem, getItem } from \"../../../utils/storage\";\r\n\r\n/** ็ผๅญๆฐๆฎ็ปๆ */\r\ninterface CachedState {\r\n unreadIds: string[];\r\n lastFetchTime: number;\r\n}\r\n\r\n/** ้ฒๆๆไน
ๅๅปถ่ฟ๏ผms๏ผ */\r\nconst PERSIST_DEBOUNCE = 300;\r\n\r\n/**\r\n * ้็ฅไธญๅฟๆ ธๅฟ็ถๆ็ฎก็\r\n *\r\n * ็ปไธ็ฎก็ๆถๆฏๅ่กจใๆช่ฏปๆฐใๅ็ฑป่ฟๆปคใๅทฒ่ฏปๆ ่ฎฐใ\r\n * API ไบคไบใWebSocket ๆกฅๆฅใ่ฝฎ่ฏข่ฐๅบฆๅๆฌๅฐ็ผๅญใ\r\n */\r\nexport function useNotificationCore(props: NotificationCenterProps) {\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ๆถๆฏๅ่กจ */\r\n const messages = ref<NotificationMessage[]>([]);\r\n /** ๅฝๅ้ไธญๅ็ฑป */\r\n const activeCategory = ref<NotificationCategory | \"all\">(\"all\");\r\n /** ๆฏๅฆๆญฃๅจๅ ่ฝฝ */\r\n const loading = ref(false);\r\n /** ๆปๆกๆฐ */\r\n const total = ref(0);\r\n /** ๅฝๅ้กต็ */\r\n const page = ref(1);\r\n /** ้ไธญ็ๆถๆฏ๏ผ่ฏฆๆ
ๅฑ็คบ๏ผ */\r\n const selectedMessage = ref<NotificationMessage | null>(null);\r\n /** Popover ๅฑๅผ็ถๆ */\r\n const popoverVisible = ref(false);\r\n /** WebSocket ่ฟๆฅ็ถๆ */\r\n const wsStatus = ref<WSConnectionStatus>(\"disconnected\");\r\n\r\n /* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ๆช่ฏปๆถๆฏๆปๆฐ */\r\n const unreadCount = computed(\r\n () => messages.value.filter((m) => m.status === \"unread\").length,\r\n );\r\n\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n const unreadByCategory = computed(() => {\r\n const counts: Record<string, number> = { system: 0, business: 0, alarm: 0 };\r\n for (const m of messages.value) {\r\n if (m.status === \"unread\" && m.category in counts) {\r\n counts[m.category]++;\r\n }\r\n }\r\n return counts;\r\n });\r\n\r\n /** ๅฝๅๅ็ฑปไธ็ๆถๆฏๅ่กจ */\r\n const filteredMessages = computed(() => {\r\n if (activeCategory.value === \"all\") return messages.value;\r\n return messages.value.filter((m) => m.category === activeCategory.value);\r\n });\r\n\r\n /** ๆฏๅฆๆๆดๅคๆถๆฏๅฏๅ ่ฝฝ */\r\n const hasMore = computed(() => messages.value.length < total.value);\r\n\r\n /* โโโ ็ผๅญ็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ็ผๅญ key */\r\n const storageKey = computed(() => props.storageKey ?? DEFAULT_STORAGE_KEY);\r\n\r\n /**\r\n * ไป็ผๅญๆขๅคๆช่ฏป็ถๆ\r\n */\r\n function restoreFromCache() {\r\n const cached = getItem<CachedState>(storageKey.value);\r\n if (cached?.unreadIds) {\r\n const idSet = new Set(cached.unreadIds);\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"unread\";\r\n }\r\n }\r\n }\r\n }\r\n\r\n /** ้ฒๆๆไน
ๅๅฎๆถๅจ */\r\n let persistTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * ๆไน
ๅๆช่ฏป็ถๆๅฐๆฌๅฐ็ผๅญ๏ผ้ฒๆ๏ผ\r\n */\r\n function persistToCache() {\r\n if (persistTimer) clearTimeout(persistTimer);\r\n persistTimer = setTimeout(() => {\r\n const unreadIds = messages.value\r\n .filter((m) => m.status === \"unread\")\r\n .map((m) => m.id);\r\n setItem<CachedState>(storageKey.value, {\r\n unreadIds,\r\n lastFetchTime: Date.now(),\r\n });\r\n persistTimer = null;\r\n }, PERSIST_DEBOUNCE);\r\n }\r\n\r\n /* โโโ API ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๆๅๆถๆฏๅ่กจ\r\n */\r\n async function fetchMessages(reset = false) {\r\n if (reset) {\r\n page.value = 1;\r\n }\r\n\r\n loading.value = true;\r\n try {\r\n if (props.fetchNotifications) {\r\n const categoryParam =\r\n activeCategory.value === \"all\" ? undefined : activeCategory.value;\r\n const pageSize = props.pageSize ?? DEFAULT_PAGE_SIZE;\r\n const result = await props.fetchNotifications({\r\n category: categoryParam,\r\n page: page.value,\r\n pageSize,\r\n });\r\n if (reset) {\r\n messages.value = result.list;\r\n } else {\r\n messages.value.push(...result.list);\r\n }\r\n total.value = result.total;\r\n } else {\r\n /* ๆช้
็ฝฎ API ๆฅๅฃ โ ไฝฟ็จๆจกๆๆฐๆฎ๏ผๅ
จ้ๅ ่ฝฝ๏ผๅฎขๆท็ซฏ่ฟๆปค๏ผ */\r\n loadMockData();\r\n }\r\n\r\n restoreFromCache();\r\n } finally {\r\n loading.value = false;\r\n }\r\n }\r\n\r\n /**\r\n * ๅ ่ฝฝๆจกๆๆฐๆฎ๏ผๅ
จ้ๅ ่ฝฝ๏ผfilteredMessages ่ด่ดฃๅฎขๆท็ซฏ่ฟๆปค๏ผ\r\n */\r\n function loadMockData() {\r\n messages.value = MOCK_MESSAGES.map((m) => ({ ...m }));\r\n total.value = MOCK_MESSAGES.length;\r\n }\r\n\r\n /**\r\n * ๅ ่ฝฝๆดๅค\r\n */\r\n async function loadMore() {\r\n if (!hasMore.value || loading.value) return;\r\n page.value++;\r\n await fetchMessages();\r\n }\r\n\r\n /**\r\n * ๆ ่ฎฐๆๅฎๆถๆฏไธบๅทฒ่ฏป\r\n */\r\n async function markAsRead(ids: string[]) {\r\n const idSet = new Set(ids);\r\n\r\n /* ไน่งๆดๆฐ */\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"read\";\r\n }\r\n }\r\n persistToCache();\r\n\r\n /* ๅฆๆ้
็ฝฎไบ API โ ๅๆญฅๅฐๆๅก็ซฏ */\r\n if (props.markAsRead) {\r\n try {\r\n await props.markAsRead(ids);\r\n } catch {\r\n /* ๅๆป */\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"unread\";\r\n }\r\n }\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๆ ่ฎฐๅ
จ้จๅทฒ่ฏป\r\n */\r\n async function markAllAsRead(category?: NotificationCategory) {\r\n const targetMessages = category\r\n ? messages.value.filter(\r\n (m) => m.category === category && m.status === \"unread\",\r\n )\r\n : messages.value.filter((m) => m.status === \"unread\");\r\n\r\n /* ไน่งๆดๆฐ */\r\n for (const msg of targetMessages) {\r\n msg.status = \"read\";\r\n }\r\n persistToCache();\r\n\r\n if (props.markAllRead) {\r\n try {\r\n await props.markAllRead(category);\r\n } catch {\r\n /* ๅๆป */\r\n for (const msg of targetMessages) {\r\n msg.status = \"unread\";\r\n }\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๅ ้คๆถๆฏ\r\n */\r\n async function deleteMessages(ids: string[]) {\r\n const idSet = new Set(ids);\r\n const backup = [...messages.value];\r\n\r\n /* ไน่งๆดๆฐ */\r\n messages.value = messages.value.filter((m) => !idSet.has(m.id));\r\n total.value = Math.max(0, total.value - ids.length);\r\n persistToCache();\r\n\r\n if (props.deleteNotification) {\r\n try {\r\n await props.deleteNotification(ids);\r\n } catch {\r\n messages.value = backup;\r\n total.value += ids.length;\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๆธ
็ฉบๆถๆฏ\r\n */\r\n async function clearMessages(category?: NotificationCategory) {\r\n const backup = [...messages.value];\r\n const backupTotal = total.value;\r\n\r\n if (category) {\r\n messages.value = messages.value.filter((m) => m.category !== category);\r\n } else {\r\n messages.value = [];\r\n }\r\n total.value = messages.value.length;\r\n persistToCache();\r\n\r\n if (props.clearNotifications) {\r\n try {\r\n await props.clearNotifications(category);\r\n } catch {\r\n messages.value = backup;\r\n total.value = backupTotal;\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /* โโโ WebSocket ๆกฅๆฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅค็ WebSocket ๆจ้ๆถๆฏ\r\n */\r\n function handleWSMessage(payload: WSNotificationPayload) {\r\n switch (payload.type) {\r\n case \"new_message\": {\r\n const msg = payload.data as NotificationMessage;\r\n /* ๅป้ๅๆๅ
ฅๅฐๅ่กจๅคด้จ */\r\n if (!messages.value.some((m) => m.id === msg.id)) {\r\n messages.value.unshift(msg);\r\n total.value++;\r\n persistToCache();\r\n showDesktopNotification(msg);\r\n }\r\n break;\r\n }\r\n case \"read_sync\": {\r\n const syncMessages = payload.data as NotificationMessage[];\r\n const readIds = new Set(syncMessages.map((m) => m.id));\r\n for (const msg of messages.value) {\r\n if (readIds.has(msg.id)) msg.status = \"read\";\r\n }\r\n persistToCache();\r\n break;\r\n }\r\n case \"count_update\": {\r\n /* ๆๅก็ซฏๆจ้็ๆช่ฏปๆฐๅฏ็จไบๆ กๅ */\r\n break;\r\n }\r\n }\r\n }\r\n\r\n /** WebSocket ่ฟๆฅ็ถๆๅๆดๅ่ฐ */\r\n function handleWSStatusChange(s: WSConnectionStatus) {\r\n wsStatus.value = s;\r\n }\r\n\r\n const { connect: wsConnect, disconnect: wsDisconnect } = useNotificationWS(\r\n handleWSMessage,\r\n handleWSStatusChange,\r\n );\r\n\r\n /* โโโ ๆก้ข้็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅ้ๆก้ข้็ฅ\r\n */\r\n function showDesktopNotification(msg: NotificationMessage) {\r\n if (!props.desktopNotification) return;\r\n if (!(\"Notification\" in window)) return;\r\n\r\n if (Notification.permission === \"granted\") {\r\n createDesktopNotification(msg);\r\n } else if (Notification.permission !== \"denied\") {\r\n Notification.requestPermission().then((permission) => {\r\n if (permission === \"granted\") {\r\n createDesktopNotification(msg);\r\n }\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * ๅๅปบๆก้ข้็ฅๅฎไพ\r\n */\r\n function createDesktopNotification(msg: NotificationMessage) {\r\n const n = new Notification(msg.title, {\r\n body: msg.summary,\r\n icon: msg.sender?.avatar || \"/robot-avatar.png\",\r\n tag: msg.id,\r\n });\r\n\r\n n.addEventListener(\"click\", () => {\r\n window.focus();\r\n selectMessage(msg);\r\n n.close();\r\n });\r\n\r\n /* 5 ็งๅ่ชๅจๅ
ณ้ญ */\r\n setTimeout(() => n.close(), 5000);\r\n }\r\n\r\n /* โโโ ่ฝฎ่ฏข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n let pollingTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n /**\r\n * ๅฏๅจ่ฝฎ่ฏข\r\n */\r\n function startPolling() {\r\n stopPolling();\r\n const interval = props.pollingInterval ?? DEFAULT_POLLING_INTERVAL;\r\n if (interval <= 0) return;\r\n\r\n pollingTimer = setInterval(() => {\r\n fetchMessages(true);\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅๆญข่ฝฎ่ฏข\r\n */\r\n function stopPolling() {\r\n if (pollingTimer) {\r\n clearInterval(pollingTimer);\r\n pollingTimer = null;\r\n }\r\n }\r\n\r\n /* โโโ ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ้ไธญ/ๆฅ็ๆถๆฏ\r\n */\r\n function selectMessage(msg: NotificationMessage) {\r\n selectedMessage.value = msg;\r\n /* ่ชๅจๆ ่ฎฐไธบๅทฒ่ฏป */\r\n if (msg.status === \"unread\") {\r\n markAsRead([msg.id]);\r\n }\r\n }\r\n\r\n /**\r\n * ่ฟๅๅ่กจ\r\n */\r\n function clearSelection() {\r\n selectedMessage.value = null;\r\n }\r\n\r\n /**\r\n * ๅๆขๅ็ฑป\r\n *\r\n * ็บฏๅฎขๆท็ซฏ่ฟๆปค๏ผMock / ๅทฒๅ ่ฝฝๆฐๆฎ๏ผ็ดๆฅๅๆข๏ผ\r\n * API ๆจกๅผๅ้ๆฐๆๅๅฏนๅบๅ็ฑปๆฐๆฎใ\r\n */\r\n function switchCategory(category: NotificationCategory | \"all\") {\r\n activeCategory.value = category;\r\n selectedMessage.value = null;\r\n\r\n /* ไป
API ๆจกๅผ้่ฆ้ๆฐๆๅ */\r\n if (props.fetchNotifications) {\r\n fetchMessages(true);\r\n }\r\n }\r\n\r\n /* โโโ ๅๅงๅ & ๆธ
็ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅๅงๅ\r\n */\r\n function init() {\r\n fetchMessages(true);\r\n startPolling();\r\n\r\n /* ๅฆๆ้
็ฝฎไบ WebSocket โ ๅปบ่ฟ */\r\n if (props.wsConfig) wsConnect(props.wsConfig);\r\n }\r\n\r\n /**\r\n * ้ๆฏ\r\n */\r\n function destroy() {\r\n stopPolling();\r\n wsDisconnect();\r\n if (persistTimer) {\r\n clearTimeout(persistTimer);\r\n persistTimer = null;\r\n }\r\n }\r\n\r\n onMounted(init);\r\n onBeforeUnmount(destroy);\r\n\r\n return {\r\n /* ็ถๆ */\r\n messages,\r\n activeCategory,\r\n loading,\r\n total,\r\n page,\r\n selectedMessage,\r\n popoverVisible,\r\n wsStatus,\r\n\r\n /* ่ฎก็ฎ */\r\n unreadCount,\r\n unreadByCategory,\r\n filteredMessages,\r\n hasMore,\r\n\r\n /* ๆไฝ */\r\n fetchMessages,\r\n loadMore,\r\n markAsRead,\r\n markAllAsRead,\r\n deleteMessages,\r\n clearMessages,\r\n selectMessage,\r\n clearSelection,\r\n switchCategory,\r\n\r\n /* WebSocket */\r\n connectWS: wsConnect,\r\n disconnectWS: wsDisconnect,\r\n };\r\n}\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅๆถ้ดๆ ผๅผๅ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\n/** ๆถ้ดๅไฝ้ๅผ */\r\nconst MINUTE = 60_000;\r\nconst HOUR = 3_600_000;\r\nconst DAY = 86_400_000;\r\nconst WEEK = 7 * DAY;\r\nconst MONTH = 30 * DAY;\r\nconst YEAR = 365 * DAY;\r\n\r\n/** ๅไพ็ผๅญ */\r\nlet _instance: ReturnType<typeof createNotificationFormat> | null = null;\r\n\r\n/**\r\n * ้็ฅๆถ้ดๆ ผๅผๅ๏ผๅไพ๏ผ\r\n *\r\n * ๅฐ ISO ๆถ้ดๆณๆ ผๅผๅไธบไบบ็ฑปๅๅฅฝ็็ธๅฏนๆถ้ดๆ่ฟฐ๏ผ\r\n * ๅฆ \"ๅๅ\"ใ\"5ๅ้ๅ\"ใ\"ๆจๅคฉ 14:30\" ็ญใ\r\n */\r\nexport function useNotificationFormat() {\r\n if (!_instance) _instance = createNotificationFormat();\r\n return _instance;\r\n}\r\n\r\n/**\r\n * ๅๅปบๆ ผๅผๅๅทฅๅ
ทๅฎไพ\r\n */\r\nfunction createNotificationFormat() {\r\n /**\r\n * ๆ ผๅผๅ็ธๅฏนๆถ้ด\r\n * @param timestamp ISO 8601 ๆถ้ดๅญ็ฌฆไธฒ\r\n */\r\n function formatRelativeTime(timestamp: string): string {\r\n const date = new Date(timestamp);\r\n const now = Date.now();\r\n const diff = now - date.getTime();\r\n\r\n if (diff < 0) return \"ๅๅ\";\r\n if (diff < MINUTE) return \"ๅๅ\";\r\n if (diff < HOUR) return `${Math.floor(diff / MINUTE)}ๅ้ๅ`;\r\n if (diff < DAY) return `${Math.floor(diff / HOUR)}ๅฐๆถๅ`;\r\n\r\n /* ๅคๆญๆฏๅฆๆฏๆจๅคฉ */\r\n const today = new Date();\r\n today.setHours(0, 0, 0, 0);\r\n const yesterday = new Date(today.getTime() - DAY);\r\n\r\n if (date >= yesterday && date < today) {\r\n return `ๆจๅคฉ ${padTime(date.getHours())}:${padTime(date.getMinutes())}`;\r\n }\r\n\r\n if (diff < WEEK) return `${Math.floor(diff / DAY)}ๅคฉๅ`;\r\n if (diff < MONTH) return `${Math.floor(diff / WEEK)}ๅจๅ`;\r\n if (diff < YEAR) return `${Math.floor(diff / MONTH)}ไธชๆๅ`;\r\n\r\n return formatFullDate(date);\r\n }\r\n\r\n /**\r\n * ๆ ผๅผๅๅฎๆดๆฅๆๆถ้ด\r\n * @param date Date ๅฏน่ฑก\r\n */\r\n function formatFullDate(date: Date): string {\r\n const y = date.getFullYear();\r\n const m = padTime(date.getMonth() + 1);\r\n const d = padTime(date.getDate());\r\n const h = padTime(date.getHours());\r\n const min = padTime(date.getMinutes());\r\n return `${y}-${m}-${d} ${h}:${min}`;\r\n }\r\n\r\n /**\r\n * ่กฅ้ถ\r\n * @param n ๆฐๅญ\r\n */\r\n function padTime(n: number): string {\r\n return n < 10 ? `0${n}` : String(n);\r\n }\r\n\r\n return {\r\n formatRelativeTime,\r\n formatFullDate,\r\n };\r\n}\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_NotificationCenter2.js","names":["$emit","count","hasUrgent","message","$emit","activeCategory","$emit","messages","scrollHeight","loading","hasMore","$emit","message","scrollHeight"],"sources":["../src/components/C_NotificationCenter/constants.ts","../src/components/C_NotificationCenter/composables/useNotificationWS.ts","../src/components/C_NotificationCenter/composables/useNotificationCore.ts","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/components/NotificationBadge.vue","../src/components/C_NotificationCenter/composables/useNotificationFormat.ts","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/components/NotificationDetail.vue","../src/components/C_NotificationCenter/index.vue","../src/components/C_NotificationCenter/index.vue","../src/components/C_NotificationCenter/index.vue"],"sourcesContent":["/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟๅธธ้้
็ฝฎ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport type { NotificationCategory, NotificationPriority } from \"./types\";\r\n\r\n/* โโโ ้ป่ฎค้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ้ป่ฎค่งๆ ๆๅคงๆพ็คบๆฐ */\r\nexport const DEFAULT_MAX_BADGE_COUNT = 99;\r\n\r\n/** ้ป่ฎคๆฏ้กตๆกๆฐ */\r\nexport const DEFAULT_PAGE_SIZE = 20;\r\n\r\n/** ้ป่ฎค่ฝฎ่ฏข้ด้๏ผms๏ผ๏ผ60 ็ง */\r\nexport const DEFAULT_POLLING_INTERVAL = 60_000;\r\n\r\n/** ้ป่ฎค็ผๅญ key */\r\nexport const DEFAULT_STORAGE_KEY = \"notification_center\";\r\n\r\n/** WebSocket ้ป่ฎค้่ฟ้ด้๏ผms๏ผ */\r\nexport const DEFAULT_WS_RECONNECT_INTERVAL = 3_000;\r\n\r\n/** WebSocket ้ป่ฎคๆๅคง้่ฟๆฌกๆฐ */\r\nexport const DEFAULT_WS_MAX_RECONNECT = 5;\r\n\r\n/** WebSocket ้ป่ฎคๅฟ่ทณ้ด้๏ผms๏ผ */\r\nexport const DEFAULT_WS_HEARTBEAT_INTERVAL = 30_000;\r\n\r\n/** WebSocket ้ป่ฎคๅฟ่ทณๆถๆฏ */\r\nexport const DEFAULT_WS_HEARTBEAT_MESSAGE = \"ping\";\r\n\r\n/* โโโ ๅ็ฑป้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๅ็ฑปๆ ็ญพๆ ๅฐ */\r\nexport const CATEGORY_MAP: Record<\r\n NotificationCategory,\r\n {\r\n label: string;\r\n icon: string;\r\n color: string;\r\n }\r\n> = {\r\n system: {\r\n label: \"็ณป็ป้็ฅ\",\r\n icon: \"mdi:cog-outline\",\r\n color: \"info\",\r\n },\r\n business: {\r\n label: \"ไธๅกๆถๆฏ\",\r\n icon: \"mdi:briefcase-outline\",\r\n color: \"success\",\r\n },\r\n alarm: {\r\n label: \"ๅ่ญฆ้ข่ญฆ\",\r\n icon: \"mdi:alert-outline\",\r\n color: \"warning\",\r\n },\r\n};\r\n\r\n/** ๅ็ฑป Tab ๅ่กจ */\r\nexport const CATEGORY_TABS: {\r\n key: NotificationCategory | \"all\";\r\n label: string;\r\n}[] = [\r\n { key: \"all\", label: \"ๅ
จ้จ\" },\r\n { key: \"system\", label: \"็ณป็ป้็ฅ\" },\r\n { key: \"business\", label: \"ไธๅกๆถๆฏ\" },\r\n { key: \"alarm\", label: \"ๅ่ญฆ้ข่ญฆ\" },\r\n];\r\n\r\n/* โโโ ไผๅ
็บง้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ไผๅ
็บงๆ ็ญพๆ ๅฐ */\r\nexport const PRIORITY_MAP: Record<\r\n NotificationPriority,\r\n {\r\n label: string;\r\n type: \"default\" | \"info\" | \"success\" | \"warning\" | \"error\";\r\n }\r\n> = {\r\n low: { label: \"ไฝ\", type: \"default\" },\r\n normal: { label: \"ๆฎ้\", type: \"info\" },\r\n high: { label: \"้่ฆ\", type: \"warning\" },\r\n urgent: { label: \"็ดงๆฅ\", type: \"error\" },\r\n};\r\n\r\n/* โโโ ๆจกๆๆฐๆฎ๏ผๅผๅ็ฏๅขไฝฟ็จ๏ผ โโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆจกๆๆถๆฏๅ่กจ */\r\nexport const MOCK_MESSAGES = [\r\n {\r\n id: \"msg-001\",\r\n title: \"็ณป็ป็ปดๆค้็ฅ\",\r\n summary:\r\n \"็ณป็ปๅฐไบไปๆ 22:00-23:00 ่ฟ่กไพ่ก็ปดๆคๅ็บง๏ผๅฑๆถ้จๅๅ่ฝๅฏ่ฝๆๆถไธๅฏ็จใ\",\r\n content:\r\n \"<p>ๅฐๆฌ็็จๆท๏ผ</p><p>ไธบไบๆๅ็ณป็ปๆง่ฝๅ็จณๅฎๆง๏ผๆไปฌ่ฎกๅไบ <strong>2026ๅนด2ๆ27ๆฅ 22:00-23:00</strong> ๅฏน็ณป็ป่ฟ่กไพ่ก็ปดๆคๅ็บงใ</p><p>็ปดๆคๆ้ด๏ผไปฅไธๅ่ฝๅฏ่ฝๆๆถไธๅฏ็จ๏ผ</p><ul><li>ๆฅ่กจๅฏผๅบ</li><li>ๆฐๆฎๅๆญฅ</li><li>ๅฎๆถไปปๅก</li></ul><p>่ฏทๆๅไฟๅญๅฅฝๆชๅฎๆ็ๅทฅไฝ๏ผๆ่ฐขๆจ็็่งฃไธๆฏๆ๏ผ</p>\",\r\n category: \"system\" as const,\r\n priority: \"high\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 15 * 60_000).toISOString(),\r\n sender: { name: \"็ณป็ป็ฎก็ๅ\", avatar: \"\" },\r\n },\r\n {\r\n id: \"msg-002\",\r\n title: \"ๅฎกๆนๆต็จๅพ
ๅค็\",\r\n summary: \"ๅผ ไธๆไบค็ใQ1 ๅญฃๅบฆ้ข็ฎ็ณ่ฏทใ้่ฆๆจๅฎกๆน๏ผ่ฏทๅฐฝๅฟซๅค็ใ\",\r\n category: \"business\" as const,\r\n priority: \"urgent\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 30 * 60_000).toISOString(),\r\n sender: {\r\n name: \"ๅผ ไธ\",\r\n avatar:\r\n \"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png\",\r\n },\r\n actionUrl: \"/workflow/approval\",\r\n actionText: \"ๅปๅฎกๆน\",\r\n },\r\n {\r\n id: \"msg-003\",\r\n title: \"ๆๅกๅจ CPU ไฝฟ็จ็ๅ่ญฆ\",\r\n summary:\r\n \"็ไบง็ฏๅข่็น prod-node-03 CPU ไฝฟ็จ็ๆ็ปญ่ถ
่ฟ 90%๏ผๅทฒ่งฆๅๅ่ญฆ้ๅผใ\",\r\n category: \"alarm\" as const,\r\n priority: \"urgent\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 2 * 60_000).toISOString(),\r\n sender: { name: \"็ๆงไธญๅฟ\" },\r\n actionUrl: \"/monitor/server\",\r\n actionText: \"ๆฅ็่ฏฆๆ
\",\r\n },\r\n {\r\n id: \"msg-004\",\r\n title: \"ๆฐ็ๆฌๅๅธ v1.14.0\",\r\n summary: \"็ณป็ปๅทฒๅ็บง่ณ v1.14.0๏ผๆฐๅข้็ฅไธญๅฟใๅทฅไฝๆตๅขๅผบ็ญๅค้กนๅ่ฝใ\",\r\n category: \"system\" as const,\r\n priority: \"normal\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 3 * 3_600_000).toISOString(),\r\n sender: { name: \"็ณป็ป็ฎก็ๅ\" },\r\n },\r\n {\r\n id: \"msg-005\",\r\n title: \"้กน็ฎไบคไปๆ้\",\r\n summary: \"ใRobot Admin ไบๆใ้กน็ฎไบคไปๆชๆญขๆฅๆไธบ 3ๆ15ๆฅ๏ผๅฝๅ่ฟๅบฆ 78%ใ\",\r\n category: \"business\" as const,\r\n priority: \"high\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 8 * 3_600_000).toISOString(),\r\n sender: { name: \"้กน็ฎ็ฎก็็ณป็ป\" },\r\n },\r\n {\r\n id: \"msg-006\",\r\n title: \"ๆฐๆฎๅบ่ฟๆฅๆฑ ๅ่ญฆ\",\r\n summary: \"ๆฐๆฎๅบ่ฟๆฅๆฑ ไฝฟ็จ็่พพๅฐ 85%๏ผๅปบ่ฎฎๅ
ณๆณจๅนถ้ๆถๆฉๅฎนใ\",\r\n category: \"alarm\" as const,\r\n priority: \"high\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 12 * 3_600_000).toISOString(),\r\n sender: { name: \"็ๆงไธญๅฟ\" },\r\n },\r\n {\r\n id: \"msg-007\",\r\n title: \"ๅจๆฅๆไบคๆ้\",\r\n summary: \"ๆฌๅจๅจๆฅๆไบคๆชๆญขๆถ้ดไธบๅจไบ 18:00๏ผ่ฏทๅๆถๆไบคใ\",\r\n category: \"business\" as const,\r\n priority: \"normal\" as const,\r\n status: \"read\" as const,\r\n timestamp: new Date(Date.now() - 24 * 3_600_000).toISOString(),\r\n sender: { name: \"OA ็ณป็ป\" },\r\n },\r\n {\r\n id: \"msg-008\",\r\n title: \"่ดฆๅทๅฎๅ
จๆ้\",\r\n summary: \"ๆฃๆตๅฐๆจ็่ดฆๅทๅจๆฐ่ฎพๅคไธ็ปๅฝ๏ผๅฆ้ๆฌไบบๆไฝ่ฏทๅๆถไฟฎๆนๅฏ็ ใ\",\r\n category: \"system\" as const,\r\n priority: \"high\" as const,\r\n status: \"unread\" as const,\r\n timestamp: new Date(Date.now() - 45 * 60_000).toISOString(),\r\n sender: { name: \"ๅฎๅ
จไธญๅฟ\" },\r\n },\r\n];\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: WebSocket ่ฟๆฅ็ฎก็\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, readonly, onBeforeUnmount } from \"vue\";\r\nimport type {\r\n NotificationWSConfig,\r\n WSConnectionStatus,\r\n WSNotificationPayload,\r\n} from \"../types\";\r\nimport {\r\n DEFAULT_WS_RECONNECT_INTERVAL,\r\n DEFAULT_WS_MAX_RECONNECT,\r\n DEFAULT_WS_HEARTBEAT_INTERVAL,\r\n DEFAULT_WS_HEARTBEAT_MESSAGE,\r\n} from \"../constants\";\r\n\r\n/**\r\n * WebSocket ่ฟๆฅ็ฎก็\r\n *\r\n * ็ฎก็ WebSocket ็ๅปบ่ฟใ้่ฟใๅฟ่ทณไฟๆดปใๆถๆฏ่งฃๆใ\r\n * ๆฏๆ่ชๅจ้่ฟใ้ดๆ tokenใๅฟ่ทณๆฃๆตใ\r\n */\r\nexport function useNotificationWS(\r\n onMessage: (payload: WSNotificationPayload) => void,\r\n onStatusChange?: (status: WSConnectionStatus) => void,\r\n) {\r\n /** ่ฟๆฅ็ถๆ */\r\n const status = ref<WSConnectionStatus>(\"disconnected\");\r\n\r\n /** WebSocket ๅฎไพ */\r\n let ws: WebSocket | null = null;\r\n /** ้่ฟ่ฎกๆฐๅจ */\r\n let reconnectCount = 0;\r\n /** ้่ฟๅฎๆถๅจ */\r\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n /** ๅฟ่ทณๅฎๆถๅจ */\r\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\r\n /** ๅฝๅ้
็ฝฎ */\r\n let currentConfig: NotificationWSConfig | null = null;\r\n\r\n /**\r\n * ๆดๆฐ่ฟๆฅ็ถๆ\r\n */\r\n function setStatus(s: WSConnectionStatus) {\r\n status.value = s;\r\n onStatusChange?.(s);\r\n }\r\n\r\n /**\r\n * ๅปบ็ซ่ฟๆฅ\r\n */\r\n function connect(config: NotificationWSConfig) {\r\n currentConfig = config;\r\n reconnectCount = 0;\r\n createConnection(config);\r\n }\r\n\r\n /**\r\n * ๅๅปบ WebSocket ่ฟๆฅ\r\n */\r\n function createConnection(config: NotificationWSConfig) {\r\n cleanup();\r\n\r\n const url = buildUrl(config);\r\n setStatus(\"connecting\");\r\n\r\n try {\r\n ws = new WebSocket(url);\r\n } catch {\r\n setStatus(\"disconnected\");\r\n scheduleReconnect(config);\r\n return;\r\n }\r\n\r\n ws.addEventListener(\"open\", () => {\r\n setStatus(\"connected\");\r\n reconnectCount = 0;\r\n startHeartbeat(config);\r\n });\r\n\r\n ws.addEventListener(\"message\", (event) => {\r\n handleMessage(event.data);\r\n });\r\n\r\n ws.addEventListener(\"close\", () => {\r\n stopHeartbeat();\r\n setStatus(\"disconnected\");\r\n\r\n if (config.autoReconnect !== false) {\r\n scheduleReconnect(config);\r\n }\r\n });\r\n\r\n ws.addEventListener(\"error\", () => {\r\n /* error ไบไปถๅ้ๅธธ็ดงๆฅ close ไบไปถ๏ผไบค็ฑ close ๅค็้่ฟ */\r\n });\r\n }\r\n\r\n /**\r\n * ๆๅปบๅธฆ token ็ WebSocket URL\r\n */\r\n function buildUrl(config: NotificationWSConfig): string {\r\n const token = config.getToken?.();\r\n if (token) {\r\n const separator = config.url.includes(\"?\") ? \"&\" : \"?\";\r\n return `${config.url}${separator}token=${encodeURIComponent(token)}`;\r\n }\r\n return config.url;\r\n }\r\n\r\n /**\r\n * ๅค็ๆฅๆถๅฐ็ๆถๆฏ\r\n */\r\n function handleMessage(raw: string | ArrayBuffer | Blob) {\r\n if (typeof raw !== \"string\") return;\r\n\r\n /* ่ฟๆปคๅฟ่ทณๅๅบ */\r\n if (raw === \"pong\") return;\r\n\r\n try {\r\n const payload = JSON.parse(raw) as WSNotificationPayload;\r\n onMessage(payload);\r\n } catch {\r\n /* ้ๆ ๅ JSON ๆถๆฏ๏ผๅฟฝ็ฅ */\r\n }\r\n }\r\n\r\n /**\r\n * ่ฐๅบฆ้่ฟ\r\n */\r\n function scheduleReconnect(config: NotificationWSConfig) {\r\n const maxAttempts = config.maxReconnectAttempts ?? DEFAULT_WS_MAX_RECONNECT;\r\n if (reconnectCount >= maxAttempts) return;\r\n\r\n reconnectCount++;\r\n setStatus(\"reconnecting\");\r\n\r\n const interval = config.reconnectInterval ?? DEFAULT_WS_RECONNECT_INTERVAL;\r\n reconnectTimer = setTimeout(() => {\r\n createConnection(config);\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅฏๅจๅฟ่ทณ\r\n */\r\n function startHeartbeat(config: NotificationWSConfig) {\r\n const interval = config.heartbeatInterval ?? DEFAULT_WS_HEARTBEAT_INTERVAL;\r\n if (interval <= 0) return;\r\n\r\n const message = config.heartbeatMessage ?? DEFAULT_WS_HEARTBEAT_MESSAGE;\r\n heartbeatTimer = setInterval(() => {\r\n if (ws?.readyState === WebSocket.OPEN) {\r\n ws.send(message);\r\n }\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅๆญขๅฟ่ทณ\r\n */\r\n function stopHeartbeat() {\r\n if (heartbeatTimer) {\r\n clearInterval(heartbeatTimer);\r\n heartbeatTimer = null;\r\n }\r\n }\r\n\r\n /**\r\n * ๆธ
็่ตๆบ\r\n */\r\n function cleanup() {\r\n stopHeartbeat();\r\n if (reconnectTimer) {\r\n clearTimeout(reconnectTimer);\r\n reconnectTimer = null;\r\n }\r\n if (ws) {\r\n ws.close();\r\n ws = null;\r\n }\r\n }\r\n\r\n /**\r\n * ๆญๅผ่ฟๆฅ\r\n */\r\n function disconnect() {\r\n currentConfig = null;\r\n reconnectCount = Infinity;\r\n cleanup();\r\n setStatus(\"disconnected\");\r\n }\r\n\r\n /**\r\n * ๆๅจ้ๆฐ่ฟๆฅ\r\n */\r\n function reconnect() {\r\n if (currentConfig) {\r\n reconnectCount = 0;\r\n createConnection(currentConfig);\r\n }\r\n }\r\n\r\n /* ็ปไปถๅธ่ฝฝๆถ่ชๅจๆธ
็ */\r\n onBeforeUnmount(cleanup);\r\n\r\n return {\r\n /** WebSocket ่ฟๆฅ็ถๆ */\r\n status: readonly(status),\r\n connect,\r\n disconnect,\r\n reconnect,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟๆ ธๅฟ็ถๆ็ฎก็\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, computed, onMounted, onBeforeUnmount } from \"vue\";\r\nimport type {\r\n NotificationMessage,\r\n NotificationCategory,\r\n NotificationCenterProps,\r\n WSNotificationPayload,\r\n WSConnectionStatus,\r\n} from \"../types\";\r\nimport {\r\n DEFAULT_PAGE_SIZE,\r\n DEFAULT_POLLING_INTERVAL,\r\n DEFAULT_STORAGE_KEY,\r\n MOCK_MESSAGES,\r\n} from \"../constants\";\r\nimport { useNotificationWS } from \"./useNotificationWS\";\r\nimport { setItem, getItem } from \"../../../utils/storage\";\r\n\r\n/** ็ผๅญๆฐๆฎ็ปๆ */\r\ninterface CachedState {\r\n unreadIds: string[];\r\n lastFetchTime: number;\r\n}\r\n\r\n/** ้ฒๆๆไน
ๅๅปถ่ฟ๏ผms๏ผ */\r\nconst PERSIST_DEBOUNCE = 300;\r\n\r\n/**\r\n * ้็ฅไธญๅฟๆ ธๅฟ็ถๆ็ฎก็\r\n *\r\n * ็ปไธ็ฎก็ๆถๆฏๅ่กจใๆช่ฏปๆฐใๅ็ฑป่ฟๆปคใๅทฒ่ฏปๆ ่ฎฐใ\r\n * API ไบคไบใWebSocket ๆกฅๆฅใ่ฝฎ่ฏข่ฐๅบฆๅๆฌๅฐ็ผๅญใ\r\n */\r\nexport function useNotificationCore(props: NotificationCenterProps) {\r\n /* โโโ ๅๅบๅผ็ถๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ๆถๆฏๅ่กจ */\r\n const messages = ref<NotificationMessage[]>([]);\r\n /** ๅฝๅ้ไธญๅ็ฑป */\r\n const activeCategory = ref<NotificationCategory | \"all\">(\"all\");\r\n /** ๆฏๅฆๆญฃๅจๅ ่ฝฝ */\r\n const loading = ref(false);\r\n /** ๆปๆกๆฐ */\r\n const total = ref(0);\r\n /** ๅฝๅ้กต็ */\r\n const page = ref(1);\r\n /** ้ไธญ็ๆถๆฏ๏ผ่ฏฆๆ
ๅฑ็คบ๏ผ */\r\n const selectedMessage = ref<NotificationMessage | null>(null);\r\n /** Popover ๅฑๅผ็ถๆ */\r\n const popoverVisible = ref(false);\r\n /** WebSocket ่ฟๆฅ็ถๆ */\r\n const wsStatus = ref<WSConnectionStatus>(\"disconnected\");\r\n\r\n /* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ๆช่ฏปๆถๆฏๆปๆฐ */\r\n const unreadCount = computed(\r\n () => messages.value.filter((m) => m.status === \"unread\").length,\r\n );\r\n\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n const unreadByCategory = computed(() => {\r\n const counts: Record<string, number> = { system: 0, business: 0, alarm: 0 };\r\n for (const m of messages.value) {\r\n if (m.status === \"unread\" && m.category in counts) {\r\n counts[m.category]++;\r\n }\r\n }\r\n return counts;\r\n });\r\n\r\n /** ๅฝๅๅ็ฑปไธ็ๆถๆฏๅ่กจ */\r\n const filteredMessages = computed(() => {\r\n if (activeCategory.value === \"all\") return messages.value;\r\n return messages.value.filter((m) => m.category === activeCategory.value);\r\n });\r\n\r\n /** ๆฏๅฆๆๆดๅคๆถๆฏๅฏๅ ่ฝฝ */\r\n const hasMore = computed(() => messages.value.length < total.value);\r\n\r\n /* โโโ ็ผๅญ็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /** ็ผๅญ key */\r\n const storageKey = computed(() => props.storageKey ?? DEFAULT_STORAGE_KEY);\r\n\r\n /**\r\n * ไป็ผๅญๆขๅคๆช่ฏป็ถๆ\r\n */\r\n function restoreFromCache() {\r\n const cached = getItem<CachedState>(storageKey.value);\r\n if (cached?.unreadIds) {\r\n const idSet = new Set(cached.unreadIds);\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"unread\";\r\n }\r\n }\r\n }\r\n }\r\n\r\n /** ้ฒๆๆไน
ๅๅฎๆถๅจ */\r\n let persistTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * ๆไน
ๅๆช่ฏป็ถๆๅฐๆฌๅฐ็ผๅญ๏ผ้ฒๆ๏ผ\r\n */\r\n function persistToCache() {\r\n if (persistTimer) clearTimeout(persistTimer);\r\n persistTimer = setTimeout(() => {\r\n const unreadIds = messages.value\r\n .filter((m) => m.status === \"unread\")\r\n .map((m) => m.id);\r\n setItem<CachedState>(storageKey.value, {\r\n unreadIds,\r\n lastFetchTime: Date.now(),\r\n });\r\n persistTimer = null;\r\n }, PERSIST_DEBOUNCE);\r\n }\r\n\r\n /* โโโ API ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๆๅๆถๆฏๅ่กจ\r\n */\r\n async function fetchMessages(reset = false) {\r\n if (reset) {\r\n page.value = 1;\r\n }\r\n\r\n loading.value = true;\r\n try {\r\n if (props.fetchNotifications) {\r\n const categoryParam =\r\n activeCategory.value === \"all\" ? undefined : activeCategory.value;\r\n const pageSize = props.pageSize ?? DEFAULT_PAGE_SIZE;\r\n const result = await props.fetchNotifications({\r\n category: categoryParam,\r\n page: page.value,\r\n pageSize,\r\n });\r\n if (reset) {\r\n messages.value = result.list;\r\n } else {\r\n messages.value.push(...result.list);\r\n }\r\n total.value = result.total;\r\n } else {\r\n /* ๆช้
็ฝฎ API ๆฅๅฃ โ ไฝฟ็จๆจกๆๆฐๆฎ๏ผๅ
จ้ๅ ่ฝฝ๏ผๅฎขๆท็ซฏ่ฟๆปค๏ผ */\r\n loadMockData();\r\n }\r\n\r\n restoreFromCache();\r\n } finally {\r\n loading.value = false;\r\n }\r\n }\r\n\r\n /**\r\n * ๅ ่ฝฝๆจกๆๆฐๆฎ๏ผๅ
จ้ๅ ่ฝฝ๏ผfilteredMessages ่ด่ดฃๅฎขๆท็ซฏ่ฟๆปค๏ผ\r\n */\r\n function loadMockData() {\r\n messages.value = MOCK_MESSAGES.map((m) => ({ ...m }));\r\n total.value = MOCK_MESSAGES.length;\r\n }\r\n\r\n /**\r\n * ๅ ่ฝฝๆดๅค\r\n */\r\n async function loadMore() {\r\n if (!hasMore.value || loading.value) return;\r\n page.value++;\r\n await fetchMessages();\r\n }\r\n\r\n /**\r\n * ๆ ่ฎฐๆๅฎๆถๆฏไธบๅทฒ่ฏป\r\n */\r\n async function markAsRead(ids: string[]) {\r\n const idSet = new Set(ids);\r\n\r\n /* ไน่งๆดๆฐ */\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"read\";\r\n }\r\n }\r\n persistToCache();\r\n\r\n /* ๅฆๆ้
็ฝฎไบ API โ ๅๆญฅๅฐๆๅก็ซฏ */\r\n if (props.markAsRead) {\r\n try {\r\n await props.markAsRead(ids);\r\n } catch {\r\n /* ๅๆป */\r\n for (const msg of messages.value) {\r\n if (idSet.has(msg.id)) {\r\n msg.status = \"unread\";\r\n }\r\n }\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๆ ่ฎฐๅ
จ้จๅทฒ่ฏป\r\n */\r\n async function markAllAsRead(category?: NotificationCategory) {\r\n const targetMessages = category\r\n ? messages.value.filter(\r\n (m) => m.category === category && m.status === \"unread\",\r\n )\r\n : messages.value.filter((m) => m.status === \"unread\");\r\n\r\n /* ไน่งๆดๆฐ */\r\n for (const msg of targetMessages) {\r\n msg.status = \"read\";\r\n }\r\n persistToCache();\r\n\r\n if (props.markAllRead) {\r\n try {\r\n await props.markAllRead(category);\r\n } catch {\r\n /* ๅๆป */\r\n for (const msg of targetMessages) {\r\n msg.status = \"unread\";\r\n }\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๅ ้คๆถๆฏ\r\n */\r\n async function deleteMessages(ids: string[]) {\r\n const idSet = new Set(ids);\r\n const backup = [...messages.value];\r\n\r\n /* ไน่งๆดๆฐ */\r\n messages.value = messages.value.filter((m) => !idSet.has(m.id));\r\n total.value = Math.max(0, total.value - ids.length);\r\n persistToCache();\r\n\r\n if (props.deleteNotification) {\r\n try {\r\n await props.deleteNotification(ids);\r\n } catch {\r\n messages.value = backup;\r\n total.value += ids.length;\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * ๆธ
็ฉบๆถๆฏ\r\n */\r\n async function clearMessages(category?: NotificationCategory) {\r\n const backup = [...messages.value];\r\n const backupTotal = total.value;\r\n\r\n if (category) {\r\n messages.value = messages.value.filter((m) => m.category !== category);\r\n } else {\r\n messages.value = [];\r\n }\r\n total.value = messages.value.length;\r\n persistToCache();\r\n\r\n if (props.clearNotifications) {\r\n try {\r\n await props.clearNotifications(category);\r\n } catch {\r\n messages.value = backup;\r\n total.value = backupTotal;\r\n persistToCache();\r\n }\r\n }\r\n }\r\n\r\n /* โโโ WebSocket ๆกฅๆฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅค็ WebSocket ๆจ้ๆถๆฏ\r\n */\r\n function handleWSMessage(payload: WSNotificationPayload) {\r\n switch (payload.type) {\r\n case \"new_message\": {\r\n const msg = payload.data as NotificationMessage;\r\n /* ๅป้ๅๆๅ
ฅๅฐๅ่กจๅคด้จ */\r\n if (!messages.value.some((m) => m.id === msg.id)) {\r\n messages.value.unshift(msg);\r\n total.value++;\r\n persistToCache();\r\n showDesktopNotification(msg);\r\n }\r\n break;\r\n }\r\n case \"read_sync\": {\r\n const syncMessages = payload.data as NotificationMessage[];\r\n const readIds = new Set(syncMessages.map((m) => m.id));\r\n for (const msg of messages.value) {\r\n if (readIds.has(msg.id)) msg.status = \"read\";\r\n }\r\n persistToCache();\r\n break;\r\n }\r\n case \"count_update\": {\r\n /* ๆๅก็ซฏๆจ้็ๆช่ฏปๆฐๅฏ็จไบๆ กๅ */\r\n break;\r\n }\r\n }\r\n }\r\n\r\n /** WebSocket ่ฟๆฅ็ถๆๅๆดๅ่ฐ */\r\n function handleWSStatusChange(s: WSConnectionStatus) {\r\n wsStatus.value = s;\r\n }\r\n\r\n const { connect: wsConnect, disconnect: wsDisconnect } = useNotificationWS(\r\n handleWSMessage,\r\n handleWSStatusChange,\r\n );\r\n\r\n /* โโโ ๆก้ข้็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅ้ๆก้ข้็ฅ\r\n */\r\n function showDesktopNotification(msg: NotificationMessage) {\r\n if (!props.desktopNotification) return;\r\n if (!(\"Notification\" in window)) return;\r\n\r\n if (Notification.permission === \"granted\") {\r\n createDesktopNotification(msg);\r\n } else if (Notification.permission !== \"denied\") {\r\n Notification.requestPermission().then((permission) => {\r\n if (permission === \"granted\") {\r\n createDesktopNotification(msg);\r\n }\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * ๅๅปบๆก้ข้็ฅๅฎไพ\r\n */\r\n function createDesktopNotification(msg: NotificationMessage) {\r\n const n = new Notification(msg.title, {\r\n body: msg.summary,\r\n icon: msg.sender?.avatar || \"/robot-avatar.png\",\r\n tag: msg.id,\r\n });\r\n\r\n n.addEventListener(\"click\", () => {\r\n window.focus();\r\n selectMessage(msg);\r\n n.close();\r\n });\r\n\r\n /* 5 ็งๅ่ชๅจๅ
ณ้ญ */\r\n setTimeout(() => n.close(), 5000);\r\n }\r\n\r\n /* โโโ ่ฝฎ่ฏข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n let pollingTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n /**\r\n * ๅฏๅจ่ฝฎ่ฏข\r\n */\r\n function startPolling() {\r\n stopPolling();\r\n const interval = props.pollingInterval ?? DEFAULT_POLLING_INTERVAL;\r\n if (interval <= 0) return;\r\n\r\n pollingTimer = setInterval(() => {\r\n fetchMessages(true);\r\n }, interval);\r\n }\r\n\r\n /**\r\n * ๅๆญข่ฝฎ่ฏข\r\n */\r\n function stopPolling() {\r\n if (pollingTimer) {\r\n clearInterval(pollingTimer);\r\n pollingTimer = null;\r\n }\r\n }\r\n\r\n /* โโโ ไบคไบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ้ไธญ/ๆฅ็ๆถๆฏ\r\n */\r\n function selectMessage(msg: NotificationMessage) {\r\n selectedMessage.value = msg;\r\n /* ่ชๅจๆ ่ฎฐไธบๅทฒ่ฏป */\r\n if (msg.status === \"unread\") {\r\n markAsRead([msg.id]);\r\n }\r\n }\r\n\r\n /**\r\n * ่ฟๅๅ่กจ\r\n */\r\n function clearSelection() {\r\n selectedMessage.value = null;\r\n }\r\n\r\n /**\r\n * ๅๆขๅ็ฑป\r\n *\r\n * ็บฏๅฎขๆท็ซฏ่ฟๆปค๏ผMock / ๅทฒๅ ่ฝฝๆฐๆฎ๏ผ็ดๆฅๅๆข๏ผ\r\n * API ๆจกๅผๅ้ๆฐๆๅๅฏนๅบๅ็ฑปๆฐๆฎใ\r\n */\r\n function switchCategory(category: NotificationCategory | \"all\") {\r\n activeCategory.value = category;\r\n selectedMessage.value = null;\r\n\r\n /* ไป
API ๆจกๅผ้่ฆ้ๆฐๆๅ */\r\n if (props.fetchNotifications) {\r\n fetchMessages(true);\r\n }\r\n }\r\n\r\n /* โโโ ๅๅงๅ & ๆธ
็ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n /**\r\n * ๅๅงๅ\r\n */\r\n function init() {\r\n fetchMessages(true);\r\n startPolling();\r\n\r\n /* ๅฆๆ้
็ฝฎไบ WebSocket โ ๅปบ่ฟ */\r\n if (props.wsConfig) wsConnect(props.wsConfig);\r\n }\r\n\r\n /**\r\n * ้ๆฏ\r\n */\r\n function destroy() {\r\n stopPolling();\r\n wsDisconnect();\r\n if (persistTimer) {\r\n clearTimeout(persistTimer);\r\n persistTimer = null;\r\n }\r\n }\r\n\r\n onMounted(init);\r\n onBeforeUnmount(destroy);\r\n\r\n return {\r\n /* ็ถๆ */\r\n messages,\r\n activeCategory,\r\n loading,\r\n total,\r\n page,\r\n selectedMessage,\r\n popoverVisible,\r\n wsStatus,\r\n\r\n /* ่ฎก็ฎ */\r\n unreadCount,\r\n unreadByCategory,\r\n filteredMessages,\r\n hasMore,\r\n\r\n /* ๆไฝ */\r\n fetchMessages,\r\n loadMore,\r\n markAsRead,\r\n markAllAsRead,\r\n deleteMessages,\r\n clearMessages,\r\n selectMessage,\r\n clearSelection,\r\n switchCategory,\r\n\r\n /* WebSocket */\r\n connectWS: wsConnect,\r\n disconnectWS: wsDisconnect,\r\n };\r\n}\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅ่งๆ \r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-badge\">\r\n <NTooltip trigger=\"hover\" placement=\"bottom\">\r\n <template #trigger>\r\n <div class=\"notification-badge__trigger\" @click=\"$emit('click')\">\r\n <C_Icon\r\n name=\"mdi:bell-outline\"\r\n :size=\"18\"\r\n class=\"notification-badge__icon\"\r\n />\r\n <!-- ๆช่ฏป่งๆ -->\r\n <Transition name=\"badge-bounce\">\r\n <span v-if=\"count > 0\" class=\"notification-badge__count\">\r\n {{ displayCount }}\r\n </span>\r\n </Transition>\r\n <!-- ่ๅฒๅจ็ป๏ผๆ็ดงๆฅๆถๆฏๆถ๏ผ -->\r\n <span v-if=\"hasUrgent\" class=\"notification-badge__pulse\" />\r\n </div>\r\n </template>\r\n ๆถๆฏ้็ฅ{{ count > 0 ? `๏ผ${count} ๆกๆช่ฏป๏ผ` : \"\" }}\r\n </NTooltip>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport { DEFAULT_MAX_BADGE_COUNT } from \"../constants\";\r\n\r\ninterface Props {\r\n /** ๆช่ฏปๆฐ้ */\r\n count?: number;\r\n /** ๆๅคงๆพ็คบๆฐ */\r\n maxCount?: number;\r\n /** ๆฏๅฆๆ็ดงๆฅๆถๆฏ */\r\n hasUrgent?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n count: 0,\r\n maxCount: DEFAULT_MAX_BADGE_COUNT,\r\n hasUrgent: false,\r\n});\r\n\r\ndefineEmits<{\r\n click: [];\r\n}>();\r\n\r\n/** ๆพ็คบๆๆฌ */\r\nconst displayCount = computed(() =>\r\n props.count > props.maxCount ? `${props.maxCount}+` : String(props.count),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-badge {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n padding: 4px;\r\n border-radius: 6px;\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n background-color: var(--c-border);\r\n\r\n .notification-badge__icon {\r\n color: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__icon {\r\n color: var(--c-text-2);\r\n transition: color var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__count {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n box-shadow: 0 0 0 2px var(--c-bg);\r\n }\r\n\r\n &__pulse {\r\n position: absolute;\r\n top: -2px;\r\n right: -4px;\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 50%;\r\n background: rgba(245, 34, 45, 0.4);\r\n animation: pulse-ring 1.5s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n@keyframes pulse-ring {\r\n 0% {\r\n transform: scale(0.8);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: scale(2.2);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n.badge-bounce-enter-active {\r\n animation: badge-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\r\n}\r\n\r\n.badge-bounce-leave-active {\r\n animation: badge-in 0.2s ease reverse;\r\n}\r\n\r\n@keyframes badge-in {\r\n 0% {\r\n transform: scale(0);\r\n opacity: 0;\r\n }\r\n\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅๆถ้ดๆ ผๅผๅ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\n/** ๆถ้ดๅไฝ้ๅผ */\r\nconst MINUTE = 60_000;\r\nconst HOUR = 3_600_000;\r\nconst DAY = 86_400_000;\r\nconst WEEK = 7 * DAY;\r\nconst MONTH = 30 * DAY;\r\nconst YEAR = 365 * DAY;\r\n\r\n/** ๅไพ็ผๅญ */\r\nlet _instance: ReturnType<typeof createNotificationFormat> | null = null;\r\n\r\n/**\r\n * ้็ฅๆถ้ดๆ ผๅผๅ๏ผๅไพ๏ผ\r\n *\r\n * ๅฐ ISO ๆถ้ดๆณๆ ผๅผๅไธบไบบ็ฑปๅๅฅฝ็็ธๅฏนๆถ้ดๆ่ฟฐ๏ผ\r\n * ๅฆ \"ๅๅ\"ใ\"5ๅ้ๅ\"ใ\"ๆจๅคฉ 14:30\" ็ญใ\r\n */\r\nexport function useNotificationFormat() {\r\n if (!_instance) _instance = createNotificationFormat();\r\n return _instance;\r\n}\r\n\r\n/**\r\n * ๅๅปบๆ ผๅผๅๅทฅๅ
ทๅฎไพ\r\n */\r\nfunction createNotificationFormat() {\r\n /**\r\n * ๆ ผๅผๅ็ธๅฏนๆถ้ด\r\n * @param timestamp ISO 8601 ๆถ้ดๅญ็ฌฆไธฒ\r\n */\r\n function formatRelativeTime(timestamp: string): string {\r\n const date = new Date(timestamp);\r\n const now = Date.now();\r\n const diff = now - date.getTime();\r\n\r\n if (diff < 0) return \"ๅๅ\";\r\n if (diff < MINUTE) return \"ๅๅ\";\r\n if (diff < HOUR) return `${Math.floor(diff / MINUTE)}ๅ้ๅ`;\r\n if (diff < DAY) return `${Math.floor(diff / HOUR)}ๅฐๆถๅ`;\r\n\r\n /* ๅคๆญๆฏๅฆๆฏๆจๅคฉ */\r\n const today = new Date();\r\n today.setHours(0, 0, 0, 0);\r\n const yesterday = new Date(today.getTime() - DAY);\r\n\r\n if (date >= yesterday && date < today) {\r\n return `ๆจๅคฉ ${padTime(date.getHours())}:${padTime(date.getMinutes())}`;\r\n }\r\n\r\n if (diff < WEEK) return `${Math.floor(diff / DAY)}ๅคฉๅ`;\r\n if (diff < MONTH) return `${Math.floor(diff / WEEK)}ๅจๅ`;\r\n if (diff < YEAR) return `${Math.floor(diff / MONTH)}ไธชๆๅ`;\r\n\r\n return formatFullDate(date);\r\n }\r\n\r\n /**\r\n * ๆ ผๅผๅๅฎๆดๆฅๆๆถ้ด\r\n * @param date Date ๅฏน่ฑก\r\n */\r\n function formatFullDate(date: Date): string {\r\n const y = date.getFullYear();\r\n const m = padTime(date.getMonth() + 1);\r\n const d = padTime(date.getDate());\r\n const h = padTime(date.getHours());\r\n const min = padTime(date.getMinutes());\r\n return `${y}-${m}-${d} ${h}:${min}`;\r\n }\r\n\r\n /**\r\n * ่กฅ้ถ\r\n * @param n ๆฐๅญ\r\n */\r\n function padTime(n: number): string {\r\n return n < 10 ? `0${n}` : String(n);\r\n }\r\n\r\n return {\r\n formatRelativeTime,\r\n formatFullDate,\r\n };\r\n}\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๅๆกๆถๆฏ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n class=\"notification-item\"\r\n :class=\"{\r\n 'notification-item--unread': message.status === 'unread',\r\n 'notification-item--urgent': message.priority === 'urgent',\r\n }\"\r\n @click=\"$emit('click', message)\"\r\n >\r\n <!-- ๆช่ฏปๆ็คบ็น -->\r\n <div class=\"notification-item__indicator\">\r\n <span\r\n v-if=\"message.status === 'unread'\"\r\n class=\"notification-item__dot\"\r\n :class=\"`notification-item__dot--${message.priority}`\"\r\n />\r\n </div>\r\n\r\n <!-- ๅคดๅ -->\r\n <div class=\"notification-item__avatar\">\r\n <NAvatar\r\n v-if=\"message.sender?.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"36\"\r\n round\r\n />\r\n <div\r\n v-else\r\n class=\"notification-item__avatar-placeholder\"\r\n :class=\"`notification-item__avatar-placeholder--${message.category}`\"\r\n >\r\n <C_Icon :name=\"categoryIcon\" :size=\"16\" />\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ
ๅฎน -->\r\n <div class=\"notification-item__body\">\r\n <div class=\"notification-item__header\">\r\n <span class=\"notification-item__title\">{{ message.title }}</span>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityConfig.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityConfig.label }}\r\n </NTag>\r\n </div>\r\n <div class=\"notification-item__summary\">\r\n {{ message.summary }}\r\n </div>\r\n <div class=\"notification-item__meta\">\r\n <span class=\"notification-item__time\">{{ formattedTime }}</span>\r\n <span v-if=\"message.sender?.name\" class=\"notification-item__sender\">\r\n {{ message.sender.name }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div class=\"notification-item__actions\" @click.stop>\r\n <NTooltip\r\n v-if=\"message.status === 'unread'\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('read', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:check\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๆ ่ฎฐๅทฒ่ฏป\r\n </NTooltip>\r\n <NTooltip trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"notification-item__action\"\r\n @click=\"$emit('delete', message.id)\"\r\n >\r\n <C_Icon name=\"mdi:delete-outline\" :size=\"16\" />\r\n </span>\r\n </template>\r\n ๅ ้ค\r\n </NTooltip>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\ndefineEmits<{\r\n click: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n}>();\r\n\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๅพๆ */\r\nconst categoryIcon = computed(\r\n () => CATEGORY_MAP[props.message.category]?.icon ?? \"mdi:bell-outline\",\r\n);\r\n\r\n/** ไผๅ
็บง้
็ฝฎ */\r\nconst priorityConfig = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n padding: 12px 16px;\r\n cursor: pointer;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n border-bottom: 1px solid var(--c-border);\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n\r\n &:hover {\r\n background-color: var(--c-bg-card);\r\n\r\n .notification-item__actions {\r\n opacity: 1;\r\n }\r\n }\r\n\r\n &--unread {\r\n background-color: color-mix(in srgb, var(--c-primary) 4%, transparent);\r\n\r\n &:hover {\r\n background-color: color-mix(in srgb, var(--c-primary) 8%, transparent);\r\n }\r\n }\r\n\r\n &--urgent {\r\n border-left: 3px solid #f5222d;\r\n padding-left: 13px;\r\n }\r\n\r\n /* โโโ ๅญๅ
็ด โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n &__indicator {\r\n flex-shrink: 0;\r\n width: 8px;\r\n display: flex;\r\n align-items: center;\r\n padding-top: 14px;\r\n }\r\n\r\n &__dot {\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--c-primary);\r\n\r\n &--urgent {\r\n background: #f5222d;\r\n box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);\r\n }\r\n\r\n &--high {\r\n background: #fa8c16;\r\n }\r\n\r\n &--normal {\r\n background: var(--c-primary);\r\n }\r\n\r\n &--low {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n\r\n &__avatar {\r\n flex-shrink: 0;\r\n }\r\n\r\n &__avatar-placeholder {\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 16px;\r\n color: #fff;\r\n\r\n &--system {\r\n background: linear-gradient(135deg, #667eea, #764ba2);\r\n }\r\n\r\n &--business {\r\n background: linear-gradient(135deg, #43e97b, #38f9d7);\r\n color: #0d1425;\r\n }\r\n\r\n &--alarm {\r\n background: linear-gradient(135deg, #f093fb, #f5576c);\r\n }\r\n }\r\n\r\n &__body {\r\n flex: 1;\r\n min-width: 0;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-bottom: 4px;\r\n }\r\n\r\n &__title {\r\n font-size: 13px;\r\n font-weight: 500;\r\n color: var(--c-text-1);\r\n line-height: 1.4;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &__summary {\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n line-height: 1.5;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-top: 6px;\r\n }\r\n\r\n &__time,\r\n &__sender {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender::before {\r\n content: \"ยท\";\r\n margin-right: 8px;\r\n }\r\n\r\n &__actions {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &__action {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 2px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: var(--c-text-4);\r\n transition: all var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n background-color: var(--c-border);\r\n }\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏๅ่กจ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-list\">\r\n <!-- ๅ็ฑป Tab -->\r\n <div class=\"notification-list__tabs\">\r\n <div\r\n v-for=\"tab in CATEGORY_TABS\"\r\n :key=\"tab.key\"\r\n class=\"notification-list__tab\"\r\n :class=\"{\r\n 'notification-list__tab--active': activeCategory === tab.key,\r\n }\"\r\n @click=\"$emit('switchCategory', tab.key)\"\r\n >\r\n <span>{{ tab.label }}</span>\r\n <span\r\n v-if=\"getTabCount(tab.key) > 0\"\r\n class=\"notification-list__tab-count\"\r\n >\r\n {{ getTabCount(tab.key) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆไฝๆ -->\r\n <div v-if=\"messages.length > 0\" class=\"notification-list__toolbar\">\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n :disabled=\"currentUnread === 0\"\r\n @click=\"$emit('markAllRead')\"\r\n >\r\n <template #icon>\r\n <C_Icon name=\"mdi:check-all\" :size=\"14\" />\r\n </template>\r\n ๅ
จ้จๅทฒ่ฏป\r\n </NButton>\r\n <NButton text size=\"tiny\" @click=\"$emit('clear')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:delete-sweep-outline\" :size=\"14\" />\r\n </template>\r\n ๆธ
็ฉบ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NScrollbar\r\n class=\"notification-list__scroll\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <template v-if=\"loading && messages.length === 0\">\r\n <!-- ้ชจๆถๅฑ -->\r\n <div v-for=\"i in 4\" :key=\"i\" class=\"notification-list__skeleton\">\r\n <NSkeleton circle :width=\"36\" :height=\"36\" />\r\n <div class=\"notification-list__skeleton-text\">\r\n <NSkeleton text :width=\"'60%'\" :height=\"14\" />\r\n <NSkeleton text :width=\"'90%'\" :height=\"12\" />\r\n <NSkeleton text :width=\"'40%'\" :height=\"10\" />\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"messages.length > 0\">\r\n <div class=\"notification-list__items\">\r\n <NotificationItem\r\n v-for=\"msg in messages\"\r\n :key=\"msg.id\"\r\n :message=\"msg\"\r\n @click=\"$emit('itemClick', $event)\"\r\n @read=\"$emit('read', $event)\"\r\n @delete=\"$emit('delete', $event)\"\r\n />\r\n </div>\r\n\r\n <!-- ๅ ่ฝฝๆดๅค -->\r\n <div\r\n v-if=\"hasMore\"\r\n class=\"notification-list__load-more\"\r\n @click=\"$emit('loadMore')\"\r\n >\r\n <NSpin v-if=\"loading\" :size=\"14\" />\r\n <span>{{ loading ? \"ๅ ่ฝฝไธญ...\" : \"ๅ ่ฝฝๆดๅค\" }}</span>\r\n </div>\r\n </template>\r\n\r\n <!-- ็ฉบ็ถๆ -->\r\n <div v-else class=\"notification-list__empty\">\r\n <C_Icon\r\n name=\"mdi:bell-off-outline\"\r\n :size=\"48\"\r\n class=\"notification-list__empty-icon\"\r\n />\r\n <span class=\"notification-list__empty-text\">ๆๆ ๆถๆฏ</span>\r\n </div>\r\n </NScrollbar>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage, NotificationCategory } from \"../types\";\r\nimport { CATEGORY_TABS } from \"../constants\";\r\nimport NotificationItem from \"./NotificationItem.vue\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๅ่กจ */\r\n messages: NotificationMessage[];\r\n /** ๅฝๅๅ็ฑป */\r\n activeCategory: NotificationCategory | \"all\";\r\n /** ๅๅ็ฑปๆช่ฏปๆฐ */\r\n unreadByCategory: Record<string, number>;\r\n /** ๆปๆช่ฏปๆฐ */\r\n unreadCount: number;\r\n /** ๆฏๅฆๅ ่ฝฝไธญ */\r\n loading: boolean;\r\n /** ๆฏๅฆๆๆดๅค */\r\n hasMore: boolean;\r\n /** ๆปๅจๅบๅ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 420,\r\n});\r\n\r\ndefineEmits<{\r\n switchCategory: [key: NotificationCategory | \"all\"];\r\n itemClick: [message: NotificationMessage];\r\n read: [id: string];\r\n delete: [id: string];\r\n markAllRead: [];\r\n clear: [];\r\n loadMore: [];\r\n}>();\r\n\r\n/** ๅฝๅๅ็ฑปไธ็ๆช่ฏปๆฐ */\r\nconst currentUnread = computed(() => {\r\n if (props.activeCategory === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[props.activeCategory] ?? 0;\r\n});\r\n\r\n/** ่ทๅ Tab ็ๆช่ฏปๆฐ */\r\nfunction getTabCount(key: NotificationCategory | \"all\"): number {\r\n if (key === \"all\") return props.unreadCount;\r\n return props.unreadByCategory[key] ?? 0;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-list {\r\n &__tabs {\r\n display: flex;\r\n border-bottom: 1px solid var(--c-border);\r\n padding: 0 4px;\r\n }\r\n\r\n &__tab {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: all var(--c-transition, 0.2s ease);\r\n user-select: none;\r\n\r\n &::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 12px;\r\n right: 12px;\r\n height: 2px;\r\n background: transparent;\r\n border-radius: 1px;\r\n transition: background var(--c-transition, 0.2s ease);\r\n }\r\n\r\n &:hover {\r\n color: var(--c-text-1);\r\n }\r\n\r\n &--active {\r\n color: var(--c-primary);\r\n font-weight: 500;\r\n\r\n &::after {\r\n background: var(--c-primary);\r\n }\r\n }\r\n }\r\n\r\n &__tab-count {\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 8px;\r\n background: #f5222d;\r\n color: #fff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n }\r\n\r\n &__toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n padding: 6px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__scroll {\r\n flex: 1;\r\n }\r\n\r\n &__skeleton {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n }\r\n\r\n &__skeleton-text {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n }\r\n\r\n &__load-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 6px;\r\n padding: 12px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n cursor: pointer;\r\n transition: color var(--c-transition, 0.2s ease);\r\n\r\n &:hover {\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px 0;\r\n gap: 12px;\r\n }\r\n\r\n &__empty-icon {\r\n color: var(--c-text-4);\r\n opacity: 0.5;\r\n }\r\n\r\n &__empty-text {\r\n font-size: 13px;\r\n color: var(--c-text-4);\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ๆถๆฏ่ฏฆๆ
\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"notification-detail\">\r\n <!-- ่ฟๅๆ -->\r\n <div class=\"notification-detail__back\">\r\n <NButton text size=\"small\" @click=\"$emit('back')\">\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-left\" :size=\"16\" />\r\n </template>\r\n ่ฟๅๅ่กจ\r\n </NButton>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅคด้จ -->\r\n <div class=\"notification-detail__header\">\r\n <h3 class=\"notification-detail__title\">{{ message.title }}</h3>\r\n <div class=\"notification-detail__meta\">\r\n <NTag :type=\"categoryTag.color\" size=\"tiny\" :bordered=\"false\" round>\r\n <template #icon>\r\n <C_Icon :name=\"categoryTag.icon\" :size=\"12\" />\r\n </template>\r\n {{ categoryTag.label }}\r\n </NTag>\r\n <NTag\r\n v-if=\"message.priority === 'urgent' || message.priority === 'high'\"\r\n :type=\"priorityTag.type\"\r\n size=\"tiny\"\r\n :bordered=\"false\"\r\n round\r\n >\r\n {{ priorityTag.label }}\r\n </NTag>\r\n <span class=\"notification-detail__time\">{{ formattedTime }}</span>\r\n </div>\r\n <div v-if=\"message.sender\" class=\"notification-detail__sender\">\r\n <NAvatar\r\n v-if=\"message.sender.avatar\"\r\n :src=\"message.sender.avatar\"\r\n :size=\"20\"\r\n round\r\n />\r\n <span>{{ message.sender.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- ๆถๆฏๅ
ๅฎน -->\r\n <NScrollbar\r\n class=\"notification-detail__content\"\r\n :style=\"{ maxHeight: `${scrollHeight}px` }\"\r\n >\r\n <div\r\n v-if=\"message.content\"\r\n class=\"notification-detail__body\"\r\n v-html=\"message.content\"\r\n />\r\n <div v-else class=\"notification-detail__body\">\r\n {{ message.summary }}\r\n </div>\r\n </NScrollbar>\r\n\r\n <!-- ๆไฝๅบ -->\r\n <div v-if=\"message.actionUrl\" class=\"notification-detail__footer\">\r\n <NButton type=\"primary\" size=\"small\" @click=\"handleAction\">\r\n {{ message.actionText || \"ๆฅ็่ฏฆๆ
\" }}\r\n <template #icon>\r\n <C_Icon name=\"mdi:arrow-right\" :size=\"16\" />\r\n </template>\r\n </NButton>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport C_Icon from \"../../C_Icon/index.vue\";\r\nimport type { NotificationMessage } from \"../types\";\r\nimport { CATEGORY_MAP, PRIORITY_MAP } from \"../constants\";\r\nimport { useNotificationFormat } from \"../composables/useNotificationFormat\";\r\n\r\ninterface Props {\r\n /** ๆถๆฏๆฐๆฎ */\r\n message: NotificationMessage;\r\n /** ๅ
ๅฎนๆปๅจๅบ้ซๅบฆ */\r\n scrollHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n scrollHeight: 320,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n back: [];\r\n action: [url: string];\r\n navigate: [url: string];\r\n}>();\r\nconst { formatRelativeTime } = useNotificationFormat();\r\n\r\n/** ๅ็ฑปๆ ็ญพ */\r\nconst categoryTag = computed(\r\n () => CATEGORY_MAP[props.message.category] ?? CATEGORY_MAP.system,\r\n);\r\n\r\n/** ไผๅ
็บงๆ ็ญพ */\r\nconst priorityTag = computed(\r\n () => PRIORITY_MAP[props.message.priority] ?? PRIORITY_MAP.normal,\r\n);\r\n\r\n/** ๆ ผๅผๅๆถ้ด */\r\nconst formattedTime = computed(() =>\r\n formatRelativeTime(props.message.timestamp),\r\n);\r\n\r\n/** ๅค็ๆไฝๆ้ฎ็นๅป */\r\nfunction handleAction() {\r\n const url = props.message.actionUrl;\r\n if (!url) return;\r\n\r\n emit(\"action\", url);\r\n\r\n if (url.startsWith(\"http\")) {\r\n window.open(url, \"_blank\");\r\n } else {\r\n emit(\"navigate\", url);\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.notification-detail {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n\r\n &__back {\r\n padding: 8px 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__header {\r\n padding: 16px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n line-height: 1.5;\r\n margin: 0 0 8px;\r\n }\r\n\r\n &__meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n\r\n &__time {\r\n font-size: 11px;\r\n color: var(--c-text-4);\r\n }\r\n\r\n &__sender {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 8px;\r\n font-size: 12px;\r\n color: var(--c-text-2);\r\n }\r\n\r\n &__content {\r\n flex: 1;\r\n min-height: 0;\r\n }\r\n\r\n &__body {\r\n padding: 16px;\r\n font-size: 13px;\r\n line-height: 1.7;\r\n color: var(--c-text-1);\r\n word-break: break-word;\r\n\r\n :deep(p) {\r\n margin: 0 0 8px;\r\n }\r\n\r\n :deep(ul),\r\n :deep(ol) {\r\n padding-left: 20px;\r\n margin: 8px 0;\r\n }\r\n\r\n :deep(li) {\r\n margin: 4px 0;\r\n }\r\n\r\n :deep(strong) {\r\n font-weight: 600;\r\n color: var(--c-primary);\r\n }\r\n }\r\n\r\n &__footer {\r\n padding: 12px 16px;\r\n border-top: 1px solid var(--c-border);\r\n display: flex;\r\n justify-content: flex-end;\r\n }\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-27\r\n * @Description: ้็ฅไธญๅฟ็ปไปถ\r\n * @Migration: naive-ui-components ็ปไปถๅบ่ฟ็งป็ๆฌ\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"c-notification-center\">\r\n <NPopover\r\n v-model:show=\"core.popoverVisible.value\"\r\n :width=\"420\"\r\n trigger=\"click\"\r\n placement=\"bottom-end\"\r\n :show-arrow=\"false\"\r\n content-class=\"c-notification-center__popover\"\r\n @after-leave=\"handlePopoverClose\"\r\n >\r\n <!-- ่งฆๅๅจ๏ผ่งๆ ้้ -->\r\n <template #trigger>\r\n <NotificationBadge\r\n :count=\"core.unreadCount.value\"\r\n :max-count=\"props.maxBadgeCount\"\r\n :has-urgent=\"hasUrgentMessage\"\r\n @click=\"handleBadgeClick\"\r\n />\r\n </template>\r\n\r\n <!-- ้ขๆฟๅ
ๅฎน -->\r\n <div class=\"c-notification-center__panel\">\r\n <!-- ๅคด้จๆ ้ข -->\r\n <div class=\"c-notification-center__header\">\r\n <span class=\"c-notification-center__title\">ๆถๆฏไธญๅฟ</span>\r\n <div class=\"c-notification-center__header-extra\">\r\n <!-- WebSocket ่ฟๆฅ็ถๆ -->\r\n <NTooltip v-if=\"props.wsConfig\" trigger=\"hover\" placement=\"top\">\r\n <template #trigger>\r\n <span\r\n class=\"c-notification-center__ws-dot\"\r\n :class=\"`c-notification-center__ws-dot--${core.wsStatus.value}`\"\r\n />\r\n </template>\r\n {{ wsStatusText }}\r\n </NTooltip>\r\n </div>\r\n </div>\r\n\r\n <!-- ๅ่กจ / ่ฏฆๆ
ๅๆข -->\r\n <Transition name=\"slide-fade\" mode=\"out-in\">\r\n <!-- ๆถๆฏ่ฏฆๆ
-->\r\n <NotificationDetail\r\n v-if=\"core.selectedMessage.value\"\r\n :key=\"core.selectedMessage.value.id\"\r\n :message=\"core.selectedMessage.value\"\r\n @back=\"core.clearSelection\"\r\n @action=\"handleAction\"\r\n @navigate=\"(url) => emit('navigate', url)\"\r\n />\r\n\r\n <!-- ๆถๆฏๅ่กจ -->\r\n <NotificationList\r\n v-else\r\n :messages=\"core.filteredMessages.value\"\r\n :active-category=\"core.activeCategory.value\"\r\n :unread-by-category=\"core.unreadByCategory.value\"\r\n :unread-count=\"core.unreadCount.value\"\r\n :loading=\"core.loading.value\"\r\n :has-more=\"core.hasMore.value\"\r\n @switch-category=\"core.switchCategory\"\r\n @item-click=\"handleItemClick\"\r\n @read=\"handleRead\"\r\n @delete=\"handleDelete\"\r\n @mark-all-read=\"handleMarkAllRead\"\r\n @clear=\"handleClear\"\r\n @load-more=\"core.loadMore\"\r\n />\r\n </Transition>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, watch } from \"vue\";\r\nimport type {\r\n NotificationCenterProps,\r\n NotificationCenterExpose,\r\n NotificationMessage,\r\n NotificationCategory,\r\n} from \"./types\";\r\nimport { useNotificationCore } from \"./composables/useNotificationCore\";\r\nimport NotificationBadge from \"./components/NotificationBadge.vue\";\r\nimport NotificationList from \"./components/NotificationList.vue\";\r\nimport NotificationDetail from \"./components/NotificationDetail.vue\";\r\n\r\ndefineOptions({ name: \"C_NotificationCenter\" });\r\n\r\nconst props = withDefaults(defineProps<NotificationCenterProps>(), {\r\n desktopNotification: false,\r\n maxBadgeCount: 99,\r\n pollingInterval: 60_000,\r\n pageSize: 20,\r\n storageKey: \"notification_center\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n itemClick: [message: NotificationMessage];\r\n read: [ids: string[]];\r\n allRead: [category?: NotificationCategory];\r\n delete: [ids: string[]];\r\n unreadChange: [count: number];\r\n wsStatusChange: [status: string];\r\n newMessage: [message: NotificationMessage];\r\n navigate: [url: string];\r\n}>();\r\n\r\nconst core = useNotificationCore(props);\r\n\r\n/* โโโ ่ฎก็ฎๅฑๆง โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ๆฏๅฆๆ็ดงๆฅๆช่ฏปๆถๆฏ */\r\nconst hasUrgentMessage = computed(() =>\r\n core.messages.value.some(\r\n (m) => m.status === \"unread\" && m.priority === \"urgent\",\r\n ),\r\n);\r\n\r\n/** WebSocket ็ถๆๆๆฌ */\r\nconst wsStatusText = computed(() => {\r\n const map: Record<string, string> = {\r\n connected: \"ๅทฒ่ฟๆฅ\",\r\n connecting: \"่ฟๆฅไธญ...\",\r\n disconnected: \"ๆช่ฟๆฅ\",\r\n reconnecting: \"้่ฟไธญ...\",\r\n };\r\n return map[core.wsStatus.value] ?? \"ๆช็ฅ\";\r\n});\r\n\r\n/* โโโ ็ๅฌๆช่ฏปๆฐๅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\nwatch(\r\n () => core.unreadCount.value,\r\n (count) => {\r\n emit(\"unreadChange\", count);\r\n },\r\n);\r\n\r\n/* โโโ ไบไปถๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n/** ่งๆ ็นๅป */\r\nfunction handleBadgeClick() {\r\n core.popoverVisible.value = !core.popoverVisible.value;\r\n}\r\n\r\n/** ็นๅปๆถๆฏ */\r\nfunction handleItemClick(message: NotificationMessage) {\r\n core.selectMessage(message);\r\n emit(\"itemClick\", message);\r\n}\r\n\r\n/** ๆ ่ฎฐๅทฒ่ฏป */\r\nfunction handleRead(id: string) {\r\n core.markAsRead([id]);\r\n emit(\"read\", [id]);\r\n}\r\n\r\n/** ๅ ้คๆถๆฏ */\r\nfunction handleDelete(id: string) {\r\n core.deleteMessages([id]);\r\n emit(\"delete\", [id]);\r\n}\r\n\r\n/** ๅ
จ้จๅทฒ่ฏป */\r\nfunction handleMarkAllRead() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.markAllAsRead(category);\r\n emit(\"allRead\", category);\r\n}\r\n\r\n/** ๆธ
็ฉบ */\r\nfunction handleClear() {\r\n const category =\r\n core.activeCategory.value === \"all\" ? undefined : core.activeCategory.value;\r\n core.clearMessages(category);\r\n}\r\n\r\n/** ๆไฝ้พๆฅ่ทณ่ฝฌ */\r\nfunction handleAction() {\r\n core.popoverVisible.value = false;\r\n}\r\n\r\n/** Popover ๅ
ณ้ญๅ้็ฝฎ่ฏฆๆ
*/\r\nfunction handlePopoverClose() {\r\n core.clearSelection();\r\n}\r\n\r\n/* โโโ Expose โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\ndefineExpose<NotificationCenterExpose>({\r\n refresh: () => core.fetchMessages(true),\r\n connectWS: () => {\r\n if (props.wsConfig) core.connectWS(props.wsConfig);\r\n },\r\n disconnectWS: core.disconnectWS,\r\n getUnreadCount: () => core.unreadCount.value,\r\n getMessages: () => core.messages.value,\r\n markRead: (ids: string[]) => core.markAsRead(ids),\r\n markAllAsRead: (category?: NotificationCategory) =>\r\n core.markAllAsRead(category),\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.c-notification-center {\r\n display: inline-flex;\r\n align-items: center;\r\n\r\n &__panel {\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 560px;\r\n }\r\n\r\n &__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 14px 16px 10px;\r\n border-bottom: 1px solid var(--c-border);\r\n }\r\n\r\n &__title {\r\n font-size: 15px;\r\n font-weight: 600;\r\n color: var(--c-text-1);\r\n }\r\n\r\n &__header-extra {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__ws-dot {\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 50%;\r\n transition: background-color var(--c-transition, 0.2s ease);\r\n\r\n &--connected {\r\n background: #52c41a;\r\n box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);\r\n }\r\n\r\n &--connecting,\r\n &--reconnecting {\r\n background: #faad14;\r\n animation: status-blink 1s ease-in-out infinite;\r\n }\r\n\r\n &--disconnected {\r\n background: var(--c-text-4);\r\n }\r\n }\r\n}\r\n\r\n@keyframes status-blink {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n/* โโโ ๅๆข่ฟๆธก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\r\n\r\n.slide-fade-enter-active {\r\n transition: all 0.25s ease-out;\r\n}\r\n\r\n.slide-fade-leave-active {\r\n transition: all 0.2s ease-in;\r\n}\r\n\r\n.slide-fade-enter-from {\r\n opacity: 0;\r\n transform: translateY(8px);\r\n}\r\n\r\n.slide-fade-leave-to {\r\n opacity: 0;\r\n transform: translateY(-8px);\r\n}\r\n</style>\r\n"],"mappings":"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"}
|