新增用户个人信息页面,整合用户信息获取和更新功能,支持表单验证和密码修改,提升用户体验和代码可维护性。同时更新相关类型定义和API路由,确保数据一致性。
This commit is contained in:
@@ -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",
|
||||
|
||||
13
client/mobile/deno.lock
generated
13
client/mobile/deno.lock
generated
@@ -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",
|
||||
|
||||
132
client/mobile/pages_profile.tsx
Normal file
132
client/mobile/pages_profile.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export interface User {
|
||||
phone?: string;
|
||||
role: string;
|
||||
avatar?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user