feat: 优化页面相关功能,新增用户注册,忘记密码,系统字体,系统样式等

This commit is contained in:
liuzhi
2025-03-18 10:28:48 +08:00
committed by Charles7c
parent 933cd6063a
commit c8a3bb5e72
54 changed files with 3551 additions and 226 deletions

BIN
public/continew.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

9
public/continew.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="33" height="33" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.8 204">
<path fill="#307AF2" d="M86.7,0l88,51v.2l-16.3,9.4v-.2L86.7,18.9Zm71.8,143.5,16.3,9.4v.2L86.8,204h0l-16.3-9.4,16.3-9.4h0l71.7-41.5v-.2Z"/>
<path fill="#12D2AC" d="M16.3,143.5v.2L58,167.8l-16.3,9.4L0,153.1v-.2Z"/>
<path fill="#12D2AC" d="M104.1,93,15.9,143.8l-.2-.1V124.9l.2.1L87.7,83.6,104.1,93Z"/>
<path fill="#0057FE" d="M88.1,0,.1,51v.2l16.3,9.4v-.2L88.1,18.9Z"/>
<path fill="#307AF2" d="M.1,50.9.2,152.6l.2.1,16.3-9.4-.2-.1-.1-82.9L.1,50.9Z"/>
<path fill="#0057FE" d="M174.7,50.9l-.1,101.7-.2.1-16.3-9.4.2-.1.1-82.9Z"/>
<path fill="#12D2AC" d="M41.7,158.5l16.1,9.4,100.6-58.7V90.4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

BIN
public/sakura.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/sakura.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

1
public/sakura.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/sakura1.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/sakura1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

9
public/sakura1.svg Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,11 @@
<!--
* @Author: liuzhi 1306086303@qq.com
* @Date: 2025-03-12 11:00:21
* @LastEditors: liuzhi 1306086303@qq.com
* @LastEditTime: 2025-03-17 16:35:19
* @FilePath: \continew-admin-ui\src\App.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<a-config-provider update-at-scroll>
<template #loading>
@@ -16,13 +24,18 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useAppStore, useUserStore } from '@/stores'
defineOptions({ name: 'App' })
const userStore = useUserStore()
const appStore = useAppStore()
appStore.initTheme()
appStore.initSiteConfig()
onMounted(() => {
appStore.initTheme()
appStore.initSiteConfig()
appStore.initFontFamily() // 初始化字体配置
})
</script>
<style scoped lang="scss">

View File

