Files
gsfdc/portal/app/pages/expert/reset.vue
T

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>