Template
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
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user