dsh-bottom-info-bar 1.3.1
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/LICENSE +21 -0
- package/README.md +9 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +659 -0
- package/lib/index.js +1285 -0
- package/package.json +57 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-bottom-info-bar", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
// Bottom Info Bar(底部信息栏插件)— client half(静态 bundle 形态)
|
|
4
|
+
// - host.call(method, args) → fetch POST /_dsh/dsh-bottom-info-bar/<method>(JSON)
|
|
5
|
+
// - ctx.interval / ctx.timeout → window.setInterval / window.setTimeout
|
|
6
|
+
// - styles.insert(css) → document 注入 <style>(installStyles)
|
|
7
|
+
// - React 由 bundle 的 require('react') 提供(seed 模块)
|
|
8
|
+
// 样式策略:① 只把数字加粗(.bi-num 700)② 服务商名加粗 ③ 高峰价琥珀色+加粗、空闲价绿色+加粗
|
|
9
|
+
// 显示行为:① 本对话花费始终显示——新会话/对话刚开始(尚无记账)时显示"本对话 ¥0.000",
|
|
10
|
+
// hover 仍可查看持久化的 今天/近一月/全部;
|
|
11
|
+
// ② 完整模式下原生统计行无 steps 门槛,对话刚开始即显示"0 轮 · 0 步"。
|
|
12
|
+
// 失败策略(AUDIT-CODE-REVIEW 缺陷 #1):逐接口容错——
|
|
13
|
+
// ① rpc 带 20s 超时且可被外部 AbortSignal 中止(组件卸载即取消),杜绝永久"加载中…";
|
|
14
|
+
// ② load 用 Promise.allSettled 逐端点处理:成功端点写新值,失败端点保留旧值并记入 errors 表;
|
|
15
|
+
// ③ 渲染永不整栏降级:旧数据照常显示,仅失败项打降级标记(分块/全局提示)。
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const React = require('react');
|
|
19
|
+
|
|
20
|
+
const RPC_BASE = '/_dsh/dsh-bottom-info-bar';
|
|
21
|
+
|
|
22
|
+
// 排版优化(正式版):完整模式下隐藏"首 token 平均 / tok/s"两个低优先级原生字段,
|
|
23
|
+
// 让原生统计行在 748px 对话宽度下单行放得下;hover 信息浮窗(title)仍显示全部原生信息。
|
|
24
|
+
const HIDE_SPEED_FIELDS = true;
|
|
25
|
+
// 订阅窗口预警阈值:任一窗口已用百分比 ≥ 该值 → 红色 ⚠(与 host 常量保持一致)
|
|
26
|
+
const WINDOW_ALERT_PERCENT = 90;
|
|
27
|
+
|
|
28
|
+
// RPC 超时兜底:host 侧 15s 超时之上再留余量;端点挂起时 20s 内必失败,杜绝永久"加载中…"
|
|
29
|
+
const RPC_TIMEOUT_MS = 20000;
|
|
30
|
+
|
|
31
|
+
// rpc(method, args, externalSignal):
|
|
32
|
+
// - 超时:20s 未响应 → abort 并以"请求超时"失败(fetch 挂起不阻塞界面)
|
|
33
|
+
// - 可中止:传入外部 AbortSignal(组件卸载时 abort)→ 立即取消并拒绝"请求已取消"
|
|
34
|
+
function rpc(method, args, externalSignal) {
|
|
35
|
+
let abortReason = null;
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timer = window.setTimeout(function () { abortReason = '请求超时'; controller.abort(); }, RPC_TIMEOUT_MS);
|
|
38
|
+
function onExternalAbort() { abortReason = '请求已取消'; controller.abort(); }
|
|
39
|
+
function cleanup() {
|
|
40
|
+
window.clearTimeout(timer);
|
|
41
|
+
if (externalSignal) externalSignal.removeEventListener('abort', onExternalAbort);
|
|
42
|
+
}
|
|
43
|
+
if (externalSignal) {
|
|
44
|
+
if (externalSignal.aborted) {
|
|
45
|
+
cleanup();
|
|
46
|
+
return Promise.reject(new Error('请求已取消'));
|
|
47
|
+
}
|
|
48
|
+
externalSignal.addEventListener('abort', onExternalAbort);
|
|
49
|
+
}
|
|
50
|
+
return fetch(RPC_BASE + '/' + method, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(args || {}),
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
}).then(function (res) {
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
return res.text().then(function (raw) {
|
|
58
|
+
let body = null;
|
|
59
|
+
try { body = JSON.parse(raw); } catch (e) { /* 非 JSON 错误体 */ }
|
|
60
|
+
throw new Error((body && body.error) || ('HTTP ' + res.status));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return res.text().then(function (raw) {
|
|
64
|
+
try { return JSON.parse(raw); } catch (e) { throw new Error('响应解析失败'); }
|
|
65
|
+
});
|
|
66
|
+
}).catch(function (err) {
|
|
67
|
+
// 本函数主动 abort(超时/外部取消)→ 统一为可读错误;其余错误原样抛出
|
|
68
|
+
if (abortReason !== null) throw new Error(abortReason);
|
|
69
|
+
if (err && err.name === 'AbortError') throw new Error('请求已取消');
|
|
70
|
+
throw err;
|
|
71
|
+
}).finally(cleanup);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// load() 的逐接口容错状态合并(模块级纯函数,供单测提取):
|
|
75
|
+
// 成功端点 → 写新值 + 清除错误;失败端点 → 保留旧值(无旧数据则为 null)+ 记录错误信息。
|
|
76
|
+
// results 与端点顺序一一对应:balance / pricing / usage / billingMode / sub。
|
|
77
|
+
function mergeLoadResults(prev, results) {
|
|
78
|
+
const keys = ['balance', 'pricing', 'usage', 'billingMode', 'sub'];
|
|
79
|
+
const next = { loading: false, errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null } };
|
|
80
|
+
for (let i = 0; i < keys.length; i++) {
|
|
81
|
+
const key = keys[i];
|
|
82
|
+
const r = results[i];
|
|
83
|
+
if (r && r.status === 'fulfilled') {
|
|
84
|
+
next[key] = r.value;
|
|
85
|
+
} else {
|
|
86
|
+
next[key] = prev[key];
|
|
87
|
+
const reason = r && r.reason;
|
|
88
|
+
next.errors[key] = reason && reason.message ? String(reason.message) : String(reason || 'RPC 失败');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function installStyles() {
|
|
95
|
+
const id = 'dsh-bottom-info-bar';
|
|
96
|
+
const existing = document.querySelector('style[data-plugin-css="' + id + '"]');
|
|
97
|
+
if (existing !== null) return function () {};
|
|
98
|
+
const style = document.createElement('style');
|
|
99
|
+
style.dataset.plugin = 'dsh-bottom-info-bar';
|
|
100
|
+
style.dataset.pluginCss = id;
|
|
101
|
+
style.textContent = `
|
|
102
|
+
.bi-root { text-align: center; max-width: var(--dsh-chat-content-width); box-sizing: border-box; width: 100%; padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px; margin: 0 auto; display: block; overflow: hidden; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary, rgba(128,128,128,0.9)); font-variant-numeric: tabular-nums; cursor: pointer; }
|
|
103
|
+
.bi-native-row { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; width: 100%; }
|
|
104
|
+
.bi-row2 { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; width: 100%; }
|
|
105
|
+
.bi-native-row > span, .bi-row2 > span { white-space: nowrap; }
|
|
106
|
+
.bi-sep { color: var(--dsw-alias-separator-primary, rgba(128,128,128,0.5)); margin: 0 8px; }
|
|
107
|
+
/* 服务商名等一般强调:加粗 600 */
|
|
108
|
+
.bi-root b { color: var(--dsw-alias-label-primary, #333); font-weight: 600; }
|
|
109
|
+
/* 数字:加粗 700(余额/倒计时/本对话花费/原生统计数字) */
|
|
110
|
+
.bi-root b.bi-num { font-weight: 700; }
|
|
111
|
+
/* 高峰价:琥珀色 + 加粗;空闲价:绿色 + 加粗 */
|
|
112
|
+
.bi-peak { color: var(--dsw-alias-state-warn-primary, #d97706); font-weight: 700; }
|
|
113
|
+
.bi-offpeak { color: var(--dsw-alias-state-success-primary, #16a34a); font-weight: 700; }
|
|
114
|
+
.bi-err { color: var(--dsw-alias-state-error-primary, #dc2626); }
|
|
115
|
+
.bi-stale{ color: var(--dsw-alias-state-warn-primary, #d97706); }
|
|
116
|
+
`;
|
|
117
|
+
document.head.appendChild(style);
|
|
118
|
+
return function () { style.remove(); };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
inject: ['slots'],
|
|
123
|
+
async apply(ctx) {
|
|
124
|
+
// slots 服务可能晚于 apply 就绪:优先 ctx.slots(inject 注入属性),回退 ctx.get('slots');
|
|
125
|
+
// 仍不可用则轮询等待(最多 60×300ms ≈ 18s),绝不提前退出导致注册丢失
|
|
126
|
+
let slots = ctx.slots || ctx.get('slots');
|
|
127
|
+
for (let i = 0; slots === undefined && i < 60; i++) {
|
|
128
|
+
await new Promise(function (resolve) { window.setTimeout(resolve, 300); });
|
|
129
|
+
slots = ctx.slots || ctx.get('slots');
|
|
130
|
+
}
|
|
131
|
+
if (slots === undefined) {
|
|
132
|
+
console.warn('[dsh-bottom-info-bar] slots 服务 18s 内未就绪,信息栏未注册');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ctx.effect(function () {
|
|
137
|
+
const disposeStyles = installStyles();
|
|
138
|
+
return function () { disposeStyles(); };
|
|
139
|
+
}, 'dsh-bottom-info-bar: styles');
|
|
140
|
+
|
|
141
|
+
// ---------- 注册:一体替换(同 id 'stats') ----------
|
|
142
|
+
let density = 'full';
|
|
143
|
+
let toggling = false; // 防抖:rpc 异步期间禁止重复切换(只允许 full/compact 两态)
|
|
144
|
+
let injectReady = false;
|
|
145
|
+
let occupantDispose = null;
|
|
146
|
+
|
|
147
|
+
function applyMode() {
|
|
148
|
+
if (occupantDispose) { occupantDispose(); occupantDispose = null; }
|
|
149
|
+
occupantDispose = slots.register(
|
|
150
|
+
// 静态注册无动态沙箱的优先级自动分配:显式给低 priority(最低者渲染)以遮蔽原生 stats 栏(priority 0)
|
|
151
|
+
{ name: 'conversation.composer.dock', id: 'stats', priority: -1000 },
|
|
152
|
+
function (slotProps) {
|
|
153
|
+
return React.createElement(BottomInfoBar, Object.assign({}, slotProps, { density: density, onToggleDensity: onToggleDensity }));
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onToggleDensity() {
|
|
159
|
+
if (toggling) return; // 切换进行中,忽略连点
|
|
160
|
+
toggling = true;
|
|
161
|
+
const next = density === 'full' ? 'compact' : 'full';
|
|
162
|
+
rpc('setInfoDensity', { density: next }).then(function () {
|
|
163
|
+
density = next;
|
|
164
|
+
toggling = false;
|
|
165
|
+
applyMode();
|
|
166
|
+
}).catch(function (err) {
|
|
167
|
+
toggling = false;
|
|
168
|
+
console.error('Bottom Info Bar 切换信息密度失败', err);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
slots.inject('conversation.composer.dock', function () {
|
|
173
|
+
injectReady = true;
|
|
174
|
+
applyMode();
|
|
175
|
+
return function () { if (occupantDispose) occupantDispose(); };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const cfg = await rpc('getConfig');
|
|
180
|
+
if (cfg && (cfg.infoDensity === 'full' || cfg.infoDensity === 'compact') && cfg.infoDensity !== density) {
|
|
181
|
+
density = cfg.infoDensity;
|
|
182
|
+
if (injectReady) applyMode();
|
|
183
|
+
}
|
|
184
|
+
} catch (err) { /* 默认完整 */ }
|
|
185
|
+
|
|
186
|
+
// ---------- 组件 ----------
|
|
187
|
+
function BottomInfoBar(props) {
|
|
188
|
+
// 原生/会话投影(hooks 无条件调用)
|
|
189
|
+
const statsProj = props.useProjection ? props.useProjection('sessionStats') : undefined;
|
|
190
|
+
const usageProj = props.useProjection ? props.useProjection('tokenUsage') : undefined;
|
|
191
|
+
|
|
192
|
+
const [state, setState] = React.useState({
|
|
193
|
+
loading: true, balance: null, pricing: null, usage: null, billingMode: null, sub: null,
|
|
194
|
+
errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null },
|
|
195
|
+
});
|
|
196
|
+
const [now, setNow] = React.useState(Date.now());
|
|
197
|
+
|
|
198
|
+
// 当前会话 ID 多路获取:slotProps 标准 kit → session 快照 → 运行时 sessions 服务
|
|
199
|
+
// (DSH 各版本注入方式不同,任一路可用即拿到真实会话 ID,避免回退到上一会话的账)
|
|
200
|
+
const propsRef = React.useRef(props);
|
|
201
|
+
propsRef.current = props;
|
|
202
|
+
const resolveSessionId = React.useCallback(function () {
|
|
203
|
+
const p = propsRef.current;
|
|
204
|
+
try {
|
|
205
|
+
if (p.sessionId) return p.sessionId;
|
|
206
|
+
if (p.session && p.session.sessionId) return p.session.sessionId;
|
|
207
|
+
const sessions = ctx.get ? ctx.get('sessions') : null;
|
|
208
|
+
const cur = sessions && sessions.list && sessions.list.getSnapshot().current;
|
|
209
|
+
if (cur) return cur;
|
|
210
|
+
} catch (e) { /* 拿不到则返回空串,host 端对空串返回 null(显示 ¥0.000) */ }
|
|
211
|
+
return '';
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
// 组件生命周期 AbortSignal:卸载时中止所有在途 RPC(配合 rpc 20s 超时,双保险防旧响应写 state)
|
|
215
|
+
const abortRef = React.useRef(null);
|
|
216
|
+
React.useEffect(function () {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
abortRef.current = controller;
|
|
219
|
+
return function () { controller.abort(); };
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const load = React.useCallback(function () {
|
|
223
|
+
const sessionId = resolveSessionId();
|
|
224
|
+
const signal = abortRef.current ? abortRef.current.signal : null;
|
|
225
|
+
// 逐接口容错:allSettled 等全部 settle(最坏 20s 超时兜底),任一失败只降级该端点,
|
|
226
|
+
// 不拖垮其他成功数据;合并逻辑在 mergeLoadResults(失败端点保留旧值 + 记录错误)
|
|
227
|
+
Promise.allSettled([
|
|
228
|
+
rpc('getBalanceSnapshot', null, signal),
|
|
229
|
+
rpc('getPricing', null, signal),
|
|
230
|
+
rpc('getUsageSummary', { sessionId: sessionId }, signal),
|
|
231
|
+
rpc('getBillingMode', null, signal),
|
|
232
|
+
rpc('getSubscriptionSnapshot', null, signal),
|
|
233
|
+
]).then(function (results) {
|
|
234
|
+
if (signal && signal.aborted) return; // 组件已卸载:放弃结果,不写 state
|
|
235
|
+
setState(function (s) { return mergeLoadResults(s, results); });
|
|
236
|
+
});
|
|
237
|
+
}, [resolveSessionId]);
|
|
238
|
+
|
|
239
|
+
React.useEffect(function () {
|
|
240
|
+
load();
|
|
241
|
+
const id = window.setInterval(load, 30000);
|
|
242
|
+
return function () { window.clearInterval(id); };
|
|
243
|
+
}, [load]);
|
|
244
|
+
|
|
245
|
+
// 模型/服务商切换秒级同步:getBillingMode 为 host 端纯本地计算(零网络开销),每 2 秒轮询一次;
|
|
246
|
+
// mode/provider/model 任一变化(即切换了模型/服务商)→ 立即完整 load(),不等 30s 主轮询。
|
|
247
|
+
// 注意:本轮询不触碰订阅接口——getSubscriptionSnapshot 仍仅由 load 调用(惰性门控 + 60s 周期不变)
|
|
248
|
+
React.useEffect(function () {
|
|
249
|
+
let lastKey = null;
|
|
250
|
+
const id = window.setInterval(function () {
|
|
251
|
+
rpc('getBillingMode').then(function (bm) {
|
|
252
|
+
if (!bm || typeof bm.mode !== 'string') return;
|
|
253
|
+
const key = bm.mode + ':' + (bm.provider || '') + ':' + (bm.model || '');
|
|
254
|
+
if (lastKey !== null && lastKey !== key) load();
|
|
255
|
+
lastKey = key;
|
|
256
|
+
}).catch(function () { /* 轮询失败静默:30s 主轮询兜底 */ });
|
|
257
|
+
}, 2000);
|
|
258
|
+
return function () { window.clearInterval(id); };
|
|
259
|
+
}, [load]);
|
|
260
|
+
|
|
261
|
+
// 会话统计变化(回复中 turns/steps/tokens 增长,回复完成时停止)→ 防抖后即时刷新花费,
|
|
262
|
+
// 不等下一个 30s 轮询:用户回复一结束即可看到真实金额
|
|
263
|
+
React.useEffect(function () {
|
|
264
|
+
if (!statsProj) return undefined;
|
|
265
|
+
const timer = window.setTimeout(load, 800);
|
|
266
|
+
return function () { window.clearTimeout(timer); };
|
|
267
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
268
|
+
}, [load,
|
|
269
|
+
statsProj && statsProj.turns,
|
|
270
|
+
statsProj && statsProj.steps,
|
|
271
|
+
statsProj && statsProj.decodeTokens,
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
React.useEffect(function () {
|
|
275
|
+
const id = window.setInterval(function () { setNow(Date.now()); }, 1000);
|
|
276
|
+
return function () { window.clearInterval(id); };
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
// ---- 与原生一致格式工具 ----
|
|
280
|
+
function formatTokens(n) {
|
|
281
|
+
const scaled = function (v) { return v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10); };
|
|
282
|
+
if (n < 1e3) return String(n);
|
|
283
|
+
if (n < 1e6) return scaled(n / 1e3) + 'K';
|
|
284
|
+
return scaled(n / 1e6) + 'M';
|
|
285
|
+
}
|
|
286
|
+
function formatDuration(ms) {
|
|
287
|
+
const s = ms / 1e3;
|
|
288
|
+
if (s < 60) return Math.round(s * 10) / 10 + 's';
|
|
289
|
+
const whole = Math.round(s);
|
|
290
|
+
const sec = whole % 60;
|
|
291
|
+
return Math.floor(whole / 60) + 'm' + String(sec).padStart(2, '0') + 's';
|
|
292
|
+
}
|
|
293
|
+
function formatTps(tps) {
|
|
294
|
+
const clamped = Math.max(0, tps);
|
|
295
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
296
|
+
}
|
|
297
|
+
function billedInput(usage) {
|
|
298
|
+
return (usage.uncachedInputTokens || 0) + (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0);
|
|
299
|
+
}
|
|
300
|
+
function fmt(n, digits) {
|
|
301
|
+
if (n == null || isNaN(n)) return '—';
|
|
302
|
+
return n.toFixed(digits == null ? 2 : digits);
|
|
303
|
+
}
|
|
304
|
+
function fmtCountdown(ms) {
|
|
305
|
+
if (ms == null || ms <= 0) return '00:00';
|
|
306
|
+
const totalSec = Math.floor(ms / 1000);
|
|
307
|
+
const h = Math.floor(totalSec / 3600);
|
|
308
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
309
|
+
const s = totalSec % 60;
|
|
310
|
+
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
311
|
+
return h > 0 ? h + 'h' + p(m) + 'm' : p(m) + ':' + p(s);
|
|
312
|
+
}
|
|
313
|
+
// 订阅窗口重置时刻(本地时区,hover 浮窗用)
|
|
314
|
+
function formatDateTime(ms) {
|
|
315
|
+
const d = new Date(ms);
|
|
316
|
+
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
317
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
318
|
+
}
|
|
319
|
+
// 订阅窗口重置倒计时(天级格式):≥1 天 → '1d 21h';≥1 小时 → '3h 12m';<1 小时 → '12:34'
|
|
320
|
+
function fmtResetCountdown(ms) {
|
|
321
|
+
if (ms == null || ms <= 0) return '00:00';
|
|
322
|
+
const totalSec = Math.floor(ms / 1000);
|
|
323
|
+
const d = Math.floor(totalSec / 86400);
|
|
324
|
+
const h = Math.floor((totalSec % 86400) / 3600);
|
|
325
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
326
|
+
if (d > 0) return d + 'd ' + h + 'h';
|
|
327
|
+
if (h > 0) return h + 'h ' + String(m).padStart(2, '0') + 'm';
|
|
328
|
+
return String(m).padStart(2, '0') + ':' + String(totalSec % 60).padStart(2, '0');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 订阅窗口剩余百分比(剩余 = 100 - 已用;钳制 ≥0 防接口异常值)
|
|
332
|
+
function remainingPercent(w) {
|
|
333
|
+
return Math.max(0, 100 - w.usedPercent);
|
|
334
|
+
}
|
|
335
|
+
// 订阅窗口紧凑行标签(5小时 → '5h',周 → '周',月 → '月');hover 明细仍用完整标签
|
|
336
|
+
function compactWindowLabel(key) {
|
|
337
|
+
if (key === 'five_hour') return '5h';
|
|
338
|
+
if (key === 'seven_day') return '周';
|
|
339
|
+
if (key === 'monthly') return '月';
|
|
340
|
+
return '窗口';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 数字统一加粗(仅数字本身)
|
|
344
|
+
function num(t) {
|
|
345
|
+
return React.createElement('b', { className: 'bi-num' }, String(t));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 服务商 + 具体模型(两种模式共用;纯显示,不拦截点击——点击冒泡到整条信息栏触发密度切换;hover 展示定价模式)
|
|
349
|
+
// M5:模型名/服务商名均取 DSH 目录名(与模型切换器完全一致);当服务商名已是模型名前缀
|
|
350
|
+
// (如 "DeepSeek" + "DeepSeek-V4-Flash")→ 只显示模型名(切换器样式,避免 "DeepSeek · DeepSeek-V4-Flash" 重复)
|
|
351
|
+
function providerGroup() {
|
|
352
|
+
const pr = state.pricing;
|
|
353
|
+
const provLabel = (pr && pr.providerDisplay) ? pr.providerDisplay : '未知';
|
|
354
|
+
const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
355
|
+
: (pr && pr.model ? pr.model : '未知模型');
|
|
356
|
+
const redundant = provLabel.length > 1 && modelLabel.toLowerCase().indexOf(provLabel.toLowerCase()) === 0;
|
|
357
|
+
const provTitle = '服务商:' + provLabel + ' ' + modelLabel + '\n'
|
|
358
|
+
+ (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(高峰 9-12、14-18 点)'
|
|
359
|
+
: (pr && pr.mode === 'flat' ? '定价:固定价' : '定价:未收录,按默认计'));
|
|
360
|
+
if (redundant) {
|
|
361
|
+
return React.createElement('span', { key: 'prov', title: provTitle },
|
|
362
|
+
React.createElement('b', null, modelLabel));
|
|
363
|
+
}
|
|
364
|
+
return React.createElement('span', { key: 'prov', title: provTitle },
|
|
365
|
+
React.createElement('b', null, provLabel),
|
|
366
|
+
' ',
|
|
367
|
+
modelLabel,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 订阅服务名(订阅制模式下"服务商"指订阅服务本身,不是模型厂商)
|
|
372
|
+
// Codex 与 ChatGPT 已合并:实际 provider openai-codex / chatgpt 均显示 ChatGPT;codex 保持 Codex
|
|
373
|
+
function subscriptionServiceName(provider) {
|
|
374
|
+
if (provider === 'chatgpt' || provider === 'openai-codex') return 'ChatGPT';
|
|
375
|
+
if (provider === 'codex') return 'Codex';
|
|
376
|
+
if (provider === 'opencode-go' || provider === 'opencode') return 'OpenCode Go';
|
|
377
|
+
return '订阅';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 订阅制模型组:订阅服务名 · 具体模型(如 `OpenCode Go · V4 Flash`、`Codex · GPT 5 Codex`)
|
|
381
|
+
function subscriptionProviderGroup() {
|
|
382
|
+
const pr = state.pricing;
|
|
383
|
+
const serviceName = subscriptionServiceName(state.billingMode && state.billingMode.provider);
|
|
384
|
+
const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
385
|
+
: (pr && pr.model ? pr.model : '未知模型');
|
|
386
|
+
const title = '订阅服务:' + serviceName + '\n模型:' + modelLabel;
|
|
387
|
+
return React.createElement('span', { key: 'subprov', title: title },
|
|
388
|
+
React.createElement('b', null, serviceName),
|
|
389
|
+
' · ',
|
|
390
|
+
modelLabel,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---- 余额制模式(v1.0.0 现状,完全不动):服务商+模型 → 余额 → 时段 → 倒计时 → 本对话花费 ----
|
|
395
|
+
function pushBalanceGroups(groups) {
|
|
396
|
+
const bal = state.balance;
|
|
397
|
+
const errors = state.errors || {};
|
|
398
|
+
const alertActive = !!(bal && bal.alert && bal.alert.active);
|
|
399
|
+
groups.push(providerGroup());
|
|
400
|
+
|
|
401
|
+
// 余额(纯金额;hover 仅展示余额,不显示充值/赠金)
|
|
402
|
+
if (bal && bal.error && bal.error.kind === 'no-key') {
|
|
403
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'nokey' },
|
|
404
|
+
'未配置 DEEPSEEK_API_KEY → 设置→模型 填写'));
|
|
405
|
+
} else if (bal && bal.data) {
|
|
406
|
+
const symbol = bal.currency === 'USD' ? '$' : '¥';
|
|
407
|
+
const balTitle = bal.estimate
|
|
408
|
+
? '余额为估算值(起始充值额减累计花费):' + symbol + fmt(bal.data.total)
|
|
409
|
+
: '余额:' + symbol + fmt(bal.data.total);
|
|
410
|
+
groups.push(React.createElement('span', { key: 'bal', title: balTitle },
|
|
411
|
+
'余额 ',
|
|
412
|
+
num(symbol + fmt(bal.data.total)),
|
|
413
|
+
bal.estimate ? React.createElement('span', { className: 'bi-stale' }, '(估算)') : null,
|
|
414
|
+
alertActive ? React.createElement('span', { className: 'bi-err', title: '余额低于阈值' }, ' ⚠') : null,
|
|
415
|
+
));
|
|
416
|
+
// host 快照失败(bal.error)或本次 RPC 失败(errors.balance)→ 均保留旧数据 + 降级标记
|
|
417
|
+
if (bal.error || errors.balance) {
|
|
418
|
+
groups.push(React.createElement('span', { className: 'bi-stale', key: 'balerr', title: '余额数据暂不可用,已保留最近一次成功的数据;自动重试中' }, '⚠ 刷新失败,显示上次快照'));
|
|
419
|
+
}
|
|
420
|
+
} else if (bal && bal.error) {
|
|
421
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'berr' }, '余额获取失败:' + bal.error.message));
|
|
422
|
+
} else if (errors.balance) {
|
|
423
|
+
// 本次 RPC 失败且无旧数据:只降级余额块,其余端点数据照常渲染
|
|
424
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'berr' }, '余额获取失败:' + errors.balance));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 时段:仅峰谷价服务商显示"高峰价/空闲价"(flat/unknown 服务商不显示;hover 展示具体价格)
|
|
428
|
+
const pr = state.pricing;
|
|
429
|
+
if (pr && pr.mode === 'peak-valley') {
|
|
430
|
+
const peakNow = pr.period === 'peak';
|
|
431
|
+
const p = pr.prices || {};
|
|
432
|
+
const periodTitle = (peakNow ? '高峰价' : '空闲价') + ':输入 ¥' + (p.inputCacheMiss != null ? p.inputCacheMiss : '?')
|
|
433
|
+
+ '/M · 缓存 ¥' + (p.inputCacheHit != null ? p.inputCacheHit : '?')
|
|
434
|
+
+ '/M · 输出 ¥' + (p.output != null ? p.output : '?') + '/M';
|
|
435
|
+
groups.push(React.createElement('span', { key: 'period', className: peakNow ? 'bi-peak' : 'bi-offpeak', title: periodTitle },
|
|
436
|
+
peakNow ? '高峰价' : '空闲价'));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 倒计时:仅峰谷价服务商显示"距高峰/距空闲"(hover 展示下次切换时刻;数字加粗)
|
|
440
|
+
if (pr && pr.mode === 'peak-valley' && pr.nextSwitch) {
|
|
441
|
+
const peakNow = pr.period === 'peak';
|
|
442
|
+
const countdownTitle = '下次切换:' + (peakNow ? '空闲价' : '高峰价') + ' 于 ' + pr.nextSwitch.atLabel;
|
|
443
|
+
groups.push(React.createElement('span', { key: 'countdown', title: countdownTitle },
|
|
444
|
+
'距' + (peakNow ? '空闲' : '高峰') + ' ',
|
|
445
|
+
num(fmtCountdown(pr.nextSwitch.at - now))));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 本对话花费(只显示钱;hover 浮窗显示 今天 / 近一月 / 全部;金额数字加粗)
|
|
449
|
+
// 始终显示:新会话/对话刚开始尚无记账时显示 ¥0.000,hover 仍可查看持久化的 今天/近一月/全部
|
|
450
|
+
const usg = state.usage;
|
|
451
|
+
if (usg) {
|
|
452
|
+
const cs = usg.currentSession;
|
|
453
|
+
const costCNY = cs && cs.costs && cs.costs.CNY != null ? cs.costs.CNY : null;
|
|
454
|
+
const costUSD = cs && cs.costs && cs.costs.USD != null ? cs.costs.USD : null;
|
|
455
|
+
const zeroTxt = (bal && bal.currency === 'USD' ? '$' : '¥') + (0).toFixed(3);
|
|
456
|
+
const costTxt = costCNY != null ? '¥' + costCNY.toFixed(3)
|
|
457
|
+
: (costUSD != null ? '$' + costUSD.toFixed(3) : zeroTxt);
|
|
458
|
+
const symbol = bal && bal.currency === 'USD' ? '$' : '¥';
|
|
459
|
+
const today = usg.todaySpend != null ? '今天 ' + symbol + fmt(usg.todaySpend, 3) : '';
|
|
460
|
+
const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
|
|
461
|
+
const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
|
|
462
|
+
const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
|
|
463
|
+
groups.push(React.createElement('span', { key: 'convo', title: detail || '本对话花费' },
|
|
464
|
+
'本对话 ',
|
|
465
|
+
num(costTxt)));
|
|
466
|
+
} else if (errors.usage) {
|
|
467
|
+
// 本次 RPC 失败且无旧数据:只降级花费块,其余端点数据照常渲染
|
|
468
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'usageerr' }, '花费获取失败:' + errors.usage));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---- 订阅制模式(互斥替换余额制版,row2 只三类信息):
|
|
473
|
+
// 订阅服务+模型 → 三窗口额度 → 距重置倒计时(最紧窗口);余额/时段/花费/token 均不显示 ----
|
|
474
|
+
function pushSubscriptionGroups(groups) {
|
|
475
|
+
groups.push(subscriptionProviderGroup());
|
|
476
|
+
const sub = state.sub;
|
|
477
|
+
const errors = state.errors || {};
|
|
478
|
+
if (!sub) {
|
|
479
|
+
if (errors.sub) {
|
|
480
|
+
// 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
|
|
481
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'suberr' }, '订阅额度获取失败:' + errors.sub));
|
|
482
|
+
} else {
|
|
483
|
+
groups.push(React.createElement('span', { key: 'subload' }, '订阅额度加载中…'));
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const rawWindows = Array.isArray(sub.windows) ? sub.windows : [];
|
|
488
|
+
const windows = rawWindows.filter(function (w) {
|
|
489
|
+
return w && typeof w.usedPercent === 'number';
|
|
490
|
+
});
|
|
491
|
+
const hasData = windows.length > 0;
|
|
492
|
+
// 错误分支:无旧数据时给出明确引导 / 错误文案;有旧数据时走下方渲染并附"刷新失败"标记。
|
|
493
|
+
// no-key(无令牌/缺 access_token)与 auth(令牌失效 401)→ 统一"未绑定/重新绑定"引导——
|
|
494
|
+
// 令牌由独立插件 dsh-chatgpt-subscription 维护,本插件只读令牌显示额度,不自行绑定/续期
|
|
495
|
+
if (sub.error && !hasData) {
|
|
496
|
+
if (sub.error.kind === 'no-key' || sub.error.kind === 'auth') {
|
|
497
|
+
const hint = sub.source === 'opencode-go'
|
|
498
|
+
? '未配置 OpenCode Go → 设置→模型 填写 OPENCODE_GO_API_KEY'
|
|
499
|
+
: '未绑定 ChatGPT 订阅 → 安装 dsh-chatgpt-subscription 插件授权绑定';
|
|
500
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'subnokey' }, hint));
|
|
501
|
+
} else {
|
|
502
|
+
groups.push(React.createElement('span', { className: 'bi-err', key: 'suberr' }, '订阅额度获取失败:' + sub.error.message));
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// 窗口缺失(如 Codex 无 5 小时窗口)→ 跳过窗口组,不占位、不报错
|
|
507
|
+
if (hasData) {
|
|
508
|
+
// 简洁模式下选择"时间最短且有重置时刻"的窗口(刷新最快,用户最需关注):
|
|
509
|
+
// 优先级:5小时 > 周 > 月(按窗口时长排序,而非已用百分比)
|
|
510
|
+
const windowPriority = { five_hour: 1, seven_day: 2, monthly: 3 };
|
|
511
|
+
const windowsWithReset = windows.filter(function (w) { return w.resetsAt; });
|
|
512
|
+
const displayWindow = windowsWithReset.length > 0
|
|
513
|
+
? windowsWithReset.slice().sort(function (a, b) {
|
|
514
|
+
const pa = windowPriority[a.key] || 99;
|
|
515
|
+
const pb = windowPriority[b.key] || 99;
|
|
516
|
+
return pa - pb;
|
|
517
|
+
})[0]
|
|
518
|
+
: null;
|
|
519
|
+
|
|
520
|
+
// 完整模式显示全部窗口;简洁模式只显示选中的那个窗口
|
|
521
|
+
const visible = full ? windows : (displayWindow ? [displayWindow] : []);
|
|
522
|
+
|
|
523
|
+
// 预警触发条件:已用 ≥80%(= 剩余 ≤20%)→ 琥珀色;否则绿色
|
|
524
|
+
const LOW_QUOTA_PERCENT = 20;
|
|
525
|
+
const alarmWindows = windows.filter(function (w) { return w.usedPercent >= (100 - LOW_QUOTA_PERCENT); });
|
|
526
|
+
const titleLines = ['订阅源:' + subscriptionServiceName(state.billingMode && state.billingMode.provider) + (sub.plan ? '(' + sub.plan + ')' : '')]
|
|
527
|
+
.concat(windows.map(function (w) {
|
|
528
|
+
return w.label + '窗口:剩余 ' + remainingPercent(w) + '%(已用 ' + w.usedPercent + '%)'
|
|
529
|
+
+ (w.resetsAt ? ' · 重置 ' + formatDateTime(w.resetsAt) + ' · 距重置 ' + fmtResetCountdown(w.resetsAt - now) : '');
|
|
530
|
+
}));
|
|
531
|
+
const winNodes = [];
|
|
532
|
+
for (let i = 0; i < visible.length; i++) {
|
|
533
|
+
const w = visible[i];
|
|
534
|
+
if (i > 0) winNodes.push(' · ');
|
|
535
|
+
const remaining = remainingPercent(w);
|
|
536
|
+
const colorClass = remaining <= LOW_QUOTA_PERCENT ? 'bi-peak' : 'bi-offpeak';
|
|
537
|
+
winNodes.push(compactWindowLabel(w.key) + ' ', React.createElement('span', { className: colorClass }, num(remaining + '%')));
|
|
538
|
+
}
|
|
539
|
+
groups.push(React.createElement('span', { key: 'subwin', title: titleLines.join('\n') },
|
|
540
|
+
...winNodes,
|
|
541
|
+
alarmWindows.length > 0
|
|
542
|
+
? React.createElement('span', {
|
|
543
|
+
className: 'bi-err', key: 'subalarm',
|
|
544
|
+
title: '窗口告急:' + alarmWindows.map(function (w) { return w.label + '窗口剩余 ≤20%'; }).join('、'),
|
|
545
|
+
}, ' ⚠')
|
|
546
|
+
: null,
|
|
547
|
+
));
|
|
548
|
+
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 均保留旧数据 + 降级标记
|
|
549
|
+
if (sub.error || errors.sub) {
|
|
550
|
+
groups.push(React.createElement('span', { className: 'bi-stale', key: 'substale', title: '订阅数据暂不可用,已保留最近一次成功的数据;自动重试中' }, '⚠ 刷新失败,显示上次快照'));
|
|
551
|
+
}
|
|
552
|
+
// 距重置倒计时(与显示的窗口一致,确保额度与倒计时匹配)
|
|
553
|
+
if (displayWindow && displayWindow.resetsAt) {
|
|
554
|
+
const cdTitle = displayWindow.label + '窗口 剩余 ' + remainingPercent(displayWindow)
|
|
555
|
+
+ '%(已用 ' + displayWindow.usedPercent + '%) · 重置 ' + formatDateTime(displayWindow.resetsAt);
|
|
556
|
+
groups.push(React.createElement('span', { key: 'subcd', title: cdTitle },
|
|
557
|
+
'距重置 ', num(fmtResetCountdown(displayWindow.resetsAt - now))));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const groups = [];
|
|
563
|
+
// 两态严格判定:density 只能是 'full' 或 'compact'(host 校验 + 本地防抖保证)
|
|
564
|
+
const full = props.density === 'full';
|
|
565
|
+
// 模式互斥:订阅制渲染订阅版 row2,余额制渲染 v1.0.0 现状,绝不叠加
|
|
566
|
+
const isSub = !!(state.billingMode && state.billingMode.mode === 'subscription');
|
|
567
|
+
// 逐接口容错渲染:loading 仅"首帧且无任何数据"时占位;此后始终渲染(旧数据 + 失败降级标记),
|
|
568
|
+
// 绝不整栏"加载失败";rpc 有 20s 超时兜底,也不存在永久"加载中…"
|
|
569
|
+
const hasAnyData = state.balance !== null || state.pricing !== null || state.usage !== null
|
|
570
|
+
|| state.billingMode !== null || state.sub !== null;
|
|
571
|
+
if (state.loading && !hasAnyData) {
|
|
572
|
+
groups.push(React.createElement('span', { key: 'loading' }, '加载中…'));
|
|
573
|
+
} else if (isSub) {
|
|
574
|
+
pushSubscriptionGroups(groups);
|
|
575
|
+
} else {
|
|
576
|
+
pushBalanceGroups(groups);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 全局降级提示:任一端点失败 → 旧数据照常渲染 + 角落提示(title 列出失败项),仅失败项降级
|
|
580
|
+
const errors = state.errors || {};
|
|
581
|
+
const failedLabels = [];
|
|
582
|
+
if (errors.balance) failedLabels.push('余额');
|
|
583
|
+
if (errors.pricing) failedLabels.push('定价');
|
|
584
|
+
if (errors.usage) failedLabels.push('花费');
|
|
585
|
+
if (errors.billingMode) failedLabels.push('模式');
|
|
586
|
+
if (errors.sub) failedLabels.push('订阅额度');
|
|
587
|
+
if (failedLabels.length > 0) {
|
|
588
|
+
groups.push(React.createElement('span', { className: 'bi-stale', key: 'degraded',
|
|
589
|
+
title: '以下数据暂不可用:' + failedLabels.join('、') + '(已保留上次成功数据,自动重试中)' },
|
|
590
|
+
'⚠ 部分数据刷新失败'));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ---- 组装 ----
|
|
594
|
+
const sepNodes = [];
|
|
595
|
+
const nodes = [];
|
|
596
|
+
for (let i = 0; i < groups.length; i++) {
|
|
597
|
+
if (i > 0) nodes.push(React.createElement('span', { key: 'sep' + i, className: 'bi-sep' }, '|'));
|
|
598
|
+
nodes.push(React.createElement('span', { key: 'g' + i }, groups[i]));
|
|
599
|
+
}
|
|
600
|
+
const row2 = React.createElement('div', { className: 'bi-row2' }, ...nodes);
|
|
601
|
+
|
|
602
|
+
let row1 = null;
|
|
603
|
+
if (full && statsProj) {
|
|
604
|
+
// 每组:{ nodes: React 节点数组(数字用 num 加粗), text: 纯文本(title 用) }
|
|
605
|
+
const ng = [];
|
|
606
|
+
function group(parts, hidden) {
|
|
607
|
+
const nodesArr = [];
|
|
608
|
+
const texts = [];
|
|
609
|
+
for (let i = 0; i < parts.length; i++) {
|
|
610
|
+
const p = parts[i];
|
|
611
|
+
if (typeof p === 'string') { nodesArr.push(p); texts.push(p); }
|
|
612
|
+
else { nodesArr.push(p); texts.push(p.props.children); }
|
|
613
|
+
}
|
|
614
|
+
ng.push({ nodes: nodesArr, text: texts.join(''), hidden: !!hidden });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
group([num(statsProj.turns), ' 轮 · ', num(statsProj.steps), ' 步']);
|
|
618
|
+
|
|
619
|
+
const durations = [];
|
|
620
|
+
if (statsProj.llmMs > 0) durations.push('LLM ', num(formatDuration(statsProj.llmMs)));
|
|
621
|
+
if (statsProj.toolMs > 0) durations.push(' · 工具调用 ', num(formatDuration(statsProj.toolMs)));
|
|
622
|
+
if (durations.length > 0) group(durations);
|
|
623
|
+
|
|
624
|
+
const speeds = [];
|
|
625
|
+
if (statsProj.ttftSteps > 0) speeds.push('首 token 平均 ', num(formatDuration(statsProj.ttftMs / statsProj.ttftSteps)));
|
|
626
|
+
if (statsProj.decodeMs > 0) speeds.push(' · ', num(formatTps(statsProj.decodeTokens / (statsProj.decodeMs / 1e3))), ' tok/s');
|
|
627
|
+
if (speeds.length > 0) group(speeds, HIDE_SPEED_FIELDS); // 不占可见版式,title 浮窗保留
|
|
628
|
+
|
|
629
|
+
if (usageProj && (billedInput(usageProj) > 0 || (usageProj.outputTokens || 0) > 0)) {
|
|
630
|
+
const denom = billedInput(usageProj);
|
|
631
|
+
const hit = denom > 0 ? Math.round(((usageProj.cacheReadTokens || 0) / denom) * 100) : null;
|
|
632
|
+
if (hit != null) group(['缓存命中 ', num(hit), '%']);
|
|
633
|
+
group(['输入 ', num(formatTokens(billedInput(usageProj))), ' tok · 输出 ', num(formatTokens(usageProj.outputTokens || 0)), ' tok']);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const nativeLine = ng.map(function (g) { return g.text; }).join(' | ');
|
|
637
|
+
const ngNodes = [];
|
|
638
|
+
let visCount = 0;
|
|
639
|
+
for (let i = 0; i < ng.length; i++) {
|
|
640
|
+
if (ng[i].hidden) continue; // 隐藏分组不占版式(title 仍含其文本)
|
|
641
|
+
if (visCount > 0) ngNodes.push(React.createElement('span', { key: 'nsep' + i, className: 'bi-sep' }, '|'));
|
|
642
|
+
visCount++;
|
|
643
|
+
ngNodes.push(React.createElement('span', { key: 'ng' + i }, ng[i].nodes));
|
|
644
|
+
}
|
|
645
|
+
row1 = React.createElement('div', { className: 'bi-native-row', title: nativeLine }, ...ngNodes);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const rootCls = 'bi-root';
|
|
649
|
+
return React.createElement('div', {
|
|
650
|
+
className: rootCls,
|
|
651
|
+
onClick: function () { props.onToggleDensity(); },
|
|
652
|
+
title: '单击切换 完整/简洁',
|
|
653
|
+
}, row1, row2);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
return module.exports;
|
|
659
|
+
} });
|