@veltra/desktop 1.7.5 → 1.7.7

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,278 @@
1
+ import { date } from '@cat-kit/core'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createApp, h, nextTick, ref } from 'vue'
4
+
5
+ import UDateRangePicker from '../date-range-picker.vue'
6
+
7
+ function mountDateRangePicker(
8
+ props: Record<string, unknown> = {},
9
+ emits: Record<string, Function> = {}
10
+ ) {
11
+ const host = document.createElement('div')
12
+ document.body.appendChild(host)
13
+
14
+ const model = ref(props.modelValue)
15
+ const app = createApp({
16
+ render() {
17
+ return h(UDateRangePicker, {
18
+ ...props,
19
+ modelValue: model.value,
20
+ 'onUpdate:modelValue': (value: unknown) => {
21
+ model.value = value
22
+ emits['update:modelValue']?.(value)
23
+ },
24
+ onChange: (dates?: [Date, Date]) => {
25
+ emits.change?.(dates)
26
+ }
27
+ })
28
+ }
29
+ })
30
+
31
+ app.component('Transition', (_, { slots }) => slots.default?.())
32
+
33
+ app.mount(host)
34
+
35
+ return {
36
+ host,
37
+ model,
38
+ app,
39
+ unmount() {
40
+ app.unmount()
41
+ host.remove()
42
+ }
43
+ }
44
+ }
45
+
46
+ describe('UDateRangePicker', () => {
47
+ it('defaults dataType to string and emits change with native Date tuple', async () => {
48
+ let changedDates: [Date, Date] | undefined
49
+ let updatedVal: unknown
50
+
51
+ const { host, unmount } = mountDateRangePicker(
52
+ {},
53
+ {
54
+ 'update:modelValue': (val: unknown) => {
55
+ updatedVal = val
56
+ },
57
+ change: (d?: [Date, Date]) => {
58
+ changedDates = d
59
+ }
60
+ }
61
+ )
62
+
63
+ try {
64
+ const inputEl = host.querySelector('input')
65
+ expect(inputEl).not.toBeNull()
66
+ inputEl?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
67
+ await nextTick()
68
+ await nextTick()
69
+
70
+ const cells = document.querySelectorAll<HTMLElement>('.u-date-panel__cell:not(.is-disabled)')
71
+ expect(cells.length).toBeGreaterThanOrEqual(20)
72
+
73
+ cells[10]!.click()
74
+ await nextTick()
75
+ cells[15]!.click()
76
+ await nextTick()
77
+
78
+ expect(Array.isArray(updatedVal)).toBe(true)
79
+ const range = updatedVal as string[]
80
+ expect(range.length).toBe(2)
81
+ expect(typeof range[0]).toBe('string')
82
+ expect(typeof range[1]).toBe('string')
83
+ expect(range[0]).toMatch(/^\d{4}-\d{2}-\d{2}$/)
84
+ expect(range[1]).toMatch(/^\d{4}-\d{2}-\d{2}$/)
85
+
86
+ expect(Array.isArray(changedDates)).toBe(true)
87
+ expect(changedDates![0] instanceof Date).toBe(true)
88
+ expect(changedDates![1] instanceof Date).toBe(true)
89
+ } finally {
90
+ unmount()
91
+ }
92
+ })
93
+
94
+ it('supports dataType="date" and emits native Date tuple for modelValue and change', async () => {
95
+ let changedDates: [Date, Date] | undefined
96
+ let updatedVal: unknown
97
+
98
+ const { host, unmount } = mountDateRangePicker(
99
+ { dataType: 'date', valueFormat: 'yyyyMMdd' },
100
+ {
101
+ 'update:modelValue': (val: unknown) => {
102
+ updatedVal = val
103
+ },
104
+ change: (d?: [Date, Date]) => {
105
+ changedDates = d
106
+ }
107
+ }
108
+ )
109
+
110
+ try {
111
+ const inputEl = host.querySelector('input')
112
+ inputEl?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
113
+ await nextTick()
114
+ await nextTick()
115
+
116
+ const cells = document.querySelectorAll<HTMLElement>('.u-date-panel__cell:not(.is-disabled)')
117
+ cells[10]!.click()
118
+ await nextTick()
119
+ cells[15]!.click()
120
+ await nextTick()
121
+
122
+ expect(Array.isArray(updatedVal)).toBe(true)
123
+ const range = updatedVal as unknown[]
124
+ expect(range[0] instanceof Date).toBe(true)
125
+ expect(range[1] instanceof Date).toBe(true)
126
+
127
+ expect(Array.isArray(changedDates)).toBe(true)
128
+ expect(changedDates![0] instanceof Date).toBe(true)
129
+ expect(changedDates![1] instanceof Date).toBe(true)
130
+ } finally {
131
+ unmount()
132
+ }
133
+ })
134
+
135
+ it('supports dataType="timestamp" and emits timestamp tuple for modelValue and native Date tuple for change', async () => {
136
+ let changedDates: [Date, Date] | undefined
137
+ let updatedVal: unknown
138
+
139
+ const { host, unmount } = mountDateRangePicker(
140
+ { dataType: 'timestamp', valueFormat: 'yyyyMMdd' },
141
+ {
142
+ 'update:modelValue': (val: unknown) => {
143
+ updatedVal = val
144
+ },
145
+ change: (d?: [Date, Date]) => {
146
+ changedDates = d
147
+ }
148
+ }
149
+ )
150
+
151
+ try {
152
+ const inputEl = host.querySelector('input')
153
+ inputEl?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
154
+ await nextTick()
155
+ await nextTick()
156
+
157
+ const cells = document.querySelectorAll<HTMLElement>('.u-date-panel__cell:not(.is-disabled)')
158
+ cells[10]!.click()
159
+ await nextTick()
160
+ cells[15]!.click()
161
+ await nextTick()
162
+
163
+ expect(Array.isArray(updatedVal)).toBe(true)
164
+ const range = updatedVal as number[]
165
+ expect(typeof range[0]).toBe('number')
166
+ expect(typeof range[1]).toBe('number')
167
+ expect(range[0]).toBeGreaterThan(0)
168
+ expect(range[1]).toBeGreaterThan(0)
169
+
170
+ expect(Array.isArray(changedDates)).toBe(true)
171
+ expect(changedDates![0] instanceof Date).toBe(true)
172
+ expect(changedDates![1] instanceof Date).toBe(true)
173
+ } finally {
174
+ unmount()
175
+ }
176
+ })
177
+
178
+ it('applies valueFormat when dataType="string"', async () => {
179
+ let changedDates: [Date, Date] | undefined
180
+ let updatedVal: unknown
181
+
182
+ const { host, unmount } = mountDateRangePicker(
183
+ { dataType: 'string', valueFormat: 'yyyy/MM/dd' },
184
+ {
185
+ 'update:modelValue': (val: unknown) => {
186
+ updatedVal = val
187
+ },
188
+ change: (d?: [Date, Date]) => {
189
+ changedDates = d
190
+ }
191
+ }
192
+ )
193
+
194
+ try {
195
+ const inputEl = host.querySelector('input')
196
+ inputEl?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
197
+ await nextTick()
198
+ await nextTick()
199
+
200
+ const cells = document.querySelectorAll<HTMLElement>('.u-date-panel__cell:not(.is-disabled)')
201
+ cells[10]!.click()
202
+ await nextTick()
203
+ cells[15]!.click()
204
+ await nextTick()
205
+
206
+ expect(Array.isArray(updatedVal)).toBe(true)
207
+ const range = updatedVal as string[]
208
+ expect(range[0]).toMatch(/^\d{4}\/\d{2}\/\d{2}$/)
209
+ expect(range[1]).toMatch(/^\d{4}\/\d{2}\/\d{2}$/)
210
+ expect(changedDates![0] instanceof Date).toBe(true)
211
+ expect(changedDates![1] instanceof Date).toBe(true)
212
+ } finally {
213
+ unmount()
214
+ }
215
+ })
216
+
217
+ it('emits undefined for modelValue and change when cleared', async () => {
218
+ let changedDates: [Date, Date] | undefined = [new Date(), new Date()]
219
+ let updatedVal: unknown = ['2024-05-01', '2024-05-10']
220
+
221
+ const { host, unmount } = mountDateRangePicker(
222
+ { modelValue: ['2024-05-01', '2024-05-10'], clearable: true },
223
+ {
224
+ 'update:modelValue': (val: unknown) => {
225
+ updatedVal = val
226
+ },
227
+ change: (d?: [Date, Date]) => {
228
+ changedDates = d
229
+ }
230
+ }
231
+ )
232
+
233
+ try {
234
+ const dropdown = host.firstElementChild as HTMLElement
235
+ dropdown.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
236
+ await new Promise((r) => setTimeout(r, 50))
237
+
238
+ const clearBtn = host.querySelector('.u-date-range-picker__clear') as HTMLElement
239
+ expect(clearBtn).not.toBeNull()
240
+ clearBtn.click()
241
+ await nextTick()
242
+
243
+ expect(updatedVal).toBeUndefined()
244
+ expect(changedDates).toBeUndefined()
245
+ } finally {
246
+ unmount()
247
+ }
248
+ })
249
+
250
+ it('correctly parses modelValue of Date tuple, timestamp tuple, and custom formatted strings', async () => {
251
+ const dates: [Date, Date] = [new Date(2024, 4, 15), new Date(2024, 4, 20)]
252
+ const d1 = mountDateRangePicker({ modelValue: dates })
253
+ await nextTick()
254
+ const inputs1 = d1.host.querySelectorAll('input')
255
+ expect(inputs1[0]?.value).toBe('2024-05-15')
256
+ expect(inputs1[1]?.value).toBe('2024-05-20')
257
+ d1.unmount()
258
+
259
+ const ts1 = date('2024-06-20').timestamp
260
+ const ts2 = date('2024-06-25').timestamp
261
+ const d2 = mountDateRangePicker({ modelValue: [ts1, ts2] })
262
+ await nextTick()
263
+ const inputs2 = d2.host.querySelectorAll('input')
264
+ expect(inputs2[0]?.value).toBe('2024-06-20')
265
+ expect(inputs2[1]?.value).toBe('2024-06-25')
266
+ d2.unmount()
267
+
268
+ const d3 = mountDateRangePicker({
269
+ modelValue: ['20240725', '20240730'],
270
+ valueFormat: 'yyyyMMdd'
271
+ })
272
+ await nextTick()
273
+ const inputs3 = d3.host.querySelectorAll('input')
274
+ expect(inputs3[0]?.value).toBe('2024-07-25')
275
+ expect(inputs3[1]?.value).toBe('2024-07-30')
276
+ d3.unmount()
277
+ })
278
+ })
@@ -31,7 +31,7 @@
31
31
 
