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 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 useSecondClock() {
815
- const [now, setNow] = import_react.default.useState(() => /* @__PURE__ */ new Date());
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
- let lastSecond = -1;
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
- "span",
954
- { key: "b", style: { fontSize: 11, color: C.label3, ...NUM } },
955
- `${t("todayOn")} ${snap.dateKey}`
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("span", {
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
- letterSpacing: "-1.2px",
969
- ...GOLD,
1112
+ color: "#D9A441",
970
1113
  ...NUM,
971
1114
  ...rest ? { opacity: 0.62 } : {}
972
1115
  }
973
- }, formatMoney(snap.todayEarned, 4)),
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 now = useSecondClock();
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
- import_react.default.createElement(HeroCard, { key: "hero", t, snap }),
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
  }