@lumen-design/date-picker 0.0.2
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/dist/DatePicker.svelte +206 -0
- package/dist/index.js +3 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DatePickerType, DatePickerSize } from './types'
|
|
3
|
+
import { onMount, onDestroy } from 'svelte'
|
|
4
|
+
import { createCenteredDropdownTransition } from '../../utils'
|
|
5
|
+
import Icon from '@lumen-design/icon'
|
|
6
|
+
import { Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, X } from 'lucide'
|
|
7
|
+
|
|
8
|
+
interface DatePickerProps {
|
|
9
|
+
modelValue?: Date | string | null
|
|
10
|
+
type?: DatePickerType
|
|
11
|
+
placeholder?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
clearable?: boolean
|
|
14
|
+
size?: DatePickerSize
|
|
15
|
+
format?: string
|
|
16
|
+
valueFormat?: string
|
|
17
|
+
disabledDate?: (date: Date) => boolean
|
|
18
|
+
showMenuArrow?: boolean
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
modelValue = $bindable(null),
|
|
24
|
+
type = 'date',
|
|
25
|
+
placeholder = '选择日期',
|
|
26
|
+
disabled = false,
|
|
27
|
+
clearable = false,
|
|
28
|
+
size = 'default',
|
|
29
|
+
format = 'YYYY-MM-DD',
|
|
30
|
+
valueFormat,
|
|
31
|
+
disabledDate,
|
|
32
|
+
showMenuArrow = true,
|
|
33
|
+
class: cls = '',
|
|
34
|
+
...attrs
|
|
35
|
+
}: DatePickerProps = $props()
|
|
36
|
+
|
|
37
|
+
let visible = $state(false)
|
|
38
|
+
let reduceMotion = $state(false)
|
|
39
|
+
let pickerRef: HTMLDivElement | null = null
|
|
40
|
+
let currentYear = $state(new Date().getFullYear())
|
|
41
|
+
let currentMonth = $state(new Date().getMonth())
|
|
42
|
+
|
|
43
|
+
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
44
|
+
|
|
45
|
+
// 优化: 移除冗余的箭头函数包装
|
|
46
|
+
const selectedDate = $derived(modelValue ? (modelValue instanceof Date ? modelValue : new Date(modelValue)) : null)
|
|
47
|
+
|
|
48
|
+
const displayValue = $derived.by(() => {
|
|
49
|
+
if (!selectedDate) return ''
|
|
50
|
+
const y = selectedDate.getFullYear()
|
|
51
|
+
const m = String(selectedDate.getMonth() + 1).padStart(2, '0')
|
|
52
|
+
const d = String(selectedDate.getDate()).padStart(2, '0')
|
|
53
|
+
return format.replace('YYYY', String(y)).replace('MM', m).replace('DD', d)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const daysInMonth = $derived.by(() => {
|
|
57
|
+
const firstDay = new Date(currentYear, currentMonth, 1)
|
|
58
|
+
const lastDay = new Date(currentYear, currentMonth + 1, 0)
|
|
59
|
+
const days: { date: Date; isCurrentMonth: boolean }[] = []
|
|
60
|
+
const startDay = firstDay.getDay()
|
|
61
|
+
|
|
62
|
+
for (let i = startDay - 1; i >= 0; i--) {
|
|
63
|
+
days.push({ date: new Date(currentYear, currentMonth, -i), isCurrentMonth: false })
|
|
64
|
+
}
|
|
65
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
66
|
+
days.push({ date: new Date(currentYear, currentMonth, i), isCurrentMonth: true })
|
|
67
|
+
}
|
|
68
|
+
const remaining = 42 - days.length
|
|
69
|
+
for (let i = 1; i <= remaining; i++) {
|
|
70
|
+
days.push({ date: new Date(currentYear, currentMonth + 1, i), isCurrentMonth: false })
|
|
71
|
+
}
|
|
72
|
+
return days
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const classes = $derived(`lm-date-picker lm-date-picker--${size}${disabled ? ' is-disabled' : ''}${cls ? ` ${cls}` : ''}`)
|
|
76
|
+
|
|
77
|
+
const handleClickOutside = (e: MouseEvent): void => {
|
|
78
|
+
if (visible && pickerRef && !pickerRef.contains(e.target as Node)) visible = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toggle = (): void => {
|
|
82
|
+
if (disabled) return
|
|
83
|
+
visible = !visible
|
|
84
|
+
if (visible && selectedDate) {
|
|
85
|
+
currentYear = selectedDate.getFullYear()
|
|
86
|
+
currentMonth = selectedDate.getMonth()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const prevMonth = (): void => {
|
|
91
|
+
if (currentMonth === 0) {
|
|
92
|
+
currentMonth = 11
|
|
93
|
+
currentYear--
|
|
94
|
+
} else {
|
|
95
|
+
currentMonth--
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const prevYear = (): void => {
|
|
100
|
+
currentYear--
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nextMonth = (): void => {
|
|
104
|
+
if (currentMonth === 11) {
|
|
105
|
+
currentMonth = 0
|
|
106
|
+
currentYear++
|
|
107
|
+
} else {
|
|
108
|
+
currentMonth++
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const nextYear = (): void => {
|
|
113
|
+
currentYear++
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const formatDate = (date: Date, fmt: string): string => {
|
|
117
|
+
const y = date.getFullYear()
|
|
118
|
+
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
119
|
+
const d = String(date.getDate()).padStart(2, '0')
|
|
120
|
+
return fmt.replace('YYYY', String(y)).replace('MM', m).replace('DD', d)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const selectDate = (date: Date): void => {
|
|
124
|
+
if (disabledDate?.(date)) return
|
|
125
|
+
modelValue = valueFormat ? formatDate(date, valueFormat) : date
|
|
126
|
+
visible = false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isSelected = (date: Date): boolean => {
|
|
130
|
+
if (!selectedDate) return false
|
|
131
|
+
return date.getFullYear() === selectedDate.getFullYear() && date.getMonth() === selectedDate.getMonth() && date.getDate() === selectedDate.getDate()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isToday = (date: Date): boolean => {
|
|
135
|
+
const today = new Date()
|
|
136
|
+
return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const handleClear = (e: MouseEvent): void => {
|
|
140
|
+
e.stopPropagation()
|
|
141
|
+
modelValue = null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const dropdownTransition = $derived(createCenteredDropdownTransition(reduceMotion))
|
|
145
|
+
|
|
146
|
+
onMount(() => {
|
|
147
|
+
document.addEventListener('click', handleClickOutside)
|
|
148
|
+
if (typeof window !== 'undefined') {
|
|
149
|
+
reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
onDestroy(() => document.removeEventListener('click', handleClickOutside))
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<div class={classes} bind:this={pickerRef} {...attrs}>
|
|
156
|
+
<button type="button" class="lm-date-picker__trigger" class:is-focus={visible} onclick={toggle} {disabled}>
|
|
157
|
+
<Icon icon={Calendar} size={14} class="lm-date-picker__prefix" />
|
|
158
|
+
<span class="lm-date-picker__input" class:is-placeholder={!displayValue}>{displayValue || placeholder}</span>
|
|
159
|
+
{#if clearable && modelValue && !disabled}
|
|
160
|
+
<span
|
|
161
|
+
class="lm-date-picker__clear"
|
|
162
|
+
role="button"
|
|
163
|
+
tabindex="0"
|
|
164
|
+
onclick={handleClear}
|
|
165
|
+
onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && handleClear(e as unknown as MouseEvent)}
|
|
166
|
+
aria-label="清空"><Icon icon={X} size={14} /></span
|
|
167
|
+
>
|
|
168
|
+
{/if}
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
{#if visible}
|
|
172
|
+
<div class="lm-date-picker__dropdown" transition:dropdownTransition>
|
|
173
|
+
{#if showMenuArrow}
|
|
174
|
+
<div class="lm-date-picker__menu-arrow" aria-hidden="true"></div>
|
|
175
|
+
{/if}
|
|
176
|
+
<div class="lm-date-picker__header">
|
|
177
|
+
<div class="lm-date-picker__nav-group">
|
|
178
|
+
<button type="button" class="lm-date-picker__nav" onclick={prevYear} aria-label="上一年"><Icon icon={ChevronsLeft} size={16} /></button>
|
|
179
|
+
<button type="button" class="lm-date-picker__nav" onclick={prevMonth} aria-label="上个月"><Icon icon={ChevronLeft} size={14} /></button>
|
|
180
|
+
</div>
|
|
181
|
+
<span class="lm-date-picker__title">{currentYear}年 {currentMonth + 1}月</span>
|
|
182
|
+
<div class="lm-date-picker__nav-group">
|
|
183
|
+
<button type="button" class="lm-date-picker__nav" onclick={nextMonth} aria-label="下个月"><Icon icon={ChevronRight} size={14} /></button>
|
|
184
|
+
<button type="button" class="lm-date-picker__nav" onclick={nextYear} aria-label="下一年"><Icon icon={ChevronsRight} size={16} /></button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="lm-date-picker__body">
|
|
188
|
+
<div class="lm-date-picker__week">
|
|
189
|
+
{#each weekDays as day}<span class="lm-date-picker__week-day">{day}</span>{/each}
|
|
190
|
+
</div>
|
|
191
|
+
<div class="lm-date-picker__days">
|
|
192
|
+
{#each daysInMonth as { date, isCurrentMonth }}
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
class="lm-date-picker__day{!isCurrentMonth ? ' is-other-month' : ''}{isSelected(date) ? ' is-selected' : ''}{isToday(date) ? ' is-today' : ''}{disabledDate?.(date)
|
|
196
|
+
? ' is-disabled'
|
|
197
|
+
: ''}"
|
|
198
|
+
disabled={disabledDate?.(date)}
|
|
199
|
+
onclick={() => selectDate(date)}>{date.getDate()}</button
|
|
200
|
+
>
|
|
201
|
+
{/each}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
package/dist/index.js
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lumen-design/date-picker",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "DatePicker component for Lumen UI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "svelte-package -i src -o dist --types",
|
|
21
|
+
"build:watch": "svelte-package -i src -o dist --types -w"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@lumen-design/icon": "0.0.2",
|
|
25
|
+
"lucide": "^0.563.0",
|
|
26
|
+
"@lumen-design/utils": "0.0.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@sveltejs/package": "^2.5.7",
|
|
30
|
+
"svelte": "5.48.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"svelte": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|