dsh-niuma 0.2.0 → 0.2.2
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 +35 -0
- package/README.md +4 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +400 -75
- package/lib/client.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.js +320 -75
- package/src/core.mjs +36 -0
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);
|
|
@@ -384,6 +405,13 @@ function earnedToday(config, now) {
|
|
|
384
405
|
const monthEarned = earnedThisMonth(config, key, date, todayEarned, rates);
|
|
385
406
|
const secondsToStart = workday && nowSec < startSec ? startSec - nowSec : 0;
|
|
386
407
|
const secondsToEnd = workday && nowSec < endSec ? endSec - nowSec : 0;
|
|
408
|
+
const monthWorkdays = rates.monthlyWorkdays;
|
|
409
|
+
let monthWorkdaysDone = 0;
|
|
410
|
+
for (let day = 1; day < date.getDate(); day += 1) {
|
|
411
|
+
if (isWorkday(dateKeyOf(new Date(date.getFullYear(), date.getMonth(), day)), config)) monthWorkdaysDone += 1;
|
|
412
|
+
}
|
|
413
|
+
if (workday && status === "off") monthWorkdaysDone += 1;
|
|
414
|
+
const monthWorkdaysLeft = Math.max(0, monthWorkdays - monthWorkdaysDone);
|
|
387
415
|
return {
|
|
388
416
|
dateKey: key,
|
|
389
417
|
isWorkday: workday,
|
|
@@ -394,6 +422,9 @@ function earnedToday(config, now) {
|
|
|
394
422
|
todayTarget,
|
|
395
423
|
todayProgress,
|
|
396
424
|
monthEarned,
|
|
425
|
+
monthWorkdays,
|
|
426
|
+
monthWorkdaysDone,
|
|
427
|
+
monthWorkdaysLeft,
|
|
397
428
|
secondsToStart,
|
|
398
429
|
secondsToEnd,
|
|
399
430
|
rates
|
|
@@ -571,19 +602,33 @@ var zh = {
|
|
|
571
602
|
"paidHoursPerDay": "\u65E5\u6709\u6548\u5DE5\u65F6",
|
|
572
603
|
"todayTarget": "\u4ECA\u65E5\u76EE\u6807",
|
|
573
604
|
"monthEarned": "\u672C\u6708\u5DF2\u8D5A",
|
|
605
|
+
"monthWorkedDays": "\u672C\u6708\u5DF2\u5DE5\u4F5C",
|
|
606
|
+
"monthRemainingDays": "\u5269\u4F59\u5DE5\u4F5C\u65E5",
|
|
574
607
|
"holidaySourceLink": "\u56FD\u52A1\u9662\u529E\u516C\u5385 2026 \u5E74\u8282\u5047\u65E5\u5B89\u6392",
|
|
575
608
|
"configLoading": "\u6B63\u5728\u8BFB\u53D6\u8BBE\u7F6E\u2026",
|
|
576
609
|
"configFailed": "\u8BBE\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C",
|
|
577
610
|
"retry": "\u91CD\u8BD5",
|
|
578
611
|
"settings": "\u8BBE\u7F6E",
|
|
612
|
+
"decimalsLabel": "\u5C0F\u6570",
|
|
613
|
+
"decimalsUnit": "\u4F4D",
|
|
614
|
+
"turboLabel": "\u52A0\u901F",
|
|
615
|
+
"refreshRate": "\u5237\u65B0\u9891\u7387",
|
|
616
|
+
"rateSecond": "\u6309\u79D2\uFF081s\uFF09",
|
|
617
|
+
"rateFrame": "\u5237\u65B0\u7387\uFF08\u224816ms\uFF09",
|
|
579
618
|
"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",
|
|
580
|
-
"salaryField": "\u5B9E\u6536\u6708\u85AA
|
|
619
|
+
"salaryField": "\u5B9E\u6536\u6708\u85AA",
|
|
620
|
+
"groupSalary": "\u5DE5\u8D44",
|
|
621
|
+
"groupWorkTime": "\u5DE5\u4F5C\u65F6\u95F4",
|
|
622
|
+
"groupLunch": "\u5348\u4F11",
|
|
623
|
+
"salaryUnit": "\u5143 / \u6708",
|
|
581
624
|
"workStartField": "\u4E0A\u73ED\u65F6\u95F4",
|
|
582
625
|
"workEndField": "\u4E0B\u73ED\u65F6\u95F4",
|
|
583
626
|
"lunchEnabledField": "\u542F\u7528\u5348\u4F11",
|
|
627
|
+
"lunchEnabledShort": "\u542F\u7528",
|
|
584
628
|
"lunchStartField": "\u5348\u4F11\u5F00\u59CB",
|
|
585
629
|
"lunchEndField": "\u5348\u4F11\u7ED3\u675F",
|
|
586
630
|
"lunchPaidField": "\u5348\u4F11\u7B97\u5DE5\u65F6",
|
|
631
|
+
"lunchPaidShort": "\u7B97\u5DE5\u65F6",
|
|
587
632
|
"holidaysCardTitle": "\u8282\u5047\u65E5\u5B89\u6392",
|
|
588
633
|
"holidays2026Label": "2026 \u5E74\uFF08\u5B98\u65B9\uFF09",
|
|
589
634
|
"holidays2027Label": "2027 \u5E74\uFF08\u9884\u4F30\uFF09",
|
|
@@ -607,6 +652,7 @@ var zh = {
|
|
|
607
652
|
"saveFailed": "\u4FDD\u5B58\u5931\u8D25\uFF1A",
|
|
608
653
|
"dayUnit": "\u5929",
|
|
609
654
|
"hourUnit": "\u5C0F\u65F6",
|
|
655
|
+
"minuteUnit": "\u5206\u949F",
|
|
610
656
|
"viewLabel": "\u67E5\u770B",
|
|
611
657
|
"viewCurrent": "\u672C\u6708",
|
|
612
658
|
"viewYear": "\u5168\u5E74\u5E73\u5747",
|
|
@@ -660,19 +706,33 @@ var en = {
|
|
|
660
706
|
"paidHoursPerDay": "Paid hours / day",
|
|
661
707
|
"todayTarget": "Today's target",
|
|
662
708
|
"monthEarned": "Earned this month",
|
|
709
|
+
"monthWorkedDays": "Worked this month",
|
|
710
|
+
"monthRemainingDays": "Workdays left",
|
|
663
711
|
"holidaySourceLink": "State Council 2026 holiday schedule",
|
|
664
712
|
"configLoading": "Loading settings\u2026",
|
|
665
713
|
"configFailed": "Failed to load settings, using defaults",
|
|
666
714
|
"retry": "Retry",
|
|
667
715
|
"settings": "Settings",
|
|
716
|
+
"decimalsLabel": "Decimals",
|
|
717
|
+
"decimalsUnit": "dp",
|
|
718
|
+
"turboLabel": "Turbo",
|
|
719
|
+
"refreshRate": "Refresh rate",
|
|
720
|
+
"rateSecond": "Per second (1s)",
|
|
721
|
+
"rateFrame": "Display rate (~16ms)",
|
|
668
722
|
"settingsHint": "Click Save to apply. Invalid input is shown in red below; the ticker keeps running.",
|
|
669
|
-
"salaryField": "Net monthly salary
|
|
723
|
+
"salaryField": "Net monthly salary",
|
|
724
|
+
"groupSalary": "Salary",
|
|
725
|
+
"groupWorkTime": "Working hours",
|
|
726
|
+
"groupLunch": "Lunch break",
|
|
727
|
+
"salaryUnit": "CNY / month",
|
|
670
728
|
"workStartField": "Work start",
|
|
671
729
|
"workEndField": "Work end",
|
|
672
730
|
"lunchEnabledField": "Enable lunch break",
|
|
731
|
+
"lunchEnabledShort": "On",
|
|
673
732
|
"lunchStartField": "Lunch start",
|
|
674
733
|
"lunchEndField": "Lunch end",
|
|
675
734
|
"lunchPaidField": "Lunch is paid",
|
|
735
|
+
"lunchPaidShort": "Paid",
|
|
676
736
|
"holidaysCardTitle": "Holiday calendar",
|
|
677
737
|
"holidays2026Label": "2026 (official)",
|
|
678
738
|
"holidays2027Label": "2027 (estimated)",
|
|
@@ -696,6 +756,7 @@ var en = {
|
|
|
696
756
|
"saveFailed": "Save failed: ",
|
|
697
757
|
"dayUnit": "d",
|
|
698
758
|
"hourUnit": "h",
|
|
759
|
+
"minuteUnit": "min",
|
|
699
760
|
"viewLabel": "View",
|
|
700
761
|
"viewCurrent": "This month",
|
|
701
762
|
"viewYear": "Year average",
|
|
@@ -735,7 +796,8 @@ function formFromConfig(config) {
|
|
|
735
796
|
lunchEnabled: config.lunchEnabled !== false,
|
|
736
797
|
lunchStart: String(config.lunchStart == null ? "" : config.lunchStart),
|
|
737
798
|
lunchEnd: String(config.lunchEnd == null ? "" : config.lunchEnd),
|
|
738
|
-
lunchPaid: config.lunchPaid === true
|
|
799
|
+
lunchPaid: config.lunchPaid === true,
|
|
800
|
+
updateMs: String(config.updateMs == null ? 1e3 : config.updateMs)
|
|
739
801
|
};
|
|
740
802
|
}
|
|
741
803
|
function patchFromForm(form) {
|
|
@@ -747,7 +809,8 @@ function patchFromForm(form) {
|
|
|
747
809
|
lunchEnabled: Boolean(form.lunchEnabled),
|
|
748
810
|
lunchStart: String(form.lunchStart || "").trim(),
|
|
749
811
|
lunchEnd: String(form.lunchEnd || "").trim(),
|
|
750
|
-
lunchPaid: Boolean(form.lunchPaid)
|
|
812
|
+
lunchPaid: Boolean(form.lunchPaid),
|
|
813
|
+
updateMs: Number(form.updateMs)
|
|
751
814
|
};
|
|
752
815
|
}
|
|
753
816
|
function useRemoteConfig() {
|
|
@@ -783,18 +846,21 @@ function useRemoteConfig() {
|
|
|
783
846
|
const refresh = import_react.default.useCallback(() => setTick((n) => n + 1), []);
|
|
784
847
|
return { ...state, refresh };
|
|
785
848
|
}
|
|
786
|
-
function
|
|
849
|
+
function useClock(updateMs) {
|
|
787
850
|
const [now, setNow] = import_react.default.useState(() => /* @__PURE__ */ new Date());
|
|
788
851
|
import_react.default.useEffect(() => {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
852
|
+
if (updateMs <= 16) {
|
|
853
|
+
let raf = 0;
|
|
854
|
+
const loop = () => {
|
|
855
|
+
setNow(/* @__PURE__ */ new Date());
|
|
856
|
+
raf = requestAnimationFrame(loop);
|
|
857
|
+
};
|
|
858
|
+
raf = requestAnimationFrame(loop);
|
|
859
|
+
return () => cancelAnimationFrame(raf);
|
|
860
|
+
}
|
|
861
|
+
const timer = setInterval(() => setNow(/* @__PURE__ */ new Date()), updateMs);
|
|
796
862
|
return () => clearInterval(timer);
|
|
797
|
-
}, []);
|
|
863
|
+
}, [updateMs]);
|
|
798
864
|
return now;
|
|
799
865
|
}
|
|
800
866
|
function NiumaIcon(props) {
|
|
@@ -870,10 +936,106 @@ function SectionTitle(props) {
|
|
|
870
936
|
props.hint ? import_react.default.createElement("span", { key: "h", style: { fontSize: 10.5, color: C.label3, ...NUM } }, props.hint) : null
|
|
871
937
|
]);
|
|
872
938
|
}
|
|
939
|
+
var ROLL_BLOCKS = Array.from({ length: 11 }, (_, i) => String(i % 10));
|
|
940
|
+
function RollingDigit(props) {
|
|
941
|
+
const heightEm = 1.22;
|
|
942
|
+
const widthEm = 0.62;
|
|
943
|
+
const pos = Number.isFinite(props.position) ? props.position : 0;
|
|
944
|
+
const wheel = (pos % 10 + 10) % 10;
|
|
945
|
+
return import_react.default.createElement("span", {
|
|
946
|
+
"aria-hidden": "true",
|
|
947
|
+
style: {
|
|
948
|
+
display: "inline-block",
|
|
949
|
+
position: "relative",
|
|
950
|
+
overflow: "hidden",
|
|
951
|
+
width: `${widthEm}em`,
|
|
952
|
+
height: `${heightEm}em`,
|
|
953
|
+
verticalAlign: "baseline"
|
|
954
|
+
}
|
|
955
|
+
}, [
|
|
956
|
+
import_react.default.createElement("span", {
|
|
957
|
+
key: "ghost",
|
|
958
|
+
style: { visibility: "hidden", display: "block", height: `${heightEm}em`, lineHeight: `${heightEm}em` }
|
|
959
|
+
}, "0"),
|
|
960
|
+
import_react.default.createElement("span", {
|
|
961
|
+
key: "strip",
|
|
962
|
+
style: {
|
|
963
|
+
position: "absolute",
|
|
964
|
+
left: 0,
|
|
965
|
+
top: 0,
|
|
966
|
+
display: "flex",
|
|
967
|
+
flexDirection: "column",
|
|
968
|
+
transform: `translateY(${-(wheel * heightEm)}em)`,
|
|
969
|
+
willChange: "transform"
|
|
970
|
+
}
|
|
971
|
+
}, ROLL_BLOCKS.map((value, i) => import_react.default.createElement("span", {
|
|
972
|
+
key: i,
|
|
973
|
+
style: { display: "block", height: `${heightEm}em`, lineHeight: `${heightEm}em` }
|
|
974
|
+
}, value)))
|
|
975
|
+
]);
|
|
976
|
+
}
|
|
977
|
+
function RollingMoney(props) {
|
|
978
|
+
const decimals = Math.max(0, Math.min(6, Number.isInteger(props.decimals) ? props.decimals : 4));
|
|
979
|
+
const text = formatMoney(props.value, decimals);
|
|
980
|
+
const scale = 10 ** decimals;
|
|
981
|
+
const chars = text.split("");
|
|
982
|
+
const significances = new Array(chars.length).fill(-1);
|
|
983
|
+
let seen = 0;
|
|
984
|
+
for (let i = chars.length - 1; i >= 0; i -= 1) {
|
|
985
|
+
if (/[0-9]/.test(chars[i])) {
|
|
986
|
+
significances[i] = seen;
|
|
987
|
+
seen += 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return import_react.default.createElement("span", {
|
|
991
|
+
style: {
|
|
992
|
+
display: "inline-flex",
|
|
993
|
+
alignItems: "baseline",
|
|
994
|
+
letterSpacing: 0,
|
|
995
|
+
position: "relative",
|
|
996
|
+
// 等宽字体保证每个数字轮宽度稳定(不裁字、不抖动)
|
|
997
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
998
|
+
...props.style || {}
|
|
999
|
+
}
|
|
1000
|
+
}, [
|
|
1001
|
+
import_react.default.createElement("span", {
|
|
1002
|
+
key: "sr",
|
|
1003
|
+
style: {
|
|
1004
|
+
position: "absolute",
|
|
1005
|
+
width: 1,
|
|
1006
|
+
height: 1,
|
|
1007
|
+
padding: 0,
|
|
1008
|
+
margin: -1,
|
|
1009
|
+
overflow: "hidden",
|
|
1010
|
+
clip: "rect(0,0,0,0)",
|
|
1011
|
+
whiteSpace: "nowrap",
|
|
1012
|
+
border: 0
|
|
1013
|
+
}
|
|
1014
|
+
}, text),
|
|
1015
|
+
import_react.default.createElement("span", {
|
|
1016
|
+
key: "visual",
|
|
1017
|
+
"aria-hidden": "true",
|
|
1018
|
+
style: { display: "inline-flex", alignItems: "baseline" }
|
|
1019
|
+
}, chars.map((char, i) => {
|
|
1020
|
+
if (!/[0-9]/.test(char)) {
|
|
1021
|
+
return import_react.default.createElement("span", {
|
|
1022
|
+
key: `s${i}`,
|
|
1023
|
+
style: { display: "inline-block", lineHeight: "1.22em" }
|
|
1024
|
+
}, char);
|
|
1025
|
+
}
|
|
1026
|
+
const position = (Number(props.value) || 0) * scale / 10 ** significances[i];
|
|
1027
|
+
return import_react.default.createElement(RollingDigit, { key: `d${i}`, position });
|
|
1028
|
+
}))
|
|
1029
|
+
]);
|
|
1030
|
+
}
|
|
873
1031
|
function HeroCard(props) {
|
|
874
1032
|
const t = props.t;
|
|
875
1033
|
const snap = props.snap;
|
|
876
1034
|
const rates = snap.rates;
|
|
1035
|
+
const decimals = Number.isInteger(props.decimals) ? props.decimals : 4;
|
|
1036
|
+
const onDecimalsChange = props.onDecimalsChange;
|
|
1037
|
+
const turbo = props.turbo === true;
|
|
1038
|
+
const onTurboChange = props.onTurboChange;
|
|
877
1039
|
const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
|
|
878
1040
|
const percentText = `${(progress * 100).toFixed(1)}%`;
|
|
879
1041
|
const live = snap.status === "working";
|
|
@@ -931,22 +1093,83 @@ function HeroCard(props) {
|
|
|
931
1093
|
key: "amt",
|
|
932
1094
|
style: { display: "flex", alignItems: "baseline", gap: 7, marginTop: 4, flexWrap: "wrap" }
|
|
933
1095
|
}, [
|
|
934
|
-
import_react.default.createElement(
|
|
1096
|
+
import_react.default.createElement(RollingMoney, {
|
|
935
1097
|
key: "val",
|
|
1098
|
+
value: snap.todayEarned,
|
|
1099
|
+
decimals,
|
|
936
1100
|
style: {
|
|
937
1101
|
fontSize: 46,
|
|
938
1102
|
lineHeight: 1.12,
|
|
939
1103
|
fontWeight: 800,
|
|
940
|
-
letterSpacing: "-1.2px",
|
|
941
1104
|
...GOLD,
|
|
942
1105
|
...NUM,
|
|
943
1106
|
...rest ? { opacity: 0.62 } : {}
|
|
944
1107
|
}
|
|
945
|
-
}
|
|
1108
|
+
}),
|
|
946
1109
|
import_react.default.createElement("span", {
|
|
947
1110
|
key: "unit",
|
|
948
1111
|
style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM }
|
|
949
|
-
}, "\u5143")
|
|
1112
|
+
}, "\u5143"),
|
|
1113
|
+
import_react.default.createElement("label", {
|
|
1114
|
+
key: "dec",
|
|
1115
|
+
title: t("decimalsLabel"),
|
|
1116
|
+
style: {
|
|
1117
|
+
marginLeft: "auto",
|
|
1118
|
+
display: "inline-flex",
|
|
1119
|
+
alignItems: "center",
|
|
1120
|
+
gap: 3,
|
|
1121
|
+
fontSize: 10,
|
|
1122
|
+
color: C.label3,
|
|
1123
|
+
...NUM
|
|
1124
|
+
}
|
|
1125
|
+
}, [
|
|
1126
|
+
import_react.default.createElement("span", { key: "l" }, t("decimalsLabel")),
|
|
1127
|
+
import_react.default.createElement("select", {
|
|
1128
|
+
key: "s",
|
|
1129
|
+
value: String(decimals),
|
|
1130
|
+
"aria-label": t("decimalsLabel"),
|
|
1131
|
+
onChange: (event) => onDecimalsChange && onDecimalsChange(Number(event.target.value)),
|
|
1132
|
+
style: {
|
|
1133
|
+
height: 22,
|
|
1134
|
+
padding: "0 2px",
|
|
1135
|
+
borderRadius: 6,
|
|
1136
|
+
border: `1px solid ${C.border1}`,
|
|
1137
|
+
background: C.layer2,
|
|
1138
|
+
color: C.label2,
|
|
1139
|
+
fontSize: 10.5,
|
|
1140
|
+
fontVariantNumeric: "tabular-nums",
|
|
1141
|
+
outline: "none",
|
|
1142
|
+
cursor: "pointer"
|
|
1143
|
+
}
|
|
1144
|
+
}, [0, 1, 2, 3, 4, 5, 6].map((n) => import_react.default.createElement("option", { key: n, value: String(n) }, String(n)))),
|
|
1145
|
+
import_react.default.createElement("span", { key: "u" }, t("decimalsUnit"))
|
|
1146
|
+
]),
|
|
1147
|
+
import_react.default.createElement("label", {
|
|
1148
|
+
key: "turbo",
|
|
1149
|
+
title: t("turboLabel"),
|
|
1150
|
+
style: {
|
|
1151
|
+
display: "inline-flex",
|
|
1152
|
+
alignItems: "center",
|
|
1153
|
+
gap: 3,
|
|
1154
|
+
fontSize: 10,
|
|
1155
|
+
padding: "1px 6px",
|
|
1156
|
+
borderRadius: 999,
|
|
1157
|
+
border: `1px solid ${turbo ? `color-mix(in srgb, ${GOLD_TINT} 60%, ${C.border1})` : C.border1}`,
|
|
1158
|
+
background: turbo ? `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)` : "transparent",
|
|
1159
|
+
color: turbo ? C.label2 : C.label3,
|
|
1160
|
+
cursor: "pointer",
|
|
1161
|
+
...NUM
|
|
1162
|
+
}
|
|
1163
|
+
}, [
|
|
1164
|
+
import_react.default.createElement("input", {
|
|
1165
|
+
key: "i",
|
|
1166
|
+
type: "checkbox",
|
|
1167
|
+
checked: turbo,
|
|
1168
|
+
onChange: (event) => onTurboChange && onTurboChange(event.target.checked),
|
|
1169
|
+
style: { accentColor: GOLD_TINT, width: 12, height: 12, margin: 0, cursor: "inherit" }
|
|
1170
|
+
}),
|
|
1171
|
+
import_react.default.createElement("span", { key: "l" }, t("turboLabel"))
|
|
1172
|
+
])
|
|
950
1173
|
]),
|
|
951
1174
|
import_react.default.createElement("div", {
|
|
952
1175
|
key: "chips",
|
|
@@ -1131,7 +1354,9 @@ function DetailsFooter(props) {
|
|
|
1131
1354
|
{ key: "workdays", label: t("workdaysThisMonth"), value: `${rates.monthlyWorkdays}${t("dayUnit")}` },
|
|
1132
1355
|
{ key: "hours", label: t("paidHoursPerDay"), value: `${trimNumber(rates.paidHours, 2)}${t("hourUnit")}` },
|
|
1133
1356
|
{ key: "target", label: t("todayTarget"), value: money(snap.todayTarget, 2) },
|
|
1134
|
-
{ key: "month", label: t("monthEarned"), value: money(snap.monthEarned, 2) }
|
|
1357
|
+
{ key: "month", label: t("monthEarned"), value: money(snap.monthEarned, 2) },
|
|
1358
|
+
{ key: "worked", label: t("monthWorkedDays"), value: `${snap.monthWorkdaysDone}${t("dayUnit")}` },
|
|
1359
|
+
{ key: "left", label: t("monthRemainingDays"), value: `${snap.monthWorkdaysLeft}${t("dayUnit")}` }
|
|
1135
1360
|
];
|
|
1136
1361
|
return import_react.default.createElement("div", {
|
|
1137
1362
|
style: {
|
|
@@ -1302,84 +1527,162 @@ function SettingsSection(props) {
|
|
|
1302
1527
|
const setCheck = props.setCheck;
|
|
1303
1528
|
const save = props.save;
|
|
1304
1529
|
const onSave = props.onSave;
|
|
1530
|
+
const rowStyle = {
|
|
1531
|
+
display: "flex",
|
|
1532
|
+
alignItems: "center",
|
|
1533
|
+
gap: 10,
|
|
1534
|
+
minHeight: 33,
|
|
1535
|
+
padding: "2px 0",
|
|
1536
|
+
borderTop: `1px solid ${C.border1}`
|
|
1537
|
+
};
|
|
1538
|
+
const rowKeyStyle = { width: 62, flex: "0 0 62px", fontSize: 11.5, color: C.label3 };
|
|
1539
|
+
const rowValStyle = { display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap", minWidth: 0 };
|
|
1305
1540
|
const inputStyle = {
|
|
1306
|
-
width: "100%",
|
|
1307
1541
|
boxSizing: "border-box",
|
|
1308
|
-
|
|
1309
|
-
|
|
1542
|
+
width: 88,
|
|
1543
|
+
minHeight: 28,
|
|
1544
|
+
padding: "4px 8px",
|
|
1545
|
+
borderRadius: 7,
|
|
1310
1546
|
border: `1px solid ${C.border}`,
|
|
1311
1547
|
background: C.layer2,
|
|
1312
1548
|
color: C.label,
|
|
1313
|
-
fontSize:
|
|
1549
|
+
fontSize: 12.5,
|
|
1314
1550
|
fontVariantNumeric: "tabular-nums",
|
|
1315
1551
|
outline: "none"
|
|
1316
1552
|
};
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1553
|
+
const selectStyle = {
|
|
1554
|
+
boxSizing: "border-box",
|
|
1555
|
+
width: 48,
|
|
1556
|
+
minHeight: 28,
|
|
1557
|
+
padding: "4px 2px 4px 6px",
|
|
1558
|
+
borderRadius: 7,
|
|
1559
|
+
border: `1px solid ${C.border}`,
|
|
1560
|
+
background: C.layer2,
|
|
1561
|
+
color: C.label,
|
|
1562
|
+
fontSize: 12.5,
|
|
1563
|
+
fontVariantNumeric: "tabular-nums",
|
|
1564
|
+
outline: "none",
|
|
1565
|
+
cursor: "pointer"
|
|
1566
|
+
};
|
|
1567
|
+
const unitStyle = { fontSize: 10.5, color: C.label3, whiteSpace: "nowrap" };
|
|
1568
|
+
const dashStyle = { fontSize: 11, color: C.label3, margin: "0 1px" };
|
|
1569
|
+
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"));
|
|
1570
|
+
const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"));
|
|
1571
|
+
const row = (key, label, values) => import_react.default.createElement("div", { key, style: rowStyle }, [
|
|
1572
|
+
import_react.default.createElement("span", { key: "k", style: rowKeyStyle }, label),
|
|
1573
|
+
import_react.default.createElement("div", { key: "v", style: rowValStyle }, values)
|
|
1574
|
+
]);
|
|
1575
|
+
const timeSelects = (field, label, disabled = false) => {
|
|
1576
|
+
const [rawHour = "00", rawMinute = "00"] = String(form[field] || "00:00").split(":");
|
|
1577
|
+
const hour = HOUR_OPTIONS.includes(rawHour) ? rawHour : "00";
|
|
1578
|
+
const minute = MINUTE_OPTIONS.includes(rawMinute) ? rawMinute : "00";
|
|
1579
|
+
const commit = (nextHour, nextMinute) => setField(field, `${nextHour}:${nextMinute}`);
|
|
1580
|
+
const style = { ...selectStyle, opacity: disabled ? 0.45 : 1, cursor: disabled ? "default" : "pointer" };
|
|
1581
|
+
return import_react.default.createElement("span", { key: field, style: { display: "inline-flex", alignItems: "center", gap: 2 } }, [
|
|
1582
|
+
import_react.default.createElement("select", {
|
|
1583
|
+
key: "h",
|
|
1584
|
+
value: hour,
|
|
1585
|
+
disabled,
|
|
1586
|
+
"aria-label": `${label} ${t("hourUnit")}`,
|
|
1587
|
+
onChange: (event) => commit(event.target.value, minute),
|
|
1588
|
+
style
|
|
1589
|
+
}, HOUR_OPTIONS.map((value) => import_react.default.createElement("option", { key: value, value }, value))),
|
|
1590
|
+
import_react.default.createElement("span", { key: "c", style: { color: C.label3, fontWeight: 700, fontSize: 12 } }, ":"),
|
|
1591
|
+
import_react.default.createElement("select", {
|
|
1592
|
+
key: "m",
|
|
1593
|
+
value: minute,
|
|
1594
|
+
disabled,
|
|
1595
|
+
"aria-label": `${label} ${t("minuteUnit")}`,
|
|
1596
|
+
onChange: (event) => commit(hour, event.target.value),
|
|
1597
|
+
style
|
|
1598
|
+
}, MINUTE_OPTIONS.map((value) => import_react.default.createElement("option", { key: value, value }, value)))
|
|
1599
|
+
]);
|
|
1600
|
+
};
|
|
1601
|
+
const timeRange = (startField, endField, label, disabled) => import_react.default.createElement("span", {
|
|
1602
|
+
key: `${startField}-${endField}`,
|
|
1603
|
+
style: { display: "inline-flex", alignItems: "center", gap: 2 }
|
|
1321
1604
|
}, [
|
|
1322
|
-
|
|
1323
|
-
import_react.default.createElement("
|
|
1324
|
-
|
|
1325
|
-
type: "text",
|
|
1326
|
-
value: form[field],
|
|
1327
|
-
placeholder: placeholder || "HH:mm",
|
|
1328
|
-
onChange: (event) => setField(field, event.target.value),
|
|
1329
|
-
style: inputStyle
|
|
1330
|
-
})
|
|
1605
|
+
timeSelects(startField, label, disabled),
|
|
1606
|
+
import_react.default.createElement("span", { key: "dash", style: dashStyle }, "\u2013"),
|
|
1607
|
+
timeSelects(endField, label, disabled)
|
|
1331
1608
|
]);
|
|
1332
|
-
const
|
|
1609
|
+
const toggle = (field, labelKey, disabled) => import_react.default.createElement("label", {
|
|
1333
1610
|
key: field,
|
|
1334
1611
|
style: {
|
|
1335
|
-
display: "flex",
|
|
1612
|
+
display: "inline-flex",
|
|
1336
1613
|
alignItems: "center",
|
|
1337
|
-
gap:
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1614
|
+
gap: 5,
|
|
1615
|
+
padding: "2px 6px",
|
|
1616
|
+
borderRadius: 8,
|
|
1617
|
+
border: `1px solid ${C.border1}`,
|
|
1618
|
+
background: C.layer2,
|
|
1619
|
+
fontSize: 10.5,
|
|
1620
|
+
color: disabled ? C.label3 : C.label2,
|
|
1621
|
+
whiteSpace: "nowrap",
|
|
1622
|
+
cursor: disabled ? "default" : "pointer",
|
|
1623
|
+
opacity: disabled ? 0.55 : 1
|
|
1343
1624
|
}
|
|
1344
1625
|
}, [
|
|
1345
1626
|
import_react.default.createElement("input", {
|
|
1346
1627
|
key: "i",
|
|
1347
1628
|
type: "checkbox",
|
|
1348
1629
|
checked: Boolean(form[field]),
|
|
1349
|
-
|
|
1630
|
+
disabled,
|
|
1631
|
+
onChange: (event) => setCheck(field, event.target.checked),
|
|
1632
|
+
style: { accentColor: GOLD_TINT, width: 13, height: 13, margin: 0, cursor: "inherit" }
|
|
1350
1633
|
}),
|
|
1351
1634
|
import_react.default.createElement("span", { key: "l" }, t(labelKey))
|
|
1352
1635
|
]);
|
|
1636
|
+
const lunchDisabled = !form.lunchEnabled;
|
|
1637
|
+
const RATE_OPTIONS = [
|
|
1638
|
+
["1000", t("rateSecond")],
|
|
1639
|
+
["250", "250 ms"],
|
|
1640
|
+
["100", "100 ms"],
|
|
1641
|
+
["50", "50 ms"],
|
|
1642
|
+
["16", t("rateFrame")]
|
|
1643
|
+
];
|
|
1353
1644
|
return import_react.default.createElement("details", {
|
|
1354
|
-
|
|
1645
|
+
open: true,
|
|
1646
|
+
style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: "10px 12px" }
|
|
1355
1647
|
}, [
|
|
1356
1648
|
import_react.default.createElement("summary", {
|
|
1357
1649
|
key: "sum",
|
|
1358
|
-
style: { cursor: "pointer", fontSize:
|
|
1650
|
+
style: { cursor: "pointer", fontSize: 12.5, fontWeight: 650, color: C.label, userSelect: "none" }
|
|
1359
1651
|
}, t("settings")),
|
|
1360
|
-
import_react.default.createElement("div", {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1652
|
+
import_react.default.createElement("div", { key: "body", style: { marginTop: 7 } }, [
|
|
1653
|
+
row("salary", t("salaryField"), [
|
|
1654
|
+
import_react.default.createElement("input", {
|
|
1655
|
+
key: "i",
|
|
1656
|
+
type: "text",
|
|
1657
|
+
inputMode: "decimal",
|
|
1658
|
+
value: form.monthlySalary,
|
|
1659
|
+
placeholder: "10000",
|
|
1660
|
+
onChange: (event) => setField("monthlySalary", event.target.value),
|
|
1661
|
+
style: inputStyle
|
|
1662
|
+
}),
|
|
1663
|
+
import_react.default.createElement("span", { key: "u", style: unitStyle }, t("salaryUnit"))
|
|
1664
|
+
]),
|
|
1665
|
+
row("worktime", t("groupWorkTime"), [
|
|
1666
|
+
timeRange("workStart", "workEnd", t("groupWorkTime"), false)
|
|
1667
|
+
]),
|
|
1668
|
+
row("lunch", t("groupLunch"), [
|
|
1669
|
+
// 时间在前、两个开关在后(放到行尾)
|
|
1670
|
+
timeRange("lunchStart", "lunchEnd", t("groupLunch"), lunchDisabled),
|
|
1671
|
+
toggle("lunchEnabled", "lunchEnabledShort", false),
|
|
1672
|
+
toggle("lunchPaid", "lunchPaidShort", lunchDisabled)
|
|
1673
|
+
]),
|
|
1674
|
+
row("refresh", t("refreshRate"), [
|
|
1675
|
+
import_react.default.createElement("select", {
|
|
1676
|
+
key: "r",
|
|
1677
|
+
value: String(form.updateMs),
|
|
1678
|
+
onChange: (event) => setField("updateMs", event.target.value),
|
|
1679
|
+
style: { ...selectStyle, width: 128, padding: "4px 6px" }
|
|
1680
|
+
}, RATE_OPTIONS.map(([value, label]) => import_react.default.createElement("option", { key: value, value }, label)))
|
|
1681
|
+
])
|
|
1379
1682
|
]),
|
|
1380
1683
|
import_react.default.createElement("div", {
|
|
1381
1684
|
key: "actions",
|
|
1382
|
-
style: { display: "flex", alignItems: "center", gap: 10,
|
|
1685
|
+
style: { display: "flex", alignItems: "center", gap: 10, marginTop: 7, paddingTop: 7, borderTop: `1px solid ${C.border1}` }
|
|
1383
1686
|
}, [
|
|
1384
1687
|
import_react.default.createElement("button", {
|
|
1385
1688
|
key: "save",
|
|
@@ -1387,25 +1690,24 @@ function SettingsSection(props) {
|
|
|
1387
1690
|
onClick: onSave,
|
|
1388
1691
|
disabled: save.status === "saving",
|
|
1389
1692
|
style: {
|
|
1390
|
-
// 金色按钮:与主卡金额、节假日强调同一套金色系;深棕字保证任何主题下都有对比度
|
|
1391
1693
|
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 72%, rgba(0,0,0,.18))`,
|
|
1392
1694
|
color: "#3A2A05",
|
|
1393
1695
|
background: save.status === "saving" ? `color-mix(in srgb, ${GOLD_TINT} 52%, #fff)` : "linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)",
|
|
1394
1696
|
borderRadius: 8,
|
|
1395
|
-
padding: "
|
|
1396
|
-
fontSize:
|
|
1697
|
+
padding: "5px 16px",
|
|
1698
|
+
fontSize: 12.5,
|
|
1397
1699
|
fontWeight: 700,
|
|
1398
1700
|
letterSpacing: ".3px",
|
|
1399
1701
|
cursor: save.status === "saving" ? "default" : "pointer",
|
|
1400
1702
|
opacity: save.status === "saving" ? 0.72 : 1,
|
|
1401
|
-
boxShadow: `0 2px
|
|
1703
|
+
boxShadow: `0 2px 9px color-mix(in srgb, ${GOLD_TINT} 34%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
|
|
1402
1704
|
textShadow: "0 1px 0 rgba(255,255,255,.28)"
|
|
1403
1705
|
}
|
|
1404
1706
|
}, save.status === "saving" ? t("saving") : t("save")),
|
|
1405
|
-
save.status === "ok" ? import_react.default.createElement("span", { key: "ok", style: { fontSize:
|
|
1707
|
+
save.status === "ok" ? import_react.default.createElement("span", { key: "ok", style: { fontSize: 11.5, color: C.ok } }, save.message || t("saved")) : null,
|
|
1406
1708
|
save.status === "error" ? import_react.default.createElement("span", {
|
|
1407
1709
|
key: "err",
|
|
1408
|
-
style: { fontSize:
|
|
1710
|
+
style: { fontSize: 11.5, color: C.error, flex: "1 1 200px", whiteSpace: "pre-wrap" }
|
|
1409
1711
|
}, `${t("saveFailed")}${save.errors.join("\uFF1B")}`) : null
|
|
1410
1712
|
])
|
|
1411
1713
|
]);
|
|
@@ -1413,7 +1715,6 @@ function SettingsSection(props) {
|
|
|
1413
1715
|
function NiumaPanel(props) {
|
|
1414
1716
|
const t = props && typeof props.t === "function" ? props.t : fallbackTranslate;
|
|
1415
1717
|
const remote = useRemoteConfig();
|
|
1416
|
-
const now = useSecondClock();
|
|
1417
1718
|
const cfg = import_react.default.useMemo(() => {
|
|
1418
1719
|
const raw = remote.data && remote.data.config ? remote.data.config : null;
|
|
1419
1720
|
if (!raw) return DEFAULTS;
|
|
@@ -1424,6 +1725,8 @@ function NiumaPanel(props) {
|
|
|
1424
1725
|
return DEFAULTS;
|
|
1425
1726
|
}
|
|
1426
1727
|
}, [remote.data]);
|
|
1728
|
+
const updateMs = cfg.turbo === true ? 16 : Number.isInteger(cfg.updateMs) ? cfg.updateMs : 1e3;
|
|
1729
|
+
const now = useClock(updateMs);
|
|
1427
1730
|
const snap = import_react.default.useMemo(() => earnedToday(cfg, now), [cfg, now]);
|
|
1428
1731
|
const rates = snap && snap.rates ? snap.rates : computeRates(cfg, snap && snap.dateKey);
|
|
1429
1732
|
const baseForm = import_react.default.useMemo(() => formFromConfig(cfg), [cfg]);
|
|
@@ -1513,6 +1816,28 @@ function NiumaPanel(props) {
|
|
|
1513
1816
|
setSave({ status: "error", errors: [String(error && error.message || error)], message: "" });
|
|
1514
1817
|
});
|
|
1515
1818
|
};
|
|
1819
|
+
const [quickError, setQuickError] = import_react.default.useState("");
|
|
1820
|
+
const postConfig = import_react.default.useCallback((patch) => {
|
|
1821
|
+
if (typeof fetch !== "function") return;
|
|
1822
|
+
fetch(`${ROUTE_BASE}/settings`, {
|
|
1823
|
+
method: "POST",
|
|
1824
|
+
headers: { "content-type": "application/json" },
|
|
1825
|
+
credentials: "same-origin",
|
|
1826
|
+
body: JSON.stringify(patch)
|
|
1827
|
+
}).then(async (response) => {
|
|
1828
|
+
let body = null;
|
|
1829
|
+
try {
|
|
1830
|
+
body = await response.json();
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
body = null;
|
|
1833
|
+
}
|
|
1834
|
+
if (!response.ok || !body || body.ok !== true) throw new Error(body && body.error || `HTTP ${response.status}`);
|
|
1835
|
+
setQuickError("");
|
|
1836
|
+
remote.refresh();
|
|
1837
|
+
}).catch((error) => setQuickError(String(error && error.message || error)));
|
|
1838
|
+
}, [remote.refresh]);
|
|
1839
|
+
const onDecimalsChange = (value) => postConfig({ decimals: value });
|
|
1840
|
+
const onTurboChange = (checked) => postConfig({ turbo: Boolean(checked) });
|
|
1516
1841
|
const holidaySource = remote.data && remote.data.holidaySource || HOLIDAY_SOURCE;
|
|
1517
1842
|
const banner = remote.status === "failed" ? {
|
|
1518
1843
|
color: C.warn,
|
|
@@ -1615,7 +1940,7 @@ function NiumaPanel(props) {
|
|
|
1615
1940
|
}
|
|
1616
1941
|
}, t("retry")) : null
|
|
1617
1942
|
]) : null,
|
|
1618
|
-
import_react.default.createElement(HeroCard, { key: "hero", t, snap }),
|
|
1943
|
+
import_react.default.createElement(HeroCard, { key: "hero", t, snap, decimals: cfg.decimals, onDecimalsChange, turbo: cfg.turbo, onTurboChange }),
|
|
1619
1944
|
import_react.default.createElement(CountdownRow, { key: "countdown", t, snap }),
|
|
1620
1945
|
import_react.default.createElement("div", {
|
|
1621
1946
|
key: "ratesHead",
|