Files
jiang13-bbs/frontend/app/u/[id]/ProfileForm.tsx
freefire 8c5de13899 fix: 空评论列表 500,并完善撤回审计与角色说明
空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 00:23:55 +08:00

194 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { Check } from "lucide-react";
import { apiUpdateProfile, type User } from "@/lib/api";
import AvatarPicker from "@/components/AvatarPicker";
interface ProfileFormProps {
user: User;
}
/**
* 个人资料编辑表单:头像(上传/裁剪/历史选择)、昵称、邮箱、签名。
* 头像由独立上传链路处理,不随本表单提交;保存成功后 router.refresh()
* 让 Header 等 SSR 区域同步最新昵称。
*/
export default function ProfileForm({ user }: ProfileFormProps) {
const router = useRouter();
const [nickname, setNickname] = useState(user.nickname || "");
const [email, setEmail] = useState(user.email || "");
const [signature, setSignature] = useState(user.signature || "");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [avatarDragOver, setAvatarDragOver] = useState(false);
const [incomingAvatarFile, setIncomingAvatarFile] = useState<File | null>(null);
const clearIncomingAvatar = useCallback(() => setIncomingAvatarFile(null), []);
const handleAvatarDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes("Files")) setAvatarDragOver(true);
};
const handleAvatarDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// 仅在离开整块卡片时取消高亮(避免子元素冒泡闪烁)
const related = e.relatedTarget as Node | null;
if (related && e.currentTarget.contains(related)) return;
setAvatarDragOver(false);
};
const handleAvatarDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setAvatarDragOver(false);
const file = e.dataTransfer.files?.[0];
if (!file) return;
setIncomingAvatarFile(file);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
const trimmed = nickname.trim();
if (!trimmed) {
setError("昵称不能为空");
return;
}
if (trimmed.length > 64) {
setError("昵称不能超过 64 个字符");
return;
}
if (signature.length > 255) {
setError("签名不能超过 255 个字符");
return;
}
setSubmitting(true);
try {
const res = await apiUpdateProfile({
nickname: trimmed,
email: email.trim(),
signature,
});
if (res.user) {
setSuccess("资料已保存");
router.refresh();
} else {
setError(res.error || "保存失败");
}
} catch {
setError("网络错误,请重试");
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="panel p-5 sm:p-7 space-y-5">
{success && (
<div className="alert-ok flex items-center gap-2" role="status">
<Check size={15} /> {success}
</div>
)}
{error && <div className="alert-error" role="alert">{error}</div>}
{/* 头像:点击上传/裁剪,或拖放图片到本区域 */}
<div
className="flex items-center gap-4 p-4 rounded-xl transition-colors"
style={{
background: avatarDragOver
? "color-mix(in srgb, var(--accent) 12%, var(--panel-2))"
: "var(--panel-2)",
border: `1px solid ${avatarDragOver ? "var(--accent)" : "var(--line)"}`,
}}
onDragOver={handleAvatarDragOver}
onDragLeave={handleAvatarDragLeave}
onDrop={handleAvatarDrop}
>
<AvatarPicker
name={nickname || user.username}
src={user.avatar || ""}
size={56}
incomingFile={incomingAvatarFile}
onIncomingConsumed={clearIncomingAvatar}
/>
<div className="flex-1 min-w-0">
<p className="text-[13.5px] font-bold" style={{ color: "var(--ink)" }}>头像</p>
<p className="text-[12px] mt-1" style={{ color: "var(--ink-3)" }}>
{avatarDragOver
? "松开即可上传并裁剪"
: "点击头像或拖放图片到此处;支持拖动裁剪,统一转为 WebP,裁剪后不超过 2MB"}
</p>
</div>
</div>
{/* 昵称 + 邮箱:桌面双列高密度排布 */}
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label className="field-label" htmlFor="pf-nickname">昵称</label>
<input
id="pf-nickname"
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
required
maxLength={64}
className="field"
placeholder="请输入昵称"
/>
</div>
<div>
<label className="field-label" htmlFor="pf-email">邮箱</label>
<input
id="pf-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
maxLength={128}
className="field"
placeholder="your@email.com"
/>
</div>
</div>
{/* 个性签名 */}
<div>
<label className="field-label" htmlFor="pf-signature">
个性签名
<span className="font-normal ml-1.5" style={{ color: "var(--ink-3)" }}>
({signature.length}/255)
</span>
</label>
<textarea
id="pf-signature"
value={signature}
onChange={(e) => setSignature(e.target.value)}
maxLength={255}
rows={3}
className="field"
placeholder="介绍一下自己吧"
/>
</div>
{/* 操作区:桌面右对齐 */}
<div className="flex justify-end pt-1 border-t" style={{ borderColor: "var(--line)" }}>
<button
type="submit"
disabled={submitting}
className="btn btn-primary w-full sm:w-auto"
>
{submitting ? "保存中..." : "保存资料"}
</button>
</div>
</form>
);
}