a36290b2e5
合并 codex/ai-game-creator-app 的独立客户端、多智能体 Runtime、Runner 与可扩展 LLM Provider 能力 保留 master 最新画布、钱包、后端和原生壳约束并完成四处语义化冲突合并 补齐根 Vitest 对独立 Tauri guest 模块的隔离测试别名并保持 HostBridge 依赖边界 统一正式工作台与 game-chat 的安全本地预览组件及 native-shell 静态门禁 修复合并态 Rust 格式、前端 lint 与共享文档验证口径
261 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|