@robot-admin/naive-ui-components 0.6.20 → 0.6.22
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/CHANGELOG.md +48 -48
- package/README.md +277 -277
- package/dist/C_ActionBar-jZPsiXtT.css.map +1 -0
- package/dist/C_ActionBar-nnfbZCea.css.map +1 -0
- package/dist/C_ActionBar.css +47 -47
- package/dist/C_ActionBar.js.map +1 -1
- package/dist/C_ActionBar2.js +1 -1
- package/dist/C_ActionBar2.js.map +1 -1
- package/dist/{C_AntV-BY8jqqmi.css.map → C_AntV-A3NXx_sF.css.map} +1 -1
- package/dist/{C_AntV-BbkHc-_a.css.map → C_AntV-D2Zp2qQF.css.map} +1 -1
- package/dist/C_AntV.css +78 -78
- package/dist/C_AntV.js.map +1 -1
- package/dist/C_AntV2.js +2 -2
- package/dist/C_AntV2.js.map +1 -1
- package/dist/{C_AudioPlayer-DKRIGhi7.css.map → C_AudioPlayer-CXl4yP_m.css.map} +1 -1
- package/dist/{C_AudioPlayer-DlB6eNsa.css.map → C_AudioPlayer-CcBtt71m.css.map} +1 -1
- package/dist/C_AudioPlayer.css +47 -47
- package/dist/C_AudioPlayer.js.map +1 -1
- package/dist/C_AudioPlayer2.js +1 -1
- package/dist/C_AudioPlayer2.js.map +1 -1
- package/dist/{C_AvatarGroup-D02zci-i.css.map → C_AvatarGroup-CtPhfH9h.css.map} +1 -1
- package/dist/{C_AvatarGroup-DatHaUlY.css.map → C_AvatarGroup-M1dNPH3E.css.map} +1 -1
- package/dist/C_AvatarGroup.css +21 -21
- package/dist/C_AvatarGroup.js.map +1 -1
- package/dist/C_AvatarGroup2.js +1 -1
- package/dist/C_AvatarGroup2.js.map +1 -1
- package/dist/{C_Barcode-DYDRDe2O.css.map → C_Barcode-DjTmDkbQ.css.map} +1 -1
- package/dist/{C_Barcode-muJdc_O4.css.map → C_Barcode-Do1Ozsmh.css.map} +1 -1
- package/dist/C_Barcode.css +7 -7
- package/dist/C_Barcode.js.map +1 -1
- package/dist/C_Barcode2.js +1 -1
- package/dist/C_Barcode2.js.map +1 -1
- package/dist/C_Breadcrumb.js.map +1 -1
- package/dist/C_Breadcrumb2.js.map +1 -1
- package/dist/{C_Captcha-9_hk6VmY.css.map → C_Captcha-CZmAETly.css.map} +1 -1
- package/dist/{C_Captcha-MtASlJ0f.css.map → C_Captcha-Ccq3DMrR.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_Chat-CcjZdAwu.css.map → C_Chat-CcdWgVE8.css.map} +1 -1
- package/dist/{C_Chat-Diq7tyWU.css.map → C_Chat-Cx0woEEe.css.map} +1 -1
- package/dist/C_Chat.css +65 -65
- package/dist/C_Chat.js.map +1 -1
- package/dist/C_Chat2.js +1 -1
- package/dist/C_Chat2.js.map +1 -1
- package/dist/{C_City-Bam08Qv3.css.map → C_City-Cv5BESaN.css.map} +1 -1
- package/dist/{C_City-Bb3W-KaN.css.map → C_City-DNKv4MpH.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-DPZlNSxL.css.map +1 -0
- package/dist/C_Code-avZjAnHD.css.map +1 -0
- package/dist/C_Code.css +3 -3
- package/dist/C_Code.js.map +1 -1
- package/dist/C_Code2.js +1 -1
- package/dist/C_Code2.js.map +1 -1
- package/dist/{C_CollapsePanel-BCmTFuDz.css.map → C_CollapsePanel-CWkFCqNQ.css.map} +1 -1
- package/dist/{C_CollapsePanel-Bxnh4Zy-.css.map → C_CollapsePanel-pcVJA_4z.css.map} +1 -1
- package/dist/C_CollapsePanel.css +35 -35
- package/dist/C_CollapsePanel.js.map +1 -1
- package/dist/C_CollapsePanel2.js +1 -1
- package/dist/C_CollapsePanel2.js.map +1 -1
- package/dist/{C_ContextMenu-B3Cgz_wJ.css.map → C_ContextMenu-CCsCeI39.css.map} +1 -1
- package/dist/{C_ContextMenu-u1LixF3_.css.map → C_ContextMenu-DOTBeltf.css.map} +1 -1
- package/dist/C_ContextMenu.css +17 -17
- package/dist/C_ContextMenu.js.map +1 -1
- package/dist/C_ContextMenu2.js +1 -1
- package/dist/C_ContextMenu2.js.map +1 -1
- package/dist/{C_Cron-Civir5-I.css.map → C_Cron-C0-8b5af.css.map} +1 -1
- package/dist/{C_Cron-iw3qpMsy.css.map → C_Cron-DqHQ92LN.css.map} +1 -1
- package/dist/C_Cron.css +37 -37
- 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.map +1 -1
- package/dist/{C_Draggable-BCjIvIwT.css.map → C_Draggable-Bq6o0qXn.css.map} +1 -1
- package/dist/{C_Draggable-DwkSlCOS.css.map → C_Draggable-r6nhx3yM.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_FilePreview-BhcbiXsV.css.map → C_FilePreview-BVarVvnD.css.map} +1 -1
- package/dist/{C_FilePreview-DJudcIbW.css.map → C_FilePreview-DHGlV4OR.css.map} +1 -1
- package/dist/C_FilePreview.css +91 -91
- package/dist/C_FilePreview.js.map +1 -1
- package/dist/C_FilePreview2.js +1 -1
- package/dist/C_FilePreview2.js.map +1 -1
- package/dist/{C_Form-BtcAPIRi.css.map → C_Form-BciQUPpS.css.map} +1 -1
- package/dist/{C_Form-D_gwQ7hC.css.map → C_Form-CKHvXL3O.css.map} +1 -1
- package/dist/C_Form.css +222 -222
- package/dist/C_Form.js.map +1 -1
- package/dist/C_Form2.js +1 -1
- package/dist/C_Form2.js.map +1 -1
- package/dist/{C_FormSearch-Bi4XYjjk.css.map → C_FormSearch-Bmd2CmG6.css.map} +1 -1
- package/dist/{C_FormSearch-DUGOoX0u.css.map → C_FormSearch-DBNtnjaL.css.map} +1 -1
- package/dist/C_FormSearch.css +45 -45
- package/dist/C_FormSearch.js.map +1 -1
- package/dist/C_FormSearch2.js +1 -1
- package/dist/C_FormSearch2.js.map +1 -1
- package/dist/{C_FormulaEditor-CJk3SSUJ.css.map → C_FormulaEditor-3WG2vtQl.css.map} +1 -1
- package/dist/{C_FormulaEditor-Cy7mjfVO.css.map → C_FormulaEditor-Cm0CokN5.css.map} +1 -1
- package/dist/C_FormulaEditor.css +78 -78
- package/dist/C_FormulaEditor.js.map +1 -1
- package/dist/C_FormulaEditor2.js +1 -1
- package/dist/C_FormulaEditor2.js.map +1 -1
- package/dist/C_FullCalendar-1U4VlG7X.css.map +1 -0
- package/dist/C_FullCalendar-DLUdCtqf.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_Icon.js.map +1 -1
- package/dist/C_Icon2.js.map +1 -1
- package/dist/{C_ImageCropper-BqZpkZfU.css.map → C_ImageCropper-D3L2-J_J.css.map} +1 -1
- package/dist/{C_ImageCropper-CylwGuzS.css.map → C_ImageCropper-DrmUlaLi.css.map} +1 -1
- package/dist/C_ImageCropper.css +10 -10
- package/dist/C_ImageCropper.js.map +1 -1
- package/dist/C_ImageCropper2.js +1 -1
- package/dist/C_ImageCropper2.js.map +1 -1
- package/dist/C_Language.js.map +1 -1
- package/dist/C_Language2.js.map +1 -1
- package/dist/{C_Login-BkdnkBKX.css.map → C_Login-Cst_W1XC.css.map} +1 -1
- package/dist/{C_Login-VkMM6OJ2.css.map → C_Login-DWZ_6K1x.css.map} +1 -1
- package/dist/C_Login.css +72 -72
- package/dist/C_Login.js.map +1 -1
- package/dist/C_Login2.js +1 -1
- package/dist/C_Login2.js.map +1 -1
- package/dist/{C_Map-DNnBcY70.css.map → C_Map-BCJmDnJT.css.map} +1 -1
- package/dist/{C_Map-LEXIx6zM.css.map → C_Map-WUMXSAfy.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-Kujzx1qG.css.map → C_Markdown-7iBnLDtL.css.map} +1 -1
- package/dist/{C_Markdown-YCAmBbNF.css.map → C_Markdown-BbTvdFCl.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_QRCode-CFNTda_1.css.map → C_QRCode-DMxSyogU.css.map} +1 -1
- package/dist/{C_QRCode-CmWr9lK-.css.map → C_QRCode-G7fiAkm4.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 +1 -0
- package/dist/C_Signature-BJUHx2BF.css.map +1 -0
- package/dist/C_Signature.css +20 -14
- 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_SplitPane-BOwr1C7K.css.map → C_SplitPane-Br2eK8IG.css.map} +1 -1
- package/dist/{C_SplitPane-N-a_D-7F.css.map → C_SplitPane-DuyzUhKB.css.map} +1 -1
- package/dist/C_SplitPane.css +31 -31
- package/dist/C_SplitPane.js.map +1 -1
- package/dist/C_SplitPane2.js +1 -1
- package/dist/C_SplitPane2.js.map +1 -1
- package/dist/C_Steps-BHYNJI1l.css.map +1 -0
- package/dist/C_Steps-DY7vl-4I.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-BV1wUw6f.css.map → C_Table-vZlcgH6x.css.map} +1 -1
- package/dist/{C_Table-LlJ-JEdm.css.map → C_Table-wzAO43l0.css.map} +1 -1
- package/dist/C_Table.css +93 -93
- package/dist/C_Table.js.map +1 -1
- package/dist/C_Table2.js +1 -1
- package/dist/C_Table2.js.map +1 -1
- package/dist/{C_TagsView-DX3LecK1.css.map → C_TagsView-BsutyC3U.css.map} +1 -1
- package/dist/{C_TagsView-DqOTa8bL.css.map → C_TagsView-Crm7yWg9.css.map} +1 -1
- package/dist/C_TagsView.css +36 -36
- package/dist/C_TagsView.js.map +1 -1
- package/dist/C_TagsView2.js +1 -1
- package/dist/C_TagsView2.js.map +1 -1
- package/dist/C_Time-Bd_e1YDN.css.map +1 -0
- package/dist/C_Time-CKb_fwdm.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-BIvw3f41.css.map → C_Timeline-CGTFTMX0.css.map} +1 -1
- package/dist/{C_Timeline-VsDfCv4Q.css.map → C_Timeline-DxKyvKer.css.map} +1 -1
- package/dist/C_Timeline.css +42 -42
- package/dist/C_Timeline.js.map +1 -1
- package/dist/C_Timeline2.js +1 -1
- package/dist/C_Timeline2.js.map +1 -1
- package/dist/{C_Transfer-BOB6OBqS.css.map → C_Transfer-CCEqnC-v.css.map} +1 -1
- package/dist/{C_Transfer-Dpq7ezWC.css.map → C_Transfer-CStPm4NP.css.map} +1 -1
- package/dist/C_Transfer.css +31 -31
- package/dist/C_Transfer.js.map +1 -1
- package/dist/C_Transfer2.js +1 -1
- package/dist/C_Transfer2.js.map +1 -1
- package/dist/{C_Tree-D98nX9qT.css.map → C_Tree-B-Le4hjQ.css.map} +1 -1
- package/dist/{C_Tree-ulfm7b-P.css.map → C_Tree-DnGc_MPb.css.map} +1 -1
- package/dist/C_Tree.css +33 -33
- package/dist/C_Tree.js.map +1 -1
- package/dist/C_Tree2.js +1 -1
- package/dist/C_Tree2.js.map +1 -1
- package/dist/{C_VideoPlayer-4J96tKfK.css.map → C_VideoPlayer-Cdgv4Zq0.css.map} +1 -1
- package/dist/{C_VideoPlayer-D35seHkm.css.map → C_VideoPlayer-rm0MODUv.css.map} +1 -1
- package/dist/C_VideoPlayer.css +13 -13
- package/dist/C_VideoPlayer.js.map +1 -1
- package/dist/C_VideoPlayer2.js +1 -1
- package/dist/C_VideoPlayer2.js.map +1 -1
- package/dist/{C_VtableGantt-C2lxFmWg.css.map → C_VtableGantt-BpY-Rng3.css.map} +1 -1
- package/dist/{C_VtableGantt-Dwnkw_WL.css.map → C_VtableGantt-DB87-jdS.css.map} +1 -1
- package/dist/C_VtableGantt.css +15 -15
- package/dist/C_VtableGantt.js.map +1 -1
- package/dist/C_VtableGantt2.js +1 -1
- package/dist/C_VtableGantt2.js.map +1 -1
- package/dist/{C_WorkFlow-2hJfvvTK.css.map → C_WorkFlow-7F6op6qN.css.map} +1 -1
- package/dist/{C_WorkFlow-DHoNjl8-.css.map → C_WorkFlow-Cy3HWA6A.css.map} +1 -1
- package/dist/C_WorkFlow.css +108 -108
- 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 +4 -4
- package/dist/constants2.d.ts +4 -4
- package/dist/constants3.d.ts +2 -2
- package/dist/constants4.d.ts +6 -6
- 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 +4 -4
- package/dist/index12.vue.d.ts +2 -2
- package/dist/index13.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 +7 -7
- package/dist/index21.vue.d.ts +1 -1
- package/dist/index22.vue.d.ts +1 -1
- package/dist/index24.vue.d.ts +3 -3
- package/dist/index25.vue.d.ts +2 -2
- package/dist/index26.vue.d.ts +3 -3
- package/dist/index3.vue.d.ts +1 -1
- package/dist/index4.vue.d.ts +6 -6
- package/dist/index6.vue.d.ts +6 -6
- package/dist/index7.vue.d.ts +2 -2
- package/dist/index8.vue.d.ts +4 -4
- 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 +1519 -1513
- package/dist/useCalendarEvents.d.ts +1 -1
- package/dist/useCollapsePanel.d.ts +11 -9
- package/dist/useCropperCore.d.ts +4 -4
- package/dist/useDraggableLayout.d.ts +2 -2
- package/dist/useDynamicFormState.d.ts +4 -4
- package/dist/useEdgeInteraction.d.ts +1 -1
- package/dist/useFullscreen.d.ts +2 -2
- package/dist/useInfiniteScroll.d.ts +3 -3
- package/dist/useQRCode.d.ts +3 -3
- package/dist/useSearchState.d.ts +2 -2
- package/dist/useSignatureHistory.d.ts +1 -1
- package/dist/useSplitResize.d.ts +5 -5
- package/dist/useTimeSelection.d.ts +2 -2
- package/dist/useTreeOperations.d.ts +4 -4
- package/dist/useWorkflowValidation.d.ts +4 -4
- package/package.json +1 -1
- package/dist/C_ActionBar-DYG0Yf3A.css.map +0 -1
- package/dist/C_ActionBar-MAjRhqcI.css.map +0 -1
- package/dist/C_Code-BFi5xxnM.css.map +0 -1
- package/dist/C_Code-CZ7yfnri.css.map +0 -1
- package/dist/C_FullCalendar-BQA0OjGB.css.map +0 -1
- package/dist/C_FullCalendar-ChJUosAi.css.map +0 -1
- package/dist/C_Signature-PN4GOLRX.css.map +0 -1
- package/dist/C_Signature-fWQqGIZ-.css.map +0 -1
- package/dist/C_Steps-Bk1krgiR.css.map +0 -1
- package/dist/C_Steps-Dn2CMpy1.css.map +0 -1
- package/dist/C_Time-BzyQVvTS.css.map +0 -1
- package/dist/C_Time-DmpKLQHc.css.map +0 -1
package/dist/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":["/**\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","/* unplugin-vue-components disabled */<!--\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","/* unplugin-vue-components disabled */<!--\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","<!--\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
|
+
{"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":"suBA2EA,MAAa,EAA6C,CACxD,CAAE,IAAK,SAAU,MAAO,SAAU,KAAM,aAAc,CACtD,CAAE,IAAK,SAAU,MAAO,KAAM,KAAM,aAAc,CAClD,CAAE,IAAK,KAAM,MAAO,KAAM,KAAM,aAAc,CAC/C,CAGY,EAA4C,CACvD,cAAe,GACf,aAAc,GACd,YAAa,GACb,YAAa,GACb,SAAU,GACV,cAAe,GACf,WAAY,GACZ,eAAgB,GACjB,69BG8YC,IAAM,GAAK,EAAa,IACtB,OAAQ,WAAmB,IAAO,WAC7B,WAAmB,GAAG,EAAK,EAAU,cAAc,CACpD,EAGA,EAAQ,EAQR,EAAO,EAkBP,EAAc,OAAgB,CAClC,GAAG,EACH,SAAU,CAAE,GAAG,EAAkB,GAAG,EAAM,SAAU,CACpD,gBAAiB,EAAM,iBAAmB,EAC3C,EAAE,CAEG,EAAO,MACL,EAAY,MAAM,SACzB,CACK,EAAa,MAAe,EAAY,MAAM,gBAAiB,CAG/D,EAAY,EAChB,WACD,CAGK,EAAkB,EAAqB,KAAK,CAC5C,EAAe,EAA2B,CAC9C,SAAU,EAAM,iBAAmB,GACnC,SAAU,EAAM,iBAAmB,GACpC,CAAC,CAEI,GAA2B,CAC/B,SAAU,CACR,CACE,SAAU,GACV,QAAS,EAAE,mBAAoB,SAAS,CACxC,QAAS,OACV,CACF,CACD,SAAU,CACR,CACE,SAAU,GACV,QAAS,EAAE,mBAAoB,QAAQ,CACvC,QAAS,OACV,CACF,CACF,CAGK,EAAa,EAAI,GAAM,CAE7B,EAAM,EAAY,GAAO,CACnB,EACF,aAAa,QACX,EAAM,WACN,KAAK,UAAU,CAAE,SAAU,EAAa,SAAU,CAAC,CACpD,CAED,aAAa,WAAW,EAAM,WAAY,EAE5C,CAEF,MAAgB,CACd,GAAI,CACF,IAAM,EAAQ,aAAa,QAAQ,EAAM,WAAY,CACrD,GAAI,EAAO,CACT,IAAM,EAAO,KAAK,MAAM,EAAM,CAC1B,EAAK,WACP,EAAa,SAAW,EAAK,SAC7B,EAAW,MAAQ,UAGjB,IAGR,CAGF,IAAM,EAAuB,SAAY,CACvC,GAAI,CACF,MAAM,EAAgB,OAAO,UAAU,MACjC,CACN,OAEF,EAAK,SAAU,CACb,GAAG,EACH,GAAI,EAAa,MACb,CACE,aAAc,EAAa,MAC3B,iBAAkB,EAAiB,MACpC,CACD,EAAE,CACP,CAAC,EAIE,EAAiB,EAAqB,KAAK,CAC3C,EAAc,EAA0B,CAAE,QAAS,GAAI,KAAM,GAAI,CAAC,CAClE,EAAY,EAAI,EAAE,CACpB,EAAwD,KAEtD,GAA0B,CAC9B,QAAS,EAAY,OAAO,MAAM,CAClC,KAAM,CACJ,CACE,SAAU,GACV,QAAS,EAAE,eAAgB,SAAS,CACpC,QAAS,OACV,CACF,CACF,CAGK,EAAa,GAAqC,CAClD,EAAe,EAAI,GAAM,CACzB,EAAe,EAAI,GAAG,CACtB,EAAmB,EAAI,EAAE,CAEzB,GAAwB,GAA+C,CAC3E,EAAa,MAAQ,GACrB,EAAa,MAAQ,EAAK,MAC1B,EAAiB,MAAQ,EAAK,WAE1B,OAA0B,CAC9B,EAAa,MAAQ,IAEjB,GAAuB,GAAmB,CAC9C,EAAa,MAAQ,EAChB,IACH,EAAa,MAAQ,GACrB,EAAiB,MAAQ,IAIvB,OAAqB,CACzB,EAAa,MAAQ,GACrB,EAAa,MAAQ,GACrB,EAAiB,MAAQ,EACzB,EAAW,OAAO,OAAO,EAGrB,EAAsB,SAAY,CACtC,GAAI,CACF,MAAM,EAAe,OAAO,UAAU,MAChC,CACN,OAEE,EAAK,MAAM,eAAiB,CAAC,EAAa,OAC9C,EAAK,iBAAkB,CAAE,GAAG,EAAa,CAAC,EAItC,OAAuB,CACtB,EAAY,UACjB,EAAK,YAAa,EAAY,QAAQ,CACtC,AAEE,KADA,cAAc,EAAe,CACZ,MAEnB,EAAU,MAAQ,GAClB,EAAiB,gBAAkB,CACjC,EAAU,QACN,EAAU,OAAS,IACrB,cAAc,EAAgB,CAC9B,EAAiB,OAElB,IAAK,GAIJ,GAAY,EAAS,KAAK,CAC1B,EAAc,EAClB,0CAA0C,KAAK,KAAK,GACrD,CAEK,OAA4B,CAChC,EAAY,MAAQ,0CAA0C,KAAK,KAAK,GACxE,EAAK,iBAAiB,EAIlB,EAAkB,EAAqB,KAAK,CAC5C,EAAe,EAA2B,CAC9C,MAAO,GACP,KAAM,GACN,SAAU,GACV,gBAAiB,GAClB,CAAC,CACI,EAAe,EAAI,EAAE,CACvB,EAA2D,KAEzD,GAA2B,CAC/B,MAAO,EAAY,OAAO,MAAM,CAChC,KAAM,CACJ,CACE,SAAU,GACV,QAAS,EAAE,eAAgB,SAAS,CACpC,QAAS,OACV,CACF,CACD,SAAU,EAAY,SAAS,KAAK,CACpC,gBAAiB,CACf,CACE,SAAU,GACV,QAAS,EAAE,qBAAsB,UAAU,CAC3C,QAAS,OACV,CACD,CACE,WAAY,EAAY,IAClB,IAAU,EAAa,SAGpB,GAFM,MAAM,EAAE,iBAAkB,YAAY,CAAC,CAItD,QAAS,OACV,CACF,CACF,CAEK,OAA0B,CACzB,EAAa,QAClB,EAAK,qBAAsB,EAAa,MAAM,CAC9C,AAEE,KADA,cAAc,EAAkB,CACZ,MAEtB,EAAa,MAAQ,GACrB,EAAoB,gBAAkB,CACpC,EAAa,QACT,EAAa,OAAS,IACxB,cAAc,EAAmB,CACjC,EAAoB,OAErB,IAAK,GAGJ,EAAuB,SAAY,CACvC,GAAI,CACF,MAAM,EAAgB,OAAO,UAAU,MACjC,CACN,OAEF,EAAK,kBAAmB,CAAE,GAAG,EAAc,CAAC,SAI9C,MAAkB,CAChB,AAEE,KADA,cAAc,EAAe,CACZ,MAEnB,AAEE,KADA,cAAc,EAAkB,CACZ,OAEtB,CAGF,EAAa,CAAE,gBAAc,CAAC,+DA9vB9B,EAgdM,MAhdN,GAgdM,CA/cJ,EAAA,2BAA+B,CAC/B,EA2BM,MA3BN,GA2BM,CA1BJ,GAWO,EAAA,OAAA,OAAA,EAAA,KAAA,CATG,EAAA,MAAY,UAAA,GAAA,CADpB,EASM,MATN,GASM,CALJ,EAIE,EAAA,CAHC,KAAM,EAAA,MAAY,SAClB,KAAM,GACP,MAAM,4DAIZ,EAAuD,KAAvD,GAAuD,EAAzB,EAAA,MAAY,MAAK,CAAA,EAAA,CAEvC,EAAA,MAAY,UAAY,EAAA,QAAS,YAAA,GAAA,CADzC,EAKI,IALJ,GAKI,EADC,EAAA,MAAY,SAAQ,CAAA,EAAA,EAAA,EAAA,OAAA,GAAA,CAEzB,EAAA,aAAiB,CAET,EAAA,QAAS,YAAA,GAAA,CADjB,EAKI,IALJ,GAKI,EADC,EAAC,cAAA,OAAA,CAAA,CAAA,EAAA,EAAA,EAAA,OAAA,GAAA,GAIR,EAAA,6BAAiC,GACjC,EAoEQ,EAAA,SAlEF,kBAAJ,IAAI,EACH,MAAO,EACP,MAAO,GACR,MAAM,gCAmBM,CAjBZ,EAiBY,EAAA,CAhBV,KAAK,WACL,MAAM,qCAcG,CAZT,EAYS,EAAA,CAXC,MAAO,EAAa,oCAAb,EAAa,SAAQ,EACnC,YAAa,EAAC,iBAAA,SAAA,CACf,UAAA,KAEW,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,sBACJ,KAAM,GACP,MAAM,kEAMd,EAoBY,EAAA,CAnBV,KAAK,WACL,MAAM,qCAiBG,CAfT,EAeS,EAAA,CAdC,MAAO,EAAa,oCAAb,EAAa,SAAQ,EACpC,KAAK,WACL,mBAAiB,QAChB,YAAa,EAAC,iBAAA,QAAA,CACf,UAAA,GACC,QAAK,EAAQ,EAAoB,CAAA,QAAA,CAAA,GAEvB,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,mBACJ,KAAM,GACP,MAAM,kEAMd,EAAA,eAAmB,CAEX,EAAA,MAAK,YAAc,EAAA,MAAK,gBAAA,GAAA,CADhC,EAkBM,MAlBN,GAkBM,CAbI,EAAA,MAAK,YAAA,GAAA,CADb,EAMY,EAAA,OAJF,QAAS,EAAA,mCAAA,EAAU,MAAA,EAC3B,MAAM,oCAEuB,CAAA,EAAA,EAA1B,EAAC,cAAA,MAAA,CAAA,CAAA,EAAA,CAAA,CAAA,mCAGE,EAAA,MAAK,gBAAA,GAAA,CADb,EAMQ,EAAA,OAJN,MAAM,kBACL,QAAK,AAAA,EAAA,KAAA,GAAE,EAAI,kBAAA,iBAEiB,CAAA,EAAA,EAA1B,EAAC,YAAA,QAAA,CAAA,CAAA,EAAA,CAAA,CAAA,6DAhEA,EAAA,QAAS,WAAA,CAAA,CAAA,CAqEnB,EAAA,wBAA4B,GAC5B,EA4DQ,EAAA,SA1DF,iBAAJ,IAAI,EACH,MAAO,EACP,MAAO,GACR,MAAM,gCAoBM,CAlBZ,EAkBY,EAAA,CAjBV,KAAK,UACL,MAAM,qCAeG,CAbT,EAaS,EAAA,CAZC,MAAO,EAAY,mCAAZ,EAAY,QAAO,EACjC,YAAa,EAAC,cAAA,SAAA,CACf,UAAA,GACA,UAAU,OAEC,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,gBACJ,KAAM,GACP,MAAM,kEAMd,EAgCY,EAAA,CA/BV,KAAK,OACL,MAAM,qCA6BA,CA3BN,EA2BM,MA3BN,GA2BM,CA1BJ,EAcS,EAAA,CAbC,MAAO,EAAY,gCAAZ,EAAY,KAAI,EAC9B,YAAa,EAAC,aAAA,SAAA,CACf,UAAA,GACA,MAAM,sBACL,QAAK,EAAQ,EAAmB,CAAA,QAAA,CAAA,GAEtB,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,yBACJ,KAAM,GACP,MAAM,0DAIZ,EAUU,EAAA,CATP,SAAU,EAAA,MAAS,GAAA,CAAS,EAAY,QACzC,MAAM,yBACL,QAAO,mBAMN,CAAA,EAAA,EAHA,EAAA,MAAS,EAAA,GAA2B,EAAA,MAAS,IAAK,EAAC,YAAA,MAAA,GAA0C,EAAC,cAAA,QAAA,CAAA,CAAA,EAAA,CAAA,CAAA,wDApD9F,EAAA,QAAS,UAAA,CAAA,CAAA,CA6DnB,EAAA,+BAAmC,CAE3B,EAAA,QAAS,YAAmB,EAAA,QAAS,WAAA,GAAA,CAD7C,EAoBU,EAAA,OAlBR,KAAK,UACL,MAAA,GACC,QAAS,EAAM,QACf,SAAoB,EAAA,QAAS,YAAmB,EAAA,MAAK,eAAa,CAAK,EAAA,MAGxE,MAAM,sBACL,QAAK,AAAA,EAAA,KAAA,GAAY,EAAA,QAAS,WAA6B,GAAoB,CAAgB,GAAmB,iBAU7G,CAAA,EAAA,EAHA,EAAA,QAAS,YAAmB,EAAA,MAAK,eAAa,CAAK,EAAA,MAA0B,EAAC,kBAAA,WAAA,CAA6C,EAAC,eAAA,MAAA,CAAA,CAAA,EAAA,CAAA,CAAA,8CAMhI,EAAA,gCAAoC,CAE5B,EAAA,MAAK,eAAiB,EAAA,QAAS,YAAA,GAAA,CADvC,EAYM,MAZN,GAYM,CARJ,EAOE,GAAA,SANI,aAAJ,IAAI,EACJ,eAAa,GACb,MAAM,OACL,UAAS,GACT,OAAM,GACN,SAAQ,8BAIb,EAAA,6BAAiC,CAEzB,EAAA,QAAS,WAAA,GAAA,CADjB,EAOU,EAAA,OALR,MAAA,GACA,MAAM,oBACL,QAAK,AAAA,EAAA,KAAA,GAAE,EAAA,MAAS,2BAEO,CAAA,EAAA,EAArB,EAAC,UAAA,KAAA,CAAA,CAAA,EAAA,CAAA,CAAA,qBAGN,EAAA,uBAA2B,CAEnB,EAAA,QAAS,UAAA,GAAA,CADjB,EA0CM,MA1CN,GA0CM,CAtCJ,EAgBM,MAhBN,EAgBM,CAfJ,EASE,GAAA,SARI,YAAJ,IAAI,GACH,MAAO,EAAA,MACP,KAAM,IACN,OAAQ,EACR,cAAa,GACd,MAAM,UACN,WAAS,UACT,yBAAuB,uZAQ3B,EAMI,IANJ,GAMI,CALF,EAGE,EAAA,CAFA,KAAK,sBACJ,KAAM,OACP,IACF,EAAG,EAAC,aAAA,oBAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAEN,EAaU,EAAA,CAZR,KAAA,GACA,KAAK,OACL,MAAM,sBACL,QAAO,KAEG,KAAI,MAIX,CAHF,EAGE,EAAA,CAFA,KAAK,cACJ,KAAM,qBAGX,CAAA,EADW,IACX,EAAG,EAAC,gBAAA,cAAA,CAAA,CAAA,EAAA,CAAA,CAAA,uBAIR,EAAA,mCAAuC,CAE/B,EAAA,QAAS,UAAA,GAAA,CADjB,EAOU,EAAA,OALR,MAAA,GACA,MAAM,oBACL,QAAK,AAAA,EAAA,KAAA,GAAE,EAAA,MAAS,2BAEO,CAAA,EAAA,EAArB,EAAC,UAAA,KAAA,CAAA,CAAA,EAAA,CAAA,CAAA,qBAGN,EAAA,qBAAyB,GACzB,EAoGQ,EAAA,SAlGF,kBAAJ,IAAI,EACH,MAAO,EACP,MAAO,GACR,MAAM,gCAoBM,CAlBZ,EAkBY,EAAA,CAjBV,KAAK,QACL,MAAM,qCAeG,CAbT,EAaS,EAAA,CAZC,MAAO,EAAa,iCAAb,EAAa,MAAK,EAChC,YAAa,EAAC,cAAA,SAAA,CACf,UAAA,GACA,UAAU,OAEC,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,gBACJ,KAAM,GACP,MAAM,kEAMd,EA+BY,EAAA,CA9BV,KAAK,OACL,MAAM,qCA4BA,CA1BN,EA0BM,MA1BN,GA0BM,CAzBJ,EAaS,EAAA,CAZC,MAAO,EAAa,iCAAb,EAAa,KAAI,EAC/B,YAAa,EAAC,aAAA,SAAA,CACf,UAAA,GACA,MAAM,wBAEK,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,yBACJ,KAAM,GACP,MAAM,0DAIZ,EAUU,EAAA,CATP,SAAU,EAAA,MAAY,GAAA,CAAS,EAAa,MAC7C,MAAM,yBACL,QAAO,mBAMN,CAAA,EAAA,EAHA,EAAA,MAAY,EAAA,GAA2B,EAAA,MAAY,IAAK,EAAC,YAAA,MAAA,GAA0C,EAAC,cAAA,QAAA,CAAA,CAAA,EAAA,CAAA,CAAA,gCAQ5G,EAkBY,EAAA,CAjBV,KAAK,WACL,MAAM,qCAeG,CAbT,EAaS,EAAA,CAZC,MAAO,EAAa,qCAAb,EAAa,SAAQ,EACpC,KAAK,WACL,mBAAiB,QAChB,YAAa,EAAC,iBAAA,QAAA,GAEJ,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,mBACJ,KAAM,GACP,MAAM,kEAMd,EAmBY,EAAA,CAlBV,KAAK,kBACL,MAAM,qCAgBG,CAdT,EAcS,EAAA,CAbC,MAAO,EAAa,4CAAb,EAAa,gBAAe,EAC3C,KAAK,WACL,mBAAiB,QAChB,YAAa,EAAC,mBAAA,UAAA,CACd,QAAK,EAAQ,EAAoB,CAAA,QAAA,CAAA,GAEvB,OAAM,MAKb,CAJF,EAIE,EAAA,CAHA,KAAK,yBACJ,KAAM,GACP,MAAM,0FA9FN,EAAA,QAAS,WAAA,CAAA,CAAA,CAqGnB,EAAA,uBAA2B,CAEnB,EAAA,QAAS,YAAA,GAAA,CADjB,EASU,EAAA,OAPR,KAAK,UACL,MAAA,GACC,QAAS,EAAM,QAChB,MAAM,sBACL,QAAO,kBAEmB,CAAA,EAAA,EAAxB,EAAC,aAAA,KAAA,CAAA,CAAA,EAAA,CAAA,CAAA,mCAGN,EAAA,yBAA6B,CAErB,EAAA,QAAS,YAAA,GAAA,CADjB,EAOU,EAAA,OALR,MAAA,GACA,MAAM,oBACL,QAAK,AAAA,EAAA,MAAA,GAAE,EAAA,MAAS,2BAEO,CAAA,EAAA,EAArB,EAAC,UAAA,KAAA,CAAA,CAAA,EAAA,CAAA,CAAA,qBAGN,EAAA,oCAAwC,CAEhC,EAAA,QAAS,aAAoB,EAAA,MAAK,cAAgB,EAAA,MAAK,cAAA,GAAA,CAD/D,EAoBM,MApBN,GAoBM,CAfI,EAAA,MAAK,cAAA,GAAA,CADb,EAOU,EAAA,OALR,MAAM,oBACN,KAAK,QACJ,QAAK,AAAA,EAAA,MAAA,GAAE,EAAA,MAAS,0BAEmB,CAAA,EAAA,EAAjC,EAAC,mBAAA,QAAA,CAAA,CAAA,EAAA,CAAA,CAAA,qBAGE,EAAA,MAAK,aAAA,GAAA,CADb,EAOU,EAAA,OALR,MAAM,oBACN,KAAK,QACJ,QAAK,AAAA,EAAA,MAAA,GAAE,EAAA,MAAS,yBAEa,CAAA,EAAA,EAA3B,EAAC,cAAA,OAAA,CAAA,CAAA,EAAA,CAAA,CAAA,qCAIR,EAAA,oCAAwC,CAEhC,EAAA,QAAS,YAAmB,EAAA,MAAK,aAAe,EAAA,MAAW,QAAA,GAAA,CADnE,EA8BW,EAAA,CAAA,IAAA,EAAA,CAAA,CA3BT,EAEa,EAAA,CAFH,MAAM,mBAAkB,CAAA,eAEhC,CAAA,EAAA,EADA,EAAC,kBAAA,SAAA,CAAA,CAAA,EAAA,CAAA,CAAA,OAEH,EAuBM,MAvBN,GAuBM,EAAA,EAAA,GAAA,CAtBJ,EAqBW,EAAA,KAAA,GApBU,EAAA,MAAZ,QADT,EAqBW,EAAA,CAnBR,IAAK,EAAS,IACf,QAAQ,QACR,UAAU,QAEC,QAAO,MAYN,CAXV,EAWU,EAAA,CAVR,OAAA,GACA,MAAM,sBACL,QAAK,GAAE,EAAI,eAAiB,EAAS,IAAG,GAE9B,KAAI,MAIX,CAHF,EAGE,EAAA,CAFC,KAAM,EAAS,KACf,KAAM,8DAKf,CAAA,EADW,IACX,EAAG,EAAS,MAAK,CAAA,EAAA,CAAA,CAAA,yCAKvB,EAAA,kCAAsC,CAE9B,EAAA,QAAS,YAAmB,EAAA,MAAK,UAAA,GAAA,CADzC,EAaM,MAbN,GAaM,CATJ,EAEU,EAAA,CAFH,MAAM,yBAAwB,CAAA,eAEnC,CAAA,EAAA,EADA,EAAC,gBAAA,SAAA,CAAA,CAAA,EAAA,CAAA,CAAA,OAEH,EAKQ,EAAA,CAJN,MAAM,yBACL,QAAK,AAAA,EAAA,MAAA,GAAE,EAAA,MAAS,2BAEa,CAAA,EAAA,EAA3B,EAAC,cAAA,OAAA,CAAA,CAAA,EAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_Map-
|
|
1
|
+
{"version":3,"file":"C_Map-BCJmDnJT.css","names":[],"sources":["../src/components/C_Map/index.vue?vue&type=style&index=0&scoped=a3602506&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled */.c-map[data-v-a3602506] {\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-a3602506] {\n width: 100%;\n height: 100%;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-loading[data-v-a3602506] {\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-WUMXSAfy.css","names":[],"sources":["../src/components/C_Map/index.vue?vue&type=style&index=0&scoped=a3602506&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled */.c-map[data-v-a3602506] {\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-a3602506] {\n width: 100%;\n height: 100%;\n border-radius: 4px;\n overflow: hidden;\n}\n.c-map .map-loading[data-v-a3602506] {\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-a3602506] {
|
|
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-a3602506] {
|
|
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-a3602506] {
|
|
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-BCJmDnJT.css.map*/
|