Files
kevisual-center/src/pages/user/page.tsx
abearxiong 66ee0d7f60 feat: implement logout on 401 response and update query handling
refactor: replace Button with div for consistent styling in AIEditorLink

refactor: update navigation handling in AppVersionList and remove LayoutMain wrapper

refactor: remove unused LayoutMain imports and components across various pages

fix: ensure user app list is set correctly in useUserAppStore

fix: update login URL format in AuthProvider

fix: adjust layout styles in EnvPage and other pages for better responsiveness

chore: update route definitions and create new routes for apps, config, domain, flowme, org, remote, token, user, and users

style: replace Button with div for delete confirmation in various components

fix: ensure correct handling of user profile image source
2026-02-22 03:24:14 +08:00

290 lines
8.8 KiB
TypeScript

'use client';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useUserStore } from './store';
import { useLayoutStore } from '@/modules/layout/store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Pencil, Key, User } from 'lucide-react';
import PandaPNG from '@/assets/panda.jpg';
const ProfileCard = () => {
const { me, getMe } = useLayoutStore();
const { setShowEdit, setShowChangePassword } = useUserStore();
useEffect(() => {
getMe();
}, [getMe]);
return (
<Card className="max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="w-5 h-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Avatar */}
<div className="flex items-center gap-4">
<div className="w-20 h-20 rounded-full overflow-hidden border-2 border-gray-200">
{me?.avatar ? (
<img
src={me.avatar}
alt="avatar"
className="w-full h-full object-cover"
/>
) : (
<img
src={PandaPNG}
alt="avatar"
className="w-full h-full object-cover"
/>
)}
</div>
<div>
<h3 className="text-lg font-semibold">{me?.username || '-'}</h3>
<p className="text-sm text-gray-500">{me?.description || '暂无描述'}</p>
</div>
</div>
{/* User Info Fields */}
<div className="space-y-4">
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
<Label></Label>
<div className="text-gray-700">{me?.username || '-'}</div>
</div>
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
<Label></Label>
<div className="flex items-center justify-between">
<span className="text-gray-700">{me?.nickname || '-'}</span>
<Button
variant="ghost"
size="sm"
onClick={() => setShowEdit(true)}>
<Pencil className="w-4 h-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-[120px_1fr] items-start gap-4">
<Label className="mt-2"></Label>
<div className="flex items-center justify-between flex-1">
<p className="text-gray-700 text-sm">{me?.description || '暂无描述'}</p>
<Button
variant="ghost"
size="sm"
onClick={() => setShowEdit(true)}>
<Pencil className="w-4 h-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
<Label>ID</Label>
<div className="text-gray-500 text-sm">{me?.id || '-'}</div>
</div>
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
<Label></Label>
<Button
variant="outline"
size="sm"
onClick={() => setShowChangePassword(true)}>
<Key className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
{/* Edit Profile Button */}
<div className="flex justify-end">
<Button onClick={() => setShowEdit(true)}>
<Pencil className="w-4 h-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
);
};
const EditProfileModal = () => {
const { showEdit, setShowEdit, setFormData, updateSelf, loading } = useUserStore();
const { me, getMe } = useLayoutStore();
const {
handleSubmit,
reset,
register,
} = useForm();
useEffect(() => {
if (showEdit) {
reset({
nickname: me?.nickname || '',
description: me?.description || '',
});
}
}, [me, showEdit, reset]);
const onSubmit = async (data: any) => {
const res = await updateSelf(data);
if (res) {
setShowEdit(false);
setFormData({});
await getMe();
}
};
return (
<Dialog open={showEdit} onOpenChange={(open) => {
setShowEdit(open);
if (!open) setFormData({});
}}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="p-4">
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-2">
<Label></Label>
<Input
{...register('nickname')}
placeholder="请输入昵称"
/>
</div>
<div className="flex flex-col gap-2">
<Label></Label>
<Textarea
{...register('description')}
placeholder="请输入个人描述"
rows={4}
/>
</div>
<div className="flex gap-2 justify-end">
<Button
type="button"
variant="outline"
onClick={() => setShowEdit(false)}
disabled={loading}>
</Button>
<Button type="submit" disabled={loading}>
{loading ? '保存中...' : '保存'}
</Button>
</div>
</form>
</div>
</DialogContent>
</Dialog>
);
};
const ChangePasswordModal = () => {
const { showChangePassword, setShowChangePassword, loading } = useUserStore();
const {
handleSubmit,
formState: { errors },
reset,
register,
} = useForm();
const onSubmit = async (data: any) => {
const { updateSelf } = useUserStore.getState();
const res = await updateSelf({
password: data.newPassword,
});
if (res) {
setShowChangePassword(false);
reset();
}
};
return (
<Dialog open={showChangePassword} onOpenChange={setShowChangePassword}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="p-4">
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-2">
<Label></Label>
<Input
{...register('newPassword', {
required: '请输入新密码',
minLength: { value: 6, message: '密码长度至少6位' },
})}
type="password"
placeholder="请输入新密码"
className={errors.newPassword ? "border-red-500" : ""}
/>
{errors.newPassword && (
<span className="text-xs text-red-500">{errors.newPassword.message as string}</span>
)}
</div>
<div className="flex flex-col gap-2">
<Label></Label>
<Input
{...register('confirmPassword', {
required: '请确认新密码',
validate: (value, formValues) =>
value === formValues.newPassword || '两次输入的密码不一致',
})}
type="password"
placeholder="请再次输入新密码"
className={errors.confirmPassword ? "border-red-500" : ""}
/>
{errors.confirmPassword && (
<span className="text-xs text-red-500">{errors.confirmPassword.message as string}</span>
)}
</div>
<div className="flex gap-2 justify-end">
<Button
type="button"
variant="outline"
onClick={() => setShowChangePassword(false)}
disabled={loading}>
</Button>
<Button type="submit" disabled={loading}>
{loading ? '保存中...' : '确认修改'}
</Button>
</div>
</form>
</div>
</DialogContent>
</Dialog>
);
};
export const UserProfile = () => {
return (
<div className="p-6 w-full h-full overflow-auto">
<ProfileCard />
<EditProfileModal />
<ChangePasswordModal />
</div>
);
};
export default UserProfile