dsh-niuma 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +7 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +948 -259
- package/lib/client.js.map +2 -2
- package/lib/index.js +48 -118
- package/package.json +3 -8
- package/src/client/index.js +716 -208
- package/src/core.mjs +101 -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,28 +116,76 @@ const zh = {
|
|
|
98
116
|
'paidHoursPerDay': '日有效工时',
|
|
99
117
|
'todayTarget': '今日目标',
|
|
100
118
|
'monthEarned': '本月已赚',
|
|
101
|
-
'
|
|
119
|
+
'monthWorkedDays': '本月已工作',
|
|
120
|
+
'monthRemainingDays': '剩余工作日',
|
|
102
121
|
'holidaySourceLink': '国务院办公厅 2026 年节假日安排',
|
|
103
122
|
'configLoading': '正在读取设置…',
|
|
104
123
|
'configFailed': '设置读取失败,使用默认值',
|
|
105
124
|
'retry': '重试',
|
|
106
125
|
'settings': '设置',
|
|
107
126
|
'settingsHint': '改完点「保存」生效;格式不对会在下方红字提示,不影响计时。',
|
|
108
|
-
'salaryField': '
|
|
127
|
+
'salaryField': '实收月薪',
|
|
128
|
+
'groupSalary': '工资',
|
|
129
|
+
'groupWorkTime': '工作时间',
|
|
130
|
+
'groupLunch': '午休',
|
|
131
|
+
'salaryUnit': '元 / 月',
|
|
109
132
|
'workStartField': '上班时间',
|
|
110
133
|
'workEndField': '下班时间',
|
|
111
134
|
'lunchEnabledField': '启用午休',
|
|
135
|
+
'lunchEnabledShort': '启用',
|
|
112
136
|
'lunchStartField': '午休开始',
|
|
113
137
|
'lunchEndField': '午休结束',
|
|
114
138
|
'lunchPaidField': '午休算工时',
|
|
115
|
-
'
|
|
116
|
-
'
|
|
139
|
+
'lunchPaidShort': '算工时',
|
|
140
|
+
'holidaysCardTitle': '节假日安排',
|
|
141
|
+
'holidays2026Label': '2026 年(官方)',
|
|
142
|
+
'holidays2027Label': '2027 年(预估)',
|
|
143
|
+
'holidayEstimateTag': '预估',
|
|
144
|
+
'holidayEstimateNote': '官方尚未公布,以上为按规则与惯例推算,仅供参考,以国务院通知为准。',
|
|
145
|
+
'holidayEstimateShort': '2027 为预估值',
|
|
146
|
+
'settingsDataNote': '节假日与调休数据见上方「节假日安排」卡片(2027 年为预估值,不可在本页编辑)。',
|
|
147
|
+
|
|
148
|
+
'holidaysCardSub': '法定假日 {days} 天 · 调休上班 {extra} 天',
|
|
149
|
+
'holidaysDaysShort': '{days} 天',
|
|
150
|
+
'holidaysExtraNote': '调休上班日 {extra} 天(周末补班,按工作日计)',
|
|
151
|
+
'holidayNewYear': '元旦',
|
|
152
|
+
'holidaySpring': '春节',
|
|
153
|
+
'holidayQingming': '清明',
|
|
154
|
+
'holidayLabour': '劳动节',
|
|
155
|
+
'holidayDragon': '端午',
|
|
156
|
+
'holidayMidAutumn': '中秋',
|
|
157
|
+
'holidayNational': '国庆',
|
|
117
158
|
'save': '保存',
|
|
118
159
|
'saving': '保存中…',
|
|
119
160
|
'saved': '已保存',
|
|
120
161
|
'saveFailed': '保存失败:',
|
|
121
162
|
'dayUnit': '天',
|
|
122
163
|
'hourUnit': '小时',
|
|
164
|
+
'minuteUnit': '分钟',
|
|
165
|
+
'viewLabel': '查看',
|
|
166
|
+
'viewCurrent': '本月',
|
|
167
|
+
'viewYear': '全年平均',
|
|
168
|
+
'monthUnit': '月',
|
|
169
|
+
'holidayShort': '假',
|
|
170
|
+
'currentHint': '按每月 {days} 个工作日、每天 {hours} 小时计',
|
|
171
|
+
'monthHint': '{year} 年 {month} 月 · 工作日 {workdays} 天 · 法定假日 {holidays} 天',
|
|
172
|
+
'yearHint': '{year} 全年 · {workdays} 个工作日 · 按年收入折算',
|
|
173
|
+
'viewOnlyHint': '仅换算,不影响今日计数器',
|
|
174
|
+
'yearWorkdays': '全年工作日',
|
|
175
|
+
'yearIncome': '全年收入',
|
|
176
|
+
'averageDaily': '平均日薪',
|
|
177
|
+
'averageHourly': '平均时薪',
|
|
178
|
+
'averageMinute': '平均分薪',
|
|
179
|
+
'averageSecond': '平均秒薪',
|
|
180
|
+
'monthWorkdaysCard': '工作日',
|
|
181
|
+
'maxHolidayBadge': '全年假日最多',
|
|
182
|
+
'maxHolidayNote': '全年假日最多:{month} 月(法定假日 {days} 天)',
|
|
183
|
+
'maxHolidayThis': '全年假日最多的月份(法定假日 {days} 天)',
|
|
184
|
+
'sectionRates': '工资换算',
|
|
185
|
+
'sectionRatesHint': '按每月 {days} 个工作日、每天 {hours} 小时计',
|
|
186
|
+
'sectionDetail': '今日口径明细',
|
|
187
|
+
'restHint': '今天休息,好好躺平',
|
|
188
|
+
'liveNow': '实时',
|
|
123
189
|
};
|
|
124
190
|
|
|
125
191
|
const en = {
|
|
@@ -150,28 +216,76 @@ const en = {
|
|
|
150
216
|
'paidHoursPerDay': 'Paid hours / day',
|
|
151
217
|
'todayTarget': "Today's target",
|
|
152
218
|
'monthEarned': 'Earned this month',
|
|
153
|
-
'
|
|
219
|
+
'monthWorkedDays': 'Worked this month',
|
|
220
|
+
'monthRemainingDays': 'Workdays left',
|
|
154
221
|
'holidaySourceLink': 'State Council 2026 holiday schedule',
|
|
155
222
|
'configLoading': 'Loading settings…',
|
|
156
223
|
'configFailed': 'Failed to load settings, using defaults',
|
|
157
224
|
'retry': 'Retry',
|
|
158
225
|
'settings': 'Settings',
|
|
159
226
|
'settingsHint': 'Click Save to apply. Invalid input is shown in red below; the ticker keeps running.',
|
|
160
|
-
'salaryField': 'Net monthly salary
|
|
227
|
+
'salaryField': 'Net monthly salary',
|
|
228
|
+
'groupSalary': 'Salary',
|
|
229
|
+
'groupWorkTime': 'Working hours',
|
|
230
|
+
'groupLunch': 'Lunch break',
|
|
231
|
+
'salaryUnit': 'CNY / month',
|
|
161
232
|
'workStartField': 'Work start',
|
|
162
233
|
'workEndField': 'Work end',
|
|
163
234
|
'lunchEnabledField': 'Enable lunch break',
|
|
235
|
+
'lunchEnabledShort': 'On',
|
|
164
236
|
'lunchStartField': 'Lunch start',
|
|
165
237
|
'lunchEndField': 'Lunch end',
|
|
166
238
|
'lunchPaidField': 'Lunch is paid',
|
|
167
|
-
'
|
|
168
|
-
'
|
|
239
|
+
'lunchPaidShort': 'Paid',
|
|
240
|
+
'holidaysCardTitle': 'Holiday calendar',
|
|
241
|
+
'holidays2026Label': '2026 (official)',
|
|
242
|
+
'holidays2027Label': '2027 (estimated)',
|
|
243
|
+
'holidayEstimateTag': 'estimate',
|
|
244
|
+
'holidayEstimateNote': 'Not announced officially yet — estimated from the rules and past practice, for reference only.',
|
|
245
|
+
'holidayEstimateShort': '2027 is estimated',
|
|
246
|
+
'settingsDataNote': 'Holiday data lives in the calendar card above (2027 values are estimates and not editable here).',
|
|
247
|
+
|
|
248
|
+
'holidaysCardSub': '{days} statutory days · {extra} make-up workdays',
|
|
249
|
+
'holidaysDaysShort': '{days} d',
|
|
250
|
+
'holidaysExtraNote': '{extra} make-up workdays (weekend shifts, counted as workdays)',
|
|
251
|
+
'holidayNewYear': "New Year's Day",
|
|
252
|
+
'holidaySpring': 'Spring Festival',
|
|
253
|
+
'holidayQingming': 'Qingming',
|
|
254
|
+
'holidayLabour': 'Labour Day',
|
|
255
|
+
'holidayDragon': 'Dragon Boat',
|
|
256
|
+
'holidayMidAutumn': 'Mid-Autumn',
|
|
257
|
+
'holidayNational': 'National Day',
|
|
169
258
|
'save': 'Save',
|
|
170
259
|
'saving': 'Saving…',
|
|
171
260
|
'saved': 'Saved',
|
|
172
261
|
'saveFailed': 'Save failed: ',
|
|
173
262
|
'dayUnit': 'd',
|
|
174
263
|
'hourUnit': 'h',
|
|
264
|
+
'minuteUnit': 'min',
|
|
265
|
+
'viewLabel': 'View',
|
|
266
|
+
'viewCurrent': 'This month',
|
|
267
|
+
'viewYear': 'Year average',
|
|
268
|
+
'monthUnit': 'M',
|
|
269
|
+
'holidayShort': 'hol.',
|
|
270
|
+
'currentHint': '{days} workdays / month · {hours} h / day',
|
|
271
|
+
'monthHint': '{year}-{month} · {workdays} workdays · {holidays} statutory holidays',
|
|
272
|
+
'yearHint': '{year} · {workdays} workdays · averaged over the year',
|
|
273
|
+
'viewOnlyHint': 'Conversion only — today\'s ticker is unaffected',
|
|
274
|
+
'yearWorkdays': 'Workdays / year',
|
|
275
|
+
'yearIncome': 'Annual income',
|
|
276
|
+
'averageDaily': 'Avg per day',
|
|
277
|
+
'averageHourly': 'Avg per hour',
|
|
278
|
+
'averageMinute': 'Avg per minute',
|
|
279
|
+
'averageSecond': 'Avg per second',
|
|
280
|
+
'monthWorkdaysCard': 'Workdays',
|
|
281
|
+
'maxHolidayBadge': 'Most holidays',
|
|
282
|
+
'maxHolidayNote': 'Most holidays: month {month} ({days} statutory days)',
|
|
283
|
+
'maxHolidayThis': 'The month with the most holidays ({days} statutory days)',
|
|
284
|
+
'sectionRates': 'Pay rates',
|
|
285
|
+
'sectionRatesHint': '{days} workdays / month · {hours} h / day',
|
|
286
|
+
'sectionDetail': "Today's basis",
|
|
287
|
+
'restHint': 'Day off — enjoy it',
|
|
288
|
+
'liveNow': 'LIVE',
|
|
175
289
|
};
|
|
176
290
|
|
|
177
291
|
/** 组件在没有 locale 注入 t 时(测试 / 兜底)直接读中文表,绝不白屏。 */
|
|
@@ -180,7 +294,7 @@ const fallbackTranslate = (key) => (zh[key] != null ? zh[key] : key);
|
|
|
180
294
|
/* ------------------------------------------------------------------ *
|
|
181
295
|
* 格式化小工具
|
|
182
296
|
* ------------------------------------------------------------------ */
|
|
183
|
-
const money = (value, digits = 2) =>
|
|
297
|
+
const money = (value, digits = 2) => `${formatMoney(value, digits)} 元`;
|
|
184
298
|
|
|
185
299
|
/** 8.00 -> '8',8.50 -> '8.5'(工时展示用) */
|
|
186
300
|
function trimNumber(value, digits = 2) {
|
|
@@ -188,13 +302,6 @@ function trimNumber(value, digits = 2) {
|
|
|
188
302
|
return fixed.replace(/\.?0+$/, '') || '0';
|
|
189
303
|
}
|
|
190
304
|
|
|
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
305
|
/** 配置 -> 设置表单草稿(所有输入框都是字符串 / 布尔,编辑中不打断计时)。 */
|
|
199
306
|
function formFromConfig(config) {
|
|
200
307
|
return {
|
|
@@ -205,8 +312,6 @@ function formFromConfig(config) {
|
|
|
205
312
|
lunchStart: String(config.lunchStart == null ? '' : config.lunchStart),
|
|
206
313
|
lunchEnd: String(config.lunchEnd == null ? '' : config.lunchEnd),
|
|
207
314
|
lunchPaid: config.lunchPaid === true,
|
|
208
|
-
holidaysText: (config.holidays || []).join('\n'),
|
|
209
|
-
extraWorkdaysText: (config.extraWorkdays || []).join('\n'),
|
|
210
315
|
};
|
|
211
316
|
}
|
|
212
317
|
|
|
@@ -221,8 +326,6 @@ function patchFromForm(form) {
|
|
|
221
326
|
lunchStart: String(form.lunchStart || '').trim(),
|
|
222
327
|
lunchEnd: String(form.lunchEnd || '').trim(),
|
|
223
328
|
lunchPaid: Boolean(form.lunchPaid),
|
|
224
|
-
holidays: splitDateLines(form.holidaysText),
|
|
225
|
-
extraWorkdays: splitDateLines(form.extraWorkdaysText),
|
|
226
329
|
};
|
|
227
330
|
}
|
|
228
331
|
|
|
@@ -309,137 +412,218 @@ function NiumaIcon(props) {
|
|
|
309
412
|
]);
|
|
310
413
|
}
|
|
311
414
|
|
|
415
|
+
/** 全局小样式:只放动效 keyframes(内联 style 写不了 keyframes)。组件内联样式仍是主体。 */
|
|
416
|
+
const PANEL_CSS = `
|
|
417
|
+
@keyframes niumaPulse { 0%,100% { opacity: .95; transform: scale(1); } 50% { opacity: .3; transform: scale(.68); } }
|
|
418
|
+
@media (prefers-reduced-motion: reduce) { .niuma-live-dot { animation: none !important; } }
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
function StyleTag() {
|
|
422
|
+
return React.createElement('style', null, PANEL_CSS);
|
|
423
|
+
}
|
|
424
|
+
|
|
312
425
|
/** 状态徽标:休息日 / 未上班 / 搬砖中 / 午休中 / 已下班。 */
|
|
313
426
|
function StatusBadge(props) {
|
|
314
427
|
const t = props.t;
|
|
315
428
|
const meta = STATUS_META[props.status] || STATUS_META.rest;
|
|
429
|
+
const live = props.status === 'working';
|
|
316
430
|
return React.createElement('span', {
|
|
317
431
|
style: {
|
|
318
432
|
display: 'inline-flex',
|
|
319
433
|
alignItems: 'center',
|
|
320
434
|
gap: 6,
|
|
321
|
-
padding: '
|
|
435
|
+
padding: '4px 11px 4px 9px',
|
|
322
436
|
borderRadius: 999,
|
|
323
437
|
fontSize: 12,
|
|
324
438
|
fontWeight: 600,
|
|
325
439
|
lineHeight: 1.5,
|
|
326
440
|
color: meta.color,
|
|
327
|
-
border: `1px solid ${meta.color}`,
|
|
328
|
-
background:
|
|
441
|
+
border: `1px solid color-mix(in srgb, ${meta.color} 42%, transparent)`,
|
|
442
|
+
background: `color-mix(in srgb, ${meta.color} 12%, transparent)`,
|
|
329
443
|
whiteSpace: 'nowrap',
|
|
330
444
|
},
|
|
331
445
|
}, [
|
|
332
446
|
React.createElement('i', {
|
|
333
447
|
key: 'dot',
|
|
448
|
+
className: live ? 'niuma-live-dot' : undefined,
|
|
334
449
|
style: { width: 6, height: 6, borderRadius: '50%', background: meta.color, display: 'inline-block' },
|
|
335
450
|
}),
|
|
336
451
|
t(meta.key),
|
|
337
452
|
]);
|
|
338
453
|
}
|
|
339
454
|
|
|
340
|
-
/**
|
|
455
|
+
/** 小节标题:左标题 + 右口径提示(同一行,窄屏自动换行)。 */
|
|
456
|
+
function SectionTitle(props) {
|
|
457
|
+
return React.createElement('div', {
|
|
458
|
+
style: {
|
|
459
|
+
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
|
460
|
+
gap: 8, flexWrap: 'wrap', margin: '2px 1px -1px',
|
|
461
|
+
},
|
|
462
|
+
}, [
|
|
463
|
+
React.createElement('span', {
|
|
464
|
+
key: 't',
|
|
465
|
+
style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' },
|
|
466
|
+
}, props.title),
|
|
467
|
+
props.hint
|
|
468
|
+
? React.createElement('span', { key: 'h', style: { fontSize: 10.5, color: C.label3, ...NUM } }, props.hint)
|
|
469
|
+
: null,
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** 今日已赚主卡:渐变底 + 超大金额 + 进度条(视觉焦点只有这一处)。 */
|
|
341
474
|
function HeroCard(props) {
|
|
342
475
|
const t = props.t;
|
|
343
476
|
const snap = props.snap;
|
|
344
|
-
|
|
477
|
+
const rates = snap.rates;
|
|
478
|
+
const progress = Math.max(0, Math.min(1, Number(snap.todayProgress) || 0));
|
|
479
|
+
const percentText = `${(progress * 100).toFixed(1)}%`;
|
|
480
|
+
const live = snap.status === 'working';
|
|
481
|
+
const rest = snap.status === 'rest';
|
|
482
|
+
const heroBackground = 'linear-gradient(135deg, '
|
|
483
|
+
+ 'color-mix(in srgb, ' + C.brand + ' 17%, ' + C.layer1 + ') 0%, '
|
|
484
|
+
+ C.layer1 + ' 58%, ' + C.layer1 + ' 100%)';
|
|
485
|
+
return React.createElement('div', {
|
|
486
|
+
style: {
|
|
487
|
+
position: 'relative',
|
|
488
|
+
overflow: 'hidden',
|
|
489
|
+
borderRadius: 18,
|
|
490
|
+
padding: '16px 18px 14px',
|
|
491
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 34%, ${C.border1})`,
|
|
492
|
+
background: heroBackground,
|
|
493
|
+
boxShadow: '0 12px 30px rgba(0,0,0,.10)',
|
|
494
|
+
boxSizing: 'border-box',
|
|
495
|
+
minWidth: 0,
|
|
496
|
+
},
|
|
497
|
+
}, [
|
|
345
498
|
React.createElement('div', {
|
|
346
|
-
key: '
|
|
499
|
+
key: 'glow',
|
|
500
|
+
'aria-hidden': 'true',
|
|
347
501
|
style: {
|
|
348
|
-
|
|
349
|
-
|
|
502
|
+
position: 'absolute', right: -52, top: -72, width: 188, height: 188, borderRadius: '50%',
|
|
503
|
+
background: `radial-gradient(circle, color-mix(in srgb, ${GOLD_TINT} 30%, transparent) 0%, transparent 68%)`,
|
|
504
|
+
pointerEvents: 'none',
|
|
350
505
|
},
|
|
506
|
+
}),
|
|
507
|
+
React.createElement('div', {
|
|
508
|
+
key: 'head',
|
|
509
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
|
|
351
510
|
}, [
|
|
352
|
-
React.createElement('span', {
|
|
511
|
+
React.createElement('span', {
|
|
512
|
+
key: 'a',
|
|
513
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, fontWeight: 600, color: C.label2 },
|
|
514
|
+
}, [
|
|
515
|
+
live
|
|
516
|
+
? React.createElement('i', {
|
|
517
|
+
key: 'dot',
|
|
518
|
+
className: 'niuma-live-dot',
|
|
519
|
+
style: { width: 7, height: 7, borderRadius: '50%', background: C.ok, display: 'inline-block' },
|
|
520
|
+
})
|
|
521
|
+
: null,
|
|
522
|
+
t('earnedToday'),
|
|
523
|
+
]),
|
|
353
524
|
React.createElement('span', { key: 'b', style: { fontSize: 11, color: C.label3, ...NUM } },
|
|
354
525
|
`${t('todayOn')} ${snap.dateKey}`),
|
|
355
526
|
]),
|
|
356
527
|
React.createElement('div', {
|
|
357
528
|
key: 'amt',
|
|
358
|
-
style: { display: 'flex', alignItems: 'baseline', gap:
|
|
529
|
+
style: { display: 'flex', alignItems: 'baseline', gap: 7, marginTop: 4, flexWrap: 'wrap' },
|
|
359
530
|
}, [
|
|
360
|
-
React.createElement('span', {
|
|
361
|
-
key: 'cur',
|
|
362
|
-
style: { fontSize: 22, fontWeight: 700, color: C.label2, ...NUM },
|
|
363
|
-
}, '¥'),
|
|
364
531
|
React.createElement('span', {
|
|
365
532
|
key: 'val',
|
|
366
533
|
style: {
|
|
367
|
-
fontSize:
|
|
368
|
-
|
|
534
|
+
fontSize: 46, lineHeight: 1.12, fontWeight: 800, letterSpacing: '-1.2px',
|
|
535
|
+
...GOLD,
|
|
536
|
+
...NUM,
|
|
537
|
+
...(rest ? { opacity: 0.62 } : {}),
|
|
369
538
|
},
|
|
370
539
|
}, formatMoney(snap.todayEarned, 4)),
|
|
540
|
+
React.createElement('span', {
|
|
541
|
+
key: 'unit',
|
|
542
|
+
style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM },
|
|
543
|
+
}, '元'),
|
|
371
544
|
]),
|
|
372
545
|
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' },
|
|
546
|
+
key: 'chips',
|
|
547
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap' },
|
|
399
548
|
}, [
|
|
400
|
-
React.createElement('span', { key: 'l', style: { fontSize: 12, color: C.label2 } }, t('todayProgress')),
|
|
401
549
|
React.createElement('span', {
|
|
402
|
-
key: '
|
|
403
|
-
style: {
|
|
404
|
-
|
|
550
|
+
key: 'per',
|
|
551
|
+
style: {
|
|
552
|
+
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
553
|
+
fontSize: 11.5, fontWeight: 600, color: C.label2,
|
|
554
|
+
background: `color-mix(in srgb, ${C.brand} 11%, transparent)`,
|
|
555
|
+
border: `1px solid color-mix(in srgb, ${C.brand} 22%, transparent)`,
|
|
556
|
+
borderRadius: 999, padding: '2px 9px', ...NUM,
|
|
557
|
+
},
|
|
558
|
+
}, `${t('perSecond')} +${money(rates.secondWage, 4)}`),
|
|
559
|
+
React.createElement('span', { key: 'target', style: { fontSize: 11, color: C.label3, ...NUM } },
|
|
560
|
+
rest ? t('restHint') : `${t('todayTarget')} ${money(snap.todayTarget, 2)}`),
|
|
561
|
+
]),
|
|
562
|
+
React.createElement('div', {
|
|
563
|
+
key: 'progTop',
|
|
564
|
+
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 13, flexWrap: 'wrap' },
|
|
565
|
+
}, [
|
|
566
|
+
React.createElement('span', { key: 'l', style: { fontSize: 11.5, color: C.label3 } }, t('todayProgress')),
|
|
567
|
+
React.createElement('span', { key: 'p', style: { fontSize: 12.5, fontWeight: 700, color: C.brand, ...NUM } }, percentText),
|
|
405
568
|
]),
|
|
406
569
|
React.createElement('div', {
|
|
407
570
|
key: 'bar',
|
|
408
|
-
style: {
|
|
409
|
-
marginTop: 6, height: 8, borderRadius: 999, background: C.layer2,
|
|
410
|
-
overflow: 'hidden',
|
|
411
|
-
},
|
|
571
|
+
style: { marginTop: 5, height: 9, borderRadius: 999, background: `color-mix(in srgb, ${C.label} 9%, transparent)`, overflow: 'hidden' },
|
|
412
572
|
}, [
|
|
413
573
|
React.createElement('div', {
|
|
414
574
|
key: 'fill',
|
|
415
575
|
style: {
|
|
416
|
-
width: `${(progress * 100).toFixed(2)}%`,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
background: C.brand,
|
|
420
|
-
transition: 'width .4s linear',
|
|
576
|
+
width: `${(progress * 100).toFixed(2)}%`, height: '100%', borderRadius: 999,
|
|
577
|
+
background: `linear-gradient(90deg, ${C.brand} 0%, color-mix(in srgb, ${C.brand} 55%, ${C.ok}) 100%)`,
|
|
578
|
+
transition: 'width .35s linear',
|
|
421
579
|
},
|
|
422
580
|
}),
|
|
423
581
|
]),
|
|
424
582
|
React.createElement('div', {
|
|
425
|
-
key: '
|
|
426
|
-
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap', fontSize:
|
|
583
|
+
key: 'progNum',
|
|
584
|
+
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 5, flexWrap: 'wrap', fontSize: 10.5, color: C.label3 },
|
|
427
585
|
}, [
|
|
428
586
|
React.createElement('span', { key: 'a' }, t('progressOfTarget')),
|
|
429
|
-
React.createElement('span', { key: 'b', style: { ...NUM } },
|
|
587
|
+
React.createElement('span', { key: 'b', style: { color: C.label2, fontWeight: 600, ...NUM } },
|
|
430
588
|
`${money(snap.todayEarned, 2)} / ${money(snap.todayTarget, 2)}`),
|
|
431
589
|
]),
|
|
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
590
|
]);
|
|
440
591
|
}
|
|
441
592
|
|
|
442
|
-
/**
|
|
593
|
+
/** 倒计时两张小卡:距上班 / 距下班。 */
|
|
594
|
+
function CountdownRow(props) {
|
|
595
|
+
const t = props.t;
|
|
596
|
+
const snap = props.snap;
|
|
597
|
+
const chip = (key, label, value, accent) => React.createElement('div', {
|
|
598
|
+
key,
|
|
599
|
+
style: {
|
|
600
|
+
flex: '1 1 120px', minWidth: 116,
|
|
601
|
+
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8,
|
|
602
|
+
padding: '9px 12px', borderRadius: 12,
|
|
603
|
+
background: accent ? `color-mix(in srgb, ${GOLD_TINT} 9%, ${C.layer1})` : C.layer1,
|
|
604
|
+
border: `1px solid ${accent ? `color-mix(in srgb, ${GOLD_TINT} 38%, ${C.border1})` : C.border1}`,
|
|
605
|
+
boxSizing: 'border-box',
|
|
606
|
+
},
|
|
607
|
+
}, [
|
|
608
|
+
React.createElement('span', {
|
|
609
|
+
key: 'l',
|
|
610
|
+
style: { fontSize: 11, color: accent ? C.label2 : C.label3, whiteSpace: 'nowrap' },
|
|
611
|
+
}, label),
|
|
612
|
+
React.createElement('span', {
|
|
613
|
+
key: 'v',
|
|
614
|
+
style: { fontSize: 13.5, fontWeight: 700, color: C.label, ...NUM },
|
|
615
|
+
}, value),
|
|
616
|
+
]);
|
|
617
|
+
return React.createElement('div', {
|
|
618
|
+
style: { display: 'flex', flexWrap: 'wrap', gap: 9 },
|
|
619
|
+
}, [
|
|
620
|
+
chip('start', t('toStart'), Number(snap.secondsToStart) > 0 ? formatDuration(snap.secondsToStart) : '—'),
|
|
621
|
+
chip('end', t('toEnd'), Number(snap.secondsToEnd) > 0 ? formatDuration(snap.secondsToEnd) : '—'),
|
|
622
|
+
chip('month', t('monthEarned'), money(snap.monthEarned, 2), true),
|
|
623
|
+
]);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** 六宫格:秒 / 分 / 时 / 日 / 周 / 月工资;第一张「秒工资」用品牌色强调。 */
|
|
443
627
|
const RATE_CARDS = [
|
|
444
628
|
{ key: 'secondWage', label: 'secondWage', digits: 4 },
|
|
445
629
|
{ key: 'minuteWage', label: 'minuteWage', digits: 4 },
|
|
@@ -449,68 +633,233 @@ const RATE_CARDS = [
|
|
|
449
633
|
{ key: 'monthlyWage', label: 'monthlyWage', digits: 2 },
|
|
450
634
|
];
|
|
451
635
|
|
|
636
|
+
/** 工资换算网格:由调用方给出 {key,label,value,accent} 卡片(本月 / 指定月 / 全年平均三种视角)。 */
|
|
452
637
|
function RatesGrid(props) {
|
|
453
|
-
const
|
|
454
|
-
const rates = props.rates;
|
|
638
|
+
const cards = Array.isArray(props.cards) ? props.cards : [];
|
|
455
639
|
return React.createElement('div', {
|
|
456
640
|
style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 8 },
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
'
|
|
460
|
-
|
|
641
|
+
}, cards.map((card, index) => {
|
|
642
|
+
const accent = card.accent === true;
|
|
643
|
+
return React.createElement('div', {
|
|
644
|
+
key: card.key,
|
|
645
|
+
'data-stat': `stat-${index}`,
|
|
646
|
+
style: {
|
|
647
|
+
...CARD,
|
|
648
|
+
padding: '10px 11px',
|
|
649
|
+
borderRadius: 12,
|
|
650
|
+
...(accent ? {
|
|
651
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 46%, ${C.border1})`,
|
|
652
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 9%, ${C.layer1})`,
|
|
653
|
+
} : {}),
|
|
654
|
+
},
|
|
655
|
+
}, [
|
|
656
|
+
React.createElement('div', {
|
|
657
|
+
key: 'l',
|
|
658
|
+
style: { fontSize: 11, color: accent ? C.label2 : C.label3, fontWeight: accent ? 600 : 400 },
|
|
659
|
+
}, card.label),
|
|
660
|
+
React.createElement('div', {
|
|
661
|
+
key: 'v',
|
|
662
|
+
style: {
|
|
663
|
+
fontSize: 15.5, fontWeight: 700, letterSpacing: '-.2px',
|
|
664
|
+
color: accent ? C.label : C.label, marginTop: 2, ...NUM,
|
|
665
|
+
},
|
|
666
|
+
}, card.value),
|
|
667
|
+
]);
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** 月份 / 全年视角选择器(原生 select,深浅色都用主题变量)。 */
|
|
672
|
+
function PeriodSelect(props) {
|
|
673
|
+
const t = props.t;
|
|
674
|
+
const year = props.year;
|
|
675
|
+
const months = props.months;
|
|
676
|
+
const style = {
|
|
677
|
+
appearance: 'none',
|
|
678
|
+
WebkitAppearance: 'none',
|
|
679
|
+
border: `1px solid ${C.border}`,
|
|
680
|
+
background: C.layer2,
|
|
681
|
+
color: C.label,
|
|
682
|
+
borderRadius: 8,
|
|
683
|
+
padding: '4px 9px',
|
|
684
|
+
fontSize: 12,
|
|
685
|
+
fontWeight: 600,
|
|
686
|
+
cursor: 'pointer',
|
|
687
|
+
outline: 'none',
|
|
688
|
+
maxWidth: 220,
|
|
689
|
+
};
|
|
690
|
+
return React.createElement('select', {
|
|
691
|
+
value: props.value,
|
|
692
|
+
'aria-label': t('viewLabel'),
|
|
693
|
+
onChange: (event) => props.onChange(event.target.value),
|
|
694
|
+
style,
|
|
461
695
|
}, [
|
|
462
|
-
React.createElement('
|
|
463
|
-
React.createElement('
|
|
464
|
-
key:
|
|
465
|
-
|
|
466
|
-
},
|
|
467
|
-
|
|
696
|
+
React.createElement('option', { key: 'current', value: 'current' }, t('viewCurrent')),
|
|
697
|
+
...months.map((month) => React.createElement('option', {
|
|
698
|
+
key: `m${month.month}`,
|
|
699
|
+
value: `m${month.month}`,
|
|
700
|
+
}, `${month.month}${t('monthUnit')} · ${month.workdays}${t('dayUnit')} · ${t('holidayShort')}${month.holidayDays}`)),
|
|
701
|
+
React.createElement('option', { key: 'year', value: 'year' }, `${year} ${t('viewYear')}`),
|
|
702
|
+
]);
|
|
468
703
|
}
|
|
469
704
|
|
|
470
|
-
/**
|
|
705
|
+
/** 口径明细:月薪 / 工作日 / 日工时 / 今日目标 / 本月已赚(节假日来源统一放在节假日卡片)。 */
|
|
471
706
|
function DetailsFooter(props) {
|
|
472
707
|
const t = props.t;
|
|
473
708
|
const snap = props.snap;
|
|
474
709
|
const rates = snap.rates;
|
|
475
|
-
const source = props.holidaySource || HOLIDAY_SOURCE;
|
|
476
710
|
const items = [
|
|
477
711
|
{ key: 'salary', label: t('monthlySalaryLabel'), value: money(rates.monthlySalary, 2) },
|
|
478
712
|
{ key: 'workdays', label: t('workdaysThisMonth'), value: `${rates.monthlyWorkdays}${t('dayUnit')}` },
|
|
479
713
|
{ key: 'hours', label: t('paidHoursPerDay'), value: `${trimNumber(rates.paidHours, 2)}${t('hourUnit')}` },
|
|
480
714
|
{ key: 'target', label: t('todayTarget'), value: money(snap.todayTarget, 2) },
|
|
481
715
|
{ key: 'month', label: t('monthEarned'), value: money(snap.monthEarned, 2) },
|
|
716
|
+
{ key: 'worked', label: t('monthWorkedDays'), value: `${snap.monthWorkdaysDone}${t('dayUnit')}` },
|
|
717
|
+
{ key: 'left', label: t('monthRemainingDays'), value: `${snap.monthWorkdaysLeft}${t('dayUnit')}` },
|
|
482
718
|
];
|
|
483
719
|
return React.createElement('div', {
|
|
484
|
-
style: {
|
|
720
|
+
style: {
|
|
721
|
+
background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12,
|
|
722
|
+
padding: '10px 12px', fontSize: 11, color: C.label3, lineHeight: 1.6,
|
|
723
|
+
},
|
|
485
724
|
}, [
|
|
486
725
|
React.createElement('div', {
|
|
487
726
|
key: 'items',
|
|
488
|
-
style: { display: '
|
|
489
|
-
}, items.map((item) => React.createElement('
|
|
727
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: '7px 12px' },
|
|
728
|
+
}, items.map((item) => React.createElement('div', {
|
|
490
729
|
key: item.key,
|
|
491
|
-
style: {
|
|
730
|
+
style: { minWidth: 0 },
|
|
492
731
|
}, [
|
|
493
|
-
React.createElement('
|
|
494
|
-
React.createElement('
|
|
732
|
+
React.createElement('div', { key: 'l', style: { fontSize: 10.5, color: C.label3 } }, item.label),
|
|
733
|
+
React.createElement('div', {
|
|
495
734
|
key: 'v',
|
|
496
|
-
style: {
|
|
735
|
+
style: { fontSize: 13, fontWeight: 650, color: C.label2, ...NUM },
|
|
497
736
|
}, item.value),
|
|
498
737
|
]))),
|
|
738
|
+
]);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** 节假日卡片:2026 官方 / 2027 预估两段,七段假日区段 + 调休上班日统计。 */
|
|
742
|
+
const HOLIDAY_LABEL_KEYS = {
|
|
743
|
+
newYear: 'holidayNewYear',
|
|
744
|
+
spring: 'holidaySpring',
|
|
745
|
+
qingming: 'holidayQingming',
|
|
746
|
+
labour: 'holidayLabour',
|
|
747
|
+
dragon: 'holidayDragon',
|
|
748
|
+
midAutumn: 'holidayMidAutumn',
|
|
749
|
+
national: 'holidayNational',
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
function HolidaysCard(props) {
|
|
753
|
+
const t = props.t;
|
|
754
|
+
const source = props.holidaySource || HOLIDAY_SOURCE;
|
|
755
|
+
const extra = Array.isArray(props.extraWorkdays) ? props.extraWorkdays : [];
|
|
756
|
+
const countYear = (year) => extra.filter((key) => String(key).startsWith(`${year}-`)).length;
|
|
757
|
+
const shortDate = (key) => key.slice(5).replace('-', '/');
|
|
758
|
+
const sections = [
|
|
759
|
+
{
|
|
760
|
+
key: 'official',
|
|
761
|
+
label: t('holidays2026Label'),
|
|
762
|
+
blocks: HOLIDAY_BLOCKS,
|
|
763
|
+
estimated: false,
|
|
764
|
+
extraCount: countYear(2026),
|
|
765
|
+
tag: null,
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
key: 'estimate',
|
|
769
|
+
label: t('holidays2027Label'),
|
|
770
|
+
blocks: HOLIDAY_BLOCKS_2027_ESTIMATED,
|
|
771
|
+
estimated: true,
|
|
772
|
+
extraCount: countYear(2027),
|
|
773
|
+
tag: t('holidayEstimateTag'),
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
const sumDays = (blocks) => blocks.reduce((sum, block) => sum + block.days, 0);
|
|
777
|
+
const renderSection = (section) => React.createElement('div', { key: section.key, style: { marginTop: section.key === 'official' ? 8 : 12 } }, [
|
|
778
|
+
React.createElement('div', {
|
|
779
|
+
key: 'head',
|
|
780
|
+
style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 },
|
|
781
|
+
}, [
|
|
782
|
+
React.createElement('span', { key: 'l', style: { fontSize: 11.5, fontWeight: 700, color: C.label2 } }, section.label),
|
|
783
|
+
section.tag
|
|
784
|
+
? React.createElement('span', {
|
|
785
|
+
key: 'tag',
|
|
786
|
+
style: {
|
|
787
|
+
fontSize: 9.5, fontWeight: 700, color: C.label2,
|
|
788
|
+
border: `1px dashed color-mix(in srgb, ${GOLD_TINT} 60%, transparent)`,
|
|
789
|
+
borderRadius: 999, padding: '0 6px', lineHeight: 1.6,
|
|
790
|
+
},
|
|
791
|
+
}, section.tag)
|
|
792
|
+
: null,
|
|
793
|
+
React.createElement('span', { key: 's', style: { marginLeft: 'auto', fontSize: 10, color: C.label3, ...NUM } },
|
|
794
|
+
t('holidaysCardSub').replace('{days}', String(sumDays(section.blocks))).replace('{extra}', String(section.extraCount))),
|
|
795
|
+
]),
|
|
796
|
+
React.createElement('div', {
|
|
797
|
+
key: 'grid',
|
|
798
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: 7, marginTop: 6 },
|
|
799
|
+
}, section.blocks.map((block) => React.createElement('div', {
|
|
800
|
+
key: block.id,
|
|
801
|
+
style: {
|
|
802
|
+
border: section.estimated
|
|
803
|
+
? `1px dashed color-mix(in srgb, ${GOLD_TINT} 40%, ${C.border1})`
|
|
804
|
+
: `1px solid ${C.border1}`,
|
|
805
|
+
borderRadius: 9,
|
|
806
|
+
padding: '6px 8px',
|
|
807
|
+
background: `color-mix(in srgb, ${GOLD_TINT} ${section.estimated ? 3 : 6}%, transparent)`,
|
|
808
|
+
},
|
|
809
|
+
}, [
|
|
810
|
+
React.createElement('div', {
|
|
811
|
+
key: 'row',
|
|
812
|
+
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 6 },
|
|
813
|
+
}, [
|
|
814
|
+
React.createElement('span', { key: 'n', style: { fontSize: 11.5, fontWeight: 650, color: C.label } },
|
|
815
|
+
t(HOLIDAY_LABEL_KEYS[block.id])),
|
|
816
|
+
React.createElement('span', {
|
|
817
|
+
key: 'd',
|
|
818
|
+
style: {
|
|
819
|
+
fontSize: 10, fontWeight: 700, color: C.label2,
|
|
820
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)`,
|
|
821
|
+
borderRadius: 999, padding: '0 6px', ...NUM,
|
|
822
|
+
},
|
|
823
|
+
}, t('holidaysDaysShort').replace('{days}', String(block.days))),
|
|
824
|
+
]),
|
|
825
|
+
React.createElement('div', { key: 'r', style: { fontSize: 10, color: C.label3, marginTop: 2, ...NUM } },
|
|
826
|
+
`${shortDate(block.from)} – ${shortDate(block.to)}`),
|
|
827
|
+
]))),
|
|
828
|
+
section.estimated
|
|
829
|
+
? React.createElement('div', {
|
|
830
|
+
key: 'est-note',
|
|
831
|
+
style: { marginTop: 6, fontSize: 10.5, color: C.label3, lineHeight: 1.5 },
|
|
832
|
+
}, `⚠ ${t('holidayEstimateNote')}`)
|
|
833
|
+
: null,
|
|
834
|
+
]);
|
|
835
|
+
return React.createElement('div', {
|
|
836
|
+
style: {
|
|
837
|
+
background: C.layer1,
|
|
838
|
+
border: `1px solid ${C.border1}`,
|
|
839
|
+
borderRadius: 12,
|
|
840
|
+
padding: '10px 12px',
|
|
841
|
+
},
|
|
842
|
+
}, [
|
|
499
843
|
React.createElement('div', {
|
|
500
|
-
key: '
|
|
501
|
-
style: { display: 'flex',
|
|
844
|
+
key: 'title',
|
|
845
|
+
style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
|
|
502
846
|
}, [
|
|
503
|
-
React.createElement('span', { key: '
|
|
847
|
+
React.createElement('span', { key: 't', style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' } },
|
|
848
|
+
t('holidaysCardTitle')),
|
|
504
849
|
React.createElement('a', {
|
|
505
850
|
key: 'a',
|
|
506
851
|
href: source.url,
|
|
507
852
|
target: '_blank',
|
|
508
853
|
rel: 'noreferrer',
|
|
509
854
|
title: source.title,
|
|
510
|
-
style: { color: C.brand, textDecoration: '
|
|
511
|
-
}, `${source.year}
|
|
512
|
-
React.createElement('span', { key: 'n', style: { color: C.label3 } }, `(${source.title})`),
|
|
855
|
+
style: { fontSize: 10.5, color: C.brand, textDecoration: 'none', fontWeight: 600 },
|
|
856
|
+
}, `${source.year} ${t('holidaySourceLink')} ↗`),
|
|
513
857
|
]),
|
|
858
|
+
sections.map(renderSection),
|
|
859
|
+
React.createElement('div', {
|
|
860
|
+
key: 'foot',
|
|
861
|
+
style: { marginTop: 8, fontSize: 10.5, color: C.label3 },
|
|
862
|
+
}, t('holidaysExtraNote').replace('{extra}', String(extra.length))),
|
|
514
863
|
]);
|
|
515
864
|
}
|
|
516
865
|
|
|
@@ -525,102 +874,111 @@ function SettingsSection(props) {
|
|
|
525
874
|
const save = props.save;
|
|
526
875
|
const onSave = props.onSave;
|
|
527
876
|
|
|
877
|
+
// E 方案:行式短控件 —— 标签固定左列,控件都按内容定宽(不拉满)
|
|
878
|
+
const rowStyle = {
|
|
879
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
880
|
+
minHeight: 33, padding: '2px 0', borderTop: `1px solid ${C.border1}`,
|
|
881
|
+
};
|
|
882
|
+
const rowKeyStyle = { width: 62, flex: '0 0 62px', fontSize: 11.5, color: C.label3 };
|
|
883
|
+
const rowValStyle = { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', minWidth: 0 };
|
|
528
884
|
const inputStyle = {
|
|
529
|
-
width: '
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
borderRadius: 8,
|
|
533
|
-
border: `1px solid ${C.border}`,
|
|
534
|
-
background: C.layer2,
|
|
535
|
-
color: C.label,
|
|
536
|
-
fontSize: 13,
|
|
537
|
-
fontVariantNumeric: 'tabular-nums',
|
|
538
|
-
outline: 'none',
|
|
885
|
+
boxSizing: 'border-box', width: 88, minHeight: 28, padding: '4px 8px',
|
|
886
|
+
borderRadius: 7, border: `1px solid ${C.border}`, background: C.layer2,
|
|
887
|
+
color: C.label, fontSize: 12.5, fontVariantNumeric: 'tabular-nums', outline: 'none',
|
|
539
888
|
};
|
|
540
|
-
const
|
|
889
|
+
const selectStyle = {
|
|
890
|
+
boxSizing: 'border-box', width: 48, minHeight: 28, padding: '4px 2px 4px 6px',
|
|
891
|
+
borderRadius: 7, border: `1px solid ${C.border}`, background: C.layer2,
|
|
892
|
+
color: C.label, fontSize: 12.5, fontVariantNumeric: 'tabular-nums', outline: 'none', cursor: 'pointer',
|
|
893
|
+
};
|
|
894
|
+
const unitStyle = { fontSize: 10.5, color: C.label3, whiteSpace: 'nowrap' };
|
|
895
|
+
const dashStyle = { fontSize: 11, color: C.label3, margin: '0 1px' };
|
|
896
|
+
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
|
897
|
+
const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
|
|
541
898
|
|
|
542
|
-
const
|
|
543
|
-
key:
|
|
544
|
-
|
|
899
|
+
const row = (key, label, values) => React.createElement('div', { key, style: rowStyle }, [
|
|
900
|
+
React.createElement('span', { key: 'k', style: rowKeyStyle }, label),
|
|
901
|
+
React.createElement('div', { key: 'v', style: rowValStyle }, values),
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
/** 24 小时制时间:小时/分钟两个下拉,各 46px(不受浏览器 locale 影响)。 */
|
|
905
|
+
const timeSelects = (field, label, disabled = false) => {
|
|
906
|
+
const [rawHour = '00', rawMinute = '00'] = String(form[field] || '00:00').split(':');
|
|
907
|
+
const hour = HOUR_OPTIONS.includes(rawHour) ? rawHour : '00';
|
|
908
|
+
const minute = MINUTE_OPTIONS.includes(rawMinute) ? rawMinute : '00';
|
|
909
|
+
const commit = (nextHour, nextMinute) => setField(field, `${nextHour}:${nextMinute}`);
|
|
910
|
+
const style = { ...selectStyle, opacity: disabled ? 0.45 : 1, cursor: disabled ? 'default' : 'pointer' };
|
|
911
|
+
return React.createElement('span', { key: field, style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, [
|
|
912
|
+
React.createElement('select', {
|
|
913
|
+
key: 'h', value: hour, disabled, 'aria-label': `${label} ${t('hourUnit')}`,
|
|
914
|
+
onChange: (event) => commit(event.target.value, minute), style,
|
|
915
|
+
}, HOUR_OPTIONS.map((value) => React.createElement('option', { key: value, value }, value))),
|
|
916
|
+
React.createElement('span', { key: 'c', style: { color: C.label3, fontWeight: 700, fontSize: 12 } }, ':'),
|
|
917
|
+
React.createElement('select', {
|
|
918
|
+
key: 'm', value: minute, disabled, 'aria-label': `${label} ${t('minuteUnit')}`,
|
|
919
|
+
onChange: (event) => commit(hour, event.target.value), style,
|
|
920
|
+
}, MINUTE_OPTIONS.map((value) => React.createElement('option', { key: value, value }, value))),
|
|
921
|
+
]);
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const timeRange = (startField, endField, label, disabled) => React.createElement('span', {
|
|
925
|
+
key: `${startField}-${endField}`,
|
|
926
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 2 },
|
|
545
927
|
}, [
|
|
546
|
-
|
|
547
|
-
React.createElement('
|
|
548
|
-
|
|
549
|
-
type: 'text',
|
|
550
|
-
value: form[field],
|
|
551
|
-
placeholder: placeholder || 'HH:mm',
|
|
552
|
-
onChange: (event) => setField(field, event.target.value),
|
|
553
|
-
style: inputStyle,
|
|
554
|
-
}),
|
|
928
|
+
timeSelects(startField, label, disabled),
|
|
929
|
+
React.createElement('span', { key: 'dash', style: dashStyle }, '–'),
|
|
930
|
+
timeSelects(endField, label, disabled),
|
|
555
931
|
]);
|
|
556
932
|
|
|
557
|
-
const
|
|
933
|
+
const toggle = (field, labelKey, disabled) => React.createElement('label', {
|
|
558
934
|
key: field,
|
|
559
935
|
style: {
|
|
560
|
-
display: 'flex', alignItems: 'center', gap:
|
|
561
|
-
|
|
936
|
+
display: 'inline-flex', alignItems: 'center', gap: 5,
|
|
937
|
+
padding: '2px 6px', borderRadius: 8,
|
|
938
|
+
border: `1px solid ${C.border1}`, background: C.layer2,
|
|
939
|
+
fontSize: 10.5, color: disabled ? C.label3 : C.label2, whiteSpace: 'nowrap',
|
|
940
|
+
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.55 : 1,
|
|
562
941
|
},
|
|
563
942
|
}, [
|
|
564
943
|
React.createElement('input', {
|
|
565
|
-
key: 'i',
|
|
566
|
-
type: 'checkbox',
|
|
567
|
-
checked: Boolean(form[field]),
|
|
944
|
+
key: 'i', type: 'checkbox', checked: Boolean(form[field]), disabled,
|
|
568
945
|
onChange: (event) => setCheck(field, event.target.checked),
|
|
946
|
+
style: { accentColor: GOLD_TINT, width: 13, height: 13, margin: 0, cursor: 'inherit' },
|
|
569
947
|
}),
|
|
570
948
|
React.createElement('span', { key: 'l' }, t(labelKey)),
|
|
571
949
|
]);
|
|
572
950
|
|
|
573
|
-
const
|
|
574
|
-
key: field,
|
|
575
|
-
style: { display: 'flex', flexDirection: 'column', gap: 3, flex: '1 1 220px', minWidth: 200 },
|
|
576
|
-
}, [
|
|
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
|
-
]);
|
|
951
|
+
const lunchDisabled = !form.lunchEnabled;
|
|
592
952
|
|
|
593
|
-
return React.createElement('details', {
|
|
953
|
+
return React.createElement('details', {
|
|
954
|
+
open: true,
|
|
955
|
+
style: { background: C.layer1, border: `1px solid ${C.border1}`, borderRadius: 12, padding: '10px 12px' },
|
|
956
|
+
}, [
|
|
594
957
|
React.createElement('summary', {
|
|
595
958
|
key: 'sum',
|
|
596
|
-
style: { cursor: 'pointer', fontSize:
|
|
959
|
+
style: { cursor: 'pointer', fontSize: 12.5, fontWeight: 650, color: C.label, userSelect: 'none' },
|
|
597
960
|
}, t('settings')),
|
|
598
|
-
React.createElement('div', {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
key: 'lists',
|
|
616
|
-
style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 8 },
|
|
617
|
-
}, [
|
|
618
|
-
textareaField('holidaysText', 'holidaysField'),
|
|
619
|
-
textareaField('extraWorkdaysText', 'extraWorkdaysField'),
|
|
961
|
+
React.createElement('div', { key: 'body', style: { marginTop: 7 } }, [
|
|
962
|
+
row('salary', t('salaryField'), [
|
|
963
|
+
React.createElement('input', {
|
|
964
|
+
key: 'i', type: 'text', inputMode: 'decimal', value: form.monthlySalary, placeholder: '10000',
|
|
965
|
+
onChange: (event) => setField('monthlySalary', event.target.value), style: inputStyle,
|
|
966
|
+
}),
|
|
967
|
+
React.createElement('span', { key: 'u', style: unitStyle }, t('salaryUnit')),
|
|
968
|
+
]),
|
|
969
|
+
row('worktime', t('groupWorkTime'), [
|
|
970
|
+
timeRange('workStart', 'workEnd', t('groupWorkTime'), false),
|
|
971
|
+
]),
|
|
972
|
+
row('lunch', t('groupLunch'), [
|
|
973
|
+
// 时间在前、两个开关在后(放到行尾)
|
|
974
|
+
timeRange('lunchStart', 'lunchEnd', t('groupLunch'), lunchDisabled),
|
|
975
|
+
toggle('lunchEnabled', 'lunchEnabledShort', false),
|
|
976
|
+
toggle('lunchPaid', 'lunchPaidShort', lunchDisabled),
|
|
977
|
+
]),
|
|
620
978
|
]),
|
|
621
979
|
React.createElement('div', {
|
|
622
980
|
key: 'actions',
|
|
623
|
-
style: { display: 'flex', alignItems: 'center', gap: 10,
|
|
981
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 7, paddingTop: 7, borderTop: `1px solid ${C.border1}` },
|
|
624
982
|
}, [
|
|
625
983
|
React.createElement('button', {
|
|
626
984
|
key: 'save',
|
|
@@ -628,23 +986,29 @@ function SettingsSection(props) {
|
|
|
628
986
|
onClick: onSave,
|
|
629
987
|
disabled: save.status === 'saving',
|
|
630
988
|
style: {
|
|
631
|
-
border: `1px solid ${
|
|
632
|
-
color:
|
|
633
|
-
background: '
|
|
989
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 72%, rgba(0,0,0,.18))`,
|
|
990
|
+
color: '#3A2A05',
|
|
991
|
+
background: save.status === 'saving'
|
|
992
|
+
? `color-mix(in srgb, ${GOLD_TINT} 52%, #fff)`
|
|
993
|
+
: 'linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)',
|
|
634
994
|
borderRadius: 8,
|
|
635
|
-
padding: '
|
|
636
|
-
fontSize:
|
|
637
|
-
fontWeight:
|
|
995
|
+
padding: '5px 16px',
|
|
996
|
+
fontSize: 12.5,
|
|
997
|
+
fontWeight: 700,
|
|
998
|
+
letterSpacing: '.3px',
|
|
638
999
|
cursor: save.status === 'saving' ? 'default' : 'pointer',
|
|
1000
|
+
opacity: save.status === 'saving' ? 0.72 : 1,
|
|
1001
|
+
boxShadow: `0 2px 9px color-mix(in srgb, ${GOLD_TINT} 34%, transparent), inset 0 1px 0 rgba(255,255,255,.45)`,
|
|
1002
|
+
textShadow: '0 1px 0 rgba(255,255,255,.28)',
|
|
639
1003
|
},
|
|
640
1004
|
}, save.status === 'saving' ? t('saving') : t('save')),
|
|
641
1005
|
save.status === 'ok'
|
|
642
|
-
? React.createElement('span', { key: 'ok', style: { fontSize:
|
|
1006
|
+
? React.createElement('span', { key: 'ok', style: { fontSize: 11.5, color: C.ok } }, save.message || t('saved'))
|
|
643
1007
|
: null,
|
|
644
1008
|
save.status === 'error'
|
|
645
1009
|
? React.createElement('span', {
|
|
646
1010
|
key: 'err',
|
|
647
|
-
style: { fontSize:
|
|
1011
|
+
style: { fontSize: 11.5, color: C.error, flex: '1 1 200px', whiteSpace: 'pre-wrap' },
|
|
648
1012
|
}, `${t('saveFailed')}${save.errors.join(';')}`)
|
|
649
1013
|
: null,
|
|
650
1014
|
]),
|
|
@@ -677,6 +1041,70 @@ function NiumaPanel(props) {
|
|
|
677
1041
|
const rates = snap && snap.rates ? snap.rates : computeRates(cfg, snap && snap.dateKey);
|
|
678
1042
|
const baseForm = React.useMemo(() => formFromConfig(cfg), [cfg]);
|
|
679
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* 「工资换算」视角:本月(跟随今日口径)/ 指定月份 / 全年平均。
|
|
1046
|
+
* 只影响换算展示,不影响顶部「今日已赚」计数器(计数器永远按今天算)。
|
|
1047
|
+
*/
|
|
1048
|
+
const viewYear = Number(String(snap && snap.dateKey ? snap.dateKey : '').slice(0, 4)) || new Date().getFullYear();
|
|
1049
|
+
const yearEstimated = viewYear === HOLIDAY_ESTIMATE_2027.year;
|
|
1050
|
+
const yearData = React.useMemo(() => yearSummary(cfg, viewYear), [cfg, viewYear]);
|
|
1051
|
+
const [view, setView] = React.useState('current');
|
|
1052
|
+
const selectedMonth = view.startsWith('m') ? yearData.months[Number(view.slice(1)) - 1] : null;
|
|
1053
|
+
const maxHolidayMonth = yearData.maxHolidayMonth;
|
|
1054
|
+
const maxHolidayText = t('maxHolidayNote')
|
|
1055
|
+
.replace('{month}', String(maxHolidayMonth.month))
|
|
1056
|
+
.replace('{days}', String(maxHolidayMonth.holidayDays));
|
|
1057
|
+
let viewCards;
|
|
1058
|
+
let viewHint;
|
|
1059
|
+
let viewNote;
|
|
1060
|
+
if (view === 'year') {
|
|
1061
|
+
// 与「本月」保持同一阅读顺序:秒 → 分 → 时 → 日,再放该视角的汇总项
|
|
1062
|
+
viewCards = [
|
|
1063
|
+
{ key: 'avgSecond', label: t('averageSecond'), value: money(yearData.averageSecondWage, 4), accent: true },
|
|
1064
|
+
{ key: 'avgMinute', label: t('averageMinute'), value: money(yearData.averageMinuteWage, 4) },
|
|
1065
|
+
{ key: 'avgHourly', label: t('averageHourly'), value: money(yearData.averageHourlyWage, 2) },
|
|
1066
|
+
{ key: 'avgDaily', label: t('averageDaily'), value: money(yearData.averageDailyWage, 2) },
|
|
1067
|
+
{ key: 'yearWorkdays', label: t('yearWorkdays'), value: `${yearData.totalWorkdays}${t('dayUnit')}` },
|
|
1068
|
+
{ key: 'yearIncome', label: t('yearIncome'), value: money(yearData.annualIncome, 2) },
|
|
1069
|
+
];
|
|
1070
|
+
viewHint = `${t('yearHint').replace('{year}', String(viewYear)).replace('{workdays}', String(yearData.totalWorkdays))} · ${t('viewOnlyHint')}`
|
|
1071
|
+
+ (yearEstimated ? ` · ${t('holidayEstimateShort')}` : '');
|
|
1072
|
+
viewNote = yearEstimated ? t('holidayEstimateNote') : maxHolidayText;
|
|
1073
|
+
} else if (selectedMonth) {
|
|
1074
|
+
// 与「本月」保持同一阅读顺序:秒 → 分 → 时 → 日 → 月 → 工作日
|
|
1075
|
+
viewCards = [
|
|
1076
|
+
{ key: 'secondWage', label: t('secondWage'), value: money(selectedMonth.secondWage, 4), accent: true },
|
|
1077
|
+
{ key: 'minuteWage', label: t('minuteWage'), value: money(selectedMonth.minuteWage, 4) },
|
|
1078
|
+
{ key: 'hourlyWage', label: t('hourlyWage'), value: money(selectedMonth.hourlyWage, 2) },
|
|
1079
|
+
{ key: 'dailyWage', label: t('dailyWage'), value: money(selectedMonth.dailyWage, 2) },
|
|
1080
|
+
{ key: 'monthlyWage', label: t('monthlyWage'), value: money(selectedMonth.monthlySalary, 2) },
|
|
1081
|
+
{ key: 'monthWorkdays', label: t('monthWorkdaysCard'), value: `${selectedMonth.workdays}${t('dayUnit')}` },
|
|
1082
|
+
];
|
|
1083
|
+
viewHint = `${t('monthHint')
|
|
1084
|
+
.replace('{year}', String(viewYear))
|
|
1085
|
+
.replace('{month}', String(selectedMonth.month))
|
|
1086
|
+
.replace('{workdays}', String(selectedMonth.workdays))
|
|
1087
|
+
.replace('{holidays}', String(selectedMonth.holidayDays))} · ${t('viewOnlyHint')}`
|
|
1088
|
+
+ (yearEstimated ? ` · ${t('holidayEstimateShort')}` : '');
|
|
1089
|
+
viewNote = yearEstimated
|
|
1090
|
+
? t('holidayEstimateNote')
|
|
1091
|
+
: (selectedMonth.month === maxHolidayMonth.month
|
|
1092
|
+
? `★ ${t('maxHolidayThis').replace('{days}', String(maxHolidayMonth.holidayDays))}`
|
|
1093
|
+
: maxHolidayText);
|
|
1094
|
+
} else {
|
|
1095
|
+
// 统一阅读顺序:秒 → 分 → 时 → 日 → 周 → 月(所有视角都从「秒」开始、从小到大)
|
|
1096
|
+
viewCards = RATE_CARDS.map((spec) => ({
|
|
1097
|
+
key: spec.key,
|
|
1098
|
+
label: t(spec.label),
|
|
1099
|
+
value: money(rates[spec.key], spec.digits),
|
|
1100
|
+
accent: spec.key === 'secondWage',
|
|
1101
|
+
}));
|
|
1102
|
+
viewHint = t('currentHint')
|
|
1103
|
+
.replace('{days}', String(rates.monthlyWorkdays))
|
|
1104
|
+
.replace('{hours}', trimNumber(rates.paidHours, 2));
|
|
1105
|
+
viewNote = maxHolidayText;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
680
1108
|
// 设置表单草稿:null = 还没改过,直接显示当前配置。
|
|
681
1109
|
const [draft, setDraft] = React.useState(null);
|
|
682
1110
|
const [save, setSave] = React.useState({ status: 'idle', errors: [], message: '' });
|
|
@@ -742,30 +1170,60 @@ function NiumaPanel(props) {
|
|
|
742
1170
|
|
|
743
1171
|
return React.createElement('div', {
|
|
744
1172
|
style: {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1173
|
+
// DSH 的 main slot 给的是定高容器:插件必须自己当滚动容器,否则展开设置后底部会被裁掉。
|
|
1174
|
+
// 注意:这里用 block + 内层 flex 的写法 —— 如果根直接 display:flex,子项会在定高容器里
|
|
1175
|
+
// 被 flex-shrink 压扁(展开设置时整张主卡高度被压缩、内容被 overflow 裁掉)。
|
|
1176
|
+
height: '100%',
|
|
1177
|
+
width: '100%',
|
|
1178
|
+
overflowY: 'auto',
|
|
1179
|
+
overflowX: 'hidden',
|
|
1180
|
+
overscrollBehavior: 'contain',
|
|
1181
|
+
scrollbarGutter: 'stable',
|
|
1182
|
+
boxSizing: 'border-box',
|
|
750
1183
|
fontSize: 13,
|
|
751
1184
|
lineHeight: 1.45,
|
|
752
1185
|
color: C.label,
|
|
753
1186
|
fontFamily: 'inherit',
|
|
754
1187
|
},
|
|
755
1188
|
}, [
|
|
756
|
-
|
|
1189
|
+
React.createElement(StyleTag, { key: 'css' }),
|
|
1190
|
+
React.createElement('div', {
|
|
1191
|
+
key: 'inner',
|
|
1192
|
+
style: {
|
|
1193
|
+
display: 'flex',
|
|
1194
|
+
flexDirection: 'column',
|
|
1195
|
+
gap: 11,
|
|
1196
|
+
width: '100%',
|
|
1197
|
+
maxWidth: 820,
|
|
1198
|
+
margin: '0 auto',
|
|
1199
|
+
padding: '16px 18px 40px',
|
|
1200
|
+
boxSizing: 'border-box',
|
|
1201
|
+
minWidth: 0,
|
|
1202
|
+
},
|
|
1203
|
+
}, [
|
|
1204
|
+
// 品牌头:图标块 + 标题/副标题 + 状态徽标
|
|
757
1205
|
React.createElement('div', {
|
|
758
1206
|
key: 'head',
|
|
759
|
-
style: { display: 'flex', alignItems: 'center', gap:
|
|
1207
|
+
style: { display: 'flex', alignItems: 'center', gap: 11, flexWrap: 'wrap' },
|
|
760
1208
|
}, [
|
|
1209
|
+
React.createElement('div', {
|
|
1210
|
+
key: 'icon',
|
|
1211
|
+
style: {
|
|
1212
|
+
width: 38, height: 38, borderRadius: 12, flex: '0 0 auto',
|
|
1213
|
+
display: 'grid', placeItems: 'center',
|
|
1214
|
+
color: C.brand,
|
|
1215
|
+
background: `color-mix(in srgb, ${C.brand} 13%, transparent)`,
|
|
1216
|
+
border: `1px solid color-mix(in srgb, ${C.brand} 26%, transparent)`,
|
|
1217
|
+
},
|
|
1218
|
+
}, React.createElement(NiumaIcon, { size: 21 })),
|
|
761
1219
|
React.createElement('div', { key: 'tt', style: { marginRight: 'auto', minWidth: 0 } }, [
|
|
762
1220
|
React.createElement('div', {
|
|
763
1221
|
key: 't',
|
|
764
|
-
style: { fontSize:
|
|
1222
|
+
style: { fontSize: 17.5, fontWeight: 800, letterSpacing: '-.3px', color: C.label },
|
|
765
1223
|
}, t('title')),
|
|
766
1224
|
React.createElement('div', {
|
|
767
1225
|
key: 's',
|
|
768
|
-
style: { fontSize: 11, color: C.label3 },
|
|
1226
|
+
style: { fontSize: 11, color: C.label3, marginTop: 1 },
|
|
769
1227
|
}, t('subtitle')),
|
|
770
1228
|
]),
|
|
771
1229
|
React.createElement(StatusBadge, { key: 'badge', t, status: snap.status }),
|
|
@@ -777,8 +1235,9 @@ function NiumaPanel(props) {
|
|
|
777
1235
|
key: 'banner',
|
|
778
1236
|
style: {
|
|
779
1237
|
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
|
780
|
-
fontSize: 12, color: banner.color, padding: '
|
|
781
|
-
borderRadius:
|
|
1238
|
+
fontSize: 12, color: banner.color, padding: '6px 10px',
|
|
1239
|
+
borderRadius: 9, border: `1px solid color-mix(in srgb, ${banner.color} 45%, transparent)`,
|
|
1240
|
+
background: `color-mix(in srgb, ${banner.color} 10%, transparent)`,
|
|
782
1241
|
},
|
|
783
1242
|
}, [
|
|
784
1243
|
React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } }, banner.text),
|
|
@@ -797,9 +1256,57 @@ function NiumaPanel(props) {
|
|
|
797
1256
|
: null,
|
|
798
1257
|
|
|
799
1258
|
React.createElement(HeroCard, { key: 'hero', t, snap }),
|
|
800
|
-
React.createElement(
|
|
801
|
-
React.createElement(
|
|
1259
|
+
React.createElement(CountdownRow, { key: 'countdown', t, snap }),
|
|
1260
|
+
React.createElement('div', {
|
|
1261
|
+
key: 'ratesHead',
|
|
1262
|
+
style: {
|
|
1263
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
1264
|
+
gap: 8, flexWrap: 'wrap', margin: '2px 1px -1px',
|
|
1265
|
+
},
|
|
1266
|
+
}, [
|
|
1267
|
+
React.createElement('span', {
|
|
1268
|
+
key: 't',
|
|
1269
|
+
style: { fontSize: 12.5, fontWeight: 700, color: C.label2, letterSpacing: '.2px' },
|
|
1270
|
+
}, t('sectionRates')),
|
|
1271
|
+
React.createElement(PeriodSelect, {
|
|
1272
|
+
key: 'period',
|
|
1273
|
+
t,
|
|
1274
|
+
value: view,
|
|
1275
|
+
onChange: setView,
|
|
1276
|
+
year: viewYear,
|
|
1277
|
+
months: yearData.months,
|
|
1278
|
+
}),
|
|
1279
|
+
]),
|
|
1280
|
+
React.createElement('div', {
|
|
1281
|
+
key: 'ratesHint',
|
|
1282
|
+
style: { fontSize: 10.5, color: C.label3, ...NUM },
|
|
1283
|
+
}, viewHint),
|
|
1284
|
+
React.createElement(RatesGrid, { key: 'rates', cards: viewCards }),
|
|
1285
|
+
viewNote
|
|
1286
|
+
? React.createElement('div', {
|
|
1287
|
+
key: 'ratesNote',
|
|
1288
|
+
style: { fontSize: 10.5, color: C.label3, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
1289
|
+
}, [
|
|
1290
|
+
React.createElement('span', {
|
|
1291
|
+
key: 'badge',
|
|
1292
|
+
style: {
|
|
1293
|
+
color: C.label2, fontWeight: 600,
|
|
1294
|
+
background: `color-mix(in srgb, ${GOLD_TINT} 14%, transparent)`,
|
|
1295
|
+
border: `1px solid color-mix(in srgb, ${GOLD_TINT} 32%, transparent)`,
|
|
1296
|
+
borderRadius: 999, padding: '1px 8px',
|
|
1297
|
+
},
|
|
1298
|
+
}, t('maxHolidayBadge')),
|
|
1299
|
+
React.createElement('span', { key: 'txt' }, viewNote),
|
|
1300
|
+
])
|
|
1301
|
+
: null,
|
|
1302
|
+
React.createElement(SectionTitle, { key: 'detailTitle', title: t('sectionDetail') }),
|
|
802
1303
|
React.createElement(DetailsFooter, { key: 'footer', t, snap, holidaySource }),
|
|
1304
|
+
React.createElement(HolidaysCard, {
|
|
1305
|
+
key: 'holidays',
|
|
1306
|
+
t,
|
|
1307
|
+
holidaySource,
|
|
1308
|
+
extraWorkdays: cfg.extraWorkdays,
|
|
1309
|
+
}),
|
|
803
1310
|
React.createElement(SettingsSection, {
|
|
804
1311
|
key: 'settings',
|
|
805
1312
|
t,
|
|
@@ -809,6 +1316,7 @@ function NiumaPanel(props) {
|
|
|
809
1316
|
save,
|
|
810
1317
|
onSave,
|
|
811
1318
|
}),
|
|
1319
|
+
]),
|
|
812
1320
|
]);
|
|
813
1321
|
}
|
|
814
1322
|
|