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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录用户可感知的变化。版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## 0.2.3 — 2026-10-22
|
|
6
|
+
|
|
7
|
+
### 新增:版本号显示 + 自动更新机制
|
|
8
|
+
|
|
9
|
+
- **面板底部显示版本号**:`牛马加速器 v0.2.x`(取自宿主 package.json,固定放最底部)
|
|
10
|
+
- **自动检查更新**:宿主启动 8 秒后查一次 npm `dsh-niuma@latest`;有新版本自动下载 tarball 解包到
|
|
11
|
+
`$DSH_HOME/niuma/updates/<version>/`,**下次启动 DSH 自动覆盖安装并生效**
|
|
12
|
+
- **手动更新**:面板发现新版本时显示「发现新版本 vX · 立即更新」按钮,点击即下载(重启生效)
|
|
13
|
+
- **安全守卫**:
|
|
14
|
+
- git 开发工作区(存在 `.git`)不自动更新,避免覆盖未提交代码
|
|
15
|
+
- `NIUMA_UPDATE_DISABLED=1` 可整体关闭
|
|
16
|
+
- 网络失败只写日志,面板继续用当前版本,不影响计时
|
|
17
|
+
- 版本比较用内部 semver 实现,不引第三方依赖
|
|
18
|
+
- 新增 `GET/POST /niuma/update` 路由;`GET /niuma/config` 载荷增加 `version`
|
|
19
|
+
- 主金额改为纯文本渲染(逐位轮盘在面板滚动容器里会出现重影);「加速」= 毫秒级刷新(rAF),
|
|
20
|
+
末位小数每帧高速变化,视觉上就是数字持续快速上涨
|
|
21
|
+
- 新增 `lib/update.js`(semver 比较、tarball 解包、暂存/应用)与 `tools/verify-update.mjs`(27 项断言)
|
|
22
|
+
|
|
23
|
+
## 0.2.2 — 2026-10-22
|
|
24
|
+
|
|
25
|
+
### 新增:主金额小数位 + 刷新频率 + 滚动数字/加速
|
|
26
|
+
|
|
27
|
+
- **主金额改连续「里程表」滚动**:每一位按位权连续转动——最后一位小数转得最快(0-9 快速循环),
|
|
28
|
+
前一位慢 10 倍,整数位几乎不动;不是整块金额跳变,而是数字持续往上滚
|
|
29
|
+
- **加速开关**:主金额右侧「加速」勾选后强制毫秒级刷新(`requestAnimationFrame`,≈16ms),
|
|
30
|
+
滚轮每帧更新,低位小数飞快转动、高位慢慢进位
|
|
31
|
+
- **小数位数可选(0–6,默认 4)**:主卡金额右侧「小数 [n] 位」,改完即时保存、立即生效
|
|
32
|
+
- **刷新频率可调**:设置区新增「刷新频率」行
|
|
33
|
+
- 按秒(1s,默认,最省电)
|
|
34
|
+
- 250 ms / 100 ms / 50 ms(毫秒级滚动)
|
|
35
|
+
- 刷新率(≈16 ms,requestAnimationFrame,跟随屏幕刷新连续跳动)
|
|
36
|
+
- core 新增配置 `decimals`(0–6 整数)、`updateMs`(16–1000 整数)与 `turbo`(布尔),宿主写接口同样校验
|
|
37
|
+
- 刷新由 `useClock(updateMs)` 驱动:≥50ms 用 setInterval,≤16ms 用 rAF
|
|
38
|
+
|
|
5
39
|
## 0.2.1 — 2026-10-21
|
|
6
40
|
|
|
7
41
|
### 新增:本月工作天数进度
|
package/README.md
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
- 节假日数据内置在插件中(2026 官方 / 2027 预估),设置区不再需要手工维护一长串日期;2027 官方安排公布后随插件更新
|
|
33
33
|
- 午休可设置起止,**可勾选午休是否算工时**;午休不算工时则午休期间金额冻结
|
|
34
34
|
- 状态徽标:休息日 / 未上班 / 搬砖中 / 午休中 / 已下班
|
|
35
|
+
- 面板显示版本号;自动检查 npm 新版本,下载后**重启 DSH 自动生效**(git 开发工作区不覆盖)
|
|
36
|
+
- **加速**:勾选后按屏幕刷新率(≈16ms)刷新,末位小数高速跳动、数字快速上涨;不勾则按刷新频率步进
|
|
37
|
+
- 主金额**小数位可选(0–6)**,右侧下拉即时生效
|
|
38
|
+
- **刷新频率可选**:按秒 / 250ms / 100ms / 50ms / 屏幕刷新率(≈16ms 连续滚动)
|
|
35
39
|
- 今日进度条、距上班/距下班倒计时
|
|
36
40
|
- **本月已工作 / 剩余工作日**(与工资口径一致:节假日、周末、调休都算进去)
|
|
37
41
|
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -187,7 +187,13 @@ var DEFAULTS = Object.freeze({
|
|
|
187
187
|
lunchEnd: "13:00",
|
|
188
188
|
lunchPaid: false,
|
|
189
189
|
holidays: DEFAULT_HOLIDAYS,
|
|
190
|
-
extraWorkdays: DEFAULT_EXTRA_WORKDAYS
|
|
190
|
+
extraWorkdays: DEFAULT_EXTRA_WORKDAYS,
|
|
191
|
+
/** 主金额小数位(0-6) */
|
|
192
|
+
decimals: 4,
|
|
193
|
+
/** 面板刷新间隔 ms:1000=按秒;16≈屏幕刷新率(连续滚动);中间值为毫秒级 */
|
|
194
|
+
updateMs: 1e3,
|
|
195
|
+
/** 加速(滚动模式):true 时强制毫秒级刷新(≈屏幕刷新率),数字连续滚动 */
|
|
196
|
+
turbo: false
|
|
191
197
|
});
|
|
192
198
|
var SALARY_MAX = 1e7;
|
|
193
199
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -266,6 +272,21 @@ function normalizeConfig(patch) {
|
|
|
266
272
|
}
|
|
267
273
|
merged.lunchEnabled = merged.lunchEnabled !== false;
|
|
268
274
|
merged.lunchPaid = merged.lunchPaid === true;
|
|
275
|
+
merged.turbo = merged.turbo === true;
|
|
276
|
+
const decimals = Number(merged.decimals);
|
|
277
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) {
|
|
278
|
+
errors.push("\u5C0F\u6570\u4F4D\u6570\u9700\u8981\u662F 0~6 \u7684\u6574\u6570");
|
|
279
|
+
merged.decimals = Number.isInteger(decimals) ? decimals : DEFAULTS.decimals;
|
|
280
|
+
} else {
|
|
281
|
+
merged.decimals = decimals;
|
|
282
|
+
}
|
|
283
|
+
const updateMs = Number(merged.updateMs);
|
|
284
|
+
if (!Number.isInteger(updateMs) || updateMs < 16 || updateMs > 1e3) {
|
|
285
|
+
errors.push("\u5237\u65B0\u95F4\u9694\u9700\u8981\u662F 16~1000ms \u7684\u6574\u6570");
|
|
286
|
+
merged.updateMs = Number.isInteger(updateMs) ? updateMs : DEFAULTS.updateMs;
|
|
287
|
+
} else {
|
|
288
|
+
merged.updateMs = updateMs;
|
|
289
|
+
}
|
|
269
290
|
const holidays = normalizeDateList(merged.holidays);
|
|
270
291
|
const extraWorkdays = normalizeDateList(merged.extraWorkdays);
|
|
271
292
|
errors.push(...holidays.errors, ...extraWorkdays.errors);
|
|
@@ -537,14 +558,6 @@ var CARD = {
|
|
|
537
558
|
boxSizing: "border-box"
|
|
538
559
|
};
|
|
539
560
|
var NUM = { fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" };
|
|
540
|
-
var GOLD = {
|
|
541
|
-
color: "#D9A441",
|
|
542
|
-
backgroundImage: "linear-gradient(180deg, #FFE9A8 0%, #F2C14E 40%, #D79A2B 72%, #B87A16 100%)",
|
|
543
|
-
WebkitBackgroundClip: "text",
|
|
544
|
-
backgroundClip: "text",
|
|
545
|
-
WebkitTextFillColor: "transparent",
|
|
546
|
-
textShadow: "0 1px 0 rgba(0,0,0,.05)"
|
|
547
|
-
};
|
|
548
561
|
var GOLD_TINT = "#E7B84D";
|
|
549
562
|
var STATUS_META = {
|
|
550
563
|
rest: { key: "statusRest", color: C.label3 },
|
|
@@ -588,6 +601,16 @@ var zh = {
|
|
|
588
601
|
"configFailed": "\u8BBE\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C",
|
|
589
602
|
"retry": "\u91CD\u8BD5",
|
|
590
603
|
"settings": "\u8BBE\u7F6E",
|
|
604
|
+
"decimalsLabel": "\u5C0F\u6570",
|
|
605
|
+
"decimalsUnit": "\u4F4D",
|
|
606
|
+
"turboLabel": "\u52A0\u901F",
|
|
607
|
+
"updateAvailable": "\u53D1\u73B0\u65B0\u7248\u672C v{version}",
|
|
608
|
+
"updateNow": "\u7ACB\u5373\u66F4\u65B0",
|
|
609
|
+
"updating": "\u6B63\u5728\u4E0B\u8F7D\u65B0\u7248\u672C\u2026",
|
|
610
|
+
"updateStaged": "v{version} \u5DF2\u4E0B\u8F7D \xB7 \u91CD\u542F DSH \u751F\u6548",
|
|
611
|
+
"refreshRate": "\u5237\u65B0\u9891\u7387",
|
|
612
|
+
"rateSecond": "\u6309\u79D2\uFF081s\uFF09",
|
|
613
|
+
"rateFrame": "\u5237\u65B0\u7387\uFF08\u224816ms\uFF09",
|
|
591
614
|
"settingsHint": "\u6539\u5B8C\u70B9\u300C\u4FDD\u5B58\u300D\u751F\u6548\uFF1B\u683C\u5F0F\u4E0D\u5BF9\u4F1A\u5728\u4E0B\u65B9\u7EA2\u5B57\u63D0\u793A\uFF0C\u4E0D\u5F71\u54CD\u8BA1\u65F6\u3002",
|
|
592
615
|
"salaryField": "\u5B9E\u6536\u6708\u85AA",
|
|
593
616
|
"groupSalary": "\u5DE5\u8D44",
|
|
@@ -686,6 +709,16 @@ var en = {
|
|
|
686
709
|
"configFailed": "Failed to load settings, using defaults",
|
|
687
710
|
"retry": "Retry",
|
|
688
711
|
"settings": "Settings",
|
|
712
|
+
"decimalsLabel": "Decimals",
|
|
713
|
+
"decimalsUnit": "dp",
|
|
714
|
+
"turboLabel": "Turbo",
|
|
715
|
+
"updateAvailable": "New version v{version}",
|
|
716
|
+
"updateNow": "Update now",
|
|
717
|
+
"updating": "Downloading update\u2026",
|
|
718
|
+
"updateStaged": "v{version} downloaded \xB7 restart DSH to apply",
|
|
719
|
+
"refreshRate": "Refresh rate",
|
|
720
|
+
"rateSecond": "Per second (1s)",
|
|
721
|
+
"rateFrame": "Display rate (~16ms)",
|
|
689
722
|
"settingsHint": "Click Save to apply. Invalid input is shown in red below; the ticker keeps running.",
|
|
690
723
|
"salaryField": "Net monthly salary",
|
|
691
724
|
"groupSalary": "Salary",
|
|
@@ -763,7 +796,8 @@ function formFromConfig(config) {
|
|
|
763
796
|
lunchEnabled: config.lunchEnabled !== false,
|
|
764
797
|
lunchStart: String(config.lunchStart == null ? "" : config.lunchStart),
|
|
765
798
|
lunchEnd: String(config.lunchEnd == null ? "" : config.lunchEnd),
|
|
766
|
-
lunchPaid: config.lunchPaid === true
|
|
799
|
+
lunchPaid: config.lunchPaid === true,
|
|
800
|
+
updateMs: String(config.updateMs == null ? 1e3 : config.updateMs)
|
|
767
801
|
};
|
|
768
802
|
}
|
|
769
803
|
function patchFromForm(form) {
|
|
@@ -775,7 +809,8 @@ function patchFromForm(form) {
|
|
|
775
809
|
lunchEnabled: Boolean(form.lunchEnabled),
|
|
776
810
|
lunchStart: String(form.lunchStart || "").trim(),
|
|
777
811
|
lunchEnd: String(form.lunchEnd || "").trim(),
|
|
778
|
-
lunchPaid: Boolean(form.lunchPaid)
|
|
812
|
+
lunchPaid: Boolean(form.lunchPaid),
|
|
813
|
+
updateMs: Number(form.updateMs)
|
|
779
814
|
};
|
|
780
815
|
}
|
|
781
816
|
function useRemoteConfig() {
|
|
@@ -811,18 +846,50 @@ function useRemoteConfig() {
|
|
|
811
846
|
const refresh = import_react.default.useCallback(() => setTick((n) => n + 1), []);
|
|
812
847
|
return { ...state, refresh };
|
|
813
848
|
}
|
|
814
|
-
function
|
|
815
|
-
const [
|
|
849
|
+
function useUpdateInfo() {
|
|
850
|
+
const [state, setState] = import_react.default.useState({ status: "loading", data: null, error: null });
|
|
851
|
+
const load = import_react.default.useCallback(() => {
|
|
852
|
+
if (typeof fetch !== "function") {
|
|
853
|
+
setState({ status: "failed", data: null, error: "fetch unavailable" });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
fetch(`${ROUTE_BASE}/update`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
|
|
857
|
+
const body = await response.json();
|
|
858
|
+
if (!response.ok || !body || body.ok !== true) throw new Error(body && body.error || `HTTP ${response.status}`);
|
|
859
|
+
return body.data || null;
|
|
860
|
+
}).then((data) => setState({ status: "ready", data, error: null })).catch((error) => setState({ status: "failed", data: null, error: String(error && error.message || error) }));
|
|
861
|
+
}, []);
|
|
816
862
|
import_react.default.useEffect(() => {
|
|
817
|
-
|
|
818
|
-
const timer = setInterval(
|
|
819
|
-
const second = Math.floor(Date.now() / 1e3);
|
|
820
|
-
if (second === lastSecond) return;
|
|
821
|
-
lastSecond = second;
|
|
822
|
-
setNow(/* @__PURE__ */ new Date());
|
|
823
|
-
}, 250);
|
|
863
|
+
load();
|
|
864
|
+
const timer = setInterval(load, 6 * 3600 * 1e3);
|
|
824
865
|
return () => clearInterval(timer);
|
|
866
|
+
}, [load]);
|
|
867
|
+
const download = import_react.default.useCallback(() => {
|
|
868
|
+
setState((prev) => ({ ...prev, status: "updating" }));
|
|
869
|
+
if (typeof fetch !== "function") return;
|
|
870
|
+
fetch(`${ROUTE_BASE}/update`, { method: "POST", credentials: "same-origin" }).then(async (response) => {
|
|
871
|
+
const body = await response.json();
|
|
872
|
+
if (!response.ok || !body || body.ok !== true) throw new Error(body && body.error || `HTTP ${response.status}`);
|
|
873
|
+
return body.data || null;
|
|
874
|
+
}).then((data) => setState({ status: "ready", data, error: null })).catch((error) => setState({ status: "failed", data: null, error: String(error && error.message || error) }));
|
|
825
875
|
}, []);
|
|
876
|
+
return { ...state, reload: load, download };
|
|
877
|
+
}
|
|
878
|
+
function useClock(updateMs) {
|
|
879
|
+
const [now, setNow] = import_react.default.useState(() => /* @__PURE__ */ new Date());
|
|
880
|
+
import_react.default.useEffect(() => {
|
|
881
|
+
if (updateMs <= 16) {
|
|
882
|
+
let raf = 0;
|
|
883
|
+
const loop = () => {
|
|
884
|
+
setNow(/* @__PURE__ */ new Date());
|
|
885
|
+
raf = requestAnimationFrame(loop);
|
|
886
|
+
};
|
|
887
|
+
raf = requestAnimationFrame(loop);
|
|
888
|
+
return () => cancelAnimationFrame(raf);
|
|
889
|
+
}
|
|
890
|
+
const timer = setInterval(() => setNow(/* @__PURE__ */ new Date()), updateMs);
|
|
891
|
+
return () => clearInterval(timer);
|
|
892
|
+
}, [updateMs]);
|
|
826
893
|
return now;
|
|
827
894
|
}
|
|
828
895
|
function NiumaIcon(props) {
|
|
@@ -898,10 +965,26 @@ function SectionTitle(props) {
|
|
|
898
965
|
props.hint ? import_react.default.createElement("span", { key: "h", style: { fontSize: 10.5, color: C.label3, ...NUM } }, props.hint) : null
|
|
899
966
|
]);
|
|
900
967
|
}
|
|
968
|
+
function RollingMoney(props) {
|
|
969
|
+
const decimals = Math.max(0, Math.min(6, Number.isInteger(props.decimals) ? props.decimals : 4));
|
|
970
|
+
return import_react.default.createElement("span", {
|
|
971
|
+
style: {
|
|
972
|
+
display: "inline-block",
|
|
973
|
+
whiteSpace: "nowrap",
|
|
974
|
+
letterSpacing: "-1.2px",
|
|
975
|
+
fontVariantNumeric: "tabular-nums",
|
|
976
|
+
...props.style || {}
|
|
977
|
+
}
|
|
978
|
+
}, formatMoney(props.value, decimals));
|
|
979
|
+
}
|
|
901
980
|
function HeroCard(props) {
|
|
902
981
|
const t = props.t;
|
|
903
982
|
const snap = props.snap;
|
|
904
983
|
const rates = snap.rates;
|
|
984
|
+
const decimals = Number.isInteger(props.decimals) ? props.decimals : 4;
|
|
985
|
+
const onDecimalsChange = props.onDecimalsChange;
|
|
986
|
+
const turbo = props.turbo === true;
|
|
987
|
+
const onTurboChange = props.onTurboChange;
|
|
905
988
|
const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
|
|
906
989
|
const percentText = `${(progress * 100).toFixed(1)}%`;
|
|
907
990
|
const live = snap.status === "working";
|
|
@@ -949,28 +1032,88 @@ function HeroCard(props) {
|
|
|
949
1032
|
}) : null,
|
|
950
1033
|
t("earnedToday")
|
|
951
1034
|
]),
|
|
952
|
-
import_react.default.createElement(
|
|
953
|
-
"
|
|
954
|
-
{
|
|
955
|
-
|
|
956
|
-
|
|
1035
|
+
import_react.default.createElement("div", {
|
|
1036
|
+
key: "b",
|
|
1037
|
+
style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }
|
|
1038
|
+
}, [
|
|
1039
|
+
import_react.default.createElement(
|
|
1040
|
+
"span",
|
|
1041
|
+
{ key: "d", style: { fontSize: 11, color: C.label3, ...NUM } },
|
|
1042
|
+
`${t("todayOn")} ${snap.dateKey}`
|
|
1043
|
+
),
|
|
1044
|
+
import_react.default.createElement("div", { key: "c", style: { display: "inline-flex", alignItems: "center", gap: 6 } }, [
|
|
1045
|
+
import_react.default.createElement("label", {
|
|
1046
|
+
key: "dec",
|
|
1047
|
+
title: t("decimalsLabel"),
|
|
1048
|
+
style: { display: "inline-flex", alignItems: "center", gap: 3, fontSize: 10, color: C.label3, ...NUM }
|
|
1049
|
+
}, [
|
|
1050
|
+
import_react.default.createElement("span", { key: "l" }, t("decimalsLabel")),
|
|
1051
|
+
import_react.default.createElement("select", {
|
|
1052
|
+
key: "s",
|
|
1053
|
+
value: String(decimals),
|
|
1054
|
+
"aria-label": t("decimalsLabel"),
|
|
1055
|
+
onChange: (event) => onDecimalsChange && onDecimalsChange(Number(event.target.value)),
|
|
1056
|
+
style: {
|
|
1057
|
+
height: 22,
|
|
1058
|
+
padding: "0 2px",
|
|
1059
|
+
borderRadius: 6,
|
|
1060
|
+
border: `1px solid ${C.border1}`,
|
|
1061
|
+
background: C.layer2,
|
|
1062
|
+
color: C.label2,
|
|
1063
|
+
fontSize: 10.5,
|
|
1064
|
+
fontVariantNumeric: "tabular-nums",
|
|
1065
|
+
outline: "none",
|
|
1066
|
+
cursor: "pointer"
|
|
1067
|
+
}
|
|
1068
|
+
}, [0, 1, 2, 3, 4, 5, 6].map((n) => import_react.default.createElement("option", { key: n, value: String(n) }, String(n)))),
|
|
1069
|
+
import_react.default.createElement("span", { key: "u" }, t("decimalsUnit"))
|
|
1070
|
+
]),
|
|
1071
|
+
import_react.default.createElement("label", {
|
|
1072
|
+
key: "turbo",
|
|
1073
|
+
title: t("turboLabel"),
|
|
1074
|
+
style: {
|
|
1075
|
+
display: "inline-flex",
|
|
1076
|
+
alignItems: "center",
|
|
1077
|
+
gap: 3,
|
|
1078
|
+
fontSize: 10,
|
|
1079
|
+
padding: "1px 6px",
|
|
1080
|
+
borderRadius: 999,
|
|
1081
|
+
border: `1px solid ${turbo ? `color-mix(in srgb, ${GOLD_TINT} 60%, ${C.border1})` : C.border1}`,
|
|
1082
|
+
background: turbo ? `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)` : "transparent",
|
|
1083
|
+
color: turbo ? C.label2 : C.label3,
|
|
1084
|
+
cursor: "pointer",
|
|
1085
|
+
...NUM
|
|
1086
|
+
}
|
|
1087
|
+
}, [
|
|
1088
|
+
import_react.default.createElement("input", {
|
|
1089
|
+
key: "i",
|
|
1090
|
+
type: "checkbox",
|
|
1091
|
+
checked: turbo,
|
|
1092
|
+
onChange: (event) => onTurboChange && onTurboChange(event.target.checked),
|
|
1093
|
+
style: { accentColor: GOLD_TINT, width: 12, height: 12, margin: 0, cursor: "inherit" }
|
|
1094
|
+
}),
|
|
1095
|
+
import_react.default.createElement("span", { key: "l" }, t("turboLabel"))
|
|
1096
|
+
])
|
|
1097
|
+
])
|
|
1098
|
+
])
|
|
957
1099
|
]),
|
|
958
1100
|
import_react.default.createElement("div", {
|
|
959
1101
|
key: "amt",
|
|
960
1102
|
style: { display: "flex", alignItems: "baseline", gap: 7, marginTop: 4, flexWrap: "wrap" }
|
|
961
1103
|
}, [
|
|
962
|
-
import_react.default.createElement(
|
|
1104
|
+
import_react.default.createElement(RollingMoney, {
|
|
963
1105
|
key: "val",
|
|
1106
|
+
value: snap.todayEarned,
|
|
1107
|
+
decimals,
|
|
964
1108
|
style: {
|
|
965
1109
|
fontSize: 46,
|
|
966
1110
|
lineHeight: 1.12,
|
|
967
1111
|
fontWeight: 800,
|
|
968
|
-
|
|
969
|
-
...GOLD,
|
|
1112
|
+
color: "#D9A441",
|
|
970
1113
|
...NUM,
|
|
971
1114
|
...rest ? { opacity: 0.62 } : {}
|
|
972
1115
|
}
|
|
973
|
-
}
|
|
1116
|
+
}),
|
|
974
1117
|
import_react.default.createElement("span", {
|
|
975
1118
|
key: "unit",
|
|
976
1119
|
style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM }
|
|
@@ -1439,6 +1582,13 @@ function SettingsSection(props) {
|
|
|
1439
1582
|
import_react.default.createElement("span", { key: "l" }, t(labelKey))
|
|
1440
1583
|
]);
|
|
1441
1584
|
const lunchDisabled = !form.lunchEnabled;
|
|
1585
|
+
const RATE_OPTIONS = [
|
|
1586
|
+
["1000", t("rateSecond")],
|
|
1587
|
+
["250", "250 ms"],
|
|
1588
|
+
["100", "100 ms"],
|
|
1589
|
+
["50", "50 ms"],
|
|
1590
|
+
["16", t("rateFrame")]
|
|
1591
|
+
];
|
|
1442
1592
|
return import_react.default.createElement("details", {
|
|
1443
1593
|
open: true,
|
|
1444
1594
|
style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: "10px 12px" }
|
|
@@ -1468,6 +1618,14 @@ function SettingsSection(props) {
|
|
|
1468
1618
|
timeRange("lunchStart", "lunchEnd", t("groupLunch"), lunchDisabled),
|
|
1469
1619
|
toggle("lunchEnabled", "lunchEnabledShort", false),
|
|
1470
1620
|
toggle("lunchPaid", "lunchPaidShort", lunchDisabled)
|
|
1621
|
+
]),
|
|
1622
|
+
row("refresh", t("refreshRate"), [
|
|
1623
|
+
import_react.default.createElement("select", {
|
|
1624
|
+
key: "r",
|
|
1625
|
+
value: String(form.updateMs),
|
|
1626
|
+
onChange: (event) => setField("updateMs", event.target.value),
|
|
1627
|
+
style: { ...selectStyle, width: 128, padding: "4px 6px" }
|
|
1628
|
+
}, RATE_OPTIONS.map(([value, label]) => import_react.default.createElement("option", { key: value, value }, label)))
|
|
1471
1629
|
])
|
|
1472
1630
|
]),
|
|
1473
1631
|
import_react.default.createElement("div", {
|
|
@@ -1505,7 +1663,7 @@ function SettingsSection(props) {
|
|
|
1505
1663
|
function NiumaPanel(props) {
|
|
1506
1664
|
const t = props && typeof props.t === "function" ? props.t : fallbackTranslate;
|
|
1507
1665
|
const remote = useRemoteConfig();
|
|
1508
|
-
const
|
|
1666
|
+
const update = useUpdateInfo();
|
|
1509
1667
|
const cfg = import_react.default.useMemo(() => {
|
|
1510
1668
|
const raw = remote.data && remote.data.config ? remote.data.config : null;
|
|
1511
1669
|
if (!raw) return DEFAULTS;
|
|
@@ -1516,6 +1674,8 @@ function NiumaPanel(props) {
|
|
|
1516
1674
|
return DEFAULTS;
|
|
1517
1675
|
}
|
|
1518
1676
|
}, [remote.data]);
|
|
1677
|
+
const updateMs = cfg.turbo === true ? 16 : Number.isInteger(cfg.updateMs) ? cfg.updateMs : 1e3;
|
|
1678
|
+
const now = useClock(updateMs);
|
|
1519
1679
|
const snap = import_react.default.useMemo(() => earnedToday(cfg, now), [cfg, now]);
|
|
1520
1680
|
const rates = snap && snap.rates ? snap.rates : computeRates(cfg, snap && snap.dateKey);
|
|
1521
1681
|
const baseForm = import_react.default.useMemo(() => formFromConfig(cfg), [cfg]);
|
|
@@ -1605,7 +1765,30 @@ function NiumaPanel(props) {
|
|
|
1605
1765
|
setSave({ status: "error", errors: [String(error && error.message || error)], message: "" });
|
|
1606
1766
|
});
|
|
1607
1767
|
};
|
|
1768
|
+
const [quickError, setQuickError] = import_react.default.useState("");
|
|
1769
|
+
const postConfig = import_react.default.useCallback((patch) => {
|
|
1770
|
+
if (typeof fetch !== "function") return;
|
|
1771
|
+
fetch(`${ROUTE_BASE}/settings`, {
|
|
1772
|
+
method: "POST",
|
|
1773
|
+
headers: { "content-type": "application/json" },
|
|
1774
|
+
credentials: "same-origin",
|
|
1775
|
+
body: JSON.stringify(patch)
|
|
1776
|
+
}).then(async (response) => {
|
|
1777
|
+
let body = null;
|
|
1778
|
+
try {
|
|
1779
|
+
body = await response.json();
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
body = null;
|
|
1782
|
+
}
|
|
1783
|
+
if (!response.ok || !body || body.ok !== true) throw new Error(body && body.error || `HTTP ${response.status}`);
|
|
1784
|
+
setQuickError("");
|
|
1785
|
+
remote.refresh();
|
|
1786
|
+
}).catch((error) => setQuickError(String(error && error.message || error)));
|
|
1787
|
+
}, [remote.refresh]);
|
|
1788
|
+
const onDecimalsChange = (value) => postConfig({ decimals: value });
|
|
1789
|
+
const onTurboChange = (checked) => postConfig({ turbo: Boolean(checked) });
|
|
1608
1790
|
const holidaySource = remote.data && remote.data.holidaySource || HOLIDAY_SOURCE;
|
|
1791
|
+
const versionText = remote.data && remote.data.version || update.data && update.data.currentVersion || "";
|
|
1609
1792
|
const banner = remote.status === "failed" ? {
|
|
1610
1793
|
color: C.warn,
|
|
1611
1794
|
text: `${t("configFailed")}${remote.error ? `\uFF08${remote.error}\uFF09` : ""}`,
|
|
@@ -1707,7 +1890,68 @@ function NiumaPanel(props) {
|
|
|
1707
1890
|
}
|
|
1708
1891
|
}, t("retry")) : null
|
|
1709
1892
|
]) : null,
|
|
1710
|
-
|
|
1893
|
+
// 版本 / 自动更新提示:已下载→提示重启;发现新版→一键更新
|
|
1894
|
+
update.data && update.data.staged ? import_react.default.createElement("div", {
|
|
1895
|
+
key: "update",
|
|
1896
|
+
style: {
|
|
1897
|
+
display: "flex",
|
|
1898
|
+
alignItems: "center",
|
|
1899
|
+
gap: 8,
|
|
1900
|
+
flexWrap: "wrap",
|
|
1901
|
+
fontSize: 11.5,
|
|
1902
|
+
color: C.label2,
|
|
1903
|
+
padding: "6px 10px",
|
|
1904
|
+
borderRadius: 9,
|
|
1905
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
|
|
1906
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`
|
|
1907
|
+
}
|
|
1908
|
+
}, [
|
|
1909
|
+
import_react.default.createElement("span", { key: "dot", "aria-hidden": "true", style: { width: 6, height: 6, borderRadius: "50%", background: GOLD_TINT } }),
|
|
1910
|
+
import_react.default.createElement(
|
|
1911
|
+
"span",
|
|
1912
|
+
{ key: "txt", style: { flex: "1 1 160px" } },
|
|
1913
|
+
t("updateStaged").replace("{version}", String(update.data.stagedVersion || update.data.latestVersion || ""))
|
|
1914
|
+
)
|
|
1915
|
+
]) : update.status === "updating" ? import_react.default.createElement("div", {
|
|
1916
|
+
key: "update",
|
|
1917
|
+
style: { fontSize: 11.5, color: C.label3, padding: "2px 2px" }
|
|
1918
|
+
}, t("updating")) : update.data && update.data.hasUpdate ? import_react.default.createElement("div", {
|
|
1919
|
+
key: "update",
|
|
1920
|
+
style: {
|
|
1921
|
+
display: "flex",
|
|
1922
|
+
alignItems: "center",
|
|
1923
|
+
gap: 8,
|
|
1924
|
+
flexWrap: "wrap",
|
|
1925
|
+
fontSize: 11.5,
|
|
1926
|
+
color: C.label2,
|
|
1927
|
+
padding: "6px 10px",
|
|
1928
|
+
borderRadius: 9,
|
|
1929
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
|
|
1930
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`
|
|
1931
|
+
}
|
|
1932
|
+
}, [
|
|
1933
|
+
import_react.default.createElement(
|
|
1934
|
+
"span",
|
|
1935
|
+
{ key: "txt", style: { flex: "1 1 160px" } },
|
|
1936
|
+
t("updateAvailable").replace("{version}", String(update.data.latestVersion || ""))
|
|
1937
|
+
),
|
|
1938
|
+
import_react.default.createElement("button", {
|
|
1939
|
+
key: "go",
|
|
1940
|
+
type: "button",
|
|
1941
|
+
onClick: update.download,
|
|
1942
|
+
style: {
|
|
1943
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 70%, rgba(0,0,0,.15))`,
|
|
1944
|
+
color: "#3A2A05",
|
|
1945
|
+
background: "linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)",
|
|
1946
|
+
borderRadius: 7,
|
|
1947
|
+
padding: "3px 12px",
|
|
1948
|
+
fontSize: 11.5,
|
|
1949
|
+
fontWeight: 700,
|
|
1950
|
+
cursor: "pointer"
|
|
1951
|
+
}
|
|
1952
|
+
}, t("updateNow"))
|
|
1953
|
+
]) : null,
|
|
1954
|
+
import_react.default.createElement(HeroCard, { key: "hero", t, snap, decimals: cfg.decimals, onDecimalsChange, turbo: cfg.turbo, onTurboChange }),
|
|
1711
1955
|
import_react.default.createElement(CountdownRow, { key: "countdown", t, snap }),
|
|
1712
1956
|
import_react.default.createElement("div", {
|
|
1713
1957
|
key: "ratesHead",
|
|
@@ -1771,7 +2015,28 @@ function NiumaPanel(props) {
|
|
|
1771
2015
|
setCheck,
|
|
1772
2016
|
save,
|
|
1773
2017
|
onSave
|
|
1774
|
-
})
|
|
2018
|
+
}),
|
|
2019
|
+
// 版本信息固定放在面板最底部
|
|
2020
|
+
import_react.default.createElement("div", {
|
|
2021
|
+
key: "version",
|
|
2022
|
+
style: {
|
|
2023
|
+
display: "flex",
|
|
2024
|
+
alignItems: "center",
|
|
2025
|
+
justifyContent: "center",
|
|
2026
|
+
gap: 6,
|
|
2027
|
+
padding: "4px 0 0",
|
|
2028
|
+
fontSize: 10.5,
|
|
2029
|
+
color: C.label3,
|
|
2030
|
+
...NUM
|
|
2031
|
+
}
|
|
2032
|
+
}, [
|
|
2033
|
+
versionText ? import_react.default.createElement("span", { key: "v" }, `\u725B\u9A6C\u52A0\u901F\u5668 v${versionText}`) : null,
|
|
2034
|
+
update.data && update.data.staged ? import_react.default.createElement(
|
|
2035
|
+
"span",
|
|
2036
|
+
{ key: "s", style: { color: C.label3 } },
|
|
2037
|
+
`\xB7 ${t("updateStaged").replace("{version}", String(update.data.stagedVersion || update.data.latestVersion || ""))}`
|
|
2038
|
+
) : null
|
|
2039
|
+
])
|
|
1775
2040
|
])
|
|
1776
2041
|
]);
|
|
1777
2042
|
}
|