rj-grid 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -1,98 +1,328 @@
1
- # rj-grid
2
-
3
- 自研**零第三方依赖**的企业级数据网格(Vue 3 + TypeScript),功能规格对标 AG Grid:虚拟滚动、冻结列、行/列拖拽、分组透视、树形、编辑与撤销、公式引擎、SSRM 服务端行模型、主题与 i18n、打印/CSV/Excel 导出、离线自然语言查询(NLQ)。gzip 约 93 KB(不含 vue / echarts)。
4
-
5
- 本仓库是该组件的**独立维护仓**:源码、单测、构建链、演示 playground、使用手册都在这里;产物已发布到**公共 npm(包名 `rj-grid`)**,宿主项目(如 join-cloud-vue3)按版本号安装,不参与组件开发。
6
-
7
- ## 仓库布局
8
-
9
- | 路径 | 作用 |
10
- |---|---|
11
- | `index.ts` | 包入口(值导出 `RjGrid` + NLQ / 查询条件内核 + 全部公共类型) |
12
- | `src/` | 组件与引擎源码(`src/types.ts` 是类型权威) |
13
- | `__tests__/` | 346 个单测用例,入口 `__tests__/run.ts` |
14
- | `build/rjDistCheck.mjs` | 产物体检五守卫(构建末尾自动跑) |
15
- | `build/rjSizeReport.mjs` | 体积报告 |
16
- | `vite.lib.config.ts` | 库构建:ESM 单文件 + 单 `style.css`,external `vue`/`echarts`,不出 sourcemap |
17
- | `vite.playground.config.ts` | 演示页 dev/build,`rj-grid` 别名指本地 `index.ts` |
18
- | `playground/` | 8 标签演示页(独立起服,不依赖宿主) |
19
- | `docs/RjGrid使用手册.md` | 完整使用手册(能力、API、坑与边界) |
20
- | `dist/` | 发布产物,**随库入库**(见「发布纪律」) |
21
-
22
- ## 快速上手(维护者)
23
-
24
- ```powershell
25
- pnpm install
26
-
27
- pnpm test # 单测 346 passed
28
- pnpm ts # vue-tsc --noEmit(自有 tsconfig,无宿主基线噪声)
29
- pnpm lint # eslint + stylelint
30
- pnpm build # vite lib → vue-tsc 产 d.ts → 产物体检五守卫
31
- pnpm size # 体积报告(可选)
32
-
33
- pnpm dev # playground 演示页(默认 :5173,alias 走本地源码)
34
- ```
35
-
36
- 发布新版本:改完源码 → `npm version patch|minor|major`(自动 bump + commit + tag)→ `git push --follow-tags` → `npm publish`(`prepublishOnly` 会自动 `pnpm build && pnpm test`,杜绝发出旧 dist)。
37
-
38
- ## 宿主接入(npm 版本号)
39
-
40
- ```jsonc
41
- // 宿主 package.json —— 从公共 npm 按语义化版本安装
42
- "dependencies": {
43
- "rj-grid": "^0.1.0"
44
- }
45
- ```
46
-
47
- ```powershell
48
- pnpm add rj-grid # 或 npm i rj-grid
49
- ```
50
-
51
- > 仍保留 git URL 方式(离线内网 / 需锁 commit 时):`"rj-grid": "git+https://gitee.com/rjding/rj-grid.git#v0.1.0"`。
52
-
53
- 从 npm 安装时目录名规范(`node_modules/rj-grid`),样式可**直接**引:`import 'rj-grid/style.css'`。仅当走 git URL 时才需下面的 CSS 垫片——pnpm 会把 git 依赖装进带 `#<commit>` 的目录名,vite 输出的资源 URL 含裸 `#`,浏览器把 `#` 之后当片段丢掉 → dev 下 404:
54
-
55
- ```css
56
- /* 宿主 src/styles/rjgrid/index.css */
57
- @import 'rj-grid/style.css';
58
- ```
59
-
60
- ```ts
61
- // 宿主 src/main.ts
62
- import '@/styles/rjgrid/index.css'
63
- ```
64
-
65
- 组件侧必须是**值导入**(`RjGrid` 不走宿主的自动组件注册,只写 type 导入会在运行期报 `Failed to resolve component: rj-grid`):
66
-
67
- ```ts
68
- import { RjGrid } from 'rj-grid'
69
- ```
70
-
71
- peer 依赖:`vue ^3.4.0` 必装;`echarts >=5.0.0` 可选(仅「集成图表」用到,包内 `await import('echarts')` 动态加载)。注意宿主没装 echarts 时 `vite build` 会因 rollup 解析不到该动态 import 而失败——要么装上,要么宿主加 `build.rollupOptions.external: ['echarts']`;组件自身有降级,图表对话框会显示一行提示而非崩页。
72
-
73
- ## 升级与回滚
74
-
75
- ```powershell
76
- pnpm update rj-grid # 在 ^0.1.0 范围内升到最新
77
- pnpm add rj-grid@latest # 跨范围拉最新
78
- ```
79
-
80
- npm 安装的 lockfile 记录精确版本与 `integrity`,CI / 他人 `pnpm install --frozen-lockfile` 可复现。回滚只需把版本钉回旧号(如 `"rj-grid": "0.1.0"`)。本地联调可临时用 `"link:../rjGrid"`,调完换回版本号并重装。
81
-
82
- ## 为什么 dist 入库
83
-
84
- 发布走 `prepublishOnly`(`pnpm build && pnpm test`):`npm publish` 打的是**构建后磁盘上的 `dist/`**(按 `files` 字段 `["dist","README.md"]` 裁剪),所以公共 npm 消费方无需依赖仓库里的 dist。dist 仍入库只是为了兼容 **git URL 安装**那条路径(git 依赖不会在宿主机跑 `prepare` 重建);保留 dist 入库能同时喂饱两条路。
85
-
86
- `build/rjDistCheck.mjs` 的五道守卫同时在把关:外部 import 只允许 `vue`/`echarts`;CSS 不引宿主资源;d.ts 不断链;dist 不得混入 `.vue/.ts/.scss` 源码;**不得带 sourcemap**——vite 的 `.map` 内嵌 `sourcesContent`(原始源码),一发包等于源码同步外流,因此 `vite.lib.config.ts` 里 `sourcemap: false` 是源码保护而非可选项。
87
-
88
- ## 私有仓注意
89
-
90
- 已发布到**公共 npm**,按版本号安装无需任何 git 凭据。仅当某宿主仍走 git URL 且仓库为 gitee 私有仓时,安装机才需具备克隆凭据(HTTPS 走 credential manager 里的 gitee 账号或部署密钥),CI 需预置,否则会卡在 git 认证。
91
-
92
- ## 文档
93
-
94
- 完整能力、API 与已知边界见 [docs/RjGrid使用手册.md](docs/RjGrid使用手册.md);`playground` 的 8 个标签既是能力地图也是回归入口。
95
-
96
- ## 赞助
97
- 码农不易,^V^
98
- ![小小支援,大大的爱](docs/88888.png)
1
+ <p align="center">
2
+ <img src="docs/images/01-overview.png" alt="rj-grid 综合功能总览" width="100%" />
3
+ </p>
4
+
5
+ <h1 align="center">rj-grid</h1>
6
+
7
+ <p align="center">
8
+ <b>零第三方依赖的企业级 Vue 3 数据网格 —— 对标 AG Grid 的全功能,MIT 免费、gzip 约 93 KB。</b><br/>
9
+ 虚拟滚动 · 冻结列 · 分组透视 · 树形 · 编辑撤销 · 公式引擎 · SSRM 服务端行模型 · 主题 & i18n · 打印/CSV/Excel/PDF · 离线自然语言查询(NLQ)
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/rj-grid"><img src="https://img.shields.io/npm/v/rj-grid.svg?logo=npm&color=cb3837" alt="npm version"/></a>
14
+ <a href="https://github.com/rjding/rjGrid"><img src="https://img.shields.io/github/stars/rjding/rjGrid?logo=github&color=181717" alt="GitHub stars"/></a>
15
+ <a href="https://gitee.com/rjding/rj-grid"><img src="https://img.shields.io/badge/Gitee-rj--grid-orange?logo=gitee&logoColor=white" alt="Gitee mirror"/></a>
16
+ <a href="https://github.com/rjding/rjGrid/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License MIT"/></a>
17
+ <a href="https://gitee.com/rjding/rj-grid"><img src="https://img.shields.io/badge/third--party%20deps-0-blue.svg" alt="Zero dependencies"/></a>
18
+ <img src="https://img.shields.io/badge/gzip-~93KB-lightgrey.svg" alt="Bundle size"/>
19
+ <a href="https://vuejs.org/"><img src="https://img.shields.io/badge/vue-3.x-brightgreen.svg?logo=vuedotjs" alt="Vue 3"/></a>
20
+ <img src="https://img.shields.io/badge/tests-346%20passed-yellowgreen.svg" alt="Tests"/>
21
+ <img src="https://img.shields.io/badge/types-TypeScript-blue.svg?logo=typescript" alt="TypeScript"/>
22
+ </p>
23
+
24
+ <p align="center">
25
+ <a href="#-快速开始">快速开始</a> ·
26
+ <a href="#-能力演示">能力演示</a> ·
27
+ <a href="#-与-ag-grid-对比">与 AG Grid 对比</a> ·
28
+ <a href="docs/RjGrid使用手册.md">完整手册</a> ·
29
+ <a href="https://github.com/rjding/rjGrid">GitHub</a> ·
30
+ <a href="https://gitee.com/rjding/rj-grid">Gitee</a> ·
31
+ <a href="README_EN.md">English</a>
32
+ </p>
33
+
34
+ ---
35
+
36
+ ## 💡 为什么选 rj-grid
37
+
38
+ 想象一下:一个 `<rj-grid>` 标签,就能替换掉你项目里那些「几万行就卡死、想加个分组又得换库」的表格——而**不额外引入任何第三方依赖**,也不用给商业表格库交授权费。
39
+
40
+ - **真·零依赖**:运行时只需 `vue`,连可选的图表都是按需 `import('echarts')`。不捆绑、不污染你的 lockfile。
41
+ - **小得离谱**:gzip 约 93 KB,却装下了别人几个库加起来的功能量。
42
+ - **10 万行丝滑**:行列双向虚拟滚动,实测 100,000 行 × 12 列秒开(生成仅约 12 ms)。
43
+ - **对标 AG Grid 全功能**:树形 / 分组 / 透视 / SSRM / 公式 / 主题 / i18n / 导出 / NLQ 一应俱全。
44
+ - **TypeScript 原生**:完整的类型推导与 `.d.ts`,编辑器全程提示,不是「先跑起来再说」的糙活。
45
+ - **免费商用**:MIT 协议,个人和企业都能放心用。
46
+
47
+ ---
48
+
49
+ ## ✨ 特性一览
50
+
51
+ | 分类 | 你会得到的能力 |
52
+ |---|---|
53
+ | 渲染 | 行列虚拟滚动(10 万行流畅)、冻结列、多级表头、密度三档、钉行、全宽行 |
54
+ | 数据 | 客户端 / 分页 / 无限滚动 / **SSRM 服务端行模型**(按需块缓存)四种模式 |
55
+ | 分组 | 树形任意深度、拖拽分组、多级分组 + 组页脚聚合、列透视(含高基数护栏) |
56
+ | 编辑 | 8 类编辑器、校验、撤销/重做、批量事务回填、内置新增/编辑行弹窗 |
57
+ | 公式 | 零依赖 Excel 风格公式:`=B2*C2`、`SUM/COUNTIF/SUMIF`、区域引用、循环引用检测 |
58
+ | 筛选 | 列头漏斗 / 浮动筛选行 / 快速搜索 / 高级过滤(跨列 AND/OR 表达式树)四路叠加 |
59
+ | 外观 | 主题引擎(4 预设 × 明暗 × 强调色 × 自定义 CSS 变量)、i18n(中/英 + 文案覆盖)、图标替换 |
60
+ | 输出 | 打印 / CSV / Excel / PDF,支持选中行 / 当前视图 / 后端导出三种范围 |
61
+ | AI | 纯离线确定性 **NLQ**,中英双语随 i18n 切换:中文用「数量大于50 按单价降序」、英文用 `qty > 50 sort by price desc`,回显随语言 |
62
+ | 状态 | `stateKey` 自动持久化 + `getState/setState` 编程式存取,刷新不丢视图 |
63
+
64
+ ---
65
+
66
+ ## 🚀 快速开始
67
+
68
+ > 三步,两分钟,跑起来。
69
+
70
+ **① 安装**
71
+
72
+ ```bash
73
+ npm install rj-grid
74
+ # 或 pnpm add rj-grid / yarn add rj-grid
75
+ ```
76
+
77
+ **② 引入组件与样式**
78
+
79
+ ```ts
80
+ // main.ts —— 样式全局引一次即可
81
+ import 'rj-grid/style.css'
82
+ ```
83
+
84
+ **③ 直接使用**
85
+
86
+ ```vue
87
+ <template>
88
+ <rj-grid :columns="cols" :rows="rows" row-key="id" :height="480" show-toolbar />
89
+ </template>
90
+
91
+ <script setup lang="ts">
92
+ import { RjGrid } from 'rj-grid' // ⚠️ 必须「值导入」,不能只写 type
93
+ import type { RjColumn, RjRowData } from 'rj-grid'
94
+
95
+ const cols: RjColumn[] = [
96
+ { field: 'name', title: '名称', width: 160, filter: 'text' },
97
+ { field: 'qty', title: '数量', type: 'num', aggFunc: 'sum' },
98
+ { field: 'price', title: '单价', type: 'money', aggFunc: 'avg' }
99
+ ]
100
+ const rows: RjRowData[] = [
101
+ { id: 1, name: '轴承', qty: 100, price: 12.5 },
102
+ { id: 2, name: '电机', qty: 30, price: 480 }
103
+ ]
104
+ </script>
105
+ ```
106
+
107
+ `show-toolbar` 一开,冻结列、分组、筛选、导出、列显隐全部自带,无需再写一行代码。想解锁更多能力,加个属性即可 —— 每个特性的完整参数见 [📖 使用手册](docs/RjGrid使用手册.md)。
108
+
109
+ ---
110
+
111
+ ## 🎬 能力演示
112
+
113
+ 真实产品截图/动图,全部来自 `playground` 演示页:
114
+
115
+ <table>
116
+ <tr>
117
+ <td width="50%">
118
+ <b>🎨 主题引擎 · 4 预设 × 明暗 × 多语言</b><br/>
119
+ <img src="docs/images/theme-cycle.gif" alt="主题切换动画" width="100%"/>
120
+ </td>
121
+ <td width="50%">
122
+ <b>🤖 离线自然语言查询(NLQ)</b><br/>
123
+ 输入一句话,自动完成「筛选 + 排序 + 分组」。<br/>
124
+ <img src="docs/images/nlq-query.gif" alt="NLQ 查询动画" width="100%"/>
125
+ </td>
126
+ </tr>
127
+ <tr>
128
+ <td>
129
+ <b>🌲 树形层级 · 一键展开 / 折叠</b><br/>
130
+ <img src="docs/images/tree-expand.gif" alt="树形展开折叠动画" width="100%"/>
131
+ </td>
132
+ <td>
133
+ <b>🔍 快速过滤 · 跨全列即时匹配高亮</b><br/>
134
+ <img src="docs/images/quick-filter.gif" alt="快速过滤动画" width="100%"/>
135
+ </td>
136
+ </tr>
137
+ <tr>
138
+ <td>
139
+ <b>🧮 公式引擎 · Excel 风格零依赖</b><br/>
140
+ <img src="docs/images/15-formula.png" alt="公式列" width="100%"/>
141
+ </td>
142
+ <td>
143
+ <b>📦 内置行编辑 / 新增弹窗 · 由列定义自动生成表单</b><br/>
144
+ <img src="docs/images/08-row-form.png" alt="行弹窗" width="100%"/>
145
+ </td>
146
+ </tr>
147
+ <tr>
148
+ <td>
149
+ <b>📊 分组聚合 + 组页脚</b><br/>
150
+ <img src="docs/images/12-group.png" alt="分组聚合" width="100%"/>
151
+ </td>
152
+ <td>
153
+ <b>⚡ 10 万行虚拟滚动 · 生成仅约 12 ms</b><br/>
154
+ <img src="docs/images/10-perf-100k.png" alt="大数据性能" width="100%"/>
155
+ </td>
156
+ </tr>
157
+ <tr>
158
+ <td>
159
+ <b>↕️ 行拖动 · 拖手柄重排行序</b><br/>
160
+ <img src="docs/images/row-drag.gif" alt="行拖动动画" width="100%"/>
161
+ </td>
162
+ <td>
163
+ <b>⇄ 列拖动 · 列菜单「列」页签拖拽换序(列序实时改变)</b><br/>
164
+ <img src="docs/images/col-drag.gif" alt="列拖动动画" width="100%"/>
165
+ </td>
166
+ </tr>
167
+ <tr>
168
+ <td colspan="2">
169
+ <b>➕ 动态添加查询字段 · queryable 一行代码获得可配置查询栏</b><br/>
170
+ <img src="docs/images/query-field.gif" alt="动态查询字段动画" width="100%"/>
171
+ </td>
172
+ </tr>
173
+ <tr>
174
+ <td width="50%">
175
+ <b>🔎 服务端查询 + 分页 · 条件栏与页器同行</b><br/>
176
+ <img src="docs/images/13-server-pagination.png" alt="服务端查询分页" width="100%"/>
177
+ </td>
178
+ <td width="50%">
179
+ <b>☰ 密度三档 · 一屏看更多 / 宽松易读自由切</b><br/>
180
+ <img src="docs/images/21-density.png" alt="显示密度切换" width="100%"/>
181
+ </td>
182
+ </tr>
183
+ <tr>
184
+ <td width="50%">
185
+ <b>💾 保存视图 · 一键存当前查询/列布局/排序为快照</b><br/>
186
+ <img src="docs/images/save-view.gif" alt="保存视图动画" width="100%"/>
187
+ </td>
188
+ <td width="50%">
189
+ <b>▦ 工具面板 · 列显隐/分组/透视拖拽管理</b><br/>
190
+ <img src="docs/images/panel.gif" alt="工具面板动画" width="100%"/>
191
+ </td>
192
+ </tr>
193
+ <tr>
194
+ <td colspan="2">
195
+ <b>🖨 打印 / 导出 · 前端(选中行/当前视图)+ 后端导出三档可选</b><br/>
196
+ <img src="docs/images/print-export.gif" alt="打印导出范围动画" width="100%"/>
197
+ </td>
198
+ </tr>
199
+ </table>
200
+
201
+ ---
202
+
203
+ ## 🔍 与 AG Grid 对比
204
+
205
+ | 维度 | rj-grid | AG Grid |
206
+ |---|---|---|
207
+ | 授权 / 费用 | ✅ MIT 完全免费 | ⚠️ 企业级特性需商业授权 |
208
+ | 第三方运行时依赖 | ✅ 仅 `vue` | ⚠️ 自带框架与模块体系 |
209
+ | gzip 体积 | ✅ 约 93 KB | 显著更大 |
210
+ | Vue 3 原生 | ✅ 值导入即用,非二次封装 | 提供 Vue 封装层 |
211
+ | 离线自然语言查询 | ✅ 内置纯确定性 NLQ | ❌ 无 |
212
+ | 虚拟滚动 / 分组 / 透视 / SSRM / 公式 / 主题 / 导出 | ✅ 全部内置 | ✅ 部分在付费版 |
213
+
214
+ > rj-grid 的定位不是「山寨 AG Grid」,而是给 Vue 项目一个**免费、轻量、开箱即用**的高质量选择。
215
+
216
+ ---
217
+
218
+ ## 🧩 数据模式(按需选择)
219
+
220
+ ```vue
221
+ <!-- 客户端:全量入 rows,排序/筛选/分组全在浏览器,虚拟滚动扛 10 万行 -->
222
+ <rj-grid :columns="cols" :rows="rows" row-key="id" />
223
+
224
+ <!-- 分页:后端分页,loadData 自动携带排序/筛选/页码,分页器位置由 pager-position 决定 -->
225
+ <rj-grid :columns="cols" data-mode="pagination" :load-data="loadData" :page-size="20" />
226
+
227
+ <!-- 无限滚动:滚动到底按块续取,无页码 -->
228
+ <rj-grid :columns="cols" data-mode="infinite" :load-data="loadData" />
229
+
230
+ <!-- SSRM 服务端行模型:分组/大数据块缓存,按需分块加载 -->
231
+ <rj-grid :columns="cols" data-mode="serverSide" :load-data="loadData" server-side-grouping />
232
+ ```
233
+
234
+ ```ts
235
+ import type { RjLoadServerParams } from 'rj-grid'
236
+ // params 自动带 start/end/page/pageSize/sort/filters/quickFilterText/rowGroup…,平铺给后端即可
237
+ async function loadData(p: RjLoadServerParams) {
238
+ const res = await api.queryPage(p)
239
+ return { rows: res.list, total: res.total, success: true }
240
+ }
241
+ ```
242
+
243
+ 各模式的完整配置与回调签名见 [手册 §6 数据模式](docs/RjGrid使用手册.md)。
244
+
245
+ ---
246
+
247
+ ## 🛠 兼容性与技术栈
248
+
249
+ - **框架**:Vue `^3.4.0`(唯一必装 peer 依赖)
250
+ - **可选**:`echarts >= 5.0.0`(仅「集成图表」对话框用到,未安装时优雅降级不崩页)
251
+ - **语言**:TypeScript 全量类型,`.d.ts` 随包发布
252
+ - **样式**:单一 `rj-grid/style.css`,主题走 CSS 变量,可按需覆盖
253
+ - **构建**:Vite 库模式 ESM 单文件,`sideEffects` 仅 CSS,Tree-shaking 友好
254
+
255
+ ---
256
+
257
+ ## 🖥 本地运行演示页
258
+
259
+ 演示页(`playground/`)独立起服、不依赖宿主项目,8 个标签就是能力地图:
260
+
261
+ ```bash
262
+ git clone https://github.com/rjding/rjGrid.git # 国内可用镜像:git clone https://gitee.com/rjding/rj-grid.git
263
+ cd rjGrid
264
+ pnpm install
265
+ pnpm dev # 默认 http://localhost:5173
266
+ ```
267
+
268
+ > ① 综合功能 ② 大数据性能 ③ 树形/分组/透视 ④ 服务端模式 ⑤ SSRM 块缓存 ⑥ 公式引擎 ⑦ 主题 & 国际化 ⑧ AI 自然语言查询
269
+
270
+ ---
271
+
272
+ <details>
273
+ <summary><b>📚 维护者信息(仓库结构 / 开发命令 / 发布纪律)</b></summary>
274
+
275
+ ### 仓库布局
276
+
277
+ | 路径 | 作用 |
278
+ |---|---|
279
+ | `index.ts` | 包入口(值导出 `RjGrid` + NLQ / 查询条件内核 + 全部公共类型) |
280
+ | `src/` | 组件与引擎源码(`src/types.ts` 是类型权威) |
281
+ | `__tests__/` | 346 个单测用例,入口 `__tests__/run.ts` |
282
+ | `build/rjDistCheck.mjs` | 产物体检五守卫(构建末尾自动跑) |
283
+ | `vite.lib.config.ts` | 库构建:ESM 单文件 + 单 `style.css`,external `vue`/`echarts`,不出 sourcemap |
284
+ | `playground/` | 8 标签演示页 |
285
+ | `docs/RjGrid使用手册.md` | 完整使用手册(能力、API、坑与边界) |
286
+ | `dist/` | 发布产物,随库入库 |
287
+
288
+ ### 开发命令
289
+
290
+ ```bash
291
+ pnpm test # 单测 346 passed
292
+ pnpm ts # vue-tsc --noEmit
293
+ pnpm lint # eslint + stylelint
294
+ pnpm build # vite lib → vue-tsc 产 d.ts → 产物体检五守卫
295
+ pnpm size # 体积报告
296
+ pnpm dev # playground 演示页
297
+ ```
298
+
299
+ ### 发布新版本
300
+
301
+ 改完源码 → `npm version patch|minor|major`(自动 bump + commit + tag)→ `git push --follow-tags` → `npm publish`(`prepublishOnly` 会自动 `pnpm build && pnpm test`,杜绝发出旧 dist)。
302
+
303
+ ### 宿主接入补充
304
+
305
+ - 从 npm 安装目录名规范(`node_modules/rj-grid`),样式可直接 `import 'rj-grid/style.css'`。
306
+ - 组件侧必须**值导入** `import { RjGrid } from 'rj-grid'`,只写 type 导入会在运行期报 `Failed to resolve component: rj-grid`。
307
+ - 走 git URL 时 pnpm 会把依赖装进带 `#<commit>` 的目录名,vite 资源 URL 含裸 `#` 会被当片段丢掉 → dev 下 404,此时用 CSS `@import` 垫片(详见手册 §15.4)。
308
+ - `dist` 入库仅为兼容 git URL 安装路径;`sourcemap: false` 是源码保护而非可选项,产物五守卫同时在把关。
309
+
310
+ </details>
311
+
312
+ ---
313
+
314
+ ## 🤝 贡献
315
+
316
+ 欢迎 Issue 与 PR:提 bug、要特性、补文档都行。改动请附带相应单测,保证 `pnpm test && pnpm ts && pnpm lint && pnpm build` 全绿。
317
+
318
+ ## 📄 许可证
319
+
320
+ [MIT](LICENSE) — 个人与企业均可免费使用、修改与再分发。
321
+
322
+ ## ☕ 支持作者
323
+
324
+ 如果 rj-grid 帮到了你,欢迎点个 ⭐ Star,或请作者喝杯咖啡:
325
+
326
+ <p align="center">
327
+ <img src="docs/88888.png" alt="小小支援,大大的爱" width="240"/>
328
+ </p>
package/dist/rj-grid.js CHANGED
@@ -564,6 +564,9 @@ function useColumnState(sourceColumns) {
564
564
  widthMap.set(colIdOf(c), clampWidth(Math.round(cur[i] * factor), c));
565
565
  });
