diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx index d8deed12e..f95b7ddab 100644 --- a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx +++ b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx @@ -1107,14 +1107,17 @@ export function GameDistributionPublishPanel({ */} {forkSource ? (

- {`授权继承自父作品${ - parentAuthorizationLabel - ? `:${parentAuthorizationLabel}` - : '(档位读取中或不可用)' - }。衍生作品的授权档位在创建时由父作品快照继承,本页与网页端都不能再修改(服务端会拒绝任何提升)。`} + + {parentAuthorizationLabel + ? `授权继承自父作品:${parentAuthorizationLabel}` + : '授权继承自父作品'} + + + 衍生作品的授权档位在创建时由父作品快照继承,本页与网页端都不能再修改(服务端会拒绝任何提升)。 +

) : (
void; }; -const sectionClass = - 'mx-auto mt-5.5 grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]'; - export default function ForkConfirmView({ onProjectCreated, onCancel, @@ -62,7 +82,6 @@ export default function ForkConfirmView({ const [remainingSeconds, setRemainingSeconds] = useState( MANDATORY_READING_SECONDS, ); - const noticeListRef = useRef(null); const gameId = target?.gameId ?? ''; const notices = useMemo( @@ -120,13 +139,9 @@ export default function ForkConfirmView({ }, [card?.coverObjectKey]); // 强制阅读:进入页面后倒计时 3 秒,其间确认按钮禁用(重新打开确认页会重新计时)。 + // 门禁只与**时间**有关:须知随页面自然流动,不要求用户把某个内层容器滚到底。 useEffect(() => { setRemainingSeconds(MANDATORY_READING_SECONDS); - // 须知区一进来就滚到顶部,避免上次滚动位置让用户误以为已经读过。 - // 用 `scrollTop` 而不是 `scrollTo()`:jsdom 没实现后者,而这里只需要复位。 - if (noticeListRef.current) { - noticeListRef.current.scrollTop = 0; - } const startedAt = Date.now(); const timer = window.setInterval(() => { const elapsed = Math.floor((Date.now() - startedAt) / 1000); @@ -146,6 +161,7 @@ export default function ForkConfirmView({ ); const reachedStages = fork.progress.reached; const activeStage = fork.progress.current; + const syncedCount = reachedStages.length; async function confirmFork() { if (!gameId || readingLocked || fork.busy) { @@ -169,13 +185,16 @@ export default function ForkConfirmView({ if (!target) { return ( -
-
-

+

+
+

没有待确认的 Fork;请回到「共创」页选择一个作品。

- + {/* + 吸附底部操作条:`sticky bottom-0` 挂在外壳滚动容器(`.launcher-main`)下沿, + 长页面滚动时始终可见;状态说明也放在条内,避免它被顶到看不见的地方。 + */} +
+
+
+ + +
+ {fork.busy ? ( +

+ 正在同步工程:内容较大时需要几分钟,请不要关闭客户端。同步期间不能取消。 +

+ ) : null} + {!fork.busy && reachedStages.length > 0 ? ( +

+

+ ) : null}
- {fork.busy ? ( -

- 正在同步工程:内容较大时需要几分钟,请不要关闭客户端。同步期间不能取消。 -

- ) : null} - {!fork.busy && reachedStages.length > 0 ? ( -

-

- ) : null} -
+
); } diff --git a/apps/ai-game-creator-shell/tests/forkConfirm.test.tsx b/apps/ai-game-creator-shell/tests/forkConfirm.test.tsx index 0db9b3a98..0cdc09961 100644 --- a/apps/ai-game-creator-shell/tests/forkConfirm.test.tsx +++ b/apps/ai-game-creator-shell/tests/forkConfirm.test.tsx @@ -56,10 +56,10 @@ function installInvoke( function renderPage() { const onProjectCreated = vi.fn(); const onCancel = vi.fn(); - render( + const rendered = render( , ); - return { onProjectCreated, onCancel }; + return { ...rendered, onProjectCreated, onCancel }; } function confirmButton() { @@ -185,6 +185,61 @@ describe('Fork 工程确认页', () => { expect(notices.textContent).toContain('会被平台下架并要求整改'); }); + it('须知块不再自建内层滚动,页面只靠外壳滚动容器', async () => { + installInvoke(() => null); + const { container } = renderPage(); + + const root = container.querySelector('.fork-page'); + expect(root).not.toBeNull(); + // 页面内不得出现任何纵向滚动容器:须知全文随页面自然流动。 + expect(container.querySelector('.fork-page .overflow-y-auto')).toBeNull(); + expect(container.querySelector('.fork-page .overflow-auto')).toBeNull(); + expect(container.querySelector('.fork-page .overflow-scroll')).toBeNull(); + const notices = screen.getByLabelText('授权须知全文'); + for (const className of Array.from(notices.classList)) { + expect(className.startsWith('max-h-')).toBe(false); + expect(className.startsWith('overflow-y-')).toBe(false); + } + // 须知仍然是**全文**:五条正文都在这一个容器里逐条渲染。 + expect(notices.querySelectorAll('article')).toHaveLength(5); + }); + + it('底部操作条是吸附页脚:停在滚动容器下沿,不随内容滚走', async () => { + installInvoke(() => null); + const { container } = renderPage(); + + const footer = container.querySelector( + '[aria-label="Fork 操作"]', + ) as HTMLElement; + expect(footer).not.toBeNull(); + // `sticky bottom-0` 挂在页面根下(滚动容器是外壳的 `.launcher-main`)。 + expect(footer.classList.contains('sticky')).toBe(true); + expect(footer.classList.contains('bottom-0')).toBe(true); + const pageRoot = container.querySelector('.fork-page') as HTMLElement; + expect(footer.parentElement).toBe(pageRoot); + expect(pageRoot.lastElementChild).toBe(footer); + expect(footer.className).toContain('border-t'); + // 两个动作都在页脚里,长页面下也够得到。 + expect(footer.textContent).toContain('取消'); + expect(footer.textContent).toContain('确认并进入编辑'); + }); + + it('状态是徽章形态,与按钮可区分(倒计时同时出现在徽章与按钮上)', async () => { + installInvoke(() => null); + renderPage(); + + const badge = screen.getByText(/^3s 后可确认$/); + // 徽章:圆角胶囊 + 描边,不是按钮。 + expect(badge.tagName).toBe('SMALL'); + expect(badge.className).toContain('rounded-full'); + expect(badge.className).toContain('border'); + expect(badge.closest('button')).toBeNull(); + expect(screen.getByText('确认后开始').className).toContain('rounded-full'); + + finishMandatoryReading(); + expect(await screen.findByText('已可确认')).not.toBeNull(); + }); + it('全开放授权时商用权限改写成可商用', async () => { useForkConfirmStore.getState().close(); useForkConfirmStore.getState().open({