niuma-ui 1.0.0

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.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,22 @@
1
+ import { computed, inject } from 'vue'
2
+ import { localeMap } from '../locale/messages'
3
+ import { defaultLocale, type RsLocale } from '../locale/types'
4
+ import { rsConfigKey } from './useRsConfig'
5
+
6
+ export type RsTranslateFn = (key: string, fallback?: string) => string
7
+
8
+ /** 组件内安全取文案:Provider 内跟随 locale,否则回退 defaultLocale */
9
+ export function useRsI18n() {
10
+ const ctx = inject(rsConfigKey, null)
11
+ const locale = computed<RsLocale>(() => ctx?.locale.value ?? defaultLocale)
12
+
13
+ function t(key: string, fallback?: string): string {
14
+ return localeMap[locale.value][key] ?? fallback ?? key
15
+ }
16
+
17
+ return { t, locale }
18
+ }
19
+
20
+ export function createTranslator(locale: RsLocale): RsTranslateFn {
21
+ return (key, fallback) => localeMap[locale][key] ?? fallback ?? key
22
+ }
@@ -0,0 +1,187 @@
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ onMounted,
5
+ onUnmounted,
6
+ ref,
7
+ watch,
8
+ type ComputedRef,
9
+ type Ref,
10
+ } from 'vue'
11
+ import { resolveVisibleTabValues, type RsTabItem, type RsTabsOverflow } from '../components/tabs-utils'
12
+
13
+ interface UseRsTabsNavOptions {
14
+ items: ComputedRef<RsTabItem[]>
15
+ activeValue: Ref<string>
16
+ overflow: ComputedRef<RsTabsOverflow | false>
17
+ canAdd: ComputedRef<boolean>
18
+ navRef: Ref<HTMLElement | null>
19
+ measureRef: Ref<HTMLElement | null>
20
+ overflowRef: Ref<HTMLElement | null>
21
+ addButtonWidth: number
22
+ }
23
+
24
+ export function useRsTabsNav(options: UseRsTabsNavOptions) {
25
+ const navViewportRef = ref<HTMLElement | null>(null)
26
+ const triggerWidths = ref<Map<string, number>>(new Map())
27
+ const visibleValues = ref<Set<string>>(new Set())
28
+ const canScrollPrev = ref(false)
29
+ const canScrollNext = ref(false)
30
+
31
+ let resizeObserver: ResizeObserver | null = null
32
+
33
+ const useDropdownOverflow = computed(() => options.overflow.value === 'dropdown')
34
+ const useScrollOverflow = computed(() => options.overflow.value === 'scroll')
35
+
36
+ const visibleTabItems = computed(() =>
37
+ options.items.value.filter((item) => visibleValues.value.has(item.value)),
38
+ )
39
+
40
+ const hiddenItems = computed(() =>
41
+ options.items.value.filter((item) => !visibleValues.value.has(item.value)),
42
+ )
43
+
44
+ const overflowMenuItems = computed(() =>
45
+ hiddenItems.value.map((item) => ({ value: item.value, label: item.label })),
46
+ )
47
+
48
+ const activeInHidden = computed(() =>
49
+ hiddenItems.value.some((item) => item.value === options.activeValue.value),
50
+ )
51
+
52
+ const showOverflowMenu = computed(
53
+ () => useDropdownOverflow.value && hiddenItems.value.length > 0,
54
+ )
55
+
56
+ function measureTriggerWidths(): Map<string, number> {
57
+ const measureRoot = options.measureRef.value
58
+ if (measureRoot) {
59
+ const next = new Map<string, number>()
60
+ for (const node of measureRoot.querySelectorAll<HTMLElement>('[data-tab-value]')) {
61
+ const value = node.dataset.tabValue
62
+ if (value) next.set(value, node.offsetWidth)
63
+ }
64
+ if (next.size > 0) return next
65
+ }
66
+
67
+ const viewport = navViewportRef.value
68
+ const next = new Map<string, number>()
69
+ if (!viewport) return next
70
+ for (const trigger of viewport.querySelectorAll<HTMLElement>('.rs-tabs__trigger[data-tab-value]')) {
71
+ const value = trigger.dataset.tabValue
72
+ if (value) next.set(value, trigger.offsetWidth)
73
+ }
74
+ return next
75
+ }
76
+
77
+ function reservedWidth(): number {
78
+ let width = 0
79
+ if (useDropdownOverflow.value && options.items.value.length > 1) {
80
+ width += options.overflowRef.value?.offsetWidth ?? 76
81
+ }
82
+ if (options.canAdd.value) width += options.addButtonWidth
83
+ return width
84
+ }
85
+
86
+ function recomputeVisibleTabs() {
87
+ if (!useDropdownOverflow.value) {
88
+ visibleValues.value = new Set(options.items.value.map((item) => item.value))
89
+ return
90
+ }
91
+
92
+ const nav = options.navRef.value
93
+ if (!nav) return
94
+
95
+ triggerWidths.value = measureTriggerWidths()
96
+ const availableWidth = nav.clientWidth
97
+ visibleValues.value = resolveVisibleTabValues(
98
+ options.items.value,
99
+ triggerWidths.value,
100
+ availableWidth,
101
+ options.activeValue.value,
102
+ reservedWidth(),
103
+ { keepActiveVisible: false },
104
+ )
105
+ }
106
+
107
+ function updateScrollButtons() {
108
+ const viewport = navViewportRef.value
109
+ if (!viewport || !useScrollOverflow.value) {
110
+ canScrollPrev.value = false
111
+ canScrollNext.value = false
112
+ return
113
+ }
114
+ canScrollPrev.value = viewport.scrollLeft > 1
115
+ canScrollNext.value = viewport.scrollLeft + viewport.clientWidth < viewport.scrollWidth - 1
116
+ }
117
+
118
+ function scrollNav(direction: -1 | 1) {
119
+ navViewportRef.value?.scrollBy({ left: direction * 180, behavior: 'smooth' })
120
+ }
121
+
122
+ function onNavScroll() {
123
+ updateScrollButtons()
124
+ }
125
+
126
+ async function scrollActiveTabIntoView() {
127
+ if (!useScrollOverflow.value) return
128
+ await nextTick()
129
+ const viewport = navViewportRef.value
130
+ if (!viewport) return
131
+ const active = viewport.querySelector<HTMLElement>(`.rs-tabs__trigger[data-state='active']`)
132
+ active?.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' })
133
+ updateScrollButtons()
134
+ }
135
+
136
+ function scheduleLayout() {
137
+ nextTick(() => {
138
+ recomputeVisibleTabs()
139
+ updateScrollButtons()
140
+ if (!useDropdownOverflow.value) return
141
+ nextTick(() => {
142
+ recomputeVisibleTabs()
143
+ updateScrollButtons()
144
+ })
145
+ })
146
+ }
147
+
148
+ onMounted(() => {
149
+ scheduleLayout()
150
+ const nav = options.navRef.value
151
+ if (!nav || typeof ResizeObserver === 'undefined') return
152
+ resizeObserver = new ResizeObserver(() => scheduleLayout())
153
+ resizeObserver.observe(nav)
154
+ })
155
+
156
+ onUnmounted(() => {
157
+ resizeObserver?.disconnect()
158
+ resizeObserver = null
159
+ })
160
+
161
+ watch(
162
+ () => [options.items.value, options.overflow.value, options.canAdd.value] as const,
163
+ () => scheduleLayout(),
164
+ { deep: true },
165
+ )
166
+
167
+ watch(options.activeValue, () => {
168
+ scheduleLayout()
169
+ void scrollActiveTabIntoView()
170
+ })
171
+
172
+ return {
173
+ navViewportRef,
174
+ useDropdownOverflow,
175
+ useScrollOverflow,
176
+ visibleTabItems,
177
+ hiddenItems,
178
+ overflowMenuItems,
179
+ activeInHidden,
180
+ showOverflowMenu,
181
+ canScrollPrev,
182
+ canScrollNext,
183
+ scrollNav,
184
+ onNavScroll,
185
+ scheduleLayout,
186
+ }
187
+ }
@@ -0,0 +1,48 @@
1
+ import { toast } from 'vue-sonner'
2
+ import type { RsToastPosition, RsToastType } from '../components/overlay-utils'
3
+
4
+ export type { RsToastPosition, RsToastType }
5
+
6
+ export type RsToastInput =
7
+ | string
8
+ | {
9
+ title: string
10
+ description?: string
11
+ position?: RsToastPosition
12
+ duration?: number
13
+ }
14
+
15
+ function resolveToast(input: RsToastInput): {
16
+ title: string
17
+ description?: string
18
+ position?: RsToastPosition
19
+ duration?: number
20
+ } {
21
+ if (typeof input === 'string') return { title: input }
22
+ return input
23
+ }
24
+
25
+ export function useRsToast() {
26
+ function show(type: RsToastType, input: RsToastInput): void {
27
+ const payload = resolveToast(input)
28
+ const options = {
29
+ description: payload.description,
30
+ ...(payload.position ? { position: payload.position } : {}),
31
+ ...(payload.duration ? { duration: payload.duration } : {}),
32
+ class: `rs-toast rs-toast--${type}`,
33
+ }
34
+
35
+ if (type === 'success') toast.success(payload.title, options)
36
+ else if (type === 'error') toast.error(payload.title, options)
37
+ else if (type === 'warning') toast.warning(payload.title, options)
38
+ else toast.info(payload.title, options)
39
+ }
40
+
41
+ return {
42
+ success: (input: RsToastInput) => show('success', input),
43
+ error: (input: RsToastInput) => show('error', input),
44
+ info: (input: RsToastInput) => show('info', input),
45
+ warning: (input: RsToastInput) => show('warning', input),
46
+ dismiss: toast.dismiss,
47
+ }
48
+ }
@@ -0,0 +1,239 @@
1
+ import { nextTick, onUnmounted, ref, type Ref } from 'vue'
2
+
3
+ export const RS_TABLE_TIP_ATTR = 'data-rs-table-tip'
4
+ export const RS_TABLE_TIP_MODE_ATTR = 'data-rs-table-tip-mode'
5
+ export const RS_TABLE_TIP_TEXT_ATTR = 'data-rs-table-tip-text'
6
+ /** 列头 headerTip:整表共享浮层,悬停 th-label 显示 */
7
+ export const RS_TABLE_HEADER_TIP_ATTR = 'data-rs-table-header-tip'
8
+
9
+ const TIP_SIDE_OFFSET = 6
10
+ const TIP_VIEWPORT_PAD = 8
11
+ const TIP_HIDE_GRACE_MS = 80
12
+
13
+ export interface TableCellTooltipState {
14
+ visible: boolean
15
+ ready: boolean
16
+ text: string
17
+ kind: 'cell' | 'header'
18
+ style: { left: string; top: string }
19
+ }
20
+
21
+ function isHeaderTipHost(host: HTMLElement): boolean {
22
+ return host.hasAttribute(RS_TABLE_HEADER_TIP_ATTR)
23
+ }
24
+
25
+ function findTipHost(target: EventTarget | null): HTMLElement | null {
26
+ if (!(target instanceof Element)) return null
27
+ const header = target.closest(`[${RS_TABLE_HEADER_TIP_ATTR}]`)
28
+ if (header instanceof HTMLElement) return header
29
+ const cell = target.closest(`[${RS_TABLE_TIP_ATTR}], .rs-table__cell-tip`)
30
+ return cell instanceof HTMLElement ? cell : null
31
+ }
32
+
33
+ function resolveMeasureEl(host: HTMLElement): HTMLElement {
34
+ if (host.classList.contains('rs-table__ellipsis-text')) return host
35
+ const inner = host.querySelector('.rs-table__ellipsis-text')
36
+ return inner instanceof HTMLElement ? inner : host
37
+ }
38
+
39
+ function resolveTipText(host: HTMLElement): string | null {
40
+ if (isHeaderTipHost(host)) {
41
+ const text = host.getAttribute(RS_TABLE_HEADER_TIP_ATTR)?.trim()
42
+ return text || null
43
+ }
44
+
45
+ if (host.hasAttribute(RS_TABLE_TIP_ATTR)) {
46
+ const text = host.getAttribute(RS_TABLE_TIP_ATTR)?.trim()
47
+ return text || null
48
+ }
49
+
50
+ const mode = host.getAttribute(RS_TABLE_TIP_MODE_ATTR)
51
+ if (!mode) return null
52
+
53
+ const text = host.getAttribute(RS_TABLE_TIP_TEXT_ATTR)?.trim() ?? ''
54
+ if (mode === 'always') return text || null
55
+
56
+ if (mode === 'overflow') {
57
+ const measure = resolveMeasureEl(host)
58
+ if (measure.scrollWidth <= measure.clientWidth) return null
59
+ return text || measure.textContent?.trim() || null
60
+ }
61
+
62
+ return null
63
+ }
64
+
65
+ function clamp(value: number, min: number, max: number): number {
66
+ return Math.min(Math.max(value, min), max)
67
+ }
68
+
69
+ export function useTableCellTooltip(options: {
70
+ enabled: () => boolean
71
+ /** 列头 headerTip 共享浮层;默认 true */
72
+ headerEnabled?: () => boolean
73
+ delay: () => number
74
+ tipRef: Ref<HTMLElement | null>
75
+ }): {
76
+ state: Ref<TableCellTooltipState>
77
+ onPointerOver: (event: PointerEvent) => void
78
+ onPointerOut: (event: PointerEvent) => void
79
+ hide: () => void
80
+ } {
81
+ const state = ref<TableCellTooltipState>({
82
+ visible: false,
83
+ ready: false,
84
+ text: '',
85
+ kind: 'cell',
86
+ style: { left: '0', top: '0' },
87
+ })
88
+
89
+ let tipHost: HTMLElement | null = null
90
+ let showTimer = 0
91
+ let hideTimer = 0
92
+ let positionRaf = 0
93
+
94
+ function clearTimers(): void {
95
+ if (showTimer) {
96
+ clearTimeout(showTimer)
97
+ showTimer = 0
98
+ }
99
+ if (hideTimer) {
100
+ clearTimeout(hideTimer)
101
+ hideTimer = 0
102
+ }
103
+ }
104
+
105
+ function cancelPositionRaf(): void {
106
+ if (positionRaf) {
107
+ cancelAnimationFrame(positionRaf)
108
+ positionRaf = 0
109
+ }
110
+ }
111
+
112
+ function positionTip(host: HTMLElement): void {
113
+ const tipEl = options.tipRef.value
114
+ if (!tipEl) return
115
+
116
+ const hostRect = host.getBoundingClientRect()
117
+ const tipRect = tipEl.getBoundingClientRect()
118
+ const vw = window.innerWidth
119
+ const vh = window.innerHeight
120
+
121
+ let left: number
122
+ let top: number
123
+
124
+ if (isHeaderTipHost(host)) {
125
+ left = hostRect.left
126
+ top = hostRect.bottom + TIP_SIDE_OFFSET
127
+ if (top + tipRect.height > vh - TIP_VIEWPORT_PAD) {
128
+ top = hostRect.top - tipRect.height - TIP_SIDE_OFFSET
129
+ }
130
+ } else {
131
+ left = hostRect.left + hostRect.width / 2 - tipRect.width / 2
132
+ top = hostRect.top - tipRect.height - TIP_SIDE_OFFSET
133
+ if (top < TIP_VIEWPORT_PAD) {
134
+ top = hostRect.bottom + TIP_SIDE_OFFSET
135
+ }
136
+ }
137
+
138
+ left = clamp(left, TIP_VIEWPORT_PAD, Math.max(TIP_VIEWPORT_PAD, vw - tipRect.width - TIP_VIEWPORT_PAD))
139
+ top = clamp(top, TIP_VIEWPORT_PAD, Math.max(TIP_VIEWPORT_PAD, vh - tipRect.height - TIP_VIEWPORT_PAD))
140
+
141
+ state.value.style = {
142
+ left: `${left}px`,
143
+ top: `${top}px`,
144
+ }
145
+ state.value.ready = true
146
+ }
147
+
148
+ function hide(): void {
149
+ clearTimers()
150
+ cancelPositionRaf()
151
+ tipHost = null
152
+ state.value.visible = false
153
+ state.value.ready = false
154
+ state.value.text = ''
155
+ state.value.kind = 'cell'
156
+ }
157
+
158
+ function show(host: HTMLElement, text: string): void {
159
+ tipHost = host
160
+ state.value.text = text
161
+ state.value.kind = isHeaderTipHost(host) ? 'header' : 'cell'
162
+ state.value.ready = false
163
+ state.value.visible = true
164
+
165
+ cancelPositionRaf()
166
+ nextTick(() => {
167
+ positionRaf = requestAnimationFrame(() => {
168
+ positionRaf = 0
169
+ if (tipHost === host) positionTip(host)
170
+ })
171
+ })
172
+ }
173
+
174
+ function scheduleShow(host: HTMLElement): void {
175
+ const header = isHeaderTipHost(host)
176
+ if (header) {
177
+ if (options.headerEnabled && !options.headerEnabled()) return
178
+ } else if (!options.enabled()) {
179
+ return
180
+ }
181
+
182
+ const text = resolveTipText(host)
183
+ if (!text) {
184
+ hide()
185
+ return
186
+ }
187
+
188
+ clearTimers()
189
+ if (tipHost !== host) tipHost = host
190
+
191
+ const delay = options.delay()
192
+ if (delay <= 0) {
193
+ show(host, text)
194
+ return
195
+ }
196
+
197
+ showTimer = window.setTimeout(() => {
198
+ showTimer = 0
199
+ if (tipHost === host) show(host, text)
200
+ }, delay)
201
+ }
202
+
203
+ function scheduleHide(): void {
204
+ clearTimers()
205
+ hideTimer = window.setTimeout(hide, TIP_HIDE_GRACE_MS)
206
+ }
207
+
208
+ function onPointerOver(event: PointerEvent): void {
209
+ if (event.pointerType === 'touch') return
210
+
211
+ const host = findTipHost(event.target)
212
+ if (!host) {
213
+ if (tipHost) scheduleHide()
214
+ return
215
+ }
216
+
217
+ if (host === tipHost && state.value.visible) return
218
+ if (tipHost && host !== tipHost) hide()
219
+ scheduleShow(host)
220
+ }
221
+
222
+ function onPointerOut(event: PointerEvent): void {
223
+ const related = event.relatedTarget
224
+ if (related instanceof Element && findTipHost(related)) return
225
+ scheduleHide()
226
+ }
227
+
228
+ onUnmounted(() => {
229
+ clearTimers()
230
+ cancelPositionRaf()
231
+ })
232
+
233
+ return {
234
+ state,
235
+ onPointerOver,
236
+ onPointerOut,
237
+ hide,
238
+ }
239
+ }