# 鐵人賽 第十五天 ### TLTR * 編輯4個檔案 * \src\store\modules\user.ts * \src\views\login\index.vue * \src\views\login\utils\rule.ts * \src\views\login\utils\static.ts * 新增4個檔案 * \src\views\login\utils\enums.ts * \src\views\login\utils\verifyCode.ts * \src\views\login\components\regist.vue * \src\views\login\components\update.vue --- ### 編輯 #### \src\store\modules\user.ts ```ts= //\src\store\modules\user.ts import { defineStore } from "pinia"; import { store } from "/@/store"; import { userType } from "./types"; import { router } from "/@/router"; import { routerArrays } from "/@/layout/types"; import { storageSession } from "@pureadmin/utils"; import { getLogin, refreshToken } from "/@/api/user"; import { getToken, setToken, removeToken } from "/@/utils/auth"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; const data = getToken(); let token = ""; let name = ""; if (data) { const dataJson = JSON.parse(data); if (dataJson) { token = dataJson?.accessToken; name = dataJson?.name ?? "admin"; } } export const useUserStore = defineStore({ id: "pure-user", state: (): userType => ({ token, name, // 前端生成的验证码(按实际需求替换) verifyCode: "", // 數值依照這邊的 \src\views\login\utils\enums.ts index+1 currentPage: 0 }), actions: { SET_TOKEN(token) { this.token = token; }, SET_NAME(name) { this.name = name; }, SET_VERIFYCODE(verifyCode) { this.verifyCode = verifyCode; }, SET_CURRENTPAGE(value) { this.currentPage = value; }, /** 登入 */ async loginByUsername(data) { return new Promise<void>((resolve, reject) => { getLogin(data) .then(data => { if (data) { setToken(data); resolve(); } }) .catch(error => { reject(error); }); }); }, /** 登出 清空缓存 */ logOut() { this.token = ""; this.name = ""; removeToken(); storageSession.clear(); useMultiTagsStoreHook().handleTags("equal", routerArrays); router.push("/login"); }, /** 刷新token */ async refreshToken(data) { removeToken(); return refreshToken(data).then(data => { if (data) { setToken(data); return data; } }); } } }); export function useUserStoreHook() { return useUserStore(store); } ``` #### \src\views\login\index.vue ```ts= <script setup lang="ts"> //\src\views\login\index.vue import { useI18n } from "vue-i18n"; import Motion from "./utils/motion"; import { useRouter } from "vue-router"; import { loginRules } from "./utils/rule"; import Regist from "./components/regist.vue"; import Update from "./components/update.vue"; import { initRouter } from "/@/router/utils"; import { useNav } from "/@/layout/hooks/useNav"; import { message } from "@pureadmin/components"; import type { FormInstance } from "element-plus"; import { storageSession } from "@pureadmin/utils"; import { $t, transformI18n } from "/@/plugins/i18n"; import { ref, reactive, computed } from "vue"; import { operates } from "./utils/enums"; import { useLayout } from "/@/layout/hooks/useLayout"; import { useUserStoreHook } from "/@/store/modules/user"; import { bg, avatar, currentWeek } from "./utils/static"; import { useRenderIcon } from "/@/components/ReIcon/src/hooks"; import { useTranslationLang } from "/@/layout/hooks/useTranslationLang"; import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange"; import dayIcon from "/@/assets/svg/day.svg?component"; import darkIcon from "/@/assets/svg/dark.svg?component"; import globalization from "/@/assets/svg/globalization.svg?component"; defineOptions({ name: "Login" }); const router = useRouter(); const loading = ref(false); const checked = ref(false); const ruleFormRef = ref<FormInstance>(); const currentPage = computed(() => { return useUserStoreHook().currentPage; }); const { initStorage } = useLayout(); initStorage(); const { t } = useI18n(); const { dataTheme, dataThemeChange } = useDataThemeChange(); const { title, getDropdownItemStyle, getDropdownItemClass } = useNav(); const { locale, translationCh, translationEn } = useTranslationLang(); const ruleForm = reactive({ username: "admin", password: "admin123" }); const onLogin = async (formEl: FormInstance | undefined) => { loading.value = true; if (!formEl) return; await formEl.validate((valid, fields) => { if (valid) { // 模拟请求,需根据实际开发进行修改 setTimeout(() => { loading.value = false; storageSession.setItem("info", { username: "admin", accessToken: "eyJhbGciOiJIUzUxMiJ9.test" }); initRouter("admin").then(() => {}); message.success("登录成功"); router.push("/"); }, 2000); } else { loading.value = false; return fields; } }); }; function onHandle(value) { useUserStoreHook().SET_CURRENTPAGE(value); } dataThemeChange(); </script> <template> <div class="select-none"> <img :src="bg" class="wave" /> <div class="flex-c absolute right-5 top-3"> <!-- 主题 --> <el-switch v-model="dataTheme" inline-prompt :active-icon="dayIcon" :inactive-icon="darkIcon" @change="dataThemeChange" /> <!-- 国际化 --> <el-dropdown trigger="click"> <globalization class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300" /> <template #dropdown> <el-dropdown-menu class="translation"> <el-dropdown-item :style="getDropdownItemStyle(locale, 'zh')" :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" @click="translationCh" > <IconifyIconOffline class="check-zh" v-show="locale === 'zh'" icon="check" /> 简体中文 </el-dropdown-item> <el-dropdown-item :style="getDropdownItemStyle(locale, 'en')" :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" @click="translationEn" > <span class="check-en" v-show="locale === 'en'"> <IconifyIconOffline icon="check" /> </span> English </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> <div class="login-container"> <div class="img"> <component :is="currentWeek" /> </div> <div class="login-box"> <div class="login-form"> <avatar class="avatar" /> <Motion> <h2 class="outline-none">{{ title }}</h2> </Motion> <el-form v-if="currentPage === 0" ref="ruleFormRef" :model="ruleForm" :rules="loginRules" size="large" @keyup.enter="onLogin(ruleFormRef)" > <Motion :delay="100"> <el-form-item :rules="[ { required: true, message: transformI18n($t('login.usernameReg')), trigger: 'blur' } ]" prop="username" > <el-input clearable v-model="ruleForm.username" :placeholder="t('login.username')" :prefix-icon="useRenderIcon('user')" /> </el-form-item> </Motion> <Motion :delay="150"> <el-form-item prop="password"> <el-input clearable show-password v-model="ruleForm.password" :placeholder="t('login.password')" :prefix-icon="useRenderIcon('lock')" /> </el-form-item> </Motion> <Motion :delay="250"> <el-form-item> <div class="w-full h-[20px] flex justify-between items-center"> <el-checkbox v-model="checked"> {{ t("login.remember") }} </el-checkbox> <el-button link type="primary" @click="useUserStoreHook().SET_CURRENTPAGE(4)" > {{ t("login.forget") }} </el-button> </div> <el-button class="w-full mt-4" size="default" type="primary" :loading="loading" @click="onLogin(ruleFormRef)" > {{ t("login.login") }} </el-button> </el-form-item> </Motion> <Motion :delay="300"> <el-form-item> <div class="w-full h-[20px] flex justify-between items-center"> <el-button v-for="(item, index) in operates" :key="index" class="w-full mt-4" size="default" @click="onHandle(index + 1)" > {{ t(item.title) }} </el-button> </div> </el-form-item> </Motion> </el-form> <!-- 注册 --> <Regist v-if="currentPage === 1" /> <!-- 忘记密码 --> <Update v-if="currentPage === 4" /> </div> </div> </div> </div> </template> <style scoped> @import url("/@/style/login.css"); </style> <style lang="scss" scoped> :deep(.el-input-group__append, .el-input-group__prepend) { padding: 0; } .translation { ::v-deep(.el-dropdown-menu__item) { padding: 5px 40px; } .check-zh { position: absolute; left: 20px; } .check-en { position: absolute; left: 20px; } } </style> ``` #### \src\views\login\utils\rule.ts ```ts= //\src\views\login\utils\rule.ts import { reactive } from "vue"; // 取消作者寫的中國手機號碼判斷 // import { isPhone } from "@pureadmin/utils"; import type { FormRules } from "element-plus"; import { $t, transformI18n } from "/@/plugins/i18n"; import { useUserStoreHook } from "/@/store/modules/user"; /** 6位数字验证码正则 */ export const REGEXP_SIX = /^\d{6}$/; /** 手机号码正则 */ export function isPhone<T>(countryCodes: string, value: T): boolean { let reg = /^09[0-9]{8}$/; switch (countryCodes) { case "+86": reg = /^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/; break; case "+886": reg = /^09[0-9]{8}$/; break; } // @ts-expect-error return reg.test(value); } /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ export const REGEXP_PWD = /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; /** 登录校验 */ const loginRules = reactive(<FormRules>{ password: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.passwordReg")))); } else if (!REGEXP_PWD.test(value)) { callback(new Error(transformI18n($t("login.passwordRuleReg")))); } else { callback(); } }, trigger: "blur" } ], verifyCode: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.verifyCodeReg")))); } else if (useUserStoreHook().verifyCode !== value) { callback(new Error(transformI18n($t("login.verifyCodeCorrectReg")))); } else { callback(); } }, trigger: "blur" } ] }); /** 手机登录校验 */ const phoneRules = reactive(<FormRules>{ phone: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.phoneReg")))); } else if (!isPhone("+886", value)) { callback(new Error(transformI18n($t("login.phoneCorrectReg")))); } else { callback(); } }, trigger: "blur" } ], verifyCode: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.verifyCodeReg")))); } else if (!REGEXP_SIX.test(value)) { callback(new Error(transformI18n($t("login.verifyCodeSixReg")))); } else { callback(); } }, trigger: "blur" } ] }); /** 忘记密码校验 */ const updateRules = reactive(<FormRules>{ phone: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.phoneReg")))); } else if (!isPhone("+886", value)) { callback(new Error(transformI18n($t("login.phoneCorrectReg")))); } else { callback(); } }, trigger: "blur" } ], verifyCode: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.verifyCodeReg")))); } else if (!REGEXP_SIX.test(value)) { callback(new Error(transformI18n($t("login.verifyCodeSixReg")))); } else { callback(); } }, trigger: "blur" } ], password: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.passwordReg")))); } else if (!REGEXP_PWD.test(value)) { callback(new Error(transformI18n($t("login.passwordRuleReg")))); } else { callback(); } }, trigger: "blur" } ] }); export { loginRules, phoneRules, updateRules }; ``` #### \src\views\login\utils\static.ts ```ts= //\src\views\login\utils\static.ts import { computed } from "vue"; import bg from "/@/assets/login/bg.png"; import avatar from "/@/assets/login/avatar.svg?component"; import illustration0 from "/@/assets/login/illustration.svg?component"; const currentWeek = computed(() => { return illustration0; }); export { bg, avatar, currentWeek }; ``` ### 新增 #### \src\views\login\components\regist.vue ```ts <script setup lang="ts"> // \src\views\login\components\regist.vue import { useI18n } from "vue-i18n"; import { ref, reactive } from "vue"; import Motion from "../utils/motion"; import { updateRules } from "../utils/rule"; import { message } from "@pureadmin/components"; import type { FormInstance } from "element-plus"; import { useVerifyCode } from "../utils/verifyCode"; import { $t, transformI18n } from "/@/plugins/i18n"; import { useUserStoreHook } from "/@/store/modules/user"; import { useRenderIcon } from "/@/components/ReIcon/src/hooks"; const { t } = useI18n(); const checked = ref(false); const loading = ref(false); const ruleForm = reactive({ username: "", phone: "", verifyCode: "", password: "", repeatPassword: "" }); const ruleFormRef = ref<FormInstance>(); const { isDisabled, text } = useVerifyCode(); const repeatPasswordRule = [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.passwordSureReg")))); } else if (ruleForm.password !== value) { callback(new Error(transformI18n($t("login.passwordDifferentReg")))); } else { callback(); } }, trigger: "blur" } ]; const onUpdate = async (formEl: FormInstance | undefined) => { loading.value = true; if (!formEl) return; await formEl.validate((valid, fields) => { if (valid) { if (checked.value) { // 模拟请求,需根据实际开发进行修改 setTimeout(() => { message.success(transformI18n($t("login.registerSuccess"))); loading.value = false; }, 2000); } else { loading.value = false; message.warning(transformI18n($t("login.tickPrivacy"))); } } else { loading.value = false; return fields; } }); }; function onBack() { useVerifyCode().end(); useUserStoreHook().SET_CURRENTPAGE(0); } </script> <template> <el-form ref="ruleFormRef" :model="ruleForm" :rules="updateRules" size="large" > <Motion> <el-form-item :rules="[ { required: true, message: transformI18n($t('login.usernameReg')), trigger: 'blur' } ]" prop="username" > <el-input clearable v-model="ruleForm.username" :placeholder="t('login.username')" :prefix-icon="useRenderIcon('user')" /> </el-form-item> </Motion> <Motion :delay="100"> <el-form-item prop="phone"> <el-input clearable v-model="ruleForm.phone" :placeholder="t('login.phone')" :prefix-icon="useRenderIcon('iphone')" /> </el-form-item> </Motion> <Motion :delay="150"> <el-form-item prop="verifyCode"> <div class="w-full flex justify-between"> <el-input clearable v-model="ruleForm.verifyCode" :placeholder="t('login.smsVerifyCode')" :prefix-icon=" useRenderIcon('ri:shield-keyhole-line', { online: true }) " /> <el-button :disabled="isDisabled" class="ml-2" @click="useVerifyCode().start(ruleFormRef, 'phone')" > {{ text.length > 0 ? text + t("login.info") : t("login.getVerifyCode") }} </el-button> </div> </el-form-item> </Motion> <Motion :delay="200"> <el-form-item prop="password"> <el-input clearable show-password v-model="ruleForm.password" :placeholder="t('login.password')" :prefix-icon="useRenderIcon('lock')" /> </el-form-item> </Motion> <Motion :delay="250"> <el-form-item :rules="repeatPasswordRule" prop="repeatPassword"> <el-input clearable show-password v-model="ruleForm.repeatPassword" :placeholder="t('login.sure')" :prefix-icon="useRenderIcon('lock')" /> </el-form-item> </Motion> <Motion :delay="300"> <el-form-item> <el-checkbox v-model="checked"> {{ t("login.readAccept") }} </el-checkbox> <el-button link type="primary"> {{ t("login.privacyPolicy") }} </el-button> </el-form-item> </Motion> <Motion :delay="350"> <el-form-item> <el-button class="w-full" size="default" type="primary" :loading="loading" @click="onUpdate(ruleFormRef)" > {{ t("login.definite") }} </el-button> </el-form-item> </Motion> <Motion :delay="400"> <el-form-item> <el-button class="w-full" size="default" @click="onBack"> {{ t("login.back") }} </el-button> </el-form-item> </Motion> </el-form> </template> ``` #### \src\views\login\components\update.vue ```ts <script setup lang="ts"> //\src\views\login\components\update.vue import { useI18n } from "vue-i18n"; import { ref, reactive } from "vue"; import Motion from "../utils/motion"; import { updateRules } from "../utils/rule"; import { message } from "@pureadmin/components"; import type { FormInstance } from "element-plus"; import { useVerifyCode } from "../utils/verifyCode"; import { $t, transformI18n } from "/@/plugins/i18n"; import { useUserStoreHook } from "/@/store/modules/user"; import { useRenderIcon } from "/@/components/ReIcon/src/hooks"; const { t } = useI18n(); const loading = ref(false); const ruleForm = reactive({ phone: "", verifyCode: "", password: "", repeatPassword: "" }); const ruleFormRef = ref<FormInstance>(); const { isDisabled, text } = useVerifyCode(); const repeatPasswordRule = [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.passwordSureReg")))); } else if (ruleForm.password !== value) { callback(new Error(transformI18n($t("login.passwordDifferentReg")))); } else { callback(); } }, trigger: "blur" } ]; const onUpdate = async (formEl: FormInstance | undefined) => { loading.value = true; if (!formEl) return; await formEl.validate((valid, fields) => { if (valid) { // 模拟请求,需根据实际开发进行修改 setTimeout(() => { message.success(transformI18n($t("login.passwordUpdateReg"))); loading.value = false; }, 2000); } else { loading.value = false; return fields; } }); }; function onBack() { useVerifyCode().end(); useUserStoreHook().SET_CURRENTPAGE(0); } </script> <template> <el-form ref="ruleFormRef" :model="ruleForm" :rules="updateRules" size="large" > <Motion> <el-form-item prop="phone"> <el-input clearable v-model="ruleForm.phone" :placeholder="t('login.phone')" :prefix-icon="useRenderIcon('iphone')" /> </el-form-item> </Motion> <Motion :delay="100"> <el-form-item prop="verifyCode"> <div class="w-full flex justify-between"> <el-input clearable v-model="ruleForm.verifyCode" :placeholder="t('login.smsVerifyCode')" :prefix-icon=" useRenderIcon('ri:shield-keyhole-line', { online: true }) " /> <el-button :disabled="isDisabled" class="ml-2" @click="useVerifyCode().start(ruleFormRef, 'phone')" > {{ text.length > 0 ? text + t("login.info") : t("login.getVerifyCode") }} </el-button> </div> </el-form-item> </Motion> <Motion :delay="150"> <el-form-item prop="password"> <el-input clearable show-password v-model="ruleForm.password" :placeholder="t('login.password')" :prefix-icon="useRenderIcon('lock')" /> </el-form-item> </Motion> <Motion :delay="200"> <el-form-item :rules="repeatPasswordRule" prop="repeatPassword"> <el-input clearable show-password v-model="ruleForm.repeatPassword" :placeholder="t('login.sure')" :prefix-icon="useRenderIcon('lock')" /> </el-form-item> </Motion> <Motion :delay="250"> <el-form-item> <el-button class="w-full" size="default" type="primary" :loading="loading" @click="onUpdate(ruleFormRef)" > {{ t("login.definite") }} </el-button> </el-form-item> </Motion> <Motion :delay="300"> <el-form-item> <el-button class="w-full" size="default" @click="onBack"> {{ t("login.back") }} </el-button> </el-form-item> </Motion> </el-form> </template> ``` --- #### \src\views\login\utils\enums.ts ```ts= //\src\views\login\utils\enums.ts import { $t } from "/@/plugins/i18n"; const operates = [ { title: $t("login.register") } // { // title: $t("login.phoneLogin") // }, // { // title: $t("login.qRCodeLogin") // }, ]; const thirdParty = [ { title: $t("login.weChatLogin"), icon: "wechat" }, { title: $t("login.alipayLogin"), icon: "alipay" }, { title: $t("login.qqLogin"), icon: "qq" }, { title: $t("login.weiboLogin"), icon: "weibo" } ]; export { operates, thirdParty }; ``` --- #### \src\views\login\utils\verifyCode.ts ```ts= //\src\views\login\utils\verifyCode.ts import type { FormInstance, FormItemProp } from "element-plus"; import { cloneDeep } from "lodash-unified"; import { ref } from "vue"; const isDisabled = ref(false); const timer = ref(null); const text = ref(""); export const useVerifyCode = () => { const start = async ( formEl: FormInstance | undefined, props: FormItemProp, time = 60 ) => { if (!formEl) return; const initTime = cloneDeep(time); await formEl.validateField(props, isValid => { if (isValid) { clearInterval(timer.value); timer.value = setInterval(() => { if (time > 0) { text.value = `${time}`; isDisabled.value = true; time -= 1; } else { text.value = ""; isDisabled.value = false; clearInterval(timer.value); time = initTime; } }, 1000); } }); }; const end = () => { text.value = ""; isDisabled.value = false; clearInterval(timer.value); }; return { isDisabled, timer, text, start, end }; }; ``` --- ### 確認OK 可以work,還要補icon * \src\components\ReIcon\src\iconifyIconOffline.ts ```ts= import iphoneIcon from "@iconify-icons/ep/iphone"; addIcon("iphone", iphoneIcon); ``` ![](https://i.imgur.com/harJXaQ.png) ![](https://i.imgur.com/5n2TClq.png) ![](https://i.imgur.com/Da2wxiN.png) ![](https://i.imgur.com/Pc8S8A7.png)