Template
UI / layout - rebuild left mode: full-width blue header (60px) + white sidebar below + card-style tags view - introduce design tokens in variables.css (brand #347df9, sidebar 240px, menu item 49px) and theme overrides for dark / dark-blue themes - restyle navigation bar (logo + platform name / top nav / tools + user), logo, sidebar (selected state: light blue bg + blue bar + bold), tags view, app main - global styles: .app-container white card, .head-title, .btn-blue/.btn-white, el-table / el-dialog look, brand color injected into --el-color-primary - add favorites store (localStorage) wired to sidebar star and header top nav - disable template watermark / footer / promo notifications by default Login (serverschool) - port school-iot-admin login page (background art, right side card, keep-login) - OAuth2 password mode: GET /oauth/token (raw axios) + GET /uc/account/current - port phone lookup (/wx/getAccoutList) and forget-password flow (/sms/sendCode + /sms/verifyCode) as components/login/Forget.vue - user store exposes accountInfo / accountType / teamId / roleId and strips password Infra - move all devServer proxies and env vars from school-iot-admin/vue.config.js (default targets point to production) - fix empty sidebar: guard short-circuit skipped setRoutes when login page already fetched user info
279 lines
7.7 KiB
Vue
279 lines
7.7 KiB
Vue
<script lang="ts" setup>
|
||
import type { FormInstance, FormRules } from "element-plus"
|
||
import type { AccountItem } from "../apis/type"
|
||
import { QuestionFilled } from "@element-plus/icons-vue"
|
||
import { getAccountListApi, sendSmsCodeApi, verifySmsCodeApi } from "../apis"
|
||
|
||
interface Emits {
|
||
(e: "backToLogin"): void
|
||
(e: "passwordResetSuccess", username: string, password: string): void
|
||
}
|
||
|
||
defineOptions({
|
||
name: "ForgetPassword"
|
||
})
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
/** 表单元素的引用 */
|
||
const formRef = useTemplateRef<FormInstance>("formRef")
|
||
|
||
/** 表单数据 */
|
||
const form = reactive({
|
||
phone: "",
|
||
userName: "",
|
||
code: "",
|
||
newPassword: "",
|
||
confirmPassword: ""
|
||
})
|
||
|
||
/** 手机号查出的账号列表 */
|
||
const accountList = ref<AccountItem[]>([])
|
||
|
||
/** 上一次查询过的手机号(避免重复请求) */
|
||
const oldPhone = ref("")
|
||
|
||
/** 验证码倒计时 */
|
||
const countdown = ref(0)
|
||
|
||
let timer: ReturnType<typeof setInterval> | undefined
|
||
|
||
/** 发送验证码 Loading */
|
||
const smsLoading = ref(false)
|
||
|
||
/** 提交 Loading */
|
||
const submitLoading = ref(false)
|
||
|
||
/** 重置成功提示弹窗 */
|
||
const dialogVisible = ref(false)
|
||
|
||
/** 校验两次密码一致 */
|
||
function validateConfirm(_rule: unknown, value: string, callback: (error?: Error) => void) {
|
||
if (value !== form.newPassword) callback(new Error("两次输入的密码不一致"))
|
||
else callback()
|
||
}
|
||
|
||
/** 表单校验规则(对齐 school-iot-admin 的忘记密码表单) */
|
||
const rules: FormRules = {
|
||
phone: [
|
||
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||
{ pattern: /^1[3-9]\d{9}$/, message: "手机号格式错误", trigger: "blur" }
|
||
],
|
||
userName: [{ required: true, message: "请选择用户名", trigger: "change" }],
|
||
code: [
|
||
{ required: true, message: "请输入验证码", trigger: "blur" },
|
||
{ len: 6, message: "验证码必须是6位数字", trigger: "blur" }
|
||
],
|
||
newPassword: [
|
||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||
{ min: 8, message: "密码长度不能少于8位", trigger: "blur" },
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (/^(?:\d+|[a-z]+)$/i.test(value)) callback(new Error("密码强度太弱,请包含字母、数字"))
|
||
else callback()
|
||
},
|
||
trigger: "blur"
|
||
}
|
||
],
|
||
confirmPassword: [
|
||
{ required: true, message: "请再次输入密码", trigger: "blur" },
|
||
{ validator: validateConfirm, trigger: "blur" }
|
||
]
|
||
}
|
||
|
||
/** 根据手机号查询账号列表 */
|
||
async function loadAccountList() {
|
||
try {
|
||
const res = await getAccountListApi(form.phone)
|
||
accountList.value = res.success ? (res.data ?? []) : []
|
||
form.userName = accountList.value[0]?.userName ?? ""
|
||
} catch {
|
||
accountList.value = []
|
||
form.userName = ""
|
||
}
|
||
}
|
||
|
||
/** 手机号失焦:合法手机号才查询账号列表 */
|
||
function handlePhoneBlur() {
|
||
const phone = form.phone
|
||
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
|
||
if (oldPhone.value !== phone) {
|
||
form.userName = ""
|
||
oldPhone.value = phone
|
||
loadAccountList()
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 倒计时 */
|
||
function tick() {
|
||
timer = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) {
|
||
clearInterval(timer)
|
||
timer = undefined
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
/** 获取验证码 */
|
||
async function getVerifyCode() {
|
||
if (!form.phone) {
|
||
ElMessage.warning("请先输入手机号")
|
||
return
|
||
}
|
||
smsLoading.value = true
|
||
countdown.value = 120
|
||
tick()
|
||
try {
|
||
const res = await sendSmsCodeApi(form.phone)
|
||
if (res.success) {
|
||
ElMessage.success("验证码已发送")
|
||
} else {
|
||
throw new Error(res.msg || "发送失败")
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error((error as Error).message || "发送失败")
|
||
countdown.value = 0
|
||
if (timer) {
|
||
clearInterval(timer)
|
||
timer = undefined
|
||
}
|
||
} finally {
|
||
smsLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 提交重置密码 */
|
||
async function onSubmit() {
|
||
const valid = await formRef.value?.validate().catch(() => false)
|
||
if (!valid) {
|
||
ElMessage.warning("请检查输入内容")
|
||
return
|
||
}
|
||
submitLoading.value = true
|
||
try {
|
||
const res = await verifySmsCodeApi({
|
||
userName: form.userName,
|
||
newPassword: form.newPassword,
|
||
phone: form.phone,
|
||
code: form.code
|
||
})
|
||
if (res.success) {
|
||
ElMessage.success("密码重置成功")
|
||
dialogVisible.value = true
|
||
} else {
|
||
ElMessage.error(res.msg || "提交失败")
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error((error as Error).message || "提交失败")
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 重置成功弹窗的确定按钮:回填账号密码并返回登录表单 */
|
||
function ok() {
|
||
dialogVisible.value = false
|
||
emit("passwordResetSuccess", form.userName, form.newPassword)
|
||
formRef.value?.resetFields()
|
||
}
|
||
|
||
/** 返回登录 */
|
||
function goBackLogin() {
|
||
emit("backToLogin")
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="forget-container">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="auto" @submit.prevent>
|
||
<el-form-item label="手机号" prop="phone">
|
||
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" @blur="handlePhoneBlur" />
|
||
</el-form-item>
|
||
<el-form-item label="用户名" prop="userName">
|
||
<el-select
|
||
v-model="form.userName"
|
||
placeholder="请选择用户名"
|
||
:disabled="accountList.length === 0"
|
||
style="width: 100%"
|
||
>
|
||
<template #prefix>
|
||
<el-tooltip effect="dark" content="此处为小程序对应的用户名" placement="top">
|
||
<el-icon class="question-icon">
|
||
<QuestionFilled />
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</template>
|
||
<el-option v-for="item in accountList" :key="item.userName" :label="item.userName" :value="item.userName" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="验证码" prop="code">
|
||
<el-input v-model="form.code" placeholder="请输入验证码" maxlength="6" :disabled="!form.userName">
|
||
<template #append>
|
||
<el-button :disabled="countdown > 0 || !form.userName" :loading="smsLoading" @click="getVerifyCode">
|
||
{{ countdown > 0 ? `${countdown}s 后重试` : "获取验证码" }}
|
||
</el-button>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="新密码" prop="newPassword">
|
||
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password />
|
||
</el-form-item>
|
||
<el-form-item label="确认密码" prop="confirmPassword">
|
||
<el-input v-model="form.confirmPassword" placeholder="请再次输入新密码" type="password" show-password />
|
||
</el-form-item>
|
||
<el-button type="primary" class="submit-button" :loading="submitLoading" @click="onSubmit">
|
||
提交
|
||
</el-button>
|
||
<el-button class="submit-button" @click="goBackLogin">
|
||
返回登录
|
||
</el-button>
|
||
</el-form>
|
||
|
||
<el-dialog v-model="dialogVisible" title="提示" width="30%" append-to-body>
|
||
<div class="reset-success-tip">
|
||
<p>关联的小程序旧密码失效,用修改后的密码进行登入</p>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="ok">
|
||
确 定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.forget-container {
|
||
width: 100%;
|
||
|
||
.question-icon {
|
||
margin-right: 4px;
|
||
color: var(--el-text-color-placeholder);
|
||
cursor: help;
|
||
}
|
||
|
||
.submit-button {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
|
||
& + .submit-button {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
.reset-success-tip {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
}
|
||
</style>
|