dsh-dock 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -20
- package/client.js +1426 -931
- package/features/animation/view.jsx +26 -12
- package/features/balance/view.js +8 -6
- package/features/mobile/host.js +18 -311
- package/features/mobile/view.js +295 -480
- package/features/mobile-relay/host.js +3 -4
- package/features/mobile-relay/view.jsx +1 -2
- package/features/notify/host.js +52 -16
- package/features/notify/view.jsx +884 -377
- package/features/runstate/host.js +1 -11
- package/features/runstate/view.jsx +32 -15
- package/features/tokenlog/view.jsx +8 -3
- package/index.js +1 -1
- package/package.json +2 -2
- package/screenshots/dock-notify.png +0 -0
- package/src/client.jsx +36 -6
- package/src/host-core.js +8 -1
- package/src/shared.js +86 -0
- package/src/task-track.js +87 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Host 通信:fetch('dsh-dock/animation/<method>')(见 features/animation/host.js)。
|
|
16
16
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
17
|
-
import { openPanel, panelNav, subscribePanel } from "../../src/shared.js";
|
|
17
|
+
import { openPanel, panelNav, subscribePanel, taskWaiting, taskApprovals, taskStalled } from "../../src/shared.js";
|
|
18
18
|
|
|
19
19
|
// ---------- Host RPC 桥接 ----------
|
|
20
20
|
function rpcCall(method, args) {
|
|
@@ -721,7 +721,11 @@ function MissionDeparture(props) {
|
|
|
721
721
|
}
|
|
722
722
|
function ProgrammingStuck(props) {
|
|
723
723
|
const waiting = !!props.waiting;
|
|
724
|
-
|
|
724
|
+
// 等人的性质要说清:等批准(要不要执行)与等回答(选哪个答案)是两件事
|
|
725
|
+
const task = props.task;
|
|
726
|
+
const label = !waiting ? "编程等待中"
|
|
727
|
+
: (taskApprovals(task).length > 0 ? "等待确认" : "等待回答");
|
|
728
|
+
return <div className={"dkan-space-stuck" + (waiting ? " waiting" : "")} aria-hidden="true"><FreighterHull units={3} empty /><span className="dkan-stuck-code"><i />{`{ }`}</span><strong>{label}</strong><small>{truncate((props.task && props.task.title) || "工具暂无新响应", 28)}</small></div>;
|
|
725
729
|
}
|
|
726
730
|
|
|
727
731
|
// ---------- 全局浮层:轮询 + 动效(功能启用即常驻;通知在 features/notify/) ----------
|
|
@@ -889,9 +893,13 @@ export function AnimationOverlay(props) {
|
|
|
889
893
|
const phase = active.length > 0
|
|
890
894
|
? active.reduce((best, x) => (!best || (x.phaseAt || 0) > (best.phaseAt || 0) ? x : best), null).phase
|
|
891
895
|
: "think";
|
|
892
|
-
//
|
|
896
|
+
// 有会话在等用户介入时,状态文字让位给"等待确认 / 等待回答"(优先级最高:不动手就停住)。
|
|
893
897
|
// 会话级卡片(等待确认/编程等待中)只对"当前正在看的会话"出现,别的会话等待不打扰。
|
|
894
|
-
|
|
898
|
+
// 等人介入的口径统一走 src/shared.js 的 taskWaiting——只认 approvals 会把「提问等回答」
|
|
899
|
+
// 漏成"编程等待中",甚至被卡住判定误报(2026-11-01 用户实测)。
|
|
900
|
+
const waitingTask = active.find((x) => taskWaiting(x)) || null;
|
|
901
|
+
// 等的是批准还是回答:徽标文字跟着变,别一律说「等待确认」把提问吞进去
|
|
902
|
+
const waitWord = waitingTask && taskApprovals(waitingTask).length === 0 ? "等待回答" : "等待确认";
|
|
895
903
|
const sessionTitleRef = useRef({ at: 0, title: "" });
|
|
896
904
|
const currentTitle = useMemo(() => {
|
|
897
905
|
// 标题查询要 getBoundingClientRect(强制布局),限频 1s;面板页/任务行为不受此限制
|
|
@@ -903,7 +911,7 @@ export function AnimationOverlay(props) {
|
|
|
903
911
|
return c.title;
|
|
904
912
|
}, [snap.status]);
|
|
905
913
|
const onScreenTasks = useMemo(() => active.filter((x) => taskOnScreen(x, currentTitle)), [active, currentTitle]);
|
|
906
|
-
const waitingTaskOnScreen = onScreenTasks.find((x) =>
|
|
914
|
+
const waitingTaskOnScreen = onScreenTasks.find((x) => taskWaiting(x)) || null;
|
|
907
915
|
useTicker(ctx, animOn);
|
|
908
916
|
// 徽标计时:以最早开始的任务为基准
|
|
909
917
|
const now = Date.now();
|
|
@@ -936,7 +944,13 @@ export function AnimationOverlay(props) {
|
|
|
936
944
|
const spaceGalaxies = spaceModeOn && spaceDocks ? spaceGalaxyLayout() : null;
|
|
937
945
|
// Host 在编程阶段持续 18 秒没有 chunk/tool 活动,才把它视为卡住;正常长任务不会误触发。
|
|
938
946
|
// 卡住卡片同样只对当前会话出现。
|
|
939
|
-
|
|
947
|
+
// 停在原地等人的会话不算卡住(等批准、等回答都是合法停点,会话零事件是必然的)——
|
|
948
|
+
// 与【任务通知】的卡住判定同一口径:两处都直接调 src/shared.js 的 taskStalled,
|
|
949
|
+
// 它已经把「有在飞的工具调用」「正在压缩上下文」这两种正在干活的安静排除在外
|
|
950
|
+
//(2026-11-02 实测:25 分钟里 18 次误报全落在 compaction 窗口内,最长 96.7 秒)。
|
|
951
|
+
// 别在这里另写一份表达式,否则动画报卡住而通知不报,自相矛盾。
|
|
952
|
+
const stuckTask = onScreenTasks.find((task) => task.phase === "code"
|
|
953
|
+
&& taskStalled(task, now, 18000)) || null;
|
|
940
954
|
const robotScaleFromConfig = Math.max(.85, Math.min(2.2, Number(cfg && cfg.robotScale) || 1.35));
|
|
941
955
|
const speedStyle = { "--dkan-speed": Number(speed).toFixed(2) };
|
|
942
956
|
const saveRobotScale = useCallback((robotScale) => {
|
|
@@ -1050,7 +1064,7 @@ export function AnimationOverlay(props) {
|
|
|
1050
1064
|
? <AmbientLayer mode={mode} phase={phase} speed={Number(speed).toFixed(2)} tasks={active} galaxies={spaceGalaxies} /> : null}
|
|
1051
1065
|
{/* 星际模式:空闲时在输入框上方待命;任务卡住时在中央呈现编程等待状态。 */}
|
|
1052
1066
|
{spaceModeOn && spaceDocks && active.length === 0 && deliveries.length === 0 ? <ReadyFreighter dock={spaceDocks.ready} /> : null}
|
|
1053
|
-
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={
|
|
1067
|
+
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={taskWaiting(stuckTask)} /> : null}
|
|
1054
1068
|
{!panelOpen ? departures.map((departure) => <MissionDeparture key={departure.id} departure={departure} />) : null}
|
|
1055
1069
|
{/* 环屏巡航:一颗光点沿屏幕边缘转圈(orbit 模式) */}
|
|
1056
1070
|
{ambientOn && mode === "orbit" ? <div className="dkan-orbit" style={speedStyle} aria-hidden="true" /> : null}
|
|
@@ -1059,7 +1073,7 @@ export function AnimationOverlay(props) {
|
|
|
1059
1073
|
{/* 桌面伙伴:背侧视角紧凑双工位人物,阶段与任务同步;整卡可拖到任意位置(robot 模式) */}
|
|
1060
1074
|
{ambientOn && mode === "robot" ? (
|
|
1061
1075
|
<div className="dkan-botcard" role="button" tabIndex={0} style={Object.assign({}, botCardStyle, waitingTask ? { "--dkan-phase": "#b45309" } : null)}
|
|
1062
|
-
title={active.length + " 个任务 · " + (waitingTask ?
|
|
1076
|
+
title={active.length + " 个任务 · " + (waitingTask ? waitWord : phaseLabel(phase)) + " · 点击查看任务动画页 · 可拖动到任意位置"}
|
|
1063
1077
|
onPointerDown={onBotPointerDown}
|
|
1064
1078
|
onPointerMove={onBotPointerMove}
|
|
1065
1079
|
onPointerUp={onBotPointerUp}
|
|
@@ -1068,7 +1082,7 @@ export function AnimationOverlay(props) {
|
|
|
1068
1082
|
onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onBotClick(); } }}>
|
|
1069
1083
|
<RobotScene phase={phase} tasks={active} />
|
|
1070
1084
|
<span className="dkan-bot-cap">
|
|
1071
|
-
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ?
|
|
1085
|
+
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? waitWord : phaseLabel(phase)}
|
|
1072
1086
|
</span>
|
|
1073
1087
|
<button type="button" className="dkan-bot-resize" aria-label="调整桌面伙伴大小"
|
|
1074
1088
|
title="拖动调整桌面伙伴大小"
|
|
@@ -1089,14 +1103,14 @@ export function AnimationOverlay(props) {
|
|
|
1089
1103
|
{ambientOn && mode !== "robot" ? (
|
|
1090
1104
|
<button type="button" className={"dkan-badge" + (mode === "breathe" ? " dkan-badge-breathe" : "")}
|
|
1091
1105
|
style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(waitingTask ? "code" : phase) })}
|
|
1092
|
-
title={active.length + " 个任务进行中 · " + (waitingTask ?
|
|
1106
|
+
title={active.length + " 个任务进行中 · " + (waitingTask ? waitWord : phaseLabel(phase)) + " · 点击查看任务动画页"}
|
|
1093
1107
|
onClick={() => openPanel("animation")}>
|
|
1094
1108
|
<span className="dkan-dotwrap">
|
|
1095
1109
|
<span className="dkan-dot" />
|
|
1096
1110
|
{mode === "breathe" ? <span className="dkan-halo" /> : null}
|
|
1097
1111
|
{mode === "ring" ? <span className="dkan-ring" /> : null}
|
|
1098
1112
|
</span>
|
|
1099
|
-
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ?
|
|
1113
|
+
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? waitWord : phaseLabel(phase)}</span>
|
|
1100
1114
|
</button>
|
|
1101
1115
|
) : null}
|
|
1102
1116
|
{/* 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场 */}
|
|
@@ -1261,7 +1275,7 @@ function AnimationView(props) {
|
|
|
1261
1275
|
// 运行状态(进行中任务 / 最近完成 / 等待确认)已独立成左侧菜单项,这里只留一句指路
|
|
1262
1276
|
rows.push(
|
|
1263
1277
|
<div key="hint" className="dkan-note">
|
|
1264
|
-
任务运行状态(进行中任务、阶段、Token
|
|
1278
|
+
任务运行状态(进行中任务、阶段、Token、等人介入(等批准 / 等回答)与最近完成)已独立成左侧「运行状态」菜单;本页只负责动效。
|
|
1265
1279
|
</div>
|
|
1266
1280
|
);
|
|
1267
1281
|
return <div className="dkan-root">{rows}</div>;
|
package/features/balance/view.js
CHANGED
|
@@ -260,10 +260,10 @@ function chipBalanceText(p) {
|
|
|
260
260
|
const b = p && p.balance;
|
|
261
261
|
if (!b) return "余额 …";
|
|
262
262
|
if (b.status !== "ok") {
|
|
263
|
-
if (b.status === "login-required") return "
|
|
264
|
-
if (b.status === "no-credential") return "
|
|
265
|
-
if (b.status === "unsupported") return "
|
|
266
|
-
return "
|
|
263
|
+
if (b.status === "login-required") return "余额 需登录";
|
|
264
|
+
if (b.status === "no-credential") return "余额 无密钥";
|
|
265
|
+
if (b.status === "unsupported") return "余额 不支持";
|
|
266
|
+
return "余额 查询失败";
|
|
267
267
|
}
|
|
268
268
|
if (b.kind === "quota") {
|
|
269
269
|
const v = Number(b.remaining);
|
|
@@ -289,11 +289,12 @@ function BalanceChip(props) {
|
|
|
289
289
|
const def = selProvider || (data && data.default ? data.default.provider : null);
|
|
290
290
|
const cur = def ? providers.find((p) => p.id === def) : null;
|
|
291
291
|
const accent = cur ? accentOf(cur.id) : "#64748b";
|
|
292
|
-
const text = snap.error ? "
|
|
292
|
+
const text = snap.error ? "余额 失败" : (!data ? (snap.loading ? "余额 …" : "余额 —") : (cur ? chipBalanceText(cur) : "余额 —"));
|
|
293
293
|
const selModel = sel && (sel.model || (sel.selected && sel.selected.model)) || null;
|
|
294
294
|
const title = cur
|
|
295
295
|
? (cur.displayName || cur.id) + (selModel ? " · 当前模型 " + selModel : "") + " · 点击在功能坞查看余额详情"
|
|
296
296
|
: "点击打开功能坞 · 模型余额";
|
|
297
|
+
const mm = /^(余额 )([\s\S]*)$/.exec(text);
|
|
297
298
|
return react.createElement("button", {
|
|
298
299
|
type: "button",
|
|
299
300
|
className: "dockchip" + (snap.error || (cur && cur.balance && cur.balance.status === "error") ? " err" : ""),
|
|
@@ -302,7 +303,8 @@ function BalanceChip(props) {
|
|
|
302
303
|
onClick: () => openPanel("balance", def ? { provider: def } : null),
|
|
303
304
|
},
|
|
304
305
|
react.createElement("span", { className: "dockchip-dot", style: { background: accent } }),
|
|
305
|
-
react.createElement("span",
|
|
306
|
+
mm ? react.createElement("span", { className: "dockchip-pre" }, mm[1]) : null,
|
|
307
|
+
react.createElement("span", null, mm ? mm[2] : text));
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
export const feature = {
|
package/features/mobile/host.js
CHANGED
|
@@ -1,315 +1,22 @@
|
|
|
1
|
-
// dsh-dock · 功能模块【手机适配】·
|
|
1
|
+
// dsh-dock · 功能模块【手机适配】· 宿主半部(开关占位)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// - 只有【手机适配】启用时才有手机端 chrome(含未开远程访问的本机窄屏);
|
|
8
|
-
// - 停用【手机适配】= 整套窄屏排版还原,与远程访问开关互不影响。
|
|
3
|
+
// 手机适配的实现全部在客户端半部(features/mobile/view.js):样式与行为在 Overlay
|
|
4
|
+
// 挂载时注入、卸载时移除,所以这个宿主半部**什么都不注入**,只保留一个 feature 描述符,
|
|
5
|
+
// 让功能开关能照常持久化(settings 的 features 表按 id 记开关,路由 /dsh-dock/features
|
|
6
|
+
// 用 FEATURES 列表校验 id 是否存在)。
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// 注入三件事(幂等,均可重复调用):
|
|
16
|
-
// 1) MOBILE_LAYOUT_CSS:窄屏排版——设置/功能坞弹层纵向堆叠、侧栏浮层化、左上角汉堡菜单、
|
|
17
|
-
// 抽屉遮罩、滚动体留白、滚动兜底(见下);
|
|
18
|
-
// 2) MOBILE_BEHAVIOR_JS:窄屏行为——遮罩显隐、汉堡按钮(展开抽屉)、软键盘让位、
|
|
19
|
-
// 「点会话收抽屉、点文件夹不收」的全局捕获。
|
|
20
|
-
// 客户端半部(features/mobile/view.js)在宿主脚本缺位时自建同一套 chrome,两者用
|
|
21
|
-
// window.__dshDockMobileDrawer 先到先得互斥,绝不各建一套(曾出现双 Tab 栏与 CSS 互相覆盖)。
|
|
22
|
-
//
|
|
23
|
-
// ⚠️ 滚动兜底(本轮修复的核心):手机上出现「哪都滑不动」。实测两处独立根因——
|
|
24
|
-
// a) 设置弹层:本模块把 [role=dialog][class*=panel] 改成 flex-direction:column + height:100vh,
|
|
25
|
-
// 内容列是默认 min-height:auto 的 flex item,于是被内容撑高(实测 1216px 塞在 844px 里),
|
|
26
|
-
// 再被父级 overflow:hidden 裁掉,列内的 [class*=options] 滚不动。补 min-height:0 后
|
|
27
|
-
// 实测 scrollTop 0 → 425。详见 MOBILE_LAYOUT_CSS 内注释与 scripts/test-mobile-host.mjs。
|
|
28
|
-
// b) 通用兜底:给所有纵向滚动体显式声明 touch-action:pan-y + iOS 惯性滚动,避免祖先的
|
|
29
|
-
// 合成层/触控声明把触摸滚动吃掉(无头 390×844 实测:会话长文 50000px、侧栏列表、
|
|
30
|
-
// 功能坞内容均可滚动)。
|
|
31
|
-
//
|
|
32
|
-
// 类名用语义后缀匹配(哈希前缀随 DSH 版本会变,后缀稳定);DSH 升级改了结构时选择器
|
|
33
|
-
// 自然失效,不影响其他功能。
|
|
34
|
-
const MOBILE_LAYOUT_CSS = [
|
|
35
|
-
'@media (max-width:700px){',
|
|
36
|
-
// ── 设置/插件管理弹层:官方只有「左导航 + 右内容」两栏,窄屏内容栏被压到不足百像素
|
|
37
|
-
// (一字一行)。改纵向堆叠:导航横排在上、内容占满。
|
|
38
|
-
// ⚠️ min-height:0 是本轮的关键修复:内容列若是默认 min-height:auto,会被内容撑高、
|
|
39
|
-
// 再被 .panel 的 overflow:hidden 裁掉,列内滚动容器([class*=options])因此滚不动。
|
|
40
|
-
' [role="dialog"][class*="panel"]{flex-direction:column;width:100vw!important;max-width:100vw!important;height:100vh!important;height:100dvh!important;max-height:none!important;border-radius:0!important}',
|
|
41
|
-
' [role="dialog"][class*="panel"]>[class*="nav"]{flex:none!important;width:auto!important;height:auto!important;flex-direction:row;align-items:center;gap:2px;padding:6px 8px;overflow-x:auto;border-bottom:1px solid color-mix(in srgb,gray 25%,transparent)}',
|
|
42
|
-
' [role="dialog"][class*="panel"] [class*="navList"]{flex-direction:row;overflow-x:auto;gap:2px}',
|
|
43
|
-
' [role="dialog"][class*="panel"] [class*="navTitle"]{flex:none;white-space:nowrap;margin-right:6px}',
|
|
44
|
-
' [role="dialog"][class*="panel"]>[class*="nav"] [class*="navCell"]{flex:none}',
|
|
45
|
-
' [role="dialog"][class*="panel"]>[class*="content"]{flex:1 1 auto!important;width:auto!important;min-width:0!important;min-height:0!important;max-width:none!important}',
|
|
46
|
-
// ── 侧栏:窄屏不再用 grid 栅格挤占会话区(会话区只剩 ~110px),改为浮层覆盖。
|
|
47
|
-
' [class$="frame"]{grid-template-columns:0px minmax(0,1fr) 0px!important}',
|
|
48
|
-
' [class$="_handle"]{display:none!important}',
|
|
49
|
-
' [class$="frame"]:not([data-sidebar-collapsed]) [class*="sidebarCol"]{position:fixed!important;top:0!important;bottom:0!important;left:0!important;width:min(85vw,320px)!important;z-index:80!important;box-shadow:0 12px 48px rgba(0,0,0,.45)}',
|
|
50
|
-
// 右侧详情列:dsh 0.1.7 起 detailsCol 改名 rightbarCol(data-rightbar-col),且窄屏
|
|
51
|
-
// (<768px)原生就把面板切成全屏(data-sidebar-right-panel="fullscreen",自身 100vw
|
|
52
|
-
// 盖满视口),不再需要旧版「固定到右缘抽屉」的兜底规则。
|
|
53
|
-
// ⚠️ 实测原生全屏态两个缺陷(手机上双重曝光):① 面板背景透明——会话页 Hero 从缝隙里
|
|
54
|
-
// 整屏透出来,两层内容叠花;② 面板/panelBody 是 pointer-events:none,空隙处的点击
|
|
55
|
-
// 穿透到下层(能误点到 Hero 卡片和底部 Tab)。这里补不透明背景 + 拦截穿透;z 88 把
|
|
56
|
-
// 底部 Tab(65) 压在面板下,配合行为脚本 rightOpen 时主动隐藏(双保险)。
|
|
57
|
-
' [class*="rightbarCol"]{min-width:0}',
|
|
58
|
-
' [data-sidebar-right-panel="fullscreen"][data-sidebar-right-open]{z-index:88!important;pointer-events:auto!important;background:var(--dsw-alias-bg-layer-2,rgb(22,24,28))!important}',
|
|
59
|
-
' [class*="overlayLayer"]{z-index:90!important}',
|
|
60
|
-
// ── 抽屉遮罩:侧栏浮层展开时盖住页面,点击收起(阻断滚动穿透,故 touch-action:none)。
|
|
61
|
-
// 会话入口统一走左上角汉堡菜单,不设左缘浮动把手、不再有底部常驻 Tab 栏。
|
|
62
|
-
' .dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}',
|
|
63
|
-
// ── 左上角汉堡菜单(三条横线,44×44 触控面积,避让刘海/圆角安全区)。
|
|
64
|
-
// z 序 66:高于正文,低于遮罩 75 / 抽屉 80 / overlayLayer 90 / 功能坞面板 200,
|
|
65
|
-
// 这些浮层打开时自然盖住汉堡。
|
|
66
|
-
// ⚠️ 视觉上刻意「不贴合卡片」:页面左上角本来就是空的,原生同类控件(右上的侧栏开关)
|
|
67
|
-
// 是**裸图标**——无底色、无边框、无阴影。早期版本做成白底圆角卡片 + 阴影 + 模糊,
|
|
68
|
-
// 悬浮在白底页面上像一块贴上去的异物(用户实测反馈「没有贴合边缘,很突兀」)。
|
|
69
|
-
// 现在与原生同款:透明底、无边框无阴影,只留图标;点按才给极轻的底色反馈。
|
|
70
|
-
' .dsh-mobile-burger{position:fixed;left:env(safe-area-inset-left,0px);top:env(safe-area-inset-top,0px);z-index:66;box-sizing:border-box;width:44px;height:44px;display:flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:0;background:transparent;color:var(--dsw-alias-label-primary,#e6eaf2);cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation;transition:color .15s ease}',
|
|
71
|
-
' .dsh-mobile-burger svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round}',
|
|
72
|
-
' .dsh-mobile-burger.on{color:#4d9fff}',
|
|
73
|
-
' .dsh-mobile-burger:active{background:color-mix(in srgb,currentColor 12%,transparent)}',
|
|
74
|
-
// 待确认任务角标:挂在汉堡右上角。底部 Tab 栏移除前这个提醒挂在「插件」页签上,
|
|
75
|
-
// 现在改挂汉堡——小圆点/数字锚在按钮右上角,不占布局、不破坏裸图标观感。
|
|
76
|
-
// 等待确认(要人介入)= 红点 + 脉冲;进行中 = 蓝色数字(弱一级)。
|
|
77
|
-
' .dsh-mobile-burger-badge{position:absolute;top:4px;right:4px;box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#4d9fff;color:#fff;font:700 9px/16px inherit;font-variant-numeric:tabular-nums;text-align:center;pointer-events:none;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1,#12151b)}',
|
|
78
|
-
' .dsh-mobile-burger-badge[hidden]{display:none}',
|
|
79
|
-
' .dsh-mobile-burger-badge.wait{background:#ef4444;animation:dsh-burger-badge-pulse 1.2s ease-in-out infinite}',
|
|
80
|
-
' @keyframes dsh-burger-badge-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}',
|
|
81
|
-
' @media (prefers-reduced-motion:reduce){.dsh-mobile-burger-badge.wait{animation:none}}',
|
|
82
|
-
// ── 给标题让出汉堡宽度(用户实测:标题被汉堡压住——截图里「更新所有仓库代码」的首字
|
|
83
|
-
// 直接叠在三条横线上)。汉堡是 fixed 定位(贴安全区、44×44),**不占布局**,而原生
|
|
84
|
-
// 标题从头部的 padding-left:20px 起排——真实实例 390×844 逐字实测:会话标题首字
|
|
85
|
-
// x=28、汉堡右缘 44 → **重叠 16px**;插件页/自动化任务页的页头(自身 padding-left:0,
|
|
86
|
-
// 靠外层容器给 24px)首字 x=24 → 重叠 20px。
|
|
87
|
-
// 改法照原生排版习惯走:**给容器补内边距**,而不是给汉堡加底色/加宽(那是把问题搬到
|
|
88
|
-
// 视觉上)。与客户端半部(features/mobile/view.js)逐字同款,改一处必须同步另一处。
|
|
89
|
-
' header:has([class*="titleRow"]){padding-left:calc(44px + env(safe-area-inset-left,0px))}',
|
|
90
|
-
' [class$="_pageHead"],[class$="_pageHeading"]{padding-left:calc(28px + env(safe-area-inset-left,0px));gap:4px}',
|
|
91
|
-
// ── 输入区吸附到视口底部:欢迎内容原生是 justify-content:center 垂直居中,
|
|
92
|
-
// 手机视口下输入卡片悬在中部、下方留出大片空白。改用「输入区 margin-top:auto」贴底。
|
|
93
|
-
// 底部 Tab 栏已移除,滚动体留白从 72px(Tab 栏高度 + 安全区)进一步收到
|
|
94
|
-
// **只剩安全区**:真实实例 390×844 像素扫描实测,输入卡片下方还有 51px 空白带,
|
|
95
|
-
// 其中 24px 是下面这条滚动体内边距、6px 是 composerSeat 的内边距——都是 Tab 栏时代的
|
|
96
|
-
// 遗留,没有底栏后纯属浪费;收掉后这 30px 直接变成会话区可显示的正文。
|
|
97
|
-
// dsh 自带的 ~21px 卡片下边距保留,视觉仍有呼吸感、不会贴死屏幕边缘。
|
|
98
|
-
//
|
|
99
|
-
// ⚠️ 绝不能再给滚动体加 justify-content:flex-end/center 来贴底:那会把内容挤到
|
|
100
|
-
// 滚动体起点之外,而 Chrome 不把起点方向的溢出算进 scrollHeight(手机视口实测
|
|
101
|
-
// scrollHeight==clientHeight、maxScroll==0),结果是会话记录完全滚不动、首条消息
|
|
102
|
-
// 永远停在视口上方。auto 外边距只在有剩余空间时生效、不产生起点溢出。
|
|
103
|
-
//
|
|
104
|
-
// 软键盘弹出(visualViewport 判定,行为脚本挂 html.dsh-dock-kbd):汉堡让位。
|
|
105
|
-
' html:not(.dsh-dock-kbd) [class*="scrollBody"]{padding-bottom:env(safe-area-inset-bottom,0px)!important}',
|
|
106
|
-
' html.dsh-dock-kbd [class*="scrollBody"]{padding-bottom:env(safe-area-inset-bottom,0px)!important}',
|
|
107
|
-
' html.dsh-dock-kbd .dsh-mobile-burger{display:none!important}',
|
|
108
|
-
' [class*="scrollBody"]>[class*="composerSeat"]{margin-top:auto;padding-bottom:0}',
|
|
109
|
-
// ── 滚动兜底(见文件头 a/b 两条根因):所有纵向滚动体显式声明可纵向平移 + iOS 惯性滚动,
|
|
110
|
-
// 避免祖先合成层/触控声明把触摸滚动吃掉;overscroll-behavior 防滚动穿透到外层。
|
|
111
|
-
' [class*="scrollBody"],[class*="sidebarCol"] [class*="list"],[role="dialog"] [class*="options"],[class*="dockm-content"]{-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;touch-action:pan-y}',
|
|
112
|
-
// 输入区工具行上的 dsh-dock chip(用量/余额):窄屏下 28cqw 的宽度上限把它们挤成
|
|
113
|
-
// 「…」「余..」(手机实测),既读不出数值又白占输入行空间。手机端整组收起,数值在
|
|
114
|
-
// 功能坞(抽屉里进)看;桌面端不动。
|
|
115
|
-
' .dockchip-row{display:none}',
|
|
116
|
-
// 收起的侧栏轨道(collapsed rail)在窄屏只剩 1px 占位,但轨道里的按钮仍会溢出贴在
|
|
117
|
-
// 左边缘(添加工作区/搜索/功能坞/设置等露出半边)。这些入口已由汉堡菜单 + 抽屉接管;
|
|
118
|
-
// 展开抽屉(z 80)不受影响。
|
|
119
|
-
' [class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}',
|
|
120
|
-
// ⚠️ 列级 visibility:hidden 在 dsh 0.2.0 上挡不住:工作区浏览器为了展开/收拢动画给自己的
|
|
121
|
-
// headerActions/sectionLabel 显式声明 visibility:visible(ui-workspace WorkspaceBrowser.module.css),
|
|
122
|
-
// visibility 可被后代覆盖,轨道里的「添加工作区」等按钮就从隐藏列里「透」出来(实测手机
|
|
123
|
-
// 截图左缘游离按钮)。display:none 不可被后代翻转——但只能打在**轨道根节点**上,绝不能
|
|
124
|
-
// 打在列上:frame 是「侧栏轨 0px + 中列 1fr + 右轨 0px」的网格,列 display:none 会从网格
|
|
125
|
-
// 流里摘掉,自动放置把中列顶进 0px 轨(实测中列宽 0、整页空屏)。轨道根(类名同时含
|
|
126
|
-
// root 与 collapsed 两个语义后缀)隐藏后列还在流里占着 0px 轨,中列照常 1fr。
|
|
127
|
-
// 弹层打开时豁免(:not(:has([role="dialog"]))):设置/插件管理弹层挂在列内部
|
|
128
|
-
// (footArea → settingsArea → overlay → panel),display:none 会把它连坐成空气弹层;
|
|
129
|
-
// 此时退回上面的 visibility 机制(由下面那条 [role=dialog] 强制可见),窄屏弹层本身
|
|
130
|
-
// 100vw×100dvh 全屏,透出的轨道也在它下面。
|
|
131
|
-
' [class*="sidebarCol"]:has([class*="collapsed"]):not(:has([role="dialog"])) [class*="root"][class*="collapsed"]{display:none!important}',
|
|
132
|
-
// ⚠️ 设置/插件管理弹层挂在侧栏列内部(footArea → settingsArea → overlay → panel),
|
|
133
|
-
// 上面隐藏整列会把它连坐隐藏——点「设置」得到一个 390×844 的空气弹层(属性齐全但不绘制)。
|
|
134
|
-
// visibility 可被后代覆盖,弹层关闭态会整体卸载(DOM 里没有 [role=dialog]),所以强制
|
|
135
|
-
// 显示只在弹层真正打开时命中,是安全的;列本身继续隐藏,轨道按钮照旧被藏住。
|
|
136
|
-
' [class*="sidebarCol"]:has([class*="collapsed"]) [role="dialog"]{visibility:visible}',
|
|
137
|
-
// 功能坞面板在手机端抬到原生弹层之上:从侧栏进的官方插件管理页等原生弹层 z 序高于
|
|
138
|
-
// 面板默认的 200,功能坞开在下面会被盖住——「插件」页签点了像没反应/露出原页面。
|
|
139
|
-
' [class*="dockm-backdrop"]{z-index:2000!important}',
|
|
140
|
-
'}',
|
|
141
|
-
].join('\n')
|
|
142
|
-
const MOBILE_LAYOUT_MARKER = 'data-dsh-mobile-layout'
|
|
143
|
-
|
|
144
|
-
// 窄屏行为补丁:侧边栏浮层化后,原生的「保持展开」会一直挡住半屏——
|
|
145
|
-
// 点会话行/新会话后自动收起;点侧栏外的页面区域也收起(浮层语义)。
|
|
146
|
-
// 点工作区文件夹**不收**(只展开/收拢分组);点侧栏入口(插件/设置…)等面板弹出后再收,
|
|
147
|
-
// 避免面板被抽屉盖住(等于「点了没反应」)。会话入口是左上角汉堡菜单。
|
|
148
|
-
// 注入点在 <head> 后,body 尚未解析,DOM 创建一律推迟到 DOMContentLoaded;
|
|
149
|
-
// 显隐同步走 MutationObserver(属性翻转即时)+ 低速轮询(覆盖路由重渲等一切边角)。
|
|
150
|
-
const MOBILE_BEHAVIOR_JS = [
|
|
151
|
-
';(function(){',
|
|
152
|
-
'var mq=window.matchMedia?window.matchMedia("(max-width:700px)"):null;',
|
|
153
|
-
'if(!mq)return;',
|
|
154
|
-
// 与 dsh-dock 客户端 Overlay 版(features/mobile/view.js,刷新即生效)互斥:
|
|
155
|
-
// 本脚本在 <head> 先执行,存在即接管;客户端版看到旗标自动让位。
|
|
156
|
-
'if(window.__dshDockMobileDrawer)return;',
|
|
157
|
-
'window.__dshDockMobileDrawer="host";',
|
|
158
|
-
'function narrow(){return mq.matches}',
|
|
159
|
-
'function frameEl(){return document.querySelector(\'[class$="frame"]\')}',
|
|
160
|
-
'function sidebarCollapsed(){var f=frameEl();return !!f&&f.hasAttribute("data-sidebar-collapsed")}',
|
|
161
|
-
'function dialogOpen(){return !!document.querySelector(\'[role="dialog"][class*="panel"]\')}',
|
|
162
|
-
'function collapse(){var b=document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');if(b)b.click()}',
|
|
163
|
-
'function expand(){var b=document.querySelector(\'button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]\');if(b)b.click()}',
|
|
164
|
-
'function closeDetails(){var d=document.querySelector(\'[data-sidebar-right-panel][data-sidebar-right-open]\');if(!d)return;var c=d.querySelector(\'[data-sidebar-right-toggle]\');if(c)c.click()}',
|
|
165
|
-
'var scrim=null,burger=null,badge=null,runstateGone=false;',
|
|
166
|
-
// 左上角汉堡菜单:语义上就是原「会话」页签——点开左侧会话抽屉。插件/设置/运行状态等
|
|
167
|
-
// 入口本来就在抽屉里(原生侧栏 rail),展开即可达,不重复提供。
|
|
168
|
-
'function ensureChrome(){',
|
|
169
|
-
' if(!scrim){scrim=document.createElement("div");scrim.className="dsh-mobile-scrim";document.body.appendChild(scrim)}',
|
|
170
|
-
// 兜底摘除历史上可能残留的旧 chrome(旧 Tab 栏),保证全程只有一份。
|
|
171
|
-
' var old=document.querySelectorAll(".dsh-mobile-tabbar");for(var i=0;i<old.length;i++)old[i].remove();',
|
|
172
|
-
' if(!burger){',
|
|
173
|
-
' burger=document.createElement("button");burger.type="button";burger.className="dsh-mobile-burger";',
|
|
174
|
-
' burger.setAttribute("aria-label","打开会话列表");burger.setAttribute("aria-expanded","false");',
|
|
175
|
-
' burger.innerHTML=\'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg><span class="dsh-mobile-burger-badge" hidden></span>\';',
|
|
176
|
-
' badge=burger.querySelector(".dsh-mobile-burger-badge");',
|
|
177
|
-
' burger.addEventListener("click",function(){expand()});',
|
|
178
|
-
' document.body.appendChild(burger);',
|
|
179
|
-
' }',
|
|
180
|
-
'}',
|
|
181
|
-
'function syncBurger(){',
|
|
182
|
-
' if(!burger)return;',
|
|
183
|
-
' var expanded=!sidebarCollapsed();',
|
|
184
|
-
' burger.setAttribute("aria-expanded",expanded?"true":"false");',
|
|
185
|
-
' if(expanded)burger.classList.add("on");else burger.classList.remove("on");',
|
|
186
|
-
'}',
|
|
187
|
-
// 待确认任务角标:挂汉堡右上角(替代底部 Tab 栏「插件」页签上那个角标)。
|
|
188
|
-
// 等待确认=红点脉冲(要人介入),进行中=蓝色数字。runstate 路由不存在时静默停轮询。
|
|
189
|
-
// ⚠️ 宿主注入脚本拿不到客户端的功能开关,因此靠【运行状态】注册路由时置的旗标
|
|
190
|
-
// window.__dshDockRunstate(features/runstate/host.js)判断:功能停用时宿主根本不注册
|
|
191
|
-
// 该路由,无条件轮询会先打一次必然 405/404 的请求(控制台一条噪音),
|
|
192
|
-
// 与【用量记录】chip 当年同一个毛病。旗标缺失/为假就完全不轮询。
|
|
193
|
-
'function pollTasks(){',
|
|
194
|
-
' if(runstateGone||!badge||!narrow()||document.visibilityState==="hidden"||!burger||!burger.isConnected)return;',
|
|
195
|
-
' if(!window.__dshDockRunstate){badge.hidden=true;return}',
|
|
196
|
-
' fetch("dsh-dock/runstate/status",{method:"POST",headers:{"content-type":"application/json"},body:"{}"}).then(function(r){',
|
|
197
|
-
' if(r.status===404||r.status===405){runstateGone=true;badge.hidden=true;return null}',
|
|
198
|
-
' return r.json()',
|
|
199
|
-
' }).then(function(body){',
|
|
200
|
-
' if(!body||!badge)return;',
|
|
201
|
-
' var active=(body.data&&body.data.active)||[],wait=0;',
|
|
202
|
-
' for(var i=0;i<active.length;i++){var ap=active[i].approvals;if(ap&&ap.length)wait+=ap.length}',
|
|
203
|
-
' if(wait>0){badge.textContent=wait>99?"99+":String(wait);badge.className="dsh-mobile-burger-badge wait";badge.hidden=false;burger.setAttribute("title",wait+" 个任务等待你确认")}',
|
|
204
|
-
' else if(active.length>0){badge.textContent=active.length>99?"99+":String(active.length);badge.className="dsh-mobile-burger-badge";badge.hidden=false;burger.setAttribute("title",active.length+" 个任务进行中")}',
|
|
205
|
-
' else{badge.hidden=true;burger.removeAttribute("title")}',
|
|
206
|
-
' }).catch(function(){})',
|
|
207
|
-
'}',
|
|
208
|
-
// 软键盘判定:visualViewport 与布局视口高度差超阈值视为键盘弹出(挂 html.dsh-dock-kbd,
|
|
209
|
-
// 样式表隐藏汉堡并收窄滚动体留白)。
|
|
210
|
-
'var vv=window.visualViewport||null;',
|
|
211
|
-
'function kbdSync(){',
|
|
212
|
-
' var kbd=false;',
|
|
213
|
-
' if(vv&&vv.height>0)kbd=(window.innerHeight-vv.height)>120;',
|
|
214
|
-
' var de=document.documentElement;',
|
|
215
|
-
' if(de.classList.toggle)de.classList.toggle("dsh-dock-kbd",kbd);',
|
|
216
|
-
' else if(kbd)de.classList.add("dsh-dock-kbd");else de.classList.remove("dsh-dock-kbd");',
|
|
217
|
-
'}',
|
|
218
|
-
'function syncChrome(){',
|
|
219
|
-
' kbdSync();',
|
|
220
|
-
' if(!scrim)return;',
|
|
221
|
-
' var on=narrow()&&!!frameEl()&&!dialogOpen();',
|
|
222
|
-
' var col=sidebarCollapsed();',
|
|
223
|
-
// 右侧详情面板在窄屏是原生全屏(z 40),而汉堡 66 比它高会压边——
|
|
224
|
-
// 面板打开时整体让位,收起(面板自带的折叠按钮)后再回来。
|
|
225
|
-
' var rightOpen=!!document.querySelector(\'[data-sidebar-right-panel][data-sidebar-right-open]\');',
|
|
226
|
-
// 遮罩只在「抽屉真的展开」时显示;收起态必须 display:none——它是全屏 touch-action:none
|
|
227
|
-
// 浮层,残留一份会让整页都滑不动。
|
|
228
|
-
' scrim.style.display=(on&&!col)?"":"none";',
|
|
229
|
-
// 汉堡:窄屏且页面骨架就绪后常驻;右侧详情面板打开时隐藏(原生全屏面板会压边)。
|
|
230
|
-
' if(burger)burger.style.display=narrow()&&!!frameEl()&&!rightOpen?"":"none";',
|
|
231
|
-
' syncBurger();',
|
|
232
|
-
'}',
|
|
233
|
-
'function boot(){',
|
|
234
|
-
' ensureChrome();syncChrome();',
|
|
235
|
-
' if(typeof MutationObserver!=="undefined"){new MutationObserver(function(){syncChrome()}).observe(document.documentElement,{attributes:true,attributeFilter:["data-sidebar-collapsed"],subtree:true})}',
|
|
236
|
-
' setInterval(syncChrome,1500);',
|
|
237
|
-
// 角标轮询:低频 3s,仅窄屏 + 页面可见时发;路由不存在则静默停。
|
|
238
|
-
' setInterval(pollTasks,3000);pollTasks();',
|
|
239
|
-
' document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible")pollTasks()});',
|
|
240
|
-
' var onMq=function(){syncChrome()};',
|
|
241
|
-
' if(mq.addEventListener)mq.addEventListener("change",onMq);else if(mq.addListener)mq.addListener(onMq);',
|
|
242
|
-
' if(vv&&vv.addEventListener)vv.addEventListener("resize",kbdSync);',
|
|
243
|
-
' window.addEventListener("resize",kbdSync);',
|
|
244
|
-
'}',
|
|
245
|
-
'if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",boot);else boot();',
|
|
246
|
-
'document.addEventListener("click",function(e){',
|
|
247
|
-
' if(!narrow())return;',
|
|
248
|
-
' var t=e.target;',
|
|
249
|
-
' if(!t||!t.closest)return;',
|
|
250
|
-
' if(t.closest(".dsh-mobile-burger"))return;',
|
|
251
|
-
' var expanded=!!document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');',
|
|
252
|
-
' var col=document.querySelector(\'[class*="sidebarCol"]\');',
|
|
253
|
-
' var inSidebar=col&&col.contains(t);',
|
|
254
|
-
' if(!expanded&&!inSidebar)return;',
|
|
255
|
-
' if(inSidebar){',
|
|
256
|
-
// 只有点「会话」才收抽屉:会话行 data-row-key 以 session: 开头(dsh 0.1.7+ 结构,
|
|
257
|
-
// ui-workspace Rows.tsx);工作区文件夹行是 workspace: 开头——点它只是展开/收拢分组,
|
|
258
|
-
// 绝不能收抽屉(用户实测「点文件夹抽屉被收起」)。搜索结果行无 data-row-key,回退按无子叶判定。
|
|
259
|
-
' var row=t.closest(\'[role="treeitem"]\');',
|
|
260
|
-
' var rowKey=(row&&row.getAttribute("data-row-key"))||"";',
|
|
261
|
-
' var sessionRow=(rowKey&&rowKey.indexOf("session:")===0)||(row&&!rowKey&&!row.querySelector(\'[role="treeitem"]\')&&row.closest(\'[role="tree"]\'));',
|
|
262
|
-
' if(sessionRow||t.closest(\'[class*="newSession"]\')){setTimeout(function(){collapse();closeDetails()},300);return}',
|
|
263
|
-
// 工作区文件夹:只展开/收拢分组——既不收抽屉,也不做「面板弹出即收起」探测(那会把
|
|
264
|
-
// 文件夹点击误判成侧栏入口)。
|
|
265
|
-
' if(rowKey.indexOf("workspace:")===0)return;',
|
|
266
|
-
// 侧栏入口(插件/自动化任务/设置/记忆/自动进化…)点击后会弹出面板,而面板 z 序
|
|
267
|
-
// 常低于展开的抽屉——被盖住等于「点了没反应」(用户实测截图)。稍候探测:有面板
|
|
268
|
-
// 弹出就收起抽屉让它露出来;目录折叠/搜索等不弹面板的点击不受影响。
|
|
269
|
-
' setTimeout(function(){if(document.querySelector(\'[role="dialog"]\'))collapse()},260);',
|
|
270
|
-
' return;',
|
|
271
|
-
' }',
|
|
272
|
-
' if(t.closest(\'[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[data-sidebar-right-panel]\'))return;',
|
|
273
|
-
' collapse()',
|
|
274
|
-
'},true)',
|
|
275
|
-
'})();',
|
|
276
|
-
].join('\n')
|
|
277
|
-
const MOBILE_BEHAVIOR_MARKER = 'data-dsh-mobile-behave'
|
|
278
|
-
|
|
279
|
-
/** 把窄屏排版样式与行为脚本内联注入 index.html 的 <head> 之后(幂等)。 */
|
|
280
|
-
export function injectMobile(html) {
|
|
281
|
-
let out = html
|
|
282
|
-
const match = /<head(?:\s[^>]*)?>/i.exec(out)
|
|
283
|
-
const at = match ? match.index + match[0].length : 0
|
|
284
|
-
const parts = []
|
|
285
|
-
if (!out.includes(MOBILE_LAYOUT_MARKER)) parts.push(`<style ${MOBILE_LAYOUT_MARKER}>${MOBILE_LAYOUT_CSS}</style>`)
|
|
286
|
-
if (!out.includes(MOBILE_BEHAVIOR_MARKER)) parts.push(`<script ${MOBILE_BEHAVIOR_MARKER}>${MOBILE_BEHAVIOR_JS}</script>`)
|
|
287
|
-
if (!parts.length) return out
|
|
288
|
-
const injected = parts.join('')
|
|
289
|
-
return at > 0 ? out.slice(0, at) + injected + out.slice(at) : injected + out
|
|
290
|
-
}
|
|
291
|
-
|
|
8
|
+
// 为什么不再经 webServer.tapIndex 内联注入 index.html:
|
|
9
|
+
// · 旧版这样做必须重启 dsh web 才能彻底摘除注入的 <style>/<script>;用户要求
|
|
10
|
+
// 「关闭就要关闭得彻底、不能干扰原来的」。改成客户端运行时注入 + 卸载清理后,
|
|
11
|
+
// 关闭开关的瞬间样式、body 属性、遮罩/按钮、监听器、观察器、定时器全部消失,
|
|
12
|
+
// index.html 里一个标记都不残留,也不动 <meta viewport>。
|
|
292
13
|
export const feature = {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
disposers.push(ctx.inject(['webServer'], (wsCtx) => {
|
|
302
|
-
if (typeof wsCtx.webServer.tapIndex !== 'function') {
|
|
303
|
-
console.warn('[dsh-dock] 手机适配:webServer.tapIndex 不可用(dsh 版本过旧),窄屏排版未注入')
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
disposers.push(wsCtx.effect(() => wsCtx.webServer.tapIndex(injectMobile)))
|
|
307
|
-
}))
|
|
308
|
-
return () => {
|
|
309
|
-
while (disposers.length) {
|
|
310
|
-
const dispose = disposers.pop()
|
|
311
|
-
try { if (typeof dispose === 'function') dispose() } catch { /* ignore */ }
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
},
|
|
14
|
+
id: 'mobile',
|
|
15
|
+
name: '手机适配',
|
|
16
|
+
description: '手机/窄屏体验优化(客户端运行时注入,关闭后零残留):16px 输入防缩放、触控热区、代码块滚动、会话 chip 收起、设置弹层单列与旧版抽屉兜底',
|
|
17
|
+
defaultEnabled: false,
|
|
18
|
+
setup() {
|
|
19
|
+
// 无宿主副作用;返回空清理函数以匹配生命周期约定。
|
|
20
|
+
return () => {}
|
|
21
|
+
},
|
|
315
22
|
}
|