feat: add auth API clients and view scaffolding

This commit is contained in:
2026-10-15 21:29:21 +08:00
parent bca123303e
commit 460f120364
8 changed files with 97 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
import type { ChangePasswordPayload, CurrentUser, LoginPayload, RegisterPayload, UpdateProfilePayload } from '../types/auth'
export class AuthApiUnavailableError extends Error {
constructor(public readonly status: number, message: string) { super(message) }
}
async function authRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(path, {
credentials: 'include',
headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...init.headers },
...init,
})
if (!response.ok) {
const body = await response.json().catch(() => null)
throw new AuthApiUnavailableError(response.status, body?.detail || `认证请求失败(${response.status})`)
}
return response.status === 204 ? undefined as T : response.json() as Promise<T>
}
export const register = (payload: RegisterPayload) => authRequest<CurrentUser>('/api/auth/register', { method: 'POST', body: JSON.stringify(payload) })
export const login = (payload: LoginPayload) => authRequest<CurrentUser>('/api/auth/login', { method: 'POST', body: JSON.stringify(payload) })
export const logout = () => authRequest<void>('/api/auth/logout', { method: 'POST' })
export const getCurrentUser = () => authRequest<CurrentUser>('/api/auth/me')
export const updateMyProfile = (payload: UpdateProfilePayload) => authRequest<CurrentUser>('/api/users/me', { method: 'PATCH', body: JSON.stringify(payload) })
export const changePassword = (payload: ChangePasswordPayload) => authRequest<void>('/api/auth/change-password', { method: 'POST', body: JSON.stringify(payload) })
export const getUsers = () => authRequest<CurrentUser[]>('/api/admin/users')
+16
View File
@@ -0,0 +1,16 @@
export type UserRole = 'user' | 'admin'
export type CurrentUser = {
id: number
username: string
nickname: string | null
email: string | null
role: UserRole
created_at?: string
conversation_count?: number
}
export type RegisterPayload = { username: string; password: string; email?: string; nickname?: string }
export type LoginPayload = { username: string; password: string }
export type UpdateProfilePayload = { nickname?: string; email?: string }
export type ChangePasswordPayload = { current_password: string; new_password: string }
@@ -0,0 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue'
import { changePassword, updateMyProfile } from '../services/auth'
const nickname=ref(''), email=ref(''), currentPassword=ref(''), newPassword=ref(''), notice=ref(''), error=ref('')
async function saveProfile(){try{await updateMyProfile({nickname:nickname.value||undefined,email:email.value||undefined});notice.value='资料已更新。'}catch(e){error.value=e instanceof Error?e.message:'更新失败'}}
async function savePassword(){try{if(newPassword.value.length<8)throw new Error('新密码至少 8 位。');await changePassword({current_password:currentPassword.value,new_password:newPassword.value});notice.value='密码已更新。'}catch(e){error.value=e instanceof Error?e.message:'修改失败'}}
</script>
<template><main><h1>账户设置</h1><p v-if="notice">{{notice}}</p><p v-if="error">{{error}}</p><form @submit.prevent="saveProfile"><input v-model.trim="nickname" placeholder="昵称"/><input v-model.trim="email" type="email" placeholder="邮箱"/><button>保存资料</button></form><form @submit.prevent="savePassword"><input v-model="currentPassword" type="password" placeholder="当前密码"/><input v-model="newPassword" type="password" placeholder="新密码"/><button>修改密码</button></form></main></template>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { login } from '../services/auth'
const username=ref(''), password=ref(''), error=ref(''), loading=ref(false)
async function submit(){error.value='';if(!username.value||!password.value){error.value='请输入用户名和密码。';return}loading.value=true;try{await login({username:username.value,password:password.value})}catch(e){error.value=e instanceof Error?e.message:'登录失败'}finally{loading.value=false}}
</script>
<template><main><h1>登录</h1><form @submit.prevent="submit"><input v-model.trim="username" autocomplete="username" placeholder="用户名"/><input v-model="password" type="password" autocomplete="current-password" placeholder="密码"/><p v-if="error">{{error}}</p><button :disabled="loading">{{loading?'登录中…':'登录'}}</button></form></main></template>
+8
View File
@@ -0,0 +1,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getCurrentUser, logout } from '../services/auth'
import type { CurrentUser } from '../types/auth'
const user=ref<CurrentUser|null>(null), error=ref('')
onMounted(async()=>{try{user.value=await getCurrentUser()}catch(e){error.value=e instanceof Error?e.message:'无法读取个人资料'}})
</script>
<template><main><h1>个人中心</h1><p v-if="error">{{error}}</p><dl v-else-if="user"><dt>用户名</dt><dd>{{user.username}}</dd><dt>昵称</dt><dd>{{user.nickname||'未设置'}}</dd><dt>邮箱</dt><dd>{{user.email||'未设置'}}</dd><dt>角色</dt><dd>{{user.role}}</dd></dl><button @click="logout">退出登录</button></main></template>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { register } from '../services/auth'
const username=ref(''), password=ref(''), email=ref(''), error=ref('')
async function submit(){error.value='';if(username.value.length<3||password.value.length<8){error.value='用户名至少 3 位,密码至少 8 位。';return}try{await register({username:username.value,password:password.value,email:email.value||undefined})}catch(e){error.value=e instanceof Error?e.message:'注册失败'}}
</script>
<template><main><h1>注册</h1><form @submit.prevent="submit"><input v-model.trim="username" placeholder="用户名"/><input v-model.trim="email" type="email" placeholder="邮箱(可选)"/><input v-model="password" type="password" placeholder="密码"/><p v-if="error">{{error}}</p><button>创建账户</button></form></main></template>
@@ -0,0 +1,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getUsers } from '../services/auth'
import type { CurrentUser } from '../types/auth'
const users=ref<CurrentUser[]>([]), error=ref('')
onMounted(async()=>{try{users.value=await getUsers()}catch(e){error.value=e instanceof Error?e.message:'无权读取用户列表'}})
</script>
<template><main><h1>用户管理</h1><p v-if="error">{{error}}</p><table v-else><tr><th>用户名</th><th>昵称</th><th>邮箱</th><th>角色</th></tr><tr v-for="user in users" :key="user.id"><td>{{user.username}}</td><td>{{user.nickname}}</td><td>{{user.email}}</td><td>{{user.role}}</td></tr></table></main></template>