@robot-admin/naive-ui-components 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/C_ActionBar-BAcfOfRw.css.map +1 -0
- package/dist/C_ActionBar-DG2HW7t5.css.map +1 -0
- package/dist/C_ActionBar.css +47 -47
- package/dist/C_ActionBar.js.map +1 -1
- package/dist/C_ActionBar2.js +1 -1
- package/dist/C_ActionBar2.js.map +1 -1
- package/dist/{C_AntV-BY8jqqmi.css.map → C_AntV-D_jTK8oZ.css.map} +1 -1
- package/dist/{C_AntV-BbkHc-_a.css.map → C_AntV-DanTAq22.css.map} +1 -1
- package/dist/C_AntV.css +79 -79
- package/dist/C_AntV.js.map +1 -1
- package/dist/C_AntV2.js +3 -3
- package/dist/C_AntV2.js.map +1 -1
- package/dist/{C_AudioPlayer-DKRIGhi7.css.map → C_AudioPlayer-BnYIJXPD.css.map} +1 -1
- package/dist/{C_AudioPlayer-DlB6eNsa.css.map → C_AudioPlayer-ldTy9Y34.css.map} +1 -1
- package/dist/C_AudioPlayer.css +47 -47
- package/dist/C_AudioPlayer.js.map +1 -1
- package/dist/C_AudioPlayer2.js +1 -1
- package/dist/C_AudioPlayer2.js.map +1 -1
- package/dist/{C_AvatarGroup-D02zci-i.css.map → C_AvatarGroup-B9d-GJ9c.css.map} +1 -1
- package/dist/{C_AvatarGroup-DatHaUlY.css.map → C_AvatarGroup-SIzxuuZr.css.map} +1 -1
- package/dist/C_AvatarGroup.css +21 -21
- package/dist/C_AvatarGroup.js.map +1 -1
- package/dist/C_AvatarGroup2.js +1 -1
- package/dist/C_AvatarGroup2.js.map +1 -1
- package/dist/{C_Barcode-DYDRDe2O.css.map → C_Barcode-CAU2mFcx.css.map} +1 -1
- package/dist/{C_Barcode-muJdc_O4.css.map → C_Barcode-D2oJaEg3.css.map} +1 -1
- package/dist/C_Barcode.css +7 -7
- package/dist/C_Barcode.js.map +1 -1
- package/dist/C_Barcode2.js +1 -1
- package/dist/C_Barcode2.js.map +1 -1
- package/dist/C_Breadcrumb.js.map +1 -1
- package/dist/C_Breadcrumb2.js +1 -1
- package/dist/C_Breadcrumb2.js.map +1 -1
- package/dist/{C_Captcha-9_hk6VmY.css.map → C_Captcha-B3yrtG7t.css.map} +1 -1
- package/dist/{C_Captcha-MtASlJ0f.css.map → C_Captcha-D4hDdWDu.css.map} +1 -1
- package/dist/C_Captcha.css +19 -19
- package/dist/C_Captcha.js.map +1 -1
- package/dist/C_Captcha2.js +1 -1
- package/dist/C_Captcha2.js.map +1 -1
- package/dist/C_Cascade-BE7t1OX5.css.map +1 -0
- package/dist/C_Cascade-CpDiau1y.css.map +1 -0
- package/dist/C_Cascade.css +3 -3
- package/dist/C_Cascade.js.map +1 -1
- package/dist/C_Cascade2.js +1 -1
- package/dist/C_Cascade2.js.map +1 -1
- package/dist/{C_Chat-Csjz_BFw.css.map → C_Chat-CF1SU7mc.css.map} +1 -1
- package/dist/{C_Chat-DO1ZBA_0.css.map → C_Chat-DeUv6uuI.css.map} +1 -1
- package/dist/C_Chat.css +65 -65
- package/dist/C_Chat.js.map +1 -1
- package/dist/C_Chat2.js +1 -1
- package/dist/C_Chat2.js.map +1 -1
- package/dist/{C_City-Bam08Qv3.css.map → C_City-DdkX6u64.css.map} +1 -1
- package/dist/{C_City-Bb3W-KaN.css.map → C_City-iP-dIvui.css.map} +1 -1
- package/dist/C_City.css +29 -29
- package/dist/C_City.js.map +1 -1
- package/dist/C_City2.js +1 -1
- package/dist/C_City2.js.map +1 -1
- package/dist/C_Code-BWmJMNS7.css.map +1 -0
- package/dist/C_Code-k14vjgVl.css.map +1 -0
- package/dist/C_Code.css +3 -3
- package/dist/C_Code.js.map +1 -1
- package/dist/C_Code2.js +1 -1
- package/dist/C_Code2.js.map +1 -1
- package/dist/{C_CollapsePanel-CWkFCqNQ.css.map → C_CollapsePanel-BSKcWSmY.css.map} +1 -1
- package/dist/{C_CollapsePanel-pcVJA_4z.css.map → C_CollapsePanel-DRvRq8tB.css.map} +1 -1
- package/dist/C_CollapsePanel.css +35 -35
- package/dist/C_CollapsePanel.js.map +1 -1
- package/dist/C_CollapsePanel2.js +1 -1
- package/dist/C_CollapsePanel2.js.map +1 -1
- package/dist/{C_ContextMenu-B3Cgz_wJ.css.map → C_ContextMenu-Bn_uhJSo.css.map} +1 -1
- package/dist/{C_ContextMenu-u1LixF3_.css.map → C_ContextMenu-C9iuxG2n.css.map} +1 -1
- package/dist/C_ContextMenu.css +17 -17
- package/dist/C_ContextMenu.js.map +1 -1
- package/dist/C_ContextMenu2.js +1 -1
- package/dist/C_ContextMenu2.js.map +1 -1
- package/dist/{C_Cron-Civir5-I.css.map → C_Cron-BIQ2By9v.css.map} +1 -1
- package/dist/{C_Cron-iw3qpMsy.css.map → C_Cron-BNSAGD6A.css.map} +1 -1
- package/dist/C_Cron.css +57 -57
- package/dist/C_Cron.js.map +1 -1
- package/dist/C_Cron2.js +1 -1
- package/dist/C_Cron2.js.map +1 -1
- package/dist/C_Date.js.map +1 -1
- package/dist/C_Date2.js +1 -1
- package/dist/C_Date2.js.map +1 -1
- package/dist/{C_Draggable-BCjIvIwT.css.map → C_Draggable-BDAbk4s5.css.map} +1 -1
- package/dist/{C_Draggable-DwkSlCOS.css.map → C_Draggable-UhfDLXop.css.map} +1 -1
- package/dist/C_Draggable.css +46 -46
- package/dist/C_Draggable.js.map +1 -1
- package/dist/C_Draggable2.js +1 -1
- package/dist/C_Draggable2.js.map +1 -1
- package/dist/{C_Editor-BfjjYQ9B.css.map → C_Editor-CZOaChru.css.map} +1 -1
- package/dist/{C_Editor-Do2StvoJ.css.map → C_Editor-kJAQq5z6.css.map} +1 -1
- package/dist/C_Editor.css +14 -14
- package/dist/C_Editor.js.map +1 -1
- package/dist/C_Editor2.js +1 -1
- package/dist/C_Editor2.js.map +1 -1
- package/dist/{C_FilePreview-BhcbiXsV.css.map → C_FilePreview-Bs_omgJS.css.map} +1 -1
- package/dist/{C_FilePreview-DJudcIbW.css.map → C_FilePreview-DbwoyF2j.css.map} +1 -1
- package/dist/C_FilePreview.css +91 -91
- package/dist/C_FilePreview.js.map +1 -1
- package/dist/C_FilePreview2.js +1 -1
- package/dist/C_FilePreview2.js.map +1 -1
- package/dist/{C_Form-BtcAPIRi.css.map → C_Form-SCq-DZaZ.css.map} +1 -1
- package/dist/{C_Form-D_gwQ7hC.css.map → C_Form-mcd1nKvF.css.map} +1 -1
- package/dist/C_Form.css +223 -223
- package/dist/C_Form.js.map +1 -1
- package/dist/C_Form2.js +1 -1
- package/dist/C_Form2.js.map +1 -1
- package/dist/{C_FormSearch-Bi4XYjjk.css.map → C_FormSearch-C3F8peAY.css.map} +1 -1
- package/dist/{C_FormSearch-DUGOoX0u.css.map → C_FormSearch-qW56bJPo.css.map} +1 -1
- package/dist/C_FormSearch.css +45 -45
- package/dist/C_FormSearch.js.map +1 -1
- package/dist/C_FormSearch2.js +1 -1
- package/dist/C_FormSearch2.js.map +1 -1
- package/dist/{C_FormulaEditor-CJk3SSUJ.css.map → C_FormulaEditor-ChOvDbsZ.css.map} +1 -1
- package/dist/{C_FormulaEditor-Cy7mjfVO.css.map → C_FormulaEditor-mfiIUDY4.css.map} +1 -1
- package/dist/C_FormulaEditor.css +78 -78
- package/dist/C_FormulaEditor.js.map +1 -1
- package/dist/C_FormulaEditor2.js +1 -1
- package/dist/C_FormulaEditor2.js.map +1 -1
- package/dist/C_FullCalendar-C2nsqrgS.css.map +1 -0
- package/dist/C_FullCalendar-DdFtXVhy.css.map +1 -0
- package/dist/C_FullCalendar.css +2 -2
- package/dist/C_FullCalendar.js.map +1 -1
- package/dist/C_FullCalendar2.js +1 -1
- package/dist/C_FullCalendar2.js.map +1 -1
- package/dist/{C_GlobalSearch-B6wK3m3k.css.map → C_GlobalSearch-Dq4Wo01K.css.map} +1 -1
- package/dist/{C_GlobalSearch-DoPjldFm.css.map → C_GlobalSearch-RB7J43gu.css.map} +1 -1
- package/dist/C_GlobalSearch.css +112 -112
- package/dist/C_GlobalSearch.js.map +1 -1
- package/dist/C_GlobalSearch2.js +1 -1
- package/dist/C_GlobalSearch2.js.map +1 -1
- package/dist/C_Guide.js.map +1 -1
- package/dist/C_Guide2.js +1 -1
- package/dist/C_Guide2.js.map +1 -1
- package/dist/C_Icon.js.map +1 -1
- package/dist/C_Icon2.js.map +1 -1
- package/dist/{C_ImageCropper-BqZpkZfU.css.map → C_ImageCropper-CdTX2nxU.css.map} +1 -1
- package/dist/{C_ImageCropper-CylwGuzS.css.map → C_ImageCropper-ilOqPklJ.css.map} +1 -1
- package/dist/C_ImageCropper.css +23 -23
- package/dist/C_ImageCropper.js.map +1 -1
- package/dist/C_ImageCropper2.js +1 -1
- package/dist/C_ImageCropper2.js.map +1 -1
- package/dist/C_Language.js.map +1 -1
- package/dist/C_Language2.js +1 -1
- package/dist/C_Language2.js.map +1 -1
- package/dist/{C_Login-BkdnkBKX.css.map → C_Login-B3qYkLrN.css.map} +1 -1
- package/dist/{C_Login-VkMM6OJ2.css.map → C_Login-xMI1_ojE.css.map} +1 -1
- package/dist/C_Login.css +72 -72
- package/dist/C_Login.js.map +1 -1
- package/dist/C_Login2.js +1 -1
- package/dist/C_Login2.js.map +1 -1
- package/dist/{C_Map-DNnBcY70.css.map → C_Map-CWMMN58_.css.map} +1 -1
- package/dist/{C_Map-LEXIx6zM.css.map → C_Map-xSR2H-9o.css.map} +1 -1
- package/dist/C_Map.css +4 -4
- package/dist/C_Map.js.map +1 -1
- package/dist/C_Map2.js +1 -1
- package/dist/C_Map2.js.map +1 -1
- package/dist/{C_Markdown-DwmI0_0Q.css.map → C_Markdown-C7lTVJ1S.css.map} +1 -1
- package/dist/{C_Markdown-k0794RW0.css.map → C_Markdown-CZDm_baQ.css.map} +1 -1
- package/dist/C_Markdown.css +33 -33
- package/dist/C_Markdown.js.map +1 -1
- package/dist/C_Markdown2.js +1 -1
- package/dist/C_Markdown2.js.map +1 -1
- package/dist/C_Menu.js.map +1 -1
- package/dist/C_Menu2.js +1 -1
- package/dist/C_Menu2.js.map +1 -1
- package/dist/{C_NotificationCenter-DT6fQ79O.css.map → C_NotificationCenter-D7GGGWg7.css.map} +1 -1
- package/dist/{C_NotificationCenter-DbgBiyqB.css.map → C_NotificationCenter-SSmeHA45.css.map} +1 -1
- package/dist/C_NotificationCenter.css +89 -89
- package/dist/C_NotificationCenter.js.map +1 -1
- package/dist/C_NotificationCenter2.js +1 -1
- package/dist/C_NotificationCenter2.js.map +1 -1
- package/dist/C_OrgChart.js.map +1 -1
- package/dist/C_OrgChart2.js.map +1 -1
- package/dist/C_Progress.js.map +1 -1
- package/dist/C_Progress2.js +1 -1
- package/dist/C_Progress2.js.map +1 -1
- package/dist/{C_QRCode-CFNTda_1.css.map → C_QRCode-B_1lHUdx.css.map} +1 -1
- package/dist/{C_QRCode-CmWr9lK-.css.map → C_QRCode-CuQRYu0s.css.map} +1 -1
- package/dist/C_QRCode.css +9 -9
- package/dist/C_QRCode.js.map +1 -1
- package/dist/C_QRCode2.js +1 -1
- package/dist/C_QRCode2.js.map +1 -1
- package/dist/{C_Signature-BDx4ctsE.css.map → C_Signature-Bp-RBpAl.css.map} +1 -1
- package/dist/{C_Signature-Bz_uNbwv.css.map → C_Signature-Cb2VW1ok.css.map} +1 -1
- package/dist/C_Signature.css +15 -15
- package/dist/C_Signature.js.map +1 -1
- package/dist/C_Signature2.js +1 -1
- package/dist/C_Signature2.js.map +1 -1
- package/dist/C_Skeleton.js.map +1 -1
- package/dist/C_Skeleton2.js.map +1 -1
- package/dist/{C_SplitPane-BOwr1C7K.css.map → C_SplitPane-CgGWYsRE.css.map} +1 -1
- package/dist/{C_SplitPane-N-a_D-7F.css.map → C_SplitPane-DGf6WEeR.css.map} +1 -1
- package/dist/C_SplitPane.css +31 -31
- package/dist/C_SplitPane.js.map +1 -1
- package/dist/C_SplitPane2.js +1 -1
- package/dist/C_SplitPane2.js.map +1 -1
- package/dist/C_Steps-Buyg7Axn.css.map +1 -0
- package/dist/C_Steps-DNY91VbQ.css.map +1 -0
- package/dist/C_Steps.css +38 -38
- package/dist/C_Steps.js.map +1 -1
- package/dist/C_Steps2.js +1 -1
- package/dist/C_Steps2.js.map +1 -1
- package/dist/{C_Table-BcM5LqAM.css.map → C_Table--kbQaaBv.css.map} +1 -1
- package/dist/{C_Table-HATVoq1F.css.map → C_Table-D631kdXC.css.map} +1 -1
- package/dist/C_Table.css +104 -104
- package/dist/C_Table.js.map +1 -1
- package/dist/C_Table2.js +2 -2
- package/dist/C_Table2.js.map +1 -1
- package/dist/{C_TagsView-DX3LecK1.css.map → C_TagsView-COjGNM9h.css.map} +1 -1
- package/dist/{C_TagsView-DqOTa8bL.css.map → C_TagsView-zUDj9S06.css.map} +1 -1
- package/dist/C_TagsView.css +36 -36
- package/dist/C_TagsView.js.map +1 -1
- package/dist/C_TagsView2.js +1 -1
- package/dist/C_TagsView2.js.map +1 -1
- package/dist/C_Theme.js.map +1 -1
- package/dist/C_Theme2.js +1 -1
- package/dist/C_Theme2.js.map +1 -1
- package/dist/C_Time-BQ9PGy8B.css.map +1 -0
- package/dist/C_Time-CO73krC6.css.map +1 -0
- package/dist/C_Time.css +5 -5
- package/dist/C_Time.js.map +1 -1
- package/dist/C_Time2.js +1 -1
- package/dist/C_Time2.js.map +1 -1
- package/dist/{C_Timeline-BKkimrZx.css.map → C_Timeline-4rk0Dzgn.css.map} +1 -1
- package/dist/{C_Timeline-DTuSCX6p.css.map → C_Timeline-DdESmqwL.css.map} +1 -1
- package/dist/C_Timeline.css +45 -45
- package/dist/C_Timeline.js.map +1 -1
- package/dist/C_Timeline2.js +1 -1
- package/dist/C_Timeline2.js.map +1 -1
- package/dist/{C_Transfer-BOB6OBqS.css.map → C_Transfer-B-vjvhjI.css.map} +1 -1
- package/dist/{C_Transfer-Dpq7ezWC.css.map → C_Transfer-EzdHzZNv.css.map} +1 -1
- package/dist/C_Transfer.css +31 -31
- package/dist/C_Transfer.js.map +1 -1
- package/dist/C_Transfer2.js +1 -1
- package/dist/C_Transfer2.js.map +1 -1
- package/dist/{C_Tree-D98nX9qT.css.map → C_Tree-CsXgGfhn.css.map} +1 -1
- package/dist/{C_Tree-ulfm7b-P.css.map → C_Tree-P7eRpnOa.css.map} +1 -1
- package/dist/C_Tree.css +33 -33
- package/dist/C_Tree.js.map +1 -1
- package/dist/C_Tree2.js +1 -1
- package/dist/C_Tree2.js.map +1 -1
- package/dist/{C_Upload-BR9fg5r4.css.map → C_Upload-CHjiVVh7.css.map} +1 -1
- package/dist/{C_Upload-CPqf74Ea.css.map → C_Upload-DE3OLsZD.css.map} +1 -1
- package/dist/C_Upload.css +50 -50
- package/dist/C_Upload.js.map +1 -1
- package/dist/C_Upload2.js +2 -2
- package/dist/C_Upload2.js.map +1 -1
- package/dist/{C_VideoPlayer-4J96tKfK.css.map → C_VideoPlayer-CnaNOFtW.css.map} +1 -1
- package/dist/{C_VideoPlayer-D35seHkm.css.map → C_VideoPlayer-cnP88zNV.css.map} +1 -1
- package/dist/C_VideoPlayer.css +68 -68
- package/dist/C_VideoPlayer.js.map +1 -1
- package/dist/C_VideoPlayer2.js +3 -3
- package/dist/C_VideoPlayer2.js.map +1 -1
- package/dist/{C_VtableGantt-C2lxFmWg.css.map → C_VtableGantt-CZlJgvpC.css.map} +1 -1
- package/dist/{C_VtableGantt-Dwnkw_WL.css.map → C_VtableGantt-DKfQkql0.css.map} +1 -1
- package/dist/C_VtableGantt.css +15 -15
- package/dist/C_VtableGantt.js.map +1 -1
- package/dist/C_VtableGantt2.js +1 -1
- package/dist/C_VtableGantt2.js.map +1 -1
- package/dist/{C_WaterFall-C4HDqf-a.css.map → C_WaterFall-BJ_39ext.css.map} +1 -1
- package/dist/{C_WaterFall-HWB-gPON.css.map → C_WaterFall-cYc1vntE.css.map} +1 -1
- package/dist/C_WaterFall.css +16 -16
- package/dist/C_WaterFall.js.map +1 -1
- package/dist/C_WaterFall2.js +1 -1
- package/dist/C_WaterFall2.js.map +1 -1
- package/dist/{C_WorkFlow-2hJfvvTK.css.map → C_WorkFlow-BFzRGGhJ.css.map} +1 -1
- package/dist/{C_WorkFlow-DHoNjl8-.css.map → C_WorkFlow-DSyCOvRp.css.map} +1 -1
- package/dist/C_WorkFlow.css +193 -193
- package/dist/C_WorkFlow.js.map +1 -1
- package/dist/C_WorkFlow2.js +1 -1
- package/dist/C_WorkFlow2.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants2.d.ts +2 -2
- package/dist/constants3.d.ts +1 -1
- package/dist/constants4.d.ts +1 -1
- package/dist/constants5.d.ts +2 -2
- package/dist/data.d.ts +1 -1
- package/dist/index.vue.d.ts +2 -2
- package/dist/index11.vue.d.ts +2 -2
- package/dist/index12.vue.d.ts +1 -1
- package/dist/index13.vue.d.ts +2 -2
- package/dist/index14.vue.d.ts +3 -3
- package/dist/index15.vue.d.ts +6 -6
- package/dist/index17.vue.d.ts +1 -1
- package/dist/index18.vue.d.ts +2 -2
- package/dist/index2.vue.d.ts +6 -6
- package/dist/index20.vue.d.ts +1 -1
- package/dist/index21.vue.d.ts +2 -2
- package/dist/index22.vue.d.ts +2 -2
- package/dist/index24.vue.d.ts +1 -1
- package/dist/index26.vue.d.ts +3 -3
- package/dist/index27.vue.d.ts +1 -1
- package/dist/index28.vue.d.ts +2 -2
- package/dist/index3.vue.d.ts +2 -2
- package/dist/index4.vue.d.ts +2 -2
- package/dist/index6.vue.d.ts +3 -3
- package/dist/style.css +1991 -1991
- package/dist/useCalendarEvents.d.ts +1 -1
- package/dist/useCollapsePanel.d.ts +3 -3
- package/dist/useCropperCore.d.ts +4 -4
- package/dist/useDraggableLayout.d.ts +2 -2
- package/dist/useDynamicFormState.d.ts +13 -13
- package/dist/useEdgeInteraction.d.ts +1 -1
- package/dist/useInfiniteScroll.d.ts +3 -3
- package/dist/useModalEdit.d.ts +3 -3
- package/dist/useQRCode.d.ts +3 -3
- package/dist/useSignatureHistory.d.ts +1 -1
- package/dist/useSplitResize.d.ts +5 -5
- package/dist/useTimeSelection.d.ts +2 -2
- package/dist/useTreeOperations.d.ts +4 -4
- package/dist/useWorkflowValidation.d.ts +4 -4
- package/package.json +4 -7
- package/dist/C_ActionBar-DYG0Yf3A.css.map +0 -1
- package/dist/C_ActionBar-MAjRhqcI.css.map +0 -1
- package/dist/C_Cascade-Dq0S5Jkm.css.map +0 -1
- package/dist/C_Cascade-IUUHIh7r.css.map +0 -1
- package/dist/C_Code-BFi5xxnM.css.map +0 -1
- package/dist/C_Code-CZ7yfnri.css.map +0 -1
- package/dist/C_FullCalendar-BQA0OjGB.css.map +0 -1
- package/dist/C_FullCalendar-ChJUosAi.css.map +0 -1
- package/dist/C_Steps-Bk1krgiR.css.map +0 -1
- package/dist/C_Steps-Dn2CMpy1.css.map +0 -1
- package/dist/C_Time-BzyQVvTS.css.map +0 -1
- package/dist/C_Time-DmpKLQHc.css.map +0 -1
- package/dist/themes/glass-morphism.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_NotificationCenter2.js","names":[],"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/composables/useNotificationFormat.ts","../src/components/C_NotificationCenter/components/NotificationItem.vue","../src/components/C_NotificationCenter/components/NotificationList.vue","../src/components/C_NotificationCenter/components/NotificationDetail.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/** 时间单位阈值 */\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 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=\"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/composables/useNotificationFormat.ts","../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/NotificationDetail.vue","../src/components/C_NotificationCenter/components/NotificationDetail.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/** 时间单位阈值 */\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 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=\"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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"}
|