566
566
  }
567
+ function clearWidths() {
568
+ widthMap.clear();
569
+ }
567
570
  function moveColumn(dragId, targetId) {
568
571
  if (dragId === targetId)
569
572
  return;
@@ -637,6 +640,7 @@ function useColumnState(sourceColumns) {
637
640
  setAutoWidth,
638
641
  hasSizedWidth,
639
642
  sizeToFit,
643
+ clearWidths,
640
644
  moveColumn,
641
645
  togglePin,
642
646
  toggleHide,
@@ -4058,6 +4062,7 @@ const zhCN = {
4058
4062
  expServerNoPdf: "后端导出不含 PDF(浏览器打印属前端渲染),已改用前端范围",
4059
4063
  expServerMissing: "未接入后端导出:请设置 serverExport 回调",
4060
4064
  expFailed: "导出失败",
4065
+ printCap: "数据量较大:仅打印前 {n} 行(共 {total} 行)。请缩小范围或走后端导出获取全量",
4061
4066
  resetState: "重置列状态",
4062
4067
  groupBanner: "⬇ 将列标题拖到此处进行行分组(或点面板“分组”页签)",
4063
4068
  // 加载 / 空态
@@ -4222,6 +4227,7 @@ const zhCN = {
4222
4227
  querySearch: "查询",
4223
4228
  queryReset: "重置",
4224
4229
  querySaveView: "存为视图",
4230
+ queryRemove: "移除该条件",
4225
4231
  queryInput: "请输入",
4226
4232
  // 行编辑弹窗与内置确认框(查询栏操作按钮)
4227
4233
  rowFormTitle: "编辑行",
@@ -4294,6 +4300,7 @@ const enUS = {
4294
4300
  expServerNoPdf: "Server export has no PDF (browser printing is front-end); fell back to a front-end range",
4295
4301
  expServerMissing: "No server export wired up: provide a serverExport callback",
4296
4302
  expFailed: "Export failed",
4303
+ printCap: "Large data set: printing the first {n} of {total} rows only. Narrow the range or use Server export for the full set",
4297
4304
  resetState: "Reset column state",
4298
4305
  groupBanner: "⬇ Drag column headers here to group rows (or Panel → Group)",
4299
4306
  loading: "Loading…",
@@ -4444,6 +4451,7 @@ const enUS = {
4444
4451
  querySearch: "Search",
4445
4452
  queryReset: "Reset",
4446
4453
  querySaveView: "Save as view",
4454
+ queryRemove: "Remove this condition",
4447
4455
  queryInput: "Enter",
4448
4456
  // Row edit dialog & built-in confirm (query bar action buttons)
4449
4457
  rowFormTitle: "Edit Row",
@@ -4879,7 +4887,9 @@ function parseNLQ(text, columns) {
4879
4887
  return { ...empty, message: "empty" };
4880
4888
  const result = { ok: false, filters: [], clauses: [] };
4881
4889
  let buf = raw;
4882
- const lim = buf.match(/(?:前|取|返回|最多|top)\s*(\d+)\s*(?:行|条|个|名|件)?/i);
4890
+ const lim = buf.match(
4891
+ /(?:前|取|返回|最多|top|first|limit|show(?:\s+me)?|give\s+me)\s*(?:the\s+)?(\d+)\s*(?:行|条|个|名|件|rows?|records?|items?|results?)?/i
4892
+ );
4883
4893
  if (lim) {
4884
4894
  result.limit = parseInt(lim[1], 10);
4885
4895
  buf = blank(buf, lim.index, lim[0].length);
@@ -4903,6 +4913,7 @@ function parseNLQ(text, columns) {
4903
4913
  if (col) {
4904
4914
  result.groupBy = col.field || col.title || col.colId;
4905
4915
  result.groupColId = col.colId;
4916
+ result.groupTitle = col.title || col.field || col.colId;
4906
4917
  buf = blank(buf, g.start, g.len);
4907
4918
  }
4908
4919
  }
@@ -4911,14 +4922,21 @@ function parseNLQ(text, columns) {
4911
4922
  /sort\s*by\s+([^,,、;;]+)/i,
4912
4923
  /([^,,、;;\s]+?)(?:升序|降序|倒序|从大到小|从小到大)/i,
4913
4924
  // 比较级:X最高/最低/最大/最小/最贵/最便宜/最多/最少(可带尾随「的」一并消费,避免泄漏进相邻筛选值)
4914
- /([^,,、;;\s]+?)最(?:高|低|大|小|贵|便宜|多|少)的?/i
4925
+ /([^,,、;;\s]+?)最(?:高|低|大|小|贵|便宜|多|少)的?/i,
4926
+ // 英文比较级(后缀式):X highest / X most expensive / X cheapest(不含裸 most/least,以免吞掉 at most/at least)
4927
+ /([a-z][a-z ]*?)\s+(?:the\s+)?(?:highest|lowest|largest|biggest|smallest|most expensive|least expensive|cheapest)\b/i,
4928
+ // 英文比较级(前缀式):highest X / cheapest X
4929
+ /\b(?:highest|lowest|largest|biggest|smallest|most expensive|cheapest|least expensive)\s+(?:the\s+)?([a-z][a-z ]*?)(?=$|[,,、;;.]|\s+(?:and|by|of)\b)/i
4915
4930
  ]);
4916
4931
  if (s) {
4917
4932
  const col = resolveColumn(s.name, columns);
4918
4933
  if (col) {
4919
4934
  const dirWord = (s.word || "").toLowerCase();
4920
- const desc = /降序|倒序|从大到小|desc|最高|最大|最贵|最多/.test(dirWord);
4935
+ const desc = /降序|倒序|从大到小|desc|最高|最大|最贵|最多|highest|largest|biggest|most expensive/.test(
4936
+ dirWord
4937
+ );
4921
4938
  result.sort = { field: col.field || col.colId, dir: desc ? "desc" : "asc" };
4939
+ result.sortTitle = col.title || col.field || col.colId;
4922
4940
  buf = blank(buf, s.start, s.len);
4923
4941
  }
4924
4942
  }
@@ -5154,21 +5172,72 @@ function mergeClauses(clauses, columns) {
5154
5172
  }
5155
5173
  return out;
5156
5174
  }
5157
- function explainNLQ(r) {
5175
+ const OP_LABEL = {
5176
+ zh: {
5177
+ eq: "=",
5178
+ ne: "≠",
5179
+ gt: ">",
5180
+ gte: "≥",
5181
+ lt: "<",
5182
+ lte: "≤",
5183
+ contains: "包含",
5184
+ notContains: "不包含",
5185
+ startsWith: "开头是",
5186
+ endsWith: "结尾为",
5187
+ blank: "为空",
5188
+ notBlank: "非空",
5189
+ in: "属于",
5190
+ notIn: "不属于",
5191
+ inRange: "介于"
5192
+ },
5193
+ en: {
5194
+ eq: "=",
5195
+ ne: "≠",
5196
+ gt: ">",
5197
+ gte: "≥",
5198
+ lt: "<",
5199
+ lte: "≤",
5200
+ contains: "contains",
5201
+ notContains: "not contains",
5202
+ startsWith: "starts with",
5203
+ endsWith: "ends with",
5204
+ blank: "is empty",
5205
+ notBlank: "is not empty",
5206
+ in: "is",
5207
+ notIn: "not",
5208
+ inRange: "between"
5209
+ }
5210
+ };
5211
+ function explainNLQ(r, lang = "zh") {
5212
+ const en = lang === "en";
5213
+ const labels = OP_LABEL[lang];
5158
5214
  const parts = [];
5159
5215
  for (const c of r.clauses) {
5160
- const val = c.value2 != null ? `${c.value1}~${c.value2}` : c.value1 ?? "";
5161
- parts.push(`${c.title || c.field || c.colId} ${c.op} ${val}`.trim());
5216
+ const opLabel = labels[c.op] || c.op;
5217
+ const name = c.title || c.field || c.colId;
5218
+ let val;
5219
+ if (c.op === "blank" || c.op === "notBlank")
5220
+ val = "";
5221
+ else if (c.value2 != null)
5222
+ val = `${c.value1}~${c.value2}`;
5223
+ else if (Array.isArray(c.value1))
5224
+ val = c.value1.join(en ? " / " : "、");
5225
+ else
5226
+ val = String(c.value1 ?? "");
5227
+ const glue = en || /^[=<>≤≥≠]/.test(opLabel) ? " " : "";
5228
+ parts.push(`${name} ${opLabel}${glue}${val}`.trim());
5162
5229
  }
5163
5230
  if (r.sort)
5164
- parts.push(`sort ${r.sort.field} ${r.sort.dir}`);
5231
+ parts.push(
5232
+ en ? `sort by ${r.sortTitle || r.sort.field} ${r.sort.dir}` : `按${r.sortTitle || r.sort.field} ${r.sort.dir === "desc" ? "降序" : "升序"}`
5233
+ );
5165
5234
  if (r.groupBy)
5166
- parts.push(`group ${r.groupBy}`);
5235
+ parts.push(en ? `group by ${r.groupTitle || r.groupBy}` : `按${r.groupTitle || r.groupBy}分组`);
5167
5236
  if (r.limit)
5168
- parts.push(`top ${r.limit}`);
5237
+ parts.push(en ? `top ${r.limit}` : `前${r.limit}`);
5169
5238
  if (r.search)
5170
- parts.push(`search "${r.search}"`);
5171
- return parts.join(" | ");
5239
+ parts.push(en ? `search "${r.search}"` : `搜索“${r.search}”`);
5240
+ return parts.join(" · ");
5172
5241
  }
5173
5242
  const csvCell = (v) => {
5174
5243
  if (v == null)
@@ -7694,7 +7763,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
7694
7763
  onMounted(() => {
7695
7764
  const el = menuRef.value;
7696
7765
  if (el)
7697
- emit("reposition", el.offsetHeight);
7766
+ emit("reposition", el.offsetHeight, el.offsetWidth);
7698
7767
  });
7699
7768
  const hoverIdx = ref(-1);
7700
7769
  function onClick(item) {
@@ -9026,7 +9095,7 @@ const _hoisted_3$4 = ["value", "onChange"];
9026
9095
  const _hoisted_4$3 = ["value"];
9027
9096
  const _hoisted_5$3 = ["value", "placeholder", "onInput"];
9028
9097
  const _hoisted_6$3 = ["value", "placeholder", "onInput"];
9029
- const _hoisted_7$2 = ["onClick"];
9098
+ const _hoisted_7$2 = ["title", "onClick"];
9030
9099
  const _hoisted_8$2 = {
9031
9100
  value: "",
9032
9101
  disabled: ""
@@ -9259,7 +9328,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
9259
9328
  }, null, 40, _hoisted_6$3)),
9260
9329
  createElementVNode("span", {
9261
9330
  class: "rj-query-del",
9262
- title: "移除",
9331
+ title: unref(t)("queryRemove"),
9263
9332
  onClick: ($event) => remove(i)
9264
9333
  }, "✕", 8, _hoisted_7$2)
9265
9334
  ]);
@@ -9849,6 +9918,7 @@ function buildPrintHtml(input, opts = {}) {
9849
9918
  const headBg = o.headerDark ? "#4472c4" : "#eef1f6";
9850
9919
  const headColor = o.headerDark ? "#ffffff" : "#1f2329";
9851
9920
  const zebraRule = o.zebra ? "tbody tr:nth-child(even) td{background:#f7f9fc}" : "";
9921
+ const rowBreakRule = input.matrix.length > 300 ? "tr { break-inside: auto; page-break-inside: auto; }" : "tr { page-break-inside: avoid; break-inside: avoid; }";
9852
9922
  const style = `
9853
9923
  @page { ${pageBox} margin: ${o.margins}mm; }
9854
9924
  ${o.pageNumbers ? `@page { @bottom-center { content: ${pageContentCss(o.pageNumberText)}; font-size: 10px; color: #999; } }` : ""}
@@ -9860,7 +9930,7 @@ body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", Arial, sans-se
9860
9930
  .rj-print-sub { font-size: 12px; color: #666; margin: 0 0 10px; }
9861
9931
  table { border-collapse: collapse; width: 100%; table-layout: ${o.scaleToFit ? "auto" : "fixed"}; font-size: 12px; }
9862
9932
  thead { display: table-header-group; }
9863
- tr { page-break-inside: avoid; break-inside: avoid; }
9933
+ ${rowBreakRule}
9864
9934
  th, td { border: ${border}; padding: 4px 8px; text-align: left; vertical-align: top; word-break: break-word; }
9865
9935
  thead th { background: ${headBg}; color: ${headColor}; font-weight: 600; text-align: center; }
9866
9936
  ${zebraRule}
@@ -9948,19 +10018,19 @@ const _hoisted_7 = {
9948
10018
  };
9949
10019
  const _hoisted_8 = { key: 0 };
9950
10020
  const _hoisted_9 = ["onClick"];
9951
- const _hoisted_10 = ["title"];
9952
- const _hoisted_11 = ["title"];
9953
- const _hoisted_12 = ["title"];
9954
- const _hoisted_13 = ["title"];
10021
+ const _hoisted_10 = ["data-tip", "aria-label"];
10022
+ const _hoisted_11 = ["data-tip", "aria-label"];
10023
+ const _hoisted_12 = ["data-tip", "aria-label"];
10024
+ const _hoisted_13 = ["data-tip", "aria-label"];
9955
10025
  const _hoisted_14 = {
9956
10026
  key: 5,
9957
10027
  class: "rj-export-group"
9958
10028
  };
9959
- const _hoisted_15 = ["title"];
9960
- const _hoisted_16 = ["title"];
9961
- const _hoisted_17 = ["title"];
9962
- const _hoisted_18 = ["title"];
9963
- const _hoisted_19 = ["title"];
10029
+ const _hoisted_15 = ["data-tip"];
10030
+ const _hoisted_16 = ["data-tip"];
10031
+ const _hoisted_17 = ["data-tip"];
10032
+ const _hoisted_18 = ["data-tip"];
10033
+ const _hoisted_19 = ["data-tip", "aria-label"];
9964
10034
  const _hoisted_20 = { style: { "display": "flex", "flex": "1", "min-height": "0" } };
9965
10035
  const _hoisted_21 = ["role"];
9966
10036
  const _hoisted_22 = ["aria-rowindex", "aria-selected", "data-rk"];
@@ -10112,6 +10182,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
10112
10182
  exportRange: { default: "auto" },
10113
10183
  serverExport: {},
10114
10184
  exportFileName: {},
10185
+ printMaxRows: { default: 1e3 },
10115
10186
  exportRawValues: { type: Boolean },
10116
10187
  statusBar: { type: Boolean, default: true },
10117
10188
  statusAggregations: { default: () => ["sum", "avg", "count", "min", "max"] },
@@ -10508,6 +10579,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
10508
10579
  const quickPop = ref(null);
10509
10580
  const quickInputRef = ref();
10510
10581
  function toggleQuickPop(ev) {
10582
+ tip.value = null;
10511
10583
  if (quickPop.value) {
10512
10584
  quickPop.value = null;
10513
10585
  return;
@@ -12590,25 +12662,50 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
12590
12662
  }));
12591
12663
  });
12592
12664
  const menu = ref(null);
12593
- function openMenuAt(x, y, items, above) {
12665
+ const tip = ref(null);
12666
+ function onTipOver(e) {
12667
+ var _a2, _b;
12668
+ const host = e.currentTarget;
12669
+ const el = (_b = (_a2 = e.target) == null ? void 0 : _a2.closest) == null ? void 0 : _b.call(_a2, "[data-tip]");
12670
+ if (!el || !host.contains(el))
12671
+ return;
12672
+ const r = el.getBoundingClientRect();
12673
+ tip.value = { text: el.getAttribute("data-tip") || "", x: r.right, y: r.bottom + 6 };
12674
+ }
12675
+ function onTipOut(e) {
12676
+ var _a2, _b;
12677
+ const el = (_b = (_a2 = e.target) == null ? void 0 : _a2.closest) == null ? void 0 : _b.call(_a2, "[data-tip]");
12678
+ const to = e.relatedTarget;
12679
+ if (el && to && el.contains(to))
12680
+ return;
12681
+ tip.value = null;
12682
+ }
12683
+ function openMenuAt(x, y, items, above, estW = 220) {
12594
12684
  if (!items.length)
12595
12685
  return;
12686
+ tip.value = null;
12596
12687
  const est = items.length * 28 + 16;
12597
12688
  const bottom = window.innerHeight - 8;
12689
+ const xc = Math.max(8, Math.min(x, window.innerWidth - estW - 8));
12598
12690
  if (y + est <= bottom) {
12599
- menu.value = { items, x, y, limit: bottom };
12691
+ menu.value = { items, x: xc, y, limit: bottom };
12600
12692
  return;
12601
12693
  }
12602
12694
  const lim = (above ?? y) - 6;
12603
- menu.value = { items, x, y: Math.max(8, (above ?? y) - est - 8), limit: lim };
12695
+ menu.value = { items, x: xc, y: Math.max(8, (above ?? y) - est - 8), limit: lim };
12604
12696
  }
12605
- function onMenuReposition(h2) {
12697
+ function onMenuReposition(h2, w) {
12606
12698
  const m = menu.value;
12607
12699
  if (!m)
12608
12700
  return;
12609
12701
  const lim = m.limit ?? window.innerHeight - 8;
12702
+ const next = { ...m };
12610
12703
  if (m.y + h2 > lim)
12611
- menu.value = { ...m, y: Math.max(8, lim - h2) };
12704
+ next.y = Math.max(8, lim - h2);
12705
+ if (w && m.x + w > window.innerWidth - 8)
12706
+ next.x = Math.max(8, window.innerWidth - 8 - w);
12707
+ if (next.y !== m.y || next.x !== m.x)
12708
+ menu.value = next;
12612
12709
  }
12613
12710
  function onMenuSelect(item) {
12614
12711
  var _a2;
@@ -12890,7 +12987,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
12890
12987
  { isSeparator: true },
12891
12988
  { name: t("scopeServer"), action: () => callServerExport(fmt) }
12892
12989
  );
12893
- openMenuAt(ev.clientX, ev.clientY, items);
12990
+ const el = ev.currentTarget;
12991
+ const r = el.getBoundingClientRect();
12992
+ openMenuAt(r.left, r.bottom + 6, items, r.top);
12894
12993
  }
12895
12994
  function runFormat(fmt, scope) {
12896
12995
  if (fmt === "print")
@@ -13010,7 +13109,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13010
13109
  return v.join(" ");
13011
13110
  return displayOf(col, v, row, 0);
13012
13111
  }
13013
- function buildPrintInput(params) {
13112
+ function buildPrintInput(params, limit = 0) {
13014
13113
  const cols = exportColumnsOf(params);
13015
13114
  const columns = cols.map((c) => {
13016
13115
  var _a2;
@@ -13022,26 +13121,32 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13022
13121
  });
13023
13122
  const matrix = [];
13024
13123
  const rowStyles = [];
13124
+ const capped = limit > 0;
13125
+ const full = () => capped && matrix.length >= limit;
13025
13126
  const pushRow = (d, kind) => {
13026
13127
  matrix.push(cols.map((c) => printTextOf(c, d.data)));
13027
13128
  rowStyles.push(kind);
13028
13129
  };
13029
13130
  const scope = scopeOf(params);
13030
13131
  if (scope === "view") {
13031
- rowModel.processed.value.displayRows.forEach((d) => {
13132
+ for (const d of rowModel.processed.value.displayRows) {
13133
+ if (full())
13134
+ break;
13032
13135
  if (d.type === "group" && !d.isFooter)
13033
13136
  pushRow(d, 1);
13034
13137
  else if (d.type === "row")
13035
13138
  pushRow(d, 0);
13036
- });
13139
+ }
13037
13140
  } else {
13038
- pickExportRows(scope, exportRowSource(), false).forEach((r) => {
13141
+ for (const r of pickExportRows(scope, exportRowSource(), false)) {
13142
+ if (full())
13143
+ break;
13039
13144
  matrix.push(cols.map((c) => printTextOf(c, r)));
13040
13145
  rowStyles.push(0);
13041
- });
13146
+ }
13042
13147
  }
13043
13148
  const srow = rowModel.summaryRow.value;
13044
- if (scope === "view" && props.showSummary && srow) {
13149
+ if (scope === "view" && props.showSummary && srow && !full()) {
13045
13150
  matrix.push(
13046
13151
  cols.map((c) => {
13047
13152
  const raw = srow[colIdOf(c)] ?? cellRawValue(c, srow);
@@ -13079,8 +13184,22 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13079
13184
  warnIfLoadedOnly(scopeOf(params));
13080
13185
  withExportBusy(() => runPrint(params));
13081
13186
  }
13187
+ function printRowEstimate(scope) {
13188
+ if (scope === "view")
13189
+ return rowModel.processed.value.displayRows.reduce(
13190
+ (n, d) => n + (d.type === "row" || d.type === "group" && !d.isFooter ? 1 : 0),
13191
+ 0
13192
+ );
13193
+ return pickExportRows(scope, exportRowSource(), false).length;
13194
+ }
13082
13195
  function runPrint(params) {
13083
- printHtml(buildPrintInput(params), printOpts(params));
13196
+ const cap = props.printMaxRows;
13197
+ const total = printRowEstimate(scopeOf(params));
13198
+ const limit = cap > 0 && total > cap ? cap : 0;
13199
+ const input = buildPrintInput(params, limit);
13200
+ if (limit)
13201
+ showToast(t("printCap", { n: cap, total }));
13202
+ printHtml(input, printOpts(params));
13084
13203
  }
13085
13204
  const rowGroupPayload = computed(() => {
13086
13205
  void aggRev.value;
@@ -13575,6 +13694,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13575
13694
  scheduleSave();
13576
13695
  dispatchEvent("columnEverythingChanged", {});
13577
13696
  },
13697
+ /** 清除宽度覆盖:把列宽还原到列定义/内容自适应默认值(配合 autoSizeAll 做「自适应↔还原」切换) */
13698
+ resetColumnWidths: () => {
13699
+ colState.clearWidths();
13700
+ applyDefaultAutoWidths();
13701
+ scheduleSave();
13702
+ dispatchEvent("columnEverythingChanged", {});
13703
+ },
13578
13704
  // 主题 / i18n(运行时切换)
13579
13705
  setTheme: (params) => {
13580
13706
  themeState.value = { ...params };
@@ -13816,18 +13942,24 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13816
13942
  onDragover: _cache[21] || (_cache[21] = withModifiers(() => {
13817
13943
  }, ["stop"])),
13818
13944
  onDrop: _cache[22] || (_cache[22] = withModifiers(() => {
13819
- }, ["stop"]))
13945
+ }, ["stop"])),
13946
+ onPointerover: onTipOver,
13947
+ onPointerout: onTipOut,
13948
+ onFocusin: onTipOver,
13949
+ onFocusout: onTipOut
13820
13950
  }, [
13821
13951
  _ctx.quickFilterEnabled ? (openBlock(), createElementBlock("button", {
13822
13952
  key: 0,
13823
13953
  class: normalizeClass(["rj-btn rj-tool-btn rj-quick-btn", { "is-active": !!unref(rowModel).quickFilter.value }]),
13824
- title: t("quickBtn"),
13954
+ "data-tip": t("quickBtn"),
13955
+ "aria-label": t("quickBtn"),
13825
13956
  onClick: _cache[9] || (_cache[9] = ($event) => toggleQuickPop($event))
13826
13957
  }, "🔍", 10, _hoisted_10)) : createCommentVNode("", true),
13827
13958
  _ctx.viewable ? (openBlock(), createElementBlock("button", {
13828
13959
  key: 1,
13829
13960
  class: normalizeClass(["rj-btn rj-tool-btn rj-view-btn", { "is-active": !!currentViewId.value }]),
13830
- title: t("viewBtn"),
13961
+ "data-tip": t("viewBtn"),
13962
+ "aria-label": t("viewBtn"),
13831
13963
  onClick: _cache[10] || (_cache[10] = ($event) => openViewMenu($event))
13832
13964
  }, "◈", 10, _hoisted_11)) : createCommentVNode("", true),
13833
13965
  showExpandCollapse.value ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
@@ -13847,12 +13979,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13847
13979
  }, toDisplayString(t("chart")), 1)) : createCommentVNode("", true),
13848
13980
  createElementVNode("button", {
13849
13981
  class: "rj-btn",
13850
- title: t("density") + " · " + DENSITY_LABELS.value[densityIdx.value],
13982
+ "data-tip": t("density") + " · " + DENSITY_LABELS.value[densityIdx.value],
13983
+ "aria-label": t("density"),
13851
13984
  onClick: _cache[14] || (_cache[14] = ($event) => densityIdx.value = (densityIdx.value + 1) % 3)
13852
13985
  }, toDisplayString(["▤", "☰", "▥"][densityIdx.value]), 9, _hoisted_12),
13853
13986
  createElementVNode("button", {
13854
13987
  class: "rj-btn",
13855
- title: dark.value ? t("toLight") : t("toDark"),
13988
+ "data-tip": dark.value ? t("toLight") : t("toDark"),
13989
+ "aria-label": dark.value ? t("toLight") : t("toDark"),
13856
13990
  onClick: _cache[15] || (_cache[15] = ($event) => toggleDark())
13857
13991
  }, toDisplayString(dark.value ? "☀" : "🌙"), 9, _hoisted_13),
13858
13992
  _ctx.toolPanel ? (openBlock(), createElementBlock("button", {
@@ -13863,29 +13997,30 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13863
13997
  _ctx.exportable ? (openBlock(), createElementBlock("div", _hoisted_14, [
13864
13998
  createElementVNode("button", {
13865
13999
  class: "rj-btn rj-btn-export rj-export-btn",
13866
- title: t("printTip"),
14000
+ "data-tip": t("printTip"),
13867
14001
  onClick: _cache[17] || (_cache[17] = ($event) => openRangeMenu($event, "print"))
13868
14002
  }, toDisplayString(t("printBtn")), 9, _hoisted_15),
13869
14003
  createElementVNode("button", {
13870
14004
  class: "rj-btn rj-btn-export rj-export-btn",
13871
- title: t("csvTip"),
14005
+ "data-tip": t("csvTip"),
13872
14006
  onClick: _cache[18] || (_cache[18] = ($event) => openRangeMenu($event, "csv"))
13873
14007
  }, " CSV ", 8, _hoisted_16),
13874
14008
  createElementVNode("button", {
13875
14009
  class: "rj-btn rj-btn-export rj-export-btn",
13876
- title: t("pdfTip"),
14010
+ "data-tip": t("pdfTip"),
13877
14011
  onClick: _cache[19] || (_cache[19] = ($event) => openRangeMenu($event, "pdf"))
13878
14012
  }, " PDF ", 8, _hoisted_17),
13879
14013
  createElementVNode("button", {
13880
14014
  class: "rj-btn rj-btn-export rj-export-btn",
13881
- title: t("excelTip"),
14015
+ "data-tip": t("excelTip"),
13882
14016
  onClick: _cache[20] || (_cache[20] = ($event) => openRangeMenu($event, "excel"))
13883
14017
  }, " Excel ", 8, _hoisted_18)
13884
14018
  ])) : createCommentVNode("", true),
13885
14019
  _ctx.stateKey ? (openBlock(), createElementBlock("button", {
13886
14020
  key: 6,
13887
14021
  class: "rj-btn",
13888
- title: t("resetState"),
14022
+ "data-tip": t("resetState"),
14023
+ "aria-label": t("resetState"),
13889
14024
  onClick: resetState
13890
14025
  }, "↺", 8, _hoisted_19)) : createCommentVNode("", true)
13891
14026
  ], 32)) : createCommentVNode("", true)
@@ -14452,8 +14587,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
14452
14587
  onClear: clearAdvancedFilter,
14453
14588
  onClose: _cache[43] || (_cache[43] = ($event) => advDialog.value = null)
14454
14589
  }, null, 8, ["model", "columns", "x", "y"])) : createCommentVNode("", true),
14455
- menu.value ? (openBlock(), createBlock(_sfc_main$a, {
14590
+ tip.value ? (openBlock(), createElementBlock("div", {
14456
14591
  key: 8,
14592
+ class: "rj-tip",
14593
+ role: "tooltip",
14594
+ style: normalizeStyle({ left: tip.value.x + "px", top: tip.value.y + "px" })
14595
+ }, toDisplayString(tip.value.text), 5)) : createCommentVNode("", true),
14596
+ menu.value ? (openBlock(), createBlock(_sfc_main$a, {
14597
+ key: 9,
14457
14598
  items: menu.value.items,
14458
14599
  x: menu.value.x,
14459
14600
  y: menu.value.y,
@@ -14461,7 +14602,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
14461
14602
  onReposition: onMenuReposition
14462
14603
  }, null, 8, ["items", "x", "y"])) : createCommentVNode("", true),
14463
14604
  colMenu.value ? (openBlock(), createBlock(_sfc_main$9, {
14464
- key: 9,
14605
+ key: 10,
14465
14606
  col: colMenu.value.col,
14466
14607
  "col-id": colMenu.value.colId,
14467
14608
  x: colMenu.value.x,
@@ -14489,13 +14630,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
14489
14630
  onColDrop
14490
14631
  }, null, 8, ["col", "col-id", "x", "y", "can-sort", "can-filter", "can-group", "sort-dir", "pinned", "filter-type", "filter-model", "unique-values", "columns"])) : createCommentVNode("", true),
14491
14632
  chartOpen.value ? (openBlock(), createBlock(_sfc_main$6, {
14492
- key: 10,
14633
+ key: 11,
14493
14634
  columns: pipelineCols.value,
14494
14635
  rows: chartRows.value,
14495
14636
  onClose: _cache[44] || (_cache[44] = ($event) => chartOpen.value = false)
14496
14637
  }, null, 8, ["columns", "rows"])) : createCommentVNode("", true),
14497
14638
  rowFormDlg.value ? (openBlock(), createBlock(_sfc_main$3, {
14498
- key: 11,
14639
+ key: 12,
14499
14640
  fields: rowFormDlg.value.fields,
14500
14641
  rows: rowFormDlg.value.rows,
14501
14642
  mode: rowFormDlg.value.mode,
@@ -14505,13 +14646,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
14505
14646
  onClose: _cache[45] || (_cache[45] = ($event) => rowFormDlg.value = null)
14506
14647
  }, null, 8, ["fields", "rows", "mode", "title", "width"])) : createCommentVNode("", true),
14507
14648
  confirmDlg.value ? (openBlock(), createBlock(_sfc_main$2, {
14508
- key: 12,
14649
+ key: 13,
14509
14650
  message: confirmDlg.value.message,
14510
14651
  onOk: onConfirmOk,
14511
14652
  onCancel: _cache[46] || (_cache[46] = ($event) => confirmDlg.value = null)
14512
14653
  }, null, 8, ["message"])) : createCommentVNode("", true),
14513
14654
  dragGhost.value ? (openBlock(), createElementBlock("div", {
14514
- key: 13,
14655
+ key: 14,
14515
14656
  ref_key: "dragGhostRef",
14516
14657
  ref: dragGhostRef,
14517
14658
  class: "rj-drag-ghost",
@@ -14532,7 +14673,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
14532
14673
  !dragGhost.value.cells.length ? (openBlock(), createElementBlock("span", _hoisted_51, toDisplayString(t("dragRow")), 1)) : createCommentVNode("", true)
14533
14674
  ], 4)) : createCommentVNode("", true),
14534
14675
  imgPreview.value ? (openBlock(), createElementBlock("div", {
14535
- key: 14,
14676
+ key: 15,
14536
14677
  class: "rj-img-viewer",
14537
14678
  role: "dialog",
14538
14679
  "aria-modal": "true",
package/dist/style.css CHANGED
@@ -10,7 +10,7 @@
10
10
  --rj-text-secondary: #909399;
11
11
  --rj-primary: #3b76f6;
12
12
  --rj-primary-bg: #eaf1fe;
13
- --rj-hover-bg: #f2f6fd;
13
+ --rj-hover-bg: #e6edf9;
14
14
  --rj-selected-bg: #e2edfd;
15
15
  --rj-range-bg: rgba(59, 118, 246, 0.1);
16
16
  --rj-fill-bg: rgba(59, 118, 246, 0.16);
@@ -52,7 +52,7 @@
52
52
  --rj-text-secondary: #8a919f;
53
53
  --rj-primary: #5c8dfb;
54
54
  --rj-primary-bg: #24324d;
55
- --rj-hover-bg: #272d3a;
55
+ --rj-hover-bg: #313a4d;
56
56
  --rj-selected-bg: #2a3a5c;
57
57
  --rj-mask: rgba(30, 34, 42, 0.75);
58
58
  --rj-flash-bg: #5a4c1e;
@@ -509,6 +509,25 @@
509
509
  font-weight: 500;
510
510
  }
511
511
 
512
+ .rj-tip {
513
+ position: fixed;
514
+ z-index: 3100;
515
+ width: max-content;
516
+ max-width: 240px;
517
+ padding: 3px 8px;
518
+ font-size: 12px;
519
+ font-weight: 400;
520
+ line-height: 1.4;
521
+ color: var(--rj-bg);
522
+ text-align: left;
523
+ white-space: normal;
524
+ pointer-events: none;
525
+ background: var(--rj-text);
526
+ border-radius: var(--rj-radius);
527
+ box-shadow: var(--rj-shadow);
528
+ transform: translateX(-100%);
529
+ }
530
+
512
531
  .rj-pager {
513
532
  display: flex;
514
533
  align-items: center;
@@ -5,14 +5,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
5
5
  y: number;
6
6
  }>>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
7
  select: (item: RjMenuItem) => void;
8
- reposition: (h: number) => void;
8
+ reposition: (h: number, w: number) => void;
9
9
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
10
10
  items: RjMenuItem[];
11
11
  x: number;
12
12
  y: number;
13
13
  }>>> & Readonly<{
14
14
  onSelect?: ((item: RjMenuItem) => any) | undefined;
15
- onReposition?: ((h: number) => any) | undefined;
15
+ onReposition?: ((h: number, w: number) => any) | undefined;
16
16
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
17
17
  export default _default;
18
18
  type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
@@ -96,6 +96,12 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
96
96
  */
97
97
  serverExport?: ((params: RjServerExportParams) => void | Promise<void>) | undefined;
98
98
  exportFileName?: string | undefined;
99
+ /**
100
+ * 打印 / PDF 单次最大行数护栏:浏览器打印靠把整表排版进隐藏 iframe,
101
+ * 上万行会生成十几 MB HTML 并让主线程卡死(页面假死)。超过此上限只打印前 N 行并提示,
102
+ * 需全量请先筛选 / 用分页 / 走后端导出,或按宿主机器性能调大本值。置 0 或负数则不限。
103
+ */
104
+ printMaxRows?: number | undefined;
99
105
  /** 导出取值口径:默认导出「所见即所得」的格式化文本(与屏幕一致、单文件内口径统一);
100
106
  * 置 true 则导出原始类型值,让 Excel 里数字仍是数字(对齐 AG Grid useCellValuesForExports) */
101
107
  exportRawValues?: boolean | undefined;
@@ -182,6 +188,7 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
182
188
  copyHeadersToClipboard: boolean;
183
189
  markDirtyCells: boolean;
184
190
  rowHover: boolean;
191
+ printMaxRows: number;
185
192
  queryable: boolean;
186
193
  viewable: boolean;
187
194
  queryFields: () => never[];
@@ -303,6 +310,12 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
303
310
  */
304
311
  serverExport?: ((params: RjServerExportParams) => void | Promise<void>) | undefined;
305
312
  exportFileName?: string | undefined;
313
+ /**
314
+ * 打印 / PDF 单次最大行数护栏:浏览器打印靠把整表排版进隐藏 iframe,
315
+ * 上万行会生成十几 MB HTML 并让主线程卡死(页面假死)。超过此上限只打印前 N 行并提示,
316
+ * 需全量请先筛选 / 用分页 / 走后端导出,或按宿主机器性能调大本值。置 0 或负数则不限。
317
+ */
318
+ printMaxRows?: number | undefined;
306
319
  /** 导出取值口径:默认导出「所见即所得」的格式化文本(与屏幕一致、单文件内口径统一);
307
320
  * 置 true 则导出原始类型值,让 Excel 里数字仍是数字(对齐 AG Grid useCellValuesForExports) */
308
321
  exportRawValues?: boolean | undefined;
@@ -389,6 +402,7 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
389
402
  copyHeadersToClipboard: boolean;
390
403
  markDirtyCells: boolean;
391
404
  rowHover: boolean;
405
+ printMaxRows: number;
392
406
  queryable: boolean;
393
407
  viewable: boolean;
394
408
  queryFields: () => never[];
@@ -472,6 +486,7 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
472
486
  detailHeight: number;
473
487
  fullWidthHeight: number;
474
488
  exportRange: "auto" | RjExportScopeResolved;
489
+ printMaxRows: number;
475
490
  statusBar: boolean;
476
491
  statusAggregations: ("sum" | "avg" | "min" | "max" | "count")[];
477
492
  undoRedoCellEditing: boolean;
@@ -594,6 +609,8 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
594
609
  autoSizeColumn: (colId: string) => void;
595
610
  autoSizeAll: () => void;
596
611
  sizeColumnsToFit: () => void;
612
+ /** 清除宽度覆盖:把列宽还原到列定义/内容自适应默认值(配合 autoSizeAll 做「自适应↔还原」切换) */
613
+ resetColumnWidths: () => void;
597
614
  setTheme: (params: RjThemeParams) => void;
598
615
  getTheme: () => {
599
616
  preset?: "legacy" | "alpine" | "quartz" | "material" | undefined;
@@ -728,6 +745,8 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
728
745
  autoSizeColumn: (colId: string) => void;
729
746
  autoSizeAll: () => void;
730
747
  sizeColumnsToFit: () => void;
748
+ /** 清除宽度覆盖:把列宽还原到列定义/内容自适应默认值(配合 autoSizeAll 做「自适应↔还原」切换) */
749
+ resetColumnWidths: () => void;
731
750
  setTheme: (params: RjThemeParams) => void;
732
751
  getTheme: () => {
733
752
  preset?: "legacy" | "alpine" | "quartz" | "material" | undefined;
@@ -866,6 +885,8 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
866
885
  autoSizeColumn: (colId: string) => void;
867
886
  autoSizeAll: () => void;
868
887
  sizeColumnsToFit: () => void;
888
+ /** 清除宽度覆盖:把列宽还原到列定义/内容自适应默认值(配合 autoSizeAll 做「自适应↔还原」切换) */
889
+ resetColumnWidths: () => void;
869
890
  setTheme: (params: RjThemeParams) => void;
870
891
  getTheme: () => {
871
892
  preset?: "legacy" | "alpine" | "quartz" | "material" | undefined;
@@ -1001,6 +1022,8 @@ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<im
1001
1022
  autoSizeColumn: (colId: string) => void;
1002
1023
  autoSizeAll: () => void;
1003
1024
  sizeColumnsToFit: () => void;
1025
+ /** 清除宽度覆盖:把列宽还原到列定义/内容自适应默认值(配合 autoSizeAll 做「自适应↔还原」切换) */
1026
+ resetColumnWidths: () => void;
1004
1027
  setTheme: (params: RjThemeParams) => void;
1005
1028
  getTheme: () => {
1006
1029
  preset?: "legacy" | "alpine" | "quartz" | "material" | undefined;
@@ -1,4 +1,5 @@
1
1
  import type { RjFilterModel, RjSortState } from './types';
2
+ import type { RjLang } from './locale';
2
3
  export type RjNlqFilterType = 'text' | 'number' | 'date' | 'select';
3
4
  /** 参与解析的列描述(由网格从列定义构建) */
4
5
  export interface RjNlqColumn {
@@ -39,13 +40,20 @@ export interface RjNlqResult {
39
40
  model: RjFilterModel;
40
41
  }[];
41
42
  sort?: RjSortState;
43
+ /** 排序列展示名(title,供 explainNLQ 本地化回显) */
44
+ sortTitle?: string;
42
45
  groupBy?: string;
43
46
  groupColId?: string;
47
+ /** 分组列展示名(title,供 explainNLQ 本地化回显) */
48
+ groupTitle?: string;
44
49
  limit?: number;
45
50
  search?: string;
46
51
  clauses: RjNlqClause[];
47
52
  }
48
53
  /** 解析自然语言查询 */
49
54
  export declare function parseNLQ(text: string, columns: RjNlqColumn[]): RjNlqResult;
50
- /** 生成人类可读的解析摘要(供 UI 回显,key 交由上层 i18n;此处给结构串) */
51
- export declare function explainNLQ(r: RjNlqResult): string;
55
+ /**
56
+ * 生成人类可读的解析摘要(供 UI 回显)。传 lang 则随语言本地化:
57
+ * 中文如「数量 > 100 · 按金额 降序 · 前5 · 搜索“轴承”」,英文如「qty > 100 · sort by amount desc · top 5 · search "bolt"」。
58
+ */
59
+ export declare function explainNLQ(r: RjNlqResult, lang?: RjLang): string;
@@ -630,6 +630,8 @@ export interface RjGridApi extends ComponentPublicInstance {
630
630
  autoSizeAll: () => void;
631
631
  /** 列宽:按视口宽度等比缩放普通区列(对标 sizeColumnsToFit) */
632
632
  sizeColumnsToFit: () => void;
633
+ /** 清除全部列宽覆盖,还原到列定义/内容自适应默认宽(与 autoSizeAll 配合做自适应↔还原切换) */
634
+ resetColumnWidths: () => void;
633
635
  /** 主题引擎(对标 AG Grid Theming):运行时替换预设/明暗/强调色等 */
634
636
  setTheme: (params: RjThemeParams) => void;
635
637
  /** 读取当前生效的主题参数 */
@@ -60,6 +60,7 @@ export declare function useColumnState(sourceColumns: () => RjColumn[]): {
60
60
  setAutoWidth: (colId: string, width: number) => void;
61
61
  hasSizedWidth: (colId: string) => boolean;
62
62
  sizeToFit: () => void;
63
+ clearWidths: () => void;
63
64
  moveColumn: (dragId: string, targetId: string) => void;
64
65
  togglePin: (colId: string, pin: 'left' | 'right' | null) => void;
65
66
  toggleHide: (colId: string, hide?: boolean) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rj-grid",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "零第三方依赖的自研 Vue3 虚拟表格:虚拟滚动 / 冻结列 / 行拖拽 / 分组透视 / 公式 / SSRM / 主题 i18n / 自然语言查询",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,7 @@
46
46
  }
47
47
  },
48
48
  "scripts": {
49
+ "start": "vite --config vite.playground.config.ts",
49
50
  "dev": "vite --config vite.playground.config.ts",
50
51
  "build:play": "vite build --config vite.playground.config.ts",
51
52
  "preview:play": "vite preview --config vite.playground.config.ts",
@@ -55,6 +56,8 @@
55
56
  "lint": "eslint --fix --ext .ts,.vue index.ts src __tests__ playground && stylelint \"src/styles/*.scss\"",
56
57
  "ts": "node --max_old_space_size=8192 ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit -p tsconfig.json",
57
58
  "size": "node ./node_modules/vite/bin/vite.js build --config vite.size.config.ts && node build/rjSizeReport.mjs",
59
+ "publish:github": "node scripts/publish-github.mjs",
60
+ "publish:github:push": "node scripts/publish-github.mjs --push",
58
61
  "prepublishOnly": "pnpm build && pnpm test"
59
62
  },
60
63
  "keywords": [