@xlui/xux-ui 0.2.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.
- package/LICENSE +194 -0
- package/README.md +2 -0
- package/README.zip +0 -0
- package/dist/index.css +1 -1
- package/dist/index.js +33 -33
- package/dist/index.mjs +3019 -1435
- package/package.json +4 -1
- package/src/components/DateTimePicker/index.vue +919 -0
- package/src/components/Modal/index.vue +18 -12
- package/src/components/Radio/index.vue +620 -0
- package/src/components/Score/index.vue +264 -0
- package/src/composables/DateTime.ts +391 -0
- package/src/index.ts +8 -0
@@ -0,0 +1,264 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="x-score" :class="[`score-${size}`, { 'score-disabled': disabled }]">
|
3
|
+
<div class="rating">
|
4
|
+
<label
|
5
|
+
v-for="star in maxStars"
|
6
|
+
:key="star"
|
7
|
+
:class="{ 'star-filled': star <= modelValue }"
|
8
|
+
:title="getStarTitle(star)"
|
9
|
+
@click="handleChange(star)"
|
10
|
+
>
|
11
|
+
<input
|
12
|
+
type="radio"
|
13
|
+
:name="`star-radio-${uniqueId}`"
|
14
|
+
:value="star"
|
15
|
+
:disabled="disabled"
|
16
|
+
:checked="modelValue === star"
|
17
|
+
style="display: none;"
|
18
|
+
/>
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
20
|
+
<path
|
21
|
+
pathLength="360"
|
22
|
+
d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"
|
23
|
+
></path>
|
24
|
+
</svg>
|
25
|
+
</label>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<!-- 显示评分文字 -->
|
29
|
+
<div v-if="showText" class="score-text">
|
30
|
+
{{ getScoreText() }}
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<!-- 显示数值 -->
|
34
|
+
<div v-if="showValue" class="score-value">
|
35
|
+
{{ modelValue }}/{{ maxStars }}
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
</template>
|
39
|
+
|
40
|
+
<script setup lang="ts">
|
41
|
+
import { computed } from 'vue'
|
42
|
+
|
43
|
+
export interface ScoreProps {
|
44
|
+
/** 当前评分值 */
|
45
|
+
modelValue?: number
|
46
|
+
/** 最大星数 */
|
47
|
+
maxStars?: number
|
48
|
+
/** 组件尺寸 */
|
49
|
+
size?: 'small' | 'medium' | 'large'
|
50
|
+
/** 是否禁用 */
|
51
|
+
disabled?: boolean
|
52
|
+
/** 是否显示评分文字 */
|
53
|
+
showText?: boolean
|
54
|
+
/** 是否显示数值 */
|
55
|
+
showValue?: boolean
|
56
|
+
/** 自定义评分文字 */
|
57
|
+
texts?: string[]
|
58
|
+
/** 是否允许半星评分 */
|
59
|
+
allowHalf?: boolean
|
60
|
+
/** 自定义颜色 */
|
61
|
+
color?: string
|
62
|
+
}
|
63
|
+
|
64
|
+
const props = withDefaults(defineProps<ScoreProps>(), {
|
65
|
+
modelValue: 0,
|
66
|
+
maxStars: 5,
|
67
|
+
size: 'medium',
|
68
|
+
disabled: false,
|
69
|
+
showText: false,
|
70
|
+
showValue: false,
|
71
|
+
texts: () => ['极差', '差', '一般', '好', '极好'],
|
72
|
+
allowHalf: false,
|
73
|
+
color: '#ffc73a'
|
74
|
+
})
|
75
|
+
|
76
|
+
const emit = defineEmits<{
|
77
|
+
'update:modelValue': [value: number]
|
78
|
+
change: [value: number]
|
79
|
+
}>()
|
80
|
+
|
81
|
+
// 生成唯一ID避免多个组件冲突
|
82
|
+
const uniqueId = computed(() => Math.random().toString(36).substr(2, 9))
|
83
|
+
|
84
|
+
// 处理评分变化
|
85
|
+
const handleChange = (value: number) => {
|
86
|
+
if (props.disabled) return
|
87
|
+
|
88
|
+
emit('update:modelValue', value)
|
89
|
+
emit('change', value)
|
90
|
+
}
|
91
|
+
|
92
|
+
// 获取星星标题
|
93
|
+
const getStarTitle = (star: number) => {
|
94
|
+
if (props.texts && props.texts[star - 1]) {
|
95
|
+
return `${star}星 - ${props.texts[star - 1]}`
|
96
|
+
}
|
97
|
+
return `${star}星`
|
98
|
+
}
|
99
|
+
|
100
|
+
// 获取评分文字
|
101
|
+
const getScoreText = () => {
|
102
|
+
if (props.modelValue === 0) return '未评分'
|
103
|
+
if (props.texts && props.texts[props.modelValue - 1]) {
|
104
|
+
return props.texts[props.modelValue - 1]
|
105
|
+
}
|
106
|
+
return `${props.modelValue}星`
|
107
|
+
}
|
108
|
+
</script>
|
109
|
+
|
110
|
+
|
111
|
+
<style scoped>
|
112
|
+
.x-score {
|
113
|
+
display: inline-flex;
|
114
|
+
flex-direction: column;
|
115
|
+
align-items: flex-start;
|
116
|
+
gap: 0.5rem;
|
117
|
+
}
|
118
|
+
|
119
|
+
.rating {
|
120
|
+
display: flex;
|
121
|
+
gap: 0.3rem;
|
122
|
+
--stroke: #666;
|
123
|
+
--fill: v-bind(color);
|
124
|
+
}
|
125
|
+
|
126
|
+
.rating label {
|
127
|
+
cursor: pointer;
|
128
|
+
transition: transform 0.2s ease;
|
129
|
+
position: relative;
|
130
|
+
}
|
131
|
+
|
132
|
+
.rating label:hover {
|
133
|
+
transform: scale(1.1);
|
134
|
+
}
|
135
|
+
|
136
|
+
.rating svg {
|
137
|
+
width: 2rem;
|
138
|
+
height: 2rem;
|
139
|
+
overflow: visible;
|
140
|
+
fill: transparent;
|
141
|
+
stroke: var(--stroke);
|
142
|
+
stroke-linejoin: bevel;
|
143
|
+
stroke-dasharray: 12;
|
144
|
+
animation: idle 4s linear infinite;
|
145
|
+
transition: stroke 0.2s, fill 0.5s;
|
146
|
+
}
|
147
|
+
|
148
|
+
@keyframes idle {
|
149
|
+
from {
|
150
|
+
stroke-dashoffset: 24;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
.rating label:hover svg {
|
155
|
+
stroke: var(--fill);
|
156
|
+
}
|
157
|
+
|
158
|
+
/* 填充的星星样式 */
|
159
|
+
.rating label.star-filled svg {
|
160
|
+
transition: 0s;
|
161
|
+
animation: idle 4s linear infinite, yippee 0.75s backwards;
|
162
|
+
fill: var(--fill);
|
163
|
+
stroke: var(--fill);
|
164
|
+
stroke-opacity: 0;
|
165
|
+
stroke-dasharray: 0;
|
166
|
+
stroke-linejoin: miter;
|
167
|
+
stroke-width: 8px;
|
168
|
+
}
|
169
|
+
|
170
|
+
@keyframes yippee {
|
171
|
+
0% {
|
172
|
+
transform: scale(1);
|
173
|
+
fill: var(--fill);
|
174
|
+
fill-opacity: 0;
|
175
|
+
stroke-opacity: 1;
|
176
|
+
stroke: var(--stroke);
|
177
|
+
stroke-dasharray: 10;
|
178
|
+
stroke-width: 1px;
|
179
|
+
stroke-linejoin: bevel;
|
180
|
+
}
|
181
|
+
|
182
|
+
30% {
|
183
|
+
transform: scale(0);
|
184
|
+
fill: var(--fill);
|
185
|
+
fill-opacity: 0;
|
186
|
+
stroke-opacity: 1;
|
187
|
+
stroke: var(--stroke);
|
188
|
+
stroke-dasharray: 10;
|
189
|
+
stroke-width: 1px;
|
190
|
+
stroke-linejoin: bevel;
|
191
|
+
}
|
192
|
+
|
193
|
+
30.1% {
|
194
|
+
stroke: var(--fill);
|
195
|
+
stroke-dasharray: 0;
|
196
|
+
stroke-linejoin: miter;
|
197
|
+
stroke-width: 8px;
|
198
|
+
}
|
199
|
+
|
200
|
+
60% {
|
201
|
+
transform: scale(1.2);
|
202
|
+
fill: var(--fill);
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
/* 尺寸变体 */
|
207
|
+
.score-small .rating svg {
|
208
|
+
width: 1.2rem;
|
209
|
+
height: 1.2rem;
|
210
|
+
}
|
211
|
+
|
212
|
+
.score-medium .rating svg {
|
213
|
+
width: 2rem;
|
214
|
+
height: 2rem;
|
215
|
+
}
|
216
|
+
|
217
|
+
.score-large .rating svg {
|
218
|
+
width: 2.8rem;
|
219
|
+
height: 2.8rem;
|
220
|
+
}
|
221
|
+
|
222
|
+
/* 禁用状态 */
|
223
|
+
.score-disabled .rating label {
|
224
|
+
cursor: not-allowed;
|
225
|
+
opacity: 0.6;
|
226
|
+
}
|
227
|
+
|
228
|
+
.score-disabled .rating label:hover {
|
229
|
+
transform: none;
|
230
|
+
}
|
231
|
+
|
232
|
+
.score-disabled .rating label:hover svg {
|
233
|
+
stroke: var(--stroke);
|
234
|
+
}
|
235
|
+
|
236
|
+
/* 文字样式 */
|
237
|
+
.score-text {
|
238
|
+
font-size: 0.875rem;
|
239
|
+
color: #666;
|
240
|
+
font-weight: 500;
|
241
|
+
}
|
242
|
+
|
243
|
+
.score-value {
|
244
|
+
font-size: 0.75rem;
|
245
|
+
color: #999;
|
246
|
+
font-weight: 400;
|
247
|
+
}
|
248
|
+
|
249
|
+
.score-small .score-text {
|
250
|
+
font-size: 0.75rem;
|
251
|
+
}
|
252
|
+
|
253
|
+
.score-small .score-value {
|
254
|
+
font-size: 0.625rem;
|
255
|
+
}
|
256
|
+
|
257
|
+
.score-large .score-text {
|
258
|
+
font-size: 1rem;
|
259
|
+
}
|
260
|
+
|
261
|
+
.score-large .score-value {
|
262
|
+
font-size: 0.875rem;
|
263
|
+
}
|
264
|
+
</style>
|
@@ -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
@@ -13,10 +13,14 @@ export { default as XThumbnailContainer } from './components/ThumbnailContainer/
|
|
13
13
|
export { default as XSkeleton } from './components/Skeleton/index.vue'
|
14
14
|
export { default as XModal } from './components/Modal/index.vue'
|
15
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'
|
16
19
|
|
17
20
|
// 导出 Composables
|
18
21
|
export { xlMsg, default as useMsg } from './composables/Msg'
|
19
22
|
export { xlAlert, default as useAlert } from './composables/Alert'
|
23
|
+
export { useDateTime, useDateRange, useCountdown, default as useDateTimeDefault } from './composables/DateTime'
|
20
24
|
|
21
25
|
// 导出类型定义
|
22
26
|
export type { Country, CountrySelectProps } from './components/CountrySelect/index.vue'
|
@@ -28,4 +32,8 @@ export type { ThumbnailContainerProps } from './components/ThumbnailContainer/in
|
|
28
32
|
export type { SkeletonProps } from './components/Skeleton/index.vue'
|
29
33
|
export type { ModalProps } from './components/Modal/index.vue'
|
30
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'
|
31
39
|
|