@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.
Files changed (175) hide show
  1. package/dist/C_ActionBar-nnfbZCea.css.map +1 -0
  2. package/dist/C_ActionBar2.js +2 -2
  3. package/dist/C_ActionBar2.js.map +1 -1
  4. package/dist/C_AntV-DGjscTWa.css.map +1 -0
  5. package/dist/C_AntV2.js +6 -6
  6. package/dist/C_AntV2.js.map +1 -1
  7. package/dist/C_Barcode-DjTmDkbQ.css.map +1 -0
  8. package/dist/C_Barcode2.js +1 -1
  9. package/dist/C_Barcode2.js.map +1 -1
  10. package/dist/C_Captcha-Ccq3DMrR.css.map +1 -0
  11. package/dist/C_Captcha2.js +1 -1
  12. package/dist/C_Captcha2.js.map +1 -1
  13. package/dist/C_Cascade-IUUHIh7r.css.map +1 -0
  14. package/dist/C_Cascade2.js +2 -2
  15. package/dist/C_Cascade2.js.map +1 -1
  16. package/dist/C_City-Cv5BESaN.css.map +1 -0
  17. package/dist/C_City2.js +2 -2
  18. package/dist/C_City2.js.map +1 -1
  19. package/dist/C_Code-DPZlNSxL.css.map +1 -0
  20. package/dist/C_Code2.js +8 -7
  21. package/dist/C_Code2.js.map +1 -1
  22. package/dist/C_CollapsePanel-Fap-lv_5.css.map +1 -0
  23. package/dist/C_CollapsePanel2.js +1 -1
  24. package/dist/C_CollapsePanel2.js.map +1 -1
  25. package/dist/C_Cron-C0-8b5af.css.map +1 -0
  26. package/dist/C_Cron2.js +15 -14
  27. package/dist/C_Cron2.js.map +1 -1
  28. package/dist/C_Date2.js +1 -1
  29. package/dist/C_Date2.js.map +1 -1
  30. package/dist/C_Draggable-Bq6o0qXn.css.map +1 -0
  31. package/dist/C_Draggable2.js +1 -1
  32. package/dist/C_Draggable2.js.map +1 -1
  33. package/dist/C_Editor-OlxIF9-5.css.map +1 -0
  34. package/dist/C_Editor2.js +1 -1
  35. package/dist/C_Editor2.js.map +1 -1
  36. package/dist/C_FilePreview-B4XgTv-h.css.map +1 -0
  37. package/dist/C_FilePreview.cjs +1 -0
  38. package/dist/C_FilePreview.js +1 -0
  39. package/dist/C_FilePreview2.js +10 -9
  40. package/dist/C_FilePreview2.js.map +1 -1
  41. package/dist/C_Form-Cr9oX037.css.map +1 -0
  42. package/dist/C_Form2.js +48 -48
  43. package/dist/C_Form2.js.map +1 -1
  44. package/dist/C_FormSearch-DlIEoh7X.css.map +1 -0
  45. package/dist/C_FormSearch2.js +2 -2
  46. package/dist/C_FormSearch2.js.map +1 -1
  47. package/dist/C_FormulaEditor-Cm0CokN5.css.map +1 -0
  48. package/dist/C_FormulaEditor2.js +6 -6
  49. package/dist/C_FormulaEditor2.js.map +1 -1
  50. package/dist/C_FullCalendar-BULCIlVK.css.map +1 -0
  51. package/dist/C_FullCalendar2.js +2 -2
  52. package/dist/C_FullCalendar2.js.map +1 -1
  53. package/dist/C_Guide2.js +1 -1
  54. package/dist/C_Guide2.js.map +1 -1
  55. package/dist/C_Icon2.js.map +1 -1
  56. package/dist/C_ImageCropper-DrmUlaLi.css.map +1 -0
  57. package/dist/C_ImageCropper2.js +4 -4
  58. package/dist/C_ImageCropper2.js.map +1 -1
  59. package/dist/C_Language2.js +1 -1
  60. package/dist/C_Language2.js.map +1 -1
  61. package/dist/C_Map-WUMXSAfy.css.map +1 -0
  62. package/dist/C_Map2.js +2 -2
  63. package/dist/C_Map2.js.map +1 -1
  64. package/dist/C_Markdown-Dmv8yaM4.css.map +1 -0
  65. package/dist/C_Markdown2.js +4 -4
  66. package/dist/C_Markdown2.js.map +1 -1
  67. package/dist/C_NotificationCenter-DbgBiyqB.css.map +1 -0
  68. package/dist/C_NotificationCenter2.js +21 -20
  69. package/dist/C_NotificationCenter2.js.map +1 -1
  70. package/dist/C_Progress2.js +1 -1
  71. package/dist/C_Progress2.js.map +1 -1
  72. package/dist/C_QRCode-G7fiAkm4.css.map +1 -0
  73. package/dist/C_QRCode2.js +1 -1
  74. package/dist/C_QRCode2.js.map +1 -1
  75. package/dist/C_Signature-es-ZNPzr.css.map +1 -0
  76. package/dist/C_Signature2.js +2 -2
  77. package/dist/C_Signature2.js.map +1 -1
  78. package/dist/C_SplitPane-Br2eK8IG.css.map +1 -0
  79. package/dist/C_SplitPane2.js +1 -1
  80. package/dist/C_SplitPane2.js.map +1 -1
  81. package/dist/C_Steps-P9Qj9iDd.css.map +1 -0
  82. package/dist/C_Steps2.js +1 -1
  83. package/dist/C_Steps2.js.map +1 -1
  84. package/dist/C_Table-DAwAxr72.css.map +1 -0
  85. package/dist/C_Table2.js +3 -3
  86. package/dist/C_Table2.js.map +1 -1
  87. package/dist/C_Theme2.js +1 -1
  88. package/dist/C_Theme2.js.map +1 -1
  89. package/dist/C_Time-Bd_e1YDN.css.map +1 -0
  90. package/dist/C_Time2.js +2 -2
  91. package/dist/C_Time2.js.map +1 -1
  92. package/dist/C_Tree-DnGc_MPb.css.map +1 -0
  93. package/dist/C_Tree2.js +2 -2
  94. package/dist/C_Tree2.js.map +1 -1
  95. package/dist/C_Upload-i8LB_29U.css.map +1 -0
  96. package/dist/C_Upload2.js +5 -5
  97. package/dist/C_Upload2.js.map +1 -1
  98. package/dist/C_VideoPlayer-rm0MODUv.css.map +1 -0
  99. package/dist/C_VideoPlayer2.js +21 -20
  100. package/dist/C_VideoPlayer2.js.map +1 -1
  101. package/dist/C_VtableGantt-BpY-Rng3.css.map +1 -0
  102. package/dist/C_VtableGantt2.js +2 -2
  103. package/dist/C_VtableGantt2.js.map +1 -1
  104. package/dist/C_WaterFall-HWB-gPON.css.map +1 -0
  105. package/dist/C_WaterFall2.js +2 -2
  106. package/dist/C_WaterFall2.js.map +1 -1
  107. package/dist/C_WorkFlow-CSO86Cuc.css.map +1 -0
  108. package/dist/C_WorkFlow2.js +6 -6
  109. package/dist/C_WorkFlow2.js.map +1 -1
  110. package/dist/constants2.d.ts +2 -2
  111. package/dist/constants3.d.ts +2 -2
  112. package/dist/constants4.d.ts +5 -5
  113. package/dist/constants5.d.ts +2 -2
  114. package/dist/data.d.ts +1 -1
  115. package/dist/index.vue.d.ts +1 -1
  116. package/dist/index10.vue.d.ts +5 -5
  117. package/dist/index12.vue.d.ts +1 -1
  118. package/dist/index12.vue.d.ts.map +1 -1
  119. package/dist/index13.vue.d.ts +1 -1
  120. package/dist/index16.vue.d.ts +3 -3
  121. package/dist/index16.vue.d.ts.map +1 -1
  122. package/dist/index2.vue.d.ts +5 -5
  123. package/dist/index3.vue.d.ts +4 -4
  124. package/dist/index6.vue.d.ts +3 -3
  125. package/dist/index8.vue.d.ts +2 -2
  126. package/dist/style.css +1555 -1555
  127. package/dist/useCropperCore.d.ts +7 -7
  128. package/dist/useDraggableLayout.d.ts +9 -9
  129. package/dist/useDynamicFormState.d.ts +9 -9
  130. package/dist/useDynamicFormState.d.ts.map +1 -1
  131. package/dist/useEdgeInteraction.d.ts +2 -2
  132. package/dist/useInfiniteScroll.d.ts +2 -2
  133. package/dist/useModalEdit.d.ts +2 -2
  134. package/dist/useModalEdit.d.ts.map +1 -1
  135. package/dist/useQRCode.d.ts +2 -2
  136. package/dist/useSearchState.d.ts +2 -2
  137. package/dist/useSignatureHistory.d.ts +3 -3
  138. package/dist/useSplitResize.d.ts +6 -6
  139. package/dist/useSplitResize.d.ts.map +1 -1
  140. package/dist/useTimeSelection.d.ts +1 -1
  141. package/dist/useTreeOperations.d.ts +3 -3
  142. package/dist/useWorkflowValidation.d.ts +1 -1
  143. package/package.json +2 -1
  144. package/dist/C_ActionBar-DWN-woTc.css.map +0 -1
  145. package/dist/C_AntV-AFKyK6hH.css.map +0 -1
  146. package/dist/C_Barcode-P_EFj8dC.css.map +0 -1
  147. package/dist/C_Captcha-C-ef41xw.css.map +0 -1
  148. package/dist/C_Cascade-D9kNsjsV.css.map +0 -1
  149. package/dist/C_City-BCQ4ipiK.css.map +0 -1
  150. package/dist/C_Code-C9kvvEmO.css.map +0 -1
  151. package/dist/C_CollapsePanel-BUJHuYcU.css.map +0 -1
  152. package/dist/C_Cron-yx2Ob4Jl.css.map +0 -1
  153. package/dist/C_Draggable-C483syRC.css.map +0 -1
  154. package/dist/C_Editor-Bp0SyIEw.css.map +0 -1
  155. package/dist/C_FilePreview-CPqvhoCy.css.map +0 -1
  156. package/dist/C_Form-Jx7PY3sT.css.map +0 -1
  157. package/dist/C_FormSearch-DvRgxlRn.css.map +0 -1
  158. package/dist/C_FormulaEditor-DtGkt4T_.css.map +0 -1
  159. package/dist/C_FullCalendar-BF7H0YIx.css.map +0 -1
  160. package/dist/C_ImageCropper-BVJfUufl.css.map +0 -1
  161. package/dist/C_Map-DpzeuWdX.css.map +0 -1
  162. package/dist/C_Markdown-BEjxknqd.css.map +0 -1
  163. package/dist/C_NotificationCenter-0l3TY2Gn.css.map +0 -1
  164. package/dist/C_QRCode-DbdiAIPg.css.map +0 -1
  165. package/dist/C_Signature-zhHCbra9.css.map +0 -1
  166. package/dist/C_SplitPane-C6sBsfKY.css.map +0 -1
  167. package/dist/C_Steps-CODHN5Hs.css.map +0 -1
  168. package/dist/C_Table-DSNsntmT.css.map +0 -1
  169. package/dist/C_Time-BvZLYraL.css.map +0 -1
  170. package/dist/C_Tree-0GDv--jX.css.map +0 -1
  171. package/dist/C_Upload-BXd3YYLx.css.map +0 -1
  172. package/dist/C_VideoPlayer-DYG3RL0Q.css.map +0 -1
  173. package/dist/C_VtableGantt-fhItIiHE.css.map +0 -1
  174. package/dist/C_WaterFall-8sQDFXKg.css.map +0 -1
  175. 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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"}