@wenbin_wb/dsh-bridge 2.10.13 → 2.10.14
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/CHANGELOG.md +23 -0
- package/client/client.js +411 -18
- package/client/index.js +163 -3
- package/client/mobile-styles.js +303 -13
- package/lib/cloudflared-manager.mjs +19 -6
- package/lib/feishu/lark-bundled.mjs +230 -76
- package/package.json +4 -4
package/client/index.js
CHANGED
|
@@ -41,6 +41,21 @@ if (typeof window !== 'undefined') window.__dshResourceUrlCompat = RESOURCE_URL_
|
|
|
41
41
|
// 避免 768px 单点上"桥渲染顶栏、宿主却按桌面(push)布局"的错位。
|
|
42
42
|
const MOBILE_MAX_WIDTH = 767;
|
|
43
43
|
|
|
44
|
+
// 设置中心「两级钻取」的窄屏断点:必须与 mobile-styles.js 的
|
|
45
|
+
// `@media (max-width: 480px)` 6.2 块保持一致(改一处必须改另一处)。
|
|
46
|
+
// 481–767px 仍有 ~350px 以上内容宽度,保留左侧图标轨道即可;
|
|
47
|
+
// <=480px 时轨道要吃掉 21% 宽度、第三方设置节必然溢出,才切换为列表/详情两页。
|
|
48
|
+
const SETTINGS_DRILLDOWN_MAX_WIDTH = 480;
|
|
49
|
+
// 交互层的就绪开关,打在 <html> 上(监听器要在弹窗出现前装好,那时 panel 还不存在)。
|
|
50
|
+
// CSS 6.2 整块以它为前置选择器:开关没打开就整块惰性,<=480px 退回 88px 图标轨道,
|
|
51
|
+
// 不会出现「把内容藏了、却没人负责把用户带进去」的死列表。
|
|
52
|
+
const DRILLDOWN_GATE_ATTR = 'data-dshbr-drilldown';
|
|
53
|
+
const DRILLDOWN_GATE_READY = 'ready';
|
|
54
|
+
const DRILLDOWN_GATE_FLAG = 'dshBrDrilldownGate';
|
|
55
|
+
// 视图状态标记:打在宿主设置弹窗的 panel 上,CSS 6.2 块按此切换列表页/详情页。
|
|
56
|
+
const SETTINGS_VIEW_ATTR = 'data-dshbr-settings-view';
|
|
57
|
+
const SETTINGS_VIEW_DETAIL = 'detail';
|
|
58
|
+
|
|
44
59
|
function isLocalEnvironment() {
|
|
45
60
|
if (typeof window === 'undefined') return true;
|
|
46
61
|
const host = window.location.hostname || '';
|
|
@@ -2608,7 +2623,11 @@ function VersionBanner({ rpcCall }) {
|
|
|
2608
2623
|
gap: 8,
|
|
2609
2624
|
},
|
|
2610
2625
|
},
|
|
2611
|
-
|
|
2626
|
+
// 左组:版本徽标 + DSH 版本 + 检查更新。
|
|
2627
|
+
// flexWrap + minWidth:0 是必需的:此前只写 display:flex,左组既不能收缩也不能换行,
|
|
2628
|
+
// 会把整行顶到 305px;手机设置页内容列只有 277px,溢出部分被
|
|
2629
|
+
// overflow-x:hidden 静默裁掉(「检查更新」按钮被切一半)。
|
|
2630
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 } },
|
|
2612
2631
|
// 版本状态徽标
|
|
2613
2632
|
React.createElement('span', {
|
|
2614
2633
|
style: {
|
|
@@ -2675,6 +2694,10 @@ function VersionBanner({ rpcCall }) {
|
|
|
2675
2694
|
fontSize: 11,
|
|
2676
2695
|
opacity: loading ? 0.5 : 1,
|
|
2677
2696
|
gap: 4,
|
|
2697
|
+
// 按钮是 inline-flex,文字会成为匿名 flex 项;被挤窄时会把「检查更新」
|
|
2698
|
+
// 折成两行。nowrap + flexShrink:0 保证它整体换行(由父级 flexWrap 负责)。
|
|
2699
|
+
whiteSpace: 'nowrap',
|
|
2700
|
+
flexShrink: 0,
|
|
2678
2701
|
},
|
|
2679
2702
|
onClick: check,
|
|
2680
2703
|
disabled: loading || upgrading || restarting,
|
|
@@ -2952,8 +2975,57 @@ const TABS = [
|
|
|
2952
2975
|
{ id: 'ops', label: '运维监控', icon: Icons.ops },
|
|
2953
2976
|
];
|
|
2954
2977
|
|
|
2978
|
+
// Tab 条在窄屏是横向可滑动的(5 个分类在手机内容列里放不下),但滚动条被
|
|
2979
|
+
// mobile-styles.js 整段隐藏(.dsh-tabbar-container),末尾分类又恰好被切断,
|
|
2980
|
+
// 用户既看不到"还有更多"、也不知道能横滑 —— 主观上就是"页面看不到"。
|
|
2981
|
+
// 这里补两件事:按可滚动方向做边缘渐隐提示;切换时把当前项滚进可视区。
|
|
2955
2982
|
function TabBar({ active, onChange, dots }) {
|
|
2983
|
+
const scrollRef = React.useRef(null);
|
|
2984
|
+
const [edges, setEdges] = React.useState({ left: false, right: false });
|
|
2985
|
+
|
|
2986
|
+
React.useEffect(() => {
|
|
2987
|
+
const el = scrollRef.current;
|
|
2988
|
+
if (!el) return undefined;
|
|
2989
|
+
const update = () => {
|
|
2990
|
+
const max = el.scrollWidth - el.clientWidth;
|
|
2991
|
+
const left = el.scrollLeft > 1;
|
|
2992
|
+
const right = max > 1 && el.scrollLeft < max - 1;
|
|
2993
|
+
setEdges((prev) => (prev.left === left && prev.right === right ? prev : { left, right }));
|
|
2994
|
+
};
|
|
2995
|
+
update();
|
|
2996
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
2997
|
+
let observer = null;
|
|
2998
|
+
if (typeof ResizeObserver === 'function') {
|
|
2999
|
+
observer = new ResizeObserver(update);
|
|
3000
|
+
observer.observe(el);
|
|
3001
|
+
}
|
|
3002
|
+
return () => {
|
|
3003
|
+
el.removeEventListener('scroll', update);
|
|
3004
|
+
if (observer) observer.disconnect();
|
|
3005
|
+
};
|
|
3006
|
+
}, []);
|
|
3007
|
+
|
|
3008
|
+
// 当前分类可能在可视区之外(例如从状态点跳进某个 Tab),切换后主动滚入
|
|
3009
|
+
React.useEffect(() => {
|
|
3010
|
+
const el = scrollRef.current;
|
|
3011
|
+
if (!el) return;
|
|
3012
|
+
const target = el.querySelector('[data-dsh-tab-active="true"]');
|
|
3013
|
+
if (!target) return;
|
|
3014
|
+
const elRect = el.getBoundingClientRect();
|
|
3015
|
+
const tabRect = target.getBoundingClientRect();
|
|
3016
|
+
if (tabRect.left < elRect.left) el.scrollLeft -= (elRect.left - tabRect.left) + 8;
|
|
3017
|
+
else if (tabRect.right > elRect.right) el.scrollLeft += (tabRect.right - elRect.right) + 8;
|
|
3018
|
+
}, [active]);
|
|
3019
|
+
|
|
3020
|
+
const maskStops = [];
|
|
3021
|
+
if (edges.left) maskStops.push('transparent 0px', '#000 20px');
|
|
3022
|
+
else maskStops.push('#000 0px');
|
|
3023
|
+
if (edges.right) maskStops.push('#000 calc(100% - 20px)', 'transparent 100%');
|
|
3024
|
+
else maskStops.push('#000 100%');
|
|
3025
|
+
const maskImage = `linear-gradient(to right, ${maskStops.join(', ')})`;
|
|
3026
|
+
|
|
2956
3027
|
return React.createElement('div', {
|
|
3028
|
+
ref: scrollRef,
|
|
2957
3029
|
className: 'dsh-tabbar-container',
|
|
2958
3030
|
style: {
|
|
2959
3031
|
display: 'flex', gap: 4, marginBottom: 20,
|
|
@@ -2962,6 +3034,8 @@ function TabBar({ active, onChange, dots }) {
|
|
|
2962
3034
|
maxWidth: '100%', flexWrap: 'nowrap',
|
|
2963
3035
|
scrollbarWidth: 'none',
|
|
2964
3036
|
msOverflowStyle: 'none',
|
|
3037
|
+
WebkitMaskImage: maskImage,
|
|
3038
|
+
maskImage,
|
|
2965
3039
|
},
|
|
2966
3040
|
},
|
|
2967
3041
|
TABS.map(({ id, label, icon: TabIcon }) => {
|
|
@@ -2969,6 +3043,7 @@ function TabBar({ active, onChange, dots }) {
|
|
|
2969
3043
|
const hasDot = dots?.[id];
|
|
2970
3044
|
return React.createElement('button', {
|
|
2971
3045
|
key: id,
|
|
3046
|
+
'data-dsh-tab-active': isActive ? 'true' : undefined,
|
|
2972
3047
|
onClick: () => onChange(id),
|
|
2973
3048
|
style: {
|
|
2974
3049
|
font: 'inherit', cursor: 'pointer', border: 'none', background: 'none',
|
|
@@ -3757,6 +3832,86 @@ function injectMobileStyles() {
|
|
|
3757
3832
|
document.head.appendChild(style);
|
|
3758
3833
|
}
|
|
3759
3834
|
|
|
3835
|
+
// 设置中心两级钻取的交互层(配合 mobile-styles.js 6.2 的 <=480px 规则)。
|
|
3836
|
+
//
|
|
3837
|
+
// 为什么需要 JS:宿主的设置弹窗(@deepseek-ai/dsh-client-ui-settings-general)只有
|
|
3838
|
+
// 桌面布局,nav 与 content 是 panel 下的兄弟节点、由 React 按 activeId 渲染,
|
|
3839
|
+
// 没有任何"列表页/详情页"状态,也没有暴露给插件的 slot。
|
|
3840
|
+
//
|
|
3841
|
+
// 为什么用「委托点击 + 打 data 属性」而不是注入返回按钮:
|
|
3842
|
+
// panel 内部整棵树由 React 管理,往里插 DOM 会在 React 卸载/重渲染时触发
|
|
3843
|
+
// removeChild 失配。故不插入任何节点,只给 panel 打一个属性;详情页的返回入口
|
|
3844
|
+
// 复用宿主已有的标题行(CSS 把 navList 收起、给 navTitle 加 ‹ 箭头并显示 pointer),
|
|
3845
|
+
// 因此本函数对宿主 DOM 只做「读 + 打属性」,不改变结构。
|
|
3846
|
+
function setupSettingsDrilldown() {
|
|
3847
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
3848
|
+
const root = document.documentElement;
|
|
3849
|
+
// 幂等位刻意放在最前:它先于 matchMedia 守卫,故首次 apply() 时若 matchMedia 不可用,
|
|
3850
|
+
// 本会话内就不再尝试启用钻取。方向是 fail-safe —— 退回 88px 图标轨道、内容完整可达
|
|
3851
|
+
// (CSS 6.2 整段惰性),不会变成死列表。独立验收已确认该方向无风险。
|
|
3852
|
+
if (root.dataset[DRILLDOWN_GATE_FLAG] === '1') return;
|
|
3853
|
+
root.dataset[DRILLDOWN_GATE_FLAG] = '1';
|
|
3854
|
+
|
|
3855
|
+
// 断点谓词与 CSS 开关必须同源:两者都用这一个 MediaQueryList 实例。
|
|
3856
|
+
// 若让 CSS 走 `@media (max-width:480px)`、而 JS 各自去问 matchMedia,一旦
|
|
3857
|
+
// matchMedia 缺失或对该查询返回 false(polyfill/环境差异),就会出现
|
|
3858
|
+
// 「监听器装上了但恒不匹配 → 回调永远空转,而开关照常打开」的死列表。
|
|
3859
|
+
// 因此 matchMedia 不可用时不启用钻取,直接退回 88px 轨道。
|
|
3860
|
+
if (typeof window.matchMedia !== 'function') return;
|
|
3861
|
+
let narrowQuery;
|
|
3862
|
+
try {
|
|
3863
|
+
narrowQuery = window.matchMedia(`(max-width: ${SETTINGS_DRILLDOWN_MAX_WIDTH}px)`);
|
|
3864
|
+
} catch {
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
if (!narrowQuery) return;
|
|
3868
|
+
const isNarrow = () => narrowQuery.matches === true;
|
|
3869
|
+
|
|
3870
|
+
const onDocumentClick = (event) => {
|
|
3871
|
+
if (!isNarrow()) return;
|
|
3872
|
+
const target = event.target;
|
|
3873
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
3874
|
+
|
|
3875
|
+
const panel = target.closest('div[class*="VOzbGW_panel"]');
|
|
3876
|
+
if (!panel) return;
|
|
3877
|
+
const nav = panel.querySelector('nav[class*="VOzbGW_nav"]');
|
|
3878
|
+
if (!nav || !nav.contains(target)) return;
|
|
3879
|
+
|
|
3880
|
+
// 点分类行 → 进入详情页(宿主自身的 onClick 负责切换 activeId,这里不拦截)
|
|
3881
|
+
if (target.closest('button[class*="VOzbGW_navCell"]')) {
|
|
3882
|
+
panel.setAttribute(SETTINGS_VIEW_ATTR, SETTINGS_VIEW_DETAIL);
|
|
3883
|
+
if (nav.scrollTop) nav.scrollTop = 0;
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// 详情页里点折叠后的标题行 → 返回分类列表
|
|
3888
|
+
if (panel.getAttribute(SETTINGS_VIEW_ATTR) === SETTINGS_VIEW_DETAIL) {
|
|
3889
|
+
panel.removeAttribute(SETTINGS_VIEW_ATTR);
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
|
|
3893
|
+
const syncGate = () => {
|
|
3894
|
+
if (narrowQuery.matches === true) root.setAttribute(DRILLDOWN_GATE_ATTR, DRILLDOWN_GATE_READY);
|
|
3895
|
+
else root.removeAttribute(DRILLDOWN_GATE_ATTR);
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
try {
|
|
3899
|
+
document.addEventListener('click', onDocumentClick, false);
|
|
3900
|
+
} catch {
|
|
3901
|
+
// 装不上监听器就不要打开开关:CSS 6.2 保持惰性,<=480px 退回 767 块的 88px 图标轨道
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// 断点变化(转屏/窗口缩放)时让开关跟随,避免开关与实际匹配状态漂移
|
|
3906
|
+
try {
|
|
3907
|
+
if (typeof narrowQuery.addEventListener === 'function') narrowQuery.addEventListener('change', syncGate);
|
|
3908
|
+
else if (typeof narrowQuery.addListener === 'function') narrowQuery.addListener(syncGate);
|
|
3909
|
+
} catch { /* 订阅失败不影响正确性:下面的 syncGate() 仍会按当前 matches 设一次 */ }
|
|
3910
|
+
|
|
3911
|
+
// 监听器装好之后才同步 CSS 开关(见 mobile-styles.js 6.2 块的安全说明)
|
|
3912
|
+
syncGate();
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3760
3915
|
function setupMobileExperience(rpcCall, ctx) {
|
|
3761
3916
|
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
3762
3917
|
injectMobileStyles();
|
|
@@ -4587,8 +4742,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4587
4742
|
}
|
|
4588
4743
|
if (!switched && res?.sessionId && clientCtx?.sessions?.open) {
|
|
4589
4744
|
try {
|
|
4745
|
+
// 最后手段:直接打开会话。成功后无需再置位 switched ——
|
|
4746
|
+
// 此后不再有基于 switched 的分支(CodeQL js/useless-assignment-to-local)。
|
|
4590
4747
|
clientCtx.sessions.open(res.sessionId);
|
|
4591
|
-
switched = true;
|
|
4592
4748
|
} catch (e) {}
|
|
4593
4749
|
}
|
|
4594
4750
|
}
|
|
@@ -4717,8 +4873,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4717
4873
|
|
|
4718
4874
|
if (!switched && clientCtx?.sessions?.open && res.sessionId) {
|
|
4719
4875
|
try {
|
|
4876
|
+
// 最后手段:直接打开会话。此后不再读取 switched
|
|
4877
|
+
// (CodeQL js/useless-assignment-to-local),故不再置位。
|
|
4720
4878
|
clientCtx.sessions.open(res.sessionId);
|
|
4721
|
-
switched = true;
|
|
4722
4879
|
} catch (e) {
|
|
4723
4880
|
console.warn('[dsh-bridge] sessions.open failed:', e);
|
|
4724
4881
|
}
|
|
@@ -5037,6 +5194,9 @@ function setupComposerCollapse() {
|
|
|
5037
5194
|
|
|
5038
5195
|
function apply(ctx) {
|
|
5039
5196
|
window.__dshClientCtx = ctx;
|
|
5197
|
+
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5198
|
+
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5199
|
+
setupSettingsDrilldown();
|
|
5040
5200
|
const rpcCall = (endpoint, payload, signal) =>
|
|
5041
5201
|
ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
5042
5202
|
|
package/client/mobile-styles.js
CHANGED
|
@@ -20,6 +20,55 @@ export const MOBILE_STYLES_CSS = `
|
|
|
20
20
|
--dsh-mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* 挂在 <body> 下、自带遮罩的「模态根」必须能盖在设置弹窗之上。
|
|
24
|
+
—— 这是本插件抬高设置弹窗层叠后必须自己补上的一环(谁抬的谁负责)。
|
|
25
|
+
|
|
26
|
+
故障现象:插件市场设置页点「安装」没有任何反应。
|
|
27
|
+
根因链:
|
|
28
|
+
1) 本插件把设置弹窗抬到 z-index:10002(见下面 @media 块),以便盖住自己渲染的
|
|
29
|
+
移动端顶栏(9998)/抽屉遮罩(9999);
|
|
30
|
+
2) 插件市场(dshmarket 1.44.0)的安装确认框是挂在 <body> 下的
|
|
31
|
+
div._root_w1urq_2,position:fixed,但只有 z-index:1000;
|
|
32
|
+
3) 该浮层内部的对话框 div._dialog_w1urq_22 虽然带 role="dialog"、被下面的
|
|
33
|
+
10005 规则抬起来了,**但 z-index 无法逃出祖先的层叠上下文** —— 外层模态根
|
|
34
|
+
只有 1000,整棵子树(连同那个 10005)一起被设置弹窗(10002)盖死;
|
|
35
|
+
4) 结果:竖屏点安装完全看不到浮层(设置弹窗的遮罩在 elementFromPoint 上命中);
|
|
36
|
+
横屏(宽度 > 767 时本插件样式不生效,设置弹窗回落成宿主的 1000)浮层虽出现,
|
|
37
|
+
却与页面内容互相穿透、显示不完整。
|
|
38
|
+
|
|
39
|
+
选择器刻意不硬编码任何第三方类名/哈希,只认模态根的两个**结构特征**:
|
|
40
|
+
「body 的直接子级」+「自带遮罩子元素(或有 role=dialog 子元素)」。
|
|
41
|
+
实测在打开设置前/后/点安装后三个阶段,本页面上该选择器**只命中市场那一个浮层**,
|
|
42
|
+
无误伤;本插件自己的 #dsh-remote-workspace-modal 有 ID 规则(100000),优先级更高,
|
|
43
|
+
不受影响。仅在设置弹窗打开时生效(body:has(...)),把影响面压到最小。
|
|
44
|
+
|
|
45
|
+
【已知边界】判据只看结构,无法区分「真模态」与「恰好长成这样、且不可关闭的全屏
|
|
46
|
+
遮罩」。若某个插件往 <body> 下挂一个带 mask 子元素的全屏层,它也会被抬到设置
|
|
47
|
+
弹窗之上并盖住设置页。这是「模态就该盖在设置弹窗之上」的既定取舍:宁可让真模态
|
|
48
|
+
可见,也不接受"弹了却点不到"。独立验收已实测该构造会命中;若将来真踩到,
|
|
49
|
+
应改为按「该浮层是否可关闭」进一步收窄,而不是回退这条规则。 */
|
|
50
|
+
body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[class*="mask"], > div[role="dialog"]) {
|
|
51
|
+
z-index: 10050 !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* 同上场景的第二个独立问题:第三方模态对话框「比视口还高」。
|
|
55
|
+
插件市场的安装确认框是固定 584px 高 + overflow-y:hidden,在矮视口(横屏手机
|
|
56
|
+
320–430px 高,以及 320×480 这类小屏)里被居中撑开 → 上下同时被切掉,底部
|
|
57
|
+
「Cancel / Confirm install」落到视口之外,用户点不到、流程走不下去
|
|
58
|
+
(实测 667×375 时对话框 rect y=-116 h=608、按钮 y=432;844×390 同样)。
|
|
59
|
+
|
|
60
|
+
修法是让模态对话框不超过视口并允许内部滚动。这条是**自限性**的:
|
|
61
|
+
max-height 只在「视口比对话框矮」时才起作用 —— 375×667 / 390×844 等正常竖屏下
|
|
62
|
+
100dvh-16px(=651/828) > 584,声明不产生任何视觉变化。
|
|
63
|
+
vh 与 dvh 各写一遍:不支持 dvh 的环境退回 vh;两条都失效也只是保持原样。 */
|
|
64
|
+
body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[role="dialog"]) > div[role="dialog"] {
|
|
65
|
+
/* 留 32px 而不是 16px:该对话框是 content-box + 24px 纵向内边距,
|
|
66
|
+
按内容盒算的 max-height 会再多出内边距的边框盒高度,16px 时仍溢出 4px。 */
|
|
67
|
+
max-height: calc(100vh - 32px) !important;
|
|
68
|
+
max-height: calc(100dvh - 32px) !important;
|
|
69
|
+
overflow-y: auto !important;
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
/* 断点与宿主判据对齐:宿主用 viewportWidth < 768 决定右侧栏自动全屏
|
|
24
73
|
(dsh-client-ui-sidebar-right/lib/client.js),故移动端样式取 <=767px,
|
|
25
74
|
768px 起彻底交还桌面布局,避免"桥渲染顶栏、宿主却未全屏"的错位。 */
|
|
@@ -664,56 +713,91 @@ export const MOBILE_STYLES_CSS = `
|
|
|
664
713
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25) !important;
|
|
665
714
|
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
666
715
|
}
|
|
716
|
+
/* 6.1 481–767px:保留左侧图标轨道。
|
|
717
|
+
78px 轨道把 "Agent presets"/"Plugin Market" 挤到两侧只剩 ~5px、标签被压到
|
|
718
|
+
10.5px 且 word-break:break-all 会从词中间断字,故放宽到 88px / 11.5px,
|
|
719
|
+
并改用 overflow-wrap 兜底(正常单词不再被拆)。 */
|
|
667
720
|
nav[class*="VOzbGW_nav"] {
|
|
668
|
-
width:
|
|
669
|
-
min-width:
|
|
670
|
-
max-width:
|
|
671
|
-
padding: 10px
|
|
721
|
+
width: 88px !important;
|
|
722
|
+
min-width: 88px !important;
|
|
723
|
+
max-width: 88px !important;
|
|
724
|
+
padding: 10px 6px !important;
|
|
672
725
|
box-sizing: border-box !important;
|
|
673
726
|
border-right: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
|
|
674
727
|
display: flex !important;
|
|
675
728
|
flex-direction: column !important;
|
|
676
729
|
gap: 6px !important;
|
|
677
730
|
overflow-y: auto !important;
|
|
731
|
+
overscroll-behavior: contain !important;
|
|
678
732
|
}
|
|
679
733
|
nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
|
|
680
734
|
button[class*="VOzbGW_navCell"] {
|
|
681
|
-
padding:
|
|
735
|
+
padding: 7px 2px !important;
|
|
682
736
|
display: flex !important;
|
|
683
737
|
flex-direction: column !important;
|
|
684
738
|
align-items: center !important;
|
|
685
739
|
justify-content: center !important;
|
|
686
740
|
text-align: center !important;
|
|
687
741
|
height: auto !important;
|
|
688
|
-
min-height:
|
|
689
|
-
|
|
742
|
+
min-height: 46px !important;
|
|
743
|
+
/* flex:none:单元格不得被压缩,否则矮视口下末项会贴着轨道下沿被切掉 */
|
|
744
|
+
flex: 0 0 auto !important;
|
|
745
|
+
gap: 3px !important;
|
|
690
746
|
border-radius: 10px !important;
|
|
691
747
|
}
|
|
692
748
|
span[class*="VOzbGW_navLabel"] {
|
|
693
|
-
font-size:
|
|
694
|
-
line-height: 1.
|
|
749
|
+
font-size: 11.5px !important;
|
|
750
|
+
line-height: 1.25 !important;
|
|
695
751
|
white-space: normal !important;
|
|
696
|
-
word-break:
|
|
752
|
+
word-break: normal !important;
|
|
753
|
+
overflow-wrap: anywhere !important;
|
|
697
754
|
text-align: center !important;
|
|
698
755
|
}
|
|
756
|
+
|
|
757
|
+
/* 矮视口(横屏手机):压缩轨道单元格,保证 6 个分类全部落在面板内,
|
|
758
|
+
而不是靠 nav 自身的 overflow-y 滚动把末项藏起来(当前无任何可滚动提示)。 */
|
|
759
|
+
@media (max-height: 500px) {
|
|
760
|
+
nav[class*="VOzbGW_nav"] {
|
|
761
|
+
padding: 8px 6px !important;
|
|
762
|
+
gap: 4px !important;
|
|
763
|
+
}
|
|
764
|
+
nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
|
|
765
|
+
button[class*="VOzbGW_navCell"] {
|
|
766
|
+
min-height: 40px !important;
|
|
767
|
+
padding: 4px 2px !important;
|
|
768
|
+
}
|
|
769
|
+
div[class*="VOzbGW_navTitle"] {
|
|
770
|
+
font-size: 13px !important;
|
|
771
|
+
line-height: 1.3 !important;
|
|
772
|
+
padding: 0 4px !important;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
699
776
|
div[class*="VOzbGW_content"] {
|
|
700
777
|
flex: 1 1 auto !important;
|
|
701
778
|
min-width: 0 !important;
|
|
702
|
-
width: calc(100% -
|
|
703
|
-
max-width: calc(100% -
|
|
779
|
+
width: calc(100% - 88px) !important;
|
|
780
|
+
max-width: calc(100% - 88px) !important;
|
|
704
781
|
display: flex !important;
|
|
705
782
|
flex-direction: column !important;
|
|
706
783
|
overflow: hidden !important;
|
|
707
784
|
}
|
|
785
|
+
/* overflow-x 必须是 auto 而不是 hidden:宿主原样式只写了 overflow-y:auto,
|
|
786
|
+
按 CSS Overflow 规范 overflow-x 会计算为 auto(可横滑)。此前插件强制
|
|
787
|
+
hidden,把「可滚动」降级成「静默裁切」——溢出内容既看不到也滑不出来
|
|
788
|
+
(实测 375px 下裁掉 42px,Plugin Market 的 Installed/Advanced 两个 Tab
|
|
789
|
+
直接不可达)。这里恢复宿主默认,保证任何第三方设置节的内容至少可达。 */
|
|
708
790
|
div[class*="VOzbGW_options"] {
|
|
709
791
|
flex: 1 1 auto !important;
|
|
710
792
|
width: 100% !important;
|
|
711
793
|
max-width: 100% !important;
|
|
794
|
+
min-width: 0 !important;
|
|
712
795
|
box-sizing: border-box !important;
|
|
713
796
|
padding: 0 14px 20px !important;
|
|
714
|
-
overflow-x:
|
|
797
|
+
overflow-x: auto !important;
|
|
715
798
|
overflow-y: auto !important;
|
|
716
799
|
-webkit-overflow-scrolling: touch !important;
|
|
800
|
+
overscroll-behavior: contain !important;
|
|
717
801
|
}
|
|
718
802
|
|
|
719
803
|
/* 设置中心选项行手机自适应(垂直流式,防文字单字折行) */
|
|
@@ -723,12 +807,15 @@ export const MOBILE_STYLES_CSS = `
|
|
|
723
807
|
align-items: stretch !important;
|
|
724
808
|
gap: 8px !important;
|
|
725
809
|
width: 100% !important;
|
|
810
|
+
/* min-width:0:让 flex 子项可以真正收缩,而不是把父容器顶宽后溢出 */
|
|
811
|
+
min-width: 0 !important;
|
|
726
812
|
padding: 12px 0 !important;
|
|
727
813
|
box-sizing: border-box !important;
|
|
728
814
|
}
|
|
729
815
|
div[class*="VOzbGW_options"] div[class*="_rowText"] {
|
|
730
816
|
width: 100% !important;
|
|
731
817
|
max-width: 100% !important;
|
|
818
|
+
min-width: 0 !important;
|
|
732
819
|
}
|
|
733
820
|
div[class*="VOzbGW_options"] button[class*="_selector"],
|
|
734
821
|
div[class*="VOzbGW_options"] select,
|
|
@@ -796,6 +883,209 @@ export const MOBILE_STYLES_CSS = `
|
|
|
796
883
|
}
|
|
797
884
|
}
|
|
798
885
|
|
|
886
|
+
/* 6.2 <=480px 手机竖屏:设置中心改为「两级钻取」。
|
|
887
|
+
侧边轨道在 375px 屏上要吃掉 88px(21% 宽度),内容列只剩 277px,第三方设置节
|
|
888
|
+
(Plugin Market 的 Tab 条需 470px)必然溢出。改为:分类列表页 ↔ 内容详情页,
|
|
889
|
+
内容独占整宽(375px 下由 277px 提升到 355px,+28%),标签回到 15px、触控行 52px。
|
|
890
|
+
|
|
891
|
+
【整块由 html[data-dshbr-drilldown="ready"] 门控】
|
|
892
|
+
本块默认把内容区(options)隐藏、只显示分类列表;能不能从列表走进内容,完全
|
|
893
|
+
取决于 client/index.js 的点击监听器。所以本块不能以「JS 一定在」为无条件前提:
|
|
894
|
+
开关没打开时本块整体惰性,<=480px 退回 767 块的 88px 图标轨道(窄,但分类与
|
|
895
|
+
内容都完整可达),不会出现「内容被藏了、又没人能把用户带进去」的死列表。
|
|
896
|
+
|
|
897
|
+
开关何时打开(client/index.js 的 setupSettingsDrilldown):
|
|
898
|
+
- 该函数是 apply() 的第一条语句,早于注入本 CSS 的 injectMobileStyles();
|
|
899
|
+
- 只有 document.addEventListener('click', …) 成功返回后才打上 ready。
|
|
900
|
+
因此「本 CSS 已生效、而点击监听器没装上」在可执行路径上不可达;即使将来有人把
|
|
901
|
+
该调用挪到会抛异常的初始化之后,开关未开也只是退回轨道布局。
|
|
902
|
+
|
|
903
|
+
明确未覆盖:无法在脚本内探测「addEventListener 被环境静默丢弃」这类异常 ——
|
|
904
|
+
要做这种自检就必须在 document 上派发合成 click,而宿主与第三方插件合计有 6 个
|
|
905
|
+
document 级 click 监听(另有多组 mousedown/pointerdown)会被误触发,得不偿失。
|
|
906
|
+
插件 JS 完全不执行时本 CSS 也不会注入,同样不会藏内容。
|
|
907
|
+
|
|
908
|
+
开关放在 <html> 而非 panel:监听器必须在弹窗出现之前就装好,那时 panel 还不存在。
|
|
909
|
+
|
|
910
|
+
panel 上的 data-dshbr-settings-view 只表达「列表页 / 详情页」:
|
|
911
|
+
- 无该属性(含开关刚打开的首帧)→ 分类列表页
|
|
912
|
+
- "detail" → 内容详情页 */
|
|
913
|
+
@media (max-width: 480px) {
|
|
914
|
+
/* 面板由 767 块的横向轨道布局(row)改为纵向:列表页/详情页都是上下结构。
|
|
915
|
+
漏掉这一条会让 nav 与 content 并排抢宽度,分类列表被压成 0 宽(实测 options
|
|
916
|
+
只剩 24px),是本方案最容易漏的关键一步。 */
|
|
917
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"] {
|
|
918
|
+
flex-direction: column !important;
|
|
919
|
+
/* ✕ 要绝对定位到面板右上角,必须有定位祖先 */
|
|
920
|
+
position: relative !important;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* 关闭按钮钉在面板右上角,与标题/返回行同处第一行 —— 这是弹窗的常规约定。
|
|
924
|
+
之所以要绝对定位:宿主 DOM 里 ✕ 属于 content>header,而标题属于 nav,二者是
|
|
925
|
+
两棵子树。此前靠 order:-1 把 content 提到 nav 之前,结果 ✕ 落在标题上面一行;
|
|
926
|
+
详情页反过来,✕ 落到返回行下面一行。绝对定位是唯一能在不注入 DOM 的前提下
|
|
927
|
+
把两者拉回同一行的办法。 */
|
|
928
|
+
html[data-dshbr-drilldown="ready"] button[class*="VOzbGW_close"] {
|
|
929
|
+
position: absolute !important;
|
|
930
|
+
top: 9px !important;
|
|
931
|
+
right: 9px !important;
|
|
932
|
+
width: 32px !important;
|
|
933
|
+
height: 32px !important;
|
|
934
|
+
z-index: 5 !important;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* 分类列表页:内容区只剩 header(宿主/第三方 action 槽),下沉为底部动作栏。
|
|
938
|
+
不再用 order:-1 —— 标题回到 DOM 顺序的第一行,顺带让焦点顺序与视觉顺序一致。 */
|
|
939
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) > div[class*="VOzbGW_content"] {
|
|
940
|
+
flex: 0 0 auto !important;
|
|
941
|
+
width: 100% !important;
|
|
942
|
+
max-width: 100% !important;
|
|
943
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
|
|
944
|
+
}
|
|
945
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) div[class*="VOzbGW_options"] {
|
|
946
|
+
display: none !important;
|
|
947
|
+
}
|
|
948
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) > nav[class*="VOzbGW_nav"] {
|
|
949
|
+
flex: 1 1 auto !important;
|
|
950
|
+
min-height: 0 !important;
|
|
951
|
+
overflow-y: auto !important;
|
|
952
|
+
overscroll-behavior: contain !important;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* 详情页:折叠导航为「‹ 标题」一行,充当返回入口 */
|
|
956
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navList"] {
|
|
957
|
+
display: none !important;
|
|
958
|
+
}
|
|
959
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] > nav[class*="VOzbGW_nav"] {
|
|
960
|
+
flex: 0 0 auto !important;
|
|
961
|
+
overflow: visible !important;
|
|
962
|
+
cursor: pointer !important;
|
|
963
|
+
}
|
|
964
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] > nav[class*="VOzbGW_nav"]:active {
|
|
965
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05)) !important;
|
|
966
|
+
}
|
|
967
|
+
/* 详情页也把动作栏沉到底部,与列表页保持一致。
|
|
968
|
+
用 flex order 交换 header/options 的视觉位置:options 仍是那个 overflow:auto
|
|
969
|
+
的滚动容器,滚动语义不变(不用 column-reverse,避免滚动原点翻转)。 */
|
|
970
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_header"] {
|
|
971
|
+
order: 2 !important;
|
|
972
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
|
|
973
|
+
}
|
|
974
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_options"] {
|
|
975
|
+
order: 1 !important;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/* 轨道在窄屏改成整宽列表容器 */
|
|
979
|
+
html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] {
|
|
980
|
+
width: 100% !important;
|
|
981
|
+
min-width: 0 !important;
|
|
982
|
+
max-width: 100% !important;
|
|
983
|
+
padding: 10px 12px 12px !important;
|
|
984
|
+
gap: 4px !important;
|
|
985
|
+
border-right: none !important;
|
|
986
|
+
border-bottom: none !important;
|
|
987
|
+
}
|
|
988
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_navTitle"] {
|
|
989
|
+
font-size: 17px !important;
|
|
990
|
+
line-height: 1.3 !important;
|
|
991
|
+
/* 不可折行的超长 token(无空格长串)会整块溢出、把文字顶到 ✕ 下面。
|
|
992
|
+
独立验收实测 81 字符无空格标题时文字右端 823px、与 ✕ 交叠 707px²。
|
|
993
|
+
overflow-wrap: anywhere 让这种 token 也能断行 —— 留白只管盒内,管不住溢出。 */
|
|
994
|
+
overflow-wrap: anywhere !important;
|
|
995
|
+
/* 右侧 52px 给绝对定位的 ✕ 让位(紧邻规则里的声明会被后置同名规则覆盖,
|
|
996
|
+
所以留白必须写在这一条上,不能只写在前面那条) */
|
|
997
|
+
padding: 0 52px 4px 4px !important;
|
|
998
|
+
}
|
|
999
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_navList"] {
|
|
1000
|
+
flex-direction: column !important;
|
|
1001
|
+
gap: 2px !important;
|
|
1002
|
+
width: 100% !important;
|
|
1003
|
+
min-width: 0 !important;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* 列表行:图标左、文字右、行尾 › 指示 */
|
|
1007
|
+
html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
|
|
1008
|
+
html[data-dshbr-drilldown="ready"] button[class*="VOzbGW_navCell"] {
|
|
1009
|
+
flex-direction: row !important;
|
|
1010
|
+
justify-content: flex-start !important;
|
|
1011
|
+
align-items: center !important;
|
|
1012
|
+
text-align: left !important;
|
|
1013
|
+
width: 100% !important;
|
|
1014
|
+
min-height: 52px !important;
|
|
1015
|
+
padding: 12px 14px !important;
|
|
1016
|
+
gap: 12px !important;
|
|
1017
|
+
border-radius: 12px !important;
|
|
1018
|
+
}
|
|
1019
|
+
html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"]::after {
|
|
1020
|
+
content: '' !important;
|
|
1021
|
+
width: 7px !important;
|
|
1022
|
+
height: 7px !important;
|
|
1023
|
+
margin-left: auto !important;
|
|
1024
|
+
flex: none !important;
|
|
1025
|
+
border-right: 1.7px solid var(--dsw-alias-label-tertiary, #9ca3af) !important;
|
|
1026
|
+
border-bottom: 1.7px solid var(--dsw-alias-label-tertiary, #9ca3af) !important;
|
|
1027
|
+
transform: rotate(-45deg) !important;
|
|
1028
|
+
}
|
|
1029
|
+
html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"] svg {
|
|
1030
|
+
width: 20px !important;
|
|
1031
|
+
height: 20px !important;
|
|
1032
|
+
}
|
|
1033
|
+
html[data-dshbr-drilldown="ready"] span[class*="VOzbGW_navLabel"] {
|
|
1034
|
+
font-size: 15px !important;
|
|
1035
|
+
line-height: 1.35 !important;
|
|
1036
|
+
white-space: normal !important;
|
|
1037
|
+
word-break: normal !important;
|
|
1038
|
+
text-align: left !important;
|
|
1039
|
+
flex: 0 1 auto !important;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* 详情页返回行:‹ 箭头 + 标题 */
|
|
1043
|
+
/* 详情页返回行:‹ 箭头 + 标题。右侧同样要留 52px 给 ✕ ——
|
|
1044
|
+
本条比基类多一个属性选择器、特异性更高,会**整体覆盖**基类的 padding,
|
|
1045
|
+
所以留白必须在这里再写一遍(漏写时基类的 52px 会被这条的 4px 顶掉,
|
|
1046
|
+
长标题会钻到 ✕ 下面;独立验收实测注入 61 字符标题后交叠 608/707 px²)。 */
|
|
1047
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navTitle"] {
|
|
1048
|
+
display: flex !important;
|
|
1049
|
+
align-items: center !important;
|
|
1050
|
+
overflow-wrap: anywhere !important;
|
|
1051
|
+
padding: 2px 52px 2px 4px !important;
|
|
1052
|
+
}
|
|
1053
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navTitle"]::before {
|
|
1054
|
+
content: '' !important;
|
|
1055
|
+
width: 8px !important;
|
|
1056
|
+
height: 8px !important;
|
|
1057
|
+
margin: 0 10px 0 6px !important;
|
|
1058
|
+
flex: none !important;
|
|
1059
|
+
border-left: 2px solid var(--dsw-alias-label-primary, #111827) !important;
|
|
1060
|
+
border-bottom: 2px solid var(--dsw-alias-label-primary, #111827) !important;
|
|
1061
|
+
transform: rotate(45deg) !important;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/* content/header 必须是 static —— ✕ 是绝对定位的,它的包含块取决于最近的**定位祖先**。
|
|
1065
|
+
本来这个祖先就是 panel(宿主自带 position:relative);但一旦 content 或 header 被
|
|
1066
|
+
任何来源(本块、宿主其它规则、第三方插件)设成 relative/absolute,包含块就会下移,
|
|
1067
|
+
✕ 会从面板右上角跑到内容区里去。独立验收实测:给 content 加 position:relative 后
|
|
1068
|
+
✕ 的 relTop 由 9 变成 579、并与底部动作栏重叠 837 px²。 */
|
|
1069
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_content"] {
|
|
1070
|
+
position: static !important;
|
|
1071
|
+
width: 100% !important;
|
|
1072
|
+
max-width: 100% !important;
|
|
1073
|
+
flex: 1 1 auto !important;
|
|
1074
|
+
}
|
|
1075
|
+
/* header 不再当顶栏用:它现在只承载 action 槽,高度随内容(可能是宿主的一个
|
|
1076
|
+
按钮,也可能是多个插件注入的多行按钮),靠 flex 自然撑开,不设固定高度。 */
|
|
1077
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_header"] {
|
|
1078
|
+
position: static !important;
|
|
1079
|
+
height: auto !important;
|
|
1080
|
+
padding: 8px 10px !important;
|
|
1081
|
+
align-items: center !important;
|
|
1082
|
+
justify-content: flex-end !important;
|
|
1083
|
+
}
|
|
1084
|
+
html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_options"] {
|
|
1085
|
+
padding: 0 12px 16px !important;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
799
1089
|
/* 远程工作区选择弹窗移动端/桌面端自适应样式 */
|
|
800
1090
|
#dsh-remote-workspace-modal {
|
|
801
1091
|
position: fixed !important;
|