清理:删除零引用的 CoCreationHotDerived 及其 .co-creation-hot* 样式(热门衍生栏已并入图谱页子代块)
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 / 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 / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

This commit is contained in:
2026-10-07 15:51:52 +08:00
parent 5b69106b2d
commit c6677e1323
4 changed files with 3 additions and 127 deletions
@@ -482,7 +482,7 @@ A 路线里那个必须提前知道的互斥点已经按既有做法绕开:`cr
- **移动端门限豁免(2026-10-06)**:平台原有的「移动端仅支持作品展示」欢迎弹窗只看 `!isDesktopLayout`、忽略路由,会把公开只读的族谱树 / 共创主题页首屏盖住。现在按**显式白名单**豁免这几个只读浏览 stage:`game-lineage` / `game-themes` / `game-theme` / `game-co-creation`(清单在 `PlatformActiveMobileWelcomeDialog.tsx` 的 `MOBILE_WELCOME_EXEMPT_STAGES`,键名即 stage 名,不做前缀/范围匹配)。**创作 / 项目 / 发布 / 我的 / 平台首页仍保持门限**——那些才是「移动端不支持体验创作工具」的部分;弹窗文案未改(与豁免后的事实不矛盾:族谱与主题页本身就是作品展示)。
- **画布交互硬化(2026-10-07,用户口径)**:① **滚轮只缩放画布**——React 的 `onWheel` 挂在根容器上且是 **passive** 的(`preventDefault` 无效 ⇒ 页面跟着滚),因此改为在**画布元素本身**挂原生 `wheel` 监听并显式 `{ passive: false }`(不绑 `window`);② **拖动不选中/不拖走文本**——画布容器 `user-select: none` + `touch-action: none` + `overscroll-behavior: contain`,拖动期间把 `user-select: none` 压到 `document.body`(跨出画布也有效,松手恢复),并对 `selectstart` / `dragstart` 挂原生阻止、画布内图片 `-webkit-user-drag: none; pointer-events: none`;指针捕获(`setPointerCapture`)保证移出容器仍跟手;③ **平移边界**:`clampLineagePan`(纯函数)保证任何时刻至少 `LINEAGE_PAN_KEEP_PX = 96` 像素内容可见,拖到底/拖到顶不会把内容拖飞。真机实测(Chromium,1440×900):滚轮后缩放 60%→65%、`window.scrollY` 与平台滚动容器 `scrollTop` **都不变**;两次连续拖动各位移 +120px 且 `window.getSelection()` 始终为空字符串。
- **「一键开始共创」位置与未安装检测(2026-10-07,用户口径)**:按钮从共创信息卡**搬到主操作行**(与「立即玩 / 收藏」并列,保持主操作视觉权重;卡内不再重复出现)。未安装检测:纯 web **拿不到系统级安装信息**,只能靠「唤起后浏览器是否被接管」间接判断(`blur` / `visibilitychange`,超时 `FORK_LAUNCH_FALLBACK_DELAY_MS = 1200ms`);**未接管 / 移动端视为「没有客户端」→ 打开与顶栏「下载客户端」同一个下载入口**(`requestClientDownload()` 事件 → `ClientDownloadEntry` 弹窗),并保留手动复制作品 ID 的降级路径与一句人话提示。**局限**:慢启动的客户端/被用户取消的系统确认框/某些不派发 blur 的环境都会误判成「未安装」,代价只是多打开一个下载入口(用户可关掉重试),不会阻断正常路径;老客户端(不响应深链)走同一条兜底。
- **共创信息卡只留「标签 + 数据 + 入口」(2026-10-07,用户口径)**:卡里保留世代/授权标签、父代文案、数据概览(游玩合计与分解、累计衍生物、累计创作世代数)、「查看创作族谱」与所属主题 tag;**派生作品卡列表(含原「热门衍生栏」)整块移除**,改由族谱页**图下方**的「父代 / 子代」两块清单承担(父代 = 祖先链根→直接父;子代 = **直接子代**、按**热度(自身 `playCount`)降序**、同热度保持服务端创建序)。选「直接子代」的理由:与 `forkCount`/「被改编 N」同口径(数字与卡片数一致)、与画布「只看父代与子代」开关同义、更深的层级已由上面的树表达。
- **共创信息卡只留「标签 + 数据 + 入口」(2026-10-07,用户口径)**:卡里保留世代/授权标签、父代文案、数据概览(游玩合计与分解、累计衍生物、累计创作世代数)、「查看创作族谱」与所属主题 tag;**派生作品卡列表(含原「热门衍生栏」)整块移除**(`CoCreationHotDerived` 组件与其 `.co-creation-hot*` 样式已删除,零引用),改由族谱页**图下方**的「父代 / 子代」两块清单承担(父代 = 祖先链根→直接父;子代 = **直接子代**、按**热度(自身 `playCount`)降序**、同热度保持服务端创建序)。选「直接子代」的理由:与 `forkCount`/「被改编 N」同口径(数字与卡片数一致)、与画布「只看父代与子代」开关同义、更深的层级已由上面的树表达。
- **截断 / 空树 / 不可用父节点**在两种形态下都有降级:截断提示、空态、以及「原作品已不可用」的占位节点(可聚焦、不可点进详情、不展示被删作品标题与作者名)。
- **「共创」页签口径(2026-10-07,用户口径)**:页签与「游戏」页签**复用同一个 `GameGalleryPage`**(`scope="forkable"`),数据来自公开目录并带 `forkable=1`(分页/游标/去重/末页/失败态全部沿用已实现的「加载更多」);**服务端尚未生效期间客户端再兜一层同义过滤**(`forkAuthorization !== 'forbidden'`,未知按禁止失败关闭),两条口径一致、服务端上线后等价幂等。页签不再指向共创主题列表(主题列表页与路由保留,无页签入口);页面标题 `game-co-creation` → 「共创 - 陶泥儿」,路由 `/games/co-creation`,移动端门限同样按「只读浏览 = 作品展示」豁免。空态为「暂时没有开放共创的作品」。
- **初始适配口径(2026-10-07 复核)**:初次进入按**宽高同时 fit**(`min(scaleX, scaleY)` + `FIT_PADDING`)并**水平垂直都居中**;若这个比例低于可读下限 `READABLE_MIN_SCALE = 0.6`,就**不强行全览**,改用可读缩放 + 平移,并给出明确提示「树较大:已按可读缩放显示(其余代际在视口外),拖拽平移查看,或点「适应窗口」看全树。」——两种情况下都**不会**在没有提示的前提下把最深的代际丢在视口外。窄屏「图谱视图」另按**宽度**适配(见上文)。
@@ -1,81 +0,0 @@
import type { GameDistributionLineageNode } from '../../services/gameDistributionClient';
import { coCreationGenerationShortLabel } from './coCreationCopy';
import { GameCover } from './GameCover';
import type { LineagePlayCountBreakdown } from './lineageTreeLayout';
/**
* 热门衍生栏:当前作品下热度最高的 5 个分支作品(横向卡片)。
*
* 热度口径:**`playCount` 倒序**,同热度按创建顺序(服务端顺序)稳定兜底。
* `/derived` 目前不保证服务端排序,所以排序在客户端做;后端若补了排序,
* 这段逻辑仍然成立(只是排序变成幂等)。
*
* 显示口径(2026-10-06):「游玩次数」的**显示**一律是**子树合计**(父代展示自己 + 全部后代)。
* 传了 `playCountTotals` 时排序与显示都用合计(这样列表顺序与卡片上的数字一致);
* 传不进来(拿不到树)时回退服务端的**自身值**,不暗示含衍生。
*/
export const CO_CREATION_HOT_DERIVED_LIMIT = 5;
export function CoCreationHotDerived({
nodes,
playCountTotals,
onOpenGameDetail,
}: {
nodes: readonly GameDistributionLineageNode[];
/**
* 每个节点的游玩次数分解(详情页用同一棵 `/lineage` 在前端算好的,零额外请求)。
* 缺失时显示自身值。
*/
playCountTotals?: ReadonlyMap<string, LineagePlayCountBreakdown> | null;
onOpenGameDetail: (gameId: string) => void;
}) {
const shownPlayCount = (gameId: string, own: number) =>
playCountTotals?.get(gameId)?.total ?? own;
const hottest = [...nodes]
.sort(
(left, right) =>
shownPlayCount(right.gameId, right.playCount) -
shownPlayCount(left.gameId, left.playCount),
)
.slice(0, CO_CREATION_HOT_DERIVED_LIMIT);
if (hottest.length === 0) return null;
return (
<div className="co-creation-hot" aria-label="热门衍生作品">
{hottest.map((node) => (
<button
key={node.gameId}
type="button"
className="co-creation-hot__card"
aria-label={`热门衍生《${node.title}》· ${coCreationGenerationShortLabel(node.generation)}`}
onClick={() => onOpenGameDetail(node.gameId)}
>
<span className="co-creation-hot__cover">
<GameCover
game={gameForCover(node)}
objectKey={node.coverObjectKey}
/>
</span>
<span className="co-creation-hot__title">{node.title}</span>
<span className="co-creation-hot__meta">
{coCreationGenerationShortLabel(node.generation)} ·{' '}
{node.authorName?.trim() || '未署名作者'} ·{' '}
{shownPlayCount(node.gameId, node.playCount)} 次游玩
</span>
</button>
))}
</div>
);
}
/** `GameCover` 只读 title / coverColor / icon / coverObjectKey;族谱节点没有完整游戏行。 */
function gameForCover(node: GameDistributionLineageNode) {
return {
title: node.title,
coverColor: '#F3E4D0',
icon: '🎮',
coverObjectKey: node.coverObjectKey,
} as never;
}
export default CoCreationHotDerived;
@@ -4818,7 +4818,8 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
expect(
within(card).getByRole('button', { name: '查看创作族谱' }),
).toBeTruthy();
// 卡里**不再有**派生作品卡列表(热门衍生栏 / 任何父代子代清单)。
// 卡里**不再有**派生作品卡列表:`CoCreationHotDerived` 组件与它的 `.co-creation-hot*` 样式
// 已于 2026-10-07 删除(零引用),这里钉住「它不会再回来」。
expect(card.querySelector('.co-creation-hot')).toBeNull();
expect(screen.queryByLabelText('热门衍生作品')).toBeNull();
expect(within(card).queryByText('被改编出来的作品')).toBeNull();
@@ -812,50 +812,6 @@
color: var(--platform-text-soft);
cursor: not-allowed;
}
/* 热门衍生栏:横向卡片。 */
.game-detail-info-card .co-creation-hot {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(7rem, 100%), 1fr));
gap: 0.4rem;
}
.game-detail-info-card .co-creation-hot__card {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
border: 1px solid var(--platform-surface-border);
border-radius: 0.6rem;
padding: 0.3rem;
background: var(--platform-surface, #fff);
color: var(--platform-text-base);
font: inherit;
text-align: left;
cursor: pointer;
}
.game-detail-info-card .co-creation-hot__cover {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: 0.4rem;
background: var(--platform-surface-border);
}
.game-detail-info-card .co-creation-hot__cover .game-cover {
width: 100%;
height: 100%;
border-radius: 0;
}
.game-detail-info-card .co-creation-hot__title {
color: var(--platform-text-strong);
font-size: 0.74rem;
font-weight: 600;
overflow-wrap: anywhere;
}
.game-detail-info-card .co-creation-hot__meta {
color: var(--platform-text-soft);
font-size: 0.68rem;
overflow-wrap: anywhere;
}
/* 画布内悬浮控件(右下角):圆角胶囊、浅色底 + 毛玻璃、内边距统一、轻阴影。 */
.game-lineage-controls {
position: absolute;