Files
Genarrative/src/components/image-editor/ImageCanvasTopbarView.tsx
T
AIGameCreator App a36290b2e5
Project CI / Frontend tests (push) Failing after 27s
Project CI / Repository checks (push) Successful in 3m20s
Project CI / Backend tests (push) Successful in 3m52s
Project CI / Native shell tests (push) Successful in 13m19s
合并AI游戏创作智能体与通用Runtime基座
合并 codex/ai-game-creator-app 的独立客户端、多智能体 Runtime、Runner 与可扩展 LLM Provider 能力
保留 master 最新画布、钱包、后端和原生壳约束并完成四处语义化冲突合并
补齐根 Vitest 对独立 Tauri guest 模块的隔离测试别名并保持 HostBridge 依赖边界
统一正式工作台与 game-chat 的安全本地预览组件及 native-shell 静态门禁
修复合并态 Rust 格式、前端 lint 与共享文档验证口径
2026-07-30 18:01:19 +08:00

261 lines
8.4 KiB
TypeScript

import {
Check,
ChevronLeft,
Download,
Keyboard,
Pencil,
X,
} from 'lucide-react';
import { useState } from 'react';
import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry';
import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformTextField } from '../common/PlatformTextField';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import type { AssetExportStatus } from './useImageCanvasAssetExportWorkflow';
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
export type ImageCanvasTopbarViewProps = {
projectTitle: string;
projectRenameValue: string;
isRenamingProject: boolean;
isProjectRenameSaving: boolean;
projectRenameError: string | null;
layers: CanvasLayer[];
walletBalance: number | null;
walletBreakdown?: ProfileMudPointBalance | null;
isWalletBalanceLoading: boolean;
isWalletDetailsLoading: boolean;
walletDetailsError: string | null;
currentUser:
| {
id: string;
publicUserCode: string;
displayName: string;
avatarUrl: string | null;
}
| null
| undefined;
assetExportStatus: AssetExportStatus | null;
isExportingAssets: boolean;
setProjectRenameValue: (value: string) => void;
startProjectRename: () => void;
cancelProjectRename: () => void;
submitProjectRename: (projectId: string | null) => void;
resetProjectRenameError: () => void;
exportCanvasAssets: () => void | Promise<void>;
onOpenShortcuts: () => void;
onRequestWalletDetails: () => void;
onRecharge: () => void;
onOpenWalletLedger: () => void;
onOpenAccount: () => void;
onReturnToProjects?: () => void | Promise<void>;
};
function buildCanvasUserCode(user: ImageCanvasTopbarViewProps['currentUser']) {
if (user?.publicUserCode?.trim()) {
return user.publicUserCode.trim();
}
const raw =
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() || '00000000';
return `SY-${raw.slice(-8).padStart(8, '0')}`;
}
export function ImageCanvasTopbarView({
projectTitle,
projectRenameValue,
isRenamingProject,
isProjectRenameSaving,
projectRenameError,
layers,
walletBalance,
walletBreakdown,
isWalletBalanceLoading,
isWalletDetailsLoading,
walletDetailsError,
currentUser,
assetExportStatus,
isExportingAssets,
setProjectRenameValue,
startProjectRename,
cancelProjectRename,
submitProjectRename,
resetProjectRenameError,
exportCanvasAssets,
onOpenShortcuts,
onRequestWalletDetails,
onRecharge,
onOpenWalletLedger,
onOpenAccount,
onReturnToProjects,
}: ImageCanvasTopbarViewProps) {
const [isReturningToProjects, setIsReturningToProjects] = useState(false);
const projectId = useImageCanvasContextStore((state) => state.projectId);
const hasExportableLayer = layers.some(
(layer) => layer.src.trim().length > 0,
);
const userDisplayName = currentUser?.displayName?.trim() || '登录';
const userAvatarUrl = currentUser?.avatarUrl?.trim() || null;
const userAvatarLabel = userDisplayName.slice(0, 1).toUpperCase();
const userCodeLabel = currentUser
? buildCanvasUserCode(currentUser)
: '账号入口';
return (
<div className="image-canvas-editor__topbar">
<div className="image-canvas-editor__topbar-project">
<a
className="image-canvas-editor__project-back-button"
href="/project"
aria-label="返回项目页面"
aria-disabled={isReturningToProjects}
title="返回项目"
onClick={(event) => {
if (!onReturnToProjects) {
return;
}
event.preventDefault();
if (isReturningToProjects) {
return;
}
setIsReturningToProjects(true);
void Promise.resolve(onReturnToProjects()).finally(() => {
setIsReturningToProjects(false);
});
}}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</a>
<div className="image-canvas-editor__title-block">
{isRenamingProject ? (
<form
className="image-canvas-editor__project-title-form"
onSubmit={(event) => {
event.preventDefault();
submitProjectRename(projectId);
}}
>
<PlatformTextField
aria-label="项目名称"
value={projectRenameValue}
autoFocus
disabled={isProjectRenameSaving}
className="image-canvas-editor__project-title-input"
onChange={(event) => {
setProjectRenameValue(event.target.value);
resetProjectRenameError();
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
cancelProjectRename();
}
}}
/>
<EditorIconButton
type="submit"
label="保存项目名称"
title="保存"
icon={Check}
disabled={isProjectRenameSaving}
/>
<EditorIconButton
label="取消修改项目名称"
title="取消"
icon={X}
disabled={isProjectRenameSaving}
onClick={cancelProjectRename}
/>
{projectRenameError ? (
<span
className="image-canvas-editor__project-title-error"
role="alert"
>
{projectRenameError}
</span>
) : null}
</form>
) : (
<div className="image-canvas-editor__project-title-row">
<button
type="button"
className="image-canvas-editor__project-title-button"
onDoubleClick={startProjectRename}
aria-label={`编辑项目名称${projectTitle}`}
>
<h1>{projectTitle}</h1>
</button>
<EditorIconButton
className="image-canvas-editor__project-rename-button"
label="编辑项目名称"
title="编辑项目名称"
icon={Pencil}
onClick={startProjectRename}
/>
</div>
)}
</div>
</div>
<div className="image-canvas-editor__topbar-actions">
<EditorIconButton
label="查看画布快捷键"
title="快捷键"
icon={Keyboard}
onClick={onOpenShortcuts}
/>
<EditorIconButton
label="下载画布素材"
title="下载画布素材"
icon={Download}
disabled={isExportingAssets || !hasExportableLayer}
onClick={() => void exportCanvasAssets()}
/>
{assetExportStatus ? (
<PlatformStatusMessage
tone={assetExportStatus.tone}
surface="platform"
size="xs"
role={assetExportStatus.tone === 'error' ? 'alert' : 'status'}
>
{assetExportStatus.message}
</PlatformStatusMessage>
) : null}
<PlatformMudPointWalletEntry
variant="editor"
balance={walletBalance}
breakdown={walletBreakdown}
isLoading={isWalletBalanceLoading || isWalletDetailsLoading}
error={walletDetailsError}
onRequestDetails={onRequestWalletDetails}
onRecharge={onRecharge}
onOpenLedger={onOpenWalletLedger}
/>
<button
type="button"
className="image-canvas-editor__account-chip"
onClick={onOpenAccount}
aria-label={`账号 ${userDisplayName}`}
>
<span
className="image-canvas-editor__account-avatar"
aria-hidden="true"
>
{userAvatarUrl ? (
<img src={userAvatarUrl} alt="" draggable={false} />
) : (
userAvatarLabel
)}
</span>
<span className="image-canvas-editor__account-meta">
<span>{userDisplayName}</span>
<span>{userCodeLabel}</span>
</span>
</button>
</div>
</div>
);
}