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.
- package/CHANGELOG.md +32 -0
- package/README.md +6 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +814 -217
- package/lib/client.js.map +2 -2
- package/lib/index.js +48 -118
- package/package.json +3 -8
- package/src/client/index.js +623 -164
- package/src/core.mjs +88 -0
- package/src/holidays.mjs +90 -10
package/src/client/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
'
|
|
116
|
-
'
|
|
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
|
-
'
|
|
168
|
-
'
|
|
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) =>
|
|
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: '
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
481
|
+
key: 'glow',
|
|
482
|
+
'aria-hidden': 'true',
|
|
347
483
|
style: {
|
|
348
|
-
|
|
349
|
-
|
|
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', {
|
|
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:
|
|
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:
|
|
368
|
-
|
|
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: '
|
|
374
|
-
style: {
|
|
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: '
|
|
403
|
-
style: {
|
|
404
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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: '
|
|
426
|
-
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap', fontSize:
|
|
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
|
|
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
|
-
},
|
|
458
|
-
|
|
459
|
-
'
|
|
460
|
-
|
|
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('
|
|
463
|
-
React.createElement('
|
|
464
|
-
key:
|
|
465
|
-
|
|
466
|
-
},
|
|
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: {
|
|
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: '
|
|
489
|
-
}, items.map((item) => React.createElement('
|
|
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: {
|
|
710
|
+
style: { minWidth: 0 },
|
|
492
711
|
}, [
|
|
493
|
-
React.createElement('
|
|
494
|
-
React.createElement('
|
|
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: {
|
|
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: '
|
|
501
|
-
style: { display: 'flex',
|
|
824
|
+
key: 'title',
|
|
825
|
+
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
|
|
502
826
|
}, [
|
|
503
|
-
React.createElement('span', { key: '
|
|
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: '
|
|
511
|
-
}, `${source.year}
|
|
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: '
|
|
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:
|
|
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:
|
|
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
|
-
|
|
574
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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: '
|
|
946
|
+
padding: '7px 20px',
|
|
636
947
|
fontSize: 13,
|
|
637
|
-
fontWeight:
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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:
|
|
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:
|
|
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: '
|
|
781
|
-
borderRadius:
|
|
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(
|
|
801
|
-
React.createElement(
|
|
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
|
|