Files
new-school-iot-admin/src/pages/login/components/Forget.vue
T
admin 47dd605067 Align console UI with school-iot-admin and wire login to serverschool
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
2026-10-27 09:13:39 +08:00

279 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>