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 +19 -0
- package/README.md +1 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +149 -57
- package/lib/client.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.js +108 -59
- package/src/core.mjs +13 -0
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -116,19 +116,27 @@ const zh = {
|
|
|
116
116
|
'paidHoursPerDay': '日有效工时',
|
|
117
117
|
'todayTarget': '今日目标',
|
|
118
118
|
'monthEarned': '本月已赚',
|
|
119
|
+
'monthWorkedDays': '本月已工作',
|
|
120
|
+
'monthRemainingDays': '剩余工作日',
|
|
119
121
|
'holidaySourceLink': '国务院办公厅 2026 年节假日安排',
|
|
120
122
|
'configLoading': '正在读取设置…',
|
|
121
123
|
'configFailed': '设置读取失败,使用默认值',
|
|
122
124
|
'retry': '重试',
|
|
123
125
|
'settings': '设置',
|
|
124
126
|
'settingsHint': '改完点「保存」生效;格式不对会在下方红字提示,不影响计时。',
|
|
125
|
-
'salaryField': '
|
|
127
|
+
'salaryField': '实收月薪',
|
|
128
|
+
'groupSalary': '工资',
|
|
129
|
+
'groupWorkTime': '工作时间',
|
|
130
|
+
'groupLunch': '午休',
|
|
131
|
+
'salaryUnit': '元 / 月',
|
|
126
132
|
'workStartField': '上班时间',
|
|
127
133
|
'workEndField': '下班时间',
|
|
128
134
|
'lunchEnabledField': '启用午休',
|
|
135
|
+
'lunchEnabledShort': '启用',
|
|
129
136
|
'lunchStartField': '午休开始',
|
|
130
137
|
'lunchEndField': '午休结束',
|
|
131
138
|
'lunchPaidField': '午休算工时',
|
|
139
|
+
'lunchPaidShort': '算工时',
|
|
132
140
|
'holidaysCardTitle': '节假日安排',
|
|
133
141
|
'holidays2026Label': '2026 年(官方)',
|
|
134
142
|
'holidays2027Label': '2027 年(预估)',
|
|
@@ -153,6 +161,7 @@ const zh = {
|
|
|
153
161
|
'saveFailed': '保存失败:',
|
|
154
162
|
'dayUnit': '天',
|
|
155
163
|
'hourUnit': '小时',
|
|
164
|
+
'minuteUnit': '分钟',
|
|
156
165
|
'viewLabel': '查看',
|
|
157
166
|
'viewCurrent': '本月',
|
|
158
167
|
'viewYear': '全年平均',
|
|
@@ -207,19 +216,27 @@ const en = {
|
|
|
207
216
|
'paidHoursPerDay': 'Paid hours / day',
|
|
208
217
|
'todayTarget': "Today's target",
|
|
209
218
|
'monthEarned': 'Earned this month',
|
|
219
|
+
'monthWorkedDays': 'Worked this month',
|
|
220
|
+
'monthRemainingDays': 'Workdays left',
|
|
210
221
|
'holidaySourceLink': 'State Council 2026 holiday schedule',
|
|
211
222
|
'configLoading': 'Loading settings…',
|
|
212
223
|
'configFailed': 'Failed to load settings, using defaults',
|
|
213
224
|
'retry': 'Retry',
|
|
214
225
|
'settings': 'Settings',
|
|
215
226
|
'settingsHint': 'Click Save to apply. Invalid input is shown in red below; the ticker keeps running.',
|
|
216
|
-
'salaryField': 'Net monthly salary
|
|
227
|
+
'salaryField': 'Net monthly salary',
|
|
228
|
+
'groupSalary': 'Salary',
|
|
229
|
+
'groupWorkTime': 'Working hours',
|
|
230
|
+
'groupLunch': 'Lunch break',
|
|
231
|
+
'salaryUnit': 'CNY / month',
|
|
217
232
|
'workStartField': 'Work start',
|
|
218
233
|
'workEndField': 'Work end',
|
|
219
234
|
'lunchEnabledField': 'Enable lunch break',
|
|
235
|
+
'lunchEnabledShort': 'On',
|
|
220
236
|
'lunchStartField': 'Lunch start',
|
|
221
237
|
'lunchEndField': 'Lunch end',
|
|
222
238
|
'lunchPaidField': 'Lunch is paid',
|
|
239
|
+
'lunchPaidShort': 'Paid',
|
|
223
240
|
'holidaysCardTitle': 'Holiday calendar',
|
|
224
241
|
'holidays2026Label': '2026 (official)',
|
|
225
242
|
'holidays2027Label': '2027 (estimated)',
|
|
@@ -244,6 +261,7 @@ const en = {
|
|
|
244
261
|
'saveFailed': 'Save failed: ',
|
|
245
262
|
'dayUnit': 'd',
|
|
246
263
|
'hourUnit': 'h',
|
|
264
|
+
'minuteUnit': 'min',
|
|
247
265
|
'viewLabel': 'View',
|
|
248
266
|
'viewCurrent': 'This month',
|
|
249
267
|
'viewYear': 'Year average',
|
|
@@ -695,6 +713,8 @@ function DetailsFooter(props) {
|
|
|
695
713
|
{ key: 'hours', label: t('paidHoursPerDay'), value: `${trimNumber(rates.paidHours, 2)}${t('hourUnit')}` },
|
|
696
714
|
{ key: 'target', label: t('todayTarget'), value: money(snap.todayTarget, 2) },
|
|
697
715
|
{ key: 'month', label: t('monthEarned'), value: money(snap.monthEarned, 2) },
|
|
716
|
+
{ key: 'worked', label: t('monthWorkedDays'), value: `${snap.monthWorkdaysDone}${t('dayUnit')}` },
|
|
717
|
+
{ key: 'left', label: t('monthRemainingDays'), value: `${snap.monthWorkdaysLeft}${t('dayUnit')}` },
|
|
698
718
|
];
|
|
699
719
|
return React.createElement('div', {
|
|
700
720
|
style: {
|
|
@@ -854,81 +874,111 @@ function SettingsSection(props) {
|
|
|
854
874
|
const save = props.save;
|
|
855
875
|
const onSave = props.onSave;
|
|
856
876
|
|
|
877
|
+
// E 方案:行式短控件 —— 标签固定左列,控件都按内容定宽(不拉满)
|
|
878
|
+
const rowStyle = {
|
|
879
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
880
|
+
minHeight: 33, padding: '2px 0', borderTop: `1px solid ${C.border1}`,
|
|
881
|
+
};
|
|
882
|
+
const rowKeyStyle = { width: 62, flex: '0 0 62px', fontSize: 11.5, color: C.label3 };
|
|
883
|
+
const rowValStyle = { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', minWidth: 0 };
|
|
857
884
|
const inputStyle = {
|
|
858
|
-
width: '
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
borderRadius: 8,
|
|
862
|
-
border: `1px solid ${C.border}`,
|
|
863
|
-
background: C.layer2,
|
|
864
|
-
color: C.label,
|
|
865
|
-
fontSize: 13,
|
|
866
|
-
fontVariantNumeric: 'tabular-nums',
|
|
867
|
-
outline: 'none',
|
|
885
|
+
boxSizing: 'border-box', width: 88, minHeight: 28, padding: '4px 8px',
|
|
886
|
+
borderRadius: 7, border: `1px solid ${C.border}`, background: C.layer2,
|
|
887
|
+
color: C.label, fontSize: 12.5, fontVariantNumeric: 'tabular-nums', outline: 'none',
|
|
868
888
|
};
|
|
869
|
-
const
|
|
889
|
+
const selectStyle = {
|
|
890
|
+
boxSizing: 'border-box', width: 48, minHeight: 28, padding: '4px 2px 4px 6px',
|
|
891
|
+
borderRadius: 7, border: `1px solid ${C.border}`, background: C.layer2,
|
|
892
|
+
color: C.label, fontSize: 12.5, fontVariantNumeric: 'tabular-nums', outline: 'none', cursor: 'pointer',
|
|
893
|
+
};
|
|
894
|
+
const unitStyle = { fontSize: 10.5, color: C.label3, whiteSpace: 'nowrap' };
|
|
895
|
+
const dashStyle = { fontSize: 11, color: C.label3, margin: '0 1px' };
|
|
896
|
+
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
|
897
|
+
const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
|
|
870
898
|
|
|
871
|
-
const
|
|
872
|
-
key:
|
|
873
|
-
|
|
899
|
+
const row = (key, label, values) => React.createElement('div', { key, style: rowStyle }, [
|
|
900
|
+
React.createElement('span', { key: 'k', style: rowKeyStyle }, label),
|
|
901
|
+
React.createElement('div', { key: 'v', style: rowValStyle }, values),
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
/** 24 小时制时间:小时/分钟两个下拉,各 46px(不受浏览器 locale 影响)。 */
|
|
905
|
+
const timeSelects = (field, label, disabled = false) => {
|
|
906
|
+
const [rawHour = '00', rawMinute = '00'] = String(form[field] || '00:00').split(':');
|
|
907
|
+
const hour = HOUR_OPTIONS.includes(rawHour) ? rawHour : '00';
|
|
908
|
+
const minute = MINUTE_OPTIONS.includes(rawMinute) ? rawMinute : '00';
|
|
909
|
+
const commit = (nextHour, nextMinute) => setField(field, `${nextHour}:${nextMinute}`);
|
|
910
|
+
const style = { ...selectStyle, opacity: disabled ? 0.45 : 1, cursor: disabled ? 'default' : 'pointer' };
|
|
911
|
+
return React.createElement('span', { key: field, style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, [
|
|
912
|
+
React.createElement('select', {
|
|
913
|
+
key: 'h', value: hour, disabled, 'aria-label': `${label} ${t('hourUnit')}`,
|
|
914
|
+
onChange: (event) => commit(event.target.value, minute), style,
|
|
915
|
+
}, HOUR_OPTIONS.map((value) => React.createElement('option', { key: value, value }, value))),
|
|
916
|
+
React.createElement('span', { key: 'c', style: { color: C.label3, fontWeight: 700, fontSize: 12 } }, ':'),
|
|
917
|
+
React.createElement('select', {
|
|
918
|
+
key: 'm', value: minute, disabled, 'aria-label': `${label} ${t('minuteUnit')}`,
|
|
919
|
+
onChange: (event) => commit(hour, event.target.value), style,
|
|
920
|
+
}, MINUTE_OPTIONS.map((value) => React.createElement('option', { key: value, value }, value))),
|
|
921
|
+
]);
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const timeRange = (startField, endField, label, disabled) => React.createElement('span', {
|
|
925
|
+
key: `${startField}-${endField}`,
|
|
926
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 2 },
|
|
874
927
|
}, [
|
|
875
|
-
|
|
876
|
-
React.createElement('
|
|
877
|
-
|
|
878
|
-
type: 'text',
|
|
879
|
-
value: form[field],
|
|
880
|
-
placeholder: placeholder || 'HH:mm',
|
|
881
|
-
onChange: (event) => setField(field, event.target.value),
|
|
882
|
-
style: inputStyle,
|
|
883
|
-
}),
|
|
928
|
+
timeSelects(startField, label, disabled),
|
|
929
|
+
React.createElement('span', { key: 'dash', style: dashStyle }, '–'),
|
|
930
|
+
timeSelects(endField, label, disabled),
|
|
884
931
|
]);
|
|
885
932
|
|
|
886
|
-
const
|
|
933
|
+
const toggle = (field, labelKey, disabled) => React.createElement('label', {
|
|
887
934
|
key: field,
|
|
888
935
|
style: {
|
|
889
|
-
display: 'flex', alignItems: 'center', gap:
|
|
890
|
-
|
|
936
|
+
display: 'inline-flex', alignItems: 'center', gap: 5,
|
|
937
|
+
padding: '2px 6px', borderRadius: 8,
|
|
938
|
+
border: `1px solid ${C.border1}`, background: C.layer2,
|
|
939
|
+
fontSize: 10.5, color: disabled ? C.label3 : C.label2, whiteSpace: 'nowrap',
|
|
940
|
+
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.55 : 1,
|
|
891
941
|
},
|
|
892
942
|
}, [
|
|
893
943
|
React.createElement('input', {
|
|
894
|
-
key: 'i',
|
|
895
|
-
type: 'checkbox',
|
|
896
|
-
checked: Boolean(form[field]),
|
|
944
|
+
key: 'i', type: 'checkbox', checked: Boolean(form[field]), disabled,
|
|
897
945
|
onChange: (event) => setCheck(field, event.target.checked),
|
|
946
|
+
style: { accentColor: GOLD_TINT, width: 13, height: 13, margin: 0, cursor: 'inherit' },
|
|
898
947
|
}),
|
|
899
948
|
React.createElement('span', { key: 'l' }, t(labelKey)),
|
|
900
949
|
]);
|
|
901
950
|
|
|
951
|
+
const lunchDisabled = !form.lunchEnabled;
|
|
952
|
+
|
|
902
953
|
return React.createElement('details', {
|
|
903
|
-
|
|
954
|
+
open: true,
|
|
955
|
+
style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: '10px 12px' },
|
|
904
956
|
}, [
|
|
905
957
|
React.createElement('summary', {
|
|
906
958
|
key: 'sum',
|
|
907
|
-
style: { cursor: 'pointer', fontSize:
|
|
959
|
+
style: { cursor: 'pointer', fontSize: 12.5, fontWeight: 650, color: C.label, userSelect: 'none' },
|
|
908
960
|
}, t('settings')),
|
|
909
|
-
React.createElement('div', {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
checkField('lunchEnabled', 'lunchEnabledField'),
|
|
927
|
-
checkField('lunchPaid', 'lunchPaidField'),
|
|
961
|
+
React.createElement('div', { key: 'body', style: { marginTop: 7 } }, [
|
|
962
|
+
row('salary', t('salaryField'), [
|
|
963
|
+
React.createElement('input', {
|
|
964
|
+
key: 'i', type: 'text', inputMode: 'decimal', value: form.monthlySalary, placeholder: '10000',
|
|
965
|
+
onChange: (event) => setField('monthlySalary', event.target.value), style: inputStyle,
|
|
966
|
+
}),
|
|
967
|
+
React.createElement('span', { key: 'u', style: unitStyle }, t('salaryUnit')),
|
|
968
|
+
]),
|
|
969
|
+
row('worktime', t('groupWorkTime'), [
|
|
970
|
+
timeRange('workStart', 'workEnd', t('groupWorkTime'), false),
|
|
971
|
+
]),
|
|
972
|
+
row('lunch', t('groupLunch'), [
|
|
973
|
+
// 时间在前、两个开关在后(放到行尾)
|
|
974
|
+
timeRange('lunchStart', 'lunchEnd', t('groupLunch'), lunchDisabled),
|
|
975
|
+
toggle('lunchEnabled', 'lunchEnabledShort', false),
|
|
976
|
+
toggle('lunchPaid', 'lunchPaidShort', lunchDisabled),
|
|
977
|
+
]),
|
|
928
978
|
]),
|
|
929
979
|
React.createElement('div', {
|
|
930
980
|
key: 'actions',
|
|
931
|
-
style: { display: 'flex', alignItems: 'center', gap: 10,
|
|
981
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 7, paddingTop: 7, borderTop: `1px solid ${C.border1}` },
|
|
932
982
|
}, [
|
|
933
983
|
React.createElement('button', {
|
|
934
984
|
key: 'save',
|
|
@@ -936,30 +986,29 @@ function SettingsSection(props) {
|
|
|
936
986
|
onClick: onSave,
|
|
937
987
|
disabled: save.status === 'saving',
|
|
938
988
|
style: {
|
|
939
|
-
// 金色按钮:与主卡金额、节假日强调同一套金色系;深棕字保证任何主题下都有对比度
|
|
940
989
|
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 72%, rgba(0,0,0,.18))`,
|
|
941
990
|
color: '#3A2A05',
|
|
942
991
|
background: save.status === 'saving'
|
|
943
992
|
? `color-mix(in srgb, ${GOLD_TINT} 52%, #fff)`
|
|
944
993
|
: 'linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)',
|
|
945
994
|
borderRadius: 8,
|
|
946
|
-
padding: '
|
|
947
|
-
fontSize:
|
|
995
|
+
padding: '5px 16px',
|
|
996
|
+
fontSize: 12.5,
|
|
948
997
|
fontWeight: 700,
|
|
949
998
|
letterSpacing: '.3px',
|
|
950
999
|
cursor: save.status === 'saving' ? 'default' : 'pointer',
|
|
951
1000
|
opacity: save.status === 'saving' ? 0.72 : 1,
|
|
952
|
-
boxShadow: `0 2px
|
|
1001
|
+
boxShadow: `0 2px 9px color-mix(in srgb, ${GOLD_TINT} 34%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
|
|
953
1002
|
textShadow: '0 1px 0 rgba(255,255,255,.28)',
|
|
954
1003
|
},
|
|
955
1004
|
}, save.status === 'saving' ? t('saving') : t('save')),
|
|
956
1005
|
save.status === 'ok'
|
|
957
|
-
? React.createElement('span', { key: 'ok', style: { fontSize:
|
|
1006
|
+
? React.createElement('span', { key: 'ok', style: { fontSize: 11.5, color: C.ok } }, save.message || t('saved'))
|
|
958
1007
|
: null,
|
|
959
1008
|
save.status === 'error'
|
|
960
1009
|
? React.createElement('span', {
|
|
961
1010
|
key: 'err',
|
|
962
|
-
style: { fontSize:
|
|
1011
|
+
style: { fontSize: 11.5, color: C.error, flex: '1 1 200px', whiteSpace: 'pre-wrap' },
|
|
963
1012
|
}, `${t('saveFailed')}${save.errors.join(';')}`)
|
|
964
1013
|
: null,
|
|
965
1014
|
]),
|
package/src/core.mjs
CHANGED
|
@@ -342,6 +342,16 @@ export function earnedToday(config, now) {
|
|
|
342
342
|
const secondsToStart = workday && nowSec < startSec ? startSec - nowSec : 0;
|
|
343
343
|
const secondsToEnd = workday && nowSec < endSec ? endSec - nowSec : 0;
|
|
344
344
|
|
|
345
|
+
// 本月工作天数进度:已完成 = 今天之前的完整工作日 +(今天已下班则 +1);
|
|
346
|
+
// 「今天」在上班/午休中时算剩余(还没干完),这样「已工作 + 剩余 = 本月总工作日」。
|
|
347
|
+
const monthWorkdays = rates.monthlyWorkdays;
|
|
348
|
+
let monthWorkdaysDone = 0;
|
|
349
|
+
for (let day = 1; day < date.getDate(); day += 1) {
|
|
350
|
+
if (isWorkday(dateKeyOf(new Date(date.getFullYear(), date.getMonth(), day)), config)) monthWorkdaysDone += 1;
|
|
351
|
+
}
|
|
352
|
+
if (workday && status === 'off') monthWorkdaysDone += 1;
|
|
353
|
+
const monthWorkdaysLeft = Math.max(0, monthWorkdays - monthWorkdaysDone);
|
|
354
|
+
|
|
345
355
|
return {
|
|
346
356
|
dateKey: key,
|
|
347
357
|
isWorkday: workday,
|
|
@@ -352,6 +362,9 @@ export function earnedToday(config, now) {
|
|
|
352
362
|
todayTarget,
|
|
353
363
|
todayProgress,
|
|
354
364
|
monthEarned,
|
|
365
|
+
monthWorkdays,
|
|
366
|
+
monthWorkdaysDone,
|
|
367
|
+
monthWorkdaysLeft,
|
|
355
368
|
secondsToStart,
|
|
356
369
|
secondsToEnd,
|
|
357
370
|
rates,
|