@@ -62,6 +62,8 @@ export interface AccountSignupReq extends AuthReq {
username: string
nickname: string
password: string
captcha: string
uuid: string
gender: number
deptId: number
roleIds: string[]

View File

@@ -1,3 +1,11 @@
/*
* @Author: liuzhi 1306086303@qq.com
* @Date: 2025-03-12 11:00:22
* @LastEditors: liuzhi 1306086303@qq.com
* @LastEditTime: 2025-03-12 17:13:59
* @FilePath: \continew-admin-ui\src\apis\system\user-center.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type * as System from './type'
import http from '@/utils/http'
@@ -18,6 +26,11 @@ export function updateUserPassword(data: { oldPassword: string, newPassword: str
return http.patch(`${BASE_URL}/password`, data)
}
/** @desc 修改密码 */
export function updatePassword(data) {
return http.post(`${BASE_URL}/password`, data)
}
/** @desc 修改手机号 */
export function updateUserPhone(data: { phone: string, captcha: string, oldPassword: string }) {
return http.patch(`${BASE_URL}/phone`, data)

View File

@@ -20,6 +20,11 @@ export function getUser(id: string) {
return http.get<T.UserDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 注册用户 */
export function signup(data: any) {
return http.post(`${BASE_URL}/signup`, data)
}
/** @desc 新增用户 */
export function addUser(data: any) {
return http.post(`${BASE_URL}`, data)

BIN
src/assets/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -17,4 +17,11 @@
src: url('./DINPro-Regular.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'PingFang SC';
src: url('@/assets/fonts/PingFang/PingFang SC.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1611900658489" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8196" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><defs><style type="text/css"></style></defs><path d="M312.401987 313.238588V157.455059H156.618458l155.783529 155.783529z m632.863791 233.674458h77.894274v243.40915h-77.894274v-77.892601h-77.889255v-87.625621H789.483922v-77.892601h77.892601v77.892601h77.889255v-77.890928zM546.074771 1.673203v467.347241h477.085281V1.673203H546.074771z m399.191007 389.457987H623.967373V79.564131h321.298405v311.567059zM468.183843 1.673203H0.836601m866.541595 155.781856h-155.783529v155.783529h155.783529V157.455059z" fill="" p-id="8197"></path><path d="M0.836601 1.673203h467.347242v467.347241L389.856209 391.529412V78.640523l-312.801882 0.923608zM546.074771 3.346405h477.085281v467.347242L945.359477 393.202614V80.313725l-321.392104 0.923608zM546.074771 546.914719h165.523242v77.892601h-87.628967zM865.704993 714.457516L865.045752 624.80732h-75.407896l-0.152261-77.894274h77.892601v76.217725L943.686275 623.267974l1.579503-76.354928h77.894274v243.40915h-77.894274v-77.892601h-76.214379L868.392157 868.392157l78.551843 1.495843L945.359477 945.359477l77.803922 0.747922V1024L711.598013 712.432941" fill="" p-id="8198"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1611819980450" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3540" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><defs><style type="text/css"></style></defs><path d="M511.83616 0.1024C229.4272 0.1024 0.512 229.2736 0.512 511.9488c0 282.69568 228.90496 511.8464 511.31392 511.8464 282.37824 0 511.31392-229.15072 511.31392-511.8464C1023.15008 229.26336 794.2144 0.1024 511.83616 0.1024z m-92.7232 632.4224c-27.57632 0-49.7664-5.65248-77.37344-11.0592l-77.27104 38.71744 22.09792-66.52928c-55.32672-38.72768-88.4224-88.6272-88.4224-149.36064 0-105.28768 99.49184-188.13952 220.96896-188.13952 108.65664 0 203.8272 66.22208 222.95552 155.3408a191.0272 191.0272 0 0 0-21.31968-1.3312c-104.92928 0-187.8016 78.4384-187.8528 175.08352 0 16.128 2.53952 31.63136 6.84032 46.40768-6.79936 0.54272-13.68064 0.86016-20.62336 0.86016z m325.888 77.5168l16.61952 55.296-60.57984-33.30048c-22.1184 5.5296-44.29824 11.0592-66.32448 11.0592-105.13408 0-187.92448-71.8336-187.92448-160.50176 0-88.40192 82.7392-160.512 187.92448-160.512 99.30752 0 187.72992 72.05888 187.72992 160.512 0.02048 49.92-33.05472 94.08512-77.44512 127.44704z" p-id="3541"></path><path d="M501.94432 405.51424c16.70144 0 27.65824-11.07968 27.65824-27.648 0-16.62976-10.9568-27.61728-27.65824-27.61728-16.54784 0-33.11616 10.94656-33.11616 27.62752 0 16.5376 16.62976 27.648 33.11616 27.648z m-154.624-55.26528c-16.5888 0-33.29024 10.96704-33.29024 27.62752 0 16.55808 16.70144 27.648 33.30048 27.648 16.56832 0 27.56608-11.08992 27.56608-27.648 0-16.6912-10.99776-27.62752-27.56608-27.62752z m226.47808 160.4608c-10.93632 0-22.09792 11.07968-22.09792 22.09792 0 11.1616 11.14112 22.13888 22.09792 22.13888 16.77312 0 27.65824-10.94656 27.65824-22.13888-0.02048-11.01824-10.88512-22.09792-27.65824-22.09792z m121.55904 0c-10.93632 0-21.98528 11.07968-21.98528 22.09792 0 11.1616 11.10016 22.13888 21.98528 22.13888 16.61952 0 27.648-10.94656 27.648-22.13888 0-11.01824-11.02848-22.09792-27.648-22.09792z" p-id="3542"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -19,6 +19,7 @@
<i class="iconfont icon-refresh"></i>
</div>
<img
v-if="pointBackImgBase"
ref="canvas"
:src="`data:image/png;base64,${pointBackImgBase}`"
alt=""

View File

@@ -10,6 +10,7 @@
:style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
>
<img
v-if="backImgBase"
:src="`data:image/png;base64,${backImgBase}`"
alt=""
style="width: 100%; height: 100%; display: block"
@@ -74,6 +75,7 @@
}"
>
<img
v-if="blockBackImgBase"
:src="`data:image/png;base64,${blockBackImgBase}`"
alt=""
style="
@@ -345,7 +347,7 @@ export default {
function init() {
text.value = explain.value
// getPicture()
getPicture()
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight

View File

@@ -197,6 +197,11 @@ export default {
line-height: 30px;
color: #fff;
}
.verify-txt {
padding-left: 10px; /* 文本靠左 10px */
white-space: nowrap; /* 防止文本换行 */
line-height: 30px; /* 垂直居中对齐文本 */
}
.suc-bg {
background-color: rgba(92, 184, 92, 0.5);
filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7f5CB85C, endcolorstr=#7f5CB85C);

View File

@@ -0,0 +1,15 @@
/**
* Custom icon list
* All icons are loaded here for easy management
* @see https://vue.ant.design/components/icon/#Custom-Font-Icon
*
* 自定义图标加载表
* 所有图标均从这里加载,方便管理
*/
import qrCodeIcon from '@/assets/icons/qr-code.svg?inline'
const allIcon = {
qrCodeIcon
}
export default allIcon

View File

@@ -1,3 +1,11 @@
/*
* @Author: liuzhi 1306086303@qq.com
* @Date: 2025-03-12 11:00:23
* @LastEditors: liuzhi 1306086303@qq.com
* @LastEditTime: 2025-03-17 15:55:40
* @FilePath: \continew-admin-ui\src\config\setting.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export const defaultSettings: App.AppSettings = {
theme: 'light',
themeColor: '#165DFF',
@@ -12,6 +20,7 @@ export const defaultSettings: App.AppSettings = {
layout: 'left',
enableColorWeaknessMode: false,
enableMourningMode: false,
fontFamily: 'PingFang SC',
}
// 根据环境返回配置
export const getSettings = (): App.AppSettings => {

View File

@@ -41,13 +41,20 @@
<a-divider v-if="settingOpen" orientation="center">界面显示</a-divider>
<a-descriptions v-if="settingOpen" :column="1" :align="{ value: 'right' }" :value-style="{ paddingRight: 0 }">
<a-descriptions-item label="系统字体">
<a-select
v-model="appStore.fontFamily" placeholder="请选择" :options="fontFamilyList" :disabled="!appStore.tab"
:trigger-props="{ autoFitPopupMinWidth: true }" :style="{ width: '150px' }"
>
</a-select>
</a-descriptions-item>
<a-descriptions-item label="页签显示">
<a-switch v-model="appStore.tab" />
</a-descriptions-item>
<a-descriptions-item label="页签风格">
<a-select
v-model="appStore.tabMode" placeholder="请选择" :options="tabModeList" :disabled="!appStore.tab"
:trigger-props="{ autoFitPopupMinWidth: true }" :style="{ width: '120px' }"
:trigger-props="{ autoFitPopupMinWidth: true }" :style="{ width: '150px' }"
>
</a-select>
</a-descriptions-item>
@@ -57,7 +64,7 @@
<a-descriptions-item label="动画显示">
<a-select
v-model="appStore.animateMode" placeholder="请选择" :options="animateModeList"
:disabled="!appStore.animate" :style="{ width: '120px' }"
:disabled="!appStore.animate" :style="{ width: '150px' }"
>
</a-select>
</a-descriptions-item>
@@ -116,7 +123,13 @@ const tabModeList: App.TabItem[] = [
{ label: '间隔卡片', value: 'card-gutter' },
{ label: '圆角', value: 'rounded' },
]
const fontFamilyList: App.FontFamilyItem[] = [
{ label: '微软雅黑', value: 'Microsoft YaHei' },
{ label: '苹方黑体', value: 'PingFang SC' },
{ label: 'DINPro-Bold', value: 'DINPro-Bold' },
{ label: 'DINPro-Medium', value: 'DINPro-Medium' },
{ label: 'DINPro-Regular', value: 'DINPro-Regular' },
]
const animateModeList: App.AnimateItem[] = [
{ label: '默认', value: 'zoom-fade' },
{ label: '滑动', value: 'fade-slide' },
@@ -167,6 +180,7 @@ const copySettings = () => {
theme: 'light',
themeColor: appStore.themeColor,
tab: appStore.tab,
fontFamily: appStore.fontFamily,
tabMode: appStore.tabMode,
animate: appStore.animate,
animateMode: appStore.animateMode,

View File

@@ -3,13 +3,19 @@
</template>
<script setup lang="ts">
interface Props {
mode: 'left' | 'top' | 'mix'
}
defineOptions({ name: 'LayoutItem' })
withDefaults(defineProps<Props>(), {})
const emit = defineEmits(['click'])
interface Props {
mode: 'left' | 'top' | 'mix'
}
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">

View File

@@ -1,6 +1,7 @@
<template>
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
<img v-if="logo" class="logo" :src="logo" alt="logo" />
<!-- <img v-if="logo" class="logo" :src="logo" alt="logo" /> -->
<img v-if="logo" :class="title === 'SakurA Platform' ? 'logo1' : 'logo'" :src="logo" alt="logo" />
<img v-else class="logo" src="/logo.svg" alt="logo" />
<span class="system-name gi_line_1">{{ title }}</span>
</section>
@@ -59,6 +60,16 @@ const toHome = () => {
flex-shrink: 0;
}
.logo1 {
width: 50px;
height: 50px;
border-radius: 6px;
transition: all 0.2s;
overflow: hidden;
flex-shrink: 0;
margin: 0 -8px 0 -8px;
}
.system-name {
padding-left: 6px;
white-space: nowrap;

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { computed, reactive, toRefs } from 'vue'
import { computed, reactive, toRefs, watch } from 'vue'
import { generate, getRgbStr } from '@arco-design/color'
import { type BasicConfig, listSiteOptionDict } from '@/apis'
import { getSettings } from '@/config/setting'
@@ -20,6 +20,18 @@ const storeSetup = () => {
return obj
})
// 设置字体
const setFontFamily = (font: string) => {
if (!font) return
document.documentElement.style.setProperty('--current-font-family', font)
}
// 初始化字体
const initFontFamily = () => {
if (!settingConfig.fontFamily) return
setFontFamily(settingConfig.fontFamily)
}
// 设置主题色
const setThemeColor = (color: string) => {
if (!color) return
@@ -103,6 +115,17 @@ const storeSetup = () => {
immediate: true,
})
// 监听字体变化
watch(
() => settingConfig.fontFamily,
(newFont) => {
if (newFont) {
setFontFamily(newFont)
}
},
{ immediate: true },
)
const getFavicon = () => {
return siteConfig.SITE_FAVICON
}
@@ -138,6 +161,8 @@ const storeSetup = () => {
getTitle,
getCopyright,
getForRecord,
setFontFamily,
initFontFamily,
}
}

View File

@@ -0,0 +1,37 @@
// src/stores/modules/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isRegister: false,
isEmailLogin: false,
isForgotPassword: false,
activeKey: '1',
}),
actions: {
toggleMode() {
if (this.isEmailLogin) {
this.isEmailLogin = !this.isEmailLogin
} else {
this.isForgotPassword = !this.isForgotPassword
}
},
toggleEmailLoginMode() {
this.isEmailLogin = !this.isEmailLogin
},
toggleRegisterMode() {
const keyMap = {
1: '3',
2: '4',
3: '1',
4: '2',
}
this.isRegister = !this.isRegister
this.isEmailLogin = false
this.activeKey = keyMap[this.activeKey]
},
onTabChange(key: string) {
this.activeKey = key
},
},
})

View File

@@ -1,11 +1,22 @@
/*
* @Author: liuzhi 1306086303@qq.com
* @Date: 2025-03-12 11:00:23
* @LastEditors: liuzhi 1306086303@qq.com
* @LastEditTime: 2025-03-12 17:26:01
* @FilePath: \continew-admin-ui\src\stores\modules\user.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { defineStore } from 'pinia'
import { computed, reactive, ref } from 'vue'
import { useAuthStore } from './auth'
import { resetRouter } from '@/router'
import {
type AccountLoginReq,
type AccountSignupReq,
AuthTypeConstants,
type EmailLoginReq,
type PhoneLoginReq,
type PhoneSignupReq,
type UserInfo,
accountLogin as accountLoginApi,
emailLogin as emailLoginApi,
@@ -16,6 +27,7 @@ import {
} from '@/apis'
import { clearToken, getToken, setToken } from '@/utils/auth'
import { resetHasRouteFlag } from '@/router/guard'
import { signup as accountSignupApi } from '@/apis/system'
const storeSetup = () => {
const userInfo = reactive<UserInfo>({
@@ -49,7 +61,12 @@ const storeSetup = () => {
resetHasRouteFlag()
}
// 登录
// 账号注册
const accountSignup = async (req: AccountSignupReq) => {
await accountSignupApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.ACCOUNT })
}
// 账号登录
const accountLogin = async (req: AccountLoginReq) => {
const res = await accountLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.ACCOUNT })
setToken(res.data.token)
@@ -63,6 +80,11 @@ const storeSetup = () => {
token.value = res.data.token
}
// 手机号注册
const phoneSignup = async (req: PhoneSignupReq) => {
await accountSignupApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE })
}
// 手机号登录
const phoneLogin = async (req: PhoneLoginReq) => {
const res = await phoneLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE })
@@ -84,6 +106,11 @@ const storeSetup = () => {
pwdExpiredShow.value = true
resetToken()
resetRouter()
// useRouter().push('/login')
useAuthStore().activeKey = '3'
useAuthStore().isRegister = true
useAuthStore().isEmailLogin = true
useAuthStore().toggleRegisterMode()
}
// 退出登录
@@ -117,8 +144,10 @@ const storeSetup = () => {
roles,
permissions,
pwdExpiredShow,
accountSignup,
accountLogin,
emailLogin,
phoneSignup,
phoneLogin,
socialLogin,
logout,

View File

@@ -1,4 +1,5 @@
@use './var.scss' as *;
@use "../assets/fonts/font.css";
body {
--margin: 14px; // 通用外边距
@@ -16,6 +17,9 @@ body,
margin: 0;
padding: 0;
height: 100%;
font-family: var(--current-font-family), -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
@@ -26,8 +30,6 @@ body {
overflow: hidden;
color: var(--color-text-2);
background-color: #f5f7fd; // body背景颜色
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
// font-family: Avenir, Helvetica, Arial, sans-serif;
// font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, \5fae\8f6f\96c5\9ed1, Arial,
@@ -57,3 +59,7 @@ a:hover {
color: inherit;
text-decoration: none;
}
:root {
--current-font-family: 'PingFang SC';
}

8
src/types/app.d.ts vendored
View File

@@ -15,6 +15,7 @@ declare namespace App {
watermark?: string
enableColorWeaknessMode?: boolean
enableMourningMode?: boolean
fontFamily?: 'Microsoft YaHei' | 'PingFang SC' | 'DINPro-Bold' | 'DINPro-Medium' | 'DINPro-Regular' | string
}
/** 导航页签的样式类型 */
@@ -30,6 +31,13 @@ declare namespace App {
value: AnimateType
}
/** 字体类型 */
type FontFamilyType = 'Microsoft YaHei' | 'PingFang SC' | 'DINPro-Bold' | 'DINPro-Medium' | 'DINPro-Regular' | string
interface FontFamilyItem {
label: string
value: FontFamilyType
}
/** 字典项 */
interface DictItem {
disabled?: boolean

View File

@@ -356,3 +356,10 @@ export function parseCron(cron: string) {
return '表达式错误'
}
}
/** @desc 获取问候语 */
export function timeFix() {
const time = new Date()
const hour = time.getHours()
return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好'
}

View File

@@ -0,0 +1,207 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="username" hide-label>
<a-input v-model="form.username" placeholder="请输入用户名" allow-clear />
</a-form-item>
<a-form-item field="password" hide-label>
<a-input-password v-model="form.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item v-if="isCaptchaEnabled" field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<div class="captcha-container" @click="getCaptcha">
<img :src="captchaImgBase64" alt="验证码" class="captcha" />
<div v-if="form.expired" class="overlay">
<p>已过期请刷新</p>
</div>
</div>
</a-form-item>
<a-form-item>
<a-row justify="space-between" align="center" class="w-full">
<a-checkbox v-model="loginConfig.rememberMe">记住我</a-checkbox>
<a-link>忘记密码</a-link>
</a-row>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useStorage } from '@vueuse/core'
import { getImageCaptcha } from '@/apis/common'
import { useTabsStore, useUserStore } from '@/stores'
import { encryptByRsa } from '@/utils/encrypt'
const loginConfig = useStorage('login-config', {
rememberMe: true,
username: 'admin', // 演示默认值
password: 'admin123', // 演示默认值
// username: debug ? 'admin' : '', // 演示默认值
// password: debug ? 'admin123' : '', // 演示默认值
})
// 是否启用验证码
const isCaptchaEnabled = ref(true)
// 验证码图片
const captchaImgBase64 = ref()
const formRef = ref<FormInstance>()
const form = reactive({
username: loginConfig.value.username,
password: loginConfig.value.password,
captcha: '',
uuid: '',
expired: false,
})
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
captcha: [{ required: isCaptchaEnabled.value, message: '请输入验证码' }],
}
// 验证码过期定时器
let timer
const startTimer = (expireTime: number, curTime = Date.now()) => {
if (timer) {
clearTimeout(timer)
}
const remainingTime = expireTime - curTime
if (remainingTime <= 0) {
form.expired = true
return
}
timer = setTimeout(() => {
form.expired = true
}, remainingTime)
}
// 组件销毁时清理定时器
onBeforeUnmount(() => {
if (timer) {
clearTimeout(timer)
}
})
// 获取验证码
const getCaptcha = () => {
getImageCaptcha().then((res) => {
const { uuid, img, expireTime, isEnabled } = res.data
isCaptchaEnabled.value = isEnabled
captchaImgBase64.value = img
form.uuid = uuid
form.expired = false
startTimer(expireTime, Number(res.timestamp))
})
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
await userStore.accountLogin({
username: form.username,
password: encryptByRsa(form.password) || '',
captcha: form.captcha,
uuid: form.uuid,
})
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
const { rememberMe } = loginConfig.value
loginConfig.value.username = rememberMe ? form.username : ''
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
} catch (error) {
console.error(error)
getCaptcha()
form.captcha = ''
} finally {
loading.value = false
}
}
onMounted(() => {
getCaptcha()
})
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.captcha {
width: 111px;
height: 36px;
margin: 0 0 0 5px;
}
.btn {
height: 40px;
}
.captcha-container {
position: relative;
display: inline-block;
cursor: pointer;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(51, 51, 51, 0.8);
display: flex;
justify-content: center;
align-items: center;
}
.overlay p {
font-size: 12px;
color: white;
}
</style>

View File

@@ -0,0 +1,215 @@
<template>
<a-form ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin">
<a-form-item field="username" hide-label>
<a-input v-model="form.username" placeholder="请设置用户名5-20个字符" allow-clear />
</a-form-item>
<a-form-item field="password" hide-label>
<a-input-password v-model="form.password" placeholder="请设置登录密码" />
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<div class="captcha-container" @click="getCaptcha">
<img :src="captchaImgBase64" alt="验证码" class="captcha" />
<div v-if="form.expired" class="overlay">
<p>已过期请刷新</p>
</div>
</div>
</a-form-item>
<a-form-item>
<a-row justify="space-between" align="center" class="w-full">
<a-checkbox v-model="loginConfig.rememberMe">记住我</a-checkbox>
<!-- <a-link>忘记密码</a-link> -->
</a-row>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>开始体验</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useStorage } from '@vueuse/core'
import { getImageCaptcha } from '@/apis/common'
import { useTabsStore, useUserStore } from '@/stores'
import { encryptByRsa } from '@/utils/encrypt'
import { timeFix } from '@/utils'
const loginConfig = useStorage('login-config', {
rememberMe: true,
username: '',
password: ''
})
const formRef = ref<FormInstance>()
const form = reactive({
username: '',
nickname: '',
password: '',
gender: 0,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1,
captcha: '',
uuid: '',
expired: false
})
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请设置用户名5-20个字符' }],
password: [{ required: true, message: '请设置登录密码' }],
captcha: [{ required: true, message: '请输入验证码' }]
}
// 验证码过期定时器
let timer
const startTimer = (expireTime: number) => {
if (timer) {
clearTimeout(timer)
}
const remainingTime = expireTime - Date.now()
if (remainingTime <= 0) {
form.expired = true
return
}
timer = setTimeout(() => {
form.expired = true
}, remainingTime)
}
// 组件销毁时清理定时器
onBeforeUnmount(() => {
if (timer) {
clearTimeout(timer)
}
})
const captchaImgBase64 = ref()
// 获取验证码
const getCaptcha = () => {
getImageCaptcha().then((res) => {
const { uuid, img, expireTime } = res.data
form.uuid = uuid
captchaImgBase64.value = img
form.expired = false
startTimer(expireTime)
})
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 注册并登录
const handleLogin = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
await userStore.accountSignup({
username: form.username,
nickname: form.username,
password: encryptByRsa(form.password) || '',
gender: 1,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1
})
await userStore.accountLogin({
username: form.username,
password: encryptByRsa(form.password) || '',
captcha: form.captcha,
uuid: form.uuid
})
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
router.push({
path: (redirect as string) || '/',
query: {
...othersQuery
}
})
const { rememberMe } = loginConfig.value
loginConfig.value.username = rememberMe ? form.username : ''
Message.success(`注册成功,${form.username} ${timeFix()},欢迎使用`)
} catch (error) {
getCaptcha()
form.captcha = ''
Message.error(String(error))
} finally {
loading.value = false
}
}
onMounted(() => {
getCaptcha()
})
</script>
<style lang="scss" scoped>
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.captcha {
width: 111px;
height: 36px;
margin: 0 0 0 5px;
}
.btn {
height: 40px;
// margin-top: 20px;
}
.captcha-container {
position: relative;
display: inline-block;
cursor: pointer;
}
.captcha-container {
position: relative;
display: inline-block;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(51, 51, 51, 0.8);
display: flex;
justify-content: center;
align-items: center;
}
.overlay p {
font-size: 12px;
color: white;
}
</style>

View File

@@ -9,13 +9,28 @@
@submit="handleLogin"
>
<a-form-item field="username" hide-label>
<a-input v-model="form.username" placeholder="请输入用户名" allow-clear />
<!-- <a-input v-model="form.username" placeholder="请输入用户名" allow-clear /> -->
<a-input ref="inputRef" v-model="form.username" :placeholder="getPlaceholder()" allow-clear>
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item>
<a-form-item field="password" hide-label>
<a-input-password v-model="form.password" placeholder="请输入密码" />
<!-- <a-input-password v-model="form.password" placeholder="请输入密码" /> -->
<a-input-password v-model="form.password" :placeholder=" !isRegister ? '请输入登录密码' : '请设置登录密码'">
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item>
<a-form-item v-if="isCaptchaEnabled" field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<!-- <a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" /> -->
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1">
<template #prefix>
<icon-safe />
</template>
</a-input>
<div class="captcha-container" @click="getCaptcha">
<img :src="captchaImgBase64" alt="验证码" class="captcha" />
<div v-if="form.expired" class="overlay">
@@ -26,12 +41,14 @@
<a-form-item>
<a-row justify="space-between" align="center" class="w-full">
<a-checkbox v-model="loginConfig.rememberMe">记住我</a-checkbox>
<a-link>忘记密码</a-link>
<!-- <a-link>忘记密码</a-link> -->
<a-link v-if="!isRegister" @click="authStore.toggleMode">忘记密码</a-link>
</a-row>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
<!-- <a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button> -->
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>{{ !isRegister ? '立 即 登 录' : '开 始 体 验' }}</a-button>
</a-space>
</a-form-item>
</a-form>
@@ -43,13 +60,25 @@ import { useStorage } from '@vueuse/core'
import { getImageCaptcha } from '@/apis/common'
import { useTabsStore, useUserStore } from '@/stores'
import { encryptByRsa } from '@/utils/encrypt'
import { timeFix } from '@/utils'
import { useAuthStore } from '@/stores/modules/auth'
// 定义组件的 props
const props = defineProps({
isRegister: {
type: Boolean,
},
})
const inputRef = ref<HTMLInputElement | null>(null)
const loginConfig = useStorage('login-config', {
rememberMe: true,
username: 'admin', // 演示默认值
password: 'admin123', // 演示默认值
// username: debug ? 'admin' : '', // 演示默认值
// password: debug ? 'admin123' : '', // 演示默认值
// rememberMe: true,
// username: debug ? 'admin' : '',
// password: debug ? 'admin123' : ''
rememberMe: props.isRegister,
username: '',
password: '',
})
// 是否启用验证码
const isCaptchaEnabled = ref(true)
@@ -58,15 +87,22 @@ const captchaImgBase64 = ref()
const formRef = ref<FormInstance>()
const form = reactive({
username: loginConfig.value.username,
password: loginConfig.value.password,
// username: loginConfig.value.username,
// password: loginConfig.value.password,
username: !props.isRegister ? loginConfig.value.username : '',
nickname: '',
password: !props.isRegister ? loginConfig.value.password : '',
gender: 0,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1,
captcha: '',
uuid: '',
expired: false,
})
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
username: [{ required: true, message: '请设置用户名4-64个字符' }],
password: [{ required: true, message: '请设置登录密码' }],
captcha: [{ required: isCaptchaEnabled.value, message: '请输入验证码' }],
}
@@ -100,6 +136,7 @@ const getCaptcha = () => {
captchaImgBase64.value = img
form.uuid = uuid
form.expired = false
form.captcha = ''
startTimer(expireTime, Number(res.timestamp))
})
}
@@ -108,12 +145,33 @@ const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const getPlaceholder = () => {
return !props.isRegister ? '请输入用户名' : '请设置用户名4-64个字符'
}
const authStore = useAuthStore()
// const toggleForgotPasswordMode = inject<() => void>('toggleForgotPasswordMode')
// 登录或注册
const handleLogin = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
if (props.isRegister) {
await userStore.accountSignup({
username: form.username,
nickname: form.username,
password: encryptByRsa(form.password) || '',
captcha: form.captcha,
uuid: form.uuid,
gender: 0,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1,
})
}
await userStore.accountLogin({
username: form.username,
password: encryptByRsa(form.password) || '',
@@ -122,19 +180,19 @@ const handleLogin = async () => {
})
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
const { rememberMe } = loginConfig.value
loginConfig.value.username = rememberMe ? form.username : ''
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
const { rememberMe } = loginConfig.value
loginConfig.value.username = rememberMe ? form.username : ''
loginConfig.value.password = rememberMe ? form.password : ''
Message.success(`${props.isRegister ? '注册' : '登录'}成功,${form.username} ${timeFix()},欢迎使用`)
} catch (error) {
console.error(error)
// Message.error(String(error))
getCaptcha()
form.captcha = ''
} finally {
loading.value = false
}
@@ -142,9 +200,14 @@ const handleLogin = async () => {
onMounted(() => {
getCaptcha()
inputRef.value?.focus()
})
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
@@ -172,6 +235,11 @@ onMounted(() => {
border-color: rgb(var(--arcoblue-6));
}
.arco-checkbox-checked :deep(.arco-checkbox-icon-check) {
transform: scale(1.2);
transition: transform 0.3s cubic-bezier(0.3, 1.3, 0.3, 1);
}
.captcha {
width: 111px;
height: 36px;
@@ -180,6 +248,7 @@ onMounted(() => {
.btn {
height: 40px;
// margin-top: 20px;
}
.captcha-container {

View File

@@ -0,0 +1,79 @@
<template>
<div class="login-bg">
<div class="fly bg-fly-circle1"></div>
<div class="fly bg-fly-circle2"></div>
<div class="fly bg-fly-circle3"></div>
<div class="fly bg-fly-circle4"></div>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.login-bg {
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
z-index: 1;
}
.fly {
pointer-events: none;
position: fixed;
z-index: 9999;
}
.bg-fly-circle1 {
left: 40px;
top: 100px;
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(to right, rgba(var(--primary-6), 0.07) 0%, rgba(var(--primary-6), 0.04) 100%);
animation: move 2.5s linear infinite;
}
.bg-fly-circle2 {
left: 15%;
bottom: 5%;
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(to right, rgba(var(--primary-6), 0.08) 0%, rgba(var(--primary-6), 0.04) 100%);
animation: move 3s linear infinite;
}
.bg-fly-circle3 {
right: 12%;
top: 90px;
width: 145px;
height: 145px;
border-radius: 50%;
background: linear-gradient(to right, rgba(var(--primary-6), 0.1) 0%, rgba(var(--primary-6), 0.04) 100%);
animation: move 2.5s linear infinite;
}
.bg-fly-circle4 {
right: 5%;
top: 60%;
width: 160px;
height: 160px;
border-radius: 50%;
background: linear-gradient(to right, rgba(var(--primary-6), 0.02) 0%, rgba(var(--primary-6), 0.04) 100%);
animation: move 3.5s linear infinite;
}
@keyframes move {
0% {
transform: translateY(0px) scale(1);
}
50% {
transform: translateY(25px) scale(1.1);
}
100% {
transform: translateY(0px) scale(1);
}
}
</style>

View File

@@ -1,16 +1,46 @@
<!--
* @Author: liuzhi 1306086303@qq.com
* @Date: 2025-03-12 11:00:24
* @LastEditors: liuzhi 1306086303@qq.com
* @LastEditTime: 2025-03-14 11:34:27
* @FilePath: \continew-admin-ui\src\views\login\components\background\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="login-bg">
<div class="fly bg-fly-circle1"></div>
<div :class="appStore.theme === 'light' ? 'login-bg' : 'login-bg1'">
<!-- <div class="fly bg-fly-circle1"></div>
<div class="fly bg-fly-circle2"></div>
<div class="fly bg-fly-circle3"></div>
<div class="fly bg-fly-circle4"></div>
<div class="fly bg-fly-circle4"></div> -->
</div>
</template>
<script setup lang="ts"></script>
<script lang="ts" setup>
import { useAppStore } from '@/stores'
<style scoped lang="scss">
defineOptions({ name: 'Background' })
const appStore = useAppStore()
</script>
<script lang="ts">
export default {}
</script>
<style lang="scss" scoped>
.login-bg {
width: 100%;
min-height: 100%;
background: #e2effc url(@/assets/background.jpg);
background-size: 100%;
vertical-align: middle;
display: flex;
position: fixed;
overflow: hidden;
z-index: 1;
}
.login-bg1 {
width: 100%;
height: 100%;
position: fixed;
@@ -23,6 +53,7 @@
position: fixed;
z-index: 9999;
}
.bg-fly-circle1 {
left: 40px;
top: 100px;

View File

@@ -0,0 +1,176 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="email" hide-label>
<a-input v-model="form.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn"
:loading="captchaLoading"
:disabled="captchaDisable"
size="large"
@click="onCaptcha"
>
{{ captchaBtnName }}
</a-button>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
<Verify
ref="VerifyRef"
:captcha-type="captchaType"
:mode="captchaMode"
:img-size="{ width: '330px', height: '155px' }"
@success="getCaptcha"
/>
</a-form>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import type { BehaviorCaptchaReq } from '@/apis'
// import { type BehaviorCaptchaReq, getEmailCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
const formRef = ref<FormInstance>()
const form = reactive({
email: '',
captcha: '',
})
const rules: FormInstance['rules'] = {
email: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
],
captcha: [{ required: true, message: '请输入验证码' }],
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
await userStore.emailLogin(form)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
} catch (error) {
form.captcha = ''
} finally {
loading.value = false
}
}
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')
const captchaMode = ref('pop')
const captchaLoading = ref(false)
// 弹出行为验证码
const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
VerifyRef.value.show()
}
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
// 获取验证码
// eslint-disable-next-line unused-imports/no-unused-vars
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getEmailCaptcha(form.email, captchaReq)
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('邮件发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.captcha-btn {
height: 40px;
margin-left: 12px;
min-width: 98px;
border-radius: 4px;
}
.btn {
height: 40px;
}
</style>

View File

@@ -9,14 +9,24 @@
@submit="handleLogin"
>
<a-form-item field="email" hide-label>
<a-input v-model="form.email" placeholder="请输入邮箱" allow-clear />
<!-- <a-input v-model="form.email" placeholder="请输入邮箱" allow-clear /> -->
<a-input ref="inputRefd" v-model="form.email" placeholder="请输入邮箱" allow-clear @input="validatePhone">
<template #prefix>
<icon-email />
</template>
</a-input>
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<!-- <a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" /> -->
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1">
<template #prefix>
<icon-safe />
</template>
</a-input>
<a-button
class="captcha-btn"
:loading="captchaLoading"
:disabled="captchaDisable"
:disabled="!captchaDisable"
size="large"
@click="onCaptcha"
>
@@ -25,7 +35,8 @@
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
<!-- <a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button> -->
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long> </a-button>
</a-space>
</a-form-item>
<Verify
@@ -40,11 +51,13 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import type { BehaviorCaptchaReq } from '@/apis'
import { type BehaviorCaptchaReq, getEmailCaptcha } from '@/apis'
// import { type BehaviorCaptchaReq, getEmailCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
import { timeFix } from '@/utils'
const inputRefd = ref<HTMLInputElement | null>(null)
const formRef = ref<FormInstance>()
const form = reactive({
email: '',
@@ -59,6 +72,67 @@ const rules: FormInstance['rules'] = {
captcha: [{ required: true, message: '请输入验证码' }],
}
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')
const captchaMode = ref('pop')
const captchaLoading = ref(false)
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
const validatePhone = () => {
const email = form.email
const isValid = Regexp.Email.test(email)
captchaDisable.value = isValid
}
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = true
}
// 弹出行为验证码
const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
VerifyRef.value.show()
}
// 获取验证码
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getEmailCaptcha(form.email, captchaReq)
// const captchaReq: BehaviorCaptchaReq = { /* 根据需要填充属性 */ }
await getEmailCaptcha(form.email, captchaReq)
captchaLoading.value = false
captchaDisable.value = false
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
Message.success('邮件发送成功,请前往邮箱查看验证码')
// Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
@@ -72,70 +146,26 @@ const handleLogin = async () => {
await userStore.emailLogin(form)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
Message.success(`登录成功,${form.email} ${timeFix()}欢迎使用`)
} catch (error) {
form.captcha = ''
} finally {
loading.value = false
}
}
onMounted(() => {
inputRefd.value?.focus()
})
</script>
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')
const captchaMode = ref('pop')
const captchaLoading = ref(false)
// 弹出行为验证码
const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
VerifyRef.value.show()
}
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
// 获取验证码
// eslint-disable-next-line unused-imports/no-unused-vars
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getEmailCaptcha(form.email, captchaReq)
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('邮件发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">

View File

@@ -90,6 +90,10 @@ const onModify = async () => {
}
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {

View File

@@ -0,0 +1,196 @@
<template>
<a-form
ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin"
>
<a-form-item field="username" hide-label>
<a-input ref="inputRefd" v-model="form.username" placeholder="请输入用户名(账号,手机号,邮箱)" allow-clear @input="validatePhone">
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item>
<a-form-item field="password" hide-label>
<a-input-password v-model="form.password" placeholder="请输入新密码" :max-length="20" allow-clear>
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item>
<a-form-item field="email" hide-label>
<a-input v-model="form.email" placeholder="请输入邮箱" allow-clear @input="validatePhone">
<template #prefix>
<icon-email />
</template>
</a-input>
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1">
<template #prefix>
<icon-safe />
</template>
</a-input>
<a-button
class="captcha-btn" :loading="captchaLoading" :disabled="!captchaDisable" size="large"
@click="onCaptcha({})"
>
{{ captchaBtnName }}
</a-button>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long> </a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { type BehaviorCaptchaReq, getEmailCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
import { updatePassword } from '@/apis/system'
import { encryptByRsa } from '@/utils/encrypt'
import { useAuthStore } from '@/stores/modules/auth'
const authStore = useAuthStore()
const inputRefd = ref<HTMLInputElement | null>(null)
const formRef = ref<FormInstance>()
const form = reactive({
username: '',
password: '',
email: '',
captcha: '',
})
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请设置新密码' }],
email: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
],
captcha: [{ required: true, message: '请输入验证码' }],
}
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
const validatePhone = () => {
const email = form.email
const isValid = Regexp.Email.test(email)
captchaDisable.value = isValid
}
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = true
}
const captchaLoading = ref(false)
// 获取验证码
const onCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// const captchaReq: BehaviorCaptchaReq = { /* 根据需要填充属性 */ }
await getEmailCaptcha(form.email, captchaReq)
captchaLoading.value = false
captchaDisable.value = false
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
Message.success('邮件发送成功,请前往邮箱查看验证码')
// Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
const loading = ref(false)
// 登录
const handleLogin = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
await updatePassword({
username: form.username,
newPassword: encryptByRsa(form.password) || '',
email: form.email,
captcha: form.captcha,
}).then((re) => {
if (re.success) {
Message.success('修改成功,请使用新密码重新登录')
authStore.toggleMode()
}
})
} catch (error) {
// form.captcha = ''
} finally {
loading.value = false
}
}
onMounted(() => {
inputRefd.value?.focus()
})
</script>
<script lang="ts">
export default {}
</script>
<style lang="scss" scoped>
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.captcha-btn {
height: 40px;
margin-left: 12px;
min-width: 98px;
border-radius: 4px;
}
.btn {
height: 40px;
}
</style>

