dsh-niuma 0.1.1 → 0.2.0

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.
@@ -5,9 +5,16 @@ import {
5
5
  earnedToday,
6
6
  formatDuration,
7
7
  formatMoney,
8
+ monthSummary,
8
9
  normalizeConfig,
10
+ yearSummary,
9
11
  } from '../core.mjs';
10
- import { HOLIDAY_SOURCE } from '../holidays.mjs';
12
+ import {
13
+ HOLIDAY_BLOCKS,
14
+ HOLIDAY_BLOCKS_2027_ESTIMATED,
15
+ HOLIDAY_ESTIMATE_2027,
16
+ HOLIDAY_SOURCE,
17
+ } from '../holidays.mjs';
11
18
 
12
19
  /**
13
20
  * dsh-niuma 浏览器半侧:DSH 侧栏「牛马」入口 + 中栏工资实时计数器面板。
@@ -58,6 +65,17 @@ const CARD = {
58
65
  /** 数字:等宽数位 + 不换行(每秒跳动时宽度不抖)。 */
59
66
  const NUM = { fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' };
60
67
 
68
+ /** 金色金额:渐变金属质感 + 单色回退(不支持 background-clip:text 时显示金色)。 */
69
+ const GOLD = {
70
+ color: '#D9A441',
71
+ backgroundImage: 'linear-gradient(180deg, #FFE9A8 0%, #F2C14E 40%, #D79A2B 72%, #B87A16 100%)',
72
+ WebkitBackgroundClip: 'text',
73
+ backgroundClip: 'text',
74
+ WebkitTextFillColor: 'transparent',
75
+ textShadow: '0 1px 0 rgba(0,0,0,.05)',
76
+ };
77
+ const GOLD_TINT = '#E7B84D';
78
+
61
79
  /** 状态徽标配色:颜色与语义一致,深浅色都靠主题变量。 */
62
80
  const STATUS_META = {
63
81
  rest: { key: 'statusRest', color: C.label3 },
@@ -98,7 +116,6 @@ const zh = {
98
116
  'paidHoursPerDay': '日有效工时',
99
117
  'todayTarget': '今日目标',
100
118
  'monthEarned': '本月已赚',
101
- 'holidaySource': '节假日来源',
102
119
  'holidaySourceLink': '国务院办公厅 2026 年节假日安排',
103
120
  'configLoading': '正在读取设置…',
104
121
  'configFailed': '设置读取失败,使用默认值',
@@ -112,14 +129,54 @@ const zh = {
112
129
  'lunchStartField': '午休开始',
113
130
  'lunchEndField': '午休结束',
114
131
  'lunchPaidField': '午休算工时',
115
- 'holidaysField': '节假日(一行一个 YYYY-MM-DD)',
116
- 'extraWorkdaysField': '调休上班日(一行一个 YYYY-MM-DD)',
132
+ 'holidaysCardTitle': '节假日安排',
133
+ 'holidays2026Label': '2026 年(官方)',
134
+ 'holidays2027Label': '2027 年(预估)',
135
+ 'holidayEstimateTag': '预估',
136
+ 'holidayEstimateNote': '官方尚未公布,以上为按规则与惯例推算,仅供参考,以国务院通知为准。',
137
+ 'holidayEstimateShort': '2027 为预估值',
138
+ 'settingsDataNote': '节假日与调休数据见上方「节假日安排」卡片(2027 年为预估值,不可在本页编辑)。',
139
+
140
+ 'holidaysCardSub': '法定假日 {days} 天 · 调休上班 {extra} 天',
141
+ 'holidaysDaysShort': '{days} 天',
142
+ 'holidaysExtraNote': '调休上班日 {extra} 天(周末补班,按工作日计)',
143
+ 'holidayNewYear': '元旦',
144
+ 'holidaySpring': '春节',
145
+ 'holidayQingming': '清明',
146
+ 'holidayLabour': '劳动节',
147
+ 'holidayDragon': '端午',
148
+ 'holidayMidAutumn': '中秋',
149
+ 'holidayNational': '国庆',
117
150
  'save': '保存',
118
151
  'saving': '保存中…',
119
152
  'saved': '已保存',
120
153
  'saveFailed': '保存失败:',
121
154
  'dayUnit': '天',
122
155
  'hourUnit': '小时',
156
+ 'viewLabel': '查看',
157
+ 'viewCurrent': '本月',
158
+ 'viewYear': '全年平均',
159
+ 'monthUnit': '月',
160
+ 'holidayShort': '假',
161
+ 'currentHint': '按每月 {days} 个工作日、每天 {hours} 小时计',
162
+ 'monthHint': '{year} 年 {month} 月 · 工作日 {workdays} 天 · 法定假日 {holidays} 天',
163
+ 'yearHint': '{year} 全年 · {workdays} 个工作日 · 按年收入折算',
164
+ 'viewOnlyHint': '仅换算,不影响今日计数器',
165
+ 'yearWorkdays': '全年工作日',
166
+ 'yearIncome': '全年收入',
167
+ 'averageDaily': '平均日薪',
168
+ 'averageHourly': '平均时薪',
169
+ 'averageMinute': '平均分薪',
170
+ 'averageSecond': '平均秒薪',
171
+ 'monthWorkdaysCard': '工作日',
172
+ 'maxHolidayBadge': '全年假日最多',
173
+ 'maxHolidayNote': '全年假日最多:{month} 月(法定假日 {days} 天)',
174
+ 'maxHolidayThis': '全年假日最多的月份(法定假日 {days} 天)',
175
+ 'sectionRates': '工资换算',
176
+ 'sectionRatesHint': '按每月 {days} 个工作日、每天 {hours} 小时计',
177
+ 'sectionDetail': '今日口径明细',
178
+ 'restHint': '今天休息,好好躺平',
179
+ 'liveNow': '实时',
123
180
  };
124
181
 
125
182
  const en = {
@@ -150,7 +207,6 @@ const en = {
150
207
  'paidHoursPerDay': 'Paid hours / day',
151
208
  'todayTarget': "Today's target",
152
209
  'monthEarned': 'Earned this month',
153
- 'holidaySource': 'Holiday data',
154
210
  'holidaySourceLink': 'State Council 2026 holiday schedule',
155
211
  'configLoading': 'Loading settings…',
156
212
  'configFailed': 'Failed to load settings, using defaults',
@@ -164,14 +220,54 @@ const en = {
164
220
  'lunchStartField': 'Lunch start',
165
221
  'lunchEndField': 'Lunch end',
166
222
  'lunchPaidField': 'Lunch is paid',
167
- 'holidaysField': 'Holidays (one YYYY-MM-DD per line)',
168
- 'extraWorkdaysField': 'Make-up workdays (one YYYY-MM-DD per line)',
223
+ 'holidaysCardTitle': 'Holiday calendar',
224
+ 'holidays2026Label': '2026 (official)',
225
+ 'holidays2027Label': '2027 (estimated)',
226
+ 'holidayEstimateTag': 'estimate',
227
+ 'holidayEstimateNote': 'Not announced officially yet — estimated from the rules and past practice, for reference only.',
228
+ 'holidayEstimateShort': '2027 is estimated',
229
+ 'settingsDataNote': 'Holiday data lives in the calendar card above (2027 values are estimates and not editable here).',
230
+
231
+ 'holidaysCardSub': '{days} statutory days · {extra} make-up workdays',
232
+ 'holidaysDaysShort': '{days} d',
233
+ 'holidaysExtraNote': '{extra} make-up workdays (weekend shifts, counted as workdays)',
234
+ 'holidayNewYear': "New Year's Day",
235
+ 'holidaySpring': 'Spring Festival',
236
+ 'holidayQingming': 'Qingming',
237
+ 'holidayLabour': 'Labour Day',
238
+ 'holidayDragon': 'Dragon Boat',
239
+ 'holidayMidAutumn': 'Mid-Autumn',
240
+ 'holidayNational': 'National Day',
169
241
  'save': 'Save',
170
242
  'saving': 'Saving…',
171
243
  'saved': 'Saved',
172
244
  'saveFailed': 'Save failed: ',
173
245
  'dayUnit': 'd',
174
246
  'hourUnit': 'h',
247
+ 'viewLabel': 'View',
248
+ 'viewCurrent': 'This month',
249
+ 'viewYear': 'Year average',
250
+ 'monthUnit': 'M',
251
+ 'holidayShort': 'hol.',
252
+ 'currentHint': '{days} workdays / month · {hours} h / day',
253
+ 'monthHint': '{year}-{month} · {workdays} workdays · {holidays} statutory holidays',
254
+ 'yearHint': '{year} · {workdays} workdays · averaged over the year',
255
+ 'viewOnlyHint': 'Conversion only — today\'s ticker is unaffected',
256
+ 'yearWorkdays': 'Workdays / year',
257
+ 'yearIncome': 'Annual income',
258
+ 'averageDaily': 'Avg per day',
259
+ 'averageHourly': 'Avg per hour',
260
+ 'averageMinute': 'Avg per minute',
261
+ 'averageSecond': 'Avg per second',
262
+ 'monthWorkdaysCard': 'Workdays',
263
+ 'maxHolidayBadge': 'Most holidays',
264
+ 'maxHolidayNote': 'Most holidays: month {month} ({days} statutory days)',
265
+ 'maxHolidayThis': 'The month with the most holidays ({days} statutory days)',
266
+ 'sectionRates': 'Pay rates',
267
+ 'sectionRatesHint': '{days} workdays / month · {hours} h / day',
268
+ 'sectionDetail': "Today's basis",
269
+ 'restHint': 'Day off — enjoy it',
270
+ 'liveNow': 'LIVE',
175
271
  };
176
272
 
177
273
  /** 组件在没有 locale 注入 t 时(测试 / 兜底)直接读中文表,绝不白屏。 */
@@ -180,7 +276,7 @@ const fallbackTranslate = (key) => (zh[key] != null ? zh[key] : key);
180
276
  /* ------------------------------------------------------------------ *
181
277
  * 格式化小工具
182
278
  * ------------------------------------------------------------------ */
183
- const money = (value, digits = 2) => `¥${formatMoney(value, digits)}`;
279
+ const money = (value, digits = 2) => `${formatMoney(value, digits)} 元`;
184
280
 
185
281
  /** 8.00 -> '8',8.50 -> '8.5'(工时展示用) */
186
282
  function trimNumber(value, digits = 2) {
@@ -188,13 +284,6 @@ function trimNumber(value, digits = 2) {
188
284
  return fixed.replace(/\.?0+$/, '') || '0';
189
285
  }
190
286
 
191
- function splitDateLines(text) {
192
- return String(text == null ? '' : text)
193
- .split(/\r?\n/)
194
- .map((line) => line.trim())
195
- .filter((line) => line.length > 0);
196
- }
197
-
198
287
  /** 配置 -> 设置表单草稿(所有输入框都是字符串 / 布尔,编辑中不打断计时)。 */
199
288
  function formFromConfig(config) {
200
289
  return {
@@ -205,8 +294,6 @@ function formFromConfig(config) {
205
294
  lunchStart: String(config.lunchStart == null ? '' : config.lunchStart),
206
295
  lunchEnd: String(config.lunchEnd == null ? '' : config.lunchEnd),
207
296
  lunchPaid: config.lunchPaid === true,
208
- holidaysText: (config.holidays || []).join('\n'),
209
- extraWorkdaysText: (config.extraWorkdays || []).join('\n'),
210
297
  };
211
298
  }
212
299
 
@@ -221,8 +308,6 @@ function patchFromForm(form) {
221
308
  lunchStart: String(form.lunchStart || '').trim(),
222
309
  lunchEnd: String(form.lunchEnd || '').trim(),
223
310
  lunchPaid: Boolean(form.lunchPaid),
224
- holidays: splitDateLines(form.holidaysText),
225
- extraWorkdays: splitDateLines(form.extraWorkdaysText),
226
311
  };
227
312
  }
228
313
 
@@ -309,137 +394,218 @@ function NiumaIcon(props) {
309
394
  ]);
310
395
  }
311
396
 
397
+ /** 全局小样式:只放动效 keyframes(内联 style 写不了 keyframes)。组件内联样式仍是主体。 */
398
+ const PANEL_CSS = `
399
+ @keyframes niumaPulse { 0%,100% { opacity: .95; transform: scale(1); } 50% { opacity: .3; transform: scale(.68); } }
400
+ @media (prefers-reduced-motion: reduce) { .niuma-live-dot { animation: none !important; } }
401
+ `;
402
+
403
+ function StyleTag() {
404
+ return React.createElement('style', null, PANEL_CSS);
405
+ }
406
+
312
407
  /** 状态徽标:休息日 / 未上班 / 搬砖中 / 午休中 / 已下班。 */
313
408
  function StatusBadge(props) {
314
409
  const t = props.t;
315
410
  const meta = STATUS_META[props.status] || STATUS_META.rest;
411
+ const live = props.status === 'working';
316
412
  return React.createElement('span', {
317
413
  style: {
318
414
  display: 'inline-flex',
319
415
  alignItems: 'center',
320
416
  gap: 6,
321
- padding: '3px 10px',
417
+ padding: '4px 11px 4px 9px',
322
418
  borderRadius: 999,
323
419
  fontSize: 12,
324
420
  fontWeight: 600,
325
421
  lineHeight: 1.5,
326
422
  color: meta.color,
327
- border: `1px solid ${meta.color}`,
328
- background: C.layer1,
423
+ border: `1px solid color-mix(in srgb, ${meta.color} 42%, transparent)`,
424
+ background: `color-mix(in srgb, ${meta.color} 12%, transparent)`,
329
425
  whiteSpace: 'nowrap',
330
426
  },
331
427
  }, [
332
428
  React.createElement('i', {
333
429
  key: 'dot',
430
+ className: live ? 'niuma-live-dot' : undefined,
334
431
  style: { width: 6, height: 6, borderRadius: '50%', background: meta.color, display: 'inline-block' },
335
432
  }),
336
433
  t(meta.key),
337
434
  ]);
338
435
  }
339
436
 
340
- /** 今日已赚:超大数字,每秒跳动。 */
437
+ /** 小节标题:左标题 + 右口径提示(同一行,窄屏自动换行)。 */
438
+ function SectionTitle(props) {
439
+ return React.createElement('div', {
440
+ style: {
441
+ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
442
+ gap: 8, flexWrap: 'wrap', margin: '2px 1px -1px',
443
+ },
444
+ }, [
445
+ React.createElement('span', {
446
+ key: 't',
447
+ style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' },
448
+ }, props.title),
449
+ props.hint
450
+ ? React.createElement('span', { key: 'h', style: { fontSize: 10.5, color: C.label3, ...NUM } }, props.hint)
451
+ : null,
452
+ ]);
453
+ }
454
+
455
+ /** 今日已赚主卡:渐变底 + 超大金额 + 进度条(视觉焦点只有这一处)。 */
341
456
  function HeroCard(props) {
342
457
  const t = props.t;
343
458
  const snap = props.snap;
344
- return React.createElement('div', { style: { ...CARD, padding: '13px 15px 12px' } }, [
459
+ const rates = snap.rates;
460
+ const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
461
+ const percentText = `${(progress * 100).toFixed(1)}%`;
462
+ const live = snap.status === 'working';
463
+ const rest = snap.status === 'rest';
464
+ const heroBackground = 'linear-gradient(135deg, '
465
+ + 'color-mix(in srgb, ' + C.brand + ' 17%, ' + C.layer1 + ') 0%, '
466
+ + C.layer1 + ' 58%, ' + C.layer1 + ' 100%)';
467
+ return React.createElement('div', {
468
+ style: {
469
+ position: 'relative',
470
+ overflow: 'hidden',
471
+ borderRadius: 18,
472
+ padding: '16px 18px 14px',
473
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 34%, ${C.border1})`,
474
+ background: heroBackground,
475
+ boxShadow: '0 12px 30px rgba(0,0,0,.10)',
476
+ boxSizing: 'border-box',
477
+ minWidth: 0,
478
+ },
479
+ }, [
345
480
  React.createElement('div', {
346
- key: 'head',
481
+ key: 'glow',
482
+ 'aria-hidden': 'true',
347
483
  style: {
348
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
349
- gap: 8, flexWrap: 'wrap', fontSize: 12, color: C.label2,
484
+ position: 'absolute', right: -52, top: -72, width: 188, height: 188, borderRadius: '50%',
485
+ background: `radial-gradient(circle, color-mix(in srgb, ${GOLD_TINT} 30%, transparent) 0%, transparent 68%)`,
486
+ pointerEvents: 'none',
350
487
  },
488
+ }),
489
+ React.createElement('div', {
490
+ key: 'head',
491
+ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
351
492
  }, [
352
- React.createElement('span', { key: 'a' }, t('earnedToday')),
493
+ React.createElement('span', {
494
+ key: 'a',
495
+ style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, fontWeight: 600, color: C.label2 },
496
+ }, [
497
+ live
498
+ ? React.createElement('i', {
499
+ key: 'dot',
500
+ className: 'niuma-live-dot',
501
+ style: { width: 7, height: 7, borderRadius: '50%', background: C.ok, display: 'inline-block' },
502
+ })
503
+ : null,
504
+ t('earnedToday'),
505
+ ]),
353
506
  React.createElement('span', { key: 'b', style: { fontSize: 11, color: C.label3, ...NUM } },
354
507
  `${t('todayOn')} ${snap.dateKey}`),
355
508
  ]),
356
509
  React.createElement('div', {
357
510
  key: 'amt',
358
- style: { display: 'flex', alignItems: 'baseline', gap: 6, marginTop: 1, flexWrap: 'wrap' },
511
+ style: { display: 'flex', alignItems: 'baseline', gap: 7, marginTop: 4, flexWrap: 'wrap' },
359
512
  }, [
360
- React.createElement('span', {
361
- key: 'cur',
362
- style: { fontSize: 22, fontWeight: 700, color: C.label2, ...NUM },
363
- }, '¥'),
364
513
  React.createElement('span', {
365
514
  key: 'val',
366
515
  style: {
367
- fontSize: 40, lineHeight: 1.15, fontWeight: 760, letterSpacing: '-.5px',
368
- color: C.ok, ...NUM,
516
+ fontSize: 46, lineHeight: 1.12, fontWeight: 800, letterSpacing: '-1.2px',
517
+ ...GOLD,
518
+ ...NUM,
519
+ ...(rest ? { opacity: 0.62 } : {}),
369
520
  },
370
521
  }, formatMoney(snap.todayEarned, 4)),
522
+ React.createElement('span', {
523
+ key: 'unit',
524
+ style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM },
525
+ }, '元'),
371
526
  ]),
372
527
  React.createElement('div', {
373
- key: 'sub',
374
- style: { fontSize: 11, color: C.label3, marginTop: 2, ...NUM },
375
- }, `${t('perSecond')} ${money(snap.rates.secondWage, 4)}`),
376
- ]);
377
- }
378
-
379
- /** 今日进度条 + 距上班 / 距下班倒计时。 */
380
- function ProgressCard(props) {
381
- const t = props.t;
382
- const snap = props.snap;
383
- const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
384
- const percentText = `${(progress * 100).toFixed(1)}%`;
385
- const countdownChip = (key, label, seconds) => React.createElement('div', {
386
- key,
387
- style: { ...CARD, flex: '1 1 130px', minWidth: 118, padding: '8px 10px' },
388
- }, [
389
- React.createElement('div', { key: 'l', style: { fontSize: 11, color: C.label3 } }, label),
390
- React.createElement('div', {
391
- key: 'v',
392
- style: { fontSize: 14, fontWeight: 650, color: C.label, marginTop: 2, ...NUM },
393
- }, Number(seconds) > 0 ? formatDuration(seconds) : '—'),
394
- ]);
395
- return React.createElement('div', { style: CARD }, [
396
- React.createElement('div', {
397
- key: 'top',
398
- style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
528
+ key: 'chips',
529
+ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap' },
399
530
  }, [
400
- React.createElement('span', { key: 'l', style: { fontSize: 12, color: C.label2 } }, t('todayProgress')),
401
531
  React.createElement('span', {
402
- key: 'p',
403
- style: { fontSize: 13, fontWeight: 650, color: C.brand, ...NUM },
404
- }, percentText),
532
+ key: 'per',
533
+ style: {
534
+ display: 'inline-flex', alignItems: 'center', gap: 4,
535
+ fontSize: 11.5, fontWeight: 600, color: C.label2,
536
+ background: `color-mix(in srgb, ${C.brand} 11%, transparent)`,
537
+ border: `1px solid color-mix(in srgb, ${C.brand} 22%, transparent)`,
538
+ borderRadius: 999, padding: '2px 9px', ...NUM,
539
+ },
540
+ }, `${t('perSecond')} +${money(rates.secondWage, 4)}`),
541
+ React.createElement('span', { key: 'target', style: { fontSize: 11, color: C.label3, ...NUM } },
542
+ rest ? t('restHint') : `${t('todayTarget')} ${money(snap.todayTarget, 2)}`),
543
+ ]),
544
+ React.createElement('div', {
545
+ key: 'progTop',
546
+ style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 13, flexWrap: 'wrap' },
547
+ }, [
548
+ React.createElement('span', { key: 'l', style: { fontSize: 11.5, color: C.label3 } }, t('todayProgress')),
549
+ React.createElement('span', { key: 'p', style: { fontSize: 12.5, fontWeight: 700, color: C.brand, ...NUM } }, percentText),
405
550
  ]),
406
551
  React.createElement('div', {
407
552
  key: 'bar',
408
- style: {
409
- marginTop: 6, height: 8, borderRadius: 999, background: C.layer2,
410
- overflow: 'hidden',
411
- },
553
+ style: { marginTop: 5, height: 9, borderRadius: 999, background: `color-mix(in srgb, ${C.label} 9%, transparent)`, overflow: 'hidden' },
412
554
  }, [
413
555
  React.createElement('div', {
414
556
  key: 'fill',
415
557
  style: {
416
- width: `${(progress * 100).toFixed(2)}%`,
417
- height: '100%',
418
- borderRadius: 999,
419
- background: C.brand,
420
- transition: 'width .4s linear',
558
+ width: `${(progress * 100).toFixed(2)}%`, height: '100%', borderRadius: 999,
559
+ background: `linear-gradient(90deg, ${C.brand} 0%, color-mix(in srgb, ${C.brand} 55%, ${C.ok}) 100%)`,
560
+ transition: 'width .35s linear',
421
561
  },
422
562
  }),
423
563
  ]),
424
564
  React.createElement('div', {
425
- key: 'nums',
426
- style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap', fontSize: 11, color: C.label3 },
565
+ key: 'progNum',
566
+ style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap', fontSize: 10.5, color: C.label3 },
427
567
  }, [
428
568
  React.createElement('span', { key: 'a' }, t('progressOfTarget')),
429
- React.createElement('span', { key: 'b', style: { ...NUM } },
569
+ React.createElement('span', { key: 'b', style: { color: C.label2, fontWeight: 600, ...NUM } },
430
570
  `${money(snap.todayEarned, 2)} / ${money(snap.todayTarget, 2)}`),
431
571
  ]),
432
- React.createElement('div', {
433
- key: 'cd',
434
- style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 8 },
435
- }, [
436
- countdownChip('start', t('toStart'), snap.secondsToStart),
437
- countdownChip('end', t('toEnd'), snap.secondsToEnd),
438
- ]),
439
572
  ]);
440
573
  }
441
574
 
442
- /** 六宫格:秒 / 分 / 时 / 日 / 周 / 月工资。 */
575
+ /** 倒计时两张小卡:距上班 / 距下班。 */
576
+ function CountdownRow(props) {
577
+ const t = props.t;
578
+ const snap = props.snap;
579
+ const chip = (key, label, value, accent) => React.createElement('div', {
580
+ key,
581
+ style: {
582
+ flex: '1 1 120px', minWidth: 116,
583
+ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8,
584
+ padding: '9px 12px', borderRadius: 12,
585
+ background: accent ? `color-mix(in srgb, ${GOLD_TINT} 9%, ${C.layer1})` : C.layer1,
586
+ border: `1px solid ${accent ? `color-mix(in srgb, ${GOLD_TINT} 38%, ${C.border1})` : C.border1}`,
587
+ boxSizing: 'border-box',
588
+ },
589
+ }, [
590
+ React.createElement('span', {
591
+ key: 'l',
592
+ style: { fontSize: 11, color: accent ? C.label2 : C.label3, whiteSpace: 'nowrap' },
593
+ }, label),
594
+ React.createElement('span', {
595
+ key: 'v',
596
+ style: { fontSize: 13.5, fontWeight: 700, color: C.label, ...NUM },
597
+ }, value),
598
+ ]);
599
+ return React.createElement('div', {
600
+ style: { display: 'flex', flexWrap: 'wrap', gap: 9 },
601
+ }, [
602
+ chip('start', t('toStart'), Number(snap.secondsToStart) > 0 ? formatDuration(snap.secondsToStart) : '—'),
603
+ chip('end', t('toEnd'), Number(snap.secondsToEnd) > 0 ? formatDuration(snap.secondsToEnd) : '—'),
604
+ chip('month', t('monthEarned'), money(snap.monthEarned, 2), true),
605
+ ]);
606
+ }
607
+
608
+ /** 六宫格:秒 / 分 / 时 / 日 / 周 / 月工资;第一张「秒工资」用品牌色强调。 */
443
609
  const RATE_CARDS = [
444
610
  { key: 'secondWage', label: 'secondWage', digits: 4 },
445
611
  { key: 'minuteWage', label: 'minuteWage', digits: 4 },
@@ -449,30 +615,80 @@ const RATE_CARDS = [
449
615
  { key: 'monthlyWage', label: 'monthlyWage', digits: 2 },
450
616
  ];
451
617
 
618
+ /** 工资换算网格:由调用方给出 {key,label,value,accent} 卡片(本月 / 指定月 / 全年平均三种视角)。 */
452
619
  function RatesGrid(props) {
453
- const t = props.t;
454
- const rates = props.rates;
620
+ const cards = Array.isArray(props.cards) ? props.cards : [];
455
621
  return React.createElement('div', {
456
622
  style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 8 },
457
- }, RATE_CARDS.map((spec, index) => React.createElement('div', {
458
- key: spec.key,
459
- 'data-stat': `stat-${index}`,
460
- style: { ...CARD, padding: '9px 10px' },
623
+ }, cards.map((card, index) => {
624
+ const accent = card.accent === true;
625
+ return React.createElement('div', {
626
+ key: card.key,
627
+ 'data-stat': `stat-${index}`,
628
+ style: {
629
+ ...CARD,
630
+ padding: '10px 11px',
631
+ borderRadius: 12,
632
+ ...(accent ? {
633
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 46%, ${C.border1})`,
634
+ background: `color-mix(in srgb, ${GOLD_TINT} 9%, ${C.layer1})`,
635
+ } : {}),
636
+ },
637
+ }, [
638
+ React.createElement('div', {
639
+ key: 'l',
640
+ style: { fontSize: 11, color: accent ? C.label2 : C.label3, fontWeight: accent ? 600 : 400 },
641
+ }, card.label),
642
+ React.createElement('div', {
643
+ key: 'v',
644
+ style: {
645
+ fontSize: 15.5, fontWeight: 700, letterSpacing: '-.2px',
646
+ color: accent ? C.label : C.label, marginTop: 2, ...NUM,
647
+ },
648
+ }, card.value),
649
+ ]);
650
+ }));
651
+ }
652
+
653
+ /** 月份 / 全年视角选择器(原生 select,深浅色都用主题变量)。 */
654
+ function PeriodSelect(props) {
655
+ const t = props.t;
656
+ const year = props.year;
657
+ const months = props.months;
658
+ const style = {
659
+ appearance: 'none',
660
+ WebkitAppearance: 'none',
661
+ border: `1px solid ${C.border}`,
662
+ background: C.layer2,
663
+ color: C.label,
664
+ borderRadius: 8,
665
+ padding: '4px 9px',
666
+ fontSize: 12,
667
+ fontWeight: 600,
668
+ cursor: 'pointer',
669
+ outline: 'none',
670
+ maxWidth: 220,
671
+ };
672
+ return React.createElement('select', {
673
+ value: props.value,
674
+ 'aria-label': t('viewLabel'),
675
+ onChange: (event) => props.onChange(event.target.value),
676
+ style,
461
677
  }, [
462
- React.createElement('div', { key: 'l', style: { fontSize: 11, color: C.label3 } }, t(spec.label)),
463
- React.createElement('div', {
464
- key: 'v',
465
- style: { fontSize: 15, fontWeight: 650, color: C.label, marginTop: 2, ...NUM },
466
- }, money(rates[spec.key], spec.digits)),
467
- ])));
678
+ React.createElement('option', { key: 'current', value: 'current' }, t('viewCurrent')),
679
+ ...months.map((month) => React.createElement('option', {
680
+ key: `m${month.month}`,
681
+ value: `m${month.month}`,
682
+ }, `${month.month}${t('monthUnit')} · ${month.workdays}${t('dayUnit')} · ${t('holidayShort')}${month.holidayDays}`)),
683
+ React.createElement('option', { key: 'year', value: 'year' }, `${year} ${t('viewYear')}`),
684
+ ]);
468
685
  }
