merge: resolve conflicts with origin/master
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
CameraOutlined,
|
||||
CheckOutlined,
|
||||
CheckCircleFilled,
|
||||
CloseOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LockOutlined,
|
||||
MailOutlined,
|
||||
MobileOutlined,
|
||||
@@ -134,6 +137,48 @@ function mapAssetToSavedItem(asset: Awaited<ReturnType<typeof assetClient.list>>
|
||||
};
|
||||
}
|
||||
|
||||
function formatProfileDate(value: string | null | undefined): string {
|
||||
if (!value) return "刚刚";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatTaskType(type: WebGenerationPreviewTask["type"]): string {
|
||||
const labels: Record<WebGenerationPreviewTask["type"], string> = {
|
||||
image: "图像",
|
||||
video: "视频",
|
||||
agent: "智能体",
|
||||
"digital-human": "数字人",
|
||||
"character-mix": "角色融合",
|
||||
};
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
function formatTaskStatus(status: WebGenerationPreviewTask["status"]): string {
|
||||
const labels: Record<WebGenerationPreviewTask["status"], string> = {
|
||||
queued: "排队中",
|
||||
running: "生成中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
function formatAssetStatus(status: string | undefined): string {
|
||||
const normalized = String(status || "").toLowerCase();
|
||||
if (normalized === "completed" || normalized === "ready" || normalized === "success") return "可用";
|
||||
if (normalized === "running" || normalized === "processing") return "处理中";
|
||||
if (normalized === "failed" || normalized === "error") return "失败";
|
||||
return status || "资产";
|
||||
}
|
||||
|
||||
function ProfilePage({
|
||||
session,
|
||||
usage,
|
||||
@@ -187,6 +232,9 @@ function ProfilePage({
|
||||
const [profileNotice, setProfileNotice] = useState<string | null>(null);
|
||||
const [localAvatarUrl, setLocalAvatarUrl] = useState(() => session?.user.avatarUrl || readLocalProfileValue(userId, "avatar"));
|
||||
const [profileBio, setProfileBio] = useState(() => session?.user.bio || readLocalProfileValue(userId, "bio"));
|
||||
const [isBioEditing, setIsBioEditing] = useState(false);
|
||||
const [bioEditBackup, setBioEditBackup] = useState("");
|
||||
const [bioStatusNotice, setBioStatusNotice] = useState<string | null>(null);
|
||||
const [bannerUrl, setBannerUrl] = useState(() => session?.user.backgroundUrl || readLocalProfileValue(userId, "background"));
|
||||
|
||||
const completedTasks = tasks.filter((task) => task.status === "completed");
|
||||
@@ -195,6 +243,9 @@ function ProfilePage({
|
||||
const packageLabel = session?.user.activePackages?.[0]?.name || "按量积分";
|
||||
const avatarUrl = session?.user.avatarUrl || localAvatarUrl || null;
|
||||
const displayedBio = profileBio.trim() || "这个人还没有填写个性签名";
|
||||
const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
||||
const phoneLooksValid = /^1[3-9]\d{9}$/.test(phone.trim());
|
||||
const passwordLooksReady = password.length >= (mode === "register" ? 6 : 1);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalAvatarUrl(session?.user.avatarUrl || readLocalProfileValue(userId, "avatar"));
|
||||
@@ -525,8 +576,29 @@ function ProfilePage({
|
||||
void syncProfilePatch({ bio: nextBio || null });
|
||||
};
|
||||
|
||||
const startBioEdit = () => {
|
||||
setBioEditBackup(profileBio);
|
||||
setBioStatusNotice(null);
|
||||
setIsBioEditing(true);
|
||||
};
|
||||
|
||||
const confirmBioEdit = () => {
|
||||
handleBioBlur();
|
||||
setIsBioEditing(false);
|
||||
setBioStatusNotice("个性签名已保存");
|
||||
};
|
||||
|
||||
const cancelBioEdit = () => {
|
||||
setProfileBio(bioEditBackup);
|
||||
setIsBioEditing(false);
|
||||
setBioStatusNotice(null);
|
||||
};
|
||||
|
||||
const renderEmptyState = (text: string, actionLabel: string, action: () => void) => (
|
||||
<div className="profile-page__empty-state">
|
||||
<span className="profile-page__empty-mark" aria-hidden="true">
|
||||
<PlusOutlined />
|
||||
</span>
|
||||
<p className="profile-page__empty-text">{text}</p>
|
||||
<button type="button" className="profile-page__empty-btn" onClick={action}>
|
||||
<PlusOutlined />
|
||||
@@ -543,12 +615,12 @@ function ProfilePage({
|
||||
<article key={task.id} className="profile-page__list-card">
|
||||
<div className="profile-page__list-card-head">
|
||||
<strong>{task.title}</strong>
|
||||
<span>{task.type}</span>
|
||||
<span>{formatTaskType(task.type)}</span>
|
||||
</div>
|
||||
<p>{task.prompt}</p>
|
||||
<div className="profile-page__list-card-meta">
|
||||
<span>{task.status}</span>
|
||||
<span>{task.createdAt}</span>
|
||||
<span>{formatTaskStatus(task.status)}</span>
|
||||
<span>{formatProfileDate(task.createdAt)}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -565,7 +637,7 @@ function ProfilePage({
|
||||
<article key={project.id} className="profile-page__list-card">
|
||||
<div className="profile-page__list-card-head">
|
||||
<strong>{project.name}</strong>
|
||||
<span>{project.updatedAt}</span>
|
||||
<span>{formatProfileDate(project.updatedAt)}</span>
|
||||
{onDeleteProject ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -597,12 +669,12 @@ function ProfilePage({
|
||||
<article key={asset.id} className="profile-page__list-card">
|
||||
<div className="profile-page__list-card-head">
|
||||
<strong>{asset.name}</strong>
|
||||
<span>{asset.status}</span>
|
||||
<span>{formatAssetStatus(asset.status)}</span>
|
||||
</div>
|
||||
<p>{asset.description}</p>
|
||||
<div className="profile-page__list-card-meta">
|
||||
<span>{asset.type}</span>
|
||||
<span>{asset.updatedAt}</span>
|
||||
<span>{formatProfileDate(asset.updatedAt)}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -665,15 +737,39 @@ function ProfilePage({
|
||||
</span>
|
||||
</div>
|
||||
<strong className="profile-page__username">{displayName}</strong>
|
||||
<textarea
|
||||
className="profile-page__bio"
|
||||
value={profileBio}
|
||||
onChange={(event) => setProfileBio(event.target.value)}
|
||||
onBlur={handleBioBlur}
|
||||
placeholder={displayedBio}
|
||||
rows={2}
|
||||
maxLength={80}
|
||||
/>
|
||||
{isBioEditing ? (
|
||||
<div className="profile-page__bio-editor">
|
||||
<textarea
|
||||
className="profile-page__bio"
|
||||
value={profileBio}
|
||||
onChange={(event) => setProfileBio(event.target.value)}
|
||||
placeholder="填写一句个人签名"
|
||||
rows={2}
|
||||
maxLength={80}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="profile-page__bio-actions">
|
||||
<button type="button" className="profile-page__bio-action profile-page__bio-action--save" onClick={confirmBioEdit}>
|
||||
<CheckOutlined />
|
||||
保存
|
||||
</button>
|
||||
<button type="button" className="profile-page__bio-action" onClick={cancelBioEdit}>
|
||||
<CloseOutlined />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`profile-page__bio-display${profileBio.trim() ? "" : " is-empty"}`}
|
||||
onClick={startBioEdit}
|
||||
>
|
||||
<span>{displayedBio}</span>
|
||||
<EditOutlined className="profile-page__bio-edit-icon" />
|
||||
</button>
|
||||
)}
|
||||
{bioStatusNotice ? <span className="profile-page__bio-status">{bioStatusNotice}</span> : null}
|
||||
{profileNotice ? <span className="profile-page__sync-notice">{profileNotice}</span> : null}
|
||||
</div>
|
||||
|
||||
@@ -692,18 +788,21 @@ function ProfilePage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="profile-page__share-btn">
|
||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
||||
<ShareAltOutlined />
|
||||
{packageLabel}
|
||||
</button>
|
||||
|
||||
<button type="button" className="profile-page__share-btn" onClick={onOpenWorkbench}>
|
||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--primary" onClick={onOpenWorkbench}>
|
||||
<PlusOutlined />
|
||||
进入工作台
|
||||
</button>
|
||||
<button type="button" className="profile-page__share-btn" onClick={onOpenCommunity}>
|
||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--secondary" onClick={onOpenCommunity}>
|
||||
<ShareAltOutlined />
|
||||
打开社区
|
||||
</button>
|
||||
<button type="button" className="profile-page__share-btn" onClick={onLogout}>
|
||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--danger" onClick={onLogout}>
|
||||
<LockOutlined />
|
||||
退出登录
|
||||
</button>
|
||||
</aside>
|
||||
@@ -759,13 +858,25 @@ function ProfilePage({
|
||||
<div className="profile-page__upload-card profile-page__upload-card--meta">
|
||||
{accountPanel === "credits" ? (
|
||||
<>
|
||||
<span>当前账号:{displayName}</span>
|
||||
<span>积分剩余:{(usage.balanceCents / 100).toFixed(2)}</span>
|
||||
<span className="profile-page__meta-item">
|
||||
<small>当前账号</small>
|
||||
<strong>{displayName}</strong>
|
||||
</span>
|
||||
<span className="profile-page__meta-item">
|
||||
<small>积分剩余</small>
|
||||
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>任务总数:{tasks.length}</span>
|
||||
<span>已完成:{completedTasks.length}</span>
|
||||
<span className="profile-page__meta-item">
|
||||
<small>任务总数</small>
|
||||
<strong>{tasks.length}</strong>
|
||||
</span>
|
||||
<span className="profile-page__meta-item">
|
||||
<small>已完成</small>
|
||||
<strong>{completedTasks.length}</strong>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -784,12 +895,30 @@ function ProfilePage({
|
||||
<source src={AUTH_SHOWCASE_VIDEO_URL} type="video/mp4" />
|
||||
</video>
|
||||
<div className="auth-page__video-overlay">
|
||||
<h1 className="auth-page__brand">OmniAI</h1>
|
||||
<p className="auth-page__tagline">一句话,从创意到成片</p>
|
||||
<div className="auth-page__features">
|
||||
<span>AI 视频生成</span>
|
||||
<span>AI 图片创作</span>
|
||||
<span>AI 电商素材</span>
|
||||
<div className="auth-page__showcase-content">
|
||||
<div className="auth-page__brand-row">
|
||||
<h1 className="auth-page__brand">OmniAI</h1>
|
||||
</div>
|
||||
<p className="auth-page__tagline">一句话,从创意到成片</p>
|
||||
<div className="auth-page__features">
|
||||
<span>AI 视频生成</span>
|
||||
<span>AI 图片创作</span>
|
||||
<span>AI 电商素材</span>
|
||||
</div>
|
||||
<div className="auth-page__showcase-stats" aria-label="平台能力">
|
||||
<span>
|
||||
<strong>Studio</strong>
|
||||
创作工作台
|
||||
</span>
|
||||
<span>
|
||||
<strong>Assets</strong>
|
||||
资产沉淀
|
||||
</span>
|
||||
<span>
|
||||
<strong>Team</strong>
|
||||
团队协作
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -840,7 +969,8 @@ function ProfilePage({
|
||||
<MailOutlined /> 邮箱
|
||||
</button>
|
||||
<button type="button" className={authTab === "phone" ? "is-active" : ""} onClick={() => { setAuthTab("phone"); setFieldErrors({}); }}>
|
||||
<MobileOutlined /> 手机验证码
|
||||
<MobileOutlined />
|
||||
<span className="auth-page__tab-label-short">手机</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -920,6 +1050,11 @@ function ProfilePage({
|
||||
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
||||
/>
|
||||
{fieldErrors.password ? <span className="auth-page__field-error">{fieldErrors.password}</span> : null}
|
||||
{mode === "register" && passwordLooksReady && !fieldErrors.password ? (
|
||||
<span className="auth-page__field-hint">
|
||||
<CheckCircleFilled /> 密码长度符合要求
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
{mode === "login" ? (
|
||||
<div className="auth-page__forgot">
|
||||
@@ -957,6 +1092,11 @@ function ProfilePage({
|
||||
autoComplete="email"
|
||||
/>
|
||||
{fieldErrors.email ? <span className="auth-page__field-error">{fieldErrors.email}</span> : null}
|
||||
{emailLooksValid && !fieldErrors.email ? (
|
||||
<span className="auth-page__field-hint">
|
||||
<CheckCircleFilled /> 邮箱格式正确
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label className={`auth-page__field${fieldErrors.password ? " auth-page__field--error" : ""}`}>
|
||||
<span>
|
||||
@@ -971,6 +1111,11 @@ function ProfilePage({
|
||||
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
||||
/>
|
||||
{fieldErrors.password ? <span className="auth-page__field-error">{fieldErrors.password}</span> : null}
|
||||
{mode === "register" && passwordLooksReady && !fieldErrors.password ? (
|
||||
<span className="auth-page__field-hint">
|
||||
<CheckCircleFilled /> 密码长度符合要求
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
@@ -986,6 +1131,11 @@ function ProfilePage({
|
||||
<input type="tel" value={phone} onChange={(event) => { setPhone(event.target.value); clearFieldError("phone"); }} onBlur={() => handleFieldBlur("phone", phone)} placeholder="输入手机号" autoComplete="tel" />
|
||||
</div>
|
||||
{fieldErrors.phone ? <span className="auth-page__field-error">{fieldErrors.phone}</span> : null}
|
||||
{phoneLooksValid && !fieldErrors.phone ? (
|
||||
<span className="auth-page__field-hint">
|
||||
<CheckCircleFilled /> 手机号格式正确
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label className={`auth-page__field${fieldErrors.smsCode ? " auth-page__field--error" : ""}`}>
|
||||
<span>
|
||||
@@ -1011,6 +1161,11 @@ function ProfilePage({
|
||||
</span>
|
||||
<input type="password" value={password} onChange={(event) => { setPassword(event.target.value); clearFieldError("password"); }} onBlur={() => handleFieldBlur("password", password)} placeholder="至少 6 位" autoComplete="new-password" />
|
||||
{fieldErrors.password ? <span className="auth-page__field-error">{fieldErrors.password}</span> : null}
|
||||
{passwordLooksReady && !fieldErrors.password ? (
|
||||
<span className="auth-page__field-hint">
|
||||
<CheckCircleFilled /> 密码长度符合要求
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user