@c2-markdown-ui/svelte 0.0.0-stage → 0.4.3
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/README.md +91 -2
- package/package.json +36 -4
- package/src/lib/MarkdownUI.svelte +60 -0
- package/src/lib/Navbar.svelte +144 -0
- package/src/lib/WidgetEvents.svelte +139 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/widgets/Button.svelte +30 -0
- package/src/lib/widgets/ButtonGroup.svelte +37 -0
- package/src/lib/widgets/Cascader.svelte +486 -0
- package/src/lib/widgets/Chart.svelte +278 -0
- package/src/lib/widgets/DatePicker.svelte +609 -0
- package/src/lib/widgets/Form.svelte +571 -0
- package/src/lib/widgets/Incomplete.svelte +72 -0
- package/src/lib/widgets/MultipleChoiceQuestion.svelte +65 -0
- package/src/lib/widgets/NumberInput.svelte +149 -0
- package/src/lib/widgets/Quiz.svelte +301 -0
- package/src/lib/widgets/Radio.svelte +180 -0
- package/src/lib/widgets/Row.svelte +140 -0
- package/src/lib/widgets/Select.svelte +276 -0
- package/src/lib/widgets/SelectMulti.svelte +130 -0
- package/src/lib/widgets/ShortAnswerQuestion.svelte +57 -0
- package/src/lib/widgets/Slider.svelte +46 -0
- package/src/lib/widgets/Switch.svelte +60 -0
- package/src/lib/widgets/Text.svelte +58 -0
- package/src/lib/widgets/TextInput.svelte +58 -0
- package/src/lib/widgets/TreeSelect.svelte +941 -0
- package/src/lib/widgets/Widget.svelte +90 -0
- package/src/lib/widgets/chartRemoteData.ts +84 -0
- package/src/lib/widgets/index.ts +23 -0
- package/src/lib/widgets/remoteChoices.ts +90 -0
- package/src/lib/widgets.css +1754 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
label?: string;
|
|
4
|
+
/** 选择模式:date 仅日期(默认)/ datetime 日期+时间 */
|
|
5
|
+
picker?: 'date' | 'datetime';
|
|
6
|
+
/** 范围选择(antd RangePicker 效果:双月历面板) */
|
|
7
|
+
range?: boolean;
|
|
8
|
+
/** 默认值:时间戳(毫秒)或格式化时间字符串;range 模式为 [start, end] */
|
|
9
|
+
default?: number | string | (number | string)[];
|
|
10
|
+
/** 显示格式:默认按模式推导——date 为 YYYY-MM-DD,datetime 为 YYYY-MM-DD HH:mm:ss */
|
|
11
|
+
format?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** 禁用当前日期之前的日期 */
|
|
14
|
+
disableBeforeNow?: boolean;
|
|
15
|
+
/** 禁用当前日期之后的日期 */
|
|
16
|
+
disableAfterNow?: boolean;
|
|
17
|
+
hint?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/** 校验失败提示文案 */
|
|
20
|
+
message?: string;
|
|
21
|
+
/** 外部(form 提交时)传入的错误信息 */
|
|
22
|
+
error?: string;
|
|
23
|
+
/** 是否禁用(整组件不可交互,区别于 disableBeforeNow/disableAfterNow 的部分日期禁用) */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
onchange: (value: number | number[] | null) => void;
|
|
26
|
+
}
|
|
27
|
+
let {
|
|
28
|
+
label = '',
|
|
29
|
+
picker = 'date',
|
|
30
|
+
range = false,
|
|
31
|
+
default: initial,
|
|
32
|
+
format,
|
|
33
|
+
placeholder = '',
|
|
34
|
+
disableBeforeNow = false,
|
|
35
|
+
disableAfterNow = false,
|
|
36
|
+
hint = '',
|
|
37
|
+
required = false,
|
|
38
|
+
message = '',
|
|
39
|
+
error = '',
|
|
40
|
+
disabled = false,
|
|
41
|
+
onchange
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const showTime = picker === 'datetime';
|
|
45
|
+
// 实际生效的格式:默认值按模式推导——datetime 为 YYYY-MM-DD HH:mm:ss,date 为 YYYY-MM-DD
|
|
46
|
+
const effFormat = $derived(format ?? (showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'));
|
|
47
|
+
|
|
48
|
+
// ---------- 时间解析/格式化工具 ----------
|
|
49
|
+
function parseDateLike(v: number | string | undefined | null): number | null {
|
|
50
|
+
if (v === undefined || v === null || v === '') return null;
|
|
51
|
+
if (typeof v === 'number') return v;
|
|
52
|
+
const s = String(v).trim();
|
|
53
|
+
if (/^-?\d+$/.test(s)) return Number(s);
|
|
54
|
+
// 支持 2026-08-20 18:20:06 / 2026/05/11 18:20:06 / 2026-08-20 / 2026/05/11
|
|
55
|
+
const m = s.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
|
|
56
|
+
if (m) {
|
|
57
|
+
return new Date(
|
|
58
|
+
Number(m[1]), Number(m[2]) - 1, Number(m[3]),
|
|
59
|
+
Number(m[4] ?? 0), Number(m[5] ?? 0), Number(m[6] ?? 0)
|
|
60
|
+
).getTime();
|
|
61
|
+
}
|
|
62
|
+
const t = Date.parse(s);
|
|
63
|
+
return isNaN(t) ? null : t;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pad(n: number): string { return String(n).padStart(2, '0'); }
|
|
67
|
+
|
|
68
|
+
function formatDate(ts: number): string {
|
|
69
|
+
const d = new Date(ts);
|
|
70
|
+
return effFormat
|
|
71
|
+
.replace(/YYYY/g, String(d.getFullYear()))
|
|
72
|
+
.replace(/MM/g, pad(d.getMonth() + 1))
|
|
73
|
+
.replace(/DD/g, pad(d.getDate()))
|
|
74
|
+
.replace(/HH/g, pad(d.getHours()))
|
|
75
|
+
.replace(/mm/g, pad(d.getMinutes()))
|
|
76
|
+
.replace(/ss/g, pad(d.getSeconds()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type TimeState = { h: number; m: number; s: number };
|
|
80
|
+
|
|
81
|
+
// ---------- 状态 ----------
|
|
82
|
+
// 单选:选中的时间戳;范围:[start, end]
|
|
83
|
+
let selected = $state<number | null>(null);
|
|
84
|
+
let rangeStart = $state<number | null>(null);
|
|
85
|
+
let rangeEnd = $state<number | null>(null);
|
|
86
|
+
|
|
87
|
+
// 初始化 default
|
|
88
|
+
if (initial !== undefined && initial !== null) {
|
|
89
|
+
if (range && Array.isArray(initial) && initial.length >= 2) {
|
|
90
|
+
rangeStart = parseDateLike(initial[0]);
|
|
91
|
+
rangeEnd = parseDateLike(initial[1]);
|
|
92
|
+
} else if (range && !Array.isArray(initial)) {
|
|
93
|
+
rangeStart = parseDateLike(initial as any);
|
|
94
|
+
} else if (!range) {
|
|
95
|
+
selected = parseDateLike(Array.isArray(initial) ? initial[0] : (initial as any));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 面板开关与当前展示月份(range 模式为左侧面板的月份,右侧自动 +1)
|
|
100
|
+
let open = $state(false);
|
|
101
|
+
let viewYear = $state(new Date().getFullYear());
|
|
102
|
+
let viewMonth = $state(new Date().getMonth()); // 0-11
|
|
103
|
+
// datetime 模式下正在编辑的时间(单选一组;范围选择开始/结束各一组)
|
|
104
|
+
let tSingle = $state<TimeState>({ h: 0, m: 0, s: 0 });
|
|
105
|
+
let tStart = $state<TimeState>({ h: 0, m: 0, s: 0 });
|
|
106
|
+
let tEnd = $state<TimeState>({ h: 0, m: 0, s: 0 });
|
|
107
|
+
// range 模式:第二次点击为结束日期;hover 预览
|
|
108
|
+
let pickingEnd = $state(false);
|
|
109
|
+
let hoverTs = $state<number | null>(null);
|
|
110
|
+
|
|
111
|
+
let rootEl: HTMLElement | undefined = $state();
|
|
112
|
+
|
|
113
|
+
$effect(() => {
|
|
114
|
+
function onDocClick(e: MouseEvent) {
|
|
115
|
+
if (rootEl && !rootEl.contains(e.target as Node)) open = false;
|
|
116
|
+
}
|
|
117
|
+
document.addEventListener('mousedown', onDocClick);
|
|
118
|
+
return () => document.removeEventListener('mousedown', onDocClick);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function assignTime(t: TimeState, ts: number) {
|
|
122
|
+
const d = new Date(ts);
|
|
123
|
+
t.h = d.getHours();
|
|
124
|
+
t.m = d.getMinutes();
|
|
125
|
+
t.s = d.getSeconds();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 打开面板时同步视图到已有值
|
|
129
|
+
function syncView() {
|
|
130
|
+
const ref = range ? (rangeStart ?? rangeEnd) : selected;
|
|
131
|
+
const d = ref !== null ? new Date(ref) : new Date();
|
|
132
|
+
viewYear = d.getFullYear();
|
|
133
|
+
viewMonth = d.getMonth();
|
|
134
|
+
if (range) {
|
|
135
|
+
if (rangeStart !== null) assignTime(tStart, rangeStart);
|
|
136
|
+
if (rangeEnd !== null) assignTime(tEnd, rangeEnd);
|
|
137
|
+
} else if (selected !== null) {
|
|
138
|
+
assignTime(tSingle, selected);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toggleOpen() {
|
|
143
|
+
if (disabled) return;
|
|
144
|
+
open = !open;
|
|
145
|
+
if (open) { pickingEnd = false; syncView(); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------- 日历格子 ----------
|
|
149
|
+
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日'];
|
|
150
|
+
|
|
151
|
+
type Cell = { ts: number; day: number; inMonth: boolean; disabled: boolean };
|
|
152
|
+
|
|
153
|
+
function makeCells(year: number, month: number): Cell[] {
|
|
154
|
+
const first = new Date(year, month, 1);
|
|
155
|
+
// 周一为一周起点
|
|
156
|
+
const startOffset = (first.getDay() + 6) % 7;
|
|
157
|
+
const gridStart = new Date(year, month, 1 - startOffset);
|
|
158
|
+
const list: Cell[] = [];
|
|
159
|
+
const now = new Date();
|
|
160
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
161
|
+
for (let i = 0; i < 42; i++) {
|
|
162
|
+
const d = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
|
|
163
|
+
const ts = d.getTime();
|
|
164
|
+
let disabled = false;
|
|
165
|
+
if (disableBeforeNow && ts < todayStart) disabled = true;
|
|
166
|
+
if (disableAfterNow && ts > todayStart) disabled = true;
|
|
167
|
+
list.push({ ts, day: d.getDate(), inMonth: d.getMonth() === month, disabled });
|
|
168
|
+
}
|
|
169
|
+
return list;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function startOfDay(ts: number): number {
|
|
173
|
+
const d = new Date(ts);
|
|
174
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 单选 / range 左侧面板
|
|
178
|
+
const leftCells = $derived(makeCells(viewYear, viewMonth));
|
|
179
|
+
const leftTitle = $derived(`${viewMonth + 1}月`);
|
|
180
|
+
const leftYearTitle = $derived(`${viewYear}年`);
|
|
181
|
+
// range 右侧面板(右侧月份 = 左侧 + 1)
|
|
182
|
+
const rightYear = $derived(viewMonth === 11 ? viewYear + 1 : viewYear);
|
|
183
|
+
const rightMonth = $derived(viewMonth === 11 ? 0 : viewMonth + 1);
|
|
184
|
+
const rightCells = $derived(makeCells(rightYear, rightMonth));
|
|
185
|
+
const rightTitle = $derived(`${rightMonth + 1}月`);
|
|
186
|
+
const rightYearTitle = $derived(`${rightYear}年`);
|
|
187
|
+
|
|
188
|
+
function prevMonth() {
|
|
189
|
+
if (viewMonth === 0) { viewMonth = 11; viewYear--; } else viewMonth--;
|
|
190
|
+
}
|
|
191
|
+
function nextMonth() {
|
|
192
|
+
if (viewMonth === 11) { viewMonth = 0; viewYear++; } else viewMonth++;
|
|
193
|
+
}
|
|
194
|
+
function prevYear() { viewYear--; }
|
|
195
|
+
function nextYear() { viewYear++; }
|
|
196
|
+
|
|
197
|
+
// ---------- 选中逻辑 ----------
|
|
198
|
+
function applyTime(dayTs: number, t: TimeState): number {
|
|
199
|
+
if (!showTime) return new Date(dayTs).setHours(0, 0, 0, 0);
|
|
200
|
+
return new Date(dayTs).setHours(t.h, t.m, t.s, 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function emit() {
|
|
204
|
+
if (range) {
|
|
205
|
+
onchange(rangeStart !== null && rangeEnd !== null ? [rangeStart, rangeEnd] : []);
|
|
206
|
+
} else {
|
|
207
|
+
onchange(selected);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pickCell(cell: Cell) {
|
|
212
|
+
if (cell.disabled) return;
|
|
213
|
+
if (range) {
|
|
214
|
+
if (!pickingEnd || rangeStart === null || cell.ts < startOfDay(rangeStart)) {
|
|
215
|
+
// 第一次点击,或点击了早于当前开始日期的格子 → (重新)选择开始
|
|
216
|
+
rangeStart = applyTime(cell.ts, tStart);
|
|
217
|
+
rangeEnd = null;
|
|
218
|
+
pickingEnd = true;
|
|
219
|
+
} else {
|
|
220
|
+
rangeEnd = applyTime(cell.ts, tEnd);
|
|
221
|
+
pickingEnd = false;
|
|
222
|
+
// datetime 模式保持面板打开,等时间输入 + 确定;仅日期模式直接完成
|
|
223
|
+
if (!showTime) open = false;
|
|
224
|
+
emit();
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
selected = applyTime(cell.ts, tSingle);
|
|
228
|
+
if (!showTime) open = false;
|
|
229
|
+
emit();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setTime(side: 'single' | 'start' | 'end', part: 'h' | 'm' | 's', ev: Event) {
|
|
234
|
+
const input = ev.target as HTMLInputElement;
|
|
235
|
+
const n = parseInt(input.value, 10);
|
|
236
|
+
if (isNaN(n)) return;
|
|
237
|
+
const max = part === 'h' ? 23 : 59;
|
|
238
|
+
const v = Math.min(max, Math.max(0, n));
|
|
239
|
+
const t = side === 'single' ? tSingle : side === 'start' ? tStart : tEnd;
|
|
240
|
+
t[part] = v;
|
|
241
|
+
// 已有选中值时实时更新
|
|
242
|
+
if (side === 'single' && selected !== null) { selected = applyTime(selected, tSingle); emit(); }
|
|
243
|
+
if (side === 'start' && rangeStart !== null) { rangeStart = applyTime(rangeStart, tStart); if (rangeEnd !== null) emit(); }
|
|
244
|
+
if (side === 'end' && rangeEnd !== null) { rangeEnd = applyTime(rangeEnd, tEnd); if (rangeStart !== null) emit(); }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function clearValue() {
|
|
248
|
+
selected = null;
|
|
249
|
+
rangeStart = null;
|
|
250
|
+
rangeEnd = null;
|
|
251
|
+
pickingEnd = false;
|
|
252
|
+
onchange(range ? [] : null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// datetime 模式:应用时间输入并确认
|
|
256
|
+
function confirmTime() {
|
|
257
|
+
if (range) {
|
|
258
|
+
if (rangeStart !== null) rangeStart = applyTime(rangeStart, tStart);
|
|
259
|
+
if (rangeEnd !== null) rangeEnd = applyTime(rangeEnd, tEnd);
|
|
260
|
+
emit();
|
|
261
|
+
} else if (selected !== null) {
|
|
262
|
+
selected = applyTime(selected, tSingle);
|
|
263
|
+
emit();
|
|
264
|
+
}
|
|
265
|
+
open = false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------- 展示文案 ----------
|
|
269
|
+
const startText = $derived(rangeStart !== null ? formatDate(rangeStart) : '');
|
|
270
|
+
const endText = $derived(rangeEnd !== null ? formatDate(rangeEnd) : '');
|
|
271
|
+
|
|
272
|
+
// 必填校验错误文案(外部 form 提交错误优先)
|
|
273
|
+
const shownError = $derived.by(() => {
|
|
274
|
+
if (!error) return '';
|
|
275
|
+
return error;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 格子状态 class
|
|
279
|
+
function cellCls(c: Cell): string {
|
|
280
|
+
const day = startOfDay(c.ts);
|
|
281
|
+
let cls = 'dp-cell';
|
|
282
|
+
if (!c.inMonth) cls += ' dp-cell-out';
|
|
283
|
+
if (c.disabled) {
|
|
284
|
+
cls += ' dp-cell-disabled';
|
|
285
|
+
return cls;
|
|
286
|
+
}
|
|
287
|
+
const todayTs = startOfDay(Date.now());
|
|
288
|
+
if (day === todayTs) cls += ' dp-cell-today';
|
|
289
|
+
if (range && rangeStart !== null) {
|
|
290
|
+
const s = startOfDay(rangeStart);
|
|
291
|
+
// 已有结束日期用真实值;正在选结束时用 hover 位置预览(支持反向 hover)
|
|
292
|
+
const e = rangeEnd !== null ? startOfDay(rangeEnd) : (pickingEnd && hoverTs !== null ? startOfDay(hoverTs) : null);
|
|
293
|
+
if (e !== null) {
|
|
294
|
+
const lo = Math.min(s, e);
|
|
295
|
+
const hi = Math.max(s, e);
|
|
296
|
+
if (day === lo) cls += ' dp-cell-range-start';
|
|
297
|
+
if (day === hi) cls += ' dp-cell-range-end';
|
|
298
|
+
if (day > lo && day < hi) cls += ' dp-cell-in-range';
|
|
299
|
+
} else if (day === s) {
|
|
300
|
+
cls += ' dp-cell-range-start';
|
|
301
|
+
}
|
|
302
|
+
} else if (selected !== null && day === startOfDay(selected)) {
|
|
303
|
+
cls += ' dp-cell-selected';
|
|
304
|
+
}
|
|
305
|
+
return cls;
|
|
306
|
+
}
|
|
307
|
+
</script>
|
|
308
|
+
|
|
309
|
+
<div class="selector widget-date-picker" bind:this={rootEl}>
|
|
310
|
+
{#if label || hint || required}
|
|
311
|
+
<div class="widget-label-row">
|
|
312
|
+
{#if required}<span class="widget-required-mark" aria-hidden="true">*</span>{/if}
|
|
313
|
+
{#if label}<label>{label}</label>{/if}
|
|
314
|
+
{#if hint}<span class="widget-hint" data-hint={hint}>ⓘ</span>{/if}
|
|
315
|
+
</div>
|
|
316
|
+
{/if}
|
|
317
|
+
<div class="widget-field-content">
|
|
318
|
+
<button type="button" class="dp-input" class:widget-input-error={!!shownError} onclick={toggleOpen} aria-expanded={open} {disabled}>
|
|
319
|
+
<span class="dp-input-icon" aria-hidden="true">📅</span>
|
|
320
|
+
{#if range}
|
|
321
|
+
<span class="dp-input-text dp-seg" class:dp-placeholder={!startText}>{startText || placeholder || (showTime ? '开始时间' : '开始日期')}</span>
|
|
322
|
+
<span class="dp-sep" aria-hidden="true">→</span>
|
|
323
|
+
<span class="dp-input-text dp-seg" class:dp-placeholder={!endText}>{endText || (showTime ? '结束时间' : '结束日期')}</span>
|
|
324
|
+
{:else}
|
|
325
|
+
<span class="dp-input-text" class:dp-placeholder={!startText && selected === null}>{selected !== null ? formatDate(selected) : placeholder || '请选择日期'}</span>
|
|
326
|
+
{/if}
|
|
327
|
+
{#if !disabled && (range ? (startText || endText) : selected !== null)}
|
|
328
|
+
<span class="dp-clear" role="button" tabindex="-1" aria-label="清空"
|
|
329
|
+
onclick={(e) => { e.stopPropagation(); clearValue(); open = false; }}>×</span>
|
|
330
|
+
{/if}
|
|
331
|
+
</button>
|
|
332
|
+
{#if shownError}<div class="widget-error-text" role="alert">{shownError}</div>{/if}
|
|
333
|
+
|
|
334
|
+
{#snippet calPanel(cellsList: Cell[], title: string, yearTitle: string, nav: 'prev' | 'next' | 'both')}
|
|
335
|
+
<div class="dp-calendar">
|
|
336
|
+
<div class="dp-header">
|
|
337
|
+
<div class="dp-header-nav">
|
|
338
|
+
{#if nav === 'prev' || nav === 'both'}
|
|
339
|
+
<button type="button" class="dp-nav" onclick={prevYear} aria-label="上一年">«</button>
|
|
340
|
+
<button type="button" class="dp-nav" onclick={prevMonth} aria-label="上个月">‹</button>
|
|
341
|
+
{/if}
|
|
342
|
+
</div>
|
|
343
|
+
<span class="dp-title">
|
|
344
|
+
<span class="dp-title-year">{yearTitle}</span>
|
|
345
|
+
{title}
|
|
346
|
+
</span>
|
|
347
|
+
<div class="dp-header-nav">
|
|
348
|
+
{#if nav === 'next' || nav === 'both'}
|
|
349
|
+
<button type="button" class="dp-nav" onclick={nextMonth} aria-label="下个月">›</button>
|
|
350
|
+
<button type="button" class="dp-nav" onclick={nextYear} aria-label="下一年">»</button>
|
|
351
|
+
{/if}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="dp-week-header">
|
|
355
|
+
{#each WEEK_LABELS as w}<span class="dp-week">{w}</span>{/each}
|
|
356
|
+
</div>
|
|
357
|
+
<div class="dp-grid" onmouseleave={() => (hoverTs = null)}>
|
|
358
|
+
{#each cellsList as c (c.ts)}
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
class={cellCls(c)}
|
|
362
|
+
onclick={() => pickCell(c)}
|
|
363
|
+
onmouseover={() => (hoverTs = c.ts)}
|
|
364
|
+
disabled={c.disabled}
|
|
365
|
+
>{c.day}</button>
|
|
366
|
+
{/each}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
{/snippet}
|
|
370
|
+
|
|
371
|
+
{#if open}
|
|
372
|
+
<div class="dp-panel" class:dp-range={range}>
|
|
373
|
+
{#if range}
|
|
374
|
+
<div class="dp-panels">
|
|
375
|
+
{@render calPanel(leftCells, leftTitle, leftYearTitle, 'prev')}
|
|
376
|
+
{@render calPanel(rightCells, rightTitle, rightYearTitle, 'next')}
|
|
377
|
+
</div>
|
|
378
|
+
{:else}
|
|
379
|
+
{@render calPanel(leftCells, leftTitle, leftYearTitle, 'both')}
|
|
380
|
+
{/if}
|
|
381
|
+
|
|
382
|
+
{#if showTime}
|
|
383
|
+
<div class="dp-time">
|
|
384
|
+
{#if range}
|
|
385
|
+
<div class="dp-time-groups">
|
|
386
|
+
<div class="dp-time-group">
|
|
387
|
+
<span class="dp-time-label">开始</span>
|
|
388
|
+
<input class="dp-time-input" type="number" min="0" max="23" value={tStart.h} oninput={(e) => setTime('start', 'h', e)} aria-label="开始 时" />
|
|
389
|
+
<span class="dp-time-sep">:</span>
|
|
390
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tStart.m} oninput={(e) => setTime('start', 'm', e)} aria-label="开始 分" />
|
|
391
|
+
<span class="dp-time-sep">:</span>
|
|
392
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tStart.s} oninput={(e) => setTime('start', 's', e)} aria-label="开始 秒" />
|
|
393
|
+
</div>
|
|
394
|
+
<span class="dp-time-arrow" aria-hidden="true">~</span>
|
|
395
|
+
<div class="dp-time-group">
|
|
396
|
+
<span class="dp-time-label">结束</span>
|
|
397
|
+
<input class="dp-time-input" type="number" min="0" max="23" value={tEnd.h} oninput={(e) => setTime('end', 'h', e)} aria-label="结束 时" />
|
|
398
|
+
<span class="dp-time-sep">:</span>
|
|
399
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tEnd.m} oninput={(e) => setTime('end', 'm', e)} aria-label="结束 分" />
|
|
400
|
+
<span class="dp-time-sep">:</span>
|
|
401
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tEnd.s} oninput={(e) => setTime('end', 's', e)} aria-label="结束 秒" />
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="dp-footer">
|
|
405
|
+
<button type="button" class="dp-ok" onclick={confirmTime} disabled={rangeStart === null}>确定</button>
|
|
406
|
+
</div>
|
|
407
|
+
{:else}
|
|
408
|
+
<div class="dp-time-group">
|
|
409
|
+
<input class="dp-time-input" type="number" min="0" max="23" value={tSingle.h} oninput={(e) => setTime('single', 'h', e)} aria-label="时" />
|
|
410
|
+
<span class="dp-time-sep">:</span>
|
|
411
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tSingle.m} oninput={(e) => setTime('single', 'm', e)} aria-label="分" />
|
|
412
|
+
<span class="dp-time-sep">:</span>
|
|
413
|
+
<input class="dp-time-input" type="number" min="0" max="59" value={tSingle.s} oninput={(e) => setTime('single', 's', e)} aria-label="秒" />
|
|
414
|
+
<button type="button" class="dp-ok" onclick={confirmTime} disabled={selected === null}>确定</button>
|
|
415
|
+
</div>
|
|
416
|
+
{/if}
|
|
417
|
+
</div>
|
|
418
|
+
{/if}
|
|
419
|
+
</div>
|
|
420
|
+
{/if}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<style>
|
|
425
|
+
.widget-date-picker {
|
|
426
|
+
position: relative;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.dp-input {
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
gap: 0.375rem;
|
|
433
|
+
width: 100%;
|
|
434
|
+
padding: 0.625rem 0.75rem;
|
|
435
|
+
border: 1px solid #d9d9d9;
|
|
436
|
+
border-radius: 6px;
|
|
437
|
+
font-size: 0.875rem;
|
|
438
|
+
background-color: #ffffff;
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
441
|
+
text-align: left;
|
|
442
|
+
}
|
|
443
|
+
.dp-input:hover { border-color: #4096ff; }
|
|
444
|
+
.dp-input:focus-within,
|
|
445
|
+
.dp-input:focus {
|
|
446
|
+
outline: none;
|
|
447
|
+
border-color: #1677ff;
|
|
448
|
+
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
|
449
|
+
}
|
|
450
|
+
/* 禁用态:置灰 + 禁止交互 */
|
|
451
|
+
.dp-input:disabled {
|
|
452
|
+
background-color: #f5f5f5;
|
|
453
|
+
color: rgba(0, 0, 0, 0.25);
|
|
454
|
+
border-color: #d9d9d9;
|
|
455
|
+
cursor: not-allowed;
|
|
456
|
+
}
|
|
457
|
+
.dp-input:disabled:hover { border-color: #d9d9d9; }
|
|
458
|
+
.dp-input-icon { font-size: 0.875rem; }
|
|
459
|
+
.dp-input-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
460
|
+
.dp-seg { flex: 1 1 0; min-width: 0; }
|
|
461
|
+
.dp-sep { color: rgba(0, 0, 0, 0.45); font-size: 0.75rem; }
|
|
462
|
+
.dp-placeholder { color: rgba(0, 0, 0, 0.45); }
|
|
463
|
+
.dp-clear {
|
|
464
|
+
padding: 0 0.25rem;
|
|
465
|
+
color: rgba(0, 0, 0, 0.45);
|
|
466
|
+
font-size: 1rem;
|
|
467
|
+
line-height: 1;
|
|
468
|
+
}
|
|
469
|
+
.dp-clear:hover { color: rgba(0, 0, 0, 0.88); }
|
|
470
|
+
|
|
471
|
+
.dp-panel {
|
|
472
|
+
position: absolute;
|
|
473
|
+
z-index: 50;
|
|
474
|
+
top: calc(100% + 4px);
|
|
475
|
+
left: 0;
|
|
476
|
+
width: 288px;
|
|
477
|
+
padding: 0.75rem;
|
|
478
|
+
background: #ffffff;
|
|
479
|
+
border: 1px solid #d9d9d9;
|
|
480
|
+
border-radius: 8px;
|
|
481
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
482
|
+
}
|
|
483
|
+
/* range 模式:双月历并排,宽度自适应 */
|
|
484
|
+
.dp-panel.dp-range { width: auto; }
|
|
485
|
+
.dp-panels { display: flex; gap: 0.75rem; }
|
|
486
|
+
.dp-calendar { width: 264px; }
|
|
487
|
+
|
|
488
|
+
.dp-header {
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
justify-content: space-between;
|
|
492
|
+
margin-bottom: 0.5rem;
|
|
493
|
+
}
|
|
494
|
+
.dp-title { font-weight: 600; font-size: 0.875rem; color: rgba(0, 0, 0, 0.88); }
|
|
495
|
+
/* antd 风格:年份弱化、月份强调 */
|
|
496
|
+
.dp-title-year { font-weight: 400; color: rgba(0, 0, 0, 0.45); margin-right: 0.25rem; }
|
|
497
|
+
.dp-header-nav { display: flex; align-items: center; gap: 0.125rem; min-width: 3.25rem; }
|
|
498
|
+
.dp-header-nav:last-child { justify-content: flex-end; }
|
|
499
|
+
.dp-nav {
|
|
500
|
+
width: 1.5rem;
|
|
501
|
+
height: 1.5rem;
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
justify-content: center;
|
|
505
|
+
border: none;
|
|
506
|
+
background: transparent;
|
|
507
|
+
color: rgba(0, 0, 0, 0.65);
|
|
508
|
+
font-size: 1.1rem;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
border-radius: 4px;
|
|
511
|
+
}
|
|
512
|
+
.dp-nav:hover { background: #f5f5f5; color: rgba(0, 0, 0, 0.88); }
|
|
513
|
+
|
|
514
|
+
.dp-week-header {
|
|
515
|
+
display: grid;
|
|
516
|
+
grid-template-columns: repeat(7, 1fr);
|
|
517
|
+
margin-bottom: 0.25rem;
|
|
518
|
+
}
|
|
519
|
+
.dp-week {
|
|
520
|
+
text-align: center;
|
|
521
|
+
font-size: 0.75rem;
|
|
522
|
+
color: rgba(0, 0, 0, 0.45);
|
|
523
|
+
padding: 0.25rem 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.dp-grid {
|
|
527
|
+
display: grid;
|
|
528
|
+
grid-template-columns: repeat(7, 1fr);
|
|
529
|
+
gap: 2px;
|
|
530
|
+
}
|
|
531
|
+
.dp-cell {
|
|
532
|
+
position: relative;
|
|
533
|
+
height: 2rem;
|
|
534
|
+
border: none;
|
|
535
|
+
background: transparent;
|
|
536
|
+
border-radius: 4px;
|
|
537
|
+
font-size: 0.8125rem;
|
|
538
|
+
color: rgba(0, 0, 0, 0.88);
|
|
539
|
+
cursor: pointer;
|
|
540
|
+
transition: background 0.15s ease;
|
|
541
|
+
}
|
|
542
|
+
.dp-cell:hover:not(.dp-cell-disabled) { background: #f5f5f5; }
|
|
543
|
+
.dp-cell-out { color: rgba(0, 0, 0, 0.25); }
|
|
544
|
+
.dp-cell-disabled { color: rgba(0, 0, 0, 0.25); cursor: not-allowed; }
|
|
545
|
+
.dp-cell-today { color: #1677ff; font-weight: 600; }
|
|
546
|
+
.dp-cell-selected,
|
|
547
|
+
.dp-cell-range-start,
|
|
548
|
+
.dp-cell-range-end {
|
|
549
|
+
background: #1677ff;
|
|
550
|
+
color: #ffffff;
|
|
551
|
+
font-weight: 600;
|
|
552
|
+
}
|
|
553
|
+
/* antd 风格:区间首尾胶囊圆角 */
|
|
554
|
+
.dp-cell-range-start { border-radius: 4px 0 0 4px; }
|
|
555
|
+
.dp-cell-range-end { border-radius: 0 4px 4px 0; }
|
|
556
|
+
.dp-cell-range-start.dp-cell-range-end { border-radius: 4px; }
|
|
557
|
+
.dp-cell-in-range { background: #e6f4ff; color: #1677ff; }
|
|
558
|
+
.dp-cell-in-range:hover { background: #bae0ff; }
|
|
559
|
+
|
|
560
|
+
.dp-time {
|
|
561
|
+
margin-top: 0.75rem;
|
|
562
|
+
padding-top: 0.75rem;
|
|
563
|
+
border-top: 1px solid #f0f0f0;
|
|
564
|
+
}
|
|
565
|
+
.dp-time-groups {
|
|
566
|
+
display: flex;
|
|
567
|
+
align-items: center;
|
|
568
|
+
gap: 0.5rem;
|
|
569
|
+
flex-wrap: wrap;
|
|
570
|
+
}
|
|
571
|
+
.dp-time-group {
|
|
572
|
+
display: flex;
|
|
573
|
+
align-items: center;
|
|
574
|
+
gap: 0.25rem;
|
|
575
|
+
}
|
|
576
|
+
.dp-time-label { font-size: 0.75rem; color: rgba(0, 0, 0, 0.65); }
|
|
577
|
+
.dp-time-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.75rem; }
|
|
578
|
+
.dp-time-input {
|
|
579
|
+
width: 2.75rem;
|
|
580
|
+
padding: 0.25rem;
|
|
581
|
+
text-align: center;
|
|
582
|
+
border: 1px solid #d9d9d9;
|
|
583
|
+
border-radius: 4px;
|
|
584
|
+
font-size: 0.8125rem;
|
|
585
|
+
}
|
|
586
|
+
.dp-time-input:focus {
|
|
587
|
+
outline: none;
|
|
588
|
+
border-color: #1677ff;
|
|
589
|
+
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
|
590
|
+
}
|
|
591
|
+
.dp-time-sep { color: rgba(0, 0, 0, 0.65); }
|
|
592
|
+
.dp-footer {
|
|
593
|
+
display: flex;
|
|
594
|
+
justify-content: flex-end;
|
|
595
|
+
margin-top: 0.5rem;
|
|
596
|
+
}
|
|
597
|
+
.dp-ok {
|
|
598
|
+
margin-left: auto;
|
|
599
|
+
padding: 0.3rem 0.75rem;
|
|
600
|
+
background: #1677ff;
|
|
601
|
+
color: #ffffff;
|
|
602
|
+
border: none;
|
|
603
|
+
border-radius: 4px;
|
|
604
|
+
font-size: 0.8125rem;
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
}
|
|
607
|
+
.dp-ok:hover:not(:disabled) { background: #4096ff; }
|
|
608
|
+
.dp-ok:disabled { background: #91caff; cursor: not-allowed; }
|
|
609
|
+
</style>
|