469
686
 
470
- /** 明细脚注:月薪 / 当月工作日 / 日有效工时 / 今日目标 / 本月已赚 + 节假日来源。 */
687
+ /** 口径明细:月薪 / 工作日 / 日工时 / 今日目标 / 本月已赚(节假日来源统一放在节假日卡片)。 */
471
688
  function DetailsFooter(props) {
472
689
  const t = props.t;
473
690
  const snap = props.snap;
474
691
  const rates = snap.rates;
475
- const source = props.holidaySource || HOLIDAY_SOURCE;
476
692
  const items = [
477
693
  { key: 'salary', label: t('monthlySalaryLabel'), value: money(rates.monthlySalary, 2) },
478
694
  { key: 'workdays', label: t('workdaysThisMonth'), value: `${rates.monthlyWorkdays}${t('dayUnit')}` },
@@ -481,36 +697,149 @@ function DetailsFooter(props) {
481
697
  { key: 'month', label: t('monthEarned'), value: money(snap.monthEarned, 2) },
482
698
  ];
483
699
  return React.createElement('div', {
484
- style: { ...CARD, padding: '9px 12px', fontSize: 11, color: C.label3, lineHeight: 1.7 },
700
+ style: {
701
+ background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12,
702
+ padding: '10px 12px', fontSize: 11, color: C.label3, lineHeight: 1.6,
703
+ },
485
704
  }, [
486
705
  React.createElement('div', {
487
706
  key: 'items',
488
- style: { display: 'flex', flexWrap: 'wrap', gap: '2px 14px' },
489
- }, items.map((item) => React.createElement('span', {
707
+ style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: '7px 12px' },
708
+ }, items.map((item) => React.createElement('div', {
490
709
  key: item.key,
491
- style: { display: 'inline-flex', alignItems: 'baseline', gap: 4, whiteSpace: 'nowrap' },
710
+ style: { minWidth: 0 },
492
711
  }, [
493
- React.createElement('span', { key: 'l' }, `${item.label}`),
494
- React.createElement('span', {
712
+ React.createElement('div', { key: 'l', style: { fontSize: 10.5, color: C.label3 } }, item.label),
713
+ React.createElement('div', {
495
714
  key: 'v',
496
- style: { color: C.label2, fontWeight: 600, ...NUM },
715
+ style: { fontSize: 13, fontWeight: 650, color: C.label2, ...NUM },
497
716
  }, item.value),
498
717
  ]))),
718
+ ]);
719
+ }
720
+
721
+ /** 节假日卡片:2026 官方 / 2027 预估两段,七段假日区段 + 调休上班日统计。 */
722
+ const HOLIDAY_LABEL_KEYS = {
723
+ newYear: 'holidayNewYear',
724
+ spring: 'holidaySpring',
725
+ qingming: 'holidayQingming',
726
+ labour: 'holidayLabour',
727
+ dragon: 'holidayDragon',
728
+ midAutumn: 'holidayMidAutumn',
729
+ national: 'holidayNational',
730
+ };
731
+
732
+ function HolidaysCard(props) {
733
+ const t = props.t;
734
+ const source = props.holidaySource || HOLIDAY_SOURCE;
735
+ const extra = Array.isArray(props.extraWorkdays) ? props.extraWorkdays : [];
736
+ const countYear = (year) => extra.filter((key) => String(key).startsWith(`${year}-`)).length;
737
+ const shortDate = (key) => key.slice(5).replace('-', '/');
738
+ const sections = [
739
+ {
740
+ key: 'official',
741
+ label: t('holidays2026Label'),
742
+ blocks: HOLIDAY_BLOCKS,
743
+ estimated: false,
744
+ extraCount: countYear(2026),
745
+ tag: null,
746
+ },
747
+ {
748
+ key: 'estimate',
749
+ label: t('holidays2027Label'),
750
+ blocks: HOLIDAY_BLOCKS_2027_ESTIMATED,
751
+ estimated: true,
752
+ extraCount: countYear(2027),
753
+ tag: t('holidayEstimateTag'),
754
+ },
755
+ ];
756
+ const sumDays = (blocks) => blocks.reduce((sum, block) => sum + block.days, 0);
757
+ const renderSection = (section) => React.createElement('div', { key: section.key, style: { marginTop: section.key === 'official' ? 8 : 12 } }, [
758
+ React.createElement('div', {
759
+ key: 'head',
760
+ style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 },
761
+ }, [
762
+ React.createElement('span', { key: 'l', style: { fontSize: 11.5, fontWeight: 700, color: C.label2 } }, section.label),
763
+ section.tag
764
+ ? React.createElement('span', {
765
+ key: 'tag',
766
+ style: {
767
+ fontSize: 9.5, fontWeight: 700, color: C.label2,
768
+ border: `1px dashed color-mix(in srgb, ${GOLD_TINT} 60%, transparent)`,
769
+ borderRadius: 999, padding: '0 6px', lineHeight: 1.6,
770
+ },
771
+ }, section.tag)
772
+ : null,
773
+ React.createElement('span', { key: 's', style: { marginLeft: 'auto', fontSize: 10, color: C.label3, ...NUM } },
774
+ t('holidaysCardSub').replace('{days}', String(sumDays(section.blocks))).replace('{extra}', String(section.extraCount))),
775
+ ]),
776
+ React.createElement('div', {
777
+ key: 'grid',
778
+ style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: 7, marginTop: 6 },
779
+ }, section.blocks.map((block) => React.createElement('div', {
780
+ key: block.id,
781
+ style: {
782
+ border: section.estimated
783
+ ? `1px dashed color-mix(in srgb, ${GOLD_TINT} 40%, ${C.border1})`
784
+ : `1px solid ${C.border1}`,
785
+ borderRadius: 9,
786
+ padding: '6px 8px',
787
+ background: `color-mix(in srgb, ${GOLD_TINT} ${section.estimated ? 3 : 6}%, transparent)`,
788
+ },
789
+ }, [
790
+ React.createElement('div', {
791
+ key: 'row',
792
+ style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 6 },
793
+ }, [
794
+ React.createElement('span', { key: 'n', style: { fontSize: 11.5, fontWeight: 650, color: C.label } },
795
+ t(HOLIDAY_LABEL_KEYS[block.id])),
796
+ React.createElement('span', {
797
+ key: 'd',
798
+ style: {
799
+ fontSize: 10, fontWeight: 700, color: C.label2,
800
+ background: `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)`,
801
+ borderRadius: 999, padding: '0 6px', ...NUM,
802
+ },
803
+ }, t('holidaysDaysShort').replace('{days}', String(block.days))),
804
+ ]),
805
+ React.createElement('div', { key: 'r', style: { fontSize: 10, color: C.label3, marginTop: 2, ...NUM } },
806
+ `${shortDate(block.from)} – ${shortDate(block.to)}`),
807
+ ]))),
808
+ section.estimated
809
+ ? React.createElement('div', {
810
+ key: 'est-note',
811
+ style: { marginTop: 6, fontSize: 10.5, color: C.label3, lineHeight: 1.5 },
812
+ }, `⚠ ${t('holidayEstimateNote')}`)
813
+ : null,
814
+ ]);
815
+ return React.createElement('div', {
816
+ style: {
817
+ background: C.layer1,
818
+ border: `1px solid ${C.border1}`,
819
+ borderRadius: 12,
820
+ padding: '10px 12px',
821
+ },
822
+ }, [
499
823
  React.createElement('div', {
500
- key: 'src',
501
- style: { display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', gap: 4, marginTop: 2 },
824
+ key: 'title',
825
+ style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
502
826
  }, [
503
- React.createElement('span', { key: 'l' }, `${t('holidaySource')}:`),
827
+ React.createElement('span', { key: 't', style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' } },
828
+ t('holidaysCardTitle')),
504
829
  React.createElement('a', {
505
830
  key: 'a',
506
831
  href: source.url,
507
832
  target: '_blank',
508
833
  rel: 'noreferrer',
509
834
  title: source.title,
510
- style: { color: C.brand, textDecoration: 'underline' },
511
- }, `${source.year} · ${t('holidaySourceLink')} ↗`),
512
- React.createElement('span', { key: 'n', style: { color: C.label3 } }, `(${source.title})`),
835
+ style: { fontSize: 10.5, color: C.brand, textDecoration: 'none', fontWeight: 600 },
836
+ }, `${source.year} ${t('holidaySourceLink')} ↗`),
513
837
  ]),
838
+ sections.map(renderSection),
839
+ React.createElement('div', {
840
+ key: 'foot',
841
+ style: { marginTop: 8, fontSize: 10.5, color: C.label3 },
842
+ }, t('holidaysExtraNote').replace('{extra}', String(extra.length))),
514
843
  ]);
515
844
  }
516
845
 
@@ -528,7 +857,7 @@ function SettingsSection(props) {
528
857
  const inputStyle = {
529
858
  width: '100%',
530
859
  boxSizing: 'border-box',
531
- padding: '5px 8px',
860
+ padding: '6px 9px',
532
861
  borderRadius: 8,
533
862
  border: `1px solid ${C.border}`,
534
863
  background: C.layer2,
@@ -537,11 +866,11 @@ function SettingsSection(props) {
537
866
  fontVariantNumeric: 'tabular-nums',
538
867
  outline: 'none',
539
868
  };
540
- const labelStyle = { fontSize: 11, color: C.label3 };
869
+ const labelStyle = { fontSize: 10.5, color: C.label3, fontWeight: 500 };
541
870
 
542
871
  const textField = (field, labelKey, placeholder) => React.createElement('label', {
543
872
  key: field,
544
- style: { display: 'flex', flexDirection: 'column', gap: 3, flex: '1 1 130px', minWidth: 112 },
873
+ style: { display: 'flex', flexDirection: 'column', gap: 4, flex: '1 1 130px', minWidth: 112 },
545
874
  }, [
546
875
  React.createElement('span', { key: 'l', style: labelStyle }, t(labelKey)),
547
876
  React.createElement('input', {
@@ -570,38 +899,24 @@ function SettingsSection(props) {
570
899
  React.createElement('span', { key: 'l' }, t(labelKey)),
571
900
  ]);
572
901
 
573
- const textareaField = (field, labelKey) => React.createElement('label', {
574
- key: field,
575
- style: { display: 'flex', flexDirection: 'column', gap: 3, flex: '1 1 220px', minWidth: 200 },
902
+ return React.createElement('details', {
903
+ style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: '11px 13px' },
576
904
  }, [
577
- React.createElement('span', { key: 'l', style: labelStyle }, t(labelKey)),
578
- React.createElement('textarea', {
579
- key: 'i',
580
- rows: 4,
581
- value: form[field],
582
- spellCheck: false,
583
- onChange: (event) => setField(field, event.target.value),
584
- style: {
585
- ...inputStyle,
586
- resize: 'vertical',
587
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
588
- lineHeight: 1.5,
589
- },
590
- }),
591
- ]);
592
-
593
- return React.createElement('details', { style: { ...CARD, padding: '10px 12px' } }, [
594
905
  React.createElement('summary', {
595
906
  key: 'sum',
596
- style: { cursor: 'pointer', fontSize: 13, fontWeight: 600, color: C.label, userSelect: 'none' },
907
+ style: { cursor: 'pointer', fontSize: 13, fontWeight: 650, color: C.label, userSelect: 'none' },
597
908
  }, t('settings')),
598
909
  React.createElement('div', {
599
910
  key: 'hint',
600
- style: { fontSize: 11, color: C.label3, margin: '4px 0 9px' },
911
+ style: { fontSize: 10.5, color: C.label3, margin: '5px 0 10px' },
601
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')),
602
917
  React.createElement('div', {
603
918
  key: 'fields',
604
- style: { display: 'flex', flexWrap: 'wrap', gap: 8 },
919
+ style: { display: 'flex', flexWrap: 'wrap', gap: 9 },
605
920
  }, [
606
921
  textField('monthlySalary', 'salaryField', '10000'),
607
922
  textField('workStart', 'workStartField', '09:00'),
@@ -611,16 +926,9 @@ function SettingsSection(props) {
611
926
  checkField('lunchEnabled', 'lunchEnabledField'),
612
927
  checkField('lunchPaid', 'lunchPaidField'),
613
928
  ]),
614
- React.createElement('div', {
615
- key: 'lists',
616
- style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 8 },
617
- }, [
618
- textareaField('holidaysText', 'holidaysField'),
619
- textareaField('extraWorkdaysText', 'extraWorkdaysField'),
620
- ]),
621
929
  React.createElement('div', {
622
930
  key: 'actions',
623
- style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 10 },
931
+ style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 11 },
624
932
  }, [
625
933
  React.createElement('button', {
626
934
  key: 'save',
@@ -628,14 +936,21 @@ function SettingsSection(props) {
628
936
  onClick: onSave,
629
937
  disabled: save.status === 'saving',
630
938
  style: {
631
- border: `1px solid ${C.brand}`,
632
- color: C.brand,
633
- background: 'transparent',
939
+ // 金色按钮:与主卡金额、节假日强调同一套金色系;深棕字保证任何主题下都有对比度
940
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 72%, rgba(0,0,0,.18))`,
941
+ color: '#3A2A05',
942
+ background: save.status === 'saving'
943
+ ? `color-mix(in srgb, ${GOLD_TINT} 52%, #fff)`
944
+ : 'linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)',
634
945
  borderRadius: 8,
635
- padding: '6px 16px',
946
+ padding: '7px 20px',
636
947
  fontSize: 13,
637
- fontWeight: 600,
948
+ fontWeight: 700,
949
+ letterSpacing: '.3px',
638
950
  cursor: save.status === 'saving' ? 'default' : 'pointer',
951
+ 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)`,
953
+ textShadow: '0 1px 0 rgba(255,255,255,.28)',
639
954
  },
640
955
  }, save.status === 'saving' ? t('saving') : t('save')),
641
956
  save.status === 'ok'
@@ -677,6 +992,70 @@ function NiumaPanel(props) {
677
992
  const rates = snap && snap.rates ? snap.rates : computeRates(cfg, snap && snap.dateKey);
678
993
  const baseForm = React.useMemo(() => formFromConfig(cfg), [cfg]);
679
994
 
995
+ /**
996
+ * 「工资换算」视角:本月(跟随今日口径)/ 指定月份 / 全年平均。
997
+ * 只影响换算展示,不影响顶部「今日已赚」计数器(计数器永远按今天算)。
998
+ */
999
+ const viewYear = Number(String(snap && snap.dateKey ? snap.dateKey : '').slice(0, 4)) || new Date().getFullYear();
1000
+ const yearEstimated = viewYear === HOLIDAY_ESTIMATE_2027.year;
1001
+ const yearData = React.useMemo(() => yearSummary(cfg, viewYear), [cfg, viewYear]);
1002
+ const [view, setView] = React.useState('current');
1003
+ const selectedMonth = view.startsWith('m') ? yearData.months[Number(view.slice(1)) - 1] : null;
1004
+ const maxHolidayMonth = yearData.maxHolidayMonth;
1005
+ const maxHolidayText = t('maxHolidayNote')
1006
+ .replace('{month}', String(maxHolidayMonth.month))
1007
+ .replace('{days}', String(maxHolidayMonth.holidayDays));
1008
+ let viewCards;
1009
+ let viewHint;
1010
+ let viewNote;
1011
+ if (view === 'year') {
1012
+ // 与「本月」保持同一阅读顺序:秒 → 分 → 时 → 日,再放该视角的汇总项
1013
+ viewCards = [
1014
+ { key: 'avgSecond', label: t('averageSecond'), value: money(yearData.averageSecondWage, 4), accent: true },
1015
+ { key: 'avgMinute', label: t('averageMinute'), value: money(yearData.averageMinuteWage, 4) },
1016
+ { key: 'avgHourly', label: t('averageHourly'), value: money(yearData.averageHourlyWage, 2) },
1017
+ { key: 'avgDaily', label: t('averageDaily'), value: money(yearData.averageDailyWage, 2) },
1018
+ { key: 'yearWorkdays', label: t('yearWorkdays'), value: `${yearData.totalWorkdays}${t('dayUnit')}` },
1019
+ { key: 'yearIncome', label: t('yearIncome'), value: money(yearData.annualIncome, 2) },
1020
+ ];
1021
+ viewHint = `${t('yearHint').replace('{year}', String(viewYear)).replace('{workdays}', String(yearData.totalWorkdays))} · ${t('viewOnlyHint')}`
1022
+ + (yearEstimated ? ` · ${t('holidayEstimateShort')}` : '');
1023
+ viewNote = yearEstimated ? t('holidayEstimateNote') : maxHolidayText;
1024
+ } else if (selectedMonth) {
1025
+ // 与「本月」保持同一阅读顺序:秒 → 分 → 时 → 日 → 月 → 工作日
1026
+ viewCards = [
1027
+ { key: 'secondWage', label: t('secondWage'), value: money(selectedMonth.secondWage, 4), accent: true },
1028
+ { key: 'minuteWage', label: t('minuteWage'), value: money(selectedMonth.minuteWage, 4) },
1029
+ { key: 'hourlyWage', label: t('hourlyWage'), value: money(selectedMonth.hourlyWage, 2) },
1030
+ { key: 'dailyWage', label: t('dailyWage'), value: money(selectedMonth.dailyWage, 2) },
1031
+ { key: 'monthlyWage', label: t('monthlyWage'), value: money(selectedMonth.monthlySalary, 2) },
1032
+ { key: 'monthWorkdays', label: t('monthWorkdaysCard'), value: `${selectedMonth.workdays}${t('dayUnit')}` },
1033
+ ];
1034
+ viewHint = `${t('monthHint')
1035
+ .replace('{year}', String(viewYear))
1036
+ .replace('{month}', String(selectedMonth.month))
1037
+ .replace('{workdays}', String(selectedMonth.workdays))
1038
+ .replace('{holidays}', String(selectedMonth.holidayDays))} · ${t('viewOnlyHint')}`
1039
+ + (yearEstimated ? ` · ${t('holidayEstimateShort')}` : '');
1040
+ viewNote = yearEstimated
1041
+ ? t('holidayEstimateNote')
1042
+ : (selectedMonth.month === maxHolidayMonth.month
1043
+ ? `★ ${t('maxHolidayThis').replace('{days}', String(maxHolidayMonth.holidayDays))}`
1044
+ : maxHolidayText);
1045
+ } else {
1046
+ // 统一阅读顺序:秒 → 分 → 时 → 日 → 周 → 月(所有视角都从「秒」开始、从小到大)
1047
+ viewCards = RATE_CARDS.map((spec) => ({
1048
+ key: spec.key,
1049
+ label: t(spec.label),
1050
+ value: money(rates[spec.key], spec.digits),
1051
+ accent: spec.key === 'secondWage',
1052
+ }));
1053
+ viewHint = t('currentHint')
1054
+ .replace('{days}', String(rates.monthlyWorkdays))
1055
+ .replace('{hours}', trimNumber(rates.paidHours, 2));
1056
+ viewNote = maxHolidayText;
1057
+ }
1058
+
680
1059
  // 设置表单草稿:null = 还没改过,直接显示当前配置。
681
1060
  const [draft, setDraft] = React.useState(null);
682
1061
  const [save, setSave] = React.useState({ status: 'idle', errors: [], message: '' });
@@ -742,30 +1121,60 @@ function NiumaPanel(props) {
742
1121
 
743
1122
  return React.createElement('div', {
744
1123
  style: {
745
- display: 'flex',
746
- flexDirection: 'column',
747
- gap: 9,
748
- minWidth: 0,
749
- padding: '2px 1px 18px',
1124
+ // DSH 的 main slot 给的是定高容器:插件必须自己当滚动容器,否则展开设置后底部会被裁掉。
1125
+ // 注意:这里用 block + 内层 flex 的写法 —— 如果根直接 display:flex,子项会在定高容器里
1126
+ // 被 flex-shrink 压扁(展开设置时整张主卡高度被压缩、内容被 overflow 裁掉)。
1127
+ height: '100%',
1128
+ width: '100%',
1129
+ overflowY: 'auto',
1130
+ overflowX: 'hidden',
1131
+ overscrollBehavior: 'contain',
1132
+ scrollbarGutter: 'stable',
1133
+ boxSizing: 'border-box',
750
1134
  fontSize: 13,
751
1135
  lineHeight: 1.45,
752
1136
  color: C.label,
753
1137
  fontFamily: 'inherit',
754
1138
  },
755
1139
  }, [
756
- // 标题 + 状态徽标
1140
+ React.createElement(StyleTag, { key: 'css' }),
1141
+ React.createElement('div', {
1142
+ key: 'inner',
1143
+ style: {
1144
+ display: 'flex',
1145
+ flexDirection: 'column',
1146
+ gap: 11,
1147
+ width: '100%',
1148
+ maxWidth: 820,
1149
+ margin: '0 auto',
1150
+ padding: '16px 18px 40px',
1151
+ boxSizing: 'border-box',
1152
+ minWidth: 0,
1153
+ },
1154
+ }, [
1155
+ // 品牌头:图标块 + 标题/副标题 + 状态徽标
757
1156
  React.createElement('div', {
758
1157
  key: 'head',
759
- style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
1158
+ style: { display: 'flex', alignItems: 'center', gap: 11, flexWrap: 'wrap' },
760
1159
  }, [
1160
+ React.createElement('div', {
1161
+ key: 'icon',
1162
+ style: {
1163
+ width: 38, height: 38, borderRadius: 12, flex: '0 0 auto',
1164
+ display: 'grid', placeItems: 'center',
1165
+ color: C.brand,
1166
+ background: `color-mix(in srgb, ${C.brand} 13%, transparent)`,
1167
+ border: `1px solid color-mix(in srgb, ${C.brand} 26%, transparent)`,
1168
+ },
1169
+ }, React.createElement(NiumaIcon, { size: 21 })),
761
1170
  React.createElement('div', { key: 'tt', style: { marginRight: 'auto', minWidth: 0 } }, [
762
1171
  React.createElement('div', {
763
1172
  key: 't',
764
- style: { fontSize: 16, fontWeight: 700, color: C.label },
1173
+ style: { fontSize: 17.5, fontWeight: 800, letterSpacing: '-.3px', color: C.label },
765
1174
  }, t('title')),
766
1175
  React.createElement('div', {
767
1176
  key: 's',
768
- style: { fontSize: 11, color: C.label3 },
1177
+ style: { fontSize: 11, color: C.label3, marginTop: 1 },
769
1178
  }, t('subtitle')),
770
1179
  ]),
771
1180
  React.createElement(StatusBadge, { key: 'badge', t, status: snap.status }),
@@ -777,8 +1186,9 @@ function NiumaPanel(props) {
777
1186
  key: 'banner',
778
1187
  style: {
779
1188
  display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
780
- fontSize: 12, color: banner.color, padding: '5px 9px',
781
- borderRadius: 8, border: `1px solid ${banner.color}`, background: C.layer1,
1189
+ fontSize: 12, color: banner.color, padding: '6px 10px',
1190
+ borderRadius: 9, border: `1px solid color-mix(in srgb, ${banner.color} 45%, transparent)`,
1191
+ background: `color-mix(in srgb, ${banner.color} 10%, transparent)`,
782
1192
  },
783
1193
  }, [
784
1194
  React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } }, banner.text),
@@ -797,9 +1207,57 @@ function NiumaPanel(props) {
797
1207
  : null,
798
1208
 
799
1209
  React.createElement(HeroCard, { key: 'hero', t, snap }),
800
- React.createElement(ProgressCard, { key: 'progress', t, snap }),
801
- React.createElement(RatesGrid, { key: 'rates', t, rates }),
1210
+ React.createElement(CountdownRow, { key: 'countdown', t, snap }),
1211
+ React.createElement('div', {
1212
+ key: 'ratesHead',
1213
+ style: {
1214
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1215
+ gap: 8, flexWrap: 'wrap', margin: '2px 1px -1px',
1216
+ },
1217
+ }, [
1218
+ React.createElement('span', {
1219
+ key: 't',
1220
+ style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' },
1221
+ }, t('sectionRates')),
1222
+ React.createElement(PeriodSelect, {
1223
+ key: 'period',
1224
+ t,
1225
+ value: view,
1226
+ onChange: setView,
1227
+ year: viewYear,
1228
+ months: yearData.months,
1229
+ }),
1230
+ ]),
1231
+ React.createElement('div', {
1232
+ key: 'ratesHint',
1233
+ style: { fontSize: 10.5, color: C.label3, ...NUM },
1234
+ }, viewHint),
1235
+ React.createElement(RatesGrid, { key: 'rates', cards: viewCards }),
1236
+ viewNote
1237
+ ? React.createElement('div', {
1238
+ key: 'ratesNote',
1239
+ style: { fontSize: 10.5, color: C.label3, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
1240
+ }, [
1241
+ React.createElement('span', {
1242
+ key: 'badge',
1243
+ style: {
1244
+ color: C.label2, fontWeight: 600,
1245
+ background: `color-mix(in srgb, ${GOLD_TINT} 14%, transparent)`,
1246
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 32%, transparent)`,
1247
+ borderRadius: 999, padding: '1px 8px',
1248
+ },
1249
+ }, t('maxHolidayBadge')),
1250
+ React.createElement('span', { key: 'txt' }, viewNote),
1251
+ ])
1252
+ : null,
1253
+ React.createElement(SectionTitle, { key: 'detailTitle', title: t('sectionDetail') }),
802
1254
  React.createElement(DetailsFooter, { key: 'footer', t, snap, holidaySource }),
1255
+ React.createElement(HolidaysCard, {
1256
+ key: 'holidays',
1257
+ t,
1258
+ holidaySource,
1259
+ extraWorkdays: cfg.extraWorkdays,
1260
+ }),
803
1261
  React.createElement(SettingsSection, {
804
1262
  key: 'settings',
805
1263
  t,
@@ -809,6 +1267,7 @@ function NiumaPanel(props) {
809
1267
  save,
810
1268
  onSave,
811
1269
  }),
1270
+ ]),
812
1271
  ]);
813
1272
  }
814
1273