dsh-niuma 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  本文件记录用户可感知的变化。版本号遵循语义化版本。
4
4
 
5
+ ## 0.2.1 — 2026-10-21
6
+
7
+ ### 新增:本月工作天数进度
8
+
9
+ - 「今日口径明细」新增 **本月已工作 / 剩余工作日** 两项:已工作 + 剩余 = 当月总工作日
10
+ - 口径:已完成 = 今天之前的完整工作日;今天若已下班算 1 天,上班/午休中算剩余;
11
+ 周末与节假日不算工作日(与工资口径完全一致)
12
+ - 例:2026-10-21 下午上班中 → 已工作 10 天 / 剩余 8 天;18:00 下班后 → 11 / 7
13
+ - core 的 `earnedToday()` 新增 `monthWorkdays / monthWorkdaysDone / monthWorkdaysLeft` 三个字段
14
+ - **设置区默认展开**:进面板即可改工资/作息,不用先点开「设置」(手动收起后仍然保持收起)
15
+ - **时间选择器固定 24 小时制**:上班/下班/午休起止改成「小时 : 分钟」两个原生下拉(00-23 / 00-59),
16
+ 鼠标点选即可;不用 `<input type="time">` 是因为它的展示跟随浏览器 locale(英文环境会变 12 小时制 AM/PM)
17
+ - 去掉设置区两行说明文案(「改完点保存生效…」「节假日数据见上方卡片…」)
18
+ - **设置区排版(E 方案)**:行式短控件——左侧固定 62px 标签(实收月薪 / 工作时间 / 午休),
19
+ 控件全部按内容定宽:月薪输入 88px、小时/分钟下拉各 40px、勾选为内联 chip;行间细分隔线,
20
+ 保存按钮移到左侧。「午休」行顺序为 **时间 → 启用 → 算工时**(开关移到行尾),窄面板也能一行放下
21
+ - 时间下拉加宽到 48px(之前 40px 会把末位数字切掉)
22
+ - 5 项自检全绿(core 181 项)
23
+
5
24
  ## 0.2.0 — 2026-10-21
6
25
 
7
26
  界面重做(只改外观与信息层级,计算口径与配置完全不变)。
package/README.md CHANGED
@@ -33,6 +33,7 @@
33
33
  - 午休可设置起止,**可勾选午休是否算工时**;午休不算工时则午休期间金额冻结
34
34
  - 状态徽标:休息日 / 未上班 / 搬砖中 / 午休中 / 已下班
35
35
  - 今日进度条、距上班/距下班倒计时
36
+ - **本月已工作 / 剩余工作日**(与工资口径一致:节假日、周末、调休都算进去)
36
37
 
37
38
  ### 口径公式
38
39
 
