113 lines
3.8 KiB
Vue
113 lines
3.8 KiB
Vue
<template>
|
|
<div class="auth-box">
|
|
<h2 class="page-title">重置密码</h2>
|
|
<p class="hint">使用邮箱验证码设置新密码。</p>
|
|
<label>邮箱</label>
|
|
<input v-model="email" type="email" placeholder="name@example.com" autocomplete="username">
|
|
<label>邮箱验证码</label>
|
|
<div class="code-row">
|
|
<input v-model="code" type="text" placeholder="6 位验证码" maxlength="6">
|
|
<button type="button" class="btn ghost" :disabled="codeBusy || cooldown > 0" @click="sendCode">
|
|
{{ cooldown > 0 ? `${cooldown}s` : "获取验证码" }}
|
|
</button>
|
|
</div>
|
|
<label>新密码</label>
|
|
<input v-model="password" type="password" placeholder="至少 8 位,含字母和数字" autocomplete="new-password">
|
|
<div class="actions">
|
|
<button type="button" class="btn" :disabled="busy" @click="submit">重置并登录</button>
|
|
<NuxtLink :to="{ path: '/expert/login', query: { redirect } }">返回登录</NuxtLink>
|
|
</div>
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { post } = useSiteApi()
|
|
const { setSession } = useMemberAuth()
|
|
|
|
const email = ref("")
|
|
const code = ref("")
|
|
const password = ref("")
|
|
const busy = ref(false)
|
|
const codeBusy = ref(false)
|
|
const cooldown = ref(0)
|
|
const error = ref("")
|
|
const redirect = computed(() => String(route.query.redirect || "/expert/qa"))
|
|
|
|
let timer: ReturnType<typeof setInterval> | undefined
|
|
onBeforeUnmount(() => {
|
|
if (timer) clearInterval(timer)
|
|
})
|
|
|
|
function startCooldown() {
|
|
cooldown.value = 60
|
|
if (timer) clearInterval(timer)
|
|
timer = setInterval(() => {
|
|
cooldown.value -= 1
|
|
if (cooldown.value <= 0 && timer) {
|
|
clearInterval(timer)
|
|
timer = undefined
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
async function sendCode() {
|
|
error.value = ""
|
|
codeBusy.value = true
|
|
try {
|
|
await post("/auth/email-code", { email: email.value.trim(), purpose: "reset" })
|
|
startCooldown()
|
|
} catch (exc: any) {
|
|
error.value = exc?.data?.detail || exc?.message || "验证码发送失败"
|
|
} finally {
|
|
codeBusy.value = false
|
|
}
|
|
}
|
|
|
|
async function submit() {
|
|
error.value = ""
|
|
busy.value = true
|
|
try {
|
|
const body = await post<{ accessToken: string }>("/auth/reset-password", {
|
|
email: email.value.trim(),
|
|
code: code.value.trim(),
|
|
password: password.value,
|
|
})
|
|
await setSession(body.accessToken)
|
|
await router.replace(redirect.value)
|
|
} catch (exc: any) {
|
|
error.value = exc?.data?.detail || exc?.message || "重置失败"
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.auth-box { max-width: 420px; }
|
|
.page-title { margin: 0 0 12px; font-size: 22px; font-weight: 650; letter-spacing: 0.08em; }
|
|
.hint { margin: 0 0 14px; font-size: 12px; color: var(--muted, #5b6b85); }
|
|
label { display: block; margin: 0 0 6px; font-size: 13px; color: var(--muted, #5b6b85); }
|
|
input[type="email"], input[type="text"], input[type="password"] {
|
|
width: 100%; margin: 0 0 14px; padding: 10px 12px;
|
|
border: 1px solid var(--line, #d6e4ff); border-radius: 8px;
|
|
background: var(--surface, #fff); color: var(--text, #0b1f3a); font: inherit;
|
|
}
|
|
.code-row { display: flex; gap: 10px; align-items: center; margin: 0 0 14px; }
|
|
.code-row input { margin: 0; flex: 1; }
|
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
.actions a { font-size: 13px; color: var(--c, #1677ff); text-decoration: none; }
|
|
.btn {
|
|
appearance: none; border: 0; border-radius: 8px; padding: 10px 16px;
|
|
font: inherit; background: var(--c, #1677ff); color: #fff; cursor: pointer;
|
|
}
|
|
.btn.ghost {
|
|
background: var(--chip, #f7faff); color: var(--text, #0b1f3a);
|
|
border: 1px solid var(--line, #d6e4ff); white-space: nowrap;
|
|
}
|
|
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
.error { margin: 12px 0 0; color: #b91c1c; font-size: 13px; }
|
|
</style>
|