网页侧统一改名:Fork 动作名一律叫开始共创(详情页主操作/信息面板此版本/提示文案/统计口径 + 组件测试)
This commit is contained in:
@@ -751,7 +751,7 @@ function GameDetailContent({
|
||||
if (isMobileViewport) {
|
||||
setIsForkPanelOpen(true);
|
||||
setForkLaunchNotice(
|
||||
'当前设备无法直接唤起客户端,请用下面的作品 ID 或作品链接在客户端里 Fork。',
|
||||
'当前设备无法直接唤起客户端,请用下面的作品 ID 或作品链接在客户端里开始共创。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -761,7 +761,7 @@ function GameDetailContent({
|
||||
const handOffToClient = () => {
|
||||
clearForkLaunch();
|
||||
setForkLaunchNotice(
|
||||
'已唤起陶泥儿客户端。若没有自动打开,请再点一次「Fork 这个作品」手动获取作品 ID。',
|
||||
'已唤起陶泥儿客户端。若没有自动打开,请再点一次「开始共创」手动获取作品 ID。',
|
||||
);
|
||||
};
|
||||
forkLaunchHandlersRef.current = handOffToClient;
|
||||
@@ -982,7 +982,7 @@ function GameDetailContent({
|
||||
) : null}
|
||||
{publiclyVisible ? (
|
||||
/*
|
||||
主操作「一键 Fork 共创」(文档口径):**始终显示**。
|
||||
主操作「一键开始共创」(文档口径):**始终显示**。
|
||||
- 授权为「禁止共创」时置灰(aria-disabled + 视觉弱化)并在悬停/点击给出原因,
|
||||
不再静默隐藏;
|
||||
- 未登录按仓库既有登录门禁:入口照常出现,点击走登录弹窗;
|
||||
@@ -1009,13 +1009,13 @@ function GameDetailContent({
|
||||
}
|
||||
title={
|
||||
forkAuthorization === 'forbidden'
|
||||
? '作者未开放共创授权(禁止共创),无法 Fork 这部作品'
|
||||
? '作者未开放共创授权(禁止共创),无法开始共创这部作品'
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (forkAuthorization === 'forbidden') {
|
||||
setForkLaunchNotice(
|
||||
'作者未开放共创授权(禁止共创),无法 Fork 这部作品。',
|
||||
'作者未开放共创授权(禁止共创),无法开始共创这部作品。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1026,7 +1026,7 @@ function GameDetailContent({
|
||||
handleForkLaunch();
|
||||
}}
|
||||
>
|
||||
{isAuthenticated ? '一键 Fork 共创' : '登录后一键 Fork'}
|
||||
{isAuthenticated ? '一键开始共创' : '登录后一键开始共创'}
|
||||
</button>
|
||||
{(isAuthenticated || forkAuthorization === 'forbidden') &&
|
||||
forkLaunchNotice ? (
|
||||
@@ -1068,8 +1068,7 @@ function GameDetailContent({
|
||||
/>
|
||||
</span>
|
||||
<span className="game-fork-remix-note">
|
||||
Fork
|
||||
会把这部作品的整个工程(含素材与文档)复制到你的项目里继续改造;
|
||||
开始共创会把这部作品的整个工程(含素材与文档)复制到你的项目里继续改造;
|
||||
复制后对他人可见,且不可撤销。
|
||||
</span>
|
||||
{forkIdCopyState === 'copied' ? (
|
||||
|
||||
@@ -118,7 +118,7 @@ vi.mock('../../services/clipboard', () => ({
|
||||
copyTextToClipboard: copyTextToClipboardMock,
|
||||
}));
|
||||
|
||||
/** Fork 唤起:只打桩「发出唤起」这一动作,协议常量仍用真实值(测试按同一阈值推进定时器)。 */
|
||||
/** 开始共创唤起:只打桩「发出唤起」这一动作,协议常量仍用真实值(测试按同一阈值推进定时器)。 */
|
||||
const openForkProtocolMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./gameForkProtocol', async (original) => ({
|
||||
@@ -511,7 +511,7 @@ describe('GameDetailPage', () => {
|
||||
expect(await screen.findByText('暂未发布可玩版本')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('展示共创授权、代际与溯源信息,并给出 Fork 入口', async () => {
|
||||
it('展示共创授权、代际与溯源信息,并给出「一键开始共创」入口', async () => {
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({
|
||||
forkAuthorization: 'nonCommercial',
|
||||
@@ -534,8 +534,8 @@ describe('GameDetailPage', () => {
|
||||
expect(screen.getByText('2 代三创 · 改编自父代《原初之光》')).toBeTruthy();
|
||||
expect(screen.getByText('父代作者 原作者')).toBeTruthy();
|
||||
expect(screen.getByText('累计衍生作品 3')).toBeTruthy();
|
||||
// 已公开 + 授权开放 → 「一键 Fork 共创」入口出现(M1 时它还不存在,这里钉住新契约)。
|
||||
expect(screen.getByRole('button', { name: '一键 Fork 共创' })).toBeTruthy();
|
||||
// 已公开 + 授权开放 → 「一键开始共创」入口出现(M1 时它还不存在,这里钉住新契约)。
|
||||
expect(screen.getByRole('button', { name: '一键开始共创' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('旧数据缺少授权与血缘字段时只按禁止共创渲染', async () => {
|
||||
@@ -1879,7 +1879,7 @@ describe('GameLineagePage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameDetailPage Fork 入口', () => {
|
||||
describe('GameDetailPage 开始共创入口', () => {
|
||||
it('已公开且授权开放时出现入口;唤起没被接手时降级给出可复制的作品 ID 与指引', async () => {
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({ forkAuthorization: 'nonCommercial' }),
|
||||
@@ -1888,7 +1888,7 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
const entry = await screen.findByRole('button', { name: '一键 Fork 共创' });
|
||||
const entry = await screen.findByRole('button', { name: '一键开始共创' });
|
||||
// 未展开前不露出获取方式。
|
||||
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
|
||||
|
||||
@@ -1912,7 +1912,7 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('game-1')).toBeTruthy();
|
||||
expect(screen.getByText(/\/games\/detail\?id=game-1$/u)).toBeTruthy();
|
||||
// 冻结口径:Fork = 复制整个工程继续改造;只保留一句话告知(对他人可见 + 不可撤销)。
|
||||
// 冻结口径:开始共创 = 复制整个工程继续改造;只保留一句话告知(对他人可见 + 不可撤销)。
|
||||
expect(
|
||||
screen.getByText(/整个工程(含素材与文档)复制到你的项目里继续改造/u),
|
||||
).toBeTruthy();
|
||||
@@ -1940,7 +1940,7 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
const entry = await screen.findByRole('button', { name: '一键 Fork 共创' });
|
||||
const entry = await screen.findByRole('button', { name: '一键开始共创' });
|
||||
vi.useFakeTimers();
|
||||
fireEvent.click(entry);
|
||||
expect(openForkProtocolMock).toHaveBeenCalledWith('game-1');
|
||||
@@ -1970,7 +1970,7 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '一键 Fork 共创' }),
|
||||
await screen.findByRole('button', { name: '一键开始共创' }),
|
||||
);
|
||||
|
||||
expect(openForkProtocolMock).not.toHaveBeenCalled();
|
||||
@@ -1988,7 +1988,7 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
);
|
||||
|
||||
const blocked = await screen.findByRole('button', {
|
||||
name: '一键 Fork 共创',
|
||||
name: '一键开始共创',
|
||||
});
|
||||
// 不静默隐藏:按钮在,但是 aria-disabled + 有原因(悬停 title)。
|
||||
expect(blocked.getAttribute('aria-disabled')).toBe('true');
|
||||
@@ -1997,13 +1997,13 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
fireEvent.click(blocked);
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/作者未开放共创授权(禁止共创),无法 Fork 这部作品。/u,
|
||||
/作者未开放共创授权(禁止共创),无法开始共创这部作品。/u,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(openForkProtocolMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('未公开作品不出现 Fork 入口', async () => {
|
||||
it('未公开作品不出现开始共创入口', async () => {
|
||||
getMyGameMock.mockResolvedValueOnce({
|
||||
...createGame({ status: 'unpublished', forkAuthorization: 'full' }),
|
||||
latestVersion: null,
|
||||
@@ -2019,13 +2019,13 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
);
|
||||
|
||||
await screen.findByText('作品状态');
|
||||
expect(screen.queryByRole('button', { name: '一键 Fork 共创' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '一键开始共创' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '登录后一键 Fork' }),
|
||||
screen.queryByRole('button', { name: '登录后一键开始共创' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('未登录时入口显示「登录后一键 Fork」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
|
||||
it('未登录时入口显示「登录后一键开始共创」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
|
||||
authUiMock.value.user = null;
|
||||
authUiMock.value.canAccessProtectedData = false;
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
@@ -2035,7 +2035,9 @@ describe('GameDetailPage Fork 入口', () => {
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
const gate = await screen.findByRole('button', { name: '登录后一键 Fork' });
|
||||
const gate = await screen.findByRole('button', {
|
||||
name: '登录后一键开始共创',
|
||||
});
|
||||
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
|
||||
|
||||
fireEvent.click(gate);
|
||||
@@ -3198,7 +3200,7 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
expect(outlineCss).toMatch(
|
||||
/\.lineage-outline__action--co-create \{[\s\S]{0,200}?--platform-button-primary-solid/u,
|
||||
);
|
||||
// 全树的 Fork 旧文案在两种形态里都已不存在(信息面板的「Fork 此版本」不在树节点上,另见报告)。
|
||||
// 全树的 Fork 旧文案在两种形态里都已不存在(信息面板的「开始共创此版本」不在树节点上)。
|
||||
expect(outline.textContent).not.toContain('Fork');
|
||||
});
|
||||
|
||||
@@ -3473,7 +3475,7 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
});
|
||||
expect(screen.getByText(/没有检测到陶泥儿客户端/u)).toBeTruthy();
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /复制 game-leaf 去 Fork/u }),
|
||||
screen.getByRole('button', { name: /复制 game-leaf 去开始共创/u }),
|
||||
);
|
||||
await act(async () => {});
|
||||
expect(copyTextToClipboardMock).toHaveBeenCalledWith('game-leaf');
|
||||
@@ -3508,7 +3510,7 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
expect(stubCard.getAttribute('tabindex')).toBe('0');
|
||||
expect(stubCard.getAttribute('role')).toBeNull();
|
||||
expect(stubCard.getAttribute('aria-disabled')).toBe('true');
|
||||
// 被删作品只以「不可用」形式出现:没有标题、没有作者名、也没有 Fork/对比动作。
|
||||
// 被删作品只以「不可用」形式出现:没有标题、没有作者名、也没有开始共创/对比动作。
|
||||
expect(document.querySelector('.game-lineage-node--stub')).toBeTruthy();
|
||||
expect(screen.queryByText(/作者 game-deleted/u)).toBeNull();
|
||||
expect(within(stubCard).queryByRole('button')).toBeNull();
|
||||
@@ -3694,7 +3696,7 @@ describe('GameLineagePage 筛选器 / 主干线 / 信息面板(文档对齐)
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('选中节点后右侧信息面板给出简介/授权与「前往体验 / Fork 此版本」', async () => {
|
||||
it('选中节点后右侧信息面板给出简介/授权与「前往体验 / 开始共创此版本」', async () => {
|
||||
getGameLineageMock.mockResolvedValueOnce(tree);
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({
|
||||
@@ -3735,7 +3737,14 @@ describe('GameLineagePage 筛选器 / 主干线 / 信息面板(文档对齐)
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '前往体验' }));
|
||||
expect(onOpenDetail).toHaveBeenCalledWith('hot');
|
||||
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'Fork 此版本' }));
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: '开始共创此版本' }),
|
||||
).toBeTruthy();
|
||||
// 面板内不得再把该动作叫 Fork(与按钮可见名一致)。
|
||||
expect(panel.textContent).not.toContain('Fork');
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', { name: '开始共创此版本' }),
|
||||
);
|
||||
expect(openForkProtocolMock).toHaveBeenCalledWith('hot');
|
||||
});
|
||||
|
||||
@@ -3846,8 +3855,10 @@ describe('GameLineagePage 筛选器 / 主干线 / 信息面板(文档对齐)
|
||||
within(sheet).getByRole('button', { name: '前往体验' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(sheet).getByRole('button', { name: 'Fork 此版本' }),
|
||||
within(sheet).getByRole('button', { name: '开始共创此版本' }),
|
||||
).toBeTruthy();
|
||||
// 窄屏 bottom sheet 与桌面浮层同一组件:同样不得再出现 Fork 动作名。
|
||||
expect(sheet.textContent).not.toContain('Fork');
|
||||
// 遮罩点击关闭。
|
||||
fireEvent.click(screen.getByLabelText('关闭信息面板'));
|
||||
await waitFor(() =>
|
||||
@@ -4210,8 +4221,8 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
expect(screen.getByText('允许非商用共创')).toBeTruthy();
|
||||
expect(screen.getByText('累计衍生作品 3')).toBeTruthy();
|
||||
expect(screen.getByText('2 代三创 · 改编自父代《原初之光》')).toBeTruthy();
|
||||
// 主操作按文档叫「一键 Fork 共创」。
|
||||
expect(screen.getByRole('button', { name: '一键 Fork 共创' })).toBeTruthy();
|
||||
// 主操作按文档叫「一键开始共创」。
|
||||
expect(screen.getByRole('button', { name: '一键开始共创' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('累计创作世代数字段缺失时不渲染该概览项(不拿别的数字凑数)', async () => {
|
||||
@@ -4265,7 +4276,7 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
within(card).getByRole('button', { name: '查看创作族谱' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(card).getByRole('button', { name: '一键 Fork 共创' }),
|
||||
within(card).getByRole('button', { name: '一键开始共创' }),
|
||||
).toBeTruthy();
|
||||
// ② 图在**正文之后**:`.game-detail-page` 的直接子级,且排在正文容器之后。
|
||||
const page = document.querySelector('.game-detail-page') as HTMLElement;
|
||||
|
||||
@@ -88,7 +88,7 @@ type ViewportState = { scale: number; offsetX: number; offsetY: number };
|
||||
/**
|
||||
* 一个树节点:真实 HTML 卡片(可聚焦、可点击、有可访问名称),不是 SVG 矩形。
|
||||
*
|
||||
* 交互分工:主点击(含键盘 Enter/Space)进作品详情;次要动作 Fork 放在卡片内部的独立
|
||||
* 交互分工:主点击(含键盘 Enter/Space)进作品详情;次要动作「开始共创」放在卡片内部的独立
|
||||
* `<button>` 里并 `stopPropagation`,所以两者不会互相抢事件。
|
||||
*/
|
||||
function LineageTreeNodeCard({
|
||||
@@ -132,7 +132,7 @@ function LineageTreeNodeCard({
|
||||
const accessibleName = isStub
|
||||
? `原作品已不可用 · ${generationAxisLabel(generation)}`
|
||||
: `${generationAxisLabel(generation)}作品《${displayTitle}》· ${authorLabel}`;
|
||||
// 父作品不可用的占位节点只能聚焦(不可点进详情,也没有 Fork 动作)。
|
||||
// 父作品不可用的占位节点只能聚焦(不可点进详情,也没有「开始共创」动作)。
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -729,7 +729,7 @@ export function GameLineagePage({
|
||||
dragRef.current = null;
|
||||
};
|
||||
|
||||
/** 节点上的 Fork 次要动作:复用详情页同一条唤起 + 降级链路(协议模块 + 复制回退)。 */
|
||||
/** 节点上的「开始共创」次要动作:复用详情页同一条唤起 + 降级链路(协议模块 + 复制回退)。 */
|
||||
const launchForkFromNode = useCallback((gameId: string) => {
|
||||
if (forkTimerRef.current !== null) {
|
||||
window.clearTimeout(forkTimerRef.current);
|
||||
@@ -752,7 +752,7 @@ export function GameLineagePage({
|
||||
document.removeEventListener('visibilitychange', handOffToClient);
|
||||
forkHandlersRef.current = null;
|
||||
setForkNotice(
|
||||
'已唤起陶泥儿客户端。若没有自动打开,请用作品 ID 手动 Fork。',
|
||||
'已唤起陶泥儿客户端。若没有自动打开,请用作品 ID 手动开始共创。',
|
||||
);
|
||||
setForkFallbackGameId(gameId);
|
||||
};
|
||||
@@ -768,7 +768,7 @@ export function GameLineagePage({
|
||||
document.removeEventListener('visibilitychange', handOffToClient);
|
||||
forkHandlersRef.current = null;
|
||||
setForkNotice(
|
||||
'没有检测到陶泥儿客户端,已切换为手动方式:复制作品 ID 到客户端 Fork。',
|
||||
'没有检测到陶泥儿客户端,已切换为手动方式:复制作品 ID 到客户端开始共创。',
|
||||
);
|
||||
setForkFallbackGameId(gameId);
|
||||
}, FORK_LAUNCH_FALLBACK_DELAY_MS);
|
||||
@@ -949,7 +949,7 @@ export function GameLineagePage({
|
||||
{forkFallbackGameId ? (
|
||||
<CopyFeedbackButton
|
||||
state={forkCopyState}
|
||||
idleLabel={`复制 ${forkFallbackGameId} 去 Fork`}
|
||||
idleLabel={`复制 ${forkFallbackGameId} 去开始共创`}
|
||||
copiedLabel="作品 ID 已复制"
|
||||
failedLabel="复制失败"
|
||||
className="game-lineage-fork-copy"
|
||||
|
||||
@@ -102,7 +102,7 @@ export function LineageInfoPanel({
|
||||
className="game-lineage-panel__button game-lineage-panel__button--primary"
|
||||
onClick={() => onOpenFork(node.gameId)}
|
||||
>
|
||||
Fork 此版本
|
||||
开始共创此版本
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
* 节点内容复用与画布/血缘链同一个 `LineageNodeContent`(只有一套封面色块与省略规则)。
|
||||
*
|
||||
* 语义与画布一致:点击行 = **选中**(高亮祖先链 + 面包屑),行内「打开」= 进详情,
|
||||
* 「Fork」= 唤起/降级链路;主干链的导线加粗并用主题色。
|
||||
* 「开始共创」= 唤起/降级链路;主干链的导线加粗并用主题色。
|
||||
*/
|
||||
export function LineageOutlineList({
|
||||
nodes,
|
||||
|
||||
@@ -801,7 +801,7 @@ test('衍生作品的授权入口改为只读展示(授权继承父作品,
|
||||
expect(screen.queryByRole('button', { name: '共创授权' })).toBeNull();
|
||||
});
|
||||
|
||||
test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork 次数 / 世代数', async () => {
|
||||
test('分类标签按文档口径过滤,并给出总作品数 / 累计被共创次数 / 世代数', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({
|
||||
id: 'g-original',
|
||||
@@ -833,9 +833,9 @@ test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork
|
||||
|
||||
renderPage(createAuthValue());
|
||||
|
||||
// 数据概览:总作品数 + 累计被 Fork 次数(世代数字段缺失 → 该项不渲染)。
|
||||
// 数据概览:总作品数 + 累计被共创次数(世代数字段缺失 → 该项不渲染)。
|
||||
expect(await screen.findByText('总作品数 3')).toBeTruthy();
|
||||
expect(screen.getByText('累计被 Fork 次数 3')).toBeTruthy();
|
||||
expect(screen.getByText('累计被共创次数 3')).toBeTruthy();
|
||||
expect(screen.queryByText(/累计衍生世代数/u)).toBeNull();
|
||||
|
||||
// 卡片上的世代数与子衍生作品数量。
|
||||
|
||||
@@ -716,7 +716,7 @@ export function MyGamesPage({
|
||||
<div className="co-creation-overview" aria-label="创作数据概览">
|
||||
<span className="co-creation-stat">总作品数 {games.length}</span>
|
||||
<span className="co-creation-stat">
|
||||
累计被 Fork 次数 {totalForkCount}
|
||||
累计被共创次数 {totalForkCount}
|
||||
</span>
|
||||
{totalDerivedGenerations === null ? null : (
|
||||
<span className="co-creation-stat">
|
||||
|
||||
@@ -2448,7 +2448,7 @@
|
||||
|
||||
/*
|
||||
* 移动端「大纲式缩进树」(默认形态):全宽行 + 左缩进 + 肘形导线,**无横向滚动**。
|
||||
* 每行 ≥44px 触控目标;点击行=选中,行内「打开 / Fork」是次要动作。
|
||||
* 每行 ≥44px 触控目标;点击行=选中,行内「打开 / 开始共创」是次要动作。
|
||||
*/
|
||||
.lineage-outline {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user