Binary file
package/lib/client.js CHANGED
@@ -384,6 +384,13 @@ function earnedToday(config, now) {
384
384
  const monthEarned = earnedThisMonth(config, key, date, todayEarned, rates);
385
385
  const secondsToStart = workday && nowSec < startSec ? startSec - nowSec : 0;
386
386
  const secondsToEnd = workday && nowSec < endSec ? endSec - nowSec : 0;
387
+ const monthWorkdays = rates.monthlyWorkdays;
388
+ let monthWorkdaysDone = 0;
389
+ for (let day = 1; day < date.getDate(); day += 1) {
390
+ if (isWorkday(dateKeyOf(new Date(date.getFullYear(), date.getMonth(), day)), config)) monthWorkdaysDone += 1;
391
+ }
392
+ if (workday && status === "off") monthWorkdaysDone += 1;
393
+ const monthWorkdaysLeft = Math.max(0, monthWorkdays - monthWorkdaysDone);
387
394
  return {
388
395
  dateKey: key,
389
396
  isWorkday: workday,
@@ -394,6 +401,9 @@ function earnedToday(config, now) {
394
401
  todayTarget,
395
402
  todayProgress,
396
403
  monthEarned,
404
+ monthWorkdays,
405
+ monthWorkdaysDone,
406
+ monthWorkdaysLeft,
397
407
  secondsToStart,
398
408
  secondsToEnd,
399
409
  rates
@@ -571,19 +581,27 @@ var zh = {
571
581
  "paidHoursPerDay": "\u65E5\u6709\u6548\u5DE5\u65F6",
572
582
  "todayTarget": "\u4ECA\u65E5\u76EE\u6807",
573
583
  "monthEarned": "\u672C\u6708\u5DF2\u8D5A",
584
+ "monthWorkedDays": "\u672C\u6708\u5DF2\u5DE5\u4F5C",
585
+ "monthRemainingDays": "\u5269\u4F59\u5DE5\u4F5C\u65E5",
574
586
  "holidaySourceLink": "\u56FD\u52A1\u9662\u529E\u516C\u5385 2026 \u5E74\u8282\u5047\u65E5\u5B89\u6392",
575
587
  "configLoading": "\u6B63\u5728\u8BFB\u53D6\u8BBE\u7F6E\u2026",
576
588
  "configFailed": "\u8BBE\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C",
577
589
  "retry": "\u91CD\u8BD5",
578
590
  "settings": "\u8BBE\u7F6E",
579
591
  "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\uFF08\u5143/\u6708\uFF09",
592
+ "salaryField": "\u5B9E\u6536\u6708\u85AA",
593
+ "groupSalary": "\u5DE5\u8D44",
594
+ "groupWorkTime": "\u5DE5\u4F5C\u65F6\u95F4",
595
+ "groupLunch": "\u5348\u4F11",
596
+ "salaryUnit": "\u5143 / \u6708",
581
597
  "workStartField": "\u4E0A\u73ED\u65F6\u95F4",
582
598
  "workEndField": "\u4E0B\u73ED\u65F6\u95F4",
583
599
  "lunchEnabledField": "\u542F\u7528\u5348\u4F11",
600
+ "lunchEnabledShort": "\u542F\u7528",
584
601
  "lunchStartField": "\u5348\u4F11\u5F00\u59CB",
585
602
  "lunchEndField": "\u5348\u4F11\u7ED3\u675F",
586
603
  "lunchPaidField": "\u5348\u4F11\u7B97\u5DE5\u65F6",
604
+ "lunchPaidShort": "\u7B97\u5DE5\u65F6",
587
605
  "holidaysCardTitle": "\u8282\u5047\u65E5\u5B89\u6392",
588
606
  "holidays2026Label": "2026 \u5E74\uFF08\u5B98\u65B9\uFF09",
589
607
  "holidays2027Label": "2027 \u5E74\uFF08\u9884\u4F30\uFF09",
@@ -607,6 +625,7 @@ var zh = {
607
625
  "saveFailed": "\u4FDD\u5B58\u5931\u8D25\uFF1A",
608
626
  "dayUnit": "\u5929",
609
627
  "hourUnit": "\u5C0F\u65F6",
628
+ "minuteUnit": "\u5206\u949F",
610
629
  "viewLabel": "\u67E5\u770B",
611
630
  "viewCurrent": "\u672C\u6708",
612
631
  "viewYear": "\u5168\u5E74\u5E73\u5747",
@@ -660,19 +679,27 @@ var en = {
660
679
  "paidHoursPerDay": "Paid hours / day",
661
680
  "todayTarget": "Today's target",
662
681
  "monthEarned": "Earned this month",
682
+ "monthWorkedDays": "Worked this month",
683
+ "monthRemainingDays": "Workdays left",
663
684
  "holidaySourceLink": "State Council 2026 holiday schedule",
664
685
  "configLoading": "Loading settings\u2026",
665
686
  "configFailed": "Failed to load settings, using defaults",
666
687
  "retry": "Retry",
667
688
  "settings": "Settings",
668
689
  "settingsHint": "Click Save to apply. Invalid input is shown in red below; the ticker keeps running.",
669
- "salaryField": "Net monthly salary (CNY)",
690
+ "salaryField": "Net monthly salary",
691
+ "groupSalary": "Salary",
692
+ "groupWorkTime": "Working hours",
693
+ "groupLunch": "Lunch break",
694
+ "salaryUnit": "CNY / month",
670
695
  "workStartField": "Work start",
671
696
  "workEndField": "Work end",
672
697
  "lunchEnabledField": "Enable lunch break",
698
+ "lunchEnabledShort": "On",
673
699
  "lunchStartField": "Lunch start",
674
700
  "lunchEndField": "Lunch end",
675
701
  "lunchPaidField": "Lunch is paid",
702
+ "lunchPaidShort": "Paid",
676
703
  "holidaysCardTitle": "Holiday calendar",
677
704
  "holidays2026Label": "2026 (official)",
678
705
  "holidays2027Label": "2027 (estimated)",
@@ -696,6 +723,7 @@ var en = {
696
723
  "saveFailed": "Save failed: ",
697
724
  "dayUnit": "d",
698
725
  "hourUnit": "h",
726
+ "minuteUnit": "min",
699
727
  "viewLabel": "View",
700
728
  "viewCurrent": "This month",
701
729
  "viewYear": "Year average",
@@ -1131,7 +1159,9 @@ function DetailsFooter(props) {
1131
1159
  { key: "workdays", label: t("workdaysThisMonth"), value: `${rates.monthlyWorkdays}${t("dayUnit")}` },
1132
1160
  { key: "hours", label: t("paidHoursPerDay"), value: `${trimNumber(rates.paidHours, 2)}${t("hourUnit")}` },
1133
1161
  { key: "target", label: t("todayTarget"), value: money(snap.todayTarget, 2) },
1134
- { key: "month", label: t("monthEarned"), value: money(snap.monthEarned, 2) }
1162
+ { key: "month", label: t("monthEarned"), value: money(snap.monthEarned, 2) },
1163
+ { key: "worked", label: t("monthWorkedDays"), value: `${snap.monthWorkdaysDone}${t("dayUnit")}` },
1164
+ { key: "left", label: t("monthRemainingDays"), value: `${snap.monthWorkdaysLeft}${t("dayUnit")}` }
1135
1165
  ];
1136
1166
  return import_react.default.createElement("div", {
1137
1167
  style: {
@@ -1302,84 +1332,147 @@ function SettingsSection(props) {
1302
1332
  const setCheck = props.setCheck;
1303
1333
  const save = props.save;
1304
1334
  const onSave = props.onSave;
1335
+ const rowStyle = {
1336
+ display: "flex",
1337
+ alignItems: "center",
1338
+ gap: 10,
1339
+ minHeight: 33,
1340
+ padding: "2px 0",
1341
+ borderTop: `1px solid ${C.border1}`
1342
+ };
1343
+ const rowKeyStyle = { width: 62, flex: "0 0 62px", fontSize: 11.5, color: C.label3 };
1344
+ const rowValStyle = { display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap", minWidth: 0 };
1305
1345
  const inputStyle = {
1306
- width: "100%",
1307
1346
  boxSizing: "border-box",
1308
- padding: "6px 9px",
1309
- borderRadius: 8,
1347
+ width: 88,
1348
+ minHeight: 28,
1349
+ padding: "4px 8px",
1350
+ borderRadius: 7,
1310
1351
  border: `1px solid ${C.border}`,
1311
1352
  background: C.layer2,
1312
1353
  color: C.label,
1313
- fontSize: 13,
1354
+ fontSize: 12.5,
1314
1355
  fontVariantNumeric: "tabular-nums",
1315
1356
  outline: "none"
1316
1357
  };
1317
- const labelStyle = { fontSize: 10.5, color: C.label3, fontWeight: 500 };
1318
- const textField = (field, labelKey, placeholder) => import_react.default.createElement("label", {
1319
- key: field,
1320
- style: { display: "flex", flexDirection: "column", gap: 4, flex: "1 1 130px", minWidth: 112 }
1358
+ const selectStyle = {
1359
+ boxSizing: "border-box",
1360
+ width: 48,
1361
+ minHeight: 28,
1362
+ padding: "4px 2px 4px 6px",
1363
+ borderRadius: 7,
1364
+ border: `1px solid ${C.border}`,
1365
+ background: C.layer2,
1366
+ color: C.label,
1367
+ fontSize: 12.5,
1368
+ fontVariantNumeric: "tabular-nums",
1369
+ outline: "none",
1370
+ cursor: "pointer"
1371
+ };
1372
+ const unitStyle = { fontSize: 10.5, color: C.label3, whiteSpace: "nowrap" };
1373
+ const dashStyle = { fontSize: 11, color: C.label3, margin: "0 1px" };
1374
+ const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"));
1375
+ const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"));
1376
+ const row = (key, label, values) => import_react.default.createElement("div", { key, style: rowStyle }, [
1377
+ import_react.default.createElement("span", { key: "k", style: rowKeyStyle }, label),
1378
+ import_react.default.createElement("div", { key: "v", style: rowValStyle }, values)
1379
+ ]);
1380
+ const timeSelects = (field, label, disabled = false) => {
1381
+ const [rawHour = "00", rawMinute = "00"] = String(form[field] || "00:00").split(":");
1382
+ const hour = HOUR_OPTIONS.includes(rawHour) ? rawHour : "00";
1383
+ const minute = MINUTE_OPTIONS.includes(rawMinute) ? rawMinute : "00";
1384
+ const commit = (nextHour, nextMinute) => setField(field, `${nextHour}:${nextMinute}`);
1385
+ const style = { ...selectStyle, opacity: disabled ? 0.45 : 1, cursor: disabled ? "default" : "pointer" };
1386
+ return import_react.default.createElement("span", { key: field, style: { display: "inline-flex", alignItems: "center", gap: 2 } }, [
1387
+ import_react.default.createElement("select", {
1388
+ key: "h",
1389
+ value: hour,
1390
+ disabled,
1391
+ "aria-label": `${label} ${t("hourUnit")}`,
1392
+ onChange: (event) => commit(event.target.value, minute),
1393
+ style
1394
+ }, HOUR_OPTIONS.map((value) => import_react.default.createElement("option", { key: value, value }, value))),
1395
+ import_react.default.createElement("span", { key: "c", style: { color: C.label3, fontWeight: 700, fontSize: 12 } }, ":"),
1396
+ import_react.default.createElement("select", {
1397
+ key: "m",
1398
+ value: minute,
1399
+ disabled,
1400
+ "aria-label": `${label} ${t("minuteUnit")}`,
1401
+ onChange: (event) => commit(hour, event.target.value),
1402
+ style
1403
+ }, MINUTE_OPTIONS.map((value) => import_react.default.createElement("option", { key: value, value }, value)))
1404
+ ]);
1405
+ };
1406
+ const timeRange = (startField, endField, label, disabled) => import_react.default.createElement("span", {
1407
+ key: `${startField}-${endField}`,
1408
+ style: { display: "inline-flex", alignItems: "center", gap: 2 }
1321
1409
  }, [
1322
- import_react.default.createElement("span", { key: "l", style: labelStyle }, t(labelKey)),
1323
- import_react.default.createElement("input", {
1324
- key: "i",
1325
- type: "text",
1326
- value: form[field],
1327
- placeholder: placeholder || "HH:mm",
1328
- onChange: (event) => setField(field, event.target.value),
1329
- style: inputStyle
1330
- })
1410
+ timeSelects(startField, label, disabled),
1411
+ import_react.default.createElement("span", { key: "dash", style: dashStyle }, "\u2013"),
1412
+ timeSelects(endField, label, disabled)
1331
1413
  ]);
1332
- const checkField = (field, labelKey) => import_react.default.createElement("label", {
1414
+ const toggle = (field, labelKey, disabled) => import_react.default.createElement("label", {
1333
1415
  key: field,
1334
1416
  style: {
1335
- display: "flex",
1417
+ display: "inline-flex",
1336
1418
  alignItems: "center",
1337
- gap: 6,
1338
- flex: "1 1 140px",
1339
- fontSize: 12,
1340
- color: C.label2,
1341
- cursor: "pointer",
1342
- minHeight: 30
1419
+ gap: 5,
1420
+ padding: "2px 6px",
1421
+ borderRadius: 8,
1422
+ border: `1px solid ${C.border1}`,
1423
+ background: C.layer2,
1424
+ fontSize: 10.5,
1425
+ color: disabled ? C.label3 : C.label2,
1426
+ whiteSpace: "nowrap",
1427
+ cursor: disabled ? "default" : "pointer",
1428
+ opacity: disabled ? 0.55 : 1
1343
1429
  }
1344
1430
  }, [
1345
1431
  import_react.default.createElement("input", {
1346
1432
  key: "i",
1347
1433
  type: "checkbox",
1348
1434
  checked: Boolean(form[field]),
1349
- onChange: (event) => setCheck(field, event.target.checked)
1435
+ disabled,
1436
+ onChange: (event) => setCheck(field, event.target.checked),
1437
+ style: { accentColor: GOLD_TINT, width: 13, height: 13, margin: 0, cursor: "inherit" }
1350
1438
  }),
1351
1439
  import_react.default.createElement("span", { key: "l" }, t(labelKey))
1352
1440
  ]);
1441
+ const lunchDisabled = !form.lunchEnabled;
1353
1442
  return import_react.default.createElement("details", {
1354
- style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: "11px 13px" }
1443
+ open: true,
1444
+ style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: "10px 12px" }
1355
1445
  }, [
1356
1446
  import_react.default.createElement("summary", {
1357
1447
  key: "sum",
1358
- style: { cursor: "pointer", fontSize: 13, fontWeight: 650, color: C.label, userSelect: "none" }
1448
+ style: { cursor: "pointer", fontSize: 12.5, fontWeight: 650, color: C.label, userSelect: "none" }
1359
1449
  }, t("settings")),
1360
- import_react.default.createElement("div", {
1361
- key: "hint",
1362
- style: { fontSize: 10.5, color: C.label3, margin: "5px 0 10px" }
1363
- }, t("settingsHint")),
1364
- import_react.default.createElement("div", {
1365
- key: "dataNote",
1366
- style: { fontSize: 10.5, color: C.label3, margin: "-4px 0 10px", lineHeight: 1.55 }
1367
- }, t("settingsDataNote")),
1368
- import_react.default.createElement("div", {
1369
- key: "fields",
1370
- style: { display: "flex", flexWrap: "wrap", gap: 9 }
1371
- }, [
1372
- textField("monthlySalary", "salaryField", "10000"),
1373
- textField("workStart", "workStartField", "09:00"),
1374
- textField("workEnd", "workEndField", "18:00"),
1375
- textField("lunchStart", "lunchStartField", "12:00"),
1376
- textField("lunchEnd", "lunchEndField", "13:00"),
1377
- checkField("lunchEnabled", "lunchEnabledField"),
1378
- checkField("lunchPaid", "lunchPaidField")
1450
+ import_react.default.createElement("div", { key: "body", style: { marginTop: 7 } }, [
1451
+ row("salary", t("salaryField"), [
1452
+ import_react.default.createElement("input", {
1453
+ key: "i",
1454
+ type: "text",
1455
+ inputMode: "decimal",
1456
+ value: form.monthlySalary,
1457
+ placeholder: "10000",
1458
+ onChange: (event) => setField("monthlySalary", event.target.value),
1459
+ style: inputStyle
1460
+ }),
1461
+ import_react.default.createElement("span", { key: "u", style: unitStyle }, t("salaryUnit"))
1462
+ ]),
1463
+ row("worktime", t("groupWorkTime"), [
1464
+ timeRange("workStart", "workEnd", t("groupWorkTime"), false)
1465
+ ]),
1466
+ row("lunch", t("groupLunch"), [
1467
+ // 时间在前、两个开关在后(放到行尾)
1468
+ timeRange("lunchStart", "lunchEnd", t("groupLunch"), lunchDisabled),
1469
+ toggle("lunchEnabled", "lunchEnabledShort", false),
1470
+ toggle("lunchPaid", "lunchPaidShort", lunchDisabled)
1471
+ ])
1379
1472
  ]),
1380
1473
  import_react.default.createElement("div", {
1381
1474
  key: "actions",
1382
- style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginTop: 11 }
1475
+ style: { display: "flex", alignItems: "center", gap: 10, marginTop: 7, paddingTop: 7, borderTop: `1px solid ${C.border1}` }
1383
1476
  }, [
1384
1477
  import_react.default.createElement("button", {
1385
1478
  key: "save",
@@ -1387,25 +1480,24 @@ function SettingsSection(props) {
1387
1480
  onClick: onSave,
1388
1481
  disabled: save.status === "saving",
1389
1482
  style: {
1390
- // 金色按钮:与主卡金额、节假日强调同一套金色系;深棕字保证任何主题下都有对比度
1391
1483
  border: `1px solid color-mix(in srgb, ${GOLD_TINT} 72%, rgba(0,0,0,.18))`,
1392
1484
  color: "#3A2A05",
1393
1485
  background: save.status === "saving" ? `color-mix(in srgb, ${GOLD_TINT} 52%, #fff)` : "linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)",
1394
1486
  borderRadius: 8,
1395
- padding: "7px 20px",
1396
- fontSize: 13,
1487
+ padding: "5px 16px",
1488
+ fontSize: 12.5,
1397
1489
  fontWeight: 700,
1398
1490
  letterSpacing: ".3px",
1399
1491
  cursor: save.status === "saving" ? "default" : "pointer",
1400
1492
  opacity: save.status === "saving" ? 0.72 : 1,
1401
- boxShadow: `0 2px 10px color-mix(in srgb, ${GOLD_TINT} 36%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
1493
+ boxShadow: `0 2px 9px color-mix(in srgb, ${GOLD_TINT} 34%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
1402
1494
  textShadow: "0 1px 0 rgba(255,255,255,.28)"
1403
1495
  }
1404
1496
  }, save.status === "saving" ? t("saving") : t("save")),
1405
- save.status === "ok" ? import_react.default.createElement("span", { key: "ok", style: { fontSize: 12, color: C.ok } }, save.message || t("saved")) : null,
1497
+ save.status === "ok" ? import_react.default.createElement("span", { key: "ok", style: { fontSize: 11.5, color: C.ok } }, save.message || t("saved")) : null,
1406
1498
  save.status === "error" ? import_react.default.createElement("span", {
1407
1499
  key: "err",
1408
- style: { fontSize: 12, color: C.error, flex: "1 1 220px", whiteSpace: "pre-wrap" }
1500
+ style: { fontSize: 11.5, color: C.error, flex: "1 1 200px", whiteSpace: "pre-wrap" }
1409
1501
  }, `${t("saveFailed")}${save.errors.join("\uFF1B")}`) : null
1410
1502
  ])
1411
1503
  ]);