View File

@@ -0,0 +1,762 @@
export const countryNameMap = [
{
code: '+86',
name: '中国'
},
{
code: '+852',
name: '中国香港'
},
{
code: '+853',
name: '中国澳门'
},
{
code: '+886',
name: '中国台湾'
},
{
code: '+1',
name: '美国/加拿大'
},
{
code: '+7',
name: '俄罗斯/哈萨克斯坦'
},
{
code: '+81',
name: '日本'
},
{
code: '+1473',
name: '格林纳达'
},
{
code: '+41',
name: '瑞士'
},
{
code: '+232',
name: '塞拉利昂'
},
{
code: '+36',
name: '匈牙利'
},
{
code: '+1246',
name: '巴巴多斯'
},
{
code: '+216',
name: '突尼斯'
},
{
code: '+39',
name: '意大利'
},
{
code: '+229',
name: '贝宁'
},
{
code: '+62',
name: '印度尼西亚'
},
{
code: '+1869',
name: '圣基茨和尼维斯'
},
{
code: '+856',
name: '老挝'
},
{
code: '+256',
name: '乌干达'
},
{
code: '+376',
name: '安道尔'
},
{
code: '+257',
name: '布隆迪'
},
{
code: '+27',
name: '南非'
},
{
code: '+33',
name: '法国'
},
{
code: '+218',
name: '利比亚'
},
{
code: '+52',
name: '墨西哥'
},
{
code: '+241',
name: '加蓬'
},
{
code: '+389',
name: '北马其顿'
},
{
code: '+967',
name: '也门'
},
{
code: '+677',
name: '所罗门群岛'
},
{
code: '+998',
name: '乌兹别克斯坦'
},
{
code: '+20',
name: '埃及'
},
{
code: '+221',
name: '塞内加尔'
},
{
code: '+94',
name: '斯里兰卡'
},
{
code: '+970',
name: '巴勒斯坦'
},
{
code: '+880',
name: '孟加拉国'
},
{
code: '+51',
name: '秘鲁'
},
{
code: '+65',
name: '新加坡'
},
{
code: '+90',
name: '土耳其'
},
{
code: '+93',
name: '阿富汗'
},
{
code: '+44',
name: '英国'
},
{
code: '+260',
name: '赞比亚'
},
{
code: '+358',
name: '芬兰'
},
{
code: '+227',
name: '尼日尔'
},
{
code: '+245',
name: '几内亚比绍'
},
{
code: '+994',
name: '阿塞拜疆'
},
{
code: '+253',
name: '吉布提'
},
{
code: '+850',
name: '朝鲜'
},
{
code: '+230',
name: '毛里求斯'
},
{
code: '+57',
name: '哥伦比亚'
},
{
code: '+30',
name: '希腊'
},
{
code: '+385',
name: '克罗地亚'
},
{
code: '+212',
name: '摩洛哥'
},
{
code: '+213',
name: '阿尔及利亚'
},
{
code: '+31',
name: '荷兰'
},
{
code: '+249',
name: '苏丹'
},
{
code: '+679',
name: '斐济'
},
{
code: '+423',
name: '列支敦士登'
},
{
code: '+977',
name: '尼泊尔'
},
{
code: '+995',
name: '格鲁吉亚'
},
{
code: '+92',
name: '巴基斯坦'
},
{
code: '+377',
name: '摩纳哥'
},
{
code: '+267',
name: '博茨瓦纳'
},
{
code: '+961',
name: '黎巴嫩'
},
{
code: '+675',
name: '巴布亚新几内亚'
},
{
code: '+1809',
name: '多米尼加共和国'
},
{
code: '+974',
name: '卡塔尔'
},
{
code: '+261',
name: '马达加斯加'
},
{
code: '+91',
name: '印度'
},
{
code: '+963',
name: '叙利亚'
},
{
code: '+382',
name: '黑山'
},
{
code: '+595',
name: '巴拉圭'
},
{
code: '+503',
name: '萨尔瓦多'
},
{
code: '+380',
name: '乌克兰'
},
{
code: '+264',
name: '纳米比亚'
},
{
code: '+971',
name: '阿拉伯联合酋长国'
},
{
code: '+359',
name: '保加利亚'
},
{
code: '+49',
name: '德国'
},
{
code: '+855',
name: '柬埔寨'
},
{
code: '+964',
name: '伊拉克'
},
{
code: '+46',
name: '瑞典'
},
{
code: '+53',
name: '古巴'
},
{
code: '+996',
name: '吉尔吉斯斯坦'
},
{
code: '+60',
name: '马来西亚'
},
{
code: '+357',
name: '塞浦路斯'
},
{
code: '+265',
name: '马拉维'
},
{
code: '+966',
name: '沙特阿拉伯'
},
{
code: '+387',
name: '波斯尼亚和黑塞哥维那'
},
{
code: '+251',
name: '埃塞俄比亚'
},
{
code: '+34',
name: '西班牙'
},
{
code: '+386',
name: '斯洛文尼亚'
},
{
code: '+968',
name: '阿曼'
},
{
code: '+378',
name: '圣马力诺'
},
{
code: '+266',
name: '莱索托'
},
{
code: '+692',
name: '马绍尔群岛'
},
{
code: '+354',
name: '冰岛'
},
{
code: '+352',
name: '卢森堡'
},
{
code: '+54',
name: '阿根廷'
},
{
code: '+674',
name: '瑙鲁'
},
{
code: '+1767',
name: '多米尼克'
},
{
code: '+506',
name: '哥斯达黎加'
},
{
code: '+61',
name: '澳大利亚'
},
{
code: '+66',
name: '泰国'
},
{
code: '+509',
name: '海地'
},
{
code: '+688',
name: '图瓦卢'
},
{
code: '+504',
name: '洪都拉斯'
},
{
code: '+240',
name: '赤道几内亚'
},
{
code: '+1758',
name: '圣卢西亚'
},
{
code: '+375',
name: '白俄罗斯'
},
{
code: '+371',
name: '拉脱维亚'
},
{
code: '+680',
name: '帕劳'
},
{
code: '+63',
name: '菲律宾'
},
{
code: '+45',
name: '丹麦'
},
{
code: '+237',
name: '喀麦隆'
},
{
code: '+224',
name: '几内亚'
},
{
code: '+973',
name: '巴林'
},
{
code: '+597',
name: '苏里南'
},
{
code: '+252',
name: '索马里'
},
{
code: '+678',
name: '瓦努阿图'
},
{
code: '+228',
name: '多哥'
},
{
code: '+254',
name: '肯尼亚'
},
{
code: '+250',
name: '卢旺达'
},
{
code: '+372',
name: '爱沙尼亚'
},
{
code: '+40',
name: '罗马尼亚'
},
{
code: '+1868',
name: '特立尼达和多巴哥'
},
{
code: '+592',
name: '圭亚那'
},
{
code: '+670',
name: '东帝汶'
},
{
code: '+84',
name: '越南'
},
{
code: '+598',
name: '乌拉圭'
},
{
code: '+3906698',
name: '梵蒂冈'
},
{
code: '+43',
name: '奥地利'
},
{
code: '+1268',
name: '安提瓜和巴布达'
},
{
code: '+993',
name: '土库曼斯坦'
},
{
code: '+258',
name: '莫桑比克'
},
{
code: '+507',
name: '巴拿马'
},
{
code: '+691',
name: '密克罗尼西亚'
},
{
code: '+353',
name: '爱尔兰'
},
{
code: '+47',
name: '挪威'
},
{
code: '+236',
name: '中非共和国'
},
{
code: '+226',
name: '布基纳法索'
},
{
code: '+291',
name: '厄立特里亚'
},
{
code: '+255',
name: '坦桑尼亚'
},
{
code: '+82',
name: '韩国'
},
{
code: '+962',
name: '约旦'
},
{
code: '+222',
name: '毛里塔尼亚'
},
{
code: '+370',
name: '立陶宛'
},
{
code: '+421',
name: '斯洛伐克'
},
{
code: '+244',
name: '安哥拉'
},
{
code: '+76',
name: '哈萨克斯坦'
},
{
code: '+373',
name: '摩尔多瓦'
},
{
code: '+223',
name: '马里'
},
{
code: '+374',
name: '亚美尼亚'
},
{
code: '+685',
name: '萨摩亚'
},
{
code: '+591',
name: '玻利维亚'
},
{
code: '+56',
name: '智利'
},
{
code: '+1784',
name: '圣文森特和格林纳丁斯'
},
{
code: '+248',
name: '塞舌尔'
},
{
code: '+502',
name: '危地马拉'
},
{
code: '+593',
name: '厄瓜多尔'
},
{
code: '+992',
name: '塔吉克斯坦'
},
{
code: '+356',
name: '马耳他'
},
{
code: '+220',
name: '冈比亚'
},
{
code: '+234',
name: '尼日利亚'
},
{
code: '+1242',
name: '巴哈马'
},
{
code: '+965',
name: '科威特'
},
{
code: '+960',
name: '马尔代夫'
},
{
code: '+211',
name: '南苏丹'
},
{
code: '+98',
name: '伊朗'
},
{
code: '+355',
name: '阿尔巴尼亚'
},
{
code: '+55',
name: '巴西'
},
{
code: '+381',
name: '塞尔维亚'
},
{
code: '+501',
name: '伯利兹'
},
{
code: '+95',
name: '缅甸'
},
{
code: '+975',
name: '不丹'
},
{
code: '+58',
name: '委内瑞拉'
},
{
code: '+231',
name: '利比里亚'
},
{
code: '+1876',
name: '牙买加'
},
{
code: '+48',
name: '波兰'
},
{
code: '+673',
name: '文莱'
},
{
code: '+269',
name: '科摩罗'
},
{
code: '+676',
name: '汤加'
},
{
code: '+686',
name: '基里巴斯'
},
{
code: '+233',
name: '加纳'
},
{
code: '+235',
name: '乍得'
},
{
code: '+263',
name: '津巴布韦'
},
{
code: '+976',
name: '蒙古'
},
{
code: '+351',
name: '葡萄牙'
},
{
code: '+32',
name: '比利时'
},
{
code: '+972',
name: '以色列'
},
{
code: '+64',
name: '新西兰'
},
{
code: '+505',
name: '尼加拉瓜'
}
]