32
32
  <Transition name="zoom-in" mode="out-in">
33
33
  <UIcon
34
- v-if="clearable && modelValue && hovered && !disabled"
34
+ v-if="clearable && (modelValue?.length || currentRangeDate) && hovered && !disabled"
35
35
  :class="[cls.e('icon'), cls.e('clear')]"
36
36
  title="清除"
37
37
  @click.stop="handleClear"
@@ -62,14 +62,19 @@
62
62
  </template>
63
63
 
64
64
  <script lang="ts" setup>
65
- import { date, type Dater } from '@cat-kit/core'
65
+ import { date, Dater } from '@cat-kit/core'
66
66
  import { useFormFallbackProps, useUserAction } from '@veltra/compositions'
67
67
  import { Calendar, Close } from '@veltra/icons/normal'
68
68
  import { bem, FORM_EMPTY_CONTENT } from '@veltra/utils'
69
69
  import { injectFormContext } from '@veltra/utils'
70
70
  import { computed, shallowRef, watch } from 'vue'
71
71
 
72
- import type { DateRangePickerEmits, DateRangePickerProps, DropdownExposed } from '../../types'
72
+ import type {
73
+ DateRangePickerEmits,
74
+ DateRangePickerProps,
75
+ DateRangeValue,
76
+ DropdownExposed
77
+ } from '../../types'
73
78
  import { UDatePanel } from '../date-panel'
