新增用户个人信息页面,整合用户信息获取和更新功能,支持表单验证和密码修改,提升用户体验和代码可维护性。同时更新相关类型定义和API路由,确保数据一致性。

This commit is contained in:
zyh
2025-04-10 13:16:11 +00:00
parent a80321adf6
commit 1977a6757d
5 changed files with 218 additions and 1 deletions

View File

@@ -4,6 +4,7 @@
"react-dom": "https://esm.d8d.fun/react-dom@19.0.0",
"react-dom/client": "https://esm.d8d.fun/react-dom@19.0.0/client",
"react-router": "https://esm.d8d.fun/react-router@7.3.0?deps=react@19.0.0,react-dom@19.0.0",
"react-hook-form": "https://esm.d8d.fun/react-hook-form@7.55.0?deps=react@19.0.0,react-dom@19.0.0",
"@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?deps=react@19.0.0,react-dom@19.0.0",
"@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?deps=react@19.0.0,react-dom@19.0.0",
"axios": "https://esm.d8d.fun/axios@1.6.7",

View File

@@ -26,7 +26,9 @@
"https://esm.d8d.fun/@types/proxy-from-env@~1.0.4/index.d.ts": "https://esm.d8d.fun/@types/proxy-from-env@1.0.4/index.d.ts",
"https://esm.d8d.fun/@types/react-dom@~19.0.4/X-ZHJlYWN0QDE5LjAuMA/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/X-ZHJlYWN0QDE5LjAuMA/index.d.ts",
"https://esm.d8d.fun/@types/react-dom@~19.0.4/client.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/client.d.ts",
"https://esm.d8d.fun/@types/react-dom@~19.0.4/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/index.d.ts",
"https://esm.d8d.fun/@types/react-dom@~19.0.6/X-ZHJlYWN0QDE5LjAuMA/client.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/X-ZHJlYWN0QDE5LjAuMA/client.d.ts",
"https://esm.d8d.fun/@types/react@~19.0.11/index.d.ts": "https://esm.d8d.fun/@types/react@19.0.14/index.d.ts",
"https://esm.d8d.fun/@types/react@~19.0.12/index.d.ts": "https://esm.d8d.fun/@types/react@19.0.14/index.d.ts",
"https://esm.d8d.fun/@types/react@~19.0.12/jsx-runtime.d.ts": "https://esm.d8d.fun/@types/react@19.0.14/jsx-runtime.d.ts",
"https://esm.d8d.fun/@types/react@~19.0.14/X-ZHJlYWN0LWRvbUAxOS4wLjA/index.d.ts": "https://esm.d8d.fun/@types/react@19.0.14/X-ZHJlYWN0LWRvbUAxOS4wLjA/index.d.ts",
@@ -52,6 +54,7 @@
"https://esm.d8d.fun/nanoid@^5.1.2?target=denonext": "https://esm.d8d.fun/nanoid@5.1.5?target=denonext",
"https://esm.d8d.fun/node-gyp-build@^4.3.0?target=denonext": "https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext",
"https://esm.d8d.fun/proxy-from-env@^1.1.0?target=denonext": "https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext",
"https://esm.d8d.fun/react@%3E=18?target=denonext": "https://esm.d8d.fun/react@19.1.0?target=denonext",
"https://esm.d8d.fun/scheduler@^0.25.0?target=denonext": "https://esm.d8d.fun/scheduler@0.25.0?target=denonext",
"https://esm.d8d.fun/set-cookie-parser@^2.6.0?target=denonext": "https://esm.d8d.fun/set-cookie-parser@2.7.1?target=denonext",
"https://esm.d8d.fun/socket.io-client@^4.7.2?target=denonext": "https://esm.d8d.fun/socket.io-client@4.8.1?target=denonext",
@@ -78,7 +81,9 @@
"https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?deps=react@19.0.0": "d2c14cfd7f3090062c9f968f25d0ddbb277ca76055af1ac3fd22045276571a75",
"https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?deps=react@19.0.0,react-dom@19.0.0": "5e99f4d40ce60c55b5cf421c3cf3f13df1707cf53152e447b2332570412cd77a",
"https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?deps=react@19.0.0": "546051c9fdfdca5c7d51cd4cf588fe709da509274c5fcf203d616a5e87bdd595",
"https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?deps=react@19.0.0,react-dom@19.0.0": "e3940182b574da537337b1e90a1b7f380e17050457423e13d5ac8c7bc88a3cc0",
"https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/outline.mjs": "640f934a0c987f682032049e5d4a455567db676de47bca0d44e76b72023661f7",
"https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/solid.mjs": "dcbd0c377d92857b6eb23c7dbb2ee6e650b12aa6ae1ef7fcc10dc1964df8ba47",
"https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0QDE5LjAuMA/denonext/24/outline.mjs": "640f934a0c987f682032049e5d4a455567db676de47bca0d44e76b72023661f7",
"https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0QDE5LjAuMA/denonext/24/solid.mjs": "dcbd0c377d92857b6eb23c7dbb2ee6e650b12aa6ae1ef7fcc10dc1964df8ba47",
"https://esm.d8d.fun/@socket.io/component-emitter@3.1.2/denonext/component-emitter.mjs": "3c6c5f2d64d4933b577a7117df1d8855c51ff01ab3dea8f42af1adcb1a5989e7",
@@ -173,6 +178,7 @@
"https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext": "261a6cedf1fdbf159798141ba1e2311ac1510682c5c8b55dacc8cf5fdee4aa06",
"https://esm.d8d.fun/proxy-from-env@1.1.0/denonext/proxy-from-env.mjs": "f60f9c79fc3baa07c13c800798d645ae70d1b2059b8d593dcd4f8c5710b50333",
"https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext": "bf02a050a1a6aa56ddba25dbea2c355da294630e5c5520fddea4b2f30a9292bc",
"https://esm.d8d.fun/react-dom@19.0.0": "d057f65e74eca8add1702ba9a5ecbcc8e60a73dd358b7852094bde0361725137",
"https://esm.d8d.fun/react-dom@19.0.0/X-ZHJlYWN0QDE5LjAuMA/denonext/client.mjs": "af662fd134eea98f37fdcea6142accd0f8a7d2e13c1c3c9e98dc37a8c7aad46b",
"https://esm.d8d.fun/react-dom@19.0.0/X-ZHJlYWN0QDE5LjAuMA/denonext/react-dom.mjs": "a2f7bc344e1d5b7ca47e68665291e206ae4db17ee84f234f3d3e2533b9119f63",
"https://esm.d8d.fun/react-dom@19.0.0/client": "c972c16184c695fc5828dfa61d7f341edbc463d20d8108765c93a98027c24227",
@@ -180,14 +186,21 @@
"https://esm.d8d.fun/react-dom@19.0.0/denonext/client.mjs": "af662fd134eea98f37fdcea6142accd0f8a7d2e13c1c3c9e98dc37a8c7aad46b",
"https://esm.d8d.fun/react-dom@19.0.0/denonext/react-dom.mjs": "a2f7bc344e1d5b7ca47e68665291e206ae4db17ee84f234f3d3e2533b9119f63",
"https://esm.d8d.fun/react-dom@19.0.0?deps=react@19.0.0,react-dom@19.0.0": "0e49978c3f0fb4a94db9c9318aebd7e1b35651678050871a91ebb080cc3e1f83",
"https://esm.d8d.fun/react-hook-form@7.55.0/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/react-hook-form.mjs": "788ec1a54e10051f539ba435aa513802c823bad03e11e2534b1b17df99189a87",
"https://esm.d8d.fun/react-hook-form@7.55.0?deps=react@19.0.0,react-dom@19.0.0": "8ed376b3af6e11be43538b15e654692d5995232523a6dc16ce7f81263b1a3614",
"https://esm.d8d.fun/react-router@7.3.0": "ed310627e3a6bd90acbaefa1474263abd85e127041ccc5f665cf3d3a574c85c8",
"https://esm.d8d.fun/react-router@7.3.0/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/dist/development/chunk-K6CSEXPM.mjs": "441898046ad7c4fd9a6b53e13a398c9c74c4412c519e942f82b8a77f7af9f9d6",
"https://esm.d8d.fun/react-router@7.3.0/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/react-router.mjs": "b0b05fcfc3a03c5f679cd0bc69ca19aa10abaa977395df00e86b3fb114e5e346",
"https://esm.d8d.fun/react-router@7.3.0/denonext/dist/development/chunk-K6CSEXPM.mjs": "095a3225d9bbe00e5749781a8335be24f770a9e2634f88f75bb691de46a50a18",
"https://esm.d8d.fun/react-router@7.3.0/denonext/react-router.mjs": "b0b05fcfc3a03c5f679cd0bc69ca19aa10abaa977395df00e86b3fb114e5e346",
"https://esm.d8d.fun/react-router@7.3.0?deps=react@19.0.0,react-dom@19.0.0": "ad747718e32a45020d67eb4ff98f9734cb06a10ceb393baac0a965043e96cdf0",
"https://esm.d8d.fun/react@19.0.0": "ab1f4aa20ac56c237bbb204632bdb55f03a0ab005d21944eeb447e5e37879637",
"https://esm.d8d.fun/react@19.0.0/X-ZHJlYWN0LWRvbUAxOS4wLjA/denonext/react.mjs": "87fdb28d39ca8983bdba3e7ec329305f95463cfc70c015b2620b4900fa15efdd",
"https://esm.d8d.fun/react@19.0.0/denonext/jsx-runtime.mjs": "643b749fa9666fbf73619a99fd708722edb4acaa34c8cea7be783a3432367780",
"https://esm.d8d.fun/react@19.0.0/denonext/react.mjs": "87fdb28d39ca8983bdba3e7ec329305f95463cfc70c015b2620b4900fa15efdd",
"https://esm.d8d.fun/react@19.0.0?deps=react@19.0.0,react-dom@19.0.0": "05a4c12599a7d4b62ff7fc37228964902d26e2f7ba03a61d6335793b998972b7",
"https://esm.d8d.fun/react@19.1.0/denonext/react.mjs": "b43f435068776ab7a40daea8854ab1f8eca6252e86a9ac8b716bb9110ffeb76e",
"https://esm.d8d.fun/react@19.1.0?target=denonext": "8cb1e2ba1aeb012dc6807c8b3cf6ae90579b448317c9debc9d888dcabc246e66",
"https://esm.d8d.fun/scheduler@0.25.0/denonext/scheduler.mjs": "50687edf9e0034b6db97303b1b16893b59c5833c21ea8cf913dc380b537f6aaf",
"https://esm.d8d.fun/scheduler@0.25.0?target=denonext": "c12810f51123057a8a8e309cc8befaac0b5cd371cb4d61bf0372ab8046acc8e0",
"https://esm.d8d.fun/set-cookie-parser@2.7.1/denonext/set-cookie-parser.mjs": "81f09c909c63221a2460bc7602746543af6fd05b54fd866a04e81bb754bc7f26",

