niuma-ui 1.1.8 → 1.1.9

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 CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.9] - 2026-08-28
10
+
11
+ ### 修复
12
+
13
+ - `RsTable`:子表 / 表头默认 `T = any`,避免 inject 子组件把插槽 `row` 收成 `object`。宿主 `#col="{ row }"` 即可用行字段,不必断言。公开类型仍导出 `RsTableSlots` / `RsTableColumnSlotProps`。
14
+ - `RsSelect`:增加泛型 `Value` / `Multiple` / `LabelInValue`,默认单选 `string`。`@update:model-value` 不再是 `string | number | 数组 | labeled` 大联合。数字 value 写 `RsSelect<number>`;`labelInValue` 仍可用。
15
+ - `RsSplitPane`:导出 `RsSplitPaneExpose` / `RsSplitPaneInstance`,模板 ref 用该类型,避免 `InstanceType<typeof RsSplitPane>` 把 vue-tsc 打爆。
16
+ - `RsMonacoEditor`:导出 `RsMonacoEditorExpose`,模板 ref 用该类型,避免 `InstanceType<typeof RsMonacoEditor>` 把 vue-tsc 打爆。
17
+ - `RsInput`:导出 `RsInputExpose` / `RsInputInstance`,模板 ref 用该类型,避免 `InstanceType<typeof RsInput>` 把 vue-tsc 打爆。
18
+
19
+ 运行时兼容 1.1.8;以上为 TypeScript 合约收口。
20
+
9
21
  ## [1.1.8] - 2026-08-28
10
22
 
11
23
  ### 新增
@@ -183,7 +195,8 @@
183
195
 
184
196
  - 1.0 之前的私有 tag(如 `v0.1.0`)仅作历史记录;新接入请依赖 `v1.0.0` 及之后版本。
185
197
 