74
79
  import { UDropdown } from '../dropdown'
75
80
  import { UIcon } from '../icon'
@@ -79,6 +84,7 @@ defineOptions({ name: 'UDateRangePicker', inheritAttrs: false })
79
84
  const props = withDefaults(defineProps<DateRangePickerProps>(), {
80
85
  placeholder: () => ['起始日期', '结束日期'],
81
86
  type: 'date',
87
+ dataType: 'string',
82
88
  disabled: undefined,
83
89
  readonly: undefined,
84
90
  clearable: true
@@ -113,14 +119,6 @@ const { userAction, isUserActive } = useUserAction()
113
119
 
114
120
  const currentRangeDate = shallowRef<[Dater, Dater]>()
115
121
 
116
- const displayedOfStart = computed(() => {
117
- return props.modelValue?.[0]
118
- })
119
-
120
- const displayedOfEnd = computed(() => {
121
- return props.modelValue?.[1]
122
- })
123
-
124
122
  const formatStr = computed(() => {
125
123
  const { format, type } = props
126
124
  if (format) return format
@@ -130,25 +128,70 @@ const formatStr = computed(() => {
130
128
  return 'yyyy-MM-dd'
131
129
  })
132
130
 
131
+ const displayedOfStart = computed(() => {
132
+ return currentRangeDate.value?.[0]?.format(formatStr.value) ?? ''
133
+ })
134
+
135
+ const displayedOfEnd = computed(() => {
136
+ return currentRangeDate.value?.[1]?.format(formatStr.value) ?? ''
137
+ })
138
+
139
+ function parseDateValue(val?: string | number | Date): Dater | undefined {
140
+ if (val == null || val === '') return undefined
141
+ if (val instanceof Date || typeof val === 'number') {
142
+ const d = date(val)
143
+ return isNaN(d.timestamp) ? undefined : d
144
+ }
145
+ if (typeof val === 'string') {
146
+ if (props.dataType === 'string' && props.valueFormat) {
147
+ const parsed = Dater.parse(val, props.valueFormat)
148
+ if (!isNaN(parsed.timestamp)) return parsed
149
+ }
150
+ const fallback = date(val)
151
+ return isNaN(fallback.timestamp) ? undefined : fallback
152
+ }
153
+ return undefined
154
+ }
155
+
133
156
  watch(
134
157
  () => props.modelValue,
135
158
  (val) => {
136
159
  if (isUserActive()) return
137
- if (val?.length === 2) {
138
- currentRangeDate.value = [date(val[0]), date(val[1])]
139
- } else {
140
- currentRangeDate.value = undefined
160
+ if (val && val.length === 2) {
161
+ const d1 = parseDateValue(val[0])
162
+ const d2 = parseDateValue(val[1])
163
+ if (d1 && d2) {
164
+ currentRangeDate.value = [d1, d2]
165
+ return
166
+ }
141
167
  }
168
+ currentRangeDate.value = undefined
142
169
  },
143
170
  { immediate: true }
144
171
  )
145
172
 
146
- const commitSelectedRange = userAction((rangeDate: [Dater, Dater] | undefined) => {
173
+ function formatDateValue(d: Dater) {
174
+ if (props.dataType === 'date') {
175
+ return d.raw
176
+ }
177
+ if (props.dataType === 'timestamp') {
178
+ return d.timestamp
179
+ }
180
+ return d.format(props.valueFormat ?? formatStr.value)
181
+ }
182
+
183
+ function formatModelValue(range: [Dater, Dater]): DateRangeValue {
184
+ return [formatDateValue(range[0]), formatDateValue(range[1])] as DateRangeValue
185
+ }
186
+
187
+ const commitSelectedRange = userAction((rangeDate: [Dater, Dater]) => {
147
188
  currentRangeDate.value = rangeDate
148
- emit('update:modelValue', rangeDate?.map((d) => d.format(formatStr.value)) as [string, string])
189
+ emit('update:modelValue', formatModelValue(rangeDate))
190
+ emit('change', [rangeDate[0].raw, rangeDate[1].raw])
149
191
  })
150
192
 
151
193
  async function handleSelect(rangeDate: [Dater, Dater] | undefined) {
194
+ if (!rangeDate) return
152
195
  await commitSelectedRange(rangeDate)
153
196
  dropdownRef.value?.close()
154
197
  }
@@ -158,5 +201,6 @@ const hovered = shallowRef(false)
158
201
  function handleClear() {
159
202
  currentRangeDate.value = undefined
160
203
  emit('update:modelValue', undefined)
204
+ emit('change', undefined)
161
205
  }
162
206
  </script>
@@ -14,7 +14,7 @@ export type CodeEditorLang =
14
14
  export interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
15
15
  modelValue?: string
16
16
  /**
17
- * 可选语言列表。长度大于 1 时在编辑器右上角显示内置语言选择器;
17
+ * 可选语言列表。长度大于 1 时在顶部工具栏显示内置语言选择器;
18
18
  * 仅一种语言时显示语言名称标签
19
19
  */
20
20
  langs?: CodeEditorLang[]
@@ -26,6 +26,8 @@ export interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
26
26
  suffix?: string
27
27
  /** 是否使用暗色主题 */
28
28
  dark?: boolean
29
+ /** 是否显示放大按钮,默认 true;false 时不渲染放大按钮 */
30
+ zoomable?: boolean
29
31
  /**
30
32
  * 默认显示的行数,用于撑起编辑器最小高度,超出后滚动
31
33
  * @default 8
@@ -1,9 +1,15 @@
1
1
  import type { Dater } from '@cat-kit/core'
2
2
  import type { DeconstructValue, FormComponentProps } from '@veltra/utils'
3
3
 
4
+ /** 绑定值类型 */
5
+ export type DateRangePickerDataType = 'date' | 'timestamp' | 'string'
6
+
7
+ /** 日期范围绑定值 */
8
+ export type DateRangeValue = [string, string] | [number, number] | [Date, Date]
9
+
4
10
  /** date-range-picker组件属性 */
5
11
  export interface DateRangePickerProps extends FormComponentProps {
6
- modelValue?: [string, string]
12
+ modelValue?: DateRangeValue
7
13
  /** 占位 */
8
14
  placeholder?: [string, string]
9
15
  /** 日期类型 */
@@ -12,6 +18,11 @@ export interface DateRangePickerProps extends FormComponentProps {
12
18
  format?: string
13
19
  /** 日期值格式化, 当没有指定时默认使用format属性,仅当值和显示的内容不一致时才需要使用到该属性 */
14
20
  valueFormat?: string
21
+ /**
22
+ * 绑定值的类型,默认为字符串。
23
+ * 当 dataType 没有指定为字符串时,valueFormat 属性不生效
24
+ */
25
+ dataType?: DateRangePickerDataType
15
26
  /** 最小可选日期 */
16
27
  disabledDate?: (date: Dater, raw: Date) => boolean
17
28
  /** 是否显示清除按钮 */
@@ -20,7 +31,8 @@ export interface DateRangePickerProps extends FormComponentProps {
20
31
 
21
32
  /** date-range-picker组件定义的事件 */
22
33
  export interface DateRangePickerEmits {
23
- (e: 'update:modelValue', value?: [string, string]): void
34
+ (e: 'update:modelValue', value?: DateRangeValue): void
35
+ (e: 'change', dates?: [Date, Date]): void
24
36
  }
25
37
 
26
38
  /** date-range-picker组件暴露的属性和方法(组件内部使用) */