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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,53 @@
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.2] - 2026-08-13
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- `RsTable` 架构分层:`RsTableApi` / ModuleRegistry / FeatureHost、`useRsTable` 表面、`useRsTableCore` / Shell / EditLayer、ViewContext 子件与 `useRsTableHeadless`。
|
|
14
|
+
- `RsTable` 无障碍基线:`region` + `role=grid`、键盘漫游;`tabindex` 落在 grid 表上。
|
|
15
|
+
- 图表适配:`createChartSeriesTableFeature`、`mapRsTableSeriesToEChartsOption`(无 echarts 依赖)。
|
|
16
|
+
- CI 门禁:`test:perf` / `test:compat` / `test:ssr` / `test:dom-baseline`;Playwright 视觉回归 `test:visual`。
|
|
17
|
+
- 架构文档:`rs-table-architecture` / `ssr` / `chart-adapter` / `visual`。
|
|
18
|
+
- `RsMenu` 菜单项 token:在 `styles.css` 主题块维护默认值(`--rs-menu-item-active-fg|bg` 等),组件只消费变量;父级覆盖即可,无需 `:deep`。
|
|
19
|
+
- 排版 token:字号阶梯扩展至 `xl` / `2xl` / `3xl`;新增字重、字族、行高 `relaxed`。
|
|
20
|
+
- 文字语义 token:`--rs-text-primary|secondary|tertiary|disabled|inverse|link|link-hover`(兼容原 `--rs-text` / `--rs-muted`)。
|
|
21
|
+
- JS 导出:`RsFontSize` / `RsFontWeight` 与 `RS_FONT_SIZE_CSS` / `RS_FONT_WEIGHT_CSS`。
|
|
22
|
+
- `RsSwitch`:开关控件(Reka Switch)。
|
|
23
|
+
- `RsRadio` / `RsRadioItem`:单选分组。
|
|
24
|
+
- `RsTag` / `RsDynamicTags`:标签与可编辑标签组。
|
|
25
|
+
- `RsAlert`:反馈提示条。
|
|
26
|
+
- `RsDivider`:分隔线。
|
|
27
|
+
- `RsDescriptions` / `RsDescriptionsItem`:描述列表。
|
|
28
|
+
- `RsLoadingBar` + `useRsLoadingBar`:顶栏加载进度。
|
|
29
|
+
|
|
30
|
+
### 变更
|
|
31
|
+
|
|
32
|
+
- `RsPagination`:尺寸扩展为 `sm|md|lg`(对齐 Ant small/medium/large);页码按钮圆角 `sm`;当前页改为文字主色高亮、无填充底;跳转输入与每页条数同档高度;`showJumpConfirm` 默认 `false`(回车/失焦跳转)。
|
|
33
|
+
- `RsTabs`:非激活 `TabsContent` 强制 `display: none`,避免多页内容叠层。
|
|
34
|
+
- 表单标签 token:`--rs-label-font-size|font-weight|line-height|color` 写入 `styles.css`(默认对齐 Ant 14px / medium / text);`RsLabel` 与 `.rs-field__label` 共用,父级覆盖即可,无需 `:deep`。
|
|
35
|
+
- 表单控件高度对齐:`RsInput` / `RsSelect` / `RsInputNumber` / `RsDatePicker` 统一用 `--rs-control-height-*` 锁定 `height`,Select/DatePicker 行高改为 `tight`(与 Input 一致)。
|
|
36
|
+
- `RsDatePicker` 新增 `size` prop(ssm/sm/md/lg),可跟随 Form / ConfigProvider。
|
|
37
|
+
- `RsSwitch`:轨道高度用 `control-height * 2/3` 等 calc 推导(对齐 Ant 比例),随主题 token 缩放,不写死档位 px。
|
|
38
|
+
- `RsInputNumber`:恢复常规 `border` 描边(inset shadow 在部分主题下不可见);内层仍填满外壳高度。
|
|
39
|
+
- `RsSelect`:默认去掉 `max-width: 20rem` 上限(改为 `max-width: 100%`),避免表单栅格里 Select 比同列 Input 窄;`block` 仍负责拉满宽度。
|
|
40
|
+
|
|
41
|
+
### 移除
|
|
42
|
+
|
|
43
|
+
- 公开组件 `RsTableHeaderRow` / `RsTableColgroup`(轻量辅助;正式路径为 `RsTableHeader` / `RsTableColGroup`)。
|
|
44
|
+
|
|
45
|
+
## [1.0.1] - 2026-08-11
|
|
46
|
+
|
|
47
|
+
### 变更
|
|
48
|
+
|
|
49
|
+
- 图标依赖由弃用的 `lucide-vue-next` 迁移为 `@lucide/vue`。
|
|
50
|
+
- 终端依赖由弃用的 `xterm` 迁移为 `@xterm/xterm`(配合现有 `@xterm/addon-fit`)。
|
|
51
|
+
|
|
52
|
+
### 说明
|
|
53
|
+
|
|
54
|
+
- 安装时仍可能看到 `antlr4ng-cli` 弃用提示:来自 `monaco-sql-languages` → `dt-sql-parser` 的传递依赖,待上游更新;不影响使用。
|
|
55
|
+
|
|
9
56
|
## [1.0.0] - 2026-08-11
|
|
10
57
|
|
|
11
58
|
### 新增
|
|
@@ -25,5 +72,7 @@
|
|
|
25
72
|
|
|
26
73
|
- 1.0 之前的私有 tag(如 `v0.1.0`)仅作历史记录;新接入请依赖 `v1.0.0` 及之后版本。
|
|
27
74
|
|
|
28
|
-
[Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.0.
|
|
75
|
+
[Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.0.2...HEAD
|
|
76
|
+
[1.0.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.0.2
|
|
77
|
+
[1.0.1]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.0.1
|
|
29
78
|
[1.0.0]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.0.0
|
package/NOTICE
CHANGED
|
@@ -15,10 +15,10 @@ See the LICENSE file for the full license text.
|
|
|
15
15
|
|------|------------|------|
|
|
16
16
|
| Vue | MIT | peer 框架 |
|
|
17
17
|
| Reka UI | MIT | 无障碍交互原语 |
|
|
18
|
-
| Lucide (lucide
|
|
18
|
+
| Lucide (@lucide/vue) | ISC | 默认图标 |
|
|
19
19
|
| Monaco Editor | MIT | RsMonacoEditor |
|
|
20
20
|
| CodeMirror | MIT | RsCodeEditor |
|
|
21
|
-
| xterm.js | MIT | RsTerminal |
|
|
21
|
+
| xterm.js (@xterm/xterm) | MIT | RsTerminal |
|
|
22
22
|
| marked | MIT | Markdown 解析 |
|
|
23
23
|
| DOMPurify | Apache-2.0 / MPL-2.0 | HTML 消毒 |
|
|
24
24
|
| dayjs | MIT | 日期辅助 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niuma-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Vue 3 设计系统与 Rs* 组件库(--rs-* token)",
|
|
@@ -67,22 +67,24 @@
|
|
|
67
67
|
"@codemirror/state": "^6.6.0",
|
|
68
68
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
69
69
|
"@codemirror/view": "^6.43.1",
|
|
70
|
+
"@lucide/vue": "^1.31.0",
|
|
70
71
|
"@xterm/addon-fit": "^0.11.0",
|
|
72
|
+
"@xterm/xterm": "^6.0.0",
|
|
71
73
|
"codemirror": "^6.0.2",
|
|
72
74
|
"dayjs": "^1.11.21",
|
|
73
75
|
"dompurify": "^3.4.13",
|
|
74
|
-
"lucide-vue-next": "^1.0.0",
|
|
75
76
|
"marked": "^18.0.9",
|
|
76
77
|
"monaco-editor": "^0.55.1",
|
|
77
78
|
"monaco-sql-languages": "^1.2.0",
|
|
78
79
|
"reka-ui": "^2.9.10",
|
|
79
|
-
"vue-sonner": "^2.0.9"
|
|
80
|
-
"xterm": "^5.3.0"
|
|
80
|
+
"vue-sonner": "^2.0.9"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
+
"@playwright/test": "^1.62.1",
|
|
83
84
|
"@tailwindcss/vite": "^4.3.2",
|
|
84
85
|
"@types/node": "^26.1.1",
|
|
85
86
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
87
|
+
"@vue/server-renderer": "^3.5.39",
|
|
86
88
|
"@vue/test-utils": "^2.4.11",
|
|
87
89
|
"jsdom": "^29.1.1",
|
|
88
90
|
"tailwindcss": "^4.3.2",
|
|
@@ -101,6 +103,12 @@
|
|
|
101
103
|
"build:playground": "vite build --config playground/vite.config.ts",
|
|
102
104
|
"preview:playground": "vite preview --config playground/vite.config.ts",
|
|
103
105
|
"test": "vitest run",
|
|
104
|
-
"test:watch": "vitest"
|
|
106
|
+
"test:watch": "vitest",
|
|
107
|
+
"test:perf": "vitest run src/__tests__/RsTable.perf.spec.ts src/__tests__/useRsTableCore.perf.spec.ts",
|
|
108
|
+
"test:compat": "vitest run src/__tests__/rs-table-compat-matrix.spec.ts src/__tests__/rs-table-api.spec.ts src/__tests__/rs-table-isolation.spec.ts",
|
|
109
|
+
"test:ssr": "vitest run src/__tests__/RsTable.ssr.spec.ts",
|
|
110
|
+
"test:dom-baseline": "vitest run src/__tests__/RsTable.dom-baseline.spec.ts src/__tests__/rs-table-echarts-adapter.spec.ts",
|
|
111
|
+
"test:visual": "playwright test",
|
|
112
|
+
"test:visual:update": "playwright test --update-snapshots"
|
|
105
113
|
}
|
|
106
114
|
}
|
|
@@ -31,6 +31,53 @@ describe('RsButton', () => {
|
|
|
31
31
|
expect(wrapper.classes()).toContain('rs-btn--ghost')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
+
it('text variant defaults to borderless + neutral tone', () => {
|
|
35
|
+
const wrapper = mount(RsButton, {
|
|
36
|
+
props: { variant: 'text' },
|
|
37
|
+
slots: { default: '编辑' },
|
|
38
|
+
})
|
|
39
|
+
expect(wrapper.classes()).toContain('rs-btn--text')
|
|
40
|
+
expect(wrapper.classes()).toContain('rs-btn--borderless')
|
|
41
|
+
expect(wrapper.classes()).toContain('rs-btn--tone-neutral')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('text + tone applies semantic tone class', () => {
|
|
45
|
+
const wrapper = mount(RsButton, {
|
|
46
|
+
props: { variant: 'text', tone: 'primary' },
|
|
47
|
+
slots: { default: '新增' },
|
|
48
|
+
})
|
|
49
|
+
expect(wrapper.classes()).toContain('rs-btn--text')
|
|
50
|
+
expect(wrapper.classes()).toContain('rs-btn--tone-primary')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('ghost supports tone without becoming filled', () => {
|
|
54
|
+
const wrapper = mount(RsButton, {
|
|
55
|
+
props: { variant: 'ghost', tone: 'danger' },
|
|
56
|
+
slots: { default: '删除' },
|
|
57
|
+
})
|
|
58
|
+
expect(wrapper.classes()).toContain('rs-btn--ghost')
|
|
59
|
+
expect(wrapper.classes()).toContain('rs-btn--tone-danger')
|
|
60
|
+
expect(wrapper.classes()).not.toContain('rs-btn--danger')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('filled primary ignores tone class', () => {
|
|
64
|
+
const wrapper = mount(RsButton, {
|
|
65
|
+
props: { variant: 'primary', tone: 'danger' },
|
|
66
|
+
slots: { default: '保存' },
|
|
67
|
+
})
|
|
68
|
+
expect(wrapper.classes()).toContain('rs-btn--primary')
|
|
69
|
+
expect(wrapper.classes()).not.toContain('rs-btn--tone-danger')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('link defaults to primary tone', () => {
|
|
73
|
+
const wrapper = mount(RsButton, {
|
|
74
|
+
props: { variant: 'link' },
|
|
75
|
+
slots: { default: '查看' },
|
|
76
|
+
})
|
|
77
|
+
expect(wrapper.classes()).toContain('rs-btn--link')
|
|
78
|
+
expect(wrapper.classes()).toContain('rs-btn--tone-primary')
|
|
79
|
+
})
|
|
80
|
+
|
|
34
81
|
it('renders prefix icon', () => {
|
|
35
82
|
const wrapper = mount(RsButton, {
|
|
36
83
|
props: { icon: 'plus' },
|
|
@@ -34,11 +34,22 @@ describe('RsCard', () => {
|
|
|
34
34
|
expect(wrapper.find('header.rs-card__header').exists()).toBe(true)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it('omits header when no title, description, or
|
|
37
|
+
it('omits header when no title, description, header, or actions slot', () => {
|
|
38
38
|
const wrapper = mount(RsCard, { slots: { default: '仅正文' } })
|
|
39
39
|
expect(wrapper.find('header').exists()).toBe(false)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it('renders header when only actions slot is provided', () => {
|
|
43
|
+
const wrapper = mount(RsCard, {
|
|
44
|
+
slots: {
|
|
45
|
+
actions: '<button type="button">操作</button>',
|
|
46
|
+
default: '正文',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
expect(wrapper.find('header.rs-card__header').exists()).toBe(true)
|
|
50
|
+
expect(wrapper.find('.rs-card__actions button').text()).toBe('操作')
|
|
51
|
+
})
|
|
52
|
+
|
|
42
53
|
it('renders header slot instead of default heading', () => {
|
|
43
54
|
const wrapper = mount(RsCard, {
|
|
44
55
|
slots: {
|
|
@@ -61,6 +72,40 @@ describe('RsCard', () => {
|
|
|
61
72
|
expect(wrapper.find('.rs-card__actions button').text()).toBe('操作')
|
|
62
73
|
})
|
|
63
74
|
|
|
75
|
+
it('renders cover slot before header', () => {
|
|
76
|
+
const wrapper = mount(RsCard, {
|
|
77
|
+
props: { title: '封面卡' },
|
|
78
|
+
slots: {
|
|
79
|
+
cover: '<div class="cover-el">封面</div>',
|
|
80
|
+
default: '正文',
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
const cover = wrapper.find('.rs-card__cover')
|
|
84
|
+
expect(cover.exists()).toBe(true)
|
|
85
|
+
expect(cover.text()).toBe('封面')
|
|
86
|
+
const rootHtml = wrapper.html()
|
|
87
|
+
expect(rootHtml.indexOf('rs-card__cover')).toBeLessThan(rootHtml.indexOf('rs-card__header'))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders footer slot', () => {
|
|
91
|
+
const wrapper = mount(RsCard, {
|
|
92
|
+
props: { title: '带底部' },
|
|
93
|
+
slots: {
|
|
94
|
+
default: '正文',
|
|
95
|
+
footer: '<button type="button">保存</button>',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
expect(wrapper.find('footer.rs-card__footer button').text()).toBe('保存')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('omits footer when footer slot is absent', () => {
|
|
102
|
+
const wrapper = mount(RsCard, {
|
|
103
|
+
props: { title: '无底部' },
|
|
104
|
+
slots: { default: '正文' },
|
|
105
|
+
})
|
|
106
|
+
expect(wrapper.find('footer').exists()).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
64
109
|
it('applies padded body class by default', () => {
|
|
65
110
|
const wrapper = mount(RsCard, { slots: { default: '内容' } })
|
|
66
111
|
expect(wrapper.find('.rs-card__body').classes()).toContain('rs-card__body--padded')
|
|
@@ -82,6 +127,30 @@ describe('RsCard', () => {
|
|
|
82
127
|
expect(wrapper.classes()).toContain('rs-card--elevated')
|
|
83
128
|
})
|
|
84
129
|
|
|
130
|
+
it('applies hoverable class', () => {
|
|
131
|
+
const wrapper = mount(RsCard, {
|
|
132
|
+
props: { hoverable: true },
|
|
133
|
+
slots: { default: '内容' },
|
|
134
|
+
})
|
|
135
|
+
expect(wrapper.classes()).toContain('rs-card--hoverable')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('applies clip class for overflow hidden', () => {
|
|
139
|
+
const wrapper = mount(RsCard, {
|
|
140
|
+
props: { clip: true },
|
|
141
|
+
slots: { default: '内容' },
|
|
142
|
+
})
|
|
143
|
+
expect(wrapper.classes()).toContain('rs-card--clip')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('applies fill class for pane layouts', () => {
|
|
147
|
+
const wrapper = mount(RsCard, {
|
|
148
|
+
props: { fill: true },
|
|
149
|
+
slots: { default: '内容' },
|
|
150
|
+
})
|
|
151
|
+
expect(wrapper.classes()).toContain('rs-card--fill')
|
|
152
|
+
})
|
|
153
|
+
|
|
85
154
|
it('applies variant class', () => {
|
|
86
155
|
const wrapper = mount(RsCard, {
|
|
87
156
|
props: { variant: 'plain' },
|
|
@@ -94,4 +163,94 @@ describe('RsCard', () => {
|
|
|
94
163
|
const wrapper = mount(RsCard, { slots: { default: '内容' } })
|
|
95
164
|
expect(wrapper.classes()).toContain('rs-card--grouped')
|
|
96
165
|
})
|
|
166
|
+
|
|
167
|
+
it('defaults to md size', () => {
|
|
168
|
+
const wrapper = mount(RsCard, { slots: { default: '内容' } })
|
|
169
|
+
expect(wrapper.classes()).toContain('rs-card--md')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it.each(['sm', 'md', 'lg'] as const)('applies size=%s class', (size) => {
|
|
173
|
+
const wrapper = mount(RsCard, {
|
|
174
|
+
props: { size },
|
|
175
|
+
slots: { default: '内容' },
|
|
176
|
+
})
|
|
177
|
+
expect(wrapper.classes()).toContain(`rs-card--${size}`)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('defaults radius css var to md token', () => {
|
|
181
|
+
const wrapper = mount(RsCard, { slots: { default: '内容' } })
|
|
182
|
+
const style = wrapper.attributes('style') ?? ''
|
|
183
|
+
expect(style).toContain('--rs-card-radius')
|
|
184
|
+
expect(style).toMatch(/var\(--rs-radius\)|--rs-radius[^-]/)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('applies explicit radius css var', () => {
|
|
188
|
+
const wrapper = mount(RsCard, {
|
|
189
|
+
props: { radius: 'lg' },
|
|
190
|
+
slots: { default: '内容' },
|
|
191
|
+
})
|
|
192
|
+
expect(wrapper.attributes('style')).toContain('var(--rs-radius-lg)')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('applies none radius as 0', () => {
|
|
196
|
+
const wrapper = mount(RsCard, {
|
|
197
|
+
props: { radius: 'none' },
|
|
198
|
+
slots: { default: '内容' },
|
|
199
|
+
})
|
|
200
|
+
expect(wrapper.attributes('style')).toMatch(/--rs-card-radius:\s*0/)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('applies borderless class when borderless is true', () => {
|
|
204
|
+
const wrapper = mount(RsCard, {
|
|
205
|
+
props: { borderless: true },
|
|
206
|
+
slots: { default: '内容' },
|
|
207
|
+
})
|
|
208
|
+
expect(wrapper.classes()).toContain('rs-card--borderless')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('supports admin outlined preset combination', () => {
|
|
212
|
+
const wrapper = mount(RsCard, {
|
|
213
|
+
props: {
|
|
214
|
+
variant: 'outlined',
|
|
215
|
+
size: 'md',
|
|
216
|
+
radius: 'md',
|
|
217
|
+
title: '节点列表',
|
|
218
|
+
},
|
|
219
|
+
slots: { default: '表格' },
|
|
220
|
+
})
|
|
221
|
+
expect(wrapper.classes()).toEqual(
|
|
222
|
+
expect.arrayContaining(['rs-card--outlined', 'rs-card--md']),
|
|
223
|
+
)
|
|
224
|
+
expect(wrapper.find('.rs-card__title').text()).toBe('节点列表')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('supports frontend marketing preset combination', () => {
|
|
228
|
+
const wrapper = mount(RsCard, {
|
|
229
|
+
props: {
|
|
230
|
+
variant: 'filled',
|
|
231
|
+
size: 'lg',
|
|
232
|
+
radius: 'lg',
|
|
233
|
+
elevated: true,
|
|
234
|
+
hoverable: true,
|
|
235
|
+
clip: true,
|
|
236
|
+
title: '产品亮点',
|
|
237
|
+
},
|
|
238
|
+
slots: {
|
|
239
|
+
cover: '<img alt="" src="about:blank" />',
|
|
240
|
+
default: '介绍',
|
|
241
|
+
footer: '<button type="button">了解更多</button>',
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
expect(wrapper.classes()).toEqual(
|
|
245
|
+
expect.arrayContaining([
|
|
246
|
+
'rs-card--filled',
|
|
247
|
+
'rs-card--lg',
|
|
248
|
+
'rs-card--elevated',
|
|
249
|
+
'rs-card--hoverable',
|
|
250
|
+
'rs-card--clip',
|
|
251
|
+
]),
|
|
252
|
+
)
|
|
253
|
+
expect(wrapper.find('.rs-card__cover img').exists()).toBe(true)
|
|
254
|
+
expect(wrapper.find('.rs-card__footer').exists()).toBe(true)
|
|
255
|
+
})
|
|
97
256
|
})
|
|
@@ -62,6 +62,76 @@ describe('RsConfirmDialog', () => {
|
|
|
62
62
|
wrapper.unmount()
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
+
it('renders subtitle and description separately', async () => {
|
|
66
|
+
const wrapper = mount(RsConfirmDialog, {
|
|
67
|
+
props: {
|
|
68
|
+
open: true,
|
|
69
|
+
title: '确认删除',
|
|
70
|
+
subtitle: '此操作不可恢复',
|
|
71
|
+
description: '确定删除该用户吗?',
|
|
72
|
+
},
|
|
73
|
+
attachTo: document.body,
|
|
74
|
+
})
|
|
75
|
+
await flushPromises()
|
|
76
|
+
const content = document.body.querySelector('.rs-confirm-dialog__content')
|
|
77
|
+
expect(content?.querySelector('.rs-confirm-dialog__subtitle')?.textContent).toContain(
|
|
78
|
+
'此操作不可恢复',
|
|
79
|
+
)
|
|
80
|
+
expect(content?.querySelector('.rs-confirm-dialog__description')?.textContent).toContain(
|
|
81
|
+
'确定删除该用户吗?',
|
|
82
|
+
)
|
|
83
|
+
wrapper.unmount()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('applies custom width as max-width', async () => {
|
|
87
|
+
const wrapper = mount(RsConfirmDialog, {
|
|
88
|
+
props: { open: true, title: '宽', width: 500 },
|
|
89
|
+
attachTo: document.body,
|
|
90
|
+
})
|
|
91
|
+
await flushPromises()
|
|
92
|
+
const content = document.body.querySelector('.rs-confirm-dialog__content') as HTMLElement
|
|
93
|
+
expect(content.style.maxWidth).toBe('500px')
|
|
94
|
+
wrapper.unmount()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('applies preset width class', async () => {
|
|
98
|
+
const wrapper = mount(RsConfirmDialog, {
|
|
99
|
+
props: { open: true, title: 'md', width: 'md' },
|
|
100
|
+
attachTo: document.body,
|
|
101
|
+
})
|
|
102
|
+
await flushPromises()
|
|
103
|
+
const content = document.body.querySelector('.rs-confirm-dialog__content')
|
|
104
|
+
expect(content?.classList.contains('rs-confirm-dialog__content--md')).toBe(true)
|
|
105
|
+
wrapper.unmount()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('keeps open when autoCloseOnConfirm is false', async () => {
|
|
109
|
+
const Host = defineComponent({
|
|
110
|
+
components: { RsConfirmDialog },
|
|
111
|
+
setup() {
|
|
112
|
+
const open = ref(true)
|
|
113
|
+
return { open }
|
|
114
|
+
},
|
|
115
|
+
template: `
|
|
116
|
+
<RsConfirmDialog
|
|
117
|
+
v-model:open="open"
|
|
118
|
+
title="异步确认"
|
|
119
|
+
:auto-close-on-confirm="false"
|
|
120
|
+
@confirm="$emit('confirmed')"
|
|
121
|
+
/>
|
|
122
|
+
`,
|
|
123
|
+
})
|
|
124
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
125
|
+
await flushPromises()
|
|
126
|
+
const buttons = document.body.querySelectorAll('.rs-confirm-dialog__footer button')
|
|
127
|
+
const confirmBtn = buttons[buttons.length - 1] as HTMLElement
|
|
128
|
+
await confirmBtn.click()
|
|
129
|
+
await flushPromises()
|
|
130
|
+
expect(wrapper.emitted('confirmed')).toHaveLength(1)
|
|
131
|
+
expect(wrapper.findComponent(RsConfirmDialog).props('open')).toBe(true)
|
|
132
|
+
wrapper.unmount()
|
|
133
|
+
})
|
|
134
|
+
|
|
65
135
|
it('hides overlay by default', async () => {
|
|
66
136
|
const wrapper = mount(RsConfirmDialog, {
|
|
67
137
|
props: { open: true, title: '无遮罩' },
|
|
@@ -155,4 +225,61 @@ describe('RsConfirmDialog', () => {
|
|
|
155
225
|
expect(target.querySelector('.rs-confirm-dialog__content')).not.toBeNull()
|
|
156
226
|
wrapper.unmount()
|
|
157
227
|
})
|
|
228
|
+
|
|
229
|
+
it('hides cancel button when showCancel is false', async () => {
|
|
230
|
+
const wrapper = mount(RsConfirmDialog, {
|
|
231
|
+
props: {
|
|
232
|
+
open: true,
|
|
233
|
+
title: '单按钮提示',
|
|
234
|
+
showCancel: false,
|
|
235
|
+
confirmText: '知道了',
|
|
236
|
+
},
|
|
237
|
+
attachTo: document.body,
|
|
238
|
+
})
|
|
239
|
+
await flushPromises()
|
|
240
|
+
const buttons = document.body.querySelectorAll('.rs-confirm-dialog__footer button')
|
|
241
|
+
expect(buttons).toHaveLength(1)
|
|
242
|
+
expect(buttons[0]?.textContent).toContain('知道了')
|
|
243
|
+
wrapper.unmount()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('renders #extra slot between description and footer', async () => {
|
|
247
|
+
const wrapper = mount(RsConfirmDialog, {
|
|
248
|
+
props: { open: true, title: '额外内容', description: '正文' },
|
|
249
|
+
slots: {
|
|
250
|
+
extra: () => h('div', { class: 'extra-probe' }, 'extra-body'),
|
|
251
|
+
},
|
|
252
|
+
attachTo: document.body,
|
|
253
|
+
})
|
|
254
|
+
await flushPromises()
|
|
255
|
+
const extra = document.body.querySelector('.rs-confirm-dialog__extra .extra-probe')
|
|
256
|
+
expect(extra?.textContent).toContain('extra-body')
|
|
257
|
+
wrapper.unmount()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('blocks close when beforeClose returns false', async () => {
|
|
261
|
+
const Host = defineComponent({
|
|
262
|
+
components: { RsConfirmDialog },
|
|
263
|
+
setup() {
|
|
264
|
+
const open = ref(true)
|
|
265
|
+
return { open }
|
|
266
|
+
},
|
|
267
|
+
template: `
|
|
268
|
+
<RsConfirmDialog
|
|
269
|
+
v-model:open="open"
|
|
270
|
+
title="拦截"
|
|
271
|
+
:before-close="() => false"
|
|
272
|
+
/>
|
|
273
|
+
`,
|
|
274
|
+
})
|
|
275
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
276
|
+
await flushPromises()
|
|
277
|
+
const cancelBtn = document.body.querySelector(
|
|
278
|
+
'.rs-confirm-dialog__footer button',
|
|
279
|
+
) as HTMLElement
|
|
280
|
+
await cancelBtn.click()
|
|
281
|
+
await flushPromises()
|
|
282
|
+
expect(wrapper.findComponent(RsConfirmDialog).props('open')).toBe(true)
|
|
283
|
+
wrapper.unmount()
|
|
284
|
+
})
|
|
158
285
|
})
|
|
@@ -115,4 +115,16 @@ describe('RsContextMenu', () => {
|
|
|
115
115
|
expect(document.body.textContent).toContain('项 19')
|
|
116
116
|
wrapper.unmount()
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
it('does not open empty menu bubble when items are empty', async () => {
|
|
120
|
+
const wrapper = mount(RsContextMenu, {
|
|
121
|
+
props: { items: [] },
|
|
122
|
+
slots: { default: '<div class="trigger">Right click</div>' },
|
|
123
|
+
attachTo: document.body,
|
|
124
|
+
})
|
|
125
|
+
await wrapper.find('.trigger').trigger('contextmenu')
|
|
126
|
+
await flushPromises()
|
|
127
|
+
expect(document.body.querySelector('.rs-context-menu__content')).toBeNull()
|
|
128
|
+
wrapper.unmount()
|
|
129
|
+
})
|
|
118
130
|
})
|
|
@@ -60,6 +60,17 @@ describe('RsDateTimePicker', () => {
|
|
|
60
60
|
wrapper.unmount()
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('propagates size to embedded time picker', async () => {
|
|
64
|
+
const wrapper = mount(RsDateTimePicker, {
|
|
65
|
+
props: { modelValue: '', size: 'lg', open: true },
|
|
66
|
+
attachTo: document.body,
|
|
67
|
+
})
|
|
68
|
+
await flushPromises()
|
|
69
|
+
expect(wrapper.find('.rs-date-picker--lg').exists()).toBe(true)
|
|
70
|
+
expect(document.body.querySelector('.rs-time-picker--lg')).not.toBeNull()
|
|
71
|
+
wrapper.unmount()
|
|
72
|
+
})
|
|
73
|
+
|
|
63
74
|
it('disables trigger when disabled', () => {
|
|
64
75
|
const wrapper = mount(RsDateTimePicker, {
|
|
65
76
|
props: { modelValue: '2025-06-16 14:30:00', disabled: true },
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { h } from 'vue'
|
|
4
|
+
import RsDescriptions from '../components/RsDescriptions.vue'
|
|
5
|
+
import RsConfigProvider from '../components/RsConfigProvider.vue'
|
|
6
|
+
|
|
7
|
+
const sampleItems = [
|
|
8
|
+
{ key: 'name', label: '名称', value: '弱水' },
|
|
9
|
+
{ key: 'owner', label: '负责人', value: 'Ada' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('RsDescriptions', () => {
|
|
13
|
+
it('defaults to md size without config override', () => {
|
|
14
|
+
const wrapper = mount(RsDescriptions, {
|
|
15
|
+
props: { items: sampleItems },
|
|
16
|
+
})
|
|
17
|
+
expect(wrapper.classes()).toContain('rs-descriptions--md')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('inherits controlSize from RsConfigProvider when size omitted', () => {
|
|
21
|
+
const wrapper = mount(RsConfigProvider, {
|
|
22
|
+
props: { controlSize: 'sm' },
|
|
23
|
+
slots: {
|
|
24
|
+
default: () => h(RsDescriptions, { items: sampleItems }),
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
expect(wrapper.find('.rs-descriptions').classes()).toContain('rs-descriptions--sm')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('props size overrides RsConfigProvider controlSize', () => {
|
|
31
|
+
const wrapper = mount(RsConfigProvider, {
|
|
32
|
+
props: { controlSize: 'sm' },
|
|
33
|
+
slots: {
|
|
34
|
+
default: () => h(RsDescriptions, { items: sampleItems, size: 'lg' }),
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
expect(wrapper.find('.rs-descriptions').classes()).toContain('rs-descriptions--lg')
|
|
38
|
+
expect(wrapper.find('.rs-descriptions').classes()).not.toContain('rs-descriptions--sm')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('supports ssm from config', () => {
|
|
42
|
+
const wrapper = mount(RsConfigProvider, {
|
|
43
|
+
props: { controlSize: 'ssm' },
|
|
44
|
+
slots: {
|
|
45
|
+
default: () => h(RsDescriptions, { items: sampleItems }),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
expect(wrapper.find('.rs-descriptions').classes()).toContain('rs-descriptions--ssm')
|
|
49
|
+
})
|
|
50
|
+
})
|