186
- [Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.8...HEAD
198
+ [Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.9...HEAD
199
+ [1.1.9]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.9
187
200
  [1.1.8]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.8
188
201
  [1.1.7]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.7
189
202
  [1.1.6]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niuma-ui",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Vue 3 设计系统与 Rs* 组件库(--rs-* token)",
@@ -0,0 +1,28 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import type { RsSelectLabeledValue, RsSelectResolvedModel } from '../components/select-utils'
3
+
4
+ describe('RsSelect resolved model', () => {
5
+ it('defaults to a string (single select)', () => {
6
+ expectTypeOf<RsSelectResolvedModel>().toEqualTypeOf<string>()
7
+ })
8
+
9
+ it('multiple literal true is a string array', () => {
10
+ expectTypeOf<RsSelectResolvedModel<string, true>>().toEqualTypeOf<string[]>()
11
+ })
12
+
13
+ it('number value stays number | empty', () => {
14
+ expectTypeOf<RsSelectResolvedModel<number>>().toEqualTypeOf<number | ''>()
15
+ })
16
+
17
+ it('labelInValue literal true is labeled or empty', () => {
18
+ expectTypeOf<RsSelectResolvedModel<string, false, true>>().toEqualTypeOf<
19
+ RsSelectLabeledValue | ''
20
+ >()
21
+ })
22
+
23
+ it('boolean multiple keeps the union so runtime flags still type-check', () => {
24
+ expectTypeOf<RsSelectResolvedModel<string, boolean>>().toEqualTypeOf<
25
+ string | string[]
26
+ >()
27
+ })
28
+ })
@@ -0,0 +1,20 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import type {
3
+ RsTableColumnSlotProps,
4
+ RsTableSlots,
5
+ } from '../components/table/rs-table-props'
6
+
7
+ describe('RsTable slot types', () => {
8
+ it('column slot row is the table generic', () => {
9
+ type Row = { id: string; name: string }
10
+ expectTypeOf<RsTableColumnSlotProps<Row>['row']>().toEqualTypeOf<Row>()
11
+ expectTypeOf<RsTableColumnSlotProps<Row>['index']>().toEqualTypeOf<number>()
12
+ })
13
+
14
+ it('named column slots stay callable with the row generic', () => {
15
+ type Row = { id: string; name: string }
16
+ type NameSlot = NonNullable<RsTableSlots<Row>['name']>
17
+ expectTypeOf<NameSlot>().toBeFunction()
18
+ expectTypeOf<NameSlot>().parameter(0).toMatchTypeOf<{ row: Row; index: number }>()
19
+ })
20
+ })
@@ -31,7 +31,19 @@ import RsVNodeHost from './RsVNodeHost.vue'
31
31
 
32
32
  import RsIcon from './RsIcon.vue'
33
33
 
34
+ /**
35
+ * RsInput 模板 ref 请用此类型。
36
+ * 不要写 `InstanceType<typeof RsInput>`:组件实例类型过深,vue-tsc 会报 Excessive stack depth。
37
+ */
38
+ export interface RsInputExpose {
39
+ validate: (trigger?: RsFormRuleTrigger) => Promise<boolean>
40
+ clearValidation: () => void
41
+ setValue: (value: unknown) => void
42
+ setError: (message: string) => void
43
+ }
34
44
 
45
+ /** 模板 ref 实例:expose + 根节点 */
46
+ export type RsInputInstance = RsInputExpose & { $el: HTMLElement }
35
47
 
36
48
  const { t } = useRsI18n()
37
49
 
@@ -537,7 +549,7 @@ useRsFormField(() => ({
537
549
  setError,
538
550
  }))
539
551
 
540
- defineExpose({
552
+ defineExpose<RsInputExpose>({
541
553
  validate: runValidate,
542
554
  clearValidation,
543
555
  setValue,
@@ -31,6 +31,16 @@ export interface MonacoCompletionSnippet {
31
31
  preselect?: boolean
32
32
  }
33
33
 
34
+ /**
35
+ * RsMonacoEditor 模板 ref 请用此类型。
36
+ * 不要写 `InstanceType<typeof RsMonacoEditor>`:组件实例类型过深,vue-tsc 会报 Excessive stack depth。
37
+ */
38
+ export interface RsMonacoEditorExpose {
39
+ format: () => void
40
+ getEditor: () => import('monaco-editor').editor.IStandaloneCodeEditor | null
41
+ revealLine: (line: number) => void
42
+ }
43
+
34
44
  /** MonacoCompletionContext 描述一次补全请求的编辑器上下文。 */
35
45
  export interface MonacoCompletionContext {
36
46
  text: string
@@ -498,7 +508,7 @@ watch(() => props.completionTriggerCharacters, () => applySnippets(), { deep: tr
498
508
  watch(() => props.completionPrefixResolver, () => applySnippets())
499
509
 
500
510
  // ── Expose ────────────────────────────────────────────────────────────
501
- defineExpose({
511
+ defineExpose<RsMonacoEditorExpose>({
502
512
  /** 格式化文档(等价于 Shift+Alt+F) */
503
513
  format(): void {
504
514
  editor?.getAction('editor.action.formatDocument')?.run().catch(() => undefined)
@@ -1,5 +1,5 @@
1
- <script setup lang="ts">
2
- import { computed, ref, useAttrs } from 'vue'
1
+ <script setup lang="ts" generic="Value extends string | number = string, Multiple extends boolean = false, LabelInValue extends boolean = false">
2
+ import { computed, ref, useAttrs, type ModelRef } from 'vue'
3
3
 
4
4
  import { useRsI18n } from '../composables/useRsI18n'
5
5
  import type { RsComponentSize, RsRadius } from '../theme/types'
@@ -31,6 +31,7 @@ import {
31
31
  type RsSelectOptionFilterProp,
32
32
  type RsSelectOptionsInput,
33
33
  type RsSelectPlacement,
34
+ type RsSelectResolvedModel,
34
35
  type RsSelectStatus,
35
36
  type RsSelectValue,
36
37
  } from './select-utils'
@@ -53,7 +54,9 @@ import {
53
54
 
54
55
  defineOptions({ inheritAttrs: false })
55
56
 
56
- const model = defineModel<RsSelectModelValue>({ default: '' })
57
+ const model = defineModel<RsSelectResolvedModel<Value, Multiple, LabelInValue>>({
58
+ default: '' as never,
59
+ })
57
60
  const open = defineModel<boolean>('open', { default: false })
58
61
  const searchQuery = defineModel<string>('searchValue', { default: '' })
59
62
 
@@ -69,7 +72,7 @@ const props = withDefaults(
69
72
  * 开启后若未显式关 searchable,将自动启用搜索框。
70
73
  */
71
74
  creatable?: boolean
72
- multiple?: boolean
75
+ multiple?: Multiple
73
76
  required?: boolean
74
77
  name?: string
75
78
  clearable?: boolean
@@ -98,7 +101,7 @@ const props = withDefaults(
98
101
  maxTagCount?: number
99
102
  maxTagPlaceholder?: string | ((omitted: number) => string)
100
103
  multipleLimit?: number
101
- labelInValue?: boolean
104
+ labelInValue?: LabelInValue
102
105
  filterSort?: RsSelectFilterSort
103
106
  maxTagTextLength?: number
104
107
  maxTagTooltip?: boolean
@@ -125,7 +128,6 @@ const props = withDefaults(
125
128
  disabled: false,
126
129
  searchable: false,
127
130
  creatable: false,
128
- multiple: false,
129
131
  required: false,
130
132
  clearable: false,
131
133
  virtual: false,
@@ -137,7 +139,6 @@ const props = withDefaults(
137
139
  filterOption: true,
138
140
  optionFilterProp: 'label',
139
141
  optionLabelProp: 'label',
140
- labelInValue: false,
141
142
  maxTagTooltip: true,
142
143
  autoClearSearchValue: true,
143
144
  fillSearchWithValue: false,
@@ -198,7 +199,7 @@ const {
198
199
  removeTag,
199
200
  setValue,
200
201
  resetSearch,
201
- } = useRsSelect(props, model, open, searchQuery, emit, t)
202
+ } = useRsSelect(props, model as ModelRef<RsSelectModelValue>, open, searchQuery, emit, t)
202
203
 
203
204
  const resolvedDisabled = computed(() => props.disabled || formContext?.disabled.value || false)
204
205
  const resolvedSize = useResolvedRsComponentSize(() => props.size)
@@ -13,6 +13,7 @@ import {
13
13
  roundSize,
14
14
  splitSizesEqual,
15
15
  type RsSplitOrientation,
16
+ type RsSplitPaneExpose,
16
17
  type RsSplitPaneItem,
17
18
  } from './split-pane-utils'
18
19
 
@@ -440,7 +441,12 @@ function reset(): void {
440
441
  emit('resize-end', sizes.value.slice())
441
442
  }
442
443
 
443
- defineExpose({ collapse, expand, reset, getSizes: () => sizes.value.slice() })
444
+ defineExpose<RsSplitPaneExpose>({
445
+ collapse,
446
+ expand,
447
+ reset,
448
+ getSizes: () => sizes.value.slice(),
449
+ })
444
450
  </script>
445
451
 
446
452
  <template>
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="T extends import('./table-utils').RsTableRowData">
1
+ <script setup lang="ts" generic="T extends import('./table-utils').RsTableRowData = any">
2
2
  import { computed, ref, useSlots } from 'vue'
3
3
  import RsContextMenu from './RsContextMenu.vue'
4
4
  import { assembleRsTableApi } from '../composables/assembleRsTableApi'
@@ -57,6 +57,27 @@ export type RsSelectModelValue =
57
57
  | RsSelectLabeledValue[]
58
58
  | ''
59
59
 
60
+ /**
61
+ * 按泛型收窄后的 v-model。
62
+ * 默认单选、值为 string:宿主 `@update:model-value="(v: string) => void"` 可直接赋值。
63
+ * `multiple` / `labelInValue` 为字面量 true 时收成数组或 labeled;为 `boolean` 时保留联合。
64
+ */
65
+ export type RsSelectResolvedModel<
66
+ Value extends RsSelectValue = string,
67
+ Multiple extends boolean = false,
68
+ LabelInValue extends boolean = false,
69
+ > = LabelInValue extends true
70
+ ? Multiple extends true
71
+ ? RsSelectLabeledValue[]
72
+ : Multiple extends false
73
+ ? RsSelectLabeledValue | ''
74
+ : RsSelectLabeledValue | RsSelectLabeledValue[] | ''
75
+ : Multiple extends true
76
+ ? Value[]
77
+ : Multiple extends false
78
+ ? Value | ''
79
+ : Value | Value[] | ''
80
+
60
81
  /**
61
82
  * Reka ComboboxItem 禁止 value 为空串(空串表示未选中 / placeholder)。
62
83
  * 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
@@ -27,6 +27,20 @@ export interface RsSplitPaneItem {
27
27
  resizerHandle?: boolean
28
28
  }
29
29
 
30
+ /**
31
+ * RsSplitPane 模板 ref 请用此类型。
32
+ * 不要写 `InstanceType<typeof RsSplitPane>`:组件实例类型过深,vue-tsc 会报 Excessive stack depth。
33
+ */
34
+ export interface RsSplitPaneExpose {
35
+ collapse: (key: string) => void
36
+ expand: (key: string, toSize?: number) => void
37
+ reset: () => void
38
+ getSizes: () => number[]
39
+ }
40
+
41
+ /** 模板 ref 实例:expose + 根节点 */
42
+ export type RsSplitPaneInstance = RsSplitPaneExpose & { $el: HTMLElement }
43
+
30
44
  /** 由 RsSplitPaneItem 解析出的规范化约束 */
31
45
  export interface RsSplitConstraint {
32
46
  min: number
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData = any">
2
2
  /**
3
3
  * RsTable 表体视图:通过 ViewContext inject 取状态(多表互不串扰)。
4
4
  * 行级 v-memo 保留,避免抽离后丢失细粒度跳过渲染。
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData = any">
2
2
  import type { RsTableColumn, RsTableRowDropPosition, RsTableSelectionType } from '../table-utils'
3
3
  import type { RsTableCellEditTrigger } from './table-edit-utils'
4
4
  import RsTableCell from './RsTableCell.vue'
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData = any">
2
2
  import { computed, ref, watch } from 'vue'
3
3
  import type { RsTableColumn } from '../table-utils'
4
4
  import RsCheckbox from '../RsCheckbox.vue'
@@ -6,7 +6,7 @@ import RsDatePicker from '../RsDatePicker.vue'
6
6
  import RsInput from '../RsInput.vue'
7
7
  import RsInputNumber from '../RsInputNumber.vue'
8
8
  import RsSelect from '../RsSelect.vue'
9
- import type { RsSelectModelValue, RsSelectOptions } from '../select-utils'
9
+ import type { RsSelectOptions } from '../select-utils'
10
10
  import type { RsTableColumnEditorOptionsResolved, RsTableCellValueType } from '../table-utils'
11
11
  import {
12
12
  applyFocusMode,
@@ -223,15 +223,8 @@ function onKeydown(event: KeyboardEvent): void {
223
223
  }
224
224
  }
225
225
 
226
- function onSelectUpdate(value: RsSelectModelValue): void {
227
- const toToken = (item: RsSelectModelValue): string => {
228
- if (item == null || item === '') return ''
229
- if (typeof item === 'object' && !Array.isArray(item) && 'value' in item) {
230
- return String(item.value)
231
- }
232
- return String(item)
233
- }
234
- const tokens = Array.isArray(value) ? value.map(toToken) : toToken(value)
226
+ function onSelectUpdate(value: string | string[]): void {
227
+ const tokens = Array.isArray(value) ? value.map(String) : String(value)
235
228
  const empty =
236
229
  tokens === '' || (Array.isArray(tokens) && tokens.length === 0)
237
230
  // 表格内下拉默认不可清除:仅选择;显式 clearable 时才允许清空
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData = any">
2
2
  import RsIcon from '../RsIcon.vue'
3
3
  import RsTableHeaderFilter from './RsTableHeaderFilter.vue'
4
4
  import { useRsTableView } from './rs-table-view-context'
@@ -211,6 +211,72 @@ export type RsTableEmits<T extends RsTableRowData = RsTableRowData> = {
211
211
  rowEditRollback: [row: T, index: number]
212
212
  }
213
213
 
214
+ /**
215
+ * 插槽回调按双变处理:宿主把 row 写成业务行类型时,不被逆变拒绝。
216
+ * 列插槽名是动态的,vue-tsc 不能从子组件 inject 推断 T,必须由 RsTable 自身 declare。
217
+ */
218
+ type RsTableSlotFn<P> = {
219
+ bivarianceHack(props: P): unknown
220
+ }['bivarianceHack']
221
+
222
+ /** 列单元格插槽参数(`#columnKey`) */
223
+ export interface RsTableColumnSlotProps<T extends RsTableRowData = RsTableRowData> {
224
+ row: T
225
+ column: RsTableColumn<T>
226
+ index: number
227
+ }
228
+
229
+ /** 自定义编辑器插槽参数(`#edit-columnKey`) */
230
+ export interface RsTableEditSlotProps<T extends RsTableRowData = RsTableRowData>
231
+ extends RsTableColumnSlotProps<T> {
232
+ draft: string
233
+ error: string | null
234
+ update: (value: string) => void
235
+ commit: () => void
236
+ cancel: () => void
237
+ }
238
+
239
+ /** 列头插槽参数(`#header-columnKey`) */
240
+ export interface RsTableHeaderSlotProps<T extends RsTableRowData = RsTableRowData> {
241
+ column: RsTableColumn<T>
242
+ }
243
+
244
+ /** 展开行插槽参数(`#expand`) */
245
+ export interface RsTableExpandSlotProps<T extends RsTableRowData = RsTableRowData> {
246
+ row: T
247
+ index: number
248
+ }
249
+
250
+ /** 分组行插槽参数(`#group`) */
251
+ export interface RsTableGroupSlotProps {
252
+ key: string
253
+ label: string
254
+ }
255
+
256
+ /**
257
+ * 按插槽名解析参数。列名是动态 key;`edit-*` / `header-*` 用模板字面量区分。
258
+ */
259
+ export type RsTableSlotPropsOf<T extends RsTableRowData, K extends string> = K extends
260
+ | 'empty'
261
+ | 'summary'
262
+ ? Record<string, never>
263
+ : K extends 'group'
264
+ ? RsTableGroupSlotProps
265
+ : K extends 'expand'
266
+ ? RsTableExpandSlotProps<T>
267
+ : K extends `header-${string}`
268
+ ? RsTableHeaderSlotProps<T>
269
+ : K extends `edit-${string}`
270
+ ? RsTableEditSlotProps<T>
271
+ : RsTableColumnSlotProps<T>
272
+
273
+ /**
274
+ * RsTable 公开插槽。用映射类型而不是 string 索引,避免 empty/group 与列插槽冲突。
275
+ */
276
+ export type RsTableSlots<T extends RsTableRowData = RsTableRowData> = {
277
+ [K in string]?: RsTableSlotFn<RsTableSlotPropsOf<T, K>>
278
+ }
279
+
214
280
  /** withDefaults 第二参(工厂默认用函数) */
215
281
  export const RS_TABLE_PROP_DEFAULTS = {
216
282
  loading: false,
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export type { RsLoadingBarApi } from './composables/useRsLoadingBar'
37
37
  export { default as RsDropdown } from './components/RsDropdown.vue'
38
38
  export { default as RsIcon } from './components/RsIcon.vue'
39
39
  export { default as RsInput } from './components/RsInput.vue'
40
+ export type { RsInputExpose, RsInputInstance } from './components/RsInput.vue'
40
41
  export { default as RsInputNumber } from './components/RsInputNumber.vue'
41
42
  export type { RsInputNumberValue } from './components/input-number-utils'
42
43
  export {
@@ -141,6 +142,7 @@ export type {
141
142
  MonacoCompletionPrefixResolver,
142
143
  MonacoCompletionRequest,
143
144
  MonacoCompletionSnippet,
145
+ RsMonacoEditorExpose,
144
146
  } from './components/RsMonacoEditor.vue'
145
147
  export type {
146
148
  MonacoBuiltinLanguage,
@@ -224,6 +226,7 @@ export type {
224
226
  RsSelectGetPopupContainer,
225
227
  RsSelectLabeledValue,
226
228
  RsSelectModelValue,
229
+ RsSelectResolvedModel,
227
230
  RsSelectOption,
228
231
  RsSelectOptionFilterProp,
229
232
  RsSelectOptionGroup,
@@ -330,6 +333,8 @@ export { isStepSeparatorCompleted, resolveStepStatus } from './components/steps-
330
333
  export type {
331
334
  RsSplitConstraint,
332
335
  RsSplitOrientation,
336
+ RsSplitPaneExpose,
337
+ RsSplitPaneInstance,
333
338
  RsSplitPaneItem,
334
339
  RsSplitPaneSize,
335
340
  } from './components/split-pane-utils'
@@ -479,6 +484,13 @@ export {
479
484
  export type {
480
485
  RsTableProps,
481
486
  RsTableEmits,
487
+ RsTableColumnSlotProps,
488
+ RsTableEditSlotProps,
489
+ RsTableHeaderSlotProps,
490
+ RsTableExpandSlotProps,
491
+ RsTableGroupSlotProps,
492
+ RsTableSlots,
493
+ RsTableSlotPropsOf,
482
494
  } from './components/table/rs-table-props'
483
495
  export { RS_TABLE_PROP_DEFAULTS } from './components/table/rs-table-props'
484
496
  export { useRsTableGridKeyboard } from './composables/useRsTableGridKeyboard'