View File

@@ -0,0 +1,177 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="phone" hide-label>
<a-input v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear />
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn"
:loading="captchaLoading"
:disabled="captchaDisable"
size="large"
@click="onCaptcha"
>
{{ captchaBtnName }}
</a-button>
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
<Verify
ref="VerifyRef"
:captcha-type="captchaType"
:mode="captchaMode"
:img-size="{ width: '330px', height: '155px' }"
@success="getCaptcha"
/>
</a-form>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import type { BehaviorCaptchaReq } from '@/apis'
// import { type BehaviorCaptchaReq, getSmsCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
const formRef = ref<FormInstance>()
const form = reactive({
phone: '',
captcha: '',
})
const rules: FormInstance['rules'] = {
phone: [
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' },
],
captcha: [{ required: true, message: '请输入验证码' }],
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
loading.value = true
await userStore.phoneLogin(form)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
} catch (error) {
form.captcha = ''
} finally {
loading.value = false
}
}
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')
const captchaMode = ref('pop')
const captchaLoading = ref(false)
// 弹出行为验证码
const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('phone')
if (isInvalid) return
// 重置行为参数
VerifyRef.value.instance.refresh()
VerifyRef.value.show()
}
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
// 获取验证码
// eslint-disable-next-line unused-imports/no-unused-vars
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getSmsCaptcha(form.phone, captchaReq)
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('短信发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.captcha-btn {
height: 40px;
margin-left: 12px;
min-width: 98px;
border-radius: 4px;
}
.btn {
height: 40px;
}
</style>

