文案收敛:动作名统一 Fork,去掉「可玩参考 + 素材 / 不能重新发布」那套表述

- 动作名统一:「改造这个作品」→「Fork 这个作品」,「登录后可改造」→「登录后可 Fork」
- 面板指引改为「在陶泥儿客户端里打开「从平台作品开始创作」,粘贴下面的作品 ID 或作品链接。」
- 对外口径统一为「Fork = 把整个工程复制一份到你的项目里继续改造(与 git fork 同理)」,并保留一句话必要告知:整个工程(含素材与文档)复制后对他人可见,且不可撤销
- 删除「成品包只能可玩参考 + 素材」「不能直接再次发布」「参考产物在 reference/<作品ID>/」等展开说明(冻结口径 1)
- 测试同步:断言新文案与「不可撤销」告知,测试名与注释不再出现「改造」
This commit is contained in:
2026-10-06 00:48:23 +08:00
parent bdcefe91d1
commit b6fe63b551
2 changed files with 29 additions and 33 deletions
@@ -344,8 +344,8 @@ function GameDetailContent({
);
const lineage = game.lineage ?? null;
const forkCount = game.forkCount ?? 0;
// 「改造这个作品」的获取方式:网页不能唤起客户端(未注册 deep link),所以给出可复制的
// 作品 ID 与完整作品链接,让用户到客户端的「从平台作品开始创作」里粘贴。
// Fork 的获取方式:主路径是唤起客户端(见下方 handleForkLaunch),失败才降级为
// 「复制作品 ID / 链接 → 到客户端粘贴」。
const [isForkPanelOpen, setIsForkPanelOpen] = useState(false);
const { copyState: forkIdCopyState, copyText: copyForkId } = useCopyFeedback();
const { copyState: forkLinkCopyState, copyText: copyForkLink } =
@@ -445,10 +445,9 @@ function GameDetailContent({
) : null}
{canRemix ? (
/*
「改造这个作品」:服务端取件通道要求 Bearer + 已公开 + 授权非禁止。
未登录按仓库既有登录门禁处理——入口照常出现(显示「登录后可改造」),
Fork 入口:服务端取件通道要求 Bearer + 已公开 + 授权非禁止。
未登录按仓库既有登录门禁处理——入口照常出现(显示「登录后可 Fork」),
点击走登录弹窗,而不是静默消失。
网页不能唤起客户端(未注册 deep link),所以展开的是「复制 ID/链接 → 到客户端粘贴」。
*/
<>
<button
@@ -466,7 +465,7 @@ function GameDetailContent({
: onLogin()
}
>
{isAuthenticated ? '改造这个作品' : '登录后可改造'}
{isAuthenticated ? 'Fork 这个作品' : '登录后可 Fork'}
</button>
{isAuthenticated && isForkPanelOpen ? (
<span
@@ -474,8 +473,8 @@ function GameDetailContent({
id="game-fork-remix-panel"
>
<span className="game-fork-remix-steps">
打开陶泥儿客户端 → 首页「从平台作品开始创作」→
粘贴下面的作品 ID 或作品链接。
在陶泥儿客户端里打开「从平台作品开始创作」,粘贴下面的作品
ID 或作品链接。
</span>
<span className="game-fork-remix-value">
作品 ID:<code>{game.id}</code>
@@ -502,11 +501,8 @@ function GameDetailContent({
/>
</span>
<span className="game-fork-remix-note">
客户端会把成品包作为<strong>可玩参考与素材</strong>
放进新项目的 <code>reference/{game.id}/</code>{' '}
目录;成品包里没有源码,
<strong>不能直接再次发布</strong>
,请在客户端的新工程里改造后发布。
Fork 会把这部作品的整个工程(含素材与文档)复制到你的项目里继续改造;
复制后对他人可见,且不可撤销。
</span>
</span>
) : null}
@@ -344,7 +344,7 @@ describe('GameDetailPage', () => {
expect(await screen.findByText('暂未发布可玩版本')).toBeTruthy();
});
it('展示共创授权、代际与溯源信息,并给出改造入口', async () => {
it('展示共创授权、代际与溯源信息,并给出 Fork 入口', async () => {
getGameMock.mockResolvedValueOnce(
createGame({
forkAuthorization: 'nonCommercial',
@@ -367,9 +367,9 @@ describe('GameDetailPage', () => {
expect(screen.getByText('第 2 代作品 · 改编自《原初之光》')).toBeTruthy();
expect(screen.getByText('由 原作者 制作')).toBeTruthy();
expect(screen.getByText('已被改编 3 次')).toBeTruthy();
// 已公开 + 授权开放 → 「改造这个作品」入口出现(M1 时它还不存在,这里钉住新契约)。
// 已公开 + 授权开放 → 「Fork 这个作品」入口出现(M1 时它还不存在,这里钉住新契约)。
expect(
screen.getByRole('button', { name: '改造这个作品' }),
screen.getByRole('button', { name: 'Fork 这个作品' }),
).toBeTruthy();
});
@@ -1355,7 +1355,7 @@ describe('GameLineagePage', () => {
});
});
describe('GameDetailPage 改造入口', () => {
describe('GameDetailPage Fork 入口', () => {
it('已公开且授权开放时出现入口,展开后给出可复制的作品 ID 与操作指引', async () => {
getGameMock.mockResolvedValueOnce(
createGame({ forkAuthorization: 'nonCommercial' }),
@@ -1364,21 +1364,21 @@ describe('GameDetailPage 改造入口', () => {
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
);
const entry = await screen.findByRole('button', { name: '改造这个作品' });
const entry = await screen.findByRole('button', { name: 'Fork 这个作品' });
// 未展开前不露出获取方式。
expect(screen.queryByText(/打开陶泥儿客户端/u)).toBeNull();
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
fireEvent.click(entry);
expect(
screen.getByText(
'打开陶泥儿客户端 → 首页「从平台作品开始创作」→ 粘贴下面的作品 ID 或作品链接。',
'在陶泥儿客户端里打开「从平台作品开始创作」,粘贴下面的作品 ID 或作品链接。',
),
).toBeTruthy();
expect(screen.getByText('game-1')).toBeTruthy();
expect(screen.getByText(/\/games\/detail\?id=game-1$/u)).toBeTruthy();
// 诚实口径:可玩参考 + 素材、参考产物目录、不能直接再次发布。
expect(screen.getByText(/reference\/game-1\//u)).toBeTruthy();
expect(screen.getByText(/不能直接再次发布/u)).toBeTruthy();
// 冻结口径:Fork = 复制整个工程继续改造;只保留一句话告知(对他人可见 + 不可撤销)。
expect(screen.getByText(/整个工程(含素材与文档)复制到你的项目里继续改造/u)).toBeTruthy();
expect(screen.getByText(/复制后对他人可见,且不可撤销/u)).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '复制作品 ID' }));
await waitFor(() =>
@@ -1399,7 +1399,7 @@ describe('GameDetailPage 改造入口', () => {
).toBeTruthy();
});
it('授权为禁止共创时不出现改造入口', async () => {
it('授权为禁止共创时不出现 Fork 入口', async () => {
getGameMock.mockResolvedValueOnce(
createGame({ forkAuthorization: 'forbidden' }),
);
@@ -1408,11 +1408,11 @@ describe('GameDetailPage 改造入口', () => {
);
await screen.findByRole('heading', { name: '星轨防线' });
expect(screen.queryByRole('button', { name: '改造这个作品' })).toBeNull();
expect(screen.queryByRole('button', { name: '登录后可改造' })).toBeNull();
expect(screen.queryByRole('button', { name: 'Fork 这个作品' })).toBeNull();
expect(screen.queryByRole('button', { name: '登录后可 Fork' })).toBeNull();
});
it('未公开作品不出现改造入口', async () => {
it('未公开作品不出现 Fork 入口', async () => {
getMyGameMock.mockResolvedValueOnce({
...createGame({ status: 'unpublished', forkAuthorization: 'full' }),
latestVersion: null,
@@ -1428,11 +1428,11 @@ describe('GameDetailPage 改造入口', () => {
);
await screen.findByText('作品状态');
expect(screen.queryByRole('button', { name: '改造这个作品' })).toBeNull();
expect(screen.queryByRole('button', { name: '登录后可改造' })).toBeNull();
expect(screen.queryByRole('button', { name: 'Fork 这个作品' })).toBeNull();
expect(screen.queryByRole('button', { name: '登录后可 Fork' })).toBeNull();
});
it('未登录时入口显示「登录后可改造」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
it('未登录时入口显示「登录后可 Fork」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
authUiMock.value.user = null;
authUiMock.value.canAccessProtectedData = false;
getGameMock.mockResolvedValueOnce(
@@ -1442,12 +1442,12 @@ describe('GameDetailPage 改造入口', () => {
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
);
const gate = await screen.findByRole('button', { name: '登录后可改造' });
expect(screen.queryByText(/打开陶泥儿客户端/u)).toBeNull();
const gate = await screen.findByRole('button', { name: '登录后可 Fork' });
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
fireEvent.click(gate);
expect(authUiMock.value.openLoginModal).toHaveBeenCalledTimes(1);
// 未登录不得展开获取方式(也就不会给出取不到的说明)。
expect(screen.queryByText(/打开陶泥儿客户端/u)).toBeNull();
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
});
});