dsh-niuma 0.2.1 → 0.2.3
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 +34 -0
- package/README.md +4 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +297 -32
- package/lib/client.js.map +2 -2
- package/lib/index.js +93 -3
- package/lib/update.js +247 -0
- package/package.json +4 -3
- package/src/client/index.js +247 -19
- package/src/core.mjs +23 -0
package/src/client/index.js
CHANGED
|
@@ -123,6 +123,16 @@ const zh = {
|
|
|
123
123
|
'configFailed': '设置读取失败,使用默认值',
|
|
124
124
|
'retry': '重试',
|
|
125
125
|
'settings': '设置',
|
|
126
|
+
'decimalsLabel': '小数',
|
|
127
|
+
'decimalsUnit': '位',
|
|
128
|
+
'turboLabel': '加速',
|
|
129
|
+
'updateAvailable': '发现新版本 v{version}',
|
|
130
|
+
'updateNow': '立即更新',
|
|
131
|
+
'updating': '正在下载新版本…',
|
|
132
|
+
'updateStaged': 'v{version} 已下载 · 重启 DSH 生效',
|
|
133
|
+
'refreshRate': '刷新频率',
|
|
134
|
+
'rateSecond': '按秒(1s)',
|
|
135
|
+
'rateFrame': '刷新率(≈16ms)',
|
|
126
136
|
'settingsHint': '改完点「保存」生效;格式不对会在下方红字提示,不影响计时。',
|
|
127
137
|
'salaryField': '实收月薪',
|
|
128
138
|
'groupSalary': '工资',
|
|
@@ -223,6 +233,16 @@ const en = {
|
|
|
223
233
|
'configFailed': 'Failed to load settings, using defaults',
|
|
224
234
|
'retry': 'Retry',
|
|
225
235
|
'settings': 'Settings',
|
|
236
|
+
'decimalsLabel': 'Decimals',
|
|
237
|
+
'decimalsUnit': 'dp',
|
|
238
|
+
'turboLabel': 'Turbo',
|
|
239
|
+
'updateAvailable': 'New version v{version}',
|
|
240
|
+
'updateNow': 'Update now',
|
|
241
|
+
'updating': 'Downloading update…',
|
|
242
|
+
'updateStaged': 'v{version} downloaded · restart DSH to apply',
|
|
243
|
+
'refreshRate': 'Refresh rate',
|
|
244
|
+
'rateSecond': 'Per second (1s)',
|
|
245
|
+
'rateFrame': 'Display rate (~16ms)',
|
|
226
246
|
'settingsHint': 'Click Save to apply. Invalid input is shown in red below; the ticker keeps running.',
|
|
227
247
|
'salaryField': 'Net monthly salary',
|
|
228
248
|
'groupSalary': 'Salary',
|
|
@@ -312,6 +332,7 @@ function formFromConfig(config) {
|
|
|
312
332
|
lunchStart: String(config.lunchStart == null ? '' : config.lunchStart),
|
|
313
333
|
lunchEnd: String(config.lunchEnd == null ? '' : config.lunchEnd),
|
|
314
334
|
lunchPaid: config.lunchPaid === true,
|
|
335
|
+
updateMs: String(config.updateMs == null ? 1000 : config.updateMs),
|
|
315
336
|
};
|
|
316
337
|
}
|
|
317
338
|
|
|
@@ -326,6 +347,7 @@ function patchFromForm(form) {
|
|
|
326
347
|
lunchStart: String(form.lunchStart || '').trim(),
|
|
327
348
|
lunchEnd: String(form.lunchEnd || '').trim(),
|
|
328
349
|
lunchPaid: Boolean(form.lunchPaid),
|
|
350
|
+
updateMs: Number(form.updateMs),
|
|
329
351
|
};
|
|
330
352
|
}
|
|
331
353
|
|
|
@@ -371,19 +393,63 @@ function useRemoteConfig() {
|
|
|
371
393
|
return { ...state, refresh };
|
|
372
394
|
}
|
|
373
395
|
|
|
374
|
-
/**
|
|
375
|
-
function
|
|
376
|
-
const [
|
|
396
|
+
/** 版本 / 自动更新状态:挂载时查一次、每 6 小时刷新;点「立即更新」触发下载暂存。 */
|
|
397
|
+
function useUpdateInfo() {
|
|
398
|
+
const [state, setState] = React.useState({ status: 'loading', data: null, error: null });
|
|
399
|
+
const load = React.useCallback(() => {
|
|
400
|
+
if (typeof fetch !== 'function') {
|
|
401
|
+
setState({ status: 'failed', data: null, error: 'fetch unavailable' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
fetch(`${ROUTE_BASE}/update`, { cache: 'no-store', credentials: 'same-origin' })
|
|
405
|
+
.then(async (response) => {
|
|
406
|
+
const body = await response.json();
|
|
407
|
+
if (!response.ok || !body || body.ok !== true) throw new Error((body && body.error) || `HTTP ${response.status}`);
|
|
408
|
+
return body.data || null;
|
|
409
|
+
})
|
|
410
|
+
.then((data) => setState({ status: 'ready', data, error: null }))
|
|
411
|
+
.catch((error) => setState({ status: 'failed', data: null, error: String((error && error.message) || error) }));
|
|
412
|
+
}, []);
|
|
377
413
|
React.useEffect(() => {
|
|
378
|
-
|
|
379
|
-
const timer = setInterval(
|
|
380
|
-
const second = Math.floor(Date.now() / 1000);
|
|
381
|
-
if (second === lastSecond) return;
|
|
382
|
-
lastSecond = second;
|
|
383
|
-
setNow(new Date());
|
|
384
|
-
}, 250);
|
|
414
|
+
load();
|
|
415
|
+
const timer = setInterval(load, 6 * 3600 * 1000);
|
|
385
416
|
return () => clearInterval(timer);
|
|
417
|
+
}, [load]);
|
|
418
|
+
const download = React.useCallback(() => {
|
|
419
|
+
setState((prev) => ({ ...prev, status: 'updating' }));
|
|
420
|
+
if (typeof fetch !== 'function') return;
|
|
421
|
+
fetch(`${ROUTE_BASE}/update`, { method: 'POST', credentials: 'same-origin' })
|
|
422
|
+
.then(async (response) => {
|
|
423
|
+
const body = await response.json();
|
|
424
|
+
if (!response.ok || !body || body.ok !== true) throw new Error((body && body.error) || `HTTP ${response.status}`);
|
|
425
|
+
return body.data || null;
|
|
426
|
+
})
|
|
427
|
+
.then((data) => setState({ status: 'ready', data, error: null }))
|
|
428
|
+
.catch((error) => setState({ status: 'failed', data: null, error: String((error && error.message) || error) }));
|
|
386
429
|
}, []);
|
|
430
|
+
return { ...state, reload: load, download };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 当前时间:按配置的刷新间隔更新。
|
|
435
|
+
* - updateMs >= 50:setInterval(按秒 / 200ms / 100ms)
|
|
436
|
+
* - updateMs <= 16:requestAnimationFrame(跟随屏幕刷新率连续滚动)
|
|
437
|
+
*/
|
|
438
|
+
function useClock(updateMs) {
|
|
439
|
+
const [now, setNow] = React.useState(() => new Date());
|
|
440
|
+
React.useEffect(() => {
|
|
441
|
+
if (updateMs <= 16) {
|
|
442
|
+
let raf = 0;
|
|
443
|
+
const loop = () => {
|
|
444
|
+
setNow(new Date());
|
|
445
|
+
raf = requestAnimationFrame(loop);
|
|
446
|
+
};
|
|
447
|
+
raf = requestAnimationFrame(loop);
|
|
448
|
+
return () => cancelAnimationFrame(raf);
|
|
449
|
+
}
|
|
450
|
+
const timer = setInterval(() => setNow(new Date()), updateMs);
|
|
451
|
+
return () => clearInterval(timer);
|
|
452
|
+
}, [updateMs]);
|
|
387
453
|
return now;
|
|
388
454
|
}
|
|
389
455
|
|
|
@@ -471,10 +537,29 @@ function SectionTitle(props) {
|
|
|
471
537
|
}
|
|
472
538
|
|
|
473
539
|
/** 今日已赚主卡:渐变底 + 超大金额 + 进度条(视觉焦点只有这一处)。 */
|
|
540
|
+
/* ------------------------------------------------------------------ *
|
|
541
|
+
* 主金额:毫秒级刷新时数字本身高速变化(末位小数在 60fps 下每帧都在跳),
|
|
542
|
+
* 视觉上就是「欻欻」往上滚;不做逐位轮盘,避免滚动裁切带来的重影。
|
|
543
|
+
* ------------------------------------------------------------------ */
|
|
544
|
+
function RollingMoney(props) {
|
|
545
|
+
const decimals = Math.max(0, Math.min(6, Number.isInteger(props.decimals) ? props.decimals : 4));
|
|
546
|
+
return React.createElement('span', {
|
|
547
|
+
style: {
|
|
548
|
+
display: 'inline-block', whiteSpace: 'nowrap', letterSpacing: '-1.2px',
|
|
549
|
+
fontVariantNumeric: 'tabular-nums',
|
|
550
|
+
...(props.style || {}),
|
|
551
|
+
},
|
|
552
|
+
}, formatMoney(props.value, decimals));
|
|
553
|
+
}
|
|
554
|
+
|
|
474
555
|
function HeroCard(props) {
|
|
475
556
|
const t = props.t;
|
|
476
557
|
const snap = props.snap;
|
|
477
558
|
const rates = snap.rates;
|
|
559
|
+
const decimals = Number.isInteger(props.decimals) ? props.decimals : 4;
|
|
560
|
+
const onDecimalsChange = props.onDecimalsChange;
|
|
561
|
+
const turbo = props.turbo === true;
|
|
562
|
+
const onTurboChange = props.onTurboChange;
|
|
478
563
|
const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
|
|
479
564
|
const percentText = `${(progress * 100).toFixed(1)}%`;
|
|
480
565
|
const live = snap.status === 'working';
|
|
@@ -521,22 +606,64 @@ function HeroCard(props) {
|
|
|
521
606
|
: null,
|
|
522
607
|
t('earnedToday'),
|
|
523
608
|
]),
|
|
524
|
-
React.createElement('
|
|
525
|
-
|
|
609
|
+
React.createElement('div', {
|
|
610
|
+
key: 'b',
|
|
611
|
+
style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 },
|
|
612
|
+
}, [
|
|
613
|
+
React.createElement('span', { key: 'd', style: { fontSize: 11, color: C.label3, ...NUM } },
|
|
614
|
+
`${t('todayOn')} ${snap.dateKey}`),
|
|
615
|
+
React.createElement('div', { key: 'c', style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, [
|
|
616
|
+
React.createElement('label', {
|
|
617
|
+
key: 'dec', title: t('decimalsLabel'),
|
|
618
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, color: C.label3, ...NUM },
|
|
619
|
+
}, [
|
|
620
|
+
React.createElement('span', { key: 'l' }, t('decimalsLabel')),
|
|
621
|
+
React.createElement('select', {
|
|
622
|
+
key: 's', value: String(decimals), 'aria-label': t('decimalsLabel'),
|
|
623
|
+
onChange: (event) => onDecimalsChange && onDecimalsChange(Number(event.target.value)),
|
|
624
|
+
style: {
|
|
625
|
+
height: 22, padding: '0 2px', borderRadius: 6,
|
|
626
|
+
border: `1px solid ${C.border1}`, background: C.layer2, color: C.label2,
|
|
627
|
+
fontSize: 10.5, fontVariantNumeric: 'tabular-nums', outline: 'none', cursor: 'pointer',
|
|
628
|
+
},
|
|
629
|
+
}, [0, 1, 2, 3, 4, 5, 6].map((n) => React.createElement('option', { key: n, value: String(n) }, String(n)))),
|
|
630
|
+
React.createElement('span', { key: 'u' }, t('decimalsUnit')),
|
|
631
|
+
]),
|
|
632
|
+
React.createElement('label', {
|
|
633
|
+
key: 'turbo', title: t('turboLabel'),
|
|
634
|
+
style: {
|
|
635
|
+
display: 'inline-flex', alignItems: 'center', gap: 3,
|
|
636
|
+
fontSize: 10, padding: '1px 6px', borderRadius: 999,
|
|
637
|
+
border: `1px solid ${turbo ? `color-mix(in srgb, ${GOLD_TINT} 60%, ${C.border1})` : C.border1}`,
|
|
638
|
+
background: turbo ? `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)` : 'transparent',
|
|
639
|
+
color: turbo ? C.label2 : C.label3, cursor: 'pointer', ...NUM,
|
|
640
|
+
},
|
|
641
|
+
}, [
|
|
642
|
+
React.createElement('input', {
|
|
643
|
+
key: 'i', type: 'checkbox', checked: turbo,
|
|
644
|
+
onChange: (event) => onTurboChange && onTurboChange(event.target.checked),
|
|
645
|
+
style: { accentColor: GOLD_TINT, width: 12, height: 12, margin: 0, cursor: 'inherit' },
|
|
646
|
+
}),
|
|
647
|
+
React.createElement('span', { key: 'l' }, t('turboLabel')),
|
|
648
|
+
]),
|
|
649
|
+
]),
|
|
650
|
+
]),
|
|
526
651
|
]),
|
|
527
652
|
React.createElement('div', {
|
|
528
653
|
key: 'amt',
|
|
529
654
|
style: { display: 'flex', alignItems: 'baseline', gap: 7, marginTop: 4, flexWrap: 'wrap' },
|
|
530
655
|
}, [
|
|
531
|
-
React.createElement(
|
|
656
|
+
React.createElement(RollingMoney, {
|
|
532
657
|
key: 'val',
|
|
658
|
+
value: snap.todayEarned,
|
|
659
|
+
decimals,
|
|
533
660
|
style: {
|
|
534
|
-
fontSize: 46, lineHeight: 1.12, fontWeight: 800,
|
|
535
|
-
|
|
661
|
+
fontSize: 46, lineHeight: 1.12, fontWeight: 800,
|
|
662
|
+
color: '#D9A441',
|
|
536
663
|
...NUM,
|
|
537
664
|
...(rest ? { opacity: 0.62 } : {}),
|
|
538
665
|
},
|
|
539
|
-
}
|
|
666
|
+
}),
|
|
540
667
|
React.createElement('span', {
|
|
541
668
|
key: 'unit',
|
|
542
669
|
style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM },
|
|
@@ -949,6 +1076,13 @@ function SettingsSection(props) {
|
|
|
949
1076
|
]);
|
|
950
1077
|
|
|
951
1078
|
const lunchDisabled = !form.lunchEnabled;
|
|
1079
|
+
const RATE_OPTIONS = [
|
|
1080
|
+
['1000', t('rateSecond')],
|
|
1081
|
+
['250', '250 ms'],
|
|
1082
|
+
['100', '100 ms'],
|
|
1083
|
+
['50', '50 ms'],
|
|
1084
|
+
['16', t('rateFrame')],
|
|
1085
|
+
];
|
|
952
1086
|
|
|
953
1087
|
return React.createElement('details', {
|
|
954
1088
|
open: true,
|
|
@@ -975,6 +1109,14 @@ function SettingsSection(props) {
|
|
|
975
1109
|
toggle('lunchEnabled', 'lunchEnabledShort', false),
|
|
976
1110
|
toggle('lunchPaid', 'lunchPaidShort', lunchDisabled),
|
|
977
1111
|
]),
|
|
1112
|
+
row('refresh', t('refreshRate'), [
|
|
1113
|
+
React.createElement('select', {
|
|
1114
|
+
key: 'r',
|
|
1115
|
+
value: String(form.updateMs),
|
|
1116
|
+
onChange: (event) => setField('updateMs', event.target.value),
|
|
1117
|
+
style: { ...selectStyle, width: 128, padding: '4px 6px' },
|
|
1118
|
+
}, RATE_OPTIONS.map(([value, label]) => React.createElement('option', { key: value, value }, label))),
|
|
1119
|
+
]),
|
|
978
1120
|
]),
|
|
979
1121
|
React.createElement('div', {
|
|
980
1122
|
key: 'actions',
|
|
@@ -1022,7 +1164,7 @@ function NiumaPanel(props) {
|
|
|
1022
1164
|
const t = props && typeof props.t === 'function' ? props.t : fallbackTranslate;
|
|
1023
1165
|
|
|
1024
1166
|
const remote = useRemoteConfig();
|
|
1025
|
-
const
|
|
1167
|
+
const update = useUpdateInfo();
|
|
1026
1168
|
|
|
1027
1169
|
// 服务端配置 -> 规范化配置;坏数据/缺字段一律退回 DEFAULTS(计算不会抛)。
|
|
1028
1170
|
const cfg = React.useMemo(() => {
|
|
@@ -1036,7 +1178,11 @@ function NiumaPanel(props) {
|
|
|
1036
1178
|
}
|
|
1037
1179
|
}, [remote.data]);
|
|
1038
1180
|
|
|
1039
|
-
//
|
|
1181
|
+
// 刷新频率:1000=按秒;16≈屏幕刷新率(连续滚动);中间值为毫秒级
|
|
1182
|
+
const updateMs = cfg.turbo === true ? 16 : (Number.isInteger(cfg.updateMs) ? cfg.updateMs : 1000);
|
|
1183
|
+
const now = useClock(updateMs);
|
|
1184
|
+
|
|
1185
|
+
// 每次刷新重算(core 是纯函数,重算代价极小;已下班/休息日会稳定在日工资或 0)。
|
|
1040
1186
|
const snap = React.useMemo(() => earnedToday(cfg, now), [cfg, now]);
|
|
1041
1187
|
const rates = snap && snap.rates ? snap.rates : computeRates(cfg, snap && snap.dateKey);
|
|
1042
1188
|
const baseForm = React.useMemo(() => formFromConfig(cfg), [cfg]);
|
|
@@ -1157,7 +1303,30 @@ function NiumaPanel(props) {
|
|
|
1157
1303
|
});
|
|
1158
1304
|
};
|
|
1159
1305
|
|
|
1306
|
+
// 快速保存:主金额小数位这种即时偏好,不走设置表单的「保存」流程
|
|
1307
|
+
const [quickError, setQuickError] = React.useState('');
|
|
1308
|
+
const postConfig = React.useCallback((patch) => {
|
|
1309
|
+
if (typeof fetch !== 'function') return;
|
|
1310
|
+
fetch(`${ROUTE_BASE}/settings`, {
|
|
1311
|
+
method: 'POST',
|
|
1312
|
+
headers: { 'content-type': 'application/json' },
|
|
1313
|
+
credentials: 'same-origin',
|
|
1314
|
+
body: JSON.stringify(patch),
|
|
1315
|
+
})
|
|
1316
|
+
.then(async (response) => {
|
|
1317
|
+
let body = null;
|
|
1318
|
+
try { body = await response.json(); } catch (error) { body = null; }
|
|
1319
|
+
if (!response.ok || !body || body.ok !== true) throw new Error((body && body.error) || `HTTP ${response.status}`);
|
|
1320
|
+
setQuickError('');
|
|
1321
|
+
remote.refresh();
|
|
1322
|
+
})
|
|
1323
|
+
.catch((error) => setQuickError(String((error && error.message) || error)));
|
|
1324
|
+
}, [remote.refresh]);
|
|
1325
|
+
const onDecimalsChange = (value) => postConfig({ decimals: value });
|
|
1326
|
+
const onTurboChange = (checked) => postConfig({ turbo: Boolean(checked) });
|
|
1327
|
+
|
|
1160
1328
|
const holidaySource = (remote.data && remote.data.holidaySource) || HOLIDAY_SOURCE;
|
|
1329
|
+
const versionText = (remote.data && remote.data.version) || (update.data && update.data.currentVersion) || '';
|
|
1161
1330
|
const banner = remote.status === 'failed'
|
|
1162
1331
|
? {
|
|
1163
1332
|
color: C.warn,
|
|
@@ -1255,7 +1424,52 @@ function NiumaPanel(props) {
|
|
|
1255
1424
|
])
|
|
1256
1425
|
: null,
|
|
1257
1426
|
|
|
1258
|
-
|
|
1427
|
+
// 版本 / 自动更新提示:已下载→提示重启;发现新版→一键更新
|
|
1428
|
+
update.data && update.data.staged
|
|
1429
|
+
? React.createElement('div', {
|
|
1430
|
+
key: 'update',
|
|
1431
|
+
style: {
|
|
1432
|
+
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
|
1433
|
+
fontSize: 11.5, color: C.label2, padding: '6px 10px', borderRadius: 9,
|
|
1434
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
|
|
1435
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`,
|
|
1436
|
+
},
|
|
1437
|
+
}, [
|
|
1438
|
+
React.createElement('span', { key: 'dot', 'aria-hidden': 'true', style: { width: 6, height: 6, borderRadius: '50%', background: GOLD_TINT } }),
|
|
1439
|
+
React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } },
|
|
1440
|
+
t('updateStaged').replace('{version}', String(update.data.stagedVersion || update.data.latestVersion || ''))),
|
|
1441
|
+
])
|
|
1442
|
+
: update.status === 'updating'
|
|
1443
|
+
? React.createElement('div', {
|
|
1444
|
+
key: 'update',
|
|
1445
|
+
style: { fontSize: 11.5, color: C.label3, padding: '2px 2px' },
|
|
1446
|
+
}, t('updating'))
|
|
1447
|
+
: update.data && update.data.hasUpdate
|
|
1448
|
+
? React.createElement('div', {
|
|
1449
|
+
key: 'update',
|
|
1450
|
+
style: {
|
|
1451
|
+
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
|
1452
|
+
fontSize: 11.5, color: C.label2, padding: '6px 10px', borderRadius: 9,
|
|
1453
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
|
|
1454
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`,
|
|
1455
|
+
},
|
|
1456
|
+
}, [
|
|
1457
|
+
React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } },
|
|
1458
|
+
t('updateAvailable').replace('{version}', String(update.data.latestVersion || ''))),
|
|
1459
|
+
React.createElement('button', {
|
|
1460
|
+
key: 'go',
|
|
1461
|
+
type: 'button',
|
|
1462
|
+
onClick: update.download,
|
|
1463
|
+
style: {
|
|
1464
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 70%, rgba(0,0,0,.15))`,
|
|
1465
|
+
color: '#3A2A05',
|
|
1466
|
+
background: 'linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)',
|
|
1467
|
+
borderRadius: 7, padding: '3px 12px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer',
|
|
1468
|
+
},
|
|
1469
|
+
}, t('updateNow')),
|
|
1470
|
+
])
|
|
1471
|
+
: null,
|
|
1472
|
+
React.createElement(HeroCard, { key: 'hero', t, snap, decimals: cfg.decimals, onDecimalsChange, turbo: cfg.turbo, onTurboChange }),
|
|
1259
1473
|
React.createElement(CountdownRow, { key: 'countdown', t, snap }),
|
|
1260
1474
|
React.createElement('div', {
|
|
1261
1475
|
key: 'ratesHead',
|
|
@@ -1316,6 +1530,20 @@ function NiumaPanel(props) {
|
|
|
1316
1530
|
save,
|
|
1317
1531
|
onSave,
|
|
1318
1532
|
}),
|
|
1533
|
+
// 版本信息固定放在面板最底部
|
|
1534
|
+
React.createElement('div', {
|
|
1535
|
+
key: 'version',
|
|
1536
|
+
style: {
|
|
1537
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
|
1538
|
+
padding: '4px 0 0', fontSize: 10.5, color: C.label3, ...NUM,
|
|
1539
|
+
},
|
|
1540
|
+
}, [
|
|
1541
|
+
versionText ? React.createElement('span', { key: 'v' }, `牛马加速器 v${versionText}`) : null,
|
|
1542
|
+
update.data && update.data.staged
|
|
1543
|
+
? React.createElement('span', { key: 's', style: { color: C.label3 } },
|
|
1544
|
+
`· ${t('updateStaged').replace('{version}', String(update.data.stagedVersion || update.data.latestVersion || ''))}`)
|
|
1545
|
+
: null,
|
|
1546
|
+
]),
|
|
1319
1547
|
]),
|
|
1320
1548
|
]);
|
|
1321
1549
|
}
|
package/src/core.mjs
CHANGED
|
@@ -27,6 +27,12 @@ export const DEFAULTS = Object.freeze({
|
|
|
27
27
|
lunchPaid: false,
|
|
28
28
|
holidays: DEFAULT_HOLIDAYS,
|
|
29
29
|
extraWorkdays: DEFAULT_EXTRA_WORKDAYS,
|
|
30
|
+
/** 主金额小数位(0-6) */
|
|
31
|
+
decimals: 4,
|
|
32
|
+
/** 面板刷新间隔 ms:1000=按秒;16≈屏幕刷新率(连续滚动);中间值为毫秒级 */
|
|
33
|
+
updateMs: 1000,
|
|
34
|
+
/** 加速(滚动模式):true 时强制毫秒级刷新(≈屏幕刷新率),数字连续滚动 */
|
|
35
|
+
turbo: false,
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
const SALARY_MAX = 10_000_000;
|
|
@@ -159,6 +165,23 @@ export function normalizeConfig(patch) {
|
|
|
159
165
|
merged.lunchEnabled = merged.lunchEnabled !== false;
|
|
160
166
|
merged.lunchPaid = merged.lunchPaid === true;
|
|
161
167
|
|
|
168
|
+
// 展示/刷新偏好:主金额小数位 0-6;刷新间隔 16-1000ms(16 ≈ 屏幕刷新率);加速开关
|
|
169
|
+
merged.turbo = merged.turbo === true;
|
|
170
|
+
const decimals = Number(merged.decimals);
|
|
171
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) {
|
|
172
|
+
errors.push('小数位数需要是 0~6 的整数');
|
|
173
|
+
merged.decimals = Number.isInteger(decimals) ? decimals : DEFAULTS.decimals;
|
|
174
|
+
} else {
|
|
175
|
+
merged.decimals = decimals;
|
|
176
|
+
}
|
|
177
|
+
const updateMs = Number(merged.updateMs);
|
|
178
|
+
if (!Number.isInteger(updateMs) || updateMs < 16 || updateMs > 1000) {
|
|
179
|
+
errors.push('刷新间隔需要是 16~1000ms 的整数');
|
|
180
|
+
merged.updateMs = Number.isInteger(updateMs) ? updateMs : DEFAULTS.updateMs;
|
|
181
|
+
} else {
|
|
182
|
+
merged.updateMs = updateMs;
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
// 日期列表
|
|
163
186
|
const holidays = normalizeDateList(merged.holidays);
|
|
164
187
|
const extraWorkdays = normalizeDateList(merged.extraWorkdays);
|