8ff01d65dd
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
照需求文档「Fork 工程确认页(客户端)」新增独立确认页,把原来的「列表上直接 Fork」升级成 「先确认授权边界,再同步工程」。 - 父作品信息卡:封面(复用已有的有界 data URL 链路,失败/缺封面回落首字占位)、标题、作者、 当前授权类型(缺省读作「禁止共创」);数据优先用列表已加载的那份,缺了才按 gameId 走公开详情 接口,**逐项降级不报错**。 - 二次确认 + 授权须知 + 风险说明(**强制阅读 3 秒**):五类内容逐条全文可见(可滚动,不折叠), 用已统一的 Fork 口径;商用权限按父作品授权档位分档写(非商用档明确「不得用于盈利」,全开放档 写明可商用);底部确认按钮在前 3 秒禁用并显示倒计时与进度条。 - 工程同步状态条:**只显示原生真实阶段**(`game-fork-progress`:取回作品内容 → 校验摘要与字节 → 解压到项目 → 生成工程与来源记录),事件到哪一步点亮哪一步,阶段内部不可测量就只做「进行中」 指示,**没有百分比动画**。需求文档的四分类(源码/素材/场景/配置)假设按资产种类分别同步,而这条 链路是整包取回,因此如实降级并在页面上写明每个阶段在做什么。 - 底部「取消 / 确认并进入编辑」:确认 → 走既有取件建项链路 → 成功后由上层进入工作台;取消 → 回到 进入前的界面(列表)。 - 落点与入口:共创列表的卡片与「粘贴作品 ID/链接」都改为进确认页(粘贴仍用同一份解析);深链 `genarrative://fork?gameId=…` 直接打开确认页并预填父作品(带错误原因的深链仍留在列表展示)。 测试:新增 `forkConfirm.test.tsx`(10 条:信息卡四字段 + 缺字段降级、3 秒闸门与倒计时与可点、 五类须知全文与关键口径、全开放档商用文案、真实阶段点亮与未知阶段丢弃、确认后的命令参数与进工作台、 失败展示与不进工作台、取消回上层、无目标时的可读提示);列表测试改为断言「只打开确认页、不直接建项」; appSurface 增一条「深链落确认页并预填」。
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
import { openUrl } from '@tauri-apps/plugin-opener';
|
|
import {
|
|
CircleHelp,
|
|
FolderKanban,
|
|
GitFork,
|
|
Home,
|
|
LayoutTemplate,
|
|
Plus,
|
|
Settings,
|
|
User,
|
|
} from 'lucide-react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
import BRAND_ICON from '../../../../packages/shared/src/icons/taonier-product-ip.png';
|
|
import { useWalletStore } from '../stores/useWalletStore';
|
|
|
|
function cx(...classNames: Array<string | false | null | undefined>) {
|
|
return classNames.filter(Boolean).join(' ');
|
|
}
|
|
|
|
const sidebarIconButtonClass =
|
|
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text) hover:bg-(--platform-nav-item-hover-fill)';
|
|
const sidebarHelpButtonClass =
|
|
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text)';
|
|
const sidebarMenuItemClass =
|
|
'flex h-[30px] w-full cursor-pointer items-center justify-start gap-2 whitespace-nowrap rounded-[7px] border-0 bg-transparent px-[9px] py-0 text-left text-xs text-(--platform-text-strong) hover:bg-(--platform-nav-item-hover-fill) focus-visible:bg-(--platform-nav-item-hover-fill)';
|
|
|
|
export type LauncherView =
|
|
| 'home'
|
|
| 'projects'
|
|
| 'guide'
|
|
| 'contact'
|
|
| 'news'
|
|
| 'co-creation'
|
|
| 'fork-confirm'
|
|
| 'template-library'
|
|
| 'project-development';
|
|
|
|
type SidebarUserInfo = {
|
|
displayName?: string | null;
|
|
phoneNumberMasked?: string | null;
|
|
publicUserCode?: string | null;
|
|
};
|
|
|
|
type LauncherSidebarProps = {
|
|
activeView: LauncherView;
|
|
templateLibraryEnabled: boolean;
|
|
currentUser: SidebarUserInfo;
|
|
onViewChange: (view: LauncherView) => void;
|
|
onNoticeRequest: (title: string) => void;
|
|
onRechargeRequest: () => void;
|
|
onReferralRequest: (panel: 'invite' | 'community') => void;
|
|
onRuntimeConfigOpen: () => void;
|
|
onLogout: () => void;
|
|
};
|
|
|
|
type SidebarAccountMenuProps = {
|
|
currentUser: SidebarUserInfo;
|
|
onClose: () => void;
|
|
onLogout: () => void;
|
|
onNoticeRequest: (title: string) => void;
|
|
onRechargeRequest: () => void;
|
|
onReferralRequest: (panel: 'invite' | 'community') => void;
|
|
};
|
|
|
|
function formatMudPoints(value: number | null | undefined) {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
return '--';
|
|
}
|
|
return Math.max(0, Math.floor(value)).toLocaleString('zh-CN');
|
|
}
|
|
|
|
function SidebarAccountMenu({
|
|
currentUser,
|
|
onClose,
|
|
onLogout,
|
|
onNoticeRequest,
|
|
onRechargeRequest,
|
|
onReferralRequest,
|
|
}: SidebarAccountMenuProps) {
|
|
const walletBalance = useWalletStore(
|
|
(state) => state.mudPointBalance?.totalPoints ?? null,
|
|
);
|
|
const walletBalanceError = useWalletStore(
|
|
(state) => state.mudPointBalanceError || null,
|
|
);
|
|
const walletBalanceLabel = formatMudPoints(walletBalance);
|
|
const walletBalanceStatus = walletBalanceError
|
|
? walletBalanceError
|
|
: walletBalance !== null
|
|
? '泥点余额已读取'
|
|
: '正在读取泥点余额';
|
|
|
|
function showNotice(title: string) {
|
|
onClose();
|
|
onNoticeRequest(title);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="absolute -bottom-1 left-14 z-90 grid w-54.5 gap-1.5 rounded-lg border border-(--platform-subpanel-border) [background:var(--platform-subpanel-fill)] p-2.5 text-(--platform-text-strong)"
|
|
role="menu"
|
|
>
|
|
<strong className="text-[13px]">
|
|
{currentUser.displayName || '陶泥儿用户'}
|
|
</strong>
|
|
<span className="text-[11px] text-(--platform-text-soft)">
|
|
{currentUser.phoneNumberMasked ||
|
|
currentUser.publicUserCode ||
|
|
'账号信息读取中'}
|
|
</span>
|
|
<div
|
|
className="grid gap-2 rounded-xl bg-[linear-gradient(135deg,#eaa06a,#cf7a4a_58%,#b55c3b)] p-3 text-white shadow-[0_18px_38px_rgb(189_103_60_/_22%)]"
|
|
role="group"
|
|
aria-label="会员方案"
|
|
>
|
|
{/*SHOULD NOT SHOW IN THIS VERSION*/}
|
|
{/*<span className="grid size-8 shrink-0 place-items-center rounded-[0.85rem] bg-white/25">*/}
|
|
{/* <Crown size={16} aria-hidden="true" />*/}
|
|
{/*</span>*/}
|
|
{/*<span className="min-w-0">*/}
|
|
{/* <span className="block truncate text-sm font-black">*/}
|
|
{/* /!*{membershipLabel}*!/*/}
|
|
{/* </span>*/}
|
|
{/* <span className="block truncate text-[11px] font-semibold text-white/85">*/}
|
|
{/* 会员方案*/}
|
|
{/* </span>*/}
|
|
{/*</span>*/}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span
|
|
className="min-w-0 text-[12px] font-bold"
|
|
title={walletBalanceStatus}
|
|
>
|
|
剩余泥点 {walletBalanceLabel}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="grid size-7 shrink-0 place-items-center rounded-full border border-white/70 bg-white/15 text-white"
|
|
aria-label="充值泥点"
|
|
onClick={() => {
|
|
onClose();
|
|
onRechargeRequest();
|
|
}}
|
|
>
|
|
<Plus size={14} aria-hidden="true" />
|
|
</button>
|
|
{/*<button*/}
|
|
{/* type="button"*/}
|
|
{/* className="h-8 rounded-full border border-white/85 bg-white/10 px-3 text-[12px] font-black text-white"*/}
|
|
{/* onClick={() => {*/}
|
|
{/* closeMenus();*/}
|
|
{/* // TODO upgrade component and logic is outdated on this branch*/}
|
|
{/* }}*/}
|
|
{/*>*/}
|
|
{/* 升级*/}
|
|
{/*</button>*/}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className={sidebarMenuItemClass}
|
|
onClick={
|
|
() => {
|
|
onClose();
|
|
showNotice('使用指南');
|
|
}
|
|
// TODO doc is not ready
|
|
}
|
|
>
|
|
使用指南
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className={sidebarMenuItemClass}
|
|
onClick={() => {
|
|
onClose();
|
|
onReferralRequest('invite');
|
|
}}
|
|
>
|
|
邀请码
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className={sidebarMenuItemClass}
|
|
onClick={() => {
|
|
onClose();
|
|
onReferralRequest('community');
|
|
}}
|
|
>
|
|
玩家社区
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className={sidebarMenuItemClass}
|
|
onClick={() =>
|
|
openUrl(
|
|
'https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k',
|
|
)
|
|
}
|
|
>
|
|
反馈联系
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className={sidebarMenuItemClass}
|
|
onClick={() => {
|
|
onClose();
|
|
onLogout();
|
|
}}
|
|
>
|
|
退出登录
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Sidebar({
|
|
activeView,
|
|
templateLibraryEnabled,
|
|
currentUser,
|
|
onViewChange,
|
|
onNoticeRequest,
|
|
onRechargeRequest,
|
|
onReferralRequest,
|
|
onRuntimeConfigOpen,
|
|
onLogout,
|
|
}: LauncherSidebarProps) {
|
|
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
|
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
|
const helpMenuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
function closeSidebarMenusIfOutside(target: EventTarget | null) {
|
|
if (!(target instanceof Node)) {
|
|
return;
|
|
}
|
|
if (
|
|
accountMenuRef.current?.contains(target) ||
|
|
helpMenuRef.current?.contains(target)
|
|
) {
|
|
return;
|
|
}
|
|
setAccountMenuOpen(false);
|
|
}
|
|
|
|
function handlePointerDown(event: PointerEvent) {
|
|
closeSidebarMenusIfOutside(event.target);
|
|
}
|
|
|
|
function handleFocusIn(event: FocusEvent) {
|
|
closeSidebarMenusIfOutside(event.target);
|
|
}
|
|
|
|
function handleWindowBlur() {
|
|
setAccountMenuOpen(false);
|
|
}
|
|
|
|
document.addEventListener('pointerdown', handlePointerDown);
|
|
document.addEventListener('focusin', handleFocusIn);
|
|
window.addEventListener('blur', handleWindowBlur);
|
|
return () => {
|
|
document.removeEventListener('pointerdown', handlePointerDown);
|
|
document.removeEventListener('focusin', handleFocusIn);
|
|
window.removeEventListener('blur', handleWindowBlur);
|
|
};
|
|
}, []);
|
|
|
|
function closeMenus() {
|
|
setAccountMenuOpen(false);
|
|
}
|
|
|
|
function changeView(view: LauncherView) {
|
|
closeMenus();
|
|
onViewChange(view);
|
|
}
|
|
|
|
return (
|
|
<aside className="sticky top-0 z-40 flex h-screen w-full flex-col items-center gap-4 overflow-visible border-r border-(--platform-surface-border) bg-(--platform-nav-fill) px-0 pb-4.5 pt-4">
|
|
<div className="grid place-items-center" aria-label="陶泥儿">
|
|
<span className="grid size-12 place-items-center rounded-full bg-(--platform-button-primary-fill) text-[10px] font-bold tracking-normal text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow)">
|
|
<img src={BRAND_ICON} alt="brand logo" />
|
|
</span>
|
|
</div>
|
|
<nav
|
|
className="grid justify-items-center gap-3.25"
|
|
aria-label="客户端导航"
|
|
>
|
|
<button
|
|
type="button"
|
|
aria-label="首页"
|
|
title="首页"
|
|
className={cx(
|
|
sidebarIconButtonClass,
|
|
activeView === 'home' &&
|
|
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
|
)}
|
|
onClick={() => changeView('home')}
|
|
>
|
|
<Home size={17} aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="项目组"
|
|
title="项目组"
|
|
className={cx(
|
|
sidebarIconButtonClass,
|
|
activeView === 'projects' &&
|
|
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
|
)}
|
|
onClick={() => changeView('projects')}
|
|
>
|
|
<FolderKanban size={17} aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="共创"
|
|
title="共创"
|
|
className={cx(
|
|
sidebarIconButtonClass,
|
|
activeView === 'co-creation' &&
|
|
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
|
)}
|
|
onClick={() => changeView('co-creation')}
|
|
>
|
|
<GitFork size={17} aria-hidden="true" />
|
|
</button>
|
|
{templateLibraryEnabled ? (
|
|
<button
|
|
type="button"
|
|
aria-label="模板库"
|
|
title="模板库"
|
|
className={cx(
|
|
sidebarIconButtonClass,
|
|
activeView === 'template-library' &&
|
|
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
|
)}
|
|
onClick={() => changeView('template-library')}
|
|
>
|
|
<LayoutTemplate size={17} aria-hidden="true" />
|
|
</button>
|
|
) : null}
|
|
</nav>
|
|
<div className="relative z-[60] mt-auto grid justify-items-center gap-[13px]">
|
|
<div className="relative" ref={accountMenuRef}>
|
|
<button
|
|
type="button"
|
|
className={sidebarIconButtonClass}
|
|
onClick={() => {
|
|
setAccountMenuOpen((open) => !open);
|
|
}}
|
|
aria-expanded={accountMenuOpen}
|
|
aria-label="我的"
|
|
title="我的"
|
|
>
|
|
<User size={16} aria-hidden="true" />
|
|
</button>
|
|
{accountMenuOpen ? (
|
|
<SidebarAccountMenu
|
|
currentUser={currentUser}
|
|
onClose={closeMenus}
|
|
onLogout={onLogout}
|
|
onNoticeRequest={onNoticeRequest}
|
|
onRechargeRequest={onRechargeRequest}
|
|
onReferralRequest={onReferralRequest}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
<div className="relative" ref={helpMenuRef}>
|
|
<button
|
|
type="button"
|
|
aria-label="帮助"
|
|
title="帮助"
|
|
className={sidebarHelpButtonClass}
|
|
onClick={() => {
|
|
closeMenus();
|
|
onNoticeRequest('使用指南');
|
|
// TODO open the guide
|
|
// TODO doc is not ready
|
|
}}
|
|
>
|
|
<CircleHelp size={17} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={sidebarIconButtonClass}
|
|
onClick={() => {
|
|
closeMenus();
|
|
onRuntimeConfigOpen();
|
|
}}
|
|
aria-label="配置"
|
|
title="配置"
|
|
>
|
|
<Settings size={16} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|