From afb538ca8f28f1ca2d4d829438b47a400c268b6b Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 7 Oct 2026 16:12:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=B8=8E=E8=A7=84=E8=8C=83?= =?UTF-8?q?=EF=BC=9A=E6=90=AC=E5=87=BA=E5=8D=A1=E7=89=87=E7=9A=84=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E8=84=B1=E7=A6=BB=E5=8D=A1=E5=86=85=E4=BD=9C=E7=94=A8?= =?UTF-8?q?=E5=9F=9F=E5=B9=B6=E6=8C=89=E5=B9=B3=E5=8F=B0=E5=BA=A6=E9=87=8F?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E3=80=81=E5=B0=81=E9=9D=A2=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=2016:9=20=E9=93=BA=E6=BB=A1=E3=80=81=E5=85=A8=E5=B1=8F=20overl?= =?UTF-8?q?ay=20=E6=A0=B7=E5=BC=8F=EF=BC=9B=E5=90=8C=E6=AD=A5=20=C2=A73.6.?= =?UTF-8?q?1=20=E4=BA=A4=E4=BA=92=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...�技术方案】游戏共创与作品Fork-2026-10-03.md | 9 +- .../game-distribution/gameDistribution.css | 171 ++++++++++++++++-- 2 files changed, 165 insertions(+), 15 deletions(-) diff --git a/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md index 05b5b46d0..f92356b15 100644 --- a/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md +++ b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md @@ -483,9 +483,16 @@ A 路线里那个必须提前知道的互斥点已经按既有做法绕开:`cr - **画布交互硬化(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;**派生作品卡列表(含原「热门衍生栏」)整块移除**(`CoCreationHotDerived` 组件与其 `.co-creation-hot*` 样式已删除,零引用),改由族谱页**图下方**的「父代 / 子代」两块清单承担(父代 = 祖先链根→直接父;子代 = **直接子代**、按**热度(自身 `playCount`)降序**、同热度保持服务端创建序)。选「直接子代」的理由:与 `forkCount`/「被改编 N」同口径(数字与卡片数一致)、与画布「只看父代与子代」开关同义、更深的层级已由上面的树表达。 +- **交互口径(2026-10-07 二次修正,来自真机反馈)**: + - **滚轮 = 画布竖向平移**(不再是缩放):原生非 passive 监听仍绑在**画布元素**上并 `preventDefault`,所以外部页面与平台滚动容器都**不滚**;平移到边界过 `clampLineagePan`。**`Ctrl`/`⌘` + 滚轮 = 缩放**(保留通用手势与触控板缩放能力),**`Shift` + 滚轮 = 横向平移**(内容超宽时用)。两个按钮(`ZoomIn` / `ZoomOut`)与「总览」(原「适应窗口」,2026-10-07 改名)负责缩放与全览。 + - **拖动阈值**:按下时**不捕获指针、不拦默认**;移动超过 `DRAG_START_THRESHOLD_PX = 4` 才算拖动,此时才 `setPointerCapture` + 禁选 + `preventDefault`。原因:在**容器**上按下即捕获会把 `pointerup`/`click` 重定向到容器,**卡片按钮点不动**(真机复现:鼠标点「打开作品」8s 超时、键盘 Enter 正常 ⇒ 纯指针路径被吃掉)。纯点击完全走浏览器原生 `click`。 + - **「全屏查看族谱」= 真·全屏 overlay**:`createPortal` 到 `document.body` + `position: fixed; inset: 0`(实测 overlay 矩形 1440×900 == `innerWidth/innerHeight`),`role="dialog"` + `aria-modal="true"`,`Esc` 与「关闭」都可关,打开时焦点进 overlay、关闭后回触发按钮,`body` 滚动锁定并在关闭时恢复;移动端同样整屏并留 `env(safe-area-inset-*)`;里面就是 `GameLineagePage embedded`(**同一套画布**,点选/拖动/滚轮/封面全部同一份实现),打开时卸载内嵌那棵树避免双画布。独立 `/games/lineage` 页面与共创卡的「查看创作族谱」仍是页面级入口。 + - **被搬出信息卡的按钮必须脱离卡内作用域**:`.game-fork-remix-*` / `.co-creation-fork-primary*` / `.game-fork-lineage-link` 原先都写成 `.game-detail-info-card .xxx`,搬到主操作行/正文区块后**选择器不再命中**,按钮退回浏览器默认样式(细边框、无内边距)。现已把这些选择器**去作用域化**,并按平台度量对齐主操作行:主操作「一键开始共创」= 实心强调、高 48 / 圆角 16 / 内边距 0 16(与「立即玩」逐项一致,实测数值相同);次级「全屏查看族谱」= 描边 pill。 + - **封面固定 16:9 且铺满**:`.game-lineage-node__cover` 强制 `aspect-ratio: 16 / 9` + `flex: 0 0 auto`(不再被纵向 flex 拉伸),内层 `.game-cover` / `img` `100%×100%` + `object-fit: cover; object-position: center`。三处(画布卡片 / 大纲行 / 图谱下方父代子代清单)共用同一套;无封面时仍由 `GameCover` 给渐变占位(兜底未丢)。真机实测封面比例 **1.778 = 16:9**。 + - **图谱交互**:节点卡片与图下方父代/子代清单**每项都可点开详情**(真机鼠标点击与键盘 `Enter` 都验证过),焦点样式保持。 - **截断 / 空树 / 不可用父节点**在两种形态下都有降级:截断提示、空态、以及「原作品已不可用」的占位节点(可聚焦、不可点进详情、不展示被删作品标题与作者名)。 - **「共创」页签口径(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`,就**不强行全览**,改用可读缩放 + 平移,并给出明确提示「树较大:已按可读缩放显示(其余代际在视口外),拖拽平移查看,或点「适应窗口」看全树。」——两种情况下都**不会**在没有提示的前提下把最深的代际丢在视口外。窄屏「图谱视图」另按**宽度**适配(见上文)。 +- **初始适配口径(2026-10-07 复核)**:初次进入按**宽高同时 fit**(`min(scaleX, scaleY)` + `FIT_PADDING`)并**水平垂直都居中**;若这个比例低于可读下限 `READABLE_MIN_SCALE = 0.6`,就**不强行全览**,改用可读缩放 + 平移,并给出明确提示「树较大:已按可读缩放显示(其余代际在视口外),拖拽平移查看,或点「总览」看全树。」——两种情况下都**不会**在没有提示的前提下把最深的代际丢在视口外。窄屏「图谱视图」另按**宽度**适配(见上文)。 - **主干线唯一性复核(2026-10-07,真实数据)**:演示树 `共创演示·A0|星海工坊`(8 节点、`playCount` 全 0,仅 A111 = 1)实测 **每代恰好一个主干**:A0(0 代)/ A1(1 代)/ A11(2 代)/ A111(3 代);A2、A3、A12、A21 **都不是**主干。同代并列 0 热度时取「同代中**创建时间最早**」的那个(`/lineage` 契约即「代际升序、同代按创建时间升序」,布局只按输入顺序稳定取第一个),因此结果确定、不随渲染变化。**结论:主干逻辑未改**;新增「每代 isTrunk 数量恰好 1」的断言把它钉住(`lineageTreeLayout.test.ts`)。 #### 3.6.2 AGC 客户端:「共创」列表页与「开始共创」确认页(2026-10-07 补充) diff --git a/src/components/game-distribution/gameDistribution.css b/src/components/game-distribution/gameDistribution.css index 3dba9fc11..344ac0a82 100644 --- a/src/components/game-distribution/gameDistribution.css +++ b/src/components/game-distribution/gameDistribution.css @@ -355,7 +355,7 @@ color: var(--platform-text-soft); } /* 族谱入口:与卡内文字同节奏的次级按钮,窄屏也靠网格换行收在侧栏里。 */ -.game-detail-info-card .game-fork-lineage-link { +.game-fork-lineage-link { justify-self: start; border: 1px solid var(--platform-surface-border); border-radius: 999px; @@ -367,14 +367,14 @@ font-weight: 700; cursor: pointer; } -.game-detail-info-card .game-fork-lineage-link:hover { +.game-fork-lineage-link:hover { background: rgba(199, 101, 61, 0.12); } /* * 「改造这个作品」:入口 + 展开后的获取方式面板。都在共创信息卡内, * 窄屏靠网格换行与 `overflow-wrap: anywhere` 收在侧栏里,不产生横向溢出。 */ -.game-detail-info-card .game-fork-remix-link { +.game-fork-remix-link { justify-self: start; border: 1px solid var(--platform-accent-strong); border-radius: 999px; @@ -386,10 +386,10 @@ font-weight: 700; cursor: pointer; } -.game-detail-info-card .game-fork-remix-link:hover { +.game-fork-remix-link:hover { background: rgba(199, 101, 61, 0.2); } -.game-detail-info-card .game-fork-remix-panel { +.game-fork-remix-panel { display: grid; justify-items: start; gap: 0.35rem; @@ -403,27 +403,27 @@ font-size: 0.76rem; line-height: 1.6; } -.game-detail-info-card .game-fork-remix-steps { +.game-fork-remix-steps { color: var(--platform-text-base); overflow-wrap: anywhere; } -.game-detail-info-card .game-fork-remix-value { +.game-fork-remix-value { min-width: 0; color: var(--platform-text-soft); overflow-wrap: anywhere; } -.game-detail-info-card .game-fork-remix-value code { +.game-fork-remix-value code { color: var(--platform-text-strong); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.72rem; overflow-wrap: anywhere; } -.game-detail-info-card .game-fork-remix-actions { +.game-fork-remix-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; } -.game-detail-info-card .game-fork-remix-copy { +.game-fork-remix-copy { border: 1px solid var(--platform-surface-border); border-radius: 999px; padding: 0.2rem 0.7rem; @@ -434,11 +434,11 @@ font-weight: 600; cursor: pointer; } -.game-detail-info-card .game-fork-remix-note { +.game-fork-remix-note { color: var(--platform-text-soft); overflow-wrap: anywhere; } -.game-detail-info-card .game-fork-remix-status { +.game-fork-remix-status { color: var(--platform-text-soft); font-size: 0.74rem; overflow-wrap: anywhere; @@ -798,7 +798,7 @@ color: var(--platform-text-soft); font-size: 0.76rem; } -.game-detail-info-card .co-creation-fork-primary { +.co-creation-fork-primary { align-self: stretch; justify-content: center; padding: 0.45rem 0.9rem; @@ -806,7 +806,7 @@ background: rgba(199, 101, 61, 0.14); font-size: 0.86rem; } -.game-detail-info-card .co-creation-fork-primary--blocked { +.co-creation-fork-primary--blocked { border-color: var(--platform-surface-border); background: var(--platform-surface-muted, rgba(0, 0, 0, 0.04)); color: var(--platform-text-soft); @@ -2702,3 +2702,146 @@ .lineage-relation__main:hover { border-color: var(--platform-surface-hover-border); } + +/* + * 「一键开始共创」在主操作行里:**必须与「立即玩」同一套视觉权重**。 + * + * 为什么需要这段:按钮原先住在 `.game-detail-info-card` 里,样式被卡内作用域套着 + * (`.game-detail-info-card .game-fork-remix-link` / `.co-creation-fork-primary`); + * 搬到 `actions` 主操作行后**选择器不再命中**,按钮退回浏览器默认样式(细边框 + 无内边距), + * 于是与「立即玩」不是一个量级。这里把它按平台主操作按钮的度量固定下来(实心强调、lg 高度)。 + */ +.game-fork-remix-link.co-creation-fork-primary { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: center; + gap: 0.35rem; + /* 与主操作行里的「立即玩」逐项对齐(实测:高 48 / 圆角 16 / 内边距 0 16 / 字号 15)。 */ + min-height: 3rem; + padding: 0 1rem; + border: 1px solid var(--platform-button-primary-border); + border-radius: 1rem; + background: var(--platform-button-primary-solid); + color: var(--platform-button-primary-text); + font: inherit; + font-size: 0.94rem; + font-weight: 600; + cursor: pointer; +} +.game-fork-remix-link.co-creation-fork-primary:hover { + background: var(--platform-accent-strong, var(--platform-accent)); +} +/* 禁止共创:置灰但可点(点了给原因),与平台 disabled 观感一致。 */ +.co-creation-fork-primary--blocked, +.game-fork-remix-link.co-creation-fork-primary--blocked { + border-color: var(--platform-surface-border); + background: var(--platform-surface-muted, rgba(0, 0, 0, 0.04)); + color: var(--platform-text-soft); + cursor: not-allowed; +} +/* 「全屏查看族谱」= 次级动作(描边),别和主操作抢焦点。 */ +.game-fork-lineage-link { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: center; + min-height: 2rem; + padding: 0 0.85rem; + border: 1px solid var(--platform-surface-border); + border-radius: 999px; + background: var(--platform-button-secondary-fill); + color: var(--platform-button-secondary-text); + font: inherit; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} +.game-fork-lineage-link:hover { + border-color: var(--platform-surface-hover-border); +} + +/* + * 「全屏查看族谱」的真·全屏 overlay:portal 到 body + position:fixed inset:0, + * 因此不受详情页/平台 tab 面板的宽度、max-width、overflow 约束(之前是路由跳转,名不副实)。 + * 移动端同样整屏,并给刘海/底部条留安全区。 + */ +.lineage-fullscreen { + position: fixed; + inset: 0; + z-index: 90; + display: flex; + flex-direction: column; + background: var(--platform-body-fill, #fff); + padding-top: env(safe-area-inset-top); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); +} +.lineage-fullscreen__bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.55rem 1rem; + border-bottom: 1px solid var(--platform-surface-border); + background: var(--platform-surface, #fff); +} +.lineage-fullscreen__title { + margin: 0; + font-size: 1rem; + font-weight: 700; +} +.lineage-fullscreen__close { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.25rem; + padding: 0 1rem; + border: 1px solid var(--platform-surface-border); + border-radius: 999px; + background: var(--platform-button-secondary-fill); + color: var(--platform-button-secondary-text); + font: inherit; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; +} +.lineage-fullscreen__body { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + padding: 0.75rem 1rem 1rem; +} +/* overlay 里让画布吃掉整屏高度(内嵌时的高度上限是为详情页排版的)。 */ +.lineage-fullscreen__body .game-lineage-canvas { + height: min(48rem, 74vh); +} + +/* + * 封面固定 **16:9 且必须铺满**(2026-10-07 用户口径:「封面固定 16:9,并且图片一定要铺满,不要留空白」)。 + * 之前画布卡片的封面 `aspect-ratio` 是 `auto`、且作为纵向 flex 子项被拉伸,图片只占上半截 → 下半截是空白; + * 大纲/关系行的封面虽然 16:9,但内部图片被撑到 192px 高、只露出一条 → 也不是「铺满」。 + * 这里把三处统一:容器强制 16:9(`flex: 0 0 auto` 防止被 flex 拉伸)、 + * 内层 wrapper 与 img 100%×100% + `object-fit: cover`。无封面时的占位仍由 `GameCover` 自己给(保留兜底)。 + */ +.game-lineage-node__cover { + position: relative; + display: block; + width: 100%; + aspect-ratio: 16 / 9; + flex: 0 0 auto; + overflow: hidden; +} +.game-lineage-node__cover .game-cover, +.game-lineage-node__cover .game-cover__image, +.game-lineage-node__cover img { + display: block; + width: 100%; + height: 100%; +} +.game-lineage-node__cover img { + object-fit: cover; + object-position: center; +}