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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-niuma",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "牛马加速器 — DSH 工资换算实时计数器:按实收月薪、上下班时间、午休(可设是否算工时)、法定节假日自动折算秒/分/时/日/周/月工资,工资每秒滚动增长。Community salary ticker plugin for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -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 (CNY)',
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: '100%',
859
- boxSizing: 'border-box',
860
- padding: '6px 9px',
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 labelStyle = { fontSize: 10.5, color: C.label3, fontWeight: 500 };
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 textField = (field, labelKey, placeholder) => React.createElement('label', {
872
- key: field,
873
- style: { display: 'flex', flexDirection: 'column', gap: 4, flex: '1 1 130px', minWidth: 112 },
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
- React.createElement('span', { key: 'l', style: labelStyle }, t(labelKey)),
876
- React.createElement('input', {
877
- key: 'i',
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 checkField = (field, labelKey) => React.createElement('label', {
933
+ const toggle = (field, labelKey, disabled) => React.createElement('label', {
887
934
  key: field,
888
935
  style: {
889
- display: 'flex', alignItems: 'center', gap: 6, flex: '1 1 140px',
890
- fontSize: 12, color: C.label2, cursor: 'pointer', minHeight: 30,
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
- style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: '11px 13px' },
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: 13, fontWeight: 650, color: C.label, userSelect: 'none' },
959
+ style: { cursor: 'pointer', fontSize: 12.5, fontWeight: 650, color: C.label, userSelect: 'none' },
908
960
  }, t('settings')),
909
- React.createElement('div', {
910
- key: 'hint',
911
- style: { fontSize: 10.5, color: C.label3, margin: '5px 0 10px' },
912
- }, t('settingsHint')),
913
- React.createElement('div', {
914
- key: 'dataNote',
915
- style: { fontSize: 10.5, color: C.label3, margin: '-4px 0 10px', lineHeight: 1.55 },
916
- }, t('settingsDataNote')),
917
- React.createElement('div', {
918
- key: 'fields',
919
- style: { display: 'flex', flexWrap: 'wrap', gap: 9 },
920
- }, [
921
- textField('monthlySalary', 'salaryField', '10000'),
922
- textField('workStart', 'workStartField', '09:00'),
923
- textField('workEnd', 'workEndField', '18:00'),
924
- textField('lunchStart', 'lunchStartField', '12:00'),
925
- textField('lunchEnd', 'lunchEndField', '13:00'),
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, flexWrap: 'wrap', marginTop: 11 },
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: '7px 20px',
947
- fontSize: 13,
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 10px color-mix(in srgb, ${GOLD_TINT} 36%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
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: 12, color: C.ok } }, save.message || t('saved'))
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: 12, color: C.error, flex: '1 1 220px', whiteSpace: 'pre-wrap' },
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,