niuma-ui 1.0.0 → 1.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/CHANGELOG.md +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- package/src/components/RsTableHeaderRow.vue +0 -30
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, watch } from 'vue'
|
|
2
3
|
import {
|
|
3
4
|
ContextMenuContent,
|
|
4
5
|
ContextMenuPortal,
|
|
5
6
|
ContextMenuRoot,
|
|
6
7
|
ContextMenuTrigger,
|
|
7
8
|
} from './reka'
|
|
8
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
hasActionableContextMenuItems,
|
|
11
|
+
type RsContextMenuItem,
|
|
12
|
+
} from './context-menu-utils'
|
|
9
13
|
import RsContextMenuItems from './RsContextMenuItems.vue'
|
|
10
14
|
|
|
11
|
-
defineProps<{
|
|
15
|
+
const props = defineProps<{
|
|
12
16
|
items: RsContextMenuItem[]
|
|
13
17
|
/** 禁用时不拦截右键事件,直接透传 slot,浏览器原生菜单正常显示 */
|
|
14
18
|
disabled?: boolean
|
|
15
19
|
}>()
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
22
|
+
* 展开状态。不绑定时组件自行维护(与原行为一致)。
|
|
19
23
|
* 绑定后可由外部主动关闭——例如菜单所依赖的数据已失效时。
|
|
20
24
|
*/
|
|
21
25
|
const open = defineModel<boolean>('open', { default: false })
|
|
@@ -24,14 +28,49 @@ const emit = defineEmits<{
|
|
|
24
28
|
select: [key: string]
|
|
25
29
|
}>()
|
|
26
30
|
|
|
31
|
+
/** 无有效菜单项时不打开,避免空表/空白区右键出现空气泡 */
|
|
32
|
+
const hasActionableItems = computed(() => hasActionableContextMenuItems(props.items))
|
|
33
|
+
|
|
34
|
+
watch(hasActionableItems, (ok) => {
|
|
35
|
+
if (!ok && open.value) open.value = false
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function onUpdateOpen(next: boolean): void {
|
|
39
|
+
if (next && !hasActionableItems.value) {
|
|
40
|
+
open.value = false
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
open.value = next
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onTriggerContextMenu(event: MouseEvent): void {
|
|
47
|
+
if (!hasActionableItems.value) {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
open.value = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
function onSelect(item: RsContextMenuItem) {
|
|
28
54
|
emit('select', item.key)
|
|
29
55
|
}
|
|
30
56
|
</script>
|
|
31
57
|
|
|
32
58
|
<template>
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
<!--
|
|
60
|
+
as-child:把触发器 props 合并到 slot 唯一根节点,避免 span + display:contents
|
|
61
|
+
在 Teleport/Presence 开关时把子 DOM 移出 Vue 追踪(__vnode null)。
|
|
62
|
+
Content 不用 v-if:由 Root open + Presence 管理显隐,避免与 items 更新竞态。
|
|
63
|
+
-->
|
|
64
|
+
<ContextMenuRoot v-if="!disabled" :open="open" @update:open="onUpdateOpen">
|
|
65
|
+
<!--
|
|
66
|
+
不因 items 为空禁用 Trigger:表格等场景会在同一次右键里先同步填充 items,
|
|
67
|
+
若 Trigger 已 disabled,Reka 不会打开菜单(与子节点填充产生竞态)。
|
|
68
|
+
空菜单仍由 onUpdateOpen / onTriggerContextMenu 拦截。
|
|
69
|
+
-->
|
|
70
|
+
<ContextMenuTrigger
|
|
71
|
+
as-child
|
|
72
|
+
@contextmenu.stop="onTriggerContextMenu"
|
|
73
|
+
>
|
|
35
74
|
<slot />
|
|
36
75
|
</ContextMenuTrigger>
|
|
37
76
|
<ContextMenuPortal>
|
|
@@ -40,7 +79,11 @@ function onSelect(item: RsContextMenuItem) {
|
|
|
40
79
|
:side-offset="4"
|
|
41
80
|
:collision-padding="8"
|
|
42
81
|
>
|
|
43
|
-
<RsContextMenuItems
|
|
82
|
+
<RsContextMenuItems
|
|
83
|
+
v-if="hasActionableItems"
|
|
84
|
+
:items="items"
|
|
85
|
+
@select="onSelect"
|
|
86
|
+
/>
|
|
44
87
|
</ContextMenuContent>
|
|
45
88
|
</ContextMenuPortal>
|
|
46
89
|
</ContextMenuRoot>
|
|
@@ -48,16 +91,11 @@ function onSelect(item: RsContextMenuItem) {
|
|
|
48
91
|
</template>
|
|
49
92
|
|
|
50
93
|
<style>
|
|
51
|
-
/* 触发器包装:display:contents 令 span 不产生盒模型,不影响父级 flex/grid 布局。
|
|
52
|
-
ContextMenuTrigger 默认渲染为 span,加此 class 使其透明于布局。 */
|
|
53
|
-
.rs-ctx-trigger {
|
|
54
|
-
display: contents;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
94
|
/* ── 菜单容器:macOS 毛玻璃浮层 ── */
|
|
58
95
|
.rs-context-menu__content,
|
|
59
96
|
.rs-context-menu__sub-content {
|
|
60
|
-
|
|
97
|
+
/* 与 Select/DatePicker 一致:Portal 挂到 body 时须高于 modal,否则 Dialog 内右键会被挡住 */
|
|
98
|
+
z-index: calc(var(--rs-z-modal) + 2);
|
|
61
99
|
box-sizing: border-box;
|
|
62
100
|
min-width: 11rem;
|
|
63
101
|
max-width: min(22rem, calc(100vw - 1rem));
|
|
@@ -5,6 +5,14 @@ import RsCalendarGrid from './RsCalendarGrid.vue'
|
|
|
5
5
|
import RsIcon from './RsIcon.vue'
|
|
6
6
|
import RsTimePicker from './RsTimePicker.vue'
|
|
7
7
|
import { useRsI18n } from '../composables/useRsI18n'
|
|
8
|
+
import { RS_COMPONENT_SIZE_ICON_PX, type RsComponentSize } from '../theme/types'
|
|
9
|
+
import { useResolvedRsComponentSize } from './resolve-size'
|
|
10
|
+
import { useRsFormContext, useRsFormField } from './form-utils'
|
|
11
|
+
import {
|
|
12
|
+
buildLocalInputRules,
|
|
13
|
+
runFormFieldRules,
|
|
14
|
+
type RsFormRuleTrigger,
|
|
15
|
+
} from './form-rules'
|
|
8
16
|
import {
|
|
9
17
|
compareDates,
|
|
10
18
|
EMPTY_DATE_RANGE,
|
|
@@ -17,29 +25,43 @@ import {
|
|
|
17
25
|
formatDateTimeRangeLabel,
|
|
18
26
|
getNextMonth,
|
|
19
27
|
getNowDateTime,
|
|
28
|
+
formatTimestampValue,
|
|
20
29
|
getTodayDate,
|
|
21
30
|
isDateRangeEmpty,
|
|
22
31
|
isDateRangeOrdered,
|
|
23
32
|
isDateWithinBounds,
|
|
33
|
+
isTimestampRange,
|
|
34
|
+
normalizeShortcutValue,
|
|
24
35
|
parseDateTimeValue,
|
|
25
36
|
parseDateValue,
|
|
37
|
+
parseTimestampValue,
|
|
38
|
+
type RsDatePickerModelValue,
|
|
39
|
+
type RsDatePickerShortcut,
|
|
40
|
+
type RsDatePickerValueFormat,
|
|
26
41
|
type RsDateRangeValue,
|
|
27
42
|
type RsParsedDate,
|
|
28
43
|
} from './date-picker-utils'
|
|
29
44
|
import { formatTimeParts } from './time-picker-utils'
|
|
30
45
|
|
|
31
|
-
export type {
|
|
46
|
+
export type {
|
|
47
|
+
RsDatePickerModelValue,
|
|
48
|
+
RsDatePickerShortcut,
|
|
49
|
+
RsDatePickerValueFormat,
|
|
50
|
+
RsDateRangeValue,
|
|
51
|
+
} from './date-picker-utils'
|
|
32
52
|
export type RsDatePickerLabelPosition = 'top' | 'left'
|
|
33
53
|
|
|
34
|
-
function isRangeModel(value:
|
|
35
|
-
return typeof value === 'object' && value !== null && ('start' in value || 'end' in value)
|
|
54
|
+
function isRangeModel(value: RsDatePickerModelValue): value is RsDateRangeValue {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && ('start' in value || 'end' in value)
|
|
36
56
|
}
|
|
37
57
|
|
|
38
|
-
const model = defineModel<
|
|
58
|
+
const model = defineModel<RsDatePickerModelValue>({ default: '' })
|
|
39
59
|
const open = defineModel<boolean>('open', { default: false })
|
|
40
60
|
|
|
41
61
|
const props = withDefaults(
|
|
42
62
|
defineProps<{
|
|
63
|
+
/** 表单字段名,用于 RsForm rules 匹配与校验注册 */
|
|
64
|
+
name?: string
|
|
43
65
|
label?: string
|
|
44
66
|
hint?: string
|
|
45
67
|
placeholder?: string
|
|
@@ -49,21 +71,37 @@ const props = withDefaults(
|
|
|
49
71
|
minDate?: string
|
|
50
72
|
maxDate?: string
|
|
51
73
|
withTime?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* 是否选择秒。未传时:withTime 为 true 则默认带秒,纯日期为 false。
|
|
76
|
+
*/
|
|
52
77
|
withSeconds?: boolean
|
|
78
|
+
/** string:YYYY-MM-DD[/HH:mm:ss];timestamp:毫秒时间戳 / [start, end] */
|
|
79
|
+
valueFormat?: RsDatePickerValueFormat
|
|
53
80
|
labelPosition?: RsDatePickerLabelPosition
|
|
81
|
+
/** 底部快捷选项(有值时替代「今天/现在」链接) */
|
|
82
|
+
shortcuts?: RsDatePickerShortcut[]
|
|
83
|
+
size?: RsComponentSize
|
|
54
84
|
}>(),
|
|
55
85
|
{
|
|
56
86
|
disabled: false,
|
|
57
87
|
required: false,
|
|
58
88
|
range: false,
|
|
59
89
|
withTime: false,
|
|
60
|
-
|
|
90
|
+
valueFormat: 'string',
|
|
61
91
|
labelPosition: 'top',
|
|
62
92
|
},
|
|
63
93
|
)
|
|
64
94
|
|
|
65
95
|
const fieldId = useId()
|
|
66
96
|
const { t } = useRsI18n()
|
|
97
|
+
const resolvedSize = useResolvedRsComponentSize(() => props.size)
|
|
98
|
+
const triggerIconSize = computed(() => RS_COMPONENT_SIZE_ICON_PX[resolvedSize.value])
|
|
99
|
+
/** withTime 默认带秒;显式 withSeconds 优先(勿与 prop 同名,避免模板取到未传 prop) */
|
|
100
|
+
const resolvedWithSeconds = computed(() =>
|
|
101
|
+
props.withSeconds !== undefined ? props.withSeconds : props.withTime,
|
|
102
|
+
)
|
|
103
|
+
const formContext = useRsFormContext()
|
|
104
|
+
const autoMessage = ref('')
|
|
67
105
|
|
|
68
106
|
const viewYear = ref(getTodayDate().year)
|
|
69
107
|
const viewMonth = ref(getTodayDate().month)
|
|
@@ -79,12 +117,28 @@ const draftTime = ref('')
|
|
|
79
117
|
const draftStartTime = ref('')
|
|
80
118
|
const draftEndTime = ref('')
|
|
81
119
|
|
|
120
|
+
const useTimestamp = computed(() => props.valueFormat === 'timestamp')
|
|
121
|
+
|
|
82
122
|
const rangeModel = computed<RsDateRangeValue>({
|
|
83
123
|
get() {
|
|
84
124
|
if (!props.range) return { ...EMPTY_DATE_RANGE }
|
|
85
|
-
|
|
125
|
+
const value = model.value
|
|
126
|
+
if (useTimestamp.value) {
|
|
127
|
+
if (!isTimestampRange(value)) return { ...EMPTY_DATE_RANGE }
|
|
128
|
+
return {
|
|
129
|
+
start: formatTimestampValue(value[0], props.withTime),
|
|
130
|
+
end: formatTimestampValue(value[1], props.withTime),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return isRangeModel(value) ? value : { ...EMPTY_DATE_RANGE }
|
|
86
134
|
},
|
|
87
135
|
set(value) {
|
|
136
|
+
if (useTimestamp.value) {
|
|
137
|
+
const start = parseTimestampValue(value.start)
|
|
138
|
+
const end = parseTimestampValue(value.end)
|
|
139
|
+
model.value = start != null && end != null ? [start, end] : null
|
|
140
|
+
return
|
|
141
|
+
}
|
|
88
142
|
model.value = value
|
|
89
143
|
},
|
|
90
144
|
})
|
|
@@ -92,9 +146,17 @@ const rangeModel = computed<RsDateRangeValue>({
|
|
|
92
146
|
const singleModel = computed({
|
|
93
147
|
get() {
|
|
94
148
|
if (props.range) return ''
|
|
95
|
-
|
|
149
|
+
const value = model.value
|
|
150
|
+
if (useTimestamp.value) {
|
|
151
|
+
return typeof value === 'number' ? formatTimestampValue(value, props.withTime) : ''
|
|
152
|
+
}
|
|
153
|
+
return typeof value === 'string' ? value : ''
|
|
96
154
|
},
|
|
97
155
|
set(value: string) {
|
|
156
|
+
if (useTimestamp.value) {
|
|
157
|
+
model.value = value ? parseTimestampValue(value) : null
|
|
158
|
+
return
|
|
159
|
+
}
|
|
98
160
|
model.value = value
|
|
99
161
|
},
|
|
100
162
|
})
|
|
@@ -139,7 +201,7 @@ const triggerIcon = computed(() => {
|
|
|
139
201
|
|
|
140
202
|
function currentTimeValue(): string {
|
|
141
203
|
const now = getNowDateTime()
|
|
142
|
-
return formatTimeParts(now.hour, now.minute,
|
|
204
|
+
return formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
|
|
143
205
|
}
|
|
144
206
|
|
|
145
207
|
function resetRangeTimeDrafts(): void {
|
|
@@ -149,7 +211,7 @@ function resetRangeTimeDrafts(): void {
|
|
|
149
211
|
}
|
|
150
212
|
|
|
151
213
|
function combineDraftDateTime(date: RsParsedDate, time: string): string | null {
|
|
152
|
-
return formatDateTimeParts(date, time,
|
|
214
|
+
return formatDateTimeParts(date, time, resolvedWithSeconds.value)
|
|
153
215
|
}
|
|
154
216
|
|
|
155
217
|
function setEndViewFromStart(start: RsParsedDate): void {
|
|
@@ -163,7 +225,7 @@ function syncSingleDraft(): void {
|
|
|
163
225
|
const parsed = parseDateTimeValue(singleModel.value)
|
|
164
226
|
if (parsed) {
|
|
165
227
|
draftDate.value = { year: parsed.year, month: parsed.month, day: parsed.day }
|
|
166
|
-
draftTime.value = extractTimeFromDateTime(singleModel.value,
|
|
228
|
+
draftTime.value = extractTimeFromDateTime(singleModel.value, resolvedWithSeconds.value)
|
|
167
229
|
viewYear.value = parsed.year
|
|
168
230
|
viewMonth.value = parsed.month
|
|
169
231
|
return
|
|
@@ -205,7 +267,7 @@ function syncRangeDraft(): void {
|
|
|
205
267
|
draftEnd.value = end ? { year: end.year, month: end.month, day: end.day } : null
|
|
206
268
|
|
|
207
269
|
if (start) {
|
|
208
|
-
draftStartTime.value = extractTimeFromDateTime(value.start,
|
|
270
|
+
draftStartTime.value = extractTimeFromDateTime(value.start, resolvedWithSeconds.value)
|
|
209
271
|
startViewYear.value = start.year
|
|
210
272
|
startViewMonth.value = start.month
|
|
211
273
|
} else {
|
|
@@ -215,7 +277,7 @@ function syncRangeDraft(): void {
|
|
|
215
277
|
}
|
|
216
278
|
|
|
217
279
|
if (end) {
|
|
218
|
-
draftEndTime.value = extractTimeFromDateTime(value.end,
|
|
280
|
+
draftEndTime.value = extractTimeFromDateTime(value.end, resolvedWithSeconds.value)
|
|
219
281
|
endViewYear.value = end.year
|
|
220
282
|
endViewMonth.value = end.month
|
|
221
283
|
} else if (start) {
|
|
@@ -329,14 +391,42 @@ function confirmSelection(): void {
|
|
|
329
391
|
confirmSingleSelection()
|
|
330
392
|
}
|
|
331
393
|
|
|
394
|
+
function applyShortcut(shortcut: RsDatePickerShortcut): void {
|
|
395
|
+
if (props.disabled) return
|
|
396
|
+
const normalized = normalizeShortcutValue(shortcut.value(), { withTime: props.withTime })
|
|
397
|
+
if (!normalized) return
|
|
398
|
+
|
|
399
|
+
if (props.range) {
|
|
400
|
+
const rangeValue: RsDateRangeValue =
|
|
401
|
+
typeof normalized === 'string' ? { start: normalized, end: normalized } : normalized
|
|
402
|
+
if (!rangeValue.start || !rangeValue.end) return
|
|
403
|
+
if (!isDateRangeOrdered(rangeValue)) return
|
|
404
|
+
rangeModel.value = rangeValue
|
|
405
|
+
open.value = false
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (typeof normalized !== 'string') return
|
|
410
|
+
singleModel.value = normalized
|
|
411
|
+
open.value = false
|
|
412
|
+
}
|
|
413
|
+
|
|
332
414
|
function clearSelection(): void {
|
|
333
415
|
if (props.range) {
|
|
334
|
-
|
|
416
|
+
if (useTimestamp.value) {
|
|
417
|
+
model.value = null
|
|
418
|
+
} else {
|
|
419
|
+
rangeModel.value = { ...EMPTY_DATE_RANGE }
|
|
420
|
+
}
|
|
335
421
|
draftStart.value = null
|
|
336
422
|
draftEnd.value = null
|
|
337
423
|
if (props.withTime) resetRangeTimeDrafts()
|
|
338
424
|
} else {
|
|
339
|
-
|
|
425
|
+
if (useTimestamp.value) {
|
|
426
|
+
model.value = null
|
|
427
|
+
} else {
|
|
428
|
+
singleModel.value = ''
|
|
429
|
+
}
|
|
340
430
|
draftDate.value = null
|
|
341
431
|
if (props.withTime) draftTime.value = currentTimeValue()
|
|
342
432
|
}
|
|
@@ -355,7 +445,7 @@ function selectToday(): void {
|
|
|
355
445
|
endViewYear.value = today.year
|
|
356
446
|
endViewMonth.value = today.month
|
|
357
447
|
if (props.withTime) {
|
|
358
|
-
const time = formatTimeParts(now.hour, now.minute,
|
|
448
|
+
const time = formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
|
|
359
449
|
draftStartTime.value = time
|
|
360
450
|
draftEndTime.value = time
|
|
361
451
|
}
|
|
@@ -367,7 +457,7 @@ function selectToday(): void {
|
|
|
367
457
|
viewYear.value = today.year
|
|
368
458
|
viewMonth.value = today.month
|
|
369
459
|
if (props.withTime) {
|
|
370
|
-
draftTime.value = formatTimeParts(now.hour, now.minute,
|
|
460
|
+
draftTime.value = formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
|
|
371
461
|
}
|
|
372
462
|
confirmSingleSelection()
|
|
373
463
|
}
|
|
@@ -417,8 +507,62 @@ function handleEndSelect(date: RsParsedDate): void {
|
|
|
417
507
|
endViewMonth.value = date.month
|
|
418
508
|
}
|
|
419
509
|
|
|
510
|
+
function clearValidation(): void {
|
|
511
|
+
autoMessage.value = ''
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function setFieldValue(value: unknown): void {
|
|
515
|
+
if (props.range) {
|
|
516
|
+
if (isRangeModel(value as RsDatePickerModelValue)) {
|
|
517
|
+
model.value = value as RsDateRangeValue
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
521
|
+
model.value = { start: String(value[0] ?? ''), end: String(value[1] ?? '') }
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
model.value = { ...EMPTY_DATE_RANGE }
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
model.value = value == null ? '' : (value as RsDatePickerModelValue)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function runValidate(trigger: RsFormRuleTrigger = 'submit') {
|
|
531
|
+
const formRules = formContext?.getFieldRules(props.name) ?? []
|
|
532
|
+
const localRules = buildLocalInputRules({ required: props.required })
|
|
533
|
+
const rules = [...formRules, ...localRules]
|
|
534
|
+
if (!rules.length) {
|
|
535
|
+
autoMessage.value = ''
|
|
536
|
+
return { valid: true as const, name: props.name }
|
|
537
|
+
}
|
|
538
|
+
const result = await runFormFieldRules(model.value, rules, { trigger })
|
|
539
|
+
autoMessage.value = result.message ?? ''
|
|
540
|
+
return { valid: result.valid, message: result.message, name: props.name }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
useRsFormField(() => ({
|
|
544
|
+
get name() {
|
|
545
|
+
return props.name
|
|
546
|
+
},
|
|
547
|
+
getValue: () => model.value,
|
|
548
|
+
setValue: setFieldValue,
|
|
549
|
+
validate: (trigger) => runValidate(trigger ?? 'submit'),
|
|
550
|
+
clearValidation,
|
|
551
|
+
setError: (message: string) => {
|
|
552
|
+
autoMessage.value = message
|
|
553
|
+
},
|
|
554
|
+
}))
|
|
555
|
+
|
|
556
|
+
defineExpose({
|
|
557
|
+
setValue: setFieldValue,
|
|
558
|
+
clearValidation,
|
|
559
|
+
validate: runValidate,
|
|
560
|
+
})
|
|
561
|
+
|
|
420
562
|
watch(open, (isOpen) => {
|
|
421
563
|
if (isOpen) syncDraftFromModel()
|
|
564
|
+
// 关闭面板后按 change 触发校验(对齐选择类控件)
|
|
565
|
+
if (!isOpen && props.name) void runValidate('change')
|
|
422
566
|
})
|
|
423
567
|
</script>
|
|
424
568
|
|
|
@@ -430,20 +574,24 @@ watch(open, (isOpen) => {
|
|
|
430
574
|
<span v-if="required" class="rs-field__required" aria-hidden="true">*</span>
|
|
431
575
|
</span>
|
|
432
576
|
|
|
433
|
-
<div class="rs-date-picker">
|
|
577
|
+
<div class="rs-date-picker" :class="`rs-date-picker--${resolvedSize}`">
|
|
434
578
|
<PopoverRoot v-model:open="open">
|
|
435
579
|
<PopoverTrigger
|
|
436
580
|
:id="fieldId"
|
|
437
581
|
type="button"
|
|
438
582
|
class="rs-date-picker__trigger"
|
|
439
|
-
:class="{
|
|
583
|
+
:class="{
|
|
584
|
+
'rs-date-picker__trigger--placeholder': isEmpty,
|
|
585
|
+
'rs-date-picker__trigger--invalid': !!autoMessage,
|
|
586
|
+
}"
|
|
440
587
|
:disabled="disabled"
|
|
588
|
+
:aria-invalid="autoMessage ? true : undefined"
|
|
441
589
|
>
|
|
442
590
|
<span class="rs-date-picker__leading">
|
|
443
|
-
<RsIcon :name="triggerIcon" :size="
|
|
591
|
+
<RsIcon :name="triggerIcon" :size="triggerIconSize" class="rs-date-picker__icon" />
|
|
444
592
|
<span class="rs-date-picker__value">{{ displayValue }}</span>
|
|
445
593
|
</span>
|
|
446
|
-
<RsIcon name="chevron-down" :size="
|
|
594
|
+
<RsIcon name="chevron-down" :size="triggerIconSize" class="rs-date-picker__chevron" />
|
|
447
595
|
</PopoverTrigger>
|
|
448
596
|
|
|
449
597
|
<PopoverPortal>
|
|
@@ -476,7 +624,8 @@ watch(open, (isOpen) => {
|
|
|
476
624
|
v-model="draftStartTime"
|
|
477
625
|
class="rs-date-picker__time-picker"
|
|
478
626
|
embedded
|
|
479
|
-
:
|
|
627
|
+
:size="resolvedSize"
|
|
628
|
+
:with-seconds="resolvedWithSeconds"
|
|
480
629
|
:disabled="!draftStart || disabled"
|
|
481
630
|
/>
|
|
482
631
|
</div>
|
|
@@ -502,7 +651,8 @@ watch(open, (isOpen) => {
|
|
|
502
651
|
v-model="draftEndTime"
|
|
503
652
|
class="rs-date-picker__time-picker"
|
|
504
653
|
embedded
|
|
505
|
-
:
|
|
654
|
+
:size="resolvedSize"
|
|
655
|
+
:with-seconds="resolvedWithSeconds"
|
|
506
656
|
:disabled="!draftEnd || disabled"
|
|
507
657
|
/>
|
|
508
658
|
</div>
|
|
@@ -525,24 +675,39 @@ watch(open, (isOpen) => {
|
|
|
525
675
|
v-model="draftTime"
|
|
526
676
|
class="rs-date-picker__time-picker"
|
|
527
677
|
embedded
|
|
528
|
-
:
|
|
678
|
+
:size="resolvedSize"
|
|
679
|
+
:with-seconds="resolvedWithSeconds"
|
|
529
680
|
:disabled="!draftDate || disabled"
|
|
530
681
|
/>
|
|
531
682
|
</div>
|
|
532
683
|
</template>
|
|
533
684
|
|
|
534
685
|
<footer class="rs-date-picker__footer">
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
686
|
+
<div class="rs-date-picker__footer-start">
|
|
687
|
+
<div v-if="shortcuts?.length" class="rs-date-picker__shortcuts">
|
|
688
|
+
<button
|
|
689
|
+
v-for="item in shortcuts"
|
|
690
|
+
:key="item.label"
|
|
691
|
+
type="button"
|
|
692
|
+
class="rs-date-picker__shortcut"
|
|
693
|
+
:disabled="disabled"
|
|
694
|
+
@click="applyShortcut(item)"
|
|
695
|
+
>
|
|
696
|
+
{{ item.label }}
|
|
697
|
+
</button>
|
|
698
|
+
</div>
|
|
699
|
+
<button v-else type="button" class="rs-date-picker__link" @click="selectToday">
|
|
700
|
+
{{
|
|
701
|
+
range
|
|
702
|
+
? withTime
|
|
703
|
+
? t('dateTimePicker.now')
|
|
704
|
+
: t('datePicker.rangeToday')
|
|
705
|
+
: withTime
|
|
706
|
+
? t('dateTimePicker.now')
|
|
707
|
+
: t('datePicker.today')
|
|
708
|
+
}}
|
|
709
|
+
</button>
|
|
710
|
+
</div>
|
|
546
711
|
<div class="rs-date-picker__actions">
|
|
547
712
|
<button type="button" class="rs-date-picker__ghost" @click="clearSelection">
|
|
548
713
|
{{ withTime ? t('dateTimePicker.clear') : t('datePicker.clear') }}
|
|
@@ -563,6 +728,9 @@ watch(open, (isOpen) => {
|
|
|
563
728
|
</PopoverRoot>
|
|
564
729
|
</div>
|
|
565
730
|
|
|
731
|
+
<p v-if="autoMessage" class="rs-date-picker-field__error" role="alert">
|
|
732
|
+
{{ autoMessage }}
|
|
733
|
+
</p>
|
|
566
734
|
<span v-if="hint" class="rs-field__hint">{{ hint }}</span>
|
|
567
735
|
</div>
|
|
568
736
|
</template>
|
|
@@ -576,7 +744,9 @@ watch(open, (isOpen) => {
|
|
|
576
744
|
align-items: center;
|
|
577
745
|
justify-content: space-between;
|
|
578
746
|
gap: var(--rs-space-sm);
|
|
747
|
+
box-sizing: border-box;
|
|
579
748
|
width: 100%;
|
|
749
|
+
height: var(--rs-control-height-md);
|
|
580
750
|
min-height: var(--rs-control-height-md);
|
|
581
751
|
padding: 0 var(--rs-space-md);
|
|
582
752
|
border: 1px solid var(--rs-input-border, var(--rs-border));
|
|
@@ -585,6 +755,7 @@ watch(open, (isOpen) => {
|
|
|
585
755
|
color: var(--rs-text);
|
|
586
756
|
font: inherit;
|
|
587
757
|
font-size: var(--rs-font-size-sm);
|
|
758
|
+
line-height: var(--rs-line-height-tight);
|
|
588
759
|
text-align: left;
|
|
589
760
|
cursor: pointer;
|
|
590
761
|
box-shadow: var(--rs-input-shadow, none);
|
|
@@ -593,6 +764,24 @@ watch(open, (isOpen) => {
|
|
|
593
764
|
box-shadow var(--rs-transition-fast),
|
|
594
765
|
background var(--rs-transition-fast);
|
|
595
766
|
}
|
|
767
|
+
.rs-date-picker--ssm .rs-date-picker__trigger {
|
|
768
|
+
height: var(--rs-control-height-ssm);
|
|
769
|
+
min-height: var(--rs-control-height-ssm);
|
|
770
|
+
padding: 0 var(--rs-space-xs);
|
|
771
|
+
font-size: var(--rs-font-size-xs);
|
|
772
|
+
}
|
|
773
|
+
.rs-date-picker--sm .rs-date-picker__trigger {
|
|
774
|
+
height: var(--rs-control-height-sm);
|
|
775
|
+
min-height: var(--rs-control-height-sm);
|
|
776
|
+
padding: 0 var(--rs-space-sm);
|
|
777
|
+
font-size: var(--rs-font-size-xs);
|
|
778
|
+
}
|
|
779
|
+
.rs-date-picker--lg .rs-date-picker__trigger {
|
|
780
|
+
height: var(--rs-control-height-lg);
|
|
781
|
+
min-height: var(--rs-control-height-lg);
|
|
782
|
+
padding: 0 var(--rs-space-lg);
|
|
783
|
+
font-size: var(--rs-font-size-base);
|
|
784
|
+
}
|
|
596
785
|
.rs-date-picker__trigger:hover:not(:disabled) {
|
|
597
786
|
border-color: var(--rs-input-border-hover, var(--rs-border));
|
|
598
787
|
}
|
|
@@ -611,6 +800,15 @@ watch(open, (isOpen) => {
|
|
|
611
800
|
.rs-date-picker__trigger--placeholder .rs-date-picker__value {
|
|
612
801
|
color: var(--rs-placeholder);
|
|
613
802
|
}
|
|
803
|
+
.rs-date-picker__trigger--invalid {
|
|
804
|
+
border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
805
|
+
}
|
|
806
|
+
.rs-date-picker-field__error {
|
|
807
|
+
margin: var(--rs-space-xs) 0 0;
|
|
808
|
+
color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
809
|
+
font-size: var(--rs-font-size-xs);
|
|
810
|
+
line-height: var(--rs-line-height-tight);
|
|
811
|
+
}
|
|
614
812
|
.rs-date-picker__leading {
|
|
615
813
|
display: inline-flex;
|
|
616
814
|
min-width: 0;
|
|
@@ -691,6 +889,41 @@ watch(open, (isOpen) => {
|
|
|
691
889
|
border-top: 1px solid var(--rs-border-subtle);
|
|
692
890
|
background: var(--rs-surface-elevated);
|
|
693
891
|
}
|
|
892
|
+
.rs-date-picker__footer-start {
|
|
893
|
+
display: flex;
|
|
894
|
+
flex: 1;
|
|
895
|
+
align-items: center;
|
|
896
|
+
min-width: 0;
|
|
897
|
+
}
|
|
898
|
+
.rs-date-picker__shortcuts {
|
|
899
|
+
display: flex;
|
|
900
|
+
flex-wrap: wrap;
|
|
901
|
+
gap: 0.375rem;
|
|
902
|
+
align-items: center;
|
|
903
|
+
}
|
|
904
|
+
.rs-date-picker__shortcut {
|
|
905
|
+
padding: 0.125rem 0.5rem;
|
|
906
|
+
border: 1px solid var(--rs-border);
|
|
907
|
+
border-radius: var(--rs-radius-xs);
|
|
908
|
+
background: transparent;
|
|
909
|
+
color: var(--rs-text);
|
|
910
|
+
font-size: var(--rs-font-size-xs);
|
|
911
|
+
line-height: 1.5;
|
|
912
|
+
cursor: pointer;
|
|
913
|
+
transition:
|
|
914
|
+
border-color var(--rs-transition-fast),
|
|
915
|
+
color var(--rs-transition-fast),
|
|
916
|
+
background var(--rs-transition-fast);
|
|
917
|
+
}
|
|
918
|
+
.rs-date-picker__shortcut:hover:not(:disabled) {
|
|
919
|
+
border-color: var(--rs-primary);
|
|
920
|
+
color: var(--rs-primary);
|
|
921
|
+
background: color-mix(in srgb, var(--rs-primary) 8%, transparent);
|
|
922
|
+
}
|
|
923
|
+
.rs-date-picker__shortcut:disabled {
|
|
924
|
+
opacity: 0.38;
|
|
925
|
+
cursor: not-allowed;
|
|
926
|
+
}
|
|
694
927
|
.rs-date-picker__link {
|
|
695
928
|
border: 0;
|
|
696
929
|
background: transparent;
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import RsDatePicker from './RsDatePicker.vue'
|
|
3
|
-
import type {
|
|
3
|
+
import type { RsComponentSize } from '../theme/types'
|
|
4
|
+
import type {
|
|
5
|
+
RsDatePickerModelValue,
|
|
6
|
+
RsDatePickerShortcut,
|
|
7
|
+
RsDatePickerValueFormat,
|
|
8
|
+
RsDateRangeValue,
|
|
9
|
+
} from './date-picker-utils'
|
|
4
10
|
|
|
5
11
|
export type RsDateTimeRangeValue = RsDateRangeValue
|
|
6
12
|
export type RsDateTimePickerLabelPosition = 'top' | 'left'
|
|
13
|
+
export type {
|
|
14
|
+
RsDatePickerModelValue,
|
|
15
|
+
RsDatePickerShortcut,
|
|
16
|
+
RsDatePickerValueFormat,
|
|
17
|
+
}
|
|
7
18
|
|
|
8
|
-
const model = defineModel<
|
|
19
|
+
const model = defineModel<RsDatePickerModelValue>({ default: '' })
|
|
9
20
|
const open = defineModel<boolean>('open', { default: false })
|
|
10
21
|
|
|
11
22
|
withDefaults(
|
|
@@ -19,13 +30,18 @@ withDefaults(
|
|
|
19
30
|
withSeconds?: boolean
|
|
20
31
|
minDate?: string
|
|
21
32
|
maxDate?: string
|
|
33
|
+
/** string:YYYY-MM-DD HH:mm:ss;timestamp:毫秒时间戳 / [start, end] */
|
|
34
|
+
valueFormat?: RsDatePickerValueFormat
|
|
22
35
|
labelPosition?: RsDateTimePickerLabelPosition
|
|
36
|
+
shortcuts?: RsDatePickerShortcut[]
|
|
37
|
+
size?: RsComponentSize
|
|
23
38
|
}>(),
|
|
24
39
|
{
|
|
25
40
|
disabled: false,
|
|
26
41
|
required: false,
|
|
27
42
|
range: false,
|
|
28
43
|
withSeconds: false,
|
|
44
|
+
valueFormat: 'string',
|
|
29
45
|
labelPosition: 'top',
|
|
30
46
|
},
|
|
31
47
|
)
|
|
@@ -38,5 +54,6 @@ withDefaults(
|
|
|
38
54
|
v-model:open="open"
|
|
39
55
|
with-time
|
|
40
56
|
:with-seconds="withSeconds"
|
|
57
|
+
:value-format="valueFormat"
|
|
41
58
|
/>
|
|
42
59
|
</template>
|