View File

@@ -0,0 +1,132 @@
import React from 'react'
import { useNavigate } from 'react-router'
import { useForm } from 'react-hook-form'
import { UserAPI } from './api.ts'
import type { User } from '../share/types.ts'
export default function ProfilePage() {
const navigate = useNavigate()
const { register, handleSubmit, formState: { errors }, setValue } = useForm<Omit<User, 'id' | 'role' | 'avatar'> & { password?: string }>()
const [loading, setLoading] = React.useState(false)
const [user, setUser] = React.useState<User | null>(null)
// 获取当前用户信息
React.useEffect(() => {
const fetchUser = async () => {
try {
const res = await UserAPI.getUsers({ limit: 1 })
if (res.data?.length > 0) {
const userData = res.data[0]
setUser(userData)
setValue('username', userData.username)
setValue('nickname', userData.nickname)
setValue('email', userData.email)
setValue('phone', userData.phone)
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
fetchUser()
}, [setValue])
// 提交表单更新用户信息
const onSubmit = async (data: User) => {
try {
setLoading(true)
if (!user?.id) return
const updatedUser = await UserAPI.updateUser(user.id, {
nickname: data.nickname,
email: data.email,
phone: data.phone,
...(data.password ? { password: data.password } : {})
})
setUser(updatedUser.data)
alert('更新成功')
} catch (error) {
console.error('更新失败:', error)
alert('更新失败')
} finally {
setLoading(false)
}
}
return (
<div className="p-4 max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-6 text-gray-800"></h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<input
{...register('username')}
disabled
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<input
{...register('nickname', { required: '请输入昵称' })}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
{errors.nickname && <p className="mt-1 text-sm text-red-600">{errors.nickname.message}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<input
{...register('email', {
required: '请输入邮箱',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '请输入有效的邮箱地址'
}
})}
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<input
{...register('phone')}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<input
{...register('password')}
type="password"
placeholder="留空则不修改密码"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="flex space-x-3 pt-4">
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
>
{loading ? '保存中...' : '保存'}
</button>
<button
type="button"
onClick={() => navigate(-1)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
</button>
</div>
</form>
</div>
)
}

View File

@@ -32,6 +32,7 @@ export interface User {
phone?: string;
role: string;
avatar?: string;
password?: string;
}
export interface MenuItem {

View File

@@ -237,5 +237,75 @@ export function createUserRoutes(withAuth: WithAuth) {
}
})
// 获取当前用户信息
usersRoutes.get('/me', withAuth, async (c) => {
try {
const user = c.get('user')!
const apiClient = c.get('apiClient')
const userData = await apiClient.database.table('users')
.where('id', user.id)
.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
.first()
if (!user) {
return c.json({ error: '用户不存在' }, 404)
}
return c.json({
data: user,
message: '获取用户详情成功'
})
} catch (error) {
console.error('获取当前用户信息失败:', error)
return c.json({ error: '获取当前用户信息失败' }, 500)
}
})
// 更新当前用户信息
usersRoutes.put('/me', withAuth, async (c) => {
try {
const user = c.get('user')!
const apiClient = c.get('apiClient')
const body = await c.req.json()
// 验证必填字段
const { nickname, email, phone } = body
if (!nickname || !email) {
return c.json({ error: '缺少必要的用户信息' }, 400)
}
// 更新用户信息
const updateData: any = {
nickname,
email,
phone: phone || null,
updated_at: new Date()
}
// 如果提供了新密码,则更新密码
if (body.password) {
updateData.password = body.password
}
await apiClient.database.table('users')
.where('id', user.id)
.update(updateData)
const updatedUser = await apiClient.database.table('users')
.where('id', user.id)
.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
.first()
return c.json({
data: updatedUser,
message: '更新用户信息成功'
})
} catch (error) {
console.error('更新当前用户信息失败:', error)
return c.json({ error: '更新当前用户信息失败' }, 500)
}
})
return usersRoutes
}
}