View File

@@ -9,14 +9,42 @@
@submit="handleLogin"
>
<a-form-item field="phone" hide-label>
<a-input v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear />
<!-- <a-input v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear>
<template #prefix>
<icon-phone />
</template>
</a-input> -->
<a-space direction="vertical" size="mini">
<a-input-group>
<a-select
v-model="selectedCountry"
placeholder="请选择"
default-value="+86"
:trigger-props="{ autoFitPopupMinWidth: true }"
allow-search
@change="handleChange"
>
<a-option v-for="country in countries" :key="country.code" :value="country.code" :label="country.code">
{{ country.code }} {{ country.name }}
</a-option>
</a-select>
<a-input
ref="inputRefd" v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear
:style="{ width: '100%', borderRadius: '0px 4px 4px 0px' }" @input="validatePhone"
/>
</a-input-group>
</a-space>
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1">
<template #prefix>
<icon-safe />
</template>
</a-input>
<a-button
class="captcha-btn"
:loading="captchaLoading"
:disabled="captchaDisable"
:disabled="!captchaDisable"
size="large"
@click="onCaptcha"
>
@@ -25,7 +53,7 @@
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>{{ !isRegister ? '立 即 登 录' : '开 始 体 验' }}</a-button>
</a-space>
</a-form-item>
<Verify
@@ -40,14 +68,35 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import axios from 'axios'
import { countryNameMap } from './code'
import type { BehaviorCaptchaReq } from '@/apis'
// import { type BehaviorCaptchaReq, getSmsCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
import { getSmsCaptcha } from '@/apis'
import { timeFix } from '@/utils'
import { encryptByRsa } from '@/utils/encrypt'
import { getPhoneCountryCode } from '@/apis/auth'
// 定义组件的 props
const props = defineProps({
isRegister: {
type: Boolean,
},
})
const formRef = ref<FormInstance>()
const form = reactive({
phone: '',
username: '',
nickname: '',
password: 'qq111111',
gender: 0,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1,
uuid: '',
captcha: '',
})
@@ -59,30 +108,34 @@ const rules: FormInstance['rules'] = {
captcha: [{ required: true, message: '请输入验证码' }],
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
// 定义国家和地区数据
interface Country {
code: string
name: string
}
const countries = ref<Country[]>([])
const selectedCountry = ref<Country[]>() // 默认值
const inputRefd = ref<HTMLInputElement | null>(null)
// 处理选择变化
const handleChange = (value) => {
selectedCountry.value = value
}
// 获取国家和地区数据
// eslint-disable-next-line unused-imports/no-unused-vars
const fetchCountries = async () => {
try {
loading.value = true
await userStore.phoneLogin(form)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
// const response = await getPhoneCountryCode()
const response = await axios.get('https://restcountries.com/v3.1/all')
const data = response.data.map((country: any) => ({
code: country.idd.root + (country.idd.suffixes ? country.idd.suffixes[0] : ''),
name: countryNameMap[country.name.common],
})).filter((country: { code: string, name: string | undefined }) => country.name !== undefined)
countries.value = data
console.warn(countries.value)
} catch (error) {
form.captcha = ''
} finally {
loading.value = false
console.error('获取国家数据失败:', error)
}
}
@@ -104,26 +157,34 @@ const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
const validatePhone = () => {
const phone = form.phone
const isValid = Regexp.Phone.test(phone)
captchaDisable.value = isValid
}
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
captchaDisable.value = true
}
// 获取验证码
// eslint-disable-next-line unused-imports/no-unused-vars
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getSmsCaptcha(form.phone, captchaReq)
captchaLoading.value = false
captchaDisable.value = true
captchaDisable.value = false
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('短信发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
Message.success('短信发送成功演示默认【111111】')
form.captcha = '111111'
// Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
@@ -133,13 +194,97 @@ const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
}, 1000)
} catch (error) {
resetCaptcha()
// Message.error(String(error))
} finally {
captchaLoading.value = false
}
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录或注册
const handleLogin = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
loading.value = true
if (props.isRegister) {
await userStore.phoneSignup({
phone: form.phone,
captcha: form.captcha,
username: form.phone,
nickname: form.phone,
password: encryptByRsa(form.password) || '',
gender: 0,
deptId: 1,
roleIds: ['547888897925840928'],
status: 1,
})
}
await userStore.phoneLogin({
phone: form.phone,
captcha: form.captcha,
})
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
if (props.isRegister) {
Message.success(`注册成功,${form.phone} ${timeFix()},欢迎使用`)
Message.info(`初始密码【qq111111】可前往个人中心修改密码`)
} else {
Message.success(`登录成功,${form.phone} ${timeFix()},欢迎使用`)
}
} catch (error) {
form.captcha = ''
} finally {
loading.value = false
}
}
// watch(form, (newForm, oldForm) => {
// // console.log('新的表单值:', newForm)
// // console.log('旧的表单值:', oldForm)
// // 这里可以添加更多的处理逻辑
// }, { deep: true })
// 在组件挂载时获取数据
onMounted(() => {
// fetchCountries()
countries.value = countryNameMap
inputRefd.value?.focus()
})
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">
:deep(.arco-select-view-single) {
width: 90px !important;
border-radius: 4px 0px 0px 4px !important;
font-size: 13px !important;
}
:deep(.arco-select-view-value) {
font-size: 13px !important;
}
:deep(.arco-select-view-input) {
font-size: 13px !important;
}
:deep(.arco-select-option-content) {
font-size: 13px !important;
}
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
@@ -173,5 +318,6 @@ const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
.btn {
height: 40px;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,518 @@
<template>
<div v-if="isDesktop" class="login pc">
<h3 class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</h3>
<a-row align="stretch" class="login-box">
<a-col :xs="0" :sm="12" :md="13">
<div class="login-left">
<img class="login-left__img" src="@/assets/images/banner.png" alt="banner" />
</div>
</a-col>
<a-col :xs="24" :sm="12" :md="11">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
</a-tab-pane>
</a-tabs>
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
</div>
</a-col>
</a-row>
<div v-if="isDesktop" class="footer">
<div class="beian">
<div class="below text">{{ appStore.getCopyright() }}{{ appStore.getForRecord() ? ` · ${appStore.getForRecord()}` : '' }}</div>
</div>
</div>
<ToggleDark class="theme-btn" />
<Background />
</div>
<div v-else class="login h5">
<div class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<a-row align="stretch" class="login-box">
<a-col :xs="24" :sm="12" :md="11">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Background from './components/background/index.vue'
import AccountLogin from './components/account/index.vue'
import PhoneLogin from './components/phone/index.vue'
import EmailLogin from './components/email/index.vue'
import { socialAuth } from '@/apis/auth'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'
defineOptions({ name: 'Login' })
const { isDesktop } = useDevice()
const appStore = useAppStore()
const title = computed(() => appStore.getTitle())
const logo = computed(() => appStore.getLogo())
const isEmailLogin = ref(false)
const activeTab = ref('1')
// 切换登录模式
const toggleLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value
}
// 第三方登录授权
const onOauth = async (source: string) => {
const { data } = await socialAuth(source)
window.location.href = data.authorizeUrl
}
</script>
<style scoped lang="scss">
@media screen and (max-width: 570px) {
.pc {
display: none !important;
background-color: white !important;
}
.login {
height: 100%;
display: flex;
flex-direction: column;
justify-content: start;
align-items: center;
background-color: var(--color-bg-5);
color: #121314;
&-logo {
width: 100%;
height: 104px;
font-weight: 700;
font-size: 20px;
line-height: 32px;
display: flex;
padding: 0 20px;
align-items: center;
justify-content: start;
background-image: url('/src/assets/images/login_h5.jpg');
background-size: 100% 100%;
box-sizing: border-box;
img {
width: 34px;
height: 34px;
margin-right: 8px;
}
}
&-box {
width: 100%;
display: flex;
z-index: 999;
}
}
.login-right {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 30px 30px 0;
box-sizing: border-box;
&__title {
color: var(--color-text-1);
font-weight: 500;
font-size: 20px;
line-height: 32px;
margin-bottom: 20px;
}
&__form {
:deep(.arco-tabs-nav-tab) {
display: flex;
justify-content: start;
align-items: center;
}
:deep(.arco-tabs-tab) {
color: var(--color-text-2);
margin: 0 20px 0 0;
}
:deep(.arco-tabs-tab-title) {
font-size: 16px;
font-weight: 500;
line-height: 22px;
}
:deep(.arco-tabs-content) {
margin-top: 10px;
}
:deep(.arco-tabs-tab-active),
:deep(.arco-tabs-tab-title:hover) {
color: rgb(var(--arcoblue-6));
}
:deep(.arco-tabs-nav::before) {
display: none;
}
:deep(.arco-tabs-tab-title:before) {
display: none;
}
}
&__oauth {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
padding-bottom: 20px;
// margin-top: auto;
// margin-bottom: 20px;
:deep(.arco-divider-text) {
color: var(--color-text-4);
font-size: 12px;
font-weight: 400;
line-height: 20px;
}
.list {
align-items: center;
display: flex;
justify-content: center;
width: 100%;
.item {
margin-right: 15px;
}
.mode {
color: var(--color-text-2);
font-size: 12px;
font-weight: 400;
line-height: 20px;
padding: 6px 10px;
align-items: center;
border: 1px solid var(--color-border-3);
border-radius: 32px;
box-sizing: border-box;
display: flex;
height: 32px;
justify-content: center;
cursor: pointer;
.icon {
width: 21px;
height: 20px;
}
}
.mode svg {
font-size: 16px;
margin-right: 10px;
}
.mode:hover,
.mode svg:hover {
background: rgba(var(--primary-6), 0.05);
border: 1px solid rgb(var(--primary-3));
color: rgb(var(--arcoblue-6));
}
}
}
}
.theme-btn {
position: fixed;
top: 20px;
right: 30px;
z-index: 9999;
}
.footer {
align-items: center;
box-sizing: border-box;
position: absolute;
bottom: 10px;
z-index: 999;
.beian {
.text {
font-size: 12px;
font-weight: 400;
letter-spacing: 0.2px;
line-height: 20px;
text-align: center;
}
.below {
align-items: center;
display: flex;
}
}
}
}
@media screen and (min-width: 571px) {
.h5 {
display: none !important;
}
.login {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--color-bg-5);
&-logo {
position: fixed;
top: 20px;
left: 30px;
z-index: 9999;
color: var(--color-text-1);
font-weight: 500;
font-size: 20px;
line-height: 32px;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 34px;
height: 34px;
margin-right: 8px;
}
}
&-box {
width: 86%;
max-width: 850px;
height: 490px;
display: flex;
z-index: 999;
box-shadow: 0 2px 4px 2px rgba(0, 0, 0, 0.08);
}
}
.login-left {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
background: linear-gradient(60deg, rgb(var(--primary-6)), rgb(var(--primary-3)));
&__img {
width: 100%;
position: absolute;
bottom: 0;
right: 0;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
transition: all 0.3s;
object-fit: cover;
}
}
.login-right {
width: 100%;
height: 100%;
background: var(--color-bg-1);
display: flex;
flex-direction: column;
padding: 30px 30px 0;
box-sizing: border-box;
&__title {
color: var(--color-text-1);
font-weight: 500;
font-size: 20px;
line-height: 32px;
margin-bottom: 20px;
}
&__form {
:deep(.arco-tabs-nav-tab) {
display: flex;
justify-content: center;
align-items: center;
}
:deep(.arco-tabs-tab) {
color: var(--color-text-2);
}
:deep(.arco-tabs-tab-title) {
font-size: 16px;
font-weight: 500;
line-height: 22px;
}
:deep(.arco-tabs-content) {
margin-top: 10px;
}
:deep(.arco-tabs-tab-active),
:deep(.arco-tabs-tab-title:hover) {
color: rgb(var(--arcoblue-6));
}
:deep(.arco-tabs-nav::before) {
display: none;
}
:deep(.arco-tabs-tab-title:before) {
display: none;
}
}
&__oauth {
margin-top: auto;
margin-bottom: 20px;
:deep(.arco-divider-text) {
color: var(--color-text-4);
font-size: 12px;
font-weight: 400;
line-height: 20px;
}
.list {
align-items: center;
display: flex;
justify-content: center;
width: 100%;
.item {
margin-right: 15px;
}
.mode {
color: var(--color-text-2);
font-size: 12px;
font-weight: 400;
line-height: 20px;
padding: 6px 10px;
align-items: center;
border: 1px solid var(--color-border-3);
border-radius: 32px;
box-sizing: border-box;
display: flex;
height: 32px;
justify-content: center;
cursor: pointer;
.icon {
width: 21px;
height: 20px;
}
}
.mode svg {
font-size: 16px;
margin-right: 10px;
}
.mode:hover {
background: rgba(var(--primary-6), 0.05);
border: 1px solid rgb(var(--primary-3));
color: rgb(var(--arcoblue-6));
}
}
}
}
.theme-btn {
position: fixed;
top: 20px;
right: 30px;
z-index: 9999;
}
.footer {
align-items: center;
box-sizing: border-box;
position: absolute;
bottom: 10px;
z-index: 999;
.beian {
.text {
font-size: 12px;
font-weight: 400;
letter-spacing: 0.2px;
line-height: 20px;
text-align: center;
}
.below {
align-items: center;
display: flex;
}
}
}
}
</style>

