feat: API层与认证流(client拦截/守卫/登录改密实装/域API骨架/vitest)

- 自建轻量 axios client(src/api/client.ts):Bearer 注入、401 清会话跳登录
  (login 请求除外)、409 精确匹配"必须先修改初始密码"跳强制改密(区别于并发
  冲突 409)、429/网络错误统一 ElMessage、ApiError 标准化、showErrorMessage
  可opt-out——模板 utils/http 的 {code,msg,data} 信封与后端裸 HTTP 语义不符故弃用
- Pinia auth store(src/store/modules/auth.ts):token/role/tenant_slug/
  must_change 落地并持久化(all-kefu-auth key),login/logout/changePassword/
  会话过期/强制改密跳转(动态 import 断循环)
- 路由守卫:token 事实源判定、must_change 全站门(除改密页)、UserInfo 由登录
  快照派生(后端无用户信息端点);user store 残留自愈
- 登录页实装(去除角色调试开关)、强制改密页实装;用户菜单/锁屏对接真实 logout
- 域 API 骨架:tenants/config/conversations/stats/settings(类型+端点常量)
- mask 哨兵助手:__SET__ ↔ 已设置 表单语义
- vitest 25 例(client 11/store 8/mask 6);登录页拖拽验证 reset 空引用修复

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 16:28:11 +08:00
parent 6afa31ec85
commit 3973abea8b
23 changed files with 1809 additions and 132 deletions
@@ -1,4 +1,4 @@
<!-- 强制修改密码(骨架占位页;登录对接与提交逻辑在后续任务实现) -->
<!-- 强制修改密码(首登引导 / 管理员重置后的 must_change 语义) -->
<template>
<div class="force-change-password flex-cc w-full h-screen">
<ElCard shadow="never" class="card">
@@ -6,11 +6,20 @@
<span class="card-title">首次登录请修改密码</span>
</template>
<ElForm ref="formRef" :model="formData" :rules="rules" label-position="top">
<ElFormItem prop="oldPassword" label="原密码">
<ElInput
v-model.trim="formData.oldPassword"
type="password"
placeholder="请输入原密码"
show-password
autocomplete="off"
/>
</ElFormItem>
<ElFormItem prop="newPassword" label="新密码">
<ElInput
v-model.trim="formData.newPassword"
type="password"
placeholder="请输入新密码"
placeholder="请输入新密码(不少于 8 位)"
show-password
autocomplete="off"
/>
@@ -28,25 +37,31 @@
提交修改
</ElButton>
</ElForm>
<p class="tip">密码修改成功后方可进入后台(对接后端 API 在后续任务实现)</p>
<p class="tip">密码修改成功后方可进入后台;修改后当前会话保持有效,无需重新登录</p>
</ElCard>
</div>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth'
defineOptions({ name: 'ForceChangePassword' })
const authStore = useAuthStore()
const router = useRouter()
const formRef = ref<FormInstance>()
const loading = ref(false)
const formData = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const rules: FormRules = {
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 8, message: '密码长度不少于 8 位', trigger: 'blur' }
@@ -66,15 +81,27 @@
]
}
// 骨架阶段仅做表单校验,不发起请求(Task 3 对接 /api/platform/auth/*)
// 提交改密(POST /api/platform/auth/change-password):
// 成功 → must_change 清零 → 进仪表盘;400 原密码不正确 / 401 会话过期
// 由 HTTP client 统一提示
const handleSubmit = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
loading.value = true
setTimeout(() => {
try {
await authStore.changePassword({
old_password: formData.oldPassword,
new_password: formData.newPassword
})
ElMessage.success('密码修改成功,即将进入后台')
await router.push('/')
} catch {
// 错误提示已由 HTTP client 统一弹出(如 400 原密码不正确)
} finally {
loading.value = false
}, 600)
}
}
</script>