@robot-admin/naive-ui-components 0.8.1 → 0.9.0
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/README.md +294 -279
- package/dist/C_ActionBar-DYG0Yf3A.css.map +1 -0
- package/dist/C_ActionBar-MAjRhqcI.css.map +1 -0
- package/dist/C_ActionBar.cjs +1 -1
- package/dist/C_ActionBar.css +47 -47
- package/dist/C_ActionBar.js +1 -1
- 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-A3NXx_sF.css.map → C_AntV-BY8jqqmi.css.map} +1 -1
- package/dist/{C_AntV-D2Zp2qQF.css.map → C_AntV-BbkHc-_a.css.map} +1 -1
- package/dist/C_AntV.cjs +1 -1
- package/dist/C_AntV.css +78 -78
- package/dist/C_AntV.js +1 -1
- 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-CXl4yP_m.css.map → C_AudioPlayer-DKRIGhi7.css.map} +1 -1
- package/dist/{C_AudioPlayer-CcBtt71m.css.map → C_AudioPlayer-DlB6eNsa.css.map} +1 -1
- package/dist/C_AudioPlayer.cjs +1 -1
- package/dist/C_AudioPlayer.css +47 -47
- package/dist/C_AudioPlayer.js +1 -1
- 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-CtPhfH9h.css.map → C_AvatarGroup-D02zci-i.css.map} +1 -1
- package/dist/{C_AvatarGroup-M1dNPH3E.css.map → C_AvatarGroup-DatHaUlY.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-DjTmDkbQ.css.map → C_Barcode-DYDRDe2O.css.map} +1 -1
- package/dist/{C_Barcode-Do1Ozsmh.css.map → C_Barcode-muJdc_O4.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.cjs +1 -1
- package/dist/C_Breadcrumb.js +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-CZmAETly.css.map → C_Captcha-9_hk6VmY.css.map} +1 -1
- package/dist/{C_Captcha-Ccq3DMrR.css.map → C_Captcha-MtASlJ0f.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.js.map +1 -1
- package/dist/C_Cascade2.js +1 -1
- package/dist/C_Cascade2.js.map +1 -1
- package/dist/{C_Chat-CJ8KESTM.css.map → C_Chat-Csjz_BFw.css.map} +1 -1
- package/dist/{C_Chat-DzV4AFLd.css.map → C_Chat-DO1ZBA_0.css.map} +1 -1
- package/dist/C_Chat.cjs +1 -1
- package/dist/C_Chat.css +65 -65
- package/dist/C_Chat.js +1 -1
- 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-Cv5BESaN.css.map → C_City-Bam08Qv3.css.map} +1 -1
- package/dist/{C_City-DNKv4MpH.css.map → C_City-Bb3W-KaN.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-BFi5xxnM.css.map +1 -0
- package/dist/C_Code-CZ7yfnri.css.map +1 -0
- package/dist/C_Code.cjs +1 -1
- package/dist/C_Code.css +3 -3
- package/dist/C_Code.js +1 -1
- 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-CedsEM0N.css.map → C_CollapsePanel-CWkFCqNQ.css.map} +1 -1
- package/dist/{C_CollapsePanel-BkOivqn3.css.map → C_CollapsePanel-pcVJA_4z.css.map} +1 -1
- package/dist/C_CollapsePanel.cjs +1 -1
- package/dist/C_CollapsePanel.css +35 -35
- package/dist/C_CollapsePanel.js +1 -1
- 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-CCsCeI39.css.map → C_ContextMenu-B3Cgz_wJ.css.map} +1 -1
- package/dist/{C_ContextMenu-DOTBeltf.css.map → C_ContextMenu-u1LixF3_.css.map} +1 -1
- package/dist/C_ContextMenu.cjs +1 -1
- package/dist/C_ContextMenu.css +17 -17
- package/dist/C_ContextMenu.js +1 -1
- 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-C0-8b5af.css.map → C_Cron-Civir5-I.css.map} +1 -1
- package/dist/{C_Cron-DqHQ92LN.css.map → C_Cron-iw3qpMsy.css.map} +1 -1
- package/dist/C_Cron.cjs +1 -1
- package/dist/C_Cron.css +37 -37
- package/dist/C_Cron.js +1 -1
- 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-Bq6o0qXn.css.map → C_Draggable-BCjIvIwT.css.map} +1 -1
- package/dist/{C_Draggable-r6nhx3yM.css.map → C_Draggable-DwkSlCOS.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 +1 -0
- package/dist/C_Editor-Do2StvoJ.css.map +1 -0
- package/dist/C_Editor.css +55 -55
- 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-BVarVvnD.css.map → C_FilePreview-BhcbiXsV.css.map} +1 -1
- package/dist/{C_FilePreview-DHGlV4OR.css.map → C_FilePreview-DJudcIbW.css.map} +1 -1
- package/dist/C_FilePreview.cjs +1 -1
- package/dist/C_FilePreview.css +91 -91
- package/dist/C_FilePreview.js +1 -1
- 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-BciQUPpS.css.map → C_Form-BtcAPIRi.css.map} +1 -1
- package/dist/{C_Form-CKHvXL3O.css.map → C_Form-D_gwQ7hC.css.map} +1 -1
- package/dist/C_Form.cjs +1 -1
- package/dist/C_Form.css +222 -222
- package/dist/C_Form.js +1 -1
- 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-Bmd2CmG6.css.map → C_FormSearch-Bi4XYjjk.css.map} +1 -1
- package/dist/{C_FormSearch-DBNtnjaL.css.map → C_FormSearch-DUGOoX0u.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-3WG2vtQl.css.map → C_FormulaEditor-CJk3SSUJ.css.map} +1 -1
- package/dist/{C_FormulaEditor-Cm0CokN5.css.map → C_FormulaEditor-Cy7mjfVO.css.map} +1 -1
- package/dist/C_FormulaEditor.cjs +1 -1
- package/dist/C_FormulaEditor.css +78 -78
- package/dist/C_FormulaEditor.js +1 -1
- 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-BQA0OjGB.css.map +1 -0
- package/dist/C_FullCalendar-ChJUosAi.css.map +1 -0
- package/dist/C_FullCalendar.css +3 -3
- 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.js.map +1 -1
- package/dist/C_GlobalSearch2.js.map +1 -1
- package/dist/C_Guide.cjs +1 -1
- package/dist/C_Guide.js +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 +1 -1
- package/dist/C_Icon2.js.map +1 -1
- package/dist/{C_ImageCropper-D3L2-J_J.css.map → C_ImageCropper-BqZpkZfU.css.map} +1 -1
- package/dist/{C_ImageCropper-DrmUlaLi.css.map → C_ImageCropper-CylwGuzS.css.map} +1 -1
- package/dist/C_ImageCropper.cjs +1 -1
- package/dist/C_ImageCropper.css +10 -10
- package/dist/C_ImageCropper.js +1 -1
- 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.cjs +1 -1
- package/dist/C_Language.js +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-Cst_W1XC.css.map → C_Login-BkdnkBKX.css.map} +1 -1
- package/dist/{C_Login-DWZ_6K1x.css.map → C_Login-VkMM6OJ2.css.map} +1 -1
- package/dist/C_Login.cjs +1 -1
- package/dist/C_Login.css +72 -72
- package/dist/C_Login.js +1 -1
- 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-BCJmDnJT.css.map → C_Map-DNnBcY70.css.map} +1 -1
- package/dist/{C_Map-WUMXSAfy.css.map → C_Map-LEXIx6zM.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-7J0uPEuk.css.map → C_Markdown-DwmI0_0Q.css.map} +1 -1
- package/dist/{C_Markdown-C7Lg0_9z.css.map → C_Markdown-k0794RW0.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.cjs +1 -1
- package/dist/C_NotificationCenter.js +1 -1
- 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 +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-DMxSyogU.css.map → C_QRCode-CFNTda_1.css.map} +1 -1
- package/dist/{C_QRCode-G7fiAkm4.css.map → C_QRCode-CmWr9lK-.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-BJMvVPWT.css.map → C_Signature-BDx4ctsE.css.map} +1 -1
- package/dist/{C_Signature-BJUHx2BF.css.map → C_Signature-Bz_uNbwv.css.map} +1 -1
- package/dist/C_Signature.cjs +1 -1
- package/dist/C_Signature.css +15 -15
- package/dist/C_Signature.js +1 -1
- 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 +1 -1
- package/dist/C_Skeleton2.js.map +1 -1
- package/dist/{C_SplitPane-Br2eK8IG.css.map → C_SplitPane-BOwr1C7K.css.map} +1 -1
- package/dist/{C_SplitPane-DuyzUhKB.css.map → C_SplitPane-N-a_D-7F.css.map} +1 -1
- package/dist/C_SplitPane.cjs +1 -1
- package/dist/C_SplitPane.css +31 -31
- package/dist/C_SplitPane.js +1 -1
- 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-Bk1krgiR.css.map +1 -0
- package/dist/C_Steps-Dn2CMpy1.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.cjs +1 -1
- package/dist/C_Table.js +1 -1
- 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-BsutyC3U.css.map → C_TagsView-DX3LecK1.css.map} +1 -1
- package/dist/{C_TagsView-Crm7yWg9.css.map → C_TagsView-DqOTa8bL.css.map} +1 -1
- package/dist/C_TagsView.cjs +1 -1
- package/dist/C_TagsView.css +36 -36
- package/dist/C_TagsView.js +1 -1
- 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.cjs +1 -1
- package/dist/C_Theme.js +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-BzyQVvTS.css.map +1 -0
- package/dist/C_Time-DmpKLQHc.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-DG70WtJP.css.map → C_Timeline-BKkimrZx.css.map} +1 -1
- package/dist/{C_Timeline-Dzxmmdjg.css.map → C_Timeline-DTuSCX6p.css.map} +1 -1
- package/dist/C_Timeline.cjs +1 -1
- package/dist/C_Timeline.css +45 -45
- package/dist/C_Timeline.js +1 -1
- 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-CCEqnC-v.css.map → C_Transfer-BOB6OBqS.css.map} +1 -1
- package/dist/{C_Transfer-CStPm4NP.css.map → C_Transfer-Dpq7ezWC.css.map} +1 -1
- package/dist/C_Transfer.cjs +1 -1
- package/dist/C_Transfer.css +31 -31
- package/dist/C_Transfer.js +1 -1
- 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-B-Le4hjQ.css.map → C_Tree-D98nX9qT.css.map} +1 -1
- package/dist/{C_Tree-DnGc_MPb.css.map → C_Tree-ulfm7b-P.css.map} +1 -1
- package/dist/C_Tree.cjs +1 -1
- package/dist/C_Tree.css +33 -33
- package/dist/C_Tree.js +1 -1
- 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-CFAxVGVz.css.map → C_Upload-BR9fg5r4.css.map} +1 -1
- package/dist/{C_Upload-D8_QeSV0.css.map → C_Upload-CPqf74Ea.css.map} +1 -1
- package/dist/C_Upload.cjs +1 -1
- package/dist/C_Upload.css +4 -4
- package/dist/C_Upload.js +1 -1
- 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-Cdgv4Zq0.css.map → C_VideoPlayer-4J96tKfK.css.map} +1 -1
- package/dist/{C_VideoPlayer-rm0MODUv.css.map → C_VideoPlayer-D35seHkm.css.map} +1 -1
- package/dist/C_VideoPlayer.css +13 -13
- 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-BpY-Rng3.css.map → C_VtableGantt-C2lxFmWg.css.map} +1 -1
- package/dist/{C_VtableGantt-DB87-jdS.css.map → C_VtableGantt-Dwnkw_WL.css.map} +1 -1
- package/dist/C_VtableGantt.cjs +1 -1
- package/dist/C_VtableGantt.css +15 -15
- package/dist/C_VtableGantt.js +1 -1
- 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.js.map +1 -1
- package/dist/C_WaterFall2.js +1 -1
- package/dist/C_WaterFall2.js.map +1 -1
- package/dist/{C_WorkFlow-7F6op6qN.css.map → C_WorkFlow-2hJfvvTK.css.map} +1 -1
- package/dist/{C_WorkFlow-Cy3HWA6A.css.map → C_WorkFlow-DHoNjl8-.css.map} +1 -1
- package/dist/C_WorkFlow.cjs +1 -1
- package/dist/C_WorkFlow.css +108 -108
- package/dist/C_WorkFlow.js +1 -1
- 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 +3 -3
- package/dist/constants2.d.ts +4 -4
- package/dist/constants3.d.ts +3 -3
- package/dist/constants4.d.ts +4 -4
- package/dist/constants5.d.ts +2 -2
- package/dist/data.d.ts +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.vue.d.ts +1 -1
- package/dist/index10.vue.d.ts +1 -1
- package/dist/index11.vue.d.ts +4 -4
- 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 +3 -3
- package/dist/index16.vue.d.ts +2 -2
- package/dist/index17.vue.d.ts +1 -1
- package/dist/index18.vue.d.ts +5 -5
- package/dist/index2.vue.d.ts +1 -1
- package/dist/index20.vue.d.ts +1 -1
- package/dist/index22.vue.d.ts +1 -1
- package/dist/index23.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 +2 -2
- package/dist/index28.vue.d.ts +3 -3
- package/dist/index3.vue.d.ts +3 -3
- package/dist/index4.vue.d.ts +2 -2
- package/dist/index6.vue.d.ts +6 -6
- package/dist/index7.vue.d.ts +2 -2
- package/dist/index8.vue.d.ts +1 -1
- package/dist/index9.vue.d.ts +2 -2
- package/dist/menuAdapter.js.map +1 -1
- package/dist/resolver.cjs.map +1 -1
- package/dist/resolver.js.map +1 -1
- package/dist/style.css +1483 -1483
- package/dist/themes/glass-morphism.css +1 -0
- package/dist/useCalendarEvents.d.ts +1 -1
- package/dist/useCollapsePanel.d.ts +1 -1
- package/dist/useCropperCore.d.ts +2 -2
- package/dist/useDraggableLayout.d.ts +4 -4
- package/dist/useDynamicFormState.d.ts +21 -21
- package/dist/useEdgeInteraction.d.ts +2 -2
- package/dist/useModalEdit.d.ts +1 -1
- package/dist/useQRCode.d.ts +3 -3
- package/dist/useSearchState.d.ts +2 -2
- package/dist/useSignatureHistory.d.ts +4 -4
- package/dist/useSplitResize.d.ts +2 -2
- package/dist/useTimeSelection.d.ts +2 -2
- package/dist/useTreeOperations.d.ts +3 -3
- package/dist/useWorkflowValidation.d.ts +6 -6
- package/package.json +11 -8
- package/dist/C_ActionBar-jZPsiXtT.css.map +0 -1
- package/dist/C_ActionBar-nnfbZCea.css.map +0 -1
- package/dist/C_Code-DPZlNSxL.css.map +0 -1
- package/dist/C_Code-avZjAnHD.css.map +0 -1
- package/dist/C_Editor-C49qURgb.css.map +0 -1
- package/dist/C_Editor-DuxBD9Qg.css.map +0 -1
- package/dist/C_FullCalendar-1U4VlG7X.css.map +0 -1
- package/dist/C_FullCalendar-DLUdCtqf.css.map +0 -1
- package/dist/C_Steps-BHYNJI1l.css.map +0 -1
- package/dist/C_Steps-DY7vl-4I.css.map +0 -1
- package/dist/C_Time-Bd_e1YDN.css.map +0 -1
- package/dist/C_Time-CKb_fwdm.css.map +0 -1
package/dist/C_Login2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Login2.js","names":[],"sources":["../src/components/C_Login/types.ts","../src/components/C_Login/index.vue","../src/components/C_Login/index.vue","../src/components/C_Login/index.vue"],"sourcesContent":["/**\r\n * @Description: C_Login 通用登录组件 - 类型定义\r\n */\r\n\r\n// ================= 功能开关 =================\r\nexport interface LoginFeatures {\r\n /** 是否显示密码登录(默认 true) */\r\n passwordLogin?: boolean\r\n /** 是否显示验证码登录(默认 true) */\r\n captchaLogin?: boolean\r\n /** 是否显示扫码登录(默认 true) */\r\n qrcodeLogin?: boolean\r\n /** 是否显示其他账号登录(默认 true) */\r\n socialLogin?: boolean\r\n /** 是否显示注册账号(默认 true) */\r\n register?: boolean\r\n /** 是否启用人机验证拼图(默认 true) */\r\n captchaVerify?: boolean\r\n /** 是否显示记住我(默认 true) */\r\n rememberMe?: boolean\r\n /** 是否显示忘记密码(默认 true) */\r\n forgotPassword?: boolean\r\n}\r\n\r\n// ================= 社交登录 =================\r\nexport interface SocialProvider {\r\n key: string\r\n label: string\r\n /** mdi 图标名称 */\r\n icon: string\r\n}\r\n\r\n// ================= 表单数据 =================\r\nexport interface PasswordFormData {\r\n username: string\r\n password: string\r\n}\r\n\r\nexport interface CaptchaFormData {\r\n account: string\r\n code: string\r\n}\r\n\r\nexport interface RegisterFormData {\r\n phone: string\r\n code: string\r\n password: string\r\n confirmPassword: string\r\n}\r\n\r\n// ================= 组件 Props =================\r\nexport interface LoginProps {\r\n /** 标题文字 */\r\n title?: string\r\n /** 副标题 */\r\n subtitle?: string\r\n /** Logo icon 名称(mdi:xxx),不传则不显示图标 */\r\n logoIcon?: string\r\n /** 功能开关配置 */\r\n features?: LoginFeatures\r\n /** 社交登录提供商列表 */\r\n socialProviders?: SocialProvider[]\r\n /** 扫码登录二维码 URL */\r\n qrcodeUrl?: string\r\n /** localStorage key(记住我) */\r\n storageKey?: string\r\n /** 外部传入的 loading 状态 */\r\n loading?: boolean\r\n /** 表单默认用户名(Demo / 预填场景) */\r\n defaultUsername?: string\r\n /** 表单默认密码(Demo / 预填场景) */\r\n defaultPassword?: string\r\n}\r\n\r\n// ================= 默认社交登录配置 =================\r\nexport const DEFAULT_SOCIAL_PROVIDERS: SocialProvider[] = [\r\n { key: 'github', label: 'GitHub', icon: 'mdi:github' },\r\n { key: 'wechat', label: '微信', icon: 'mdi:wechat' },\r\n { key: 'qq', label: 'QQ', icon: 'mdi:qqchat' },\r\n]\r\n\r\n// ================= 默认功能开关 =================\r\nexport const DEFAULT_FEATURES: Required<LoginFeatures> = {\r\n passwordLogin: true,\r\n captchaLogin: true,\r\n qrcodeLogin: true,\r\n socialLogin: true,\r\n register: true,\r\n captchaVerify: true,\r\n rememberMe: true,\r\n forgotPassword: true,\r\n}\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: C_Login - 通用插拔式登录组件\r\n * 通过 features prop 控制功能模块的显隐,样式通过 CSS 变量覆盖\r\n-->\r\n<template>\r\n <div class=\"c-login-panel\">\r\n <!-- ===== Logo / 标题区 ===== -->\r\n <div class=\"c-login__header\">\r\n <slot name=\"logo\">\r\n <div\r\n v-if=\"mergedProps.logoIcon\"\r\n class=\"c-login__logo\"\r\n >\r\n <C_Icon\r\n :name=\"mergedProps.logoIcon\"\r\n :size=\"36\"\r\n class=\"c-login__logo-icon\"\r\n />\r\n </div>\r\n </slot>\r\n <h2 class=\"c-login__title\">{{ mergedProps.title }}</h2>\r\n <p\r\n v-if=\"mergedProps.subtitle && activeTab !== 'register'\"\r\n class=\"c-login__subtitle\"\r\n >\r\n {{ mergedProps.subtitle }}\r\n </p>\r\n <!-- 注册视图的副标题 -->\r\n <p\r\n v-if=\"activeTab === 'register'\"\r\n class=\"c-login__subtitle c-login__subtitle--register\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </p>\r\n </div>\r\n\r\n <!-- ===== 密码登录表单(默认视图) ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'password'\"\r\n ref=\"passwordFormRef\"\r\n :model=\"passwordForm\"\r\n :rules=\"passwordRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"username\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.username\"\r\n :placeholder=\"t('cl_username_ph', '请输入用户名')\"\r\n clearable\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:account-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n clearable\r\n @keyup.enter=\"handlePasswordSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <!-- 记住我 + 忘记密码 -->\r\n <div\r\n v-if=\"feat.rememberMe || feat.forgotPassword\"\r\n class=\"c-login__aux-row\"\r\n >\r\n <NCheckbox\r\n v-if=\"feat.rememberMe\"\r\n v-model:checked=\"rememberMe\"\r\n class=\"c-login__remember\"\r\n >\r\n {{ t('cl_remember', '记住我') }}\r\n </NCheckbox>\r\n <NText\r\n v-if=\"feat.forgotPassword\"\r\n class=\"c-login__forgot\"\r\n @click=\"emit('forgot-password')\"\r\n >\r\n {{ t('cl_forgot', '忘记密码?') }}\r\n </NText>\r\n </div>\r\n </NForm>\r\n\r\n <!-- ===== 验证码登录表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'captcha'\"\r\n ref=\"captchaFormRef\"\r\n :model=\"captchaForm\"\r\n :rules=\"captchaRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"account\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"captchaForm.account\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"captchaForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n @keyup.enter=\"handleCaptchaSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"countdown > 0 || !captchaForm.account\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleSendCode\"\r\n >\r\n {{\r\n countdown > 0\r\n ? `${countdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 登录按钮(密码和验证码模式) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'password' || activeTab === 'captcha'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n :disabled=\"\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n \"\r\n class=\"c-login__submit-btn\"\r\n @click=\"\r\n activeTab === 'password'\r\n ? handlePasswordSubmit()\r\n : handleCaptchaSubmit()\r\n \"\r\n >\r\n {{\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n ? t('cl_verify_first', '请先完成人机验证')\r\n : t('cl_login_btn', '登 录')\r\n }}\r\n </NButton>\r\n\r\n <!-- ===== 人机验证拼图(仅密码登录模式) ===== -->\r\n <div\r\n v-if=\"feat.captchaVerify && activeTab === 'password'\"\r\n class=\"c-login__captcha-wrap\"\r\n >\r\n <C_Captcha\r\n ref=\"captchaRef\"\r\n trigger-text=\"\"\r\n theme=\"dark\"\r\n @success=\"handleCaptchaSuccess\"\r\n @fail=\"handleCaptchaFail\"\r\n @change=\"handleCaptchaChange\"\r\n />\r\n </div>\r\n\r\n <!-- ===== 返回按钮(验证码子视图) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'captcha'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 扫码登录面板 ===== -->\r\n <div\r\n v-if=\"activeTab === 'qrcode'\"\r\n class=\"c-login__qr-panel\"\r\n >\r\n <div class=\"c-login__qr-frame\">\r\n <C_QRCode\r\n ref=\"qrcodeRef\"\r\n :value=\"qrcodeValue\"\r\n :size=\"148\"\r\n :margin=\"1\"\r\n :show-border=\"false\"\r\n color=\"#ffffff\"\r\n bg-color=\"#000000\"\r\n error-correction-level=\"M\"\r\n />\r\n <div class=\"c-login__qr-scan-line\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tr\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--bl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--br\" />\r\n </div>\r\n <p class=\"c-login__qr-hint\">\r\n <C_Icon\r\n name=\"mdi:cellphone-check\"\r\n :size=\"13\"\r\n />\r\n {{ t('cl_qr_hint', '打开手机扫码后点击「确认」即可登录') }}\r\n </p>\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n class=\"c-login__qr-refresh\"\r\n @click=\"handleQrcodeRefresh\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n name=\"mdi:refresh\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ t('cl_qr_refresh', '二维码已失效?点击刷新') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 返回按钮(扫码子视图,在二维码下方) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'qrcode'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'register'\"\r\n ref=\"registerFormRef\"\r\n :model=\"registerForm\"\r\n :rules=\"registerRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"phone\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.phone\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"registerForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"regCountdown > 0 || !registerForm.phone\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleRegSendCode\"\r\n >\r\n {{\r\n regCountdown > 0\r\n ? `${regCountdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"confirmPassword\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.confirmPassword\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_confirm_pw_ph', '请再次输入密码')\"\r\n @keyup.enter=\"handleRegisterSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-check-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 注册确认按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n class=\"c-login__submit-btn\"\r\n @click=\"handleRegisterSubmit\"\r\n >\r\n {{ t('cl_confirm', '确认') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册视图返回按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 登录方式切换(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && (feat.captchaLogin || feat.qrcodeLogin)\"\r\n class=\"c-login__mode-switch\"\r\n >\r\n <NButton\r\n v-if=\"feat.captchaLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'captcha'\"\r\n >\r\n {{ t('cl_captcha_login', '验证码登录') }}\r\n </NButton>\r\n <NButton\r\n v-if=\"feat.qrcodeLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'qrcode'\"\r\n >\r\n {{ t('cl_qr_login', '扫码登录') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 其他登录方式(仅密码登录默认视图显示) ===== -->\r\n <template\r\n v-if=\"activeTab === 'password' && feat.socialLogin && socialList.length\"\r\n >\r\n <NDivider class=\"c-login__divider\">{{\r\n t('cl_social_login', '其他账号登录')\r\n }}</NDivider>\r\n <div class=\"c-login__social-row\">\r\n <NTooltip\r\n v-for=\"provider in socialList\"\r\n :key=\"provider.key\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <NButton\r\n circle\r\n class=\"c-login__social-btn\"\r\n @click=\"emit('social-login', provider.key)\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"provider.icon\"\r\n :size=\"16\"\r\n />\r\n </template>\r\n </NButton>\r\n </template>\r\n {{ provider.label }}\r\n </NTooltip>\r\n </div>\r\n </template>\r\n\r\n <!-- ===== 注册入口(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && feat.register\"\r\n class=\"c-login__register-row\"\r\n >\r\n <NText class=\"c-login__register-hint\">{{\r\n t('cl_no_account', '还没有账号?')\r\n }}</NText>\r\n <NText\r\n class=\"c-login__register-link\"\r\n @click=\"activeTab = 'register'\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </NText>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'\r\n import type { FormInst, FormRules } from 'naive-ui/es'\r\n import C_Icon from '../C_Icon/index.vue'\r\n import C_Captcha from '../C_Captcha/index.vue'\r\n import C_QRCode from '../C_QRCode/index.vue'\r\n import { RULE_COMBOS } from '@robot-admin/form-validate'\r\n import {\r\n DEFAULT_FEATURES,\r\n DEFAULT_SOCIAL_PROVIDERS,\r\n type LoginProps,\r\n type PasswordFormData,\r\n type CaptchaFormData,\r\n type RegisterFormData,\r\n } from './types'\r\n\r\n // ===== i18n helper =====\r\n const t = (key: string, fallback: string) =>\r\n typeof (globalThis as any).$t === 'function'\r\n ? (globalThis as any).$t(key, fallback, 'robot_admin')\r\n : fallback\r\n\r\n // ===== Props & Emits =====\r\n const props = withDefaults(defineProps<LoginProps>(), {\r\n title: '欢迎回来',\r\n loading: false,\r\n storageKey: 'c_login_remember',\r\n defaultUsername: '',\r\n defaultPassword: '',\r\n })\r\n\r\n const emit = defineEmits<{\r\n (\r\n e: 'submit',\r\n data: PasswordFormData & {\r\n captchaToken?: string\r\n captchaTimestamp?: number\r\n }\r\n ): void\r\n (e: 'captcha-submit', data: CaptchaFormData): void\r\n (e: 'send-code', account: string): void\r\n (e: 'social-login', provider: string): void\r\n (e: 'forgot-password'): void\r\n (e: 'qrcode-refresh'): void\r\n (e: 'register-submit', data: RegisterFormData): void\r\n (e: 'register-send-code', phone: string): void\r\n }>()\r\n\r\n // ===== 合并 Props(填充默认值) =====\r\n const mergedProps = computed(() => ({\r\n ...props,\r\n features: { ...DEFAULT_FEATURES, ...props.features },\r\n socialProviders: props.socialProviders ?? DEFAULT_SOCIAL_PROVIDERS,\r\n }))\r\n\r\n const feat = computed(\r\n () => mergedProps.value.features as Required<typeof DEFAULT_FEATURES>\r\n )\r\n const socialList = computed(() => mergedProps.value.socialProviders!)\r\n\r\n // ===== Tab(默认密码登录) =====\r\n const activeTab = ref<'password' | 'captcha' | 'qrcode' | 'register'>(\r\n 'password'\r\n )\r\n\r\n // ===== 密码表单 =====\r\n const passwordFormRef = ref<FormInst | null>(null)\r\n const passwordForm = reactive<PasswordFormData>({\r\n username: props.defaultUsername ?? '',\r\n password: props.defaultPassword ?? '',\r\n })\r\n\r\n const passwordRules: FormRules = {\r\n username: [\r\n {\r\n required: true,\r\n message: t('cl_username_rule', '请输入用户名'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: [\r\n {\r\n required: true,\r\n message: t('cl_password_rule', '请输入密码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 记住我 =====\r\n const rememberMe = ref(false)\r\n\r\n watch(rememberMe, val => {\r\n if (val) {\r\n localStorage.setItem(\r\n props.storageKey!,\r\n JSON.stringify({ username: passwordForm.username })\r\n )\r\n } else {\r\n localStorage.removeItem(props.storageKey!)\r\n }\r\n })\r\n\r\n onMounted(() => {\r\n try {\r\n const saved = localStorage.getItem(props.storageKey!)\r\n if (saved) {\r\n const data = JSON.parse(saved)\r\n if (data.username) {\r\n passwordForm.username = data.username\r\n rememberMe.value = true\r\n }\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n })\r\n\r\n // ===== 密码登录提交 =====\r\n const handlePasswordSubmit = async () => {\r\n try {\r\n await passwordFormRef.value?.validate()\r\n } catch {\r\n return // 校验失败,表单会显示行内错误提示\r\n }\r\n emit('submit', {\r\n ...passwordForm,\r\n ...(captchaValid.value\r\n ? {\r\n captchaToken: captchaToken.value,\r\n captchaTimestamp: captchaTimestamp.value,\r\n }\r\n : {}),\r\n })\r\n }\r\n\r\n // ===== 验证码表单 =====\r\n const captchaFormRef = ref<FormInst | null>(null)\r\n const captchaForm = reactive<CaptchaFormData>({ account: '', code: '' })\r\n const countdown = ref(0)\r\n let countdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const captchaRules: FormRules = {\r\n account: RULE_COMBOS.mobile('手机号') as FormRules['account'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 人机验证 =====\r\n const captchaRef = ref<InstanceType<typeof C_Captcha>>()\r\n const captchaValid = ref(false)\r\n const captchaToken = ref('')\r\n const captchaTimestamp = ref(0)\r\n\r\n const handleCaptchaSuccess = (data: { token: string; timestamp: number }) => {\r\n captchaValid.value = true\r\n captchaToken.value = data.token\r\n captchaTimestamp.value = data.timestamp\r\n }\r\n const handleCaptchaFail = () => {\r\n captchaValid.value = false\r\n }\r\n const handleCaptchaChange = (valid: boolean) => {\r\n captchaValid.value = valid\r\n if (!valid) {\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n }\r\n }\r\n\r\n const resetCaptcha = () => {\r\n captchaValid.value = false\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n captchaRef.value?.reset()\r\n }\r\n\r\n const handleCaptchaSubmit = async () => {\r\n try {\r\n await captchaFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n if (feat.value.captchaVerify && !captchaValid.value) return\r\n emit('captcha-submit', { ...captchaForm })\r\n }\r\n\r\n // ===== 发送验证码 =====\r\n const handleSendCode = () => {\r\n if (!captchaForm.account) return\r\n emit('send-code', captchaForm.account)\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n countdown.value = 60\r\n countdownTimer = setInterval(() => {\r\n countdown.value--\r\n if (countdown.value <= 0) {\r\n clearInterval(countdownTimer!)\r\n countdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n // ===== 扫码登录 =====\r\n const qrcodeRef = ref<any>(null)\r\n const qrcodeValue = ref(\r\n `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n )\r\n\r\n const handleQrcodeRefresh = () => {\r\n qrcodeValue.value = `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n emit('qrcode-refresh')\r\n }\r\n\r\n // ===== 注册表单 =====\r\n const registerFormRef = ref<FormInst | null>(null)\r\n const registerForm = reactive<RegisterFormData>({\r\n phone: '',\r\n code: '',\r\n password: '',\r\n confirmPassword: '',\r\n })\r\n const regCountdown = ref(0)\r\n let regCountdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const registerRules: FormRules = {\r\n phone: RULE_COMBOS.mobile('手机号') as FormRules['phone'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: RULE_COMBOS.password('密码') as FormRules['password'],\r\n confirmPassword: [\r\n {\r\n required: true,\r\n message: t('cl_confirm_pw_rule', '请再次输入密码'),\r\n trigger: 'blur',\r\n },\r\n {\r\n validator: (_rule: any, value: string) => {\r\n if (value !== registerForm.password) {\r\n return new Error(t('cl_pw_mismatch', '两次密码输入不一致'))\r\n }\r\n return true\r\n },\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n const handleRegSendCode = () => {\r\n if (!registerForm.phone) return\r\n emit('register-send-code', registerForm.phone)\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n regCountdown.value = 60\r\n regCountdownTimer = setInterval(() => {\r\n regCountdown.value--\r\n if (regCountdown.value <= 0) {\r\n clearInterval(regCountdownTimer!)\r\n regCountdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n const handleRegisterSubmit = async () => {\r\n try {\r\n await registerFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n emit('register-submit', { ...registerForm })\r\n }\r\n\r\n // ===== 清理定时器 =====\r\n onUnmounted(() => {\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n })\r\n\r\n // ===== 暴露方法给父组件 =====\r\n defineExpose({ resetCaptcha })\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n @use './index.scss';\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Description: C_Login - 通用插拔式登录组件\r\n * 通过 features prop 控制功能模块的显隐,样式通过 CSS 变量覆盖\r\n-->\r\n<template>\r\n <div class=\"c-login-panel\">\r\n <!-- ===== Logo / 标题区 ===== -->\r\n <div class=\"c-login__header\">\r\n <slot name=\"logo\">\r\n <div\r\n v-if=\"mergedProps.logoIcon\"\r\n class=\"c-login__logo\"\r\n >\r\n <C_Icon\r\n :name=\"mergedProps.logoIcon\"\r\n :size=\"36\"\r\n class=\"c-login__logo-icon\"\r\n />\r\n </div>\r\n </slot>\r\n <h2 class=\"c-login__title\">{{ mergedProps.title }}</h2>\r\n <p\r\n v-if=\"mergedProps.subtitle && activeTab !== 'register'\"\r\n class=\"c-login__subtitle\"\r\n >\r\n {{ mergedProps.subtitle }}\r\n </p>\r\n <!-- 注册视图的副标题 -->\r\n <p\r\n v-if=\"activeTab === 'register'\"\r\n class=\"c-login__subtitle c-login__subtitle--register\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </p>\r\n </div>\r\n\r\n <!-- ===== 密码登录表单(默认视图) ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'password'\"\r\n ref=\"passwordFormRef\"\r\n :model=\"passwordForm\"\r\n :rules=\"passwordRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"username\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.username\"\r\n :placeholder=\"t('cl_username_ph', '请输入用户名')\"\r\n clearable\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:account-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n clearable\r\n @keyup.enter=\"handlePasswordSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <!-- 记住我 + 忘记密码 -->\r\n <div\r\n v-if=\"feat.rememberMe || feat.forgotPassword\"\r\n class=\"c-login__aux-row\"\r\n >\r\n <NCheckbox\r\n v-if=\"feat.rememberMe\"\r\n v-model:checked=\"rememberMe\"\r\n class=\"c-login__remember\"\r\n >\r\n {{ t('cl_remember', '记住我') }}\r\n </NCheckbox>\r\n <NText\r\n v-if=\"feat.forgotPassword\"\r\n class=\"c-login__forgot\"\r\n @click=\"emit('forgot-password')\"\r\n >\r\n {{ t('cl_forgot', '忘记密码?') }}\r\n </NText>\r\n </div>\r\n </NForm>\r\n\r\n <!-- ===== 验证码登录表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'captcha'\"\r\n ref=\"captchaFormRef\"\r\n :model=\"captchaForm\"\r\n :rules=\"captchaRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"account\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"captchaForm.account\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"captchaForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n @keyup.enter=\"handleCaptchaSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"countdown > 0 || !captchaForm.account\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleSendCode\"\r\n >\r\n {{\r\n countdown > 0\r\n ? `${countdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 登录按钮(密码和验证码模式) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'password' || activeTab === 'captcha'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n :disabled=\"\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n \"\r\n class=\"c-login__submit-btn\"\r\n @click=\"\r\n activeTab === 'password'\r\n ? handlePasswordSubmit()\r\n : handleCaptchaSubmit()\r\n \"\r\n >\r\n {{\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n ? t('cl_verify_first', '请先完成人机验证')\r\n : t('cl_login_btn', '登 录')\r\n }}\r\n </NButton>\r\n\r\n <!-- ===== 人机验证拼图(仅密码登录模式) ===== -->\r\n <div\r\n v-if=\"feat.captchaVerify && activeTab === 'password'\"\r\n class=\"c-login__captcha-wrap\"\r\n >\r\n <C_Captcha\r\n ref=\"captchaRef\"\r\n trigger-text=\"\"\r\n theme=\"dark\"\r\n @success=\"handleCaptchaSuccess\"\r\n @fail=\"handleCaptchaFail\"\r\n @change=\"handleCaptchaChange\"\r\n />\r\n </div>\r\n\r\n <!-- ===== 返回按钮(验证码子视图) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'captcha'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 扫码登录面板 ===== -->\r\n <div\r\n v-if=\"activeTab === 'qrcode'\"\r\n class=\"c-login__qr-panel\"\r\n >\r\n <div class=\"c-login__qr-frame\">\r\n <C_QRCode\r\n ref=\"qrcodeRef\"\r\n :value=\"qrcodeValue\"\r\n :size=\"148\"\r\n :margin=\"1\"\r\n :show-border=\"false\"\r\n color=\"#ffffff\"\r\n bg-color=\"#000000\"\r\n error-correction-level=\"M\"\r\n />\r\n <div class=\"c-login__qr-scan-line\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tr\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--bl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--br\" />\r\n </div>\r\n <p class=\"c-login__qr-hint\">\r\n <C_Icon\r\n name=\"mdi:cellphone-check\"\r\n :size=\"13\"\r\n />\r\n {{ t('cl_qr_hint', '打开手机扫码后点击「确认」即可登录') }}\r\n </p>\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n class=\"c-login__qr-refresh\"\r\n @click=\"handleQrcodeRefresh\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n name=\"mdi:refresh\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ t('cl_qr_refresh', '二维码已失效?点击刷新') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 返回按钮(扫码子视图,在二维码下方) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'qrcode'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'register'\"\r\n ref=\"registerFormRef\"\r\n :model=\"registerForm\"\r\n :rules=\"registerRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"phone\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.phone\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"registerForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"regCountdown > 0 || !registerForm.phone\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleRegSendCode\"\r\n >\r\n {{\r\n regCountdown > 0\r\n ? `${regCountdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"confirmPassword\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.confirmPassword\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_confirm_pw_ph', '请再次输入密码')\"\r\n @keyup.enter=\"handleRegisterSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-check-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 注册确认按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n class=\"c-login__submit-btn\"\r\n @click=\"handleRegisterSubmit\"\r\n >\r\n {{ t('cl_confirm', '确认') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册视图返回按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 登录方式切换(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && (feat.captchaLogin || feat.qrcodeLogin)\"\r\n class=\"c-login__mode-switch\"\r\n >\r\n <NButton\r\n v-if=\"feat.captchaLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'captcha'\"\r\n >\r\n {{ t('cl_captcha_login', '验证码登录') }}\r\n </NButton>\r\n <NButton\r\n v-if=\"feat.qrcodeLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'qrcode'\"\r\n >\r\n {{ t('cl_qr_login', '扫码登录') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 其他登录方式(仅密码登录默认视图显示) ===== -->\r\n <template\r\n v-if=\"activeTab === 'password' && feat.socialLogin && socialList.length\"\r\n >\r\n <NDivider class=\"c-login__divider\">{{\r\n t('cl_social_login', '其他账号登录')\r\n }}</NDivider>\r\n <div class=\"c-login__social-row\">\r\n <NTooltip\r\n v-for=\"provider in socialList\"\r\n :key=\"provider.key\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <NButton\r\n circle\r\n class=\"c-login__social-btn\"\r\n @click=\"emit('social-login', provider.key)\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"provider.icon\"\r\n :size=\"16\"\r\n />\r\n </template>\r\n </NButton>\r\n </template>\r\n {{ provider.label }}\r\n </NTooltip>\r\n </div>\r\n </template>\r\n\r\n <!-- ===== 注册入口(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && feat.register\"\r\n class=\"c-login__register-row\"\r\n >\r\n <NText class=\"c-login__register-hint\">{{\r\n t('cl_no_account', '还没有账号?')\r\n }}</NText>\r\n <NText\r\n class=\"c-login__register-link\"\r\n @click=\"activeTab = 'register'\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </NText>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'\r\n import type { FormInst, FormRules } from 'naive-ui/es'\r\n import C_Icon from '../C_Icon/index.vue'\r\n import C_Captcha from '../C_Captcha/index.vue'\r\n import C_QRCode from '../C_QRCode/index.vue'\r\n import { RULE_COMBOS } from '@robot-admin/form-validate'\r\n import {\r\n DEFAULT_FEATURES,\r\n DEFAULT_SOCIAL_PROVIDERS,\r\n type LoginProps,\r\n type PasswordFormData,\r\n type CaptchaFormData,\r\n type RegisterFormData,\r\n } from './types'\r\n\r\n // ===== i18n helper =====\r\n const t = (key: string, fallback: string) =>\r\n typeof (globalThis as any).$t === 'function'\r\n ? (globalThis as any).$t(key, fallback, 'robot_admin')\r\n : fallback\r\n\r\n // ===== Props & Emits =====\r\n const props = withDefaults(defineProps<LoginProps>(), {\r\n title: '欢迎回来',\r\n loading: false,\r\n storageKey: 'c_login_remember',\r\n defaultUsername: '',\r\n defaultPassword: '',\r\n })\r\n\r\n const emit = defineEmits<{\r\n (\r\n e: 'submit',\r\n data: PasswordFormData & {\r\n captchaToken?: string\r\n captchaTimestamp?: number\r\n }\r\n ): void\r\n (e: 'captcha-submit', data: CaptchaFormData): void\r\n (e: 'send-code', account: string): void\r\n (e: 'social-login', provider: string): void\r\n (e: 'forgot-password'): void\r\n (e: 'qrcode-refresh'): void\r\n (e: 'register-submit', data: RegisterFormData): void\r\n (e: 'register-send-code', phone: string): void\r\n }>()\r\n\r\n // ===== 合并 Props(填充默认值) =====\r\n const mergedProps = computed(() => ({\r\n ...props,\r\n features: { ...DEFAULT_FEATURES, ...props.features },\r\n socialProviders: props.socialProviders ?? DEFAULT_SOCIAL_PROVIDERS,\r\n }))\r\n\r\n const feat = computed(\r\n () => mergedProps.value.features as Required<typeof DEFAULT_FEATURES>\r\n )\r\n const socialList = computed(() => mergedProps.value.socialProviders!)\r\n\r\n // ===== Tab(默认密码登录) =====\r\n const activeTab = ref<'password' | 'captcha' | 'qrcode' | 'register'>(\r\n 'password'\r\n )\r\n\r\n // ===== 密码表单 =====\r\n const passwordFormRef = ref<FormInst | null>(null)\r\n const passwordForm = reactive<PasswordFormData>({\r\n username: props.defaultUsername ?? '',\r\n password: props.defaultPassword ?? '',\r\n })\r\n\r\n const passwordRules: FormRules = {\r\n username: [\r\n {\r\n required: true,\r\n message: t('cl_username_rule', '请输入用户名'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: [\r\n {\r\n required: true,\r\n message: t('cl_password_rule', '请输入密码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 记住我 =====\r\n const rememberMe = ref(false)\r\n\r\n watch(rememberMe, val => {\r\n if (val) {\r\n localStorage.setItem(\r\n props.storageKey!,\r\n JSON.stringify({ username: passwordForm.username })\r\n )\r\n } else {\r\n localStorage.removeItem(props.storageKey!)\r\n }\r\n })\r\n\r\n onMounted(() => {\r\n try {\r\n const saved = localStorage.getItem(props.storageKey!)\r\n if (saved) {\r\n const data = JSON.parse(saved)\r\n if (data.username) {\r\n passwordForm.username = data.username\r\n rememberMe.value = true\r\n }\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n })\r\n\r\n // ===== 密码登录提交 =====\r\n const handlePasswordSubmit = async () => {\r\n try {\r\n await passwordFormRef.value?.validate()\r\n } catch {\r\n return // 校验失败,表单会显示行内错误提示\r\n }\r\n emit('submit', {\r\n ...passwordForm,\r\n ...(captchaValid.value\r\n ? {\r\n captchaToken: captchaToken.value,\r\n captchaTimestamp: captchaTimestamp.value,\r\n }\r\n : {}),\r\n })\r\n }\r\n\r\n // ===== 验证码表单 =====\r\n const captchaFormRef = ref<FormInst | null>(null)\r\n const captchaForm = reactive<CaptchaFormData>({ account: '', code: '' })\r\n const countdown = ref(0)\r\n let countdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const captchaRules: FormRules = {\r\n account: RULE_COMBOS.mobile('手机号') as FormRules['account'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 人机验证 =====\r\n const captchaRef = ref<InstanceType<typeof C_Captcha>>()\r\n const captchaValid = ref(false)\r\n const captchaToken = ref('')\r\n const captchaTimestamp = ref(0)\r\n\r\n const handleCaptchaSuccess = (data: { token: string; timestamp: number }) => {\r\n captchaValid.value = true\r\n captchaToken.value = data.token\r\n captchaTimestamp.value = data.timestamp\r\n }\r\n const handleCaptchaFail = () => {\r\n captchaValid.value = false\r\n }\r\n const handleCaptchaChange = (valid: boolean) => {\r\n captchaValid.value = valid\r\n if (!valid) {\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n }\r\n }\r\n\r\n const resetCaptcha = () => {\r\n captchaValid.value = false\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n captchaRef.value?.reset()\r\n }\r\n\r\n const handleCaptchaSubmit = async () => {\r\n try {\r\n await captchaFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n if (feat.value.captchaVerify && !captchaValid.value) return\r\n emit('captcha-submit', { ...captchaForm })\r\n }\r\n\r\n // ===== 发送验证码 =====\r\n const handleSendCode = () => {\r\n if (!captchaForm.account) return\r\n emit('send-code', captchaForm.account)\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n countdown.value = 60\r\n countdownTimer = setInterval(() => {\r\n countdown.value--\r\n if (countdown.value <= 0) {\r\n clearInterval(countdownTimer!)\r\n countdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n // ===== 扫码登录 =====\r\n const qrcodeRef = ref<any>(null)\r\n const qrcodeValue = ref(\r\n `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n )\r\n\r\n const handleQrcodeRefresh = () => {\r\n qrcodeValue.value = `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n emit('qrcode-refresh')\r\n }\r\n\r\n // ===== 注册表单 =====\r\n const registerFormRef = ref<FormInst | null>(null)\r\n const registerForm = reactive<RegisterFormData>({\r\n phone: '',\r\n code: '',\r\n password: '',\r\n confirmPassword: '',\r\n })\r\n const regCountdown = ref(0)\r\n let regCountdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const registerRules: FormRules = {\r\n phone: RULE_COMBOS.mobile('手机号') as FormRules['phone'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: RULE_COMBOS.password('密码') as FormRules['password'],\r\n confirmPassword: [\r\n {\r\n required: true,\r\n message: t('cl_confirm_pw_rule', '请再次输入密码'),\r\n trigger: 'blur',\r\n },\r\n {\r\n validator: (_rule: any, value: string) => {\r\n if (value !== registerForm.password) {\r\n return new Error(t('cl_pw_mismatch', '两次密码输入不一致'))\r\n }\r\n return true\r\n },\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n const handleRegSendCode = () => {\r\n if (!registerForm.phone) return\r\n emit('register-send-code', registerForm.phone)\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n regCountdown.value = 60\r\n regCountdownTimer = setInterval(() => {\r\n regCountdown.value--\r\n if (regCountdown.value <= 0) {\r\n clearInterval(regCountdownTimer!)\r\n regCountdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n const handleRegisterSubmit = async () => {\r\n try {\r\n await registerFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n emit('register-submit', { ...registerForm })\r\n }\r\n\r\n // ===== 清理定时器 =====\r\n onUnmounted(() => {\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n })\r\n\r\n // ===== 暴露方法给父组件 =====\r\n defineExpose({ resetCaptcha })\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n @use './index.scss';\r\n</style>\r\n","<!--\r\n * @Description: C_Login - 通用插拔式登录组件\r\n * 通过 features prop 控制功能模块的显隐,样式通过 CSS 变量覆盖\r\n-->\r\n<template>\r\n <div class=\"c-login-panel\">\r\n <!-- ===== Logo / 标题区 ===== -->\r\n <div class=\"c-login__header\">\r\n <slot name=\"logo\">\r\n <div\r\n v-if=\"mergedProps.logoIcon\"\r\n class=\"c-login__logo\"\r\n >\r\n <C_Icon\r\n :name=\"mergedProps.logoIcon\"\r\n :size=\"36\"\r\n class=\"c-login__logo-icon\"\r\n />\r\n </div>\r\n </slot>\r\n <h2 class=\"c-login__title\">{{ mergedProps.title }}</h2>\r\n <p\r\n v-if=\"mergedProps.subtitle && activeTab !== 'register'\"\r\n class=\"c-login__subtitle\"\r\n >\r\n {{ mergedProps.subtitle }}\r\n </p>\r\n <!-- 注册视图的副标题 -->\r\n <p\r\n v-if=\"activeTab === 'register'\"\r\n class=\"c-login__subtitle c-login__subtitle--register\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </p>\r\n </div>\r\n\r\n <!-- ===== 密码登录表单(默认视图) ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'password'\"\r\n ref=\"passwordFormRef\"\r\n :model=\"passwordForm\"\r\n :rules=\"passwordRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"username\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.username\"\r\n :placeholder=\"t('cl_username_ph', '请输入用户名')\"\r\n clearable\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:account-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"passwordForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n clearable\r\n @keyup.enter=\"handlePasswordSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <!-- 记住我 + 忘记密码 -->\r\n <div\r\n v-if=\"feat.rememberMe || feat.forgotPassword\"\r\n class=\"c-login__aux-row\"\r\n >\r\n <NCheckbox\r\n v-if=\"feat.rememberMe\"\r\n v-model:checked=\"rememberMe\"\r\n class=\"c-login__remember\"\r\n >\r\n {{ t('cl_remember', '记住我') }}\r\n </NCheckbox>\r\n <NText\r\n v-if=\"feat.forgotPassword\"\r\n class=\"c-login__forgot\"\r\n @click=\"emit('forgot-password')\"\r\n >\r\n {{ t('cl_forgot', '忘记密码?') }}\r\n </NText>\r\n </div>\r\n </NForm>\r\n\r\n <!-- ===== 验证码登录表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'captcha'\"\r\n ref=\"captchaFormRef\"\r\n :model=\"captchaForm\"\r\n :rules=\"captchaRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"account\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"captchaForm.account\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"captchaForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n @keyup.enter=\"handleCaptchaSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"countdown > 0 || !captchaForm.account\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleSendCode\"\r\n >\r\n {{\r\n countdown > 0\r\n ? `${countdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 登录按钮(密码和验证码模式) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'password' || activeTab === 'captcha'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n :disabled=\"\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n \"\r\n class=\"c-login__submit-btn\"\r\n @click=\"\r\n activeTab === 'password'\r\n ? handlePasswordSubmit()\r\n : handleCaptchaSubmit()\r\n \"\r\n >\r\n {{\r\n activeTab === 'password' && feat.captchaVerify && !captchaValid\r\n ? t('cl_verify_first', '请先完成人机验证')\r\n : t('cl_login_btn', '登 录')\r\n }}\r\n </NButton>\r\n\r\n <!-- ===== 人机验证拼图(仅密码登录模式) ===== -->\r\n <div\r\n v-if=\"feat.captchaVerify && activeTab === 'password'\"\r\n class=\"c-login__captcha-wrap\"\r\n >\r\n <C_Captcha\r\n ref=\"captchaRef\"\r\n trigger-text=\"\"\r\n theme=\"dark\"\r\n @success=\"handleCaptchaSuccess\"\r\n @fail=\"handleCaptchaFail\"\r\n @change=\"handleCaptchaChange\"\r\n />\r\n </div>\r\n\r\n <!-- ===== 返回按钮(验证码子视图) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'captcha'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 扫码登录面板 ===== -->\r\n <div\r\n v-if=\"activeTab === 'qrcode'\"\r\n class=\"c-login__qr-panel\"\r\n >\r\n <div class=\"c-login__qr-frame\">\r\n <C_QRCode\r\n ref=\"qrcodeRef\"\r\n :value=\"qrcodeValue\"\r\n :size=\"148\"\r\n :margin=\"1\"\r\n :show-border=\"false\"\r\n color=\"#ffffff\"\r\n bg-color=\"#000000\"\r\n error-correction-level=\"M\"\r\n />\r\n <div class=\"c-login__qr-scan-line\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--tr\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--bl\" />\r\n <div class=\"c-login__qr-corner c-login__qr-corner--br\" />\r\n </div>\r\n <p class=\"c-login__qr-hint\">\r\n <C_Icon\r\n name=\"mdi:cellphone-check\"\r\n :size=\"13\"\r\n />\r\n {{ t('cl_qr_hint', '打开手机扫码后点击「确认」即可登录') }}\r\n </p>\r\n <NButton\r\n text\r\n size=\"tiny\"\r\n class=\"c-login__qr-refresh\"\r\n @click=\"handleQrcodeRefresh\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n name=\"mdi:refresh\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ t('cl_qr_refresh', '二维码已失效?点击刷新') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 返回按钮(扫码子视图,在二维码下方) ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'qrcode'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册表单 ===== -->\r\n <NForm\r\n v-show=\"activeTab === 'register'\"\r\n ref=\"registerFormRef\"\r\n :model=\"registerForm\"\r\n :rules=\"registerRules\"\r\n class=\"c-login__form\"\r\n >\r\n <NFormItem\r\n path=\"phone\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.phone\"\r\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\r\n clearable\r\n maxlength=\"11\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:cellphone\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"code\"\r\n class=\"c-login__form-item\"\r\n >\r\n <div class=\"c-login__code-row\">\r\n <NInput\r\n v-model:value=\"registerForm.code\"\r\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\r\n clearable\r\n class=\"c-login__code-input\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:shield-key-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n <NButton\r\n :disabled=\"regCountdown > 0 || !registerForm.phone\"\r\n class=\"c-login__send-code-btn\"\r\n @click=\"handleRegSendCode\"\r\n >\r\n {{\r\n regCountdown > 0\r\n ? `${regCountdown}s ${t('cl_resend', '后重发')}`\r\n : t('cl_get_code', '获取验证码')\r\n }}\r\n </NButton>\r\n </div>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"password\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.password\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n\r\n <NFormItem\r\n path=\"confirmPassword\"\r\n class=\"c-login__form-item\"\r\n >\r\n <NInput\r\n v-model:value=\"registerForm.confirmPassword\"\r\n type=\"password\"\r\n show-password-on=\"click\"\r\n :placeholder=\"t('cl_confirm_pw_ph', '请再次输入密码')\"\r\n @keyup.enter=\"handleRegisterSubmit\"\r\n >\r\n <template #prefix>\r\n <C_Icon\r\n name=\"mdi:lock-check-outline\"\r\n :size=\"16\"\r\n class=\"c-login__input-icon\"\r\n />\r\n </template>\r\n </NInput>\r\n </NFormItem>\r\n </NForm>\r\n\r\n <!-- ===== 注册确认按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n type=\"primary\"\r\n block\r\n :loading=\"props.loading\"\r\n class=\"c-login__submit-btn\"\r\n @click=\"handleRegisterSubmit\"\r\n >\r\n {{ t('cl_confirm', '确认') }}\r\n </NButton>\r\n\r\n <!-- ===== 注册视图返回按钮 ===== -->\r\n <NButton\r\n v-if=\"activeTab === 'register'\"\r\n block\r\n class=\"c-login__back-btn\"\r\n @click=\"activeTab = 'password'\"\r\n >\r\n {{ t('cl_back', '返回') }}\r\n </NButton>\r\n\r\n <!-- ===== 登录方式切换(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && (feat.captchaLogin || feat.qrcodeLogin)\"\r\n class=\"c-login__mode-switch\"\r\n >\r\n <NButton\r\n v-if=\"feat.captchaLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'captcha'\"\r\n >\r\n {{ t('cl_captcha_login', '验证码登录') }}\r\n </NButton>\r\n <NButton\r\n v-if=\"feat.qrcodeLogin\"\r\n class=\"c-login__mode-btn\"\r\n size=\"small\"\r\n @click=\"activeTab = 'qrcode'\"\r\n >\r\n {{ t('cl_qr_login', '扫码登录') }}\r\n </NButton>\r\n </div>\r\n\r\n <!-- ===== 其他登录方式(仅密码登录默认视图显示) ===== -->\r\n <template\r\n v-if=\"activeTab === 'password' && feat.socialLogin && socialList.length\"\r\n >\r\n <NDivider class=\"c-login__divider\">{{\r\n t('cl_social_login', '其他账号登录')\r\n }}</NDivider>\r\n <div class=\"c-login__social-row\">\r\n <NTooltip\r\n v-for=\"provider in socialList\"\r\n :key=\"provider.key\"\r\n trigger=\"hover\"\r\n placement=\"top\"\r\n >\r\n <template #trigger>\r\n <NButton\r\n circle\r\n class=\"c-login__social-btn\"\r\n @click=\"emit('social-login', provider.key)\"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"provider.icon\"\r\n :size=\"16\"\r\n />\r\n </template>\r\n </NButton>\r\n </template>\r\n {{ provider.label }}\r\n </NTooltip>\r\n </div>\r\n </template>\r\n\r\n <!-- ===== 注册入口(仅密码登录默认视图显示) ===== -->\r\n <div\r\n v-if=\"activeTab === 'password' && feat.register\"\r\n class=\"c-login__register-row\"\r\n >\r\n <NText class=\"c-login__register-hint\">{{\r\n t('cl_no_account', '还没有账号?')\r\n }}</NText>\r\n <NText\r\n class=\"c-login__register-link\"\r\n @click=\"activeTab = 'register'\"\r\n >\r\n {{ t('cl_register', '注册账号') }}\r\n </NText>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'\r\n import type { FormInst, FormRules } from 'naive-ui/es'\r\n import C_Icon from '../C_Icon/index.vue'\r\n import C_Captcha from '../C_Captcha/index.vue'\r\n import C_QRCode from '../C_QRCode/index.vue'\r\n import { RULE_COMBOS } from '@robot-admin/form-validate'\r\n import {\r\n DEFAULT_FEATURES,\r\n DEFAULT_SOCIAL_PROVIDERS,\r\n type LoginProps,\r\n type PasswordFormData,\r\n type CaptchaFormData,\r\n type RegisterFormData,\r\n } from './types'\r\n\r\n // ===== i18n helper =====\r\n const t = (key: string, fallback: string) =>\r\n typeof (globalThis as any).$t === 'function'\r\n ? (globalThis as any).$t(key, fallback, 'robot_admin')\r\n : fallback\r\n\r\n // ===== Props & Emits =====\r\n const props = withDefaults(defineProps<LoginProps>(), {\r\n title: '欢迎回来',\r\n loading: false,\r\n storageKey: 'c_login_remember',\r\n defaultUsername: '',\r\n defaultPassword: '',\r\n })\r\n\r\n const emit = defineEmits<{\r\n (\r\n e: 'submit',\r\n data: PasswordFormData & {\r\n captchaToken?: string\r\n captchaTimestamp?: number\r\n }\r\n ): void\r\n (e: 'captcha-submit', data: CaptchaFormData): void\r\n (e: 'send-code', account: string): void\r\n (e: 'social-login', provider: string): void\r\n (e: 'forgot-password'): void\r\n (e: 'qrcode-refresh'): void\r\n (e: 'register-submit', data: RegisterFormData): void\r\n (e: 'register-send-code', phone: string): void\r\n }>()\r\n\r\n // ===== 合并 Props(填充默认值) =====\r\n const mergedProps = computed(() => ({\r\n ...props,\r\n features: { ...DEFAULT_FEATURES, ...props.features },\r\n socialProviders: props.socialProviders ?? DEFAULT_SOCIAL_PROVIDERS,\r\n }))\r\n\r\n const feat = computed(\r\n () => mergedProps.value.features as Required<typeof DEFAULT_FEATURES>\r\n )\r\n const socialList = computed(() => mergedProps.value.socialProviders!)\r\n\r\n // ===== Tab(默认密码登录) =====\r\n const activeTab = ref<'password' | 'captcha' | 'qrcode' | 'register'>(\r\n 'password'\r\n )\r\n\r\n // ===== 密码表单 =====\r\n const passwordFormRef = ref<FormInst | null>(null)\r\n const passwordForm = reactive<PasswordFormData>({\r\n username: props.defaultUsername ?? '',\r\n password: props.defaultPassword ?? '',\r\n })\r\n\r\n const passwordRules: FormRules = {\r\n username: [\r\n {\r\n required: true,\r\n message: t('cl_username_rule', '请输入用户名'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: [\r\n {\r\n required: true,\r\n message: t('cl_password_rule', '请输入密码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 记住我 =====\r\n const rememberMe = ref(false)\r\n\r\n watch(rememberMe, val => {\r\n if (val) {\r\n localStorage.setItem(\r\n props.storageKey!,\r\n JSON.stringify({ username: passwordForm.username })\r\n )\r\n } else {\r\n localStorage.removeItem(props.storageKey!)\r\n }\r\n })\r\n\r\n onMounted(() => {\r\n try {\r\n const saved = localStorage.getItem(props.storageKey!)\r\n if (saved) {\r\n const data = JSON.parse(saved)\r\n if (data.username) {\r\n passwordForm.username = data.username\r\n rememberMe.value = true\r\n }\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n })\r\n\r\n // ===== 密码登录提交 =====\r\n const handlePasswordSubmit = async () => {\r\n try {\r\n await passwordFormRef.value?.validate()\r\n } catch {\r\n return // 校验失败,表单会显示行内错误提示\r\n }\r\n emit('submit', {\r\n ...passwordForm,\r\n ...(captchaValid.value\r\n ? {\r\n captchaToken: captchaToken.value,\r\n captchaTimestamp: captchaTimestamp.value,\r\n }\r\n : {}),\r\n })\r\n }\r\n\r\n // ===== 验证码表单 =====\r\n const captchaFormRef = ref<FormInst | null>(null)\r\n const captchaForm = reactive<CaptchaFormData>({ account: '', code: '' })\r\n const countdown = ref(0)\r\n let countdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const captchaRules: FormRules = {\r\n account: RULE_COMBOS.mobile('手机号') as FormRules['account'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n // ===== 人机验证 =====\r\n const captchaRef = ref<InstanceType<typeof C_Captcha>>()\r\n const captchaValid = ref(false)\r\n const captchaToken = ref('')\r\n const captchaTimestamp = ref(0)\r\n\r\n const handleCaptchaSuccess = (data: { token: string; timestamp: number }) => {\r\n captchaValid.value = true\r\n captchaToken.value = data.token\r\n captchaTimestamp.value = data.timestamp\r\n }\r\n const handleCaptchaFail = () => {\r\n captchaValid.value = false\r\n }\r\n const handleCaptchaChange = (valid: boolean) => {\r\n captchaValid.value = valid\r\n if (!valid) {\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n }\r\n }\r\n\r\n const resetCaptcha = () => {\r\n captchaValid.value = false\r\n captchaToken.value = ''\r\n captchaTimestamp.value = 0\r\n captchaRef.value?.reset()\r\n }\r\n\r\n const handleCaptchaSubmit = async () => {\r\n try {\r\n await captchaFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n if (feat.value.captchaVerify && !captchaValid.value) return\r\n emit('captcha-submit', { ...captchaForm })\r\n }\r\n\r\n // ===== 发送验证码 =====\r\n const handleSendCode = () => {\r\n if (!captchaForm.account) return\r\n emit('send-code', captchaForm.account)\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n countdown.value = 60\r\n countdownTimer = setInterval(() => {\r\n countdown.value--\r\n if (countdown.value <= 0) {\r\n clearInterval(countdownTimer!)\r\n countdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n // ===== 扫码登录 =====\r\n const qrcodeRef = ref<any>(null)\r\n const qrcodeValue = ref(\r\n `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n )\r\n\r\n const handleQrcodeRefresh = () => {\r\n qrcodeValue.value = `https://robot-admin.app/qrcode-login?t=${Date.now()}`\r\n emit('qrcode-refresh')\r\n }\r\n\r\n // ===== 注册表单 =====\r\n const registerFormRef = ref<FormInst | null>(null)\r\n const registerForm = reactive<RegisterFormData>({\r\n phone: '',\r\n code: '',\r\n password: '',\r\n confirmPassword: '',\r\n })\r\n const regCountdown = ref(0)\r\n let regCountdownTimer: ReturnType<typeof setInterval> | null = null\r\n\r\n const registerRules: FormRules = {\r\n phone: RULE_COMBOS.mobile('手机号') as FormRules['phone'],\r\n code: [\r\n {\r\n required: true,\r\n message: t('cl_code_rule', '请输入验证码'),\r\n trigger: 'blur',\r\n },\r\n ],\r\n password: RULE_COMBOS.password('密码') as FormRules['password'],\r\n confirmPassword: [\r\n {\r\n required: true,\r\n message: t('cl_confirm_pw_rule', '请再次输入密码'),\r\n trigger: 'blur',\r\n },\r\n {\r\n validator: (_rule: any, value: string) => {\r\n if (value !== registerForm.password) {\r\n return new Error(t('cl_pw_mismatch', '两次密码输入不一致'))\r\n }\r\n return true\r\n },\r\n trigger: 'blur',\r\n },\r\n ],\r\n }\r\n\r\n const handleRegSendCode = () => {\r\n if (!registerForm.phone) return\r\n emit('register-send-code', registerForm.phone)\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n regCountdown.value = 60\r\n regCountdownTimer = setInterval(() => {\r\n regCountdown.value--\r\n if (regCountdown.value <= 0) {\r\n clearInterval(regCountdownTimer!)\r\n regCountdownTimer = null\r\n }\r\n }, 1000)\r\n }\r\n\r\n const handleRegisterSubmit = async () => {\r\n try {\r\n await registerFormRef.value?.validate()\r\n } catch {\r\n return\r\n }\r\n emit('register-submit', { ...registerForm })\r\n }\r\n\r\n // ===== 清理定时器 =====\r\n onUnmounted(() => {\r\n if (countdownTimer) {\r\n clearInterval(countdownTimer)\r\n countdownTimer = null\r\n }\r\n if (regCountdownTimer) {\r\n clearInterval(regCountdownTimer)\r\n regCountdownTimer = null\r\n }\r\n })\r\n\r\n // ===== 暴露方法给父组件 =====\r\n defineExpose({ resetCaptcha })\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n @use './index.scss';\r\n</style>\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_Login2.js","names":[],"sources":["../src/components/C_Login/types.ts","../src/components/C_Login/index.vue"],"sourcesContent":["/**\n * @Description: C_Login 通用登录组件 - 类型定义\n */\n\n// ================= 功能开关 =================\nexport interface LoginFeatures {\n /** 是否显示密码登录(默认 true) */\n passwordLogin?: boolean\n /** 是否显示验证码登录(默认 true) */\n captchaLogin?: boolean\n /** 是否显示扫码登录(默认 true) */\n qrcodeLogin?: boolean\n /** 是否显示其他账号登录(默认 true) */\n socialLogin?: boolean\n /** 是否显示注册账号(默认 true) */\n register?: boolean\n /** 是否启用人机验证拼图(默认 true) */\n captchaVerify?: boolean\n /** 是否显示记住我(默认 true) */\n rememberMe?: boolean\n /** 是否显示忘记密码(默认 true) */\n forgotPassword?: boolean\n}\n\n// ================= 社交登录 =================\nexport interface SocialProvider {\n key: string\n label: string\n /** mdi 图标名称 */\n icon: string\n}\n\n// ================= 表单数据 =================\nexport interface PasswordFormData {\n username: string\n password: string\n}\n\nexport interface CaptchaFormData {\n account: string\n code: string\n}\n\nexport interface RegisterFormData {\n phone: string\n code: string\n password: string\n confirmPassword: string\n}\n\n// ================= 组件 Props =================\nexport interface LoginProps {\n /** 标题文字 */\n title?: string\n /** 副标题 */\n subtitle?: string\n /** Logo icon 名称(mdi:xxx),不传则不显示图标 */\n logoIcon?: string\n /** 功能开关配置 */\n features?: LoginFeatures\n /** 社交登录提供商列表 */\n socialProviders?: SocialProvider[]\n /** 扫码登录二维码 URL */\n qrcodeUrl?: string\n /** localStorage key(记住我) */\n storageKey?: string\n /** 外部传入的 loading 状态 */\n loading?: boolean\n /** 表单默认用户名(Demo / 预填场景) */\n defaultUsername?: string\n /** 表单默认密码(Demo / 预填场景) */\n defaultPassword?: string\n}\n\n// ================= 默认社交登录配置 =================\nexport const DEFAULT_SOCIAL_PROVIDERS: SocialProvider[] = [\n { key: 'github', label: 'GitHub', icon: 'mdi:github' },\n { key: 'wechat', label: '微信', icon: 'mdi:wechat' },\n { key: 'qq', label: 'QQ', icon: 'mdi:qqchat' },\n]\n\n// ================= 默认功能开关 =================\nexport const DEFAULT_FEATURES: Required<LoginFeatures> = {\n passwordLogin: true,\n captchaLogin: true,\n qrcodeLogin: true,\n socialLogin: true,\n register: true,\n captchaVerify: true,\n rememberMe: true,\n forgotPassword: true,\n}\n","<!--\n * @Description: C_Login - 通用插拔式登录组件\n * 通过 features prop 控制功能模块的显隐,样式通过 CSS 变量覆盖\n-->\n<template>\n <div class=\"c-login-panel\">\n <!-- ===== Logo / 标题区 ===== -->\n <div class=\"c-login__header\">\n <slot name=\"logo\">\n <div\n v-if=\"mergedProps.logoIcon\"\n class=\"c-login__logo\"\n >\n <C_Icon\n :name=\"mergedProps.logoIcon\"\n :size=\"36\"\n class=\"c-login__logo-icon\"\n />\n </div>\n </slot>\n <h2 class=\"c-login__title\">{{ mergedProps.title }}</h2>\n <p\n v-if=\"mergedProps.subtitle && activeTab !== 'register'\"\n class=\"c-login__subtitle\"\n >\n {{ mergedProps.subtitle }}\n </p>\n <!-- 注册视图的副标题 -->\n <p\n v-if=\"activeTab === 'register'\"\n class=\"c-login__subtitle c-login__subtitle--register\"\n >\n {{ t('cl_register', '注册账号') }}\n </p>\n </div>\n\n <!-- ===== 密码登录表单(默认视图) ===== -->\n <NForm\n v-show=\"activeTab === 'password'\"\n ref=\"passwordFormRef\"\n :model=\"passwordForm\"\n :rules=\"passwordRules\"\n class=\"c-login__form\"\n >\n <NFormItem\n path=\"username\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"passwordForm.username\"\n :placeholder=\"t('cl_username_ph', '请输入用户名')\"\n clearable\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:account-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n\n <NFormItem\n path=\"password\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"passwordForm.password\"\n type=\"password\"\n show-password-on=\"click\"\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\n clearable\n @keyup.enter=\"handlePasswordSubmit\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:lock-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n\n <!-- 记住我 + 忘记密码 -->\n <div\n v-if=\"feat.rememberMe || feat.forgotPassword\"\n class=\"c-login__aux-row\"\n >\n <NCheckbox\n v-if=\"feat.rememberMe\"\n v-model:checked=\"rememberMe\"\n class=\"c-login__remember\"\n >\n {{ t('cl_remember', '记住我') }}\n </NCheckbox>\n <NText\n v-if=\"feat.forgotPassword\"\n class=\"c-login__forgot\"\n @click=\"emit('forgot-password')\"\n >\n {{ t('cl_forgot', '忘记密码?') }}\n </NText>\n </div>\n </NForm>\n\n <!-- ===== 验证码登录表单 ===== -->\n <NForm\n v-show=\"activeTab === 'captcha'\"\n ref=\"captchaFormRef\"\n :model=\"captchaForm\"\n :rules=\"captchaRules\"\n class=\"c-login__form\"\n >\n <NFormItem\n path=\"account\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"captchaForm.account\"\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\n clearable\n maxlength=\"11\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:cellphone\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n\n <NFormItem\n path=\"code\"\n class=\"c-login__form-item\"\n >\n <div class=\"c-login__code-row\">\n <NInput\n v-model:value=\"captchaForm.code\"\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\n clearable\n class=\"c-login__code-input\"\n @keyup.enter=\"handleCaptchaSubmit\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:shield-key-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n <NButton\n :disabled=\"countdown > 0 || !captchaForm.account\"\n class=\"c-login__send-code-btn\"\n @click=\"handleSendCode\"\n >\n {{\n countdown > 0\n ? `${countdown}s ${t('cl_resend', '后重发')}`\n : t('cl_get_code', '获取验证码')\n }}\n </NButton>\n </div>\n </NFormItem>\n </NForm>\n\n <!-- ===== 登录按钮(密码和验证码模式) ===== -->\n <NButton\n v-if=\"activeTab === 'password' || activeTab === 'captcha'\"\n type=\"primary\"\n block\n :loading=\"props.loading\"\n :disabled=\"\n activeTab === 'password' && feat.captchaVerify && !captchaValid\n \"\n class=\"c-login__submit-btn\"\n @click=\"\n activeTab === 'password'\n ? handlePasswordSubmit()\n : handleCaptchaSubmit()\n \"\n >\n {{\n activeTab === 'password' && feat.captchaVerify && !captchaValid\n ? t('cl_verify_first', '请先完成人机验证')\n : t('cl_login_btn', '登 录')\n }}\n </NButton>\n\n <!-- ===== 人机验证拼图(仅密码登录模式) ===== -->\n <div\n v-if=\"feat.captchaVerify && activeTab === 'password'\"\n class=\"c-login__captcha-wrap\"\n >\n <C_Captcha\n ref=\"captchaRef\"\n trigger-text=\"\"\n theme=\"dark\"\n @success=\"handleCaptchaSuccess\"\n @fail=\"handleCaptchaFail\"\n @change=\"handleCaptchaChange\"\n />\n </div>\n\n <!-- ===== 返回按钮(验证码子视图) ===== -->\n <NButton\n v-if=\"activeTab === 'captcha'\"\n block\n class=\"c-login__back-btn\"\n @click=\"activeTab = 'password'\"\n >\n {{ t('cl_back', '返回') }}\n </NButton>\n\n <!-- ===== 扫码登录面板 ===== -->\n <div\n v-if=\"activeTab === 'qrcode'\"\n class=\"c-login__qr-panel\"\n >\n <div class=\"c-login__qr-frame\">\n <C_QRCode\n ref=\"qrcodeRef\"\n :value=\"qrcodeValue\"\n :size=\"148\"\n :margin=\"1\"\n :show-border=\"false\"\n color=\"#ffffff\"\n bg-color=\"#000000\"\n error-correction-level=\"M\"\n />\n <div class=\"c-login__qr-scan-line\" />\n <div class=\"c-login__qr-corner c-login__qr-corner--tl\" />\n <div class=\"c-login__qr-corner c-login__qr-corner--tr\" />\n <div class=\"c-login__qr-corner c-login__qr-corner--bl\" />\n <div class=\"c-login__qr-corner c-login__qr-corner--br\" />\n </div>\n <p class=\"c-login__qr-hint\">\n <C_Icon\n name=\"mdi:cellphone-check\"\n :size=\"13\"\n />\n {{ t('cl_qr_hint', '打开手机扫码后点击「确认」即可登录') }}\n </p>\n <NButton\n text\n size=\"tiny\"\n class=\"c-login__qr-refresh\"\n @click=\"handleQrcodeRefresh\"\n >\n <template #icon>\n <C_Icon\n name=\"mdi:refresh\"\n :size=\"12\"\n />\n </template>\n {{ t('cl_qr_refresh', '二维码已失效?点击刷新') }}\n </NButton>\n </div>\n\n <!-- ===== 返回按钮(扫码子视图,在二维码下方) ===== -->\n <NButton\n v-if=\"activeTab === 'qrcode'\"\n block\n class=\"c-login__back-btn\"\n @click=\"activeTab = 'password'\"\n >\n {{ t('cl_back', '返回') }}\n </NButton>\n\n <!-- ===== 注册表单 ===== -->\n <NForm\n v-show=\"activeTab === 'register'\"\n ref=\"registerFormRef\"\n :model=\"registerForm\"\n :rules=\"registerRules\"\n class=\"c-login__form\"\n >\n <NFormItem\n path=\"phone\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"registerForm.phone\"\n :placeholder=\"t('cl_phone_ph', '请输入手机号')\"\n clearable\n maxlength=\"11\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:cellphone\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n\n <NFormItem\n path=\"code\"\n class=\"c-login__form-item\"\n >\n <div class=\"c-login__code-row\">\n <NInput\n v-model:value=\"registerForm.code\"\n :placeholder=\"t('cl_code_ph', '请输入验证码')\"\n clearable\n class=\"c-login__code-input\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:shield-key-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n <NButton\n :disabled=\"regCountdown > 0 || !registerForm.phone\"\n class=\"c-login__send-code-btn\"\n @click=\"handleRegSendCode\"\n >\n {{\n regCountdown > 0\n ? `${regCountdown}s ${t('cl_resend', '后重发')}`\n : t('cl_get_code', '获取验证码')\n }}\n </NButton>\n </div>\n </NFormItem>\n\n <NFormItem\n path=\"password\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"registerForm.password\"\n type=\"password\"\n show-password-on=\"click\"\n :placeholder=\"t('cl_password_ph', '请输入密码')\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:lock-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n\n <NFormItem\n path=\"confirmPassword\"\n class=\"c-login__form-item\"\n >\n <NInput\n v-model:value=\"registerForm.confirmPassword\"\n type=\"password\"\n show-password-on=\"click\"\n :placeholder=\"t('cl_confirm_pw_ph', '请再次输入密码')\"\n @keyup.enter=\"handleRegisterSubmit\"\n >\n <template #prefix>\n <C_Icon\n name=\"mdi:lock-check-outline\"\n :size=\"16\"\n class=\"c-login__input-icon\"\n />\n </template>\n </NInput>\n </NFormItem>\n </NForm>\n\n <!-- ===== 注册确认按钮 ===== -->\n <NButton\n v-if=\"activeTab === 'register'\"\n type=\"primary\"\n block\n :loading=\"props.loading\"\n class=\"c-login__submit-btn\"\n @click=\"handleRegisterSubmit\"\n >\n {{ t('cl_confirm', '确认') }}\n </NButton>\n\n <!-- ===== 注册视图返回按钮 ===== -->\n <NButton\n v-if=\"activeTab === 'register'\"\n block\n class=\"c-login__back-btn\"\n @click=\"activeTab = 'password'\"\n >\n {{ t('cl_back', '返回') }}\n </NButton>\n\n <!-- ===== 登录方式切换(仅密码登录默认视图显示) ===== -->\n <div\n v-if=\"activeTab === 'password' && (feat.captchaLogin || feat.qrcodeLogin)\"\n class=\"c-login__mode-switch\"\n >\n <NButton\n v-if=\"feat.captchaLogin\"\n class=\"c-login__mode-btn\"\n size=\"small\"\n @click=\"activeTab = 'captcha'\"\n >\n {{ t('cl_captcha_login', '验证码登录') }}\n </NButton>\n <NButton\n v-if=\"feat.qrcodeLogin\"\n class=\"c-login__mode-btn\"\n size=\"small\"\n @click=\"activeTab = 'qrcode'\"\n >\n {{ t('cl_qr_login', '扫码登录') }}\n </NButton>\n </div>\n\n <!-- ===== 其他登录方式(仅密码登录默认视图显示) ===== -->\n <template\n v-if=\"activeTab === 'password' && feat.socialLogin && socialList.length\"\n >\n <NDivider class=\"c-login__divider\">{{\n t('cl_social_login', '其他账号登录')\n }}</NDivider>\n <div class=\"c-login__social-row\">\n <NTooltip\n v-for=\"provider in socialList\"\n :key=\"provider.key\"\n trigger=\"hover\"\n placement=\"top\"\n >\n <template #trigger>\n <NButton\n circle\n class=\"c-login__social-btn\"\n @click=\"emit('social-login', provider.key)\"\n >\n <template #icon>\n <C_Icon\n :name=\"provider.icon\"\n :size=\"16\"\n />\n </template>\n </NButton>\n </template>\n {{ provider.label }}\n </NTooltip>\n </div>\n </template>\n\n <!-- ===== 注册入口(仅密码登录默认视图显示) ===== -->\n <div\n v-if=\"activeTab === 'password' && feat.register\"\n class=\"c-login__register-row\"\n >\n <NText class=\"c-login__register-hint\">{{\n t('cl_no_account', '还没有账号?')\n }}</NText>\n <NText\n class=\"c-login__register-link\"\n @click=\"activeTab = 'register'\"\n >\n {{ t('cl_register', '注册账号') }}\n </NText>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'\n import type { FormInst, FormRules } from 'naive-ui/es'\n import C_Icon from '../C_Icon/index.vue'\n import C_Captcha from '../C_Captcha/index.vue'\n import C_QRCode from '../C_QRCode/index.vue'\n import { RULE_COMBOS } from '@robot-admin/form-validate'\n import {\n DEFAULT_FEATURES,\n DEFAULT_SOCIAL_PROVIDERS,\n type LoginProps,\n type PasswordFormData,\n type CaptchaFormData,\n type RegisterFormData,\n } from './types'\n\n // ===== i18n helper =====\n const t = (key: string, fallback: string) =>\n typeof (globalThis as any).$t === 'function'\n ? (globalThis as any).$t(key, fallback, 'robot_admin')\n : fallback\n\n // ===== Props & Emits =====\n const props = withDefaults(defineProps<LoginProps>(), {\n title: '欢迎回来',\n loading: false,\n storageKey: 'c_login_remember',\n defaultUsername: '',\n defaultPassword: '',\n })\n\n const emit = defineEmits<{\n (\n e: 'submit',\n data: PasswordFormData & {\n captchaToken?: string\n captchaTimestamp?: number\n }\n ): void\n (e: 'captcha-submit', data: CaptchaFormData): void\n (e: 'send-code', account: string): void\n (e: 'social-login', provider: string): void\n (e: 'forgot-password'): void\n (e: 'qrcode-refresh'): void\n (e: 'register-submit', data: RegisterFormData): void\n (e: 'register-send-code', phone: string): void\n }>()\n\n // ===== 合并 Props(填充默认值) =====\n const mergedProps = computed(() => ({\n ...props,\n features: { ...DEFAULT_FEATURES, ...props.features },\n socialProviders: props.socialProviders ?? DEFAULT_SOCIAL_PROVIDERS,\n }))\n\n const feat = computed(\n () => mergedProps.value.features as Required<typeof DEFAULT_FEATURES>\n )\n const socialList = computed(() => mergedProps.value.socialProviders!)\n\n // ===== Tab(默认密码登录) =====\n const activeTab = ref<'password' | 'captcha' | 'qrcode' | 'register'>(\n 'password'\n )\n\n // ===== 密码表单 =====\n const passwordFormRef = ref<FormInst | null>(null)\n const passwordForm = reactive<PasswordFormData>({\n username: props.defaultUsername ?? '',\n password: props.defaultPassword ?? '',\n })\n\n const passwordRules: FormRules = {\n username: [\n {\n required: true,\n message: t('cl_username_rule', '请输入用户名'),\n trigger: 'blur',\n },\n ],\n password: [\n {\n required: true,\n message: t('cl_password_rule', '请输入密码'),\n trigger: 'blur',\n },\n ],\n }\n\n // ===== 记住我 =====\n const rememberMe = ref(false)\n\n watch(rememberMe, val => {\n if (val) {\n localStorage.setItem(\n props.storageKey!,\n JSON.stringify({ username: passwordForm.username })\n )\n } else {\n localStorage.removeItem(props.storageKey!)\n }\n })\n\n onMounted(() => {\n try {\n const saved = localStorage.getItem(props.storageKey!)\n if (saved) {\n const data = JSON.parse(saved)\n if (data.username) {\n passwordForm.username = data.username\n rememberMe.value = true\n }\n }\n } catch {\n /* ignore */\n }\n })\n\n // ===== 密码登录提交 =====\n const handlePasswordSubmit = async () => {\n try {\n await passwordFormRef.value?.validate()\n } catch {\n return // 校验失败,表单会显示行内错误提示\n }\n emit('submit', {\n ...passwordForm,\n ...(captchaValid.value\n ? {\n captchaToken: captchaToken.value,\n captchaTimestamp: captchaTimestamp.value,\n }\n : {}),\n })\n }\n\n // ===== 验证码表单 =====\n const captchaFormRef = ref<FormInst | null>(null)\n const captchaForm = reactive<CaptchaFormData>({ account: '', code: '' })\n const countdown = ref(0)\n let countdownTimer: ReturnType<typeof setInterval> | null = null\n\n const captchaRules: FormRules = {\n account: RULE_COMBOS.mobile('手机号') as FormRules['account'],\n code: [\n {\n required: true,\n message: t('cl_code_rule', '请输入验证码'),\n trigger: 'blur',\n },\n ],\n }\n\n // ===== 人机验证 =====\n const captchaRef = ref<InstanceType<typeof C_Captcha>>()\n const captchaValid = ref(false)\n const captchaToken = ref('')\n const captchaTimestamp = ref(0)\n\n const handleCaptchaSuccess = (data: { token: string; timestamp: number }) => {\n captchaValid.value = true\n captchaToken.value = data.token\n captchaTimestamp.value = data.timestamp\n }\n const handleCaptchaFail = () => {\n captchaValid.value = false\n }\n const handleCaptchaChange = (valid: boolean) => {\n captchaValid.value = valid\n if (!valid) {\n captchaToken.value = ''\n captchaTimestamp.value = 0\n }\n }\n\n const resetCaptcha = () => {\n captchaValid.value = false\n captchaToken.value = ''\n captchaTimestamp.value = 0\n captchaRef.value?.reset()\n }\n\n const handleCaptchaSubmit = async () => {\n try {\n await captchaFormRef.value?.validate()\n } catch {\n return\n }\n if (feat.value.captchaVerify && !captchaValid.value) return\n emit('captcha-submit', { ...captchaForm })\n }\n\n // ===== 发送验证码 =====\n const handleSendCode = () => {\n if (!captchaForm.account) return\n emit('send-code', captchaForm.account)\n if (countdownTimer) {\n clearInterval(countdownTimer)\n countdownTimer = null\n }\n countdown.value = 60\n countdownTimer = setInterval(() => {\n countdown.value--\n if (countdown.value <= 0) {\n clearInterval(countdownTimer!)\n countdownTimer = null\n }\n }, 1000)\n }\n\n // ===== 扫码登录 =====\n const qrcodeRef = ref<any>(null)\n const qrcodeValue = ref(\n `https://robot-admin.app/qrcode-login?t=${Date.now()}`\n )\n\n const handleQrcodeRefresh = () => {\n qrcodeValue.value = `https://robot-admin.app/qrcode-login?t=${Date.now()}`\n emit('qrcode-refresh')\n }\n\n // ===== 注册表单 =====\n const registerFormRef = ref<FormInst | null>(null)\n const registerForm = reactive<RegisterFormData>({\n phone: '',\n code: '',\n password: '',\n confirmPassword: '',\n })\n const regCountdown = ref(0)\n let regCountdownTimer: ReturnType<typeof setInterval> | null = null\n\n const registerRules: FormRules = {\n phone: RULE_COMBOS.mobile('手机号') as FormRules['phone'],\n code: [\n {\n required: true,\n message: t('cl_code_rule', '请输入验证码'),\n trigger: 'blur',\n },\n ],\n password: RULE_COMBOS.password('密码') as FormRules['password'],\n confirmPassword: [\n {\n required: true,\n message: t('cl_confirm_pw_rule', '请再次输入密码'),\n trigger: 'blur',\n },\n {\n validator: (_rule: any, value: string) => {\n if (value !== registerForm.password) {\n return new Error(t('cl_pw_mismatch', '两次密码输入不一致'))\n }\n return true\n },\n trigger: 'blur',\n },\n ],\n }\n\n const handleRegSendCode = () => {\n if (!registerForm.phone) return\n emit('register-send-code', registerForm.phone)\n if (regCountdownTimer) {\n clearInterval(regCountdownTimer)\n regCountdownTimer = null\n }\n regCountdown.value = 60\n regCountdownTimer = setInterval(() => {\n regCountdown.value--\n if (regCountdown.value <= 0) {\n clearInterval(regCountdownTimer!)\n regCountdownTimer = null\n }\n }, 1000)\n }\n\n const handleRegisterSubmit = async () => {\n try {\n await registerFormRef.value?.validate()\n } catch {\n return\n }\n emit('register-submit', { ...registerForm })\n }\n\n // ===== 清理定时器 =====\n onUnmounted(() => {\n if (countdownTimer) {\n clearInterval(countdownTimer)\n countdownTimer = null\n }\n if (regCountdownTimer) {\n clearInterval(regCountdownTimer)\n regCountdownTimer = null\n }\n })\n\n // ===== 暴露方法给父组件 =====\n defineExpose({ resetCaptcha })\n</script>\n\n<style scoped lang=\"scss\">\n @use './index.scss';\n</style>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Map-
|
|
1
|
+
{"version":3,"file":"C_Map-DNnBcY70.css","names":[],"sources":["../src/components/C_Map/index.vue?vue&type=style&index=0&scoped=340816cd&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled */.c-map[data-v-340816cd] {\n position: relative;\n width: 100%;\n min-height: 300px;\n background: #f5f5f5;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-container[data-v-340816cd] {\n width: 100%;\n height: 100%;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-loading[data-v-340816cd] {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(255, 255, 255, 0.9);\n border-radius: 4px;\n z-index: 1000;\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Map-
|
|
1
|
+
{"version":3,"file":"C_Map-LEXIx6zM.css","names":[],"sources":["../src/components/C_Map/index.vue?vue&type=style&index=0&scoped=340816cd&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled */.c-map[data-v-340816cd] {\n position: relative;\n width: 100%;\n min-height: 300px;\n background: #f5f5f5;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-container[data-v-340816cd] {\n width: 100%;\n height: 100%;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-loading[data-v-340816cd] {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(255, 255, 255, 0.9);\n border-radius: 4px;\n z-index: 1000;\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|
package/dist/C_Map.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* unplugin-vue-components disabled */.c-map[data-v-
|
|
1
|
+
/* unplugin-vue-components disabled */.c-map[data-v-340816cd] {
|
|
2
2
|
position: relative;
|
|
3
3
|
width: 100%;
|
|
4
4
|
min-height: 300px;
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
border-radius: 4px;
|
|
7
7
|
overflow: hidden;
|
|
8
8
|
}
|
|
9
|
-
.c-map .map-container[data-v-
|
|
9
|
+
.c-map .map-container[data-v-340816cd] {
|
|
10
10
|
width: 100%;
|
|
11
11
|
height: 100%;
|
|
12
12
|
border-radius: 4px;
|
|
13
13
|
overflow: hidden;
|
|
14
14
|
}
|
|
15
|
-
.c-map .map-loading[data-v-
|
|
15
|
+
.c-map .map-loading[data-v-340816cd] {
|
|
16
16
|
position: absolute;
|
|
17
17
|
top: 0;
|
|
18
18
|
left: 0;
|
|
@@ -26,4 +26,4 @@
|
|
|
26
26
|
z-index: 1000;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/*# sourceMappingURL=C_Map-
|
|
29
|
+
/*# sourceMappingURL=C_Map-DNnBcY70.css.map*/
|
package/dist/C_Map.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Map.js","names":["height"],"sources":["../src/components/C_Map/data.ts","../src/components/C_Map/index.vue","../src/components/C_Map/index.vue","../src/components/C_Map/index.vue"],"sourcesContent":["export const MAP_TYPES = [\r\n { label: \"OpenStreetMap\", value: \"osm\" },\r\n { label: \"高德地图\", value: \"amap\" },\r\n] as const;\r\n\r\nexport type MapType = (typeof MAP_TYPES)[number][\"value\"];\r\n\r\nexport interface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nexport interface MapConfig {\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: MapType;\r\n amapKey?: string;\r\n}\r\n\r\nexport const DEFAULT_MAP_CONFIG: Required<\r\n Omit<MapConfig, \"markers\" | \"amapKey\">\r\n> = {\r\n height: \"400px\",\r\n center: [39.9042, 116.4074],\r\n zoom: 10,\r\n mapType: \"osm\",\r\n};\r\n\r\nexport const OSM_TILE_CONFIG = {\r\n url: \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\r\n attribution: \"© OpenStreetMap contributors\",\r\n maxZoom: 19,\r\n minZoom: 1,\r\n tileSize: 256,\r\n detectRetina: true,\r\n} as const;\r\n\r\nexport const AMAP_CONFIG = {\r\n apiUrl: \"https://webapi.amap.com/maps?v=2.0&key=\",\r\n note: \"高德地图需要API Key,如需使用请申请:https://lbs.amap.com/api/javascript-api/guide/create/\",\r\n} as const;\r\n\r\nexport const MAP_ICONS = {\r\n iconRetinaUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png\",\r\n iconUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png\",\r\n shadowUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png\",\r\n} as const;\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_Map.js","names":[],"sources":["../src/components/C_Map/data.ts","../src/components/C_Map/index.vue"],"sourcesContent":["export const MAP_TYPES = [\r\n { label: \"OpenStreetMap\", value: \"osm\" },\r\n { label: \"高德地图\", value: \"amap\" },\r\n] as const;\r\n\r\nexport type MapType = (typeof MAP_TYPES)[number][\"value\"];\r\n\r\nexport interface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nexport interface MapConfig {\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: MapType;\r\n amapKey?: string;\r\n}\r\n\r\nexport const DEFAULT_MAP_CONFIG: Required<\r\n Omit<MapConfig, \"markers\" | \"amapKey\">\r\n> = {\r\n height: \"400px\",\r\n center: [39.9042, 116.4074],\r\n zoom: 10,\r\n mapType: \"osm\",\r\n};\r\n\r\nexport const OSM_TILE_CONFIG = {\r\n url: \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\r\n attribution: \"© OpenStreetMap contributors\",\r\n maxZoom: 19,\r\n minZoom: 1,\r\n tileSize: 256,\r\n detectRetina: true,\r\n} as const;\r\n\r\nexport const AMAP_CONFIG = {\r\n apiUrl: \"https://webapi.amap.com/maps?v=2.0&key=\",\r\n note: \"高德地图需要API Key,如需使用请申请:https://lbs.amap.com/api/javascript-api/guide/create/\",\r\n} as const;\r\n\r\nexport const MAP_ICONS = {\r\n iconRetinaUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png\",\r\n iconUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png\",\r\n shadowUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png\",\r\n} as const;\r\n","<!--\n * @Author: ChenYu ycyplus@gmail.com\n * @Date: 2025-12-02\n * @Description: 地图组件\n * @Migration: naive-ui-components 组件库迁移版本\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"c-map\">\n <div\n ref=\"mapContainer\"\n class=\"map-container\"\n :style=\"{ height: height }\"\n ></div>\n <div v-if=\"loading\" class=\"map-loading\">\n <NSpin size=\"large\" />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\nimport { NSpin } from \"naive-ui\";\nimport L from \"leaflet\";\nimport \"leaflet/dist/leaflet.css\";\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\n\ndefineOptions({ name: \"C_Map\" });\n\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\nL.Icon.Default.mergeOptions(MAP_ICONS);\n\ninterface MapMarker {\n lat: number;\n lng: number;\n popup?: string;\n}\n\nconst props = withDefaults(\n defineProps<{\n height?: string;\n center?: [number, number];\n zoom?: number;\n markers?: MapMarker[];\n mapType?: \"osm\" | \"amap\";\n amapKey?: string;\n }>(),\n {\n height: \"400px\",\n center: () => [39.9042, 116.4074],\n zoom: 10,\n markers: () => [],\n mapType: \"osm\",\n amapKey: \"\",\n },\n);\n\nconst emit = defineEmits<{\n ready: [map: any];\n markerClick: [marker: MapMarker, event: any];\n}>();\n\nconst mapContainer = ref<HTMLElement>();\nconst loading = ref(true);\nlet map: any = null;\n\nconst initOSMMap = async () => {\n if (!mapContainer.value) return;\n try {\n mapContainer.value.innerHTML = \"\";\n map = L.map(mapContainer.value, {\n center: props.center,\n zoom: props.zoom,\n zoomControl: true,\n preferCanvas: true,\n });\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\n tileLayer.addTo(map);\n addMarkers();\n await nextTick();\n requestAnimationFrame(() => {\n map?.invalidateSize({ reset: true, pan: false });\n });\n loading.value = false;\n emit(\"ready\", map);\n } catch (error) {\n console.error(\"OpenStreetMap初始化失败:\", error);\n loading.value = false;\n }\n};\n\nconst initAMap = async () => {\n if (!mapContainer.value || !props.amapKey) return;\n try {\n mapContainer.value.innerHTML = \"\";\n const script = document.createElement(\"script\");\n script.type = \"text/javascript\";\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\n script.onload = () => {\n if ((window as any).AMap) {\n const amap = new (window as any).AMap.Map(mapContainer.value, {\n zoom: props.zoom,\n center: props.center,\n });\n addAMapMarkers(amap);\n loading.value = false;\n emit(\"ready\", amap);\n }\n };\n script.onerror = () => {\n loading.value = false;\n };\n document.head.appendChild(script);\n } catch {\n loading.value = false;\n }\n};\n\nconst addMarkers = () => {\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\n map.eachLayer((layer: any) => {\n if (layer instanceof L.Marker) {\n map.removeLayer(layer);\n }\n });\n props.markers.forEach((marker) => {\n const leafletMarker = L.marker([marker.lat, marker.lng]);\n if (marker.popup) {\n leafletMarker.bindPopup(marker.popup);\n leafletMarker.on(\"click\", (event: any) => {\n emit(\"markerClick\", marker, event);\n });\n }\n leafletMarker.addTo(map);\n });\n};\n\nconst addAMapMarkers = (amap: any) => {\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\n props.markers.forEach((marker) => {\n const amapMarker = new (window as any).AMap.Marker({\n position: [marker.lat, marker.lng],\n title: marker.popup || \"\",\n });\n if (marker.popup) {\n const infoWindow = new (window as any).AMap.InfoWindow({\n content: marker.popup,\n offset: new (window as any).AMap.Pixel(0, -30),\n });\n amapMarker.on(\"click\", () => {\n infoWindow.open(amap, amapMarker.getPosition());\n emit(\"markerClick\", marker, null);\n });\n }\n amapMarker.setMap(amap);\n });\n};\n\nwatch(\n () => props.markers,\n () => {\n if (map && props.mapType === \"osm\") addMarkers();\n },\n { deep: true },\n);\n\nwatch(\n () => props.mapType,\n async (newType, oldType) => {\n if (newType === oldType) return;\n if (map) {\n map.remove();\n map = null;\n loading.value = true;\n await nextTick();\n if (newType === \"amap\" && props.amapKey) await initAMap();\n else await initOSMMap();\n }\n },\n);\n\nwatch(\n [() => props.center, () => props.zoom],\n () => {\n if (map) map.setView(props.center, props.zoom);\n },\n { deep: true },\n);\n\nonMounted(async () => {\n await nextTick();\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\n else await initOSMMap();\n});\n\nonUnmounted(() => {\n if (map) {\n map.remove();\n map = null;\n }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@use \"./index.scss\";\n</style>\n"],"mappings":"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"}
|
package/dist/C_Map2.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./export-helper.js";import{
|
|
1
|
+
import{t as e}from"./export-helper.js";import{createCommentVNode as t,createElementBlock as n,createElementVNode as r,createVNode as i,defineComponent as a,nextTick as o,normalizeStyle as s,onMounted as c,onUnmounted as l,openBlock as u,ref as d,unref as f,watch as p}from"vue";import{NSpin as m}from"naive-ui";import h from"leaflet";import"leaflet/dist/leaflet.css";const g=[{label:`OpenStreetMap`,value:`osm`},{label:`高德地图`,value:`amap`}],_={height:`400px`,center:[39.9042,116.4074],zoom:10,mapType:`osm`},v={url:`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`,attribution:`© OpenStreetMap contributors`,maxZoom:19,minZoom:1,tileSize:256,detectRetina:!0},y={apiUrl:`https://webapi.amap.com/maps?v=2.0&key=`,note:`高德地图需要API Key,如需使用请申请:https://lbs.amap.com/api/javascript-api/guide/create/`},b={iconRetinaUrl:`https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png`,iconUrl:`https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png`,shadowUrl:`https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png`},x={class:`c-map`},S={key:0,class:`map-loading`};var C=e(a({name:`C_Map`,__name:`index`,props:{height:{default:`400px`},center:{default:()=>[39.9042,116.4074]},zoom:{default:10},markers:{default:()=>[]},mapType:{default:`osm`},amapKey:{default:``}},emits:[`ready`,`markerClick`],setup(e,{emit:a}){delete h.Icon.Default.prototype._getIconUrl,h.Icon.Default.mergeOptions(b);let g=e,_=a,C=d(),w=d(!0),T=null,E=async()=>{if(C.value)try{C.value.innerHTML=``,T=h.map(C.value,{center:g.center,zoom:g.zoom,zoomControl:!0,preferCanvas:!0}),h.tileLayer(v.url,v).addTo(T),O(),await o(),requestAnimationFrame(()=>{T?.invalidateSize({reset:!0,pan:!1})}),w.value=!1,_(`ready`,T)}catch(e){console.error(`OpenStreetMap初始化失败:`,e),w.value=!1}},D=async()=>{if(!(!C.value||!g.amapKey))try{C.value.innerHTML=``;let e=document.createElement(`script`);e.type=`text/javascript`,e.src=`${y.apiUrl}${g.amapKey}`,e.onload=()=>{if(window.AMap){let e=new window.AMap.Map(C.value,{zoom:g.zoom,center:g.center});k(e),w.value=!1,_(`ready`,e)}},e.onerror=()=>{w.value=!1},document.head.appendChild(e)}catch{w.value=!1}},O=()=>{!T||g.mapType!==`osm`||!g.markers||(T.eachLayer(e=>{e instanceof h.Marker&&T.removeLayer(e)}),g.markers.forEach(e=>{let t=h.marker([e.lat,e.lng]);e.popup&&(t.bindPopup(e.popup),t.on(`click`,t=>{_(`markerClick`,e,t)})),t.addTo(T)}))},k=e=>{!e||g.mapType!==`amap`||!g.markers||g.markers.forEach(t=>{let n=new window.AMap.Marker({position:[t.lat,t.lng],title:t.popup||``});if(t.popup){let r=new window.AMap.InfoWindow({content:t.popup,offset:new window.AMap.Pixel(0,-30)});n.on(`click`,()=>{r.open(e,n.getPosition()),_(`markerClick`,t,null)})}n.setMap(e)})};return p(()=>g.markers,()=>{T&&g.mapType===`osm`&&O()},{deep:!0}),p(()=>g.mapType,async(e,t)=>{e!==t&&T&&(T.remove(),T=null,w.value=!0,await o(),e===`amap`&&g.amapKey?await D():await E())}),p([()=>g.center,()=>g.zoom],()=>{T&&T.setView(g.center,g.zoom)},{deep:!0}),c(async()=>{await o(),g.mapType===`amap`&&g.amapKey?await D():await E()}),l(()=>{T&&=(T.remove(),null)}),(e,a)=>(u(),n(`div`,x,[r(`div`,{ref_key:`mapContainer`,ref:C,class:`map-container`,style:s({height:e.height})},null,4),w.value?(u(),n(`div`,S,[i(f(m),{size:`large`})])):t(`v-if`,!0)]))}}),[[`__scopeId`,`data-v-340816cd`]]);export{v as i,_ as n,g as r,C as t};
|
|
2
2
|
//# sourceMappingURL=C_Map2.js.map
|
package/dist/C_Map2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Map2.js","names":["height"],"sources":["../src/components/C_Map/data.ts","../src/components/C_Map/index.vue","../src/components/C_Map/index.vue","../src/components/C_Map/index.vue"],"sourcesContent":["export const MAP_TYPES = [\r\n { label: \"OpenStreetMap\", value: \"osm\" },\r\n { label: \"高德地图\", value: \"amap\" },\r\n] as const;\r\n\r\nexport type MapType = (typeof MAP_TYPES)[number][\"value\"];\r\n\r\nexport interface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nexport interface MapConfig {\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: MapType;\r\n amapKey?: string;\r\n}\r\n\r\nexport const DEFAULT_MAP_CONFIG: Required<\r\n Omit<MapConfig, \"markers\" | \"amapKey\">\r\n> = {\r\n height: \"400px\",\r\n center: [39.9042, 116.4074],\r\n zoom: 10,\r\n mapType: \"osm\",\r\n};\r\n\r\nexport const OSM_TILE_CONFIG = {\r\n url: \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\r\n attribution: \"© OpenStreetMap contributors\",\r\n maxZoom: 19,\r\n minZoom: 1,\r\n tileSize: 256,\r\n detectRetina: true,\r\n} as const;\r\n\r\nexport const AMAP_CONFIG = {\r\n apiUrl: \"https://webapi.amap.com/maps?v=2.0&key=\",\r\n note: \"高德地图需要API Key,如需使用请申请:https://lbs.amap.com/api/javascript-api/guide/create/\",\r\n} as const;\r\n\r\nexport const MAP_ICONS = {\r\n iconRetinaUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png\",\r\n iconUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png\",\r\n shadowUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png\",\r\n} as const;\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n","/* unplugin-vue-components disabled */<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2025-12-02\r\n * @Description: 地图组件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"c-map\">\r\n <div\r\n ref=\"mapContainer\"\r\n class=\"map-container\"\r\n :style=\"{ height: height }\"\r\n ></div>\r\n <div v-if=\"loading\" class=\"map-loading\">\r\n <NSpin size=\"large\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\r\nimport { NSpin } from \"naive-ui\";\r\nimport L from \"leaflet\";\r\nimport \"leaflet/dist/leaflet.css\";\r\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\r\n\r\ndefineOptions({ name: \"C_Map\" });\r\n\r\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\r\nL.Icon.Default.mergeOptions(MAP_ICONS);\r\n\r\ninterface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: \"osm\" | \"amap\";\r\n amapKey?: string;\r\n }>(),\r\n {\r\n height: \"400px\",\r\n center: () => [39.9042, 116.4074],\r\n zoom: 10,\r\n markers: () => [],\r\n mapType: \"osm\",\r\n amapKey: \"\",\r\n },\r\n);\r\n\r\nconst emit = defineEmits<{\r\n ready: [map: any];\r\n markerClick: [marker: MapMarker, event: any];\r\n}>();\r\n\r\nconst mapContainer = ref<HTMLElement>();\r\nconst loading = ref(true);\r\nlet map: any = null;\r\n\r\nconst initOSMMap = async () => {\r\n if (!mapContainer.value) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n map = L.map(mapContainer.value, {\r\n center: props.center,\r\n zoom: props.zoom,\r\n zoomControl: true,\r\n preferCanvas: true,\r\n });\r\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\r\n tileLayer.addTo(map);\r\n addMarkers();\r\n await nextTick();\r\n requestAnimationFrame(() => {\r\n map?.invalidateSize({ reset: true, pan: false });\r\n });\r\n loading.value = false;\r\n emit(\"ready\", map);\r\n } catch (error) {\r\n console.error(\"OpenStreetMap初始化失败:\", error);\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst initAMap = async () => {\r\n if (!mapContainer.value || !props.amapKey) return;\r\n try {\r\n mapContainer.value.innerHTML = \"\";\r\n const script = document.createElement(\"script\");\r\n script.type = \"text/javascript\";\r\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\r\n script.onload = () => {\r\n if ((window as any).AMap) {\r\n const amap = new (window as any).AMap.Map(mapContainer.value, {\r\n zoom: props.zoom,\r\n center: props.center,\r\n });\r\n addAMapMarkers(amap);\r\n loading.value = false;\r\n emit(\"ready\", amap);\r\n }\r\n };\r\n script.onerror = () => {\r\n loading.value = false;\r\n };\r\n document.head.appendChild(script);\r\n } catch {\r\n loading.value = false;\r\n }\r\n};\r\n\r\nconst addMarkers = () => {\r\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\r\n map.eachLayer((layer: any) => {\r\n if (layer instanceof L.Marker) {\r\n map.removeLayer(layer);\r\n }\r\n });\r\n props.markers.forEach((marker) => {\r\n const leafletMarker = L.marker([marker.lat, marker.lng]);\r\n if (marker.popup) {\r\n leafletMarker.bindPopup(marker.popup);\r\n leafletMarker.on(\"click\", (event: any) => {\r\n emit(\"markerClick\", marker, event);\r\n });\r\n }\r\n leafletMarker.addTo(map);\r\n });\r\n};\r\n\r\nconst addAMapMarkers = (amap: any) => {\r\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\r\n props.markers.forEach((marker) => {\r\n const amapMarker = new (window as any).AMap.Marker({\r\n position: [marker.lat, marker.lng],\r\n title: marker.popup || \"\",\r\n });\r\n if (marker.popup) {\r\n const infoWindow = new (window as any).AMap.InfoWindow({\r\n content: marker.popup,\r\n offset: new (window as any).AMap.Pixel(0, -30),\r\n });\r\n amapMarker.on(\"click\", () => {\r\n infoWindow.open(amap, amapMarker.getPosition());\r\n emit(\"markerClick\", marker, null);\r\n });\r\n }\r\n amapMarker.setMap(amap);\r\n });\r\n};\r\n\r\nwatch(\r\n () => props.markers,\r\n () => {\r\n if (map && props.mapType === \"osm\") addMarkers();\r\n },\r\n { deep: true },\r\n);\r\n\r\nwatch(\r\n () => props.mapType,\r\n async (newType, oldType) => {\r\n if (newType === oldType) return;\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n loading.value = true;\r\n await nextTick();\r\n if (newType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n }\r\n },\r\n);\r\n\r\nwatch(\r\n [() => props.center, () => props.zoom],\r\n () => {\r\n if (map) map.setView(props.center, props.zoom);\r\n },\r\n { deep: true },\r\n);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\r\n else await initOSMMap();\r\n});\r\n\r\nonUnmounted(() => {\r\n if (map) {\r\n map.remove();\r\n map = null;\r\n }\r\n});\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n@use \"./index.scss\";\r\n</style>\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_Map2.js","names":[],"sources":["../src/components/C_Map/data.ts","../src/components/C_Map/index.vue"],"sourcesContent":["export const MAP_TYPES = [\r\n { label: \"OpenStreetMap\", value: \"osm\" },\r\n { label: \"高德地图\", value: \"amap\" },\r\n] as const;\r\n\r\nexport type MapType = (typeof MAP_TYPES)[number][\"value\"];\r\n\r\nexport interface MapMarker {\r\n lat: number;\r\n lng: number;\r\n popup?: string;\r\n}\r\n\r\nexport interface MapConfig {\r\n height?: string;\r\n center?: [number, number];\r\n zoom?: number;\r\n markers?: MapMarker[];\r\n mapType?: MapType;\r\n amapKey?: string;\r\n}\r\n\r\nexport const DEFAULT_MAP_CONFIG: Required<\r\n Omit<MapConfig, \"markers\" | \"amapKey\">\r\n> = {\r\n height: \"400px\",\r\n center: [39.9042, 116.4074],\r\n zoom: 10,\r\n mapType: \"osm\",\r\n};\r\n\r\nexport const OSM_TILE_CONFIG = {\r\n url: \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\r\n attribution: \"© OpenStreetMap contributors\",\r\n maxZoom: 19,\r\n minZoom: 1,\r\n tileSize: 256,\r\n detectRetina: true,\r\n} as const;\r\n\r\nexport const AMAP_CONFIG = {\r\n apiUrl: \"https://webapi.amap.com/maps?v=2.0&key=\",\r\n note: \"高德地图需要API Key,如需使用请申请:https://lbs.amap.com/api/javascript-api/guide/create/\",\r\n} as const;\r\n\r\nexport const MAP_ICONS = {\r\n iconRetinaUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png\",\r\n iconUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png\",\r\n shadowUrl:\r\n \"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png\",\r\n} as const;\r\n","<!--\n * @Author: ChenYu ycyplus@gmail.com\n * @Date: 2025-12-02\n * @Description: 地图组件\n * @Migration: naive-ui-components 组件库迁移版本\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"c-map\">\n <div\n ref=\"mapContainer\"\n class=\"map-container\"\n :style=\"{ height: height }\"\n ></div>\n <div v-if=\"loading\" class=\"map-loading\">\n <NSpin size=\"large\" />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted } from \"vue\";\nimport { NSpin } from \"naive-ui\";\nimport L from \"leaflet\";\nimport \"leaflet/dist/leaflet.css\";\nimport { OSM_TILE_CONFIG, AMAP_CONFIG, MAP_ICONS } from \"./data\";\n\ndefineOptions({ name: \"C_Map\" });\n\ndelete (L.Icon.Default.prototype as any)._getIconUrl;\nL.Icon.Default.mergeOptions(MAP_ICONS);\n\ninterface MapMarker {\n lat: number;\n lng: number;\n popup?: string;\n}\n\nconst props = withDefaults(\n defineProps<{\n height?: string;\n center?: [number, number];\n zoom?: number;\n markers?: MapMarker[];\n mapType?: \"osm\" | \"amap\";\n amapKey?: string;\n }>(),\n {\n height: \"400px\",\n center: () => [39.9042, 116.4074],\n zoom: 10,\n markers: () => [],\n mapType: \"osm\",\n amapKey: \"\",\n },\n);\n\nconst emit = defineEmits<{\n ready: [map: any];\n markerClick: [marker: MapMarker, event: any];\n}>();\n\nconst mapContainer = ref<HTMLElement>();\nconst loading = ref(true);\nlet map: any = null;\n\nconst initOSMMap = async () => {\n if (!mapContainer.value) return;\n try {\n mapContainer.value.innerHTML = \"\";\n map = L.map(mapContainer.value, {\n center: props.center,\n zoom: props.zoom,\n zoomControl: true,\n preferCanvas: true,\n });\n const tileLayer = L.tileLayer(OSM_TILE_CONFIG.url, OSM_TILE_CONFIG);\n tileLayer.addTo(map);\n addMarkers();\n await nextTick();\n requestAnimationFrame(() => {\n map?.invalidateSize({ reset: true, pan: false });\n });\n loading.value = false;\n emit(\"ready\", map);\n } catch (error) {\n console.error(\"OpenStreetMap初始化失败:\", error);\n loading.value = false;\n }\n};\n\nconst initAMap = async () => {\n if (!mapContainer.value || !props.amapKey) return;\n try {\n mapContainer.value.innerHTML = \"\";\n const script = document.createElement(\"script\");\n script.type = \"text/javascript\";\n script.src = `${AMAP_CONFIG.apiUrl}${props.amapKey}`;\n script.onload = () => {\n if ((window as any).AMap) {\n const amap = new (window as any).AMap.Map(mapContainer.value, {\n zoom: props.zoom,\n center: props.center,\n });\n addAMapMarkers(amap);\n loading.value = false;\n emit(\"ready\", amap);\n }\n };\n script.onerror = () => {\n loading.value = false;\n };\n document.head.appendChild(script);\n } catch {\n loading.value = false;\n }\n};\n\nconst addMarkers = () => {\n if (!map || props.mapType !== \"osm\" || !props.markers) return;\n map.eachLayer((layer: any) => {\n if (layer instanceof L.Marker) {\n map.removeLayer(layer);\n }\n });\n props.markers.forEach((marker) => {\n const leafletMarker = L.marker([marker.lat, marker.lng]);\n if (marker.popup) {\n leafletMarker.bindPopup(marker.popup);\n leafletMarker.on(\"click\", (event: any) => {\n emit(\"markerClick\", marker, event);\n });\n }\n leafletMarker.addTo(map);\n });\n};\n\nconst addAMapMarkers = (amap: any) => {\n if (!amap || props.mapType !== \"amap\" || !props.markers) return;\n props.markers.forEach((marker) => {\n const amapMarker = new (window as any).AMap.Marker({\n position: [marker.lat, marker.lng],\n title: marker.popup || \"\",\n });\n if (marker.popup) {\n const infoWindow = new (window as any).AMap.InfoWindow({\n content: marker.popup,\n offset: new (window as any).AMap.Pixel(0, -30),\n });\n amapMarker.on(\"click\", () => {\n infoWindow.open(amap, amapMarker.getPosition());\n emit(\"markerClick\", marker, null);\n });\n }\n amapMarker.setMap(amap);\n });\n};\n\nwatch(\n () => props.markers,\n () => {\n if (map && props.mapType === \"osm\") addMarkers();\n },\n { deep: true },\n);\n\nwatch(\n () => props.mapType,\n async (newType, oldType) => {\n if (newType === oldType) return;\n if (map) {\n map.remove();\n map = null;\n loading.value = true;\n await nextTick();\n if (newType === \"amap\" && props.amapKey) await initAMap();\n else await initOSMMap();\n }\n },\n);\n\nwatch(\n [() => props.center, () => props.zoom],\n () => {\n if (map) map.setView(props.center, props.zoom);\n },\n { deep: true },\n);\n\nonMounted(async () => {\n await nextTick();\n if (props.mapType === \"amap\" && props.amapKey) await initAMap();\n else await initOSMMap();\n});\n\nonUnmounted(() => {\n if (map) {\n map.remove();\n map = null;\n }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@use \"./index.scss\";\n</style>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Markdown-
|
|
1
|
+
{"version":3,"file":"C_Markdown-DwmI0_0Q.css","names":[],"sources":["../src/components/C_Markdown/index.vue?vue&type=style&index=0&scoped=b64c492a&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled *//* C_Markdown 组件样式文件 */\n.c-markdown-wrapper[data-v-b64c492a] {\n --c-markdown-dark-icon-color: #ffffff;\n --c-markdown-dark-hover-bg: rgba(255, 255, 255, 0.08);\n --c-markdown-dark-active-bg: rgba(255, 255, 255, 0.12);\n --c-markdown-dark-scrollbar-track: rgba(255, 255, 255, 0.05);\n --c-markdown-dark-scrollbar-thumb: rgba(255, 255, 255, 0.2);\n --c-markdown-dark-scrollbar-hover: rgba(255, 255, 255, 0.3);\n position: relative;\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor {\n border-radius: 6px;\n border: 1px solid var(--c-border);\n background-color: var(--c-bg-card);\n /* 禁用状态样式 */\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor:hover {\n border-color: var(--c-primary);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor:focus-within {\n border-color: var(--c-primary);\n box-shadow: 0 0 0 2px var(--c-primary-opacity, rgba(24, 160, 88, 0.2));\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor.disabled {\n background-color: var(--c-bg-disabled, #fafafc);\n cursor: not-allowed;\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor.disabled .v-md-editor__editor {\n cursor: not-allowed;\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar {\n background-color: var(--c-bg-card);\n border-bottom: 1px solid var(--c-border);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__editor-wrapper {\n background-color: var(--c-bg-card);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__editor {\n background-color: var(--c-bg-card);\n color: var(--c-text-1);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__editor textarea {\n color: var(--c-text-1);\n caret-color: var(--c-text-1);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__preview-wrapper {\n background-color: var(--c-bg-card);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__preview {\n background-color: var(--c-bg-card);\n color: var(--c-text-1);\n}\n.c-markdown-wrapper[data-v-b64c492a] .v-md-editor .v-md-editor__toc-nav-wrapper {\n background-color: var(--c-bg-card);\n border-left: 1px solid var(--c-border);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-item {\n color: var(--c-markdown-dark-icon-color);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-item:hover {\n background-color: var(--c-markdown-dark-hover-bg);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-item.is-active {\n background-color: var(--c-markdown-dark-active-bg);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-item svg {\n fill: var(--c-markdown-dark-icon-color);\n opacity: 0.9;\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-item:hover svg {\n opacity: 1;\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-right .v-md-editor__toolbar-item {\n background-color: transparent;\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-right .v-md-editor__toolbar-item svg {\n fill: var(--c-markdown-dark-icon-color);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-right .v-md-editor__toolbar-item:hover {\n background-color: var(--c-markdown-dark-hover-bg);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__toolbar-divider {\n background-color: var(--c-border);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__editor::-webkit-scrollbar,\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__preview::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__editor::-webkit-scrollbar-track,\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__preview::-webkit-scrollbar-track {\n background-color: var(--c-markdown-dark-scrollbar-track);\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__editor::-webkit-scrollbar-thumb,\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__preview::-webkit-scrollbar-thumb {\n background-color: var(--c-markdown-dark-scrollbar-thumb);\n border-radius: 4px;\n}\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__editor::-webkit-scrollbar-thumb:hover,\n.c-markdown-wrapper.is-dark[data-v-b64c492a] .v-md-editor .v-md-editor__preview::-webkit-scrollbar-thumb:hover {\n background-color: var(--c-markdown-dark-scrollbar-hover);\n}\n.c-markdown-wrapper .word-count[data-v-b64c492a] {\n position: absolute;\n bottom: 8px;\n right: 12px;\n font-size: 12px;\n color: var(--c-text-4);\n background: var(--c-bg-card);\n padding: 2px 6px;\n border-radius: 4px;\n z-index: 10;\n}\n.c-markdown-wrapper .word-count.exceed[data-v-b64c492a] {\n color: var(--c-error);\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|