@xlui/xux-ui 0.1.0 → 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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Alert 警告提示组件
3
+ * 居中显示,支持自动关闭和手动关闭
4
+ */
5
+
6
+ interface AlertOptions {
7
+ title?: string
8
+ content: string
9
+ type?: 'success' | 'error' | 'warning' | 'info'
10
+ duration?: number // 自动关闭时间,毫秒。0 表示不自动关闭
11
+ closable?: boolean // 是否显示关闭按钮
12
+ }
13
+
14
+ export const xlAlert = () => {
15
+ // 创建 Alert 容器
16
+ const createAlertContainer = () => {
17
+ let container = document.getElementById('xl-alert-container')
18
+ if (!container) {
19
+ container = document.createElement('div')
20
+ container.id = 'xl-alert-container'
21
+ container.style.cssText = `
22
+ position: fixed;
23
+ top: 5%;
24
+ left: 50%;
25
+ transform: translate(-50%, -50%);
26
+ z-index: 10000;
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 12px;
30
+ max-width: 90%;
31
+ width: 500px;
32
+ `
33
+ document.body.appendChild(container)
34
+ }
35
+ return container
36
+ }
37
+
38
+ // 获取类型对应的样式和图标
39
+ const getTypeStyles = (type: string) => {
40
+ const styles = {
41
+ success: {
42
+ bgColor: 'rgb(240, 253, 244)',
43
+ darkBgColor: 'rgb(20, 83, 45)',
44
+ borderColor: 'rgb(134, 239, 172)',
45
+ darkBorderColor: 'rgb(22, 101, 52)',
46
+ textColor: 'rgb(22, 101, 52)',
47
+ darkTextColor: 'rgb(187, 247, 208)',
48
+ iconColor: 'rgb(34, 197, 94)',
49
+ iconSvg: `<svg stroke="currentColor" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
50
+ <path d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></path>
51
+ </svg>`
52
+ },
53
+ error: {
54
+ bgColor: 'rgb(254, 242, 242)',
55
+ darkBgColor: 'rgb(127, 29, 29)',
56
+ borderColor: 'rgb(252, 165, 165)',
57
+ darkBorderColor: 'rgb(153, 27, 27)',
58
+ textColor: 'rgb(153, 27, 27)',
59
+ darkTextColor: 'rgb(254, 202, 202)',
60
+ iconColor: 'rgb(239, 68, 68)',
61
+ iconSvg: `<svg stroke="currentColor" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
62
+ <path d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></path>
63
+ </svg>`
64
+ },
65
+ warning: {
66
+ bgColor: 'rgb(254, 252, 232)',
67
+ darkBgColor: 'rgb(133, 77, 14)',
68
+ borderColor: 'rgb(253, 224, 71)',
69
+ darkBorderColor: 'rgb(161, 98, 7)',
70
+ textColor: 'rgb(133, 77, 14)',
71
+ darkTextColor: 'rgb(254, 240, 138)',
72
+ iconColor: 'rgb(234, 179, 8)',
73
+ iconSvg: `<svg stroke="currentColor" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
74
+ <path d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></path>
75
+ </svg>`
76
+ },
77
+ info: {
78
+ bgColor: 'rgb(239, 246, 255)',
79
+ darkBgColor: 'rgb(30, 58, 138)',
80
+ borderColor: 'rgb(147, 197, 253)',
81
+ darkBorderColor: 'rgb(29, 78, 216)',
82
+ textColor: 'rgb(30, 64, 175)',
83
+ darkTextColor: 'rgb(191, 219, 254)',
84
+ iconColor: 'rgb(59, 130, 246)',
85
+ iconSvg: `<svg stroke="currentColor" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
86
+ <path d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></path>
87
+ </svg>`
88
+ }
89
+ }
90
+ return styles[type as keyof typeof styles] || styles.info
91
+ }
92
+
93
+ // 检测暗色模式
94
+ const isDarkMode = () => {
95
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
96
+ }
97
+
98
+ // 显示 Alert
99
+ const show = (options: AlertOptions) => {
100
+ const container = createAlertContainer()
101
+ const type = options.type || 'info'
102
+ const duration = options.duration !== undefined ? options.duration : 5000
103
+ const closable = options.closable !== undefined ? options.closable : true
104
+ const styles = getTypeStyles(type)
105
+ const dark = isDarkMode()
106
+
107
+ const alertId = `xl-alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
108
+
109
+ const alert = document.createElement('div')
110
+ alert.id = alertId
111
+ alert.setAttribute('role', 'alert')
112
+ alert.style.cssText = `
113
+ background-color: ${dark ? styles.darkBgColor : styles.bgColor};
114
+ border-left: 4px solid ${dark ? styles.darkBorderColor : styles.borderColor};
115
+ color: ${dark ? styles.darkTextColor : styles.textColor};
116
+ padding: 16px;
117
+ border-radius: 12px;
118
+ display: flex;
119
+ align-items: center;
120
+ transition: all 0.3s ease-in-out;
121
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
122
+ transform: scale(0.8);
123
+ opacity: 0;
124
+ position: relative;
125
+ `
126
+
127
+ // 创建内容
128
+ let innerHTML = `
129
+ <div style="color: ${styles.iconColor}; width: 20px; height: 20px; flex-shrink: 0; margin-right: 12px;">
130
+ ${styles.iconSvg}
131
+ </div>
132
+ <div style="flex: 1;">
133
+ ${options.title ? `<p style="font-weight: 600; font-size: 14px; margin-bottom: 4px;">${options.title}</p>` : ''}
134
+ <p style="font-size: ${options.title ? '13px' : '14px'}; font-weight: ${options.title ? '400' : '600'};">${options.content}</p>
135
+ </div>
136
+ `
137
+
138
+ // 添加关闭按钮
139
+ if (closable) {
140
+ innerHTML += `
141
+ <button
142
+ class="xl-alert-close"
143
+ style="
144
+ background: transparent;
145
+ border: none;
146
+ color: ${dark ? styles.darkTextColor : styles.textColor};
147
+ cursor: pointer;
148
+ padding: 4px;
149
+ margin-left: 8px;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ opacity: 0.6;
154
+ transition: opacity 0.2s;
155
+ width: 20px;
156
+ height: 20px;
157
+ flex-shrink: 0;
158
+ "
159
+ onmouseover="this.style.opacity='1'"
160
+ onmouseout="this.style.opacity='0.6'"
161
+ >
162
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
163
+ <line x1="18" y1="6" x2="6" y2="18"></line>
164
+ <line x1="6" y1="6" x2="18" y2="18"></line>
165
+ </svg>
166
+ </button>
167
+ `
168
+ }
169
+
170
+ alert.innerHTML = innerHTML
171
+
172
+ // 关闭函数
173
+ const close = () => {
174
+ alert.style.transform = 'scale(0.8)'
175
+ alert.style.opacity = '0'
176
+ setTimeout(() => {
177
+ if (alert.parentNode) {
178
+ container.removeChild(alert)
179
+ }
180
+ // 如果容器为空,移除容器
181
+ if (container.children.length === 0) {
182
+ document.body.removeChild(container)
183
+ }
184
+ }, 300)
185
+ }
186
+
187
+ // 添加关闭按钮事件
188
+ if (closable) {
189
+ const closeBtn = alert.querySelector('.xl-alert-close')
190
+ if (closeBtn) {
191
+ closeBtn.addEventListener('click', close)
192
+ }
193
+ }
194
+
195
+ container.appendChild(alert)
196
+
197
+ // 显示动画
198
+ setTimeout(() => {
199
+ alert.style.transform = 'scale(1)'
200
+ alert.style.opacity = '1'
201
+ }, 10)
202
+
203
+ // 自动关闭
204
+ if (duration > 0) {
205
+ setTimeout(close, duration)
206
+ }
207
+
208
+ return { close }
209
+ }
210
+
211
+ // 快捷方法
212
+ const success = (content: string, options?: Partial<AlertOptions>) => {
213
+ return show({ content, type: 'success', ...options })
214
+ }
215
+
216
+ const error = (content: string, options?: Partial<AlertOptions>) => {
217
+ return show({ content, type: 'error', ...options })
218
+ }
219
+
220
+ const warning = (content: string, options?: Partial<AlertOptions>) => {
221
+ return show({ content, type: 'warning', ...options })
222
+ }
223
+
224
+ const info = (content: string, options?: Partial<AlertOptions>) => {
225
+ return show({ content, type: 'info', ...options })
226
+ }
227
+
228
+ return {
229
+ show,
230
+ success,
231
+ error,
232
+ warning,
233
+ info
234
+ }
235
+ }
236
+
237
+ // 默认导出
238
+ export default xlAlert
@@ -0,0 +1,391 @@
1
+ // useDateTime.ts
2
+ import dayjs, { Dayjs } from 'dayjs'
3
+ import { ref, computed } from 'vue'
4
+
5
+ // plugins
6
+ import utc from 'dayjs/plugin/utc'
7
+ import timezone from 'dayjs/plugin/timezone'
8
+ import relativeTime from 'dayjs/plugin/relativeTime'
9
+ import duration from 'dayjs/plugin/duration'
10
+ import isSameOrAfterPlugin from 'dayjs/plugin/isSameOrAfter'
11
+ import isSameOrBeforePlugin from 'dayjs/plugin/isSameOrBefore'
12
+ import isBetweenPlugin from 'dayjs/plugin/isBetween'
13
+ import weekOfYear from 'dayjs/plugin/weekOfYear'
14
+ import isoWeek from 'dayjs/plugin/isoWeek'
15
+ import customParseFormat from 'dayjs/plugin/customParseFormat'
16
+ import advancedFormat from 'dayjs/plugin/advancedFormat'
17
+
18
+ // locale
19
+ import 'dayjs/locale/zh-cn'
20
+
21
+ dayjs.extend(utc)
22
+ dayjs.extend(timezone)
23
+ dayjs.extend(relativeTime)
24
+ dayjs.extend(duration)
25
+ dayjs.extend(isSameOrAfterPlugin)
26
+ dayjs.extend(isSameOrBeforePlugin)
27
+ dayjs.extend(isBetweenPlugin)
28
+ dayjs.extend(weekOfYear)
29
+ dayjs.extend(isoWeek)
30
+ dayjs.extend(customParseFormat)
31
+ dayjs.extend(advancedFormat)
32
+
33
+ export interface DateTimeOptions {
34
+ /** 时区(默认 Asia/Shanghai) */
35
+ timezone?: string
36
+ /** 语言(默认 zh-cn) */
37
+ locale?: string
38
+ /** 默认格式 */
39
+ defaultFormat?: string
40
+ /** 解析是否严格(默认 false) */
41
+ parseStrict?: boolean
42
+ /** 周起始:0 周日;1 周一(默认跟随 locale;传入可强制) */
43
+ weekStart?: 0 | 1
44
+ }
45
+
46
+ export interface DateRange {
47
+ start: Dayjs | null
48
+ end: Dayjs | null
49
+ }
50
+
51
+ export interface CalendarDate {
52
+ date: Dayjs
53
+ isCurrentMonth: boolean
54
+ isToday: boolean
55
+ isSelected: boolean
56
+ isDisabled: boolean
57
+ isWeekend: boolean
58
+ }
59
+
60
+ type DateInput = Dayjs | Date | string | number | null | undefined
61
+
62
+ export function useDateTime(options: DateTimeOptions = {}) {
63
+ const {
64
+ timezone: tz = 'Asia/Shanghai',
65
+ locale = 'zh-cn',
66
+ defaultFormat = 'YYYY-MM-DD HH:mm:ss',
67
+ parseStrict = false,
68
+ weekStart, // undefined = 跟随 locale
69
+ } = options
70
+
71
+ // 全局 locale(Dayjs 是全局单例,这里设一次即可)
72
+ dayjs.locale(locale)
73
+
74
+ /** 绑定 tz + locale 的工厂(核心入口) */
75
+ const d = (v?: DateInput) => (v == null ? dayjs() : dayjs(v)).tz(tz).locale(locale)
76
+
77
+ /** 归一化:接受任意输入,统一返回 Dayjs(绑定 tz) */
78
+ const normalize = (v: DateInput, fmt?: string) => {
79
+ if (fmt) return dayjs(v as any, fmt, parseStrict).tz(tz).locale(locale)
80
+ return d(v)
81
+ }
82
+
83
+ /** 可选:UTC 与本地时区互转 */
84
+ const toUTC = (v: DateInput) => normalize(v).utc()
85
+ const fromUTC = (v: DateInput) => dayjs(v as any).utc().tz(tz).locale(locale)
86
+
87
+ // ===================== 基础 =====================
88
+ const now = ref(d())
89
+
90
+ const updateNow = () => { now.value = d() }
91
+ const getCurrent = () => d()
92
+
93
+ const format = (v: DateInput, fmt: string = defaultFormat) => normalize(v).format(fmt)
94
+ const parse = (v: string | number, fmt?: string) => normalize(v, fmt)
95
+ const create = (v?: DateInput) => normalize(v)
96
+
97
+ // ===================== 比较 =====================
98
+ const isSame = (a: DateInput, b: DateInput, unit?: dayjs.OpUnitType) =>
99
+ normalize(a).isSame(normalize(b), unit)
100
+
101
+ const isAfter = (a: DateInput, b: DateInput, unit?: dayjs.OpUnitType) =>
102
+ normalize(a).isAfter(normalize(b), unit)
103
+
104
+ const isBefore = (a: DateInput, b: DateInput, unit?: dayjs.OpUnitType) =>
105
+ normalize(a).isBefore(normalize(b), unit)
106
+
107
+ const isSameOrAfter = (a: DateInput, b: DateInput, unit?: dayjs.OpUnitType) =>
108
+ normalize(a).isSameOrAfter(normalize(b), unit)
109
+
110
+ const isSameOrBefore = (a: DateInput, b: DateInput, unit?: dayjs.OpUnitType) =>
111
+ normalize(a).isSameOrBefore(normalize(b), unit)
112
+
113
+ /** 新:支持 inclusivity('()' | '[]' | '[)' | '(]'),默认 '()' 同 dayjs */
114
+ const isBetween = (
115
+ v: DateInput, start: DateInput, end: DateInput,
116
+ unit?: dayjs.OpUnitType, inclusivity?: '()' | '[]' | '[)' | '(]'
117
+ ) => normalize(v).isBetween(normalize(start), normalize(end), unit, inclusivity)
118
+
119
+ // ===================== 计算 =====================
120
+ const add = (v: DateInput, amount: number, unit: dayjs.ManipulateType) => normalize(v).add(amount, unit)
121
+ const subtract = (v: DateInput, amount: number, unit: dayjs.ManipulateType) => normalize(v).subtract(amount, unit)
122
+ const diff = (a: DateInput, b: DateInput, unit?: dayjs.QUnitType, float?: boolean) =>
123
+ normalize(a).diff(normalize(b), unit, float)
124
+
125
+ const startOf = (v: DateInput, unit: dayjs.OpUnitType) => normalize(v).startOf(unit)
126
+ const endOf = (v: DateInput, unit: dayjs.OpUnitType) => normalize(v).endOf(unit)
127
+
128
+ // ===================== 相对时间 =====================
129
+ const fromNow = (v: DateInput) => normalize(v).from(d())
130
+ const toNow = (v: DateInput) => normalize(v).to(d())
131
+ const from = (a: DateInput, b: DateInput) => normalize(a).from(normalize(b))
132
+ const to = (a: DateInput, b: DateInput) => normalize(a).to(normalize(b))
133
+
134
+ /** 新:更细粒度的人性化差值(单位可选) */
135
+ const humanizeDiff = (a: DateInput, b: DateInput = d(), unit: dayjs.QUnitType = 'millisecond') => {
136
+ const ms = normalize(a).diff(normalize(b), 'millisecond')
137
+ return dayjs.duration(Math.abs(ms)).humanize()
138
+ }
139
+
140
+ /** 新:格式化为分解的时间部分(用于倒计时/仪表) */
141
+ const formatParts = (ms: number) => {
142
+ const dur = dayjs.duration(ms)
143
+ const days = Math.floor(dur.asDays())
144
+ const hours = dur.hours()
145
+ const minutes = dur.minutes()
146
+ const seconds = dur.seconds()
147
+ return { days, hours, minutes, seconds }
148
+ }
149
+
150
+ // ===================== 日历 / 月份 =====================
151
+ /**
152
+ * 重写:遵循 locale 或指定 weekStart,保证 6×7=42 单元
153
+ * month: 0-11
154
+ */
155
+ const getCalendarDates = (
156
+ year: number,
157
+ month: number,
158
+ selectedDate?: DateInput,
159
+ minDate?: DateInput,
160
+ maxDate?: DateInput
161
+ ): CalendarDate[] => {
162
+ const firstDay = d().year(year).month(month).date(1)
163
+ const monthStart = firstDay.startOf('month')
164
+ const monthEnd = firstDay.endOf('month')
165
+
166
+ // 确定周起始(默认跟随 locale;可强制为周一)
167
+ const localWeekStart = typeof weekStart === 'number' ? weekStart : (dayjs.Ls[locale]?.weekStart ?? 0)
168
+ // Dayjs 的 startOf('week') 受 locale 影响;这里手动对齐
169
+ const start = monthStart.startOf('week').add(localWeekStart, 'day')
170
+ const gridStart = start.isAfter(monthStart) ? start.subtract(7, 'day') : start
171
+
172
+ const selected = selectedDate ? normalize(selectedDate) : null
173
+ const min = minDate ? normalize(minDate).startOf('day') : null
174
+ const max = maxDate ? normalize(maxDate).endOf('day') : null
175
+ const today = d()
176
+
177
+ const cells: CalendarDate[] = []
178
+ for (let i = 0; i < 42; i++) {
179
+ const date = gridStart.add(i, 'day')
180
+ const isCurrentMonth = date.month() === month
181
+ const isToday = date.isSame(today, 'day')
182
+ const isSelected = selected ? date.isSame(selected, 'day') : false
183
+ const wd = date.day()
184
+ const isWeekend = wd === 0 || wd === 6
185
+
186
+ let isDisabled = false
187
+ if (min && date.isBefore(min, 'day')) isDisabled = true
188
+ if (max && date.isAfter(max, 'day')) isDisabled = true
189
+
190
+ cells.push({ date, isCurrentMonth, isToday, isSelected, isDisabled, isWeekend })
191
+ }
192
+ return cells
193
+ }
194
+
195
+ const getMonthInfo = (year: number, month: number) => {
196
+ const first = d().year(year).month(month).date(1)
197
+ const last = first.endOf('month')
198
+ const daysInMonth = last.date()
199
+ const firstWeekday = first.day() // 0 Sun - 6 Sat
200
+ const lastWeekday = last.day()
201
+ const weeksInMonth = Math.ceil((firstWeekday + daysInMonth) / 7)
202
+ return { firstDay: first, lastDay: last, daysInMonth, firstWeekday, lastWeekday, weeksInMonth }
203
+ }
204
+
205
+ const getTimeRanges = () => {
206
+ const today = d()
207
+ return {
208
+ today: { start: today.startOf('day'), end: today.endOf('day') },
209
+ yesterday: {
210
+ start: today.subtract(1, 'day').startOf('day'),
211
+ end: today.subtract(1, 'day').endOf('day'),
212
+ },
213
+ thisWeek: { start: today.startOf('week'), end: today.endOf('week') },
214
+ lastWeek: {
215
+ start: today.subtract(1, 'week').startOf('week'),
216
+ end: today.subtract(1, 'week').endOf('week'),
217
+ },
218
+ thisMonth: { start: today.startOf('month'), end: today.endOf('month') },
219
+ lastMonth: {
220
+ start: today.subtract(1, 'month').startOf('month'),
221
+ end: today.subtract(1, 'month').endOf('month'),
222
+ },
223
+ thisYear: { start: today.startOf('year'), end: today.endOf('year') },
224
+ lastYear: {
225
+ start: today.subtract(1, 'year').startOf('year'),
226
+ end: today.subtract(1, 'year').endOf('year'),
227
+ },
228
+ }
229
+ }
230
+
231
+ // ===================== 校验 / 取值 =====================
232
+ const isValid = (v: DateInput, fmt?: string) =>
233
+ fmt ? dayjs(v as any, fmt, parseStrict).isValid() : dayjs(v as any).isValid()
234
+
235
+ const getUnit = (v: DateInput, unit: dayjs.UnitType) => normalize(v).get(unit)
236
+ const setUnit = (v: DateInput, unit: dayjs.UnitType, value: number) => normalize(v).set(unit, value)
237
+
238
+ const formats = {
239
+ date: 'YYYY-MM-DD',
240
+ time: 'HH:mm:ss',
241
+ datetime: 'YYYY-MM-DD HH:mm:ss',
242
+ dateTime: 'YYYY-MM-DD HH:mm',
243
+ month: 'YYYY-MM',
244
+ year: 'YYYY',
245
+ timestamp: 'X',
246
+ iso: 'YYYY-MM-DDTHH:mm:ssZ',
247
+ } as const
248
+
249
+ return {
250
+ // 基础
251
+ now: computed(() => now.value),
252
+ updateNow,
253
+ getCurrent,
254
+ format,
255
+ parse,
256
+ create,
257
+
258
+ // 比较
259
+ isSame,
260
+ isAfter,
261
+ isBefore,
262
+ isSameOrAfter,
263
+ isSameOrBefore,
264
+ isBetween,
265
+
266
+ // 计算
267
+ add,
268
+ subtract,
269
+ diff,
270
+ startOf,
271
+ endOf,
272
+
273
+ // 相对时间
274
+ fromNow,
275
+ toNow,
276
+ from,
277
+ to,
278
+ humanizeDiff,
279
+ formatParts,
280
+
281
+ // 日历
282
+ getCalendarDates,
283
+ getMonthInfo,
284
+ getTimeRanges,
285
+
286
+ // 工具
287
+ isValid,
288
+ getUnit,
289
+ setUnit,
290
+
291
+ // 常量
292
+ formats,
293
+
294
+ // tz/locale 绑定 dayjs(高级用法)
295
+ dayjs: (v?: DateInput) => normalize(v),
296
+
297
+ // UTC <-> 本地
298
+ toUTC,
299
+ fromUTC,
300
+ }
301
+ }
302
+
303
+ /** 时间范围选择器(修正:统一 tz) */
304
+ export function useDateRange(initialRange: DateRange = { start: null, end: null }, tz = 'Asia/Shanghai') {
305
+ const _d = (v?: DateInput) => (v == null ? dayjs() : dayjs(v)).tz(tz)
306
+
307
+ const range = ref<DateRange>({ start: initialRange.start ? _d(initialRange.start) : null,
308
+ end: initialRange.end ? _d(initialRange.end) : null })
309
+
310
+ const setRange = (start: DateInput, end: DateInput) => {
311
+ range.value = { start: start ? _d(start) : null, end: end ? _d(end) : null }
312
+ }
313
+
314
+ const clearRange = () => { range.value = { start: null, end: null } }
315
+
316
+ const isValidRange = computed(() => {
317
+ const { start, end } = range.value
318
+ if (!start || !end) return false
319
+ return start.isBefore(end) || start.isSame(end)
320
+ })
321
+
322
+ /** 兼容性变更:原先返回天数,这里保留 days,同时附带 ms */
323
+ const rangeDuration = computed(() => {
324
+ if (!isValidRange.value) return null
325
+ const ms = range.value.end!.diff(range.value.start!)
326
+ return { ms, days: Math.floor(dayjs.duration(ms).asDays()) }
327
+ })
328
+
329
+ return {
330
+ range: computed(() => range.value),
331
+ setRange,
332
+ clearRange,
333
+ isValidRange,
334
+ rangeDuration,
335
+ }
336
+ }
337
+
338
+ /** 倒计时(统一 tz;新增 parts 输出) */
339
+ export function useCountdown(targetDate: DateInput, tz = 'Asia/Shanghai') {
340
+ const _d = (v?: DateInput) => (v == null ? dayjs() : dayjs(v)).tz(tz)
341
+
342
+ const target = ref(_d(targetDate))
343
+ const now = ref(_d())
344
+ const isExpired = ref(false)
345
+
346
+ const durationObj = computed(() => {
347
+ const diffMs = target.value.diff(now.value)
348
+ if (diffMs <= 0) {
349
+ isExpired.value = true
350
+ return dayjs.duration(0)
351
+ }
352
+ return dayjs.duration(diffMs)
353
+ })
354
+
355
+ const updateNow = () => { now.value = _d() }
356
+
357
+ const formatDuration = (format: string = 'HH:mm:ss') => {
358
+ if (isExpired.value) return '00:00:00'
359
+ const dur = durationObj.value
360
+ const hours = Math.floor(dur.asHours())
361
+ const minutes = dur.minutes()
362
+ const seconds = dur.seconds()
363
+ return format
364
+ .replace('HH', String(hours).padStart(2, '0'))
365
+ .replace('mm', String(minutes).padStart(2, '0'))
366
+ .replace('ss', String(seconds).padStart(2, '0'))
367
+ }
368
+
369
+ const parts = computed(() => {
370
+ const dur = durationObj.value
371
+ return {
372
+ days: Math.floor(dur.asDays()),
373
+ hours: dur.hours(),
374
+ minutes: dur.minutes(),
375
+ seconds: dur.seconds(),
376
+ milliseconds: dur.milliseconds(),
377
+ }
378
+ })
379
+
380
+ return {
381
+ target: computed(() => target.value),
382
+ now: computed(() => now.value),
383
+ duration: computed(() => durationObj.value),
384
+ parts,
385
+ isExpired: computed(() => isExpired.value),
386
+ updateNow,
387
+ formatDuration,
388
+ }
389
+ }
390
+
391
+ export default useDateTime
package/src/index.ts CHANGED
@@ -12,9 +12,15 @@ export { default as XAccordion } from './components/Accordion/index.vue'
12
12
  export { default as XThumbnailContainer } from './components/ThumbnailContainer/index.vue'
13
13
  export { default as XSkeleton } from './components/Skeleton/index.vue'
14
14
  export { default as XModal } from './components/Modal/index.vue'
15
+ export { default as XTooltips } from './components/Tooltips/index.vue'
16
+ export { default as XScore } from './components/Score/index.vue'
17
+ export { default as XRadio } from './components/Radio/index.vue'
18
+ export { default as XDateTimePicker } from './components/DateTimePicker/index.vue'
15
19
 
16
20
  // 导出 Composables
17
21
  export { xlMsg, default as useMsg } from './composables/Msg'
22
+ export { xlAlert, default as useAlert } from './composables/Alert'
23
+ export { useDateTime, useDateRange, useCountdown, default as useDateTimeDefault } from './composables/DateTime'
18
24
 
19
25
  // 导出类型定义
20
26
  export type { Country, CountrySelectProps } from './components/CountrySelect/index.vue'
@@ -25,4 +31,9 @@ export type { AccordionItem, AccordionProps } from './components/Accordion/index
25
31
  export type { ThumbnailContainerProps } from './components/ThumbnailContainer/index.vue'
26
32
  export type { SkeletonProps } from './components/Skeleton/index.vue'
27
33
  export type { ModalProps } from './components/Modal/index.vue'
34
+ export type { TooltipsProps } from './components/Tooltips/index.vue'
35
+ export type { ScoreProps } from './components/Score/index.vue'
36
+ export type { RadioOption, RadioProps } from './components/Radio/index.vue'
37
+ export type { DateTimePickerProps } from './components/DateTimePicker/index.vue'
38
+ export type { DateTimeOptions, DateRange, CalendarDate } from './composables/DateTime'
28
39