View File

@@ -1,44 +1,62 @@
<template>
<div v-if="isDesktop" class="login pc">
<h3 class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<h3 v-if="title !== 'SakurA Platform'" class="login-logo">
<!-- <img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" /> -->
<img :class="title === 'SakurA Platform' ? 'login-logo-img1' : 'login-logo-img'" :src="logo" alt="logo" />
<span>{{ title }}</span>
</h3>
<a-row align="stretch" class="login-box">
<a-col :xs="0" :sm="12" :md="13">
<div class="login-left">
<img class="login-left__img" src="@/assets/images/banner.png" alt="banner" />
<a-col :xs="0" :sm="10" :md="11">
<div class="login-left" :class="title === 'SakurA Platform' ? 'sakura-bg' : 'background'">
<img v-if="title !== 'SakurA Platform'" class="login-left__img" src="@/assets/images/banner.png" alt="banner" />
<img :class="title === 'SakurA Platform' ? 'login-left__log1' : 'login-left__log'" :src="logo" alt="logo" />
<div v-if="title === 'SakurA Platform'" class="login-left__title">{{ title === 'SakurA Platform' ? 'SakurA 自动化平台' : title }}</div>
<div v-if="title === 'SakurA Platform'" class="login-left__version">{{ version }}</div>
</div>
</a-col>
<a-col :xs="24" :sm="12" :md="11">
<a-col :xs="24" :sm="10" :md="13">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
<a-tabs v-if="!authStore.isEmailLogin && !authStore.isForgotPassword" v-model:active-key="authStore.activeKey" class="login-right__form">
<a-tab-pane v-if="!authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="1" title="账号登录">
<AccountLogin v-if="authStore.activeKey === '1'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
<a-tab-pane v-if="!authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="2" title="手机号登录">
<PhoneLogin v-if="authStore.activeKey === '2'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane v-if="authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="3" title="账号注册">
<AccountLogin v-if="authStore.activeKey === '3'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane v-if="authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="4" title="手机号注册">
<PhoneLogin v-if="authStore.activeKey === '4'" :is-register="authStore.isRegister" />
</a-tab-pane>
</a-tabs>
<h3 v-if="authStore.isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="authStore.isEmailLogin" />
<h3 v-if="authStore.isForgotPassword" class="login-right__title">修改密码</h3>
<PasswordLogin v-if="authStore.isForgotPassword" />
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
<div v-show="!authStore.isRegister">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="authStore.isEmailLogin || authStore.isForgotPassword" class="mode item" @click="authStore.toggleMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="authStore.toggleEmailLoginMode"><icon-email /> 邮箱登录 </div>
<a v-if="!authStore.isForgotPassword" class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a v-if="!authStore.isForgotPassword" class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
<div v-if="!authStore.isForgotPassword" class="register">
<span style="line-height: 1.5715;">{{ !authStore.isRegister ? '没有账号?' : '已有账号?' }}</span>
<a-link @click="authStore.toggleRegisterMode">{{ !authStore.isRegister ? '去注册' : '立即登录' }}</a-link>
</div>
</div>
</div>
</a-col>
<GiSvgIcon name="qr-code" color="rgb(var(--primary-6))" class="login-right__qrcode" :size="24" />
</a-row>
<div v-if="isDesktop" class="footer">
@@ -50,78 +68,142 @@
<GiThemeBtn class="theme-btn" />
<Background />
</div>
<div v-else class="login h5">
<div class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<div class="login-logo" :class="{ 'sakura-pd': title === 'SakurA Platform' }">
<!-- <img v-if="logo" :src="logo" alt="logo" /> -->
<img v-if="logo" :class="title === 'SakurA Platform' ? 'login-logo-img1' : 'login-logo-img'" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<a-row align="stretch" class="login-box">
<a-col :xs="24" :sm="12" :md="11">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
<a-tabs v-if="!authStore.isEmailLogin && !authStore.isForgotPassword" v-model:active-key="authStore.activeKey" class="login-right__form">
<a-tab-pane v-if="!authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="1" title="账号登录">
<AccountLogin v-if="authStore.activeKey === '1'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
<a-tab-pane v-if="!authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="2" title="手机号登录">
<PhoneLogin v-if="authStore.activeKey === '2'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane v-if="authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="3" title="账号注册">
<AccountLogin v-if="authStore.activeKey === '3'" :is-register="authStore.isRegister" />
</a-tab-pane>
<a-tab-pane v-if="authStore.isRegister && !authStore.isEmailLogin && !authStore.isForgotPassword" key="4" title="手机号注册">
<PhoneLogin v-if="authStore.activeKey === '4'" :is-register="authStore.isRegister" />
</a-tab-pane>
</a-tabs>
<h3 v-if="authStore.isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="authStore.isEmailLogin" />
<h3 v-if="authStore.isForgotPassword" class="login-right__title">修改密码</h3>
<PasswordLogin v-if="authStore.isForgotPassword" />
</div>
</a-col>
</a-row>
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
<div v-show="!authStore.isRegister">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="authStore.isEmailLogin || authStore.isForgotPassword" class="mode item" @click="authStore.toggleMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="authStore.toggleEmailLoginMode"><icon-email /> 邮箱登录 </div>
<a v-if="!authStore.isForgotPassword" class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a v-if="!authStore.isForgotPassword" class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
<div v-if="!authStore.isForgotPassword" class="register">
<span style="line-height: 1.5715;">{{ !authStore.isRegister ? '没有账号?' : '已有账号?' }}</span>
<a-link @click="authStore.toggleRegisterMode">{{ !authStore.isRegister ? '去注册' : '立即登录' }}</a-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Background from './components/background/index.vue'
import AccountLogin from './components/account/index.vue'
import PhoneLogin from './components/phone/index.vue'
import EmailLogin from './components/email/index.vue'
import config from '../../../package.json'
import * as BackgroundComponent from './components/background/index.vue'
import * as AccountLoginComponent from './components/account/index.vue'
import * as PasswordLoginComponent from './components/password/index.vue'
import * as PhoneLoginComponent from './components/phone/index.vue'
import * as EmailLoginComponent from './components/email/index.vue'
import { socialAuth } from '@/apis/auth'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'
import { useAuthStore } from '@/stores/modules/auth'
defineOptions({ name: 'Login' })
defineOptions({
name: 'Login',
created() {
// this.version = config.version
},
})
// 获取组件
const Background = BackgroundComponent.default || BackgroundComponent
const AccountLogin = AccountLoginComponent.default || AccountLoginComponent
const PasswordLogin = PasswordLoginComponent.default || PasswordLoginComponent
const PhoneLogin = PhoneLoginComponent.default || PhoneLoginComponent
const EmailLogin = EmailLoginComponent.default || EmailLoginComponent
const authStore = useAuthStore()
const { isDesktop } = useDevice()
const appStore = useAppStore()
const title = computed(() => appStore.getTitle())
const logo = computed(() => appStore.getLogo())
const version = ref(config.version)
const isRegister = ref(false)
const isEmailLogin = ref(false)
const activeTab = ref('1')
const isForgotPassword = ref(false)
const activeKey = ref('1')
// 监听切换事件,更新 activeKey
const _onTabChange = (key: string | number) => {
activeKey.value = key as string
}
const keyMap = {
1: '3',
2: '4',
3: '1',
4: '2',
}
// 切换注册模式
const _toggleRegisterMode = () => {
isRegister.value = !isRegister.value
isEmailLogin.value = false
activeKey.value = keyMap[activeKey.value]
}
// 切换登录模式
const toggleLoginMode = () => {
const _toggleLoginMode = () => {
isRegister.value = !isRegister.value
isEmailLogin.value = false
activeKey.value = keyMap[activeKey.value]
}
// 切换邮箱登录模式
const _toggleEmailLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value
}
// 切换找回密码模式
const toggleForgotPasswordMode = () => {
isForgotPassword.value = !isForgotPassword.value
}
// 提供方法
provide('toggleForgotPasswordMode', toggleForgotPasswordMode)
// 第三方登录授权
const onOauth = async (source: string) => {
const { data } = await socialAuth(source)
window.location.href = data.authorizeUrl
}
onMounted(() => {
// console.log(authStore.activeKey)
// console.log(logo)
})
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@media screen and (max-width: 570px) {
.pc {
display: none !important;
@@ -144,18 +226,28 @@ const onOauth = async (source: string) => {
font-size: 20px;
line-height: 32px;
display: flex;
padding: 0 20px;
&.sakura-pd {
padding: 0 10px;
}
&:not(.sakura-pd) {
padding: 0 20px;
}
align-items: center;
justify-content: start;
background-image: url('/src/assets/images/login_h5.jpg');
background-size: 100% 100%;
box-sizing: border-box;
img {
&-img {
width: 34px;
height: 34px;
margin-right: 8px;
}
&-img1 {
width: 50px;
height: 50px;
}
}
&-box {
@@ -178,7 +270,7 @@ const onOauth = async (source: string) => {
font-weight: 500;
font-size: 20px;
line-height: 32px;
margin-bottom: 20px;
margin-bottom: 30px;
}
&__form {
@@ -203,6 +295,11 @@ const onOauth = async (source: string) => {
margin-top: 10px;
}
:deep(.arco-space-vertical),
:deep(.arco-input-group) {
width: 100%;
}
:deep(.arco-tabs-tab-active),
:deep(.arco-tabs-tab-title:hover) {
color: rgb(var(--arcoblue-6));
@@ -218,11 +315,11 @@ const onOauth = async (source: string) => {
}
&__oauth {
width: 100%;
width: 80%;
position: fixed;
bottom: 0;
left: 0;
padding-bottom: 20px;
// left: 0;
padding: 20px;
// margin-top: auto;
// margin-bottom: 20px;
@@ -276,6 +373,13 @@ const onOauth = async (source: string) => {
color: rgb(var(--arcoblue-6));
}
}
.register {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
@@ -337,32 +441,54 @@ const onOauth = async (source: string) => {
justify-content: center;
align-items: center;
img {
&-img {
width: 34px;
height: 34px;
margin-right: 8px;
// width: 60px;
// height: 60px;
// margin-right: -5px;
}
&-img1 {
width: 50px;
height: 50px;
}
}
&-box {
width: 86%;
max-width: 850px;
height: 490px;
display: flex;
max-width: 820px;
height: 510px;
z-index: 999;
box-shadow: 0 2px 4px 2px rgba(0, 0, 0, 0.08);
border-radius: 10px;
display: flex;
flex-wrap: nowrap;
}
}
.login-left {
width: 100%;
height: 100%;
border-radius: 10px 0 0 10px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
/* 子项之间的垂直间距 */
position: relative;
overflow: hidden;
background: linear-gradient(60deg, rgb(var(--primary-6)), rgb(var(--primary-3)));
// background: linear-gradient(60deg, rgb(var(--primary-6)), rgb(var(--primary-3)));
&.background {
background: linear-gradient(60deg, rgb(var(--primary-6)), rgb(var(--primary-3)));
background-size: 110%;
}
&.sakura-bg {
background: url(@/assets/images/left-bg.png) no-repeat center top;
background-size: 110%;
}
&__img {
width: 100%;
@@ -375,23 +501,66 @@ const onOauth = async (source: string) => {
transition: all 0.3s;
object-fit: cover;
}
&__log {
width: 50px !important;
height: 50px !important;
margin-bottom: 20px;
}
&__log1 {
width: 100px !important;
height: 100px !important;
}
&__title {
font-size: 22px;
font-weight: 600;
background-image: linear-gradient(135deg, #ffc626, #32bee7);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-family: auto;
opacity: 0.8;
letter-spacing: 1.5px;
text-shadow: -1px -1px 1px #deefff, 0 -1px 1px #152c48, 1px -1px 1px #0836b9, 1px 0 1px #013a4a, 1px 1px 1px #134a5a, 0 1px 1px #32bee7, -1px 1px 1px #32bee7, -1px 0 1px #75cf13, 0 0 4px #56a7d7;
}
&__version {
font-size: 18px;
font-weight: 600;
background-image: linear-gradient(135deg, #ffc626, #32bee7);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-family: auto;
opacity: 0.8;
letter-spacing: 1.5px;
text-shadow: -1px -1px 1px #deefff, 0 -1px 1px #152c48, 1px -1px 1px #0836b9, 1px 0 1px #013a4a, 1px 1px 1px #134a5a, 0 1px 1px #32bee7, -1px 1px 1px #32bee7, -1px 0 1px #75cf13, 0 0 4px #56a7d7;
}
}
.login-right {
width: 100%;
height: 100%;
border-radius: 0 10px 10px 0;
background: var(--color-bg-1);
display: flex;
flex-direction: column;
padding: 30px 30px 0;
padding: 20px 30px 0;
box-sizing: border-box;
&__qrcode {
margin-top: 8px;
margin-left: -30px;
}
&__title {
color: var(--color-text-1);
font-weight: 500;
font-size: 20px;
font-size: 22px;
line-height: 32px;
margin-bottom: 20px;
margin-bottom: 40px;
}
&__form {
@@ -415,6 +584,11 @@ const onOauth = async (source: string) => {
margin-top: 10px;
}
:deep(.arco-space-vertical),
:deep(.arco-input-group) {
width: 100%;
}
:deep(.arco-tabs-tab-active),
:deep(.arco-tabs-tab-title:hover) {
color: rgb(var(--arcoblue-6));
@@ -431,7 +605,7 @@ const onOauth = async (source: string) => {
&__oauth {
margin-top: auto;
margin-bottom: 20px;
margin-bottom: 15px;
:deep(.arco-divider-text) {
color: var(--color-text-4);
@@ -476,12 +650,20 @@ const onOauth = async (source: string) => {
margin-right: 10px;
}
.mode:hover {
.mode:hover,
.mode svg:hover {
background: rgba(var(--primary-6), 0.05);
border: 1px solid rgb(var(--primary-3));
color: rgb(var(--arcoblue-6));
}
}
.register {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: center;
}
}
}

View File

@@ -68,8 +68,6 @@ import ModifyPassword from '../components/modifyPassword/index.vue'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'
defineOptions({ name: 'PwdExpired' })
const { isDesktop } = useDevice()
const appStore = useAppStore()
const title = computed(() => appStore.getTitle())

View File

@@ -0,0 +1,289 @@
<template>
<a-modal
v-model:visible="visible" :title="title" :mask-closable="false" :esc-to-close="false"
:width="width >= 500 ? 500 : '100%'" draggable @before-ok="save" @ok="saveAfter" @close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #captcha>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn" :loading="captchaLoading" :disabled="captchaDisable" size="large"
@click="onCaptcha"
>
{{ captchaBtnName }}
</a-button>
</template>
</GiForm>
<Verify
ref="VerifyRef" :captcha-type="captchaType" :mode="captchaMode"
:img-size="{ width: '330px', height: '155px' }" @success="getCaptcha"
/>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { Message } from '@arco-design/web-vue'
import NProgress from 'nprogress'
import { type BehaviorCaptchaReq, getEmailCaptcha, updateUserEmail, updateUserPassword, updateUserPhone } from '@/apis'
import { encryptByRsa } from '@/utils/encrypt'
import { useUserStore } from '@/stores'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import * as Regexp from '@/utils/regexp'
import modalErrorWrapper from '@/utils/modal-error-wrapper'
import router from '@/router'
const { width } = useWindowSize()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const verifyType = ref()
const title = computed(
() => `修改${verifyType.value === 'phone' ? '手机号' : verifyType.value === 'email' ? '邮箱' : '密码'}`,
)
const formRef = ref<InstanceType<typeof GiForm>>()
const [form, resetForm] = useResetReactive({
phone: '',
email: '',
captcha: '',
oldPassword: '',
newPassword: '',
rePassword: '',
})
const columns: ColumnItem[] = reactive([
{
label: '手机号',
field: 'phone',
type: 'input',
span: 24,
props: {
showWordLimit: false,
},
rules: [
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' },
],
hide: () => {
return verifyType.value !== 'phone'
},
},
{
label: '邮箱',
field: 'email',
type: 'input',
span: 24,
rules: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
],
hide: () => {
return verifyType.value !== 'email'
},
},
{
label: '验证码',
field: 'captcha',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入验证码' }],
hide: () => {
return !['phone', 'email'].includes(verifyType.value)
},
},
{
label: '当前密码',
field: 'oldPassword',
type: 'input-password',
span: 24,
rules: [{ required: true, message: '请输入当前密码' }],
hide: () => {
return !userInfo.value.pwdResetTime
},
},
{
label: '新密码',
field: 'newPassword',
type: 'input-password',
span: 24,
rules: [
{ required: true, message: '请输入新密码' },
{
validator: (value, callback) => {
if (value === form.oldPassword) {
callback('新密码不能与当前密码相同')
} else {
callback()
}
},
},
],
hide: () => {
return verifyType.value !== 'password'
},
},
{
label: '确认新密码',
field: 'rePassword',
type: 'input-password',
span: 24,
props: {
placeholder: '请再次输入新密码',
},
rules: [
{ required: true, message: '请再次输入新密码' },
{
validator: (value, callback) => {
if (value !== form.newPassword) {
callback('两次输入的密码不一致')
} else {
callback()
}
},
},
],
hide: () => {
return verifyType.value !== 'password'
},
},
])
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')
const captchaMode = ref('pop')
const captchaLoading = ref(false)
// 弹出行为验证码
const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.formRef?.validateField(verifyType.value === 'phone' ? 'phone' : 'email')
if (isInvalid) return
VerifyRef.value.show()
}
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
resetCaptcha()
}
// 获取验证码
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
// 发送验证码
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
if (verifyType.value === 'phone') {
// await getSmsCaptcha(form.phone, captchaReq)
} else if (verifyType.value === 'email') {
await getEmailCaptcha(form.email, captchaReq)
}
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
// 保存
const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
try {
if (verifyType.value === 'phone') {
await updateUserPhone({
phone: form.phone,
captcha: form.captcha,
oldPassword: encryptByRsa(form.oldPassword) as string,
})
Message.success('修改成功')
} else if (verifyType.value === 'email') {
await updateUserEmail({
email: form.email,
captcha: form.captcha,
oldPassword: encryptByRsa(form.oldPassword) as string,
})
Message.success('修改成功')
} else if (verifyType.value === 'password') {
if (form.newPassword !== form.rePassword) {
Message.error('两次新密码不一致')
return false
}
if (form.newPassword === form.oldPassword) {
Message.error('新密码与旧密码不能相同')
return false
}
await updateUserPassword({
oldPassword: encryptByRsa(form.oldPassword) || '',
newPassword: encryptByRsa(form.newPassword) || '',
})
}
return true
} catch (error) {
return false
}
}
const saveAfter = async () => {
if (verifyType.value === 'password') {
modalErrorWrapper({
title: '提示',
content: '密码修改成功! 请保存好新密码,并使用新密码重新登录',
maskClosable: false,
escToClose: false,
okText: '重新登录',
async onOk() {
NProgress.done()
const userStore = useUserStore()
await userStore.logoutCallBack()
await router.replace('/login')
},
})
} else {
// 修改成功后,重新获取用户信息
await userStore.getInfo()
}
}
const visible = ref(false)
// 打开弹框
const open = (type: string) => {
verifyType.value = type
visible.value = true
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.captcha-btn {
margin-left: 12px;
min-width: 98px;
border-radius: 4px;
}
</style>

View File

@@ -6,10 +6,7 @@
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #captcha>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn" :loading="captchaLoading" :disabled="captchaDisable" size="large"
@click="onCaptcha"
>
<a-button class="captcha-btn" :loading="captchaLoading" :disabled="captchaDisable" size="large" @click="onCaptcha">
{{ captchaBtnName }}
</a-button>
</template>
@@ -51,37 +48,48 @@ const [form, resetForm] = useResetReactive({
oldPassword: '',
newPassword: '',
rePassword: '',
captchaDisable: false,
})
const columns: ColumnItem[] = reactive([
{
label: '手机号',
label: '手机号',
field: 'phone',
type: 'input',
span: 24,
props: {
showWordLimit: false,
},
rules: [
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' },
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' },
],
hide: () => {
return verifyType.value !== 'phone'
},
props: {
allowClear: true,
maxLength: 11,
onInput: () => {
form.captchaDisable = Regexp.Phone.test(form.phone)
},
},
},
{
label: '邮箱',
label: '邮箱',
field: 'email',
type: 'input',
span: 24,
rules: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
],
hide: () => {
return verifyType.value !== 'email'
},
props: {
allowClear: true,
onInput: () => {
form.captchaDisable = Regexp.Email.test(form.email)
},
},
},
{
label: '验证码',
@@ -165,20 +173,13 @@ const onCaptcha = async () => {
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
resetCaptcha()
form.captchaDisable = true
}
// 获取验证码
@@ -189,14 +190,16 @@ const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
captchaBtnName.value = '发送中...'
if (verifyType.value === 'phone') {
// await getSmsCaptcha(form.phone, captchaReq)
Message.success('短信发送成功演示默认【111111】')
form.captcha = '111111'
} else if (verifyType.value === 'email') {
await getEmailCaptcha(form.email, captchaReq)
Message.success('邮件发送成功,请前往邮箱查看验证码')
}
captchaLoading.value = false
captchaDisable.value = true
form.captchaDisable = false
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
// Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
@@ -211,6 +214,13 @@ const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
}
}
// 取消
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
resetCaptcha()
}
// 保存
const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate()

View File

@@ -73,7 +73,7 @@
</template>
</a-form-item>
<a-form-item class="input-item" field="SITE_TITLE" :label="siteConfig.SITE_TITLE.name" :help="siteConfig.SITE_TITLE.description">
<a-input v-model.trim="form.SITE_TITLE" placeholder="请输入系统名称" :max-length="18" show-word-limit />
<a-input v-model.trim="form.SITE_TITLE" placeholder="请输入系统名称" :max-length="30" show-word-limit />
</a-form-item>
<a-form-item class="input-item" field="SITE_DESCRIPTION" :label="siteConfig.SITE_DESCRIPTION.name" :help="siteConfig.SITE_DESCRIPTION.description">
<a-textarea