jianghu-ui 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +376 -0
  2. package/dist/jianghu-ui.css +2318 -0
  3. package/dist/jianghu-ui.js +2 -0
  4. package/dist/jianghu-ui.js.LICENSE.txt +1 -0
  5. package/package.json +56 -0
  6. package/src/Design.stories.mdx +195 -0
  7. package/src/Introduction.stories.mdx +148 -0
  8. package/src/components/JhAddressSelect/JhAddressSelect.md +250 -0
  9. package/src/components/JhAddressSelect/JhAddressSelect.stories.js +282 -0
  10. package/src/components/JhAddressSelect/JhAddressSelect.vue +261 -0
  11. package/src/components/JhCard/JhCard.md +246 -0
  12. package/src/components/JhCard/JhCard.stories.js +688 -0
  13. package/src/components/JhCard/JhCard.vue +604 -0
  14. package/src/components/JhCheckCard/JhCheckCard.md +245 -0
  15. package/src/components/JhCheckCard/JhCheckCard.stories.js +750 -0
  16. package/src/components/JhCheckCard/JhCheckCard.vue +476 -0
  17. package/src/components/JhConfirmDialog/JhConfirmDialog.md +70 -0
  18. package/src/components/JhConfirmDialog/JhConfirmDialog.stories.js +550 -0
  19. package/src/components/JhConfirmDialog/JhConfirmDialog.vue +181 -0
  20. package/src/components/JhDateRangePicker/JhDateRangePicker.md +56 -0
  21. package/src/components/JhDateRangePicker/JhDateRangePicker.stories.js +320 -0
  22. package/src/components/JhDateRangePicker/JhDateRangePicker.vue +307 -0
  23. package/src/components/JhDescriptions/JhDescriptions.md +724 -0
  24. package/src/components/JhDescriptions/JhDescriptions.stories.js +858 -0
  25. package/src/components/JhDescriptions/JhDescriptions.vue +933 -0
  26. package/src/components/JhDraggable/JhDraggable.md +66 -0
  27. package/src/components/JhDraggable/JhDraggable.stories.js +161 -0
  28. package/src/components/JhDraggable/JhDraggable.vue +254 -0
  29. package/src/components/JhDrawer/JhDrawer.md +68 -0
  30. package/src/components/JhDrawer/JhDrawer.stories.js +478 -0
  31. package/src/components/JhDrawer/JhDrawer.vue +281 -0
  32. package/src/components/JhDrawerForm/JhDrawerForm.md +69 -0
  33. package/src/components/JhDrawerForm/JhDrawerForm.stories.js +492 -0
  34. package/src/components/JhDrawerForm/JhDrawerForm.vue +297 -0
  35. package/src/components/JhEditableTable/JhEditableTable.md +507 -0
  36. package/src/components/JhEditableTable/JhEditableTable.stories.js +615 -0
  37. package/src/components/JhEditableTable/JhEditableTable.vue +685 -0
  38. package/src/components/JhFileInput/JhFileInput.md +56 -0
  39. package/src/components/JhFileInput/JhFileInput.stories.js +103 -0
  40. package/src/components/JhFileInput/JhFileInput.vue +253 -0
  41. package/src/components/JhForm/JhForm.md +676 -0
  42. package/src/components/JhForm/JhForm.stories.js +1375 -0
  43. package/src/components/JhForm/JhForm.vue +657 -0
  44. package/src/components/JhFormField/JhFormField.stories.js +217 -0
  45. package/src/components/JhFormField/JhFormField.vue +439 -0
  46. package/src/components/JhFormFields/JhFormFields.md +647 -0
  47. package/src/components/JhFormFields/JhFormFields.stories.js +922 -0
  48. package/src/components/JhFormFields/JhFormFields.vue +998 -0
  49. package/src/components/JhFormList/JhFormList.md +303 -0
  50. package/src/components/JhFormList/JhFormList.stories.js +661 -0
  51. package/src/components/JhFormList/JhFormList.vue +1127 -0
  52. package/src/components/JhJsonEditor/JhJsonEditor.md +54 -0
  53. package/src/components/JhJsonEditor/JhJsonEditor.stories.js +157 -0
  54. package/src/components/JhJsonEditor/JhJsonEditor.vue +178 -0
  55. package/src/components/JhLayout/JhLayout.md +580 -0
  56. package/src/components/JhLayout/JhLayout.stories.js +414 -0
  57. package/src/components/JhLayout/JhLayout.vue +387 -0
  58. package/src/components/JhList/JhList.md +441 -0
  59. package/src/components/JhList/JhList.stories.js +524 -0
  60. package/src/components/JhList/JhList.vue +571 -0
  61. package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +56 -0
  62. package/src/components/JhMarkdownEditor/JhMarkdownEditor.stories.js +191 -0
  63. package/src/components/JhMarkdownEditor/JhMarkdownEditor.vue +188 -0
  64. package/src/components/JhMask/JhMask.md +62 -0
  65. package/src/components/JhMask/JhMask.stories.js +270 -0
  66. package/src/components/JhMask/JhMask.vue +123 -0
  67. package/src/components/JhMenu/JhMenu.md +85 -0
  68. package/src/components/JhMenu/JhMenu.stories.js +384 -0
  69. package/src/components/JhMenu/JhMenu.vue +545 -0
  70. package/src/components/JhModal/JhModal.md +68 -0
  71. package/src/components/JhModal/JhModal.stories.js +562 -0
  72. package/src/components/JhModal/JhModal.vue +235 -0
  73. package/src/components/JhModalForm/JhModalForm.md +69 -0
  74. package/src/components/JhModalForm/JhModalForm.stories.js +592 -0
  75. package/src/components/JhModalForm/JhModalForm.vue +298 -0
  76. package/src/components/JhPageContainer/JhPageContainer.md +409 -0
  77. package/src/components/JhPageContainer/JhPageContainer.stories.js +209 -0
  78. package/src/components/JhPageContainer/JhPageContainer.vue +72 -0
  79. package/src/components/JhQueryFilter/JhQueryFilter.md +77 -0
  80. package/src/components/JhQueryFilter/JhQueryFilter.stories.js +684 -0
  81. package/src/components/JhQueryFilter/JhQueryFilter.vue +429 -0
  82. package/src/components/JhScene/JhScene.md +64 -0
  83. package/src/components/JhScene/JhScene.stories.js +317 -0
  84. package/src/components/JhScene/JhScene.vue +376 -0
  85. package/src/components/JhStatisticCard/JhStatisticCard.md +363 -0
  86. package/src/components/JhStatisticCard/JhStatisticCard.stories.js +847 -0
  87. package/src/components/JhStatisticCard/JhStatisticCard.vue +459 -0
  88. package/src/components/JhStepsForm/JhStepsForm.md +666 -0
  89. package/src/components/JhStepsForm/JhStepsForm.stories.js +1224 -0
  90. package/src/components/JhStepsForm/JhStepsForm.vue +749 -0
  91. package/src/components/JhTable/JhTable.md +730 -0
  92. package/src/components/JhTable/JhTable.stories.js +1444 -0
  93. package/src/components/JhTable/JhTable.vue +2298 -0
  94. package/src/components/JhTableAttachment/JhTableAttachment.md +70 -0
  95. package/src/components/JhTableAttachment/JhTableAttachment.stories.js +198 -0
  96. package/src/components/JhTableAttachment/JhTableAttachment.vue +264 -0
  97. package/src/components/JhToast/JhToast.md +67 -0
  98. package/src/components/JhToast/JhToast.stories.js +386 -0
  99. package/src/components/JhToast/JhToast.vue +239 -0
  100. package/src/components/JhTreeSelect/JhTreeSelect.md +82 -0
  101. package/src/components/JhTreeSelect/JhTreeSelect.stories.js +391 -0
  102. package/src/components/JhTreeSelect/JhTreeSelect.vue +727 -0
  103. package/src/components/JhWaterMark/JhWaterMark.md +190 -0
  104. package/src/components/JhWaterMark/JhWaterMark.stories.js +675 -0
  105. package/src/components/JhWaterMark/JhWaterMark.vue +351 -0
  106. package/src/components/README.md +52 -0
  107. package/src/index.js +135 -0
  108. package/src/style/globalCSSJHV4.css +348 -0
  109. package/src/style/globalCSSVuetifyV4.css +637 -0
  110. package/src/style/storybook.css +4 -0
  111. package/src/tailwind.css +3 -0
  112. package/src/utils/vuetify.js +31 -0
@@ -0,0 +1,1444 @@
1
+ import JhTable from './JhTable.vue';
2
+
3
+ // 示例数据
4
+ const sampleHeaders = [
5
+ { text: 'ID', value: 'id', width: 80, sortable: true },
6
+ { text: '用户名', value: 'username', copyable: true },
7
+ { text: '邮箱', value: 'email', ellipsis: true, copyable: true },
8
+ { text: '手机号', value: 'phone' },
9
+ { text: '状态', value: 'status' },
10
+ { text: '创建时间', value: 'createdAt', sortable: true },
11
+ { text: '操作', value: 'action', width: '200px', sortable: false },
12
+ ];
13
+
14
+ const sampleItems = [
15
+ { id: 1, username: '张三', email: 'zhangsan@example.com', phone: '13800138001', status: '启用', createdAt: '2024-01-01 10:20:30' },
16
+ { id: 2, username: '李四', email: 'lisi@example.com', phone: '13800138002', status: '启用', createdAt: '2024-01-02 11:30:40' },
17
+ { id: 3, username: '王五', email: 'wangwu@example.com', phone: '13800138003', status: '禁用', createdAt: '2024-01-03 12:40:50' },
18
+ { id: 4, username: '赵六', email: 'zhaoliu@example.com', phone: '13800138004', status: '启用', createdAt: '2024-01-04 13:50:00' },
19
+ { id: 5, username: '钱七', email: 'qianqi@example.com', phone: '13800138005', status: '启用', createdAt: '2024-01-05 14:00:10' },
20
+ { id: 6, username: '孙八', email: 'sunba@example.com', phone: '13800138006', status: '禁用', createdAt: '2024-01-06 15:10:20' },
21
+ { id: 7, username: '周九', email: 'zhoujiu@example.com', phone: '13800138007', status: '启用', createdAt: '2024-01-07 16:20:30' },
22
+ { id: 8, username: '吴十', email: 'wushi@example.com', phone: '13800138008', status: '启用', createdAt: '2024-01-08 17:30:40' },
23
+ ];
24
+
25
+ // 生成大量数据用于测试服务端分页
26
+ const generateMockData = (count = 100) => {
27
+ const statuses = ['启用', '禁用', '待审核'];
28
+ return Array.from({ length: count }, (_, i) => ({
29
+ id: i + 1,
30
+ username: `用户${i + 1}`,
31
+ email: `user${i + 1}@example.com`,
32
+ phone: `138${String(i).padStart(8, '0')}`,
33
+ status: statuses[i % 3],
34
+ createdAt: new Date(2024, 0, (i % 28) + 1).toLocaleString('zh-CN'),
35
+ }));
36
+ };
37
+
38
+ const allMockData = generateMockData(100);
39
+ const schemaMockData = Array.from({ length: 50 }, (_, index) => {
40
+ const statusPool = ['pending', 'approved', 'rejected'];
41
+ return {
42
+ id: 1000 + index,
43
+ username: `审批单-${index + 1}`,
44
+ status: statusPool[index % statusPool.length],
45
+ amount: 120000 + index * 413,
46
+ progress: (index * 7) % 100,
47
+ updatedAt: new Date(2024, 0, (index % 28) + 1, 12, index % 60).toISOString(),
48
+ payload: {
49
+ region: index % 2 === 0 ? '华北' : '华南',
50
+ owner: `owner-${index + 1}`,
51
+ },
52
+ };
53
+ });
54
+
55
+ export default {
56
+ title: '数据展示/JhTable - 高级表格',
57
+ component: JhTable,
58
+ tags: ['autodocs'],
59
+ argTypes: {
60
+ headers: {
61
+ control: 'object',
62
+ description: '表格表头配置,支持 copyable、ellipsis 等增强功能',
63
+ },
64
+ items: {
65
+ control: 'object',
66
+ description: '表格数据(客户端分页)',
67
+ },
68
+ request: {
69
+ control: false,
70
+ description: '数据请求函数(服务端分页),返回 { data, total, success }',
71
+ },
72
+ headerTitle: {
73
+ control: 'text',
74
+ description: '表格标题',
75
+ },
76
+ tooltip: {
77
+ control: 'text',
78
+ description: '标题提示信息',
79
+ },
80
+ cardBordered: {
81
+ control: 'boolean',
82
+ description: '是否显示卡片边框',
83
+ },
84
+ ghost: {
85
+ control: 'boolean',
86
+ description: '幽灵模式(无边框无背景)',
87
+ },
88
+ toolbar: {
89
+ control: 'object',
90
+ description: '工具栏配置 { search, refresh, setting, density, fullscreen }',
91
+ },
92
+ actionColumn: {
93
+ control: 'object',
94
+ description: '操作列配置 { title, width, fixed, buttons }',
95
+ },
96
+ loading: {
97
+ control: 'boolean',
98
+ description: '加载状态',
99
+ },
100
+ showSelect: {
101
+ control: 'boolean',
102
+ description: '是否显示选择框',
103
+ },
104
+ size: {
105
+ control: 'select',
106
+ options: ['default', 'medium', 'compact'],
107
+ description: '表格密度',
108
+ },
109
+ polling: {
110
+ control: 'number',
111
+ description: '轮询间隔(毫秒),0 表示不轮询',
112
+ },
113
+ debounceTime: {
114
+ control: 'number',
115
+ description: '搜索防抖时间(毫秒)',
116
+ },
117
+ dataTableProps: {
118
+ control: 'object',
119
+ description: '透传给 v-data-table 的原生属性 (例如 height、disable-pagination)',
120
+ },
121
+ sortBy: {
122
+ control: 'object',
123
+ description: '受控排序字段(字符串或数组)',
124
+ },
125
+ sortDesc: {
126
+ control: 'object',
127
+ description: '受控排序方向(布尔或数组)',
128
+ },
129
+ multiSort: {
130
+ control: 'boolean',
131
+ description: '是否允许多列排序',
132
+ },
133
+ mustSort: {
134
+ control: 'boolean',
135
+ description: '是否必须始终存在排序字段',
136
+ },
137
+ columnsState: {
138
+ control: 'object',
139
+ description: '列状态配置 { persistenceKey, defaultVisible, value }',
140
+ },
141
+ },
142
+ parameters: {
143
+ docs: {
144
+ description: {
145
+ component: `
146
+
147
+ 增强版数据表格组件,参考 Ant Design Pro 的 ProTable 设计,提供丰富的功能。
148
+
149
+ ## 新增功能特性(Pro 版)
150
+
151
+ ### 1. 工具栏功能
152
+ - **搜索框**:全局快速搜索
153
+ - **刷新按钮**:重新加载数据
154
+ - **列设置**:显示/隐藏列,实时生效
155
+ - **密度切换**:默认/中等/紧凑三种密度
156
+ - **全屏切换**:全屏显示表格(可选)
157
+
158
+ ### 2. 列增强功能
159
+ - **copyable**:显示复制按钮,一键复制单元格内容
160
+ - **ellipsis**:超长文本自动省略,鼠标悬停显示完整内容
161
+ - **tooltip**:鼠标悬停提示
162
+ - **自定义插槽**:完全自定义列渲染
163
+
164
+ ### 3. 操作列配置化
165
+ \`\`\`javascript
166
+ actionColumn: {
167
+ title: '操作',
168
+ width: 180,
169
+ fixed: 'right',
170
+ buttons: [
171
+ {
172
+ text: '编辑',
173
+ type: 'link', // link / icon / button
174
+ icon: 'mdi-pencil',
175
+ color: 'primary',
176
+ tooltip: '编辑记录',
177
+ onClick: (row) => { console.log('编辑', row) },
178
+ visible: (row) => row.status !== '禁用',
179
+ confirm: '确认编辑?'
180
+ },
181
+ {
182
+ text: '删除',
183
+ type: 'link',
184
+ icon: 'mdi-delete',
185
+ color: 'error',
186
+ onClick: (row) => { console.log('删除', row) },
187
+ confirm: '确认删除?'
188
+ }
189
+ ]
190
+ }
191
+ \`\`\`
192
+
193
+ ### 4. 服务端分页
194
+ 通过 \`request\` prop 支持服务端分页:
195
+ \`\`\`javascript
196
+ async fetchData(params) {
197
+ // params: { page, pageSize, search, sorter, filters }
198
+ const response = await fetch('/api/users', {
199
+ method: 'POST',
200
+ body: JSON.stringify(params),
201
+ });
202
+ return {
203
+ data: response.list,
204
+ total: response.total,
205
+ success: true,
206
+ };
207
+ }
208
+ \`\`\`
209
+
210
+ ### 5. 行选择增强
211
+ - 支持单选/多选
212
+ - \`selection-change\` 事件返回 \`{ selectedRowKeys, selectedRows }\`
213
+ - 提供 \`getSelectedRows()\` 和 \`clearSelection()\` 方法
214
+
215
+ ### 6. 响应式密度
216
+ 三种密度自动适配:
217
+ - **default**: 48px 行高
218
+ - **medium**: 40px 行高
219
+ - **compact**: 32px 行高
220
+
221
+ ## 方法
222
+
223
+ - \`reload()\`: 重新加载数据(服务端分页)
224
+ - \`reset()\`: 重置到第一页
225
+ - \`clearSelection()\`: 清空选择
226
+ - \`getSelectedRows()\`: 获取选中的行
227
+
228
+ ## 事件
229
+
230
+ - \`create-click\`: 点击新增按钮
231
+ - \`update-click\`: 点击详情按钮(item)
232
+ - \`delete-click\`: 点击删除按钮(item)
233
+ - \`row-click\`: 点击行(item, event)
234
+ - \`selection-change\`: 选择改变 ({ selectedRowKeys, selectedRows })
235
+ - \`refresh\`: 点击刷新按钮
236
+ - \`copy-success\`: 复制成功(text)
237
+ - \`request-error\`: 请求失败(error)
238
+ `,
239
+ },
240
+ },
241
+ },
242
+ };
243
+
244
+
245
+ // 基础示例
246
+ export const 基础示例 = {
247
+ args: {
248
+ headers: sampleHeaders,
249
+ items: sampleItems,
250
+ loading: false,
251
+ showSearch: true,
252
+ showCreateButton: true,
253
+ showUpdateAction: true,
254
+ showDeleteAction: true,
255
+ showSelect: false,
256
+ size: 'default',
257
+ },
258
+ render: (args) => ({
259
+ components: { JhTable },
260
+ data() {
261
+ return { args };
262
+ },
263
+ template: `
264
+ <div>
265
+ <jh-table
266
+ v-bind="args"
267
+ @create-click="handleCreate"
268
+ @update-click="handleUpdate"
269
+ @delete-click="handleDelete"
270
+ @row-click="handleRowClick"
271
+ />
272
+ </div>
273
+ `,
274
+ methods: {
275
+ handleCreate() {
276
+ alert('点击了新增按钮');
277
+ },
278
+ handleUpdate(item) {
279
+ alert('详情:' + item.username);
280
+ },
281
+ handleDelete(item) {
282
+ if (confirm('确认删除 ' + item.username + '?')) {
283
+ console.log('删除', item);
284
+ }
285
+ },
286
+ handleRowClick(item) {
287
+ console.log('行点击:', item);
288
+ },
289
+ },
290
+ }),
291
+ };
292
+
293
+ export const 值类型示例 = {
294
+ render: () => ({
295
+ components: { JhTable },
296
+ data() {
297
+ return {
298
+ headers: [
299
+ { text: '序号', value: 'index', valueType: 'index', width: 80 },
300
+ {
301
+ text: '状态',
302
+ value: 'status',
303
+ valueType: 'status',
304
+ valueEnumStatusMap: {
305
+ warning: { color: 'orange darken-2' },
306
+ error: { color: 'red darken-1' },
307
+ },
308
+ valueEnum: {
309
+ pending: { text: '待审核', status: 'warning', value: 'PENDING', icon: 'mdi-clock-outline' },
310
+ approved: { text: '已通过', status: 'success', value: 'APPROVED' },
311
+ rejected: { text: '已拒绝', status: 'error', value: 'REJECTED' },
312
+ },
313
+ search: {
314
+ valueType: 'select',
315
+ initialValue: 'PENDING',
316
+ valueEnumKey: 'value',
317
+ },
318
+ },
319
+ {
320
+ text: '账单金额',
321
+ value: 'amount',
322
+ valueType: 'money',
323
+ copyable: true,
324
+ valueProps: { currencySymbol: '¥', precision: 2 },
325
+ valueFormatter: (value) => `${(value / 10000).toFixed(2)} 万`,
326
+ search: { valueType: 'digit', placeholder: '请输入最小金额' },
327
+ },
328
+ {
329
+ text: '完成进度',
330
+ value: 'progress',
331
+ valueType: 'progress',
332
+ valueProps: { color: 'success', height: 6, showValue: true },
333
+ },
334
+ {
335
+ text: '更新时间',
336
+ value: 'updatedAt',
337
+ valueType: 'dateTime',
338
+ search: {
339
+ valueType: 'dateRange',
340
+ transform: (value) => {
341
+ if (!value || value.length < 2) return {};
342
+ return {
343
+ startDate: value[0],
344
+ endDate: value[1],
345
+ };
346
+ },
347
+ },
348
+ },
349
+ {
350
+ text: '附加信息',
351
+ value: 'payload',
352
+ valueType: 'json',
353
+ ellipsis: true,
354
+ copyable: true,
355
+ },
356
+ { text: '操作', value: 'action', sortable: false, width: 160 },
357
+ ],
358
+ };
359
+ },
360
+ methods: {
361
+ async fetchSchemaTable(params) {
362
+ await new Promise(resolve => setTimeout(resolve, 400));
363
+ let list = [...schemaMockData];
364
+
365
+ if (params.filters?.status) {
366
+ const targetStatus = String(params.filters.status).toLowerCase();
367
+ list = list.filter(row => row.status === targetStatus);
368
+ }
369
+ if (params.filters?.startDate && params.filters?.endDate) {
370
+ const start = new Date(params.filters.startDate).getTime();
371
+ const end = new Date(params.filters.endDate).getTime();
372
+ list = list.filter(row => {
373
+ const time = new Date(row.updatedAt).getTime();
374
+ return time >= start && time <= end;
375
+ });
376
+ }
377
+ if (params.filters?.amount) {
378
+ list = list.filter(row => row.amount >= Number(params.filters.amount));
379
+ }
380
+
381
+ const total = list.length;
382
+ const startIndex = (params.page - 1) * params.pageSize;
383
+ const endIndex = startIndex + params.pageSize;
384
+
385
+ return {
386
+ data: list.slice(startIndex, endIndex),
387
+ total,
388
+ success: true,
389
+ };
390
+ },
391
+ },
392
+ template: `
393
+ <jh-table
394
+ header-title="列 Schema 渲染 & 自动筛选"
395
+ tooltip="valueType / valueEnum / search transform"
396
+ :headers="headers"
397
+ :request="fetchSchemaTable"
398
+ show-filter
399
+ :toolbar="{ search: true, refresh: true, setting: true, density: true }"
400
+ :pagination="{ current: 1, pageSize: 10 }"
401
+ />
402
+ `,
403
+ }),
404
+ };
405
+
406
+ // 完整工具栏
407
+ export const 完整工具栏 = {
408
+ args: {
409
+ ...基础示例.args,
410
+ toolbar: {
411
+ search: true,
412
+ refresh: true,
413
+ setting: true,
414
+ density: true,
415
+ fullscreen: true,
416
+ },
417
+ },
418
+ render: 基础示例.render,
419
+ };
420
+
421
+ // 列增强功能
422
+ export const 列增强功能 = {
423
+ args: {
424
+ ...基础示例.args,
425
+ headers: [
426
+ { text: 'ID', value: 'id', width: 80 },
427
+ { text: '用户名', value: 'username', copyable: true },
428
+ { text: '邮箱', value: 'email', ellipsis: true, copyable: true },
429
+ {
430
+ text: '描述',
431
+ value: 'description',
432
+ ellipsis: true,
433
+ },
434
+ { text: '操作', value: 'action', width: '150px', sortable: false },
435
+ ],
436
+ items: [
437
+ {
438
+ id: 1,
439
+ username: '张三',
440
+ email: 'zhangsan@verylongemailaddress.example.com',
441
+ description: '这是一段很长很长很长很长很长很长很长很长的描述文字,用于测试省略号功能是否正常工作'
442
+ },
443
+ {
444
+ id: 2,
445
+ username: '李四',
446
+ email: 'lisi@example.com',
447
+ description: '普通描述'
448
+ },
449
+ ],
450
+ },
451
+ render: (args) => ({
452
+ components: { JhTable },
453
+ data() {
454
+ return { args };
455
+ },
456
+ template: `
457
+ <div>
458
+ <jh-table
459
+ v-bind="args"
460
+ @copy-success="handleCopySuccess"
461
+ />
462
+ </div>
463
+ `,
464
+ methods: {
465
+ handleCopySuccess(text) {
466
+ console.log('已复制:', text);
467
+ alert('已复制到剪贴板: ' + text);
468
+ },
469
+ },
470
+ }),
471
+ };
472
+
473
+ // 配置化操作列
474
+ export const 配置化操作列 = {
475
+ args: {
476
+ ...基础示例.args,
477
+ actionColumn: {
478
+ title: '操作',
479
+ width: 200,
480
+ fixed: 'right',
481
+ buttons: [
482
+ {
483
+ text: '查看',
484
+ type: 'link',
485
+ icon: 'mdi-eye',
486
+ color: 'primary',
487
+ tooltip: '查看详情',
488
+ onClick: (row) => {
489
+ alert('查看: ' + row.username);
490
+ },
491
+ },
492
+ {
493
+ text: '编辑',
494
+ type: 'link',
495
+ icon: 'mdi-pencil',
496
+ color: 'success',
497
+ tooltip: '编辑记录',
498
+ onClick: (row) => {
499
+ alert('编辑: ' + row.username);
500
+ },
501
+ visible: (row) => row.status !== '禁用',
502
+ },
503
+ {
504
+ text: '删除',
505
+ type: 'link',
506
+ icon: 'mdi-delete',
507
+ color: 'error',
508
+ tooltip: '删除记录',
509
+ confirm: '确认删除该记录?',
510
+ onClick: (row) => {
511
+ console.log('删除:', row);
512
+ alert('已删除: ' + row.username);
513
+ },
514
+ },
515
+ ],
516
+ },
517
+ },
518
+ render: 基础示例.render,
519
+ };
520
+
521
+ // 服务端分页
522
+ export const 服务端分页 = {
523
+ args: {
524
+ headers: sampleHeaders,
525
+ items: [], // 由 request 函数加载
526
+ itemsPerPage: 10,
527
+ pagination: {
528
+ current: 1,
529
+ pageSize: 10,
530
+ },
531
+ },
532
+ render: (args) => ({
533
+ components: { JhTable },
534
+ data() {
535
+ return {
536
+ args: {
537
+ ...args,
538
+ request: this.fetchData,
539
+ },
540
+ };
541
+ },
542
+ template: `
543
+ <div>
544
+ <div class="mb-4 pa-4 blue lighten-5 rounded">
545
+ <strong>服务端分页示例</strong>
546
+ <p class="mb-0 mt-2">模拟异步加载数据,支持分页、搜索。打开控制台查看请求参数。</p>
547
+ </div>
548
+ <jh-table
549
+ v-bind="args"
550
+ @refresh="handleRefresh"
551
+ />
552
+ </div>
553
+ `,
554
+ methods: {
555
+ async fetchData(params) {
556
+ console.log('请求参数:', params);
557
+
558
+ // 模拟网络延迟
559
+ await new Promise(resolve => setTimeout(resolve, 500));
560
+
561
+ // 计算分页
562
+ const { page, pageSize, search } = params;
563
+ let filteredData = [...allMockData];
564
+
565
+ // 搜索过滤
566
+ if (search) {
567
+ filteredData = filteredData.filter(item =>
568
+ Object.values(item).some(val =>
569
+ String(val).toLowerCase().includes(search.toLowerCase())
570
+ )
571
+ );
572
+ }
573
+
574
+ const sorter = params.sorter || {};
575
+ if (Array.isArray(sorter.sortBy) && sorter.sortBy.length > 0) {
576
+ filteredData.sort((a, b) => {
577
+ for (let i = 0; i < sorter.sortBy.length; i += 1) {
578
+ const key = sorter.sortBy[i];
579
+ const desc = sorter.sortDesc?.[i];
580
+ if (a[key] === b[key]) continue;
581
+ if (a[key] > b[key]) return desc ? -1 : 1;
582
+ if (a[key] < b[key]) return desc ? 1 : -1;
583
+ }
584
+ return 0;
585
+ });
586
+ }
587
+
588
+ const total = filteredData.length;
589
+ const start = (page - 1) * pageSize;
590
+ const end = start + pageSize;
591
+ const data = filteredData.slice(start, end);
592
+
593
+ return {
594
+ data,
595
+ total,
596
+ success: true,
597
+ };
598
+ },
599
+ handleRefresh() {
600
+ console.log('刷新表格');
601
+ },
602
+ },
603
+ }),
604
+ };
605
+
606
+ // 原生属性透传 + 列状态持久化
607
+ export const 增强特性 = {
608
+ render: () => ({
609
+ components: { JhTable },
610
+ data() {
611
+ return {
612
+ headers: sampleHeaders,
613
+ columnsState: null,
614
+ toolbar: {
615
+ search: true,
616
+ refresh: true,
617
+ setting: true,
618
+ density: true,
619
+ fullscreen: true,
620
+ },
621
+ filterFields: [
622
+ { key: 'username', label: '用户名', type: 'text' },
623
+ {
624
+ key: 'status',
625
+ label: '状态',
626
+ type: 'select',
627
+ options: [
628
+ { label: '启用', value: '启用' },
629
+ { label: '禁用', value: '禁用' },
630
+ { label: '待审核', value: '待审核' },
631
+ ],
632
+ },
633
+ ],
634
+ };
635
+ },
636
+ template: `
637
+ <div>
638
+ <div class="mb-4 pa-4 purple lighten-5 rounded">
639
+ <strong>列状态持久化 + 多列排序 + v-data-table 属性透传</strong>
640
+ <p class="mb-0 mt-2">隐藏列 / 切换排序 / 设置高度,刷新页面后仍旧保持。</p>
641
+ </div>
642
+ <jh-table
643
+ ref="advancedTable"
644
+ :headers="headers"
645
+ :request="fetchData"
646
+ :toolbar="toolbar"
647
+ :columns-state="{
648
+ persistenceKey: 'storybook-jh-table-columns',
649
+ value: columnsState
650
+ }"
651
+ :show-filter="true"
652
+ :filter-fields="filterFields"
653
+ multi-sort
654
+ must-sort
655
+ :sort-by="['createdAt']"
656
+ :sort-desc="[true]"
657
+ hide-default-footer
658
+ :data-table-props="{ height: '420px', fixedHeader: true }"
659
+ @columns-state-change="handleColumnStateChange"
660
+ @sort-change="handleSortChange"
661
+ @filter-search="handleFilterChange"
662
+ >
663
+ <template #toolbar-extra>
664
+ <v-btn small outlined color="primary" @click="triggerManualReload">
665
+ <v-icon left x-small>mdi-refresh</v-icon>
666
+ 重新拉取
667
+ </v-btn>
668
+ </template>
669
+ </jh-table>
670
+ </div>
671
+ `,
672
+ methods: {
673
+ async fetchData(params) {
674
+ console.log('增强特性请求参数:', params);
675
+ await new Promise(resolve => setTimeout(resolve, 400));
676
+ let filtered = [...allMockData];
677
+
678
+ if (params.search) {
679
+ filtered = filtered.filter(item =>
680
+ Object.values(item).some(val =>
681
+ String(val).toLowerCase().includes(params.search.toLowerCase())
682
+ )
683
+ );
684
+ }
685
+
686
+ if (params.filters && params.filters.status) {
687
+ filtered = filtered.filter(item => item.status === params.filters.status);
688
+ }
689
+
690
+ const sorter = params.sorter || {};
691
+ if (Array.isArray(sorter.sortBy) && sorter.sortBy.length > 0) {
692
+ filtered.sort((a, b) => {
693
+ for (let i = 0; i < sorter.sortBy.length; i += 1) {
694
+ const key = sorter.sortBy[i];
695
+ const desc = sorter.sortDesc?.[i];
696
+ if (a[key] === b[key]) continue;
697
+ if (a[key] > b[key]) return desc ? -1 : 1;
698
+ if (a[key] < b[key]) return desc ? 1 : -1;
699
+ }
700
+ return 0;
701
+ });
702
+ }
703
+
704
+ const page = params.page || 1;
705
+ const pageSize = params.pageSize || 10;
706
+ const total = filtered.length;
707
+ const start = (page - 1) * pageSize;
708
+ const end = start + pageSize;
709
+
710
+ return {
711
+ data: filtered.slice(start, end),
712
+ total,
713
+ success: true,
714
+ };
715
+ },
716
+ handleColumnStateChange(val) {
717
+ this.columnsState = val;
718
+ },
719
+ handleSortChange(payload) {
720
+ console.log('排序变化:', payload);
721
+ },
722
+ handleFilterChange(filters) {
723
+ console.log('筛选条件:', filters);
724
+ },
725
+ triggerManualReload() {
726
+ this.$refs.advancedTable?.reload?.();
727
+ },
728
+ },
729
+ }),
730
+ };
731
+
732
+ // 行选择
733
+ export const 行选择 = {
734
+ args: {
735
+ ...基础示例.args,
736
+ rowSelection: {
737
+ type: 'checkbox',
738
+ defaultSelectedRowKeys: [1],
739
+ },
740
+ },
741
+ render: (args) => ({
742
+ components: { JhTable },
743
+ data() {
744
+ return {
745
+ args: {
746
+ ...args,
747
+ rowSelection: {
748
+ ...(args.rowSelection || {}),
749
+ },
750
+ },
751
+ selectedRows: [],
752
+ };
753
+ },
754
+ created() {
755
+ if (this.args.rowSelection) {
756
+ this.args.rowSelection.onChange = this.handleRowSelectionOnChange;
757
+ }
758
+ },
759
+ template: `
760
+ <div>
761
+ <div class="mb-4 pa-4 green lighten-5 rounded">
762
+ <strong>已选择 {{ selectedRows.length }} 行</strong>
763
+ <div v-if="selectedRows.length > 0" class="mt-2">
764
+ <v-chip
765
+ v-for="row in selectedRows"
766
+ :key="row.id"
767
+ small
768
+ class="mr-2"
769
+ >
770
+ {{ row.username }}
771
+ </v-chip>
772
+ </div>
773
+ </div>
774
+ <jh-table
775
+ ref="tableRef"
776
+ v-bind="args"
777
+ @selection-change="handleSelectionChange"
778
+ />
779
+ <div class="mt-4">
780
+ <v-btn small @click="clearSelection">清空选择</v-btn>
781
+ <v-btn small class="ml-2" @click="getSelected">获取选中</v-btn>
782
+ </div>
783
+ </div>
784
+ `,
785
+ methods: {
786
+ handleSelectionChange({ selectedRows }) {
787
+ this.selectedRows = selectedRows;
788
+ console.log('选中的行:', selectedRows);
789
+ },
790
+ handleRowSelectionOnChange(keys, rows) {
791
+ console.log('rowSelection.onChange:', keys, rows);
792
+ },
793
+ clearSelection() {
794
+ this.$refs.tableRef.clearSelection();
795
+ },
796
+ getSelected() {
797
+ const rows = this.$refs.tableRef.getSelectedRows();
798
+ alert('选中 ' + rows.length + ' 行');
799
+ },
800
+ },
801
+ }),
802
+ };
803
+
804
+ // 密度切换
805
+ export const 密度切换 = {
806
+ render: () => ({
807
+ components: { JhTable },
808
+ data() {
809
+ return {
810
+ headers: sampleHeaders,
811
+ items: sampleItems.slice(0, 3),
812
+ };
813
+ },
814
+ template: `
815
+ <div>
816
+ <h3 class="mb-4">默认密度 (default)</h3>
817
+ <jh-table
818
+ :headers="headers"
819
+ :items="items"
820
+ size="default"
821
+ :toolbar="false"
822
+ class="mb-8"
823
+ />
824
+
825
+ <h3 class="mb-4">中等密度 (medium)</h3>
826
+ <jh-table
827
+ :headers="headers"
828
+ :items="items"
829
+ size="medium"
830
+ :toolbar="false"
831
+ class="mb-8"
832
+ />
833
+
834
+ <h3 class="mb-4">紧凑密度 (compact)</h3>
835
+ <jh-table
836
+ :headers="headers"
837
+ :items="items"
838
+ size="compact"
839
+ :toolbar="false"
840
+ />
841
+ </div>
842
+ `,
843
+ }),
844
+ };
845
+
846
+ // 自定义列渲染
847
+ export const 自定义列渲染 = {
848
+ args: {
849
+ ...基础示例.args,
850
+ },
851
+ render: (args) => ({
852
+ components: { JhTable },
853
+ data() {
854
+ return { args };
855
+ },
856
+ template: `
857
+ <div>
858
+ <jh-table v-bind="args">
859
+ <!-- 自定义状态列 -->
860
+ <template v-slot:item.status="{ item }">
861
+ <v-chip
862
+ :color="item.status === '启用' ? 'success' : 'error'"
863
+ small
864
+ label
865
+ >
866
+ <v-icon left x-small>
867
+ {{ item.status === '启用' ? 'mdi-check-circle' : 'mdi-close-circle' }}
868
+ </v-icon>
869
+ {{ item.status }}
870
+ </v-chip>
871
+ </template>
872
+ </jh-table>
873
+ </div>
874
+ `,
875
+ }),
876
+ };
877
+
878
+ // 加载状态
879
+ export const 加载状态 = {
880
+ args: {
881
+ ...基础示例.args,
882
+ loading: true,
883
+ },
884
+ render: 基础示例.render,
885
+ };
886
+
887
+ // 空数据
888
+ export const 空数据 = {
889
+ args: {
890
+ ...基础示例.args,
891
+ items: [],
892
+ },
893
+ render: 基础示例.render,
894
+ };
895
+
896
+ // 隐藏工具栏
897
+ export const 隐藏工具栏 = {
898
+ args: {
899
+ ...基础示例.args,
900
+ toolbar: false,
901
+ },
902
+ render: 基础示例.render,
903
+ };
904
+
905
+ // 简化工具栏
906
+ export const 简化工具栏 = {
907
+ args: {
908
+ ...基础示例.args,
909
+ toolbar: {
910
+ search: true,
911
+ refresh: true,
912
+ setting: false,
913
+ density: false,
914
+ fullscreen: false,
915
+ },
916
+ },
917
+ render: 基础示例.render,
918
+ };
919
+
920
+ // 集成高级筛选
921
+ export const 集成高级筛选 = {
922
+ args: {
923
+ headers: sampleHeaders,
924
+ items: [],
925
+ itemsPerPage: 10,
926
+ showFilter: true,
927
+ filterFields: [
928
+ {
929
+ key: 'keyword',
930
+ label: '关键词',
931
+ type: 'text',
932
+ placeholder: '请输入用户名/邮箱',
933
+ },
934
+ {
935
+ key: 'status',
936
+ label: '状态',
937
+ type: 'select',
938
+ placeholder: '请选择状态',
939
+ options: [
940
+ { text: '全部', value: '' },
941
+ { text: '启用', value: 'active' },
942
+ { text: '禁用', value: 'inactive' },
943
+ ],
944
+ },
945
+ {
946
+ key: 'department',
947
+ label: '部门',
948
+ type: 'select',
949
+ placeholder: '请选择部门',
950
+ options: [
951
+ { text: '全部', value: '' },
952
+ { text: '技术部', value: 'tech' },
953
+ { text: '产品部', value: 'product' },
954
+ { text: '设计部', value: 'design' },
955
+ ],
956
+ },
957
+ {
958
+ key: 'dateRange',
959
+ label: '创建时间',
960
+ type: 'daterange',
961
+ placeholder: '请选择日期范围',
962
+ },
963
+ ],
964
+ filterCollapsible: true,
965
+ filterDefaultCollapsed: true,
966
+ filterDefaultVisibleCount: 3,
967
+ },
968
+ render: (args) => ({
969
+ components: { JhTable },
970
+ data() {
971
+ return {
972
+ args: {
973
+ ...args,
974
+ request: this.fetchData,
975
+ },
976
+ currentFilters: {},
977
+ };
978
+ },
979
+ template: `
980
+ <div>
981
+ <div class="mb-4 pa-4 blue lighten-5 rounded">
982
+ <strong>高级筛选栏集成示例</strong>
983
+ <p class="mb-0 mt-2">展示了 JhTable 集成 JhQueryFilter 作为高级筛选栏的完整功能</p>
984
+ <div v-if="Object.keys(currentFilters).length > 0" class="mt-2">
985
+ <strong>当前筛选条件:</strong>
986
+ <pre class="mt-1">{{ JSON.stringify(currentFilters, null, 2) }}</pre>
987
+ </div>
988
+ </div>
989
+ <jh-table
990
+ v-bind="args"
991
+ @filter-search="handleFilterSearch"
992
+ @filter-reset="handleFilterReset"
993
+ />
994
+ </div>
995
+ `,
996
+ methods: {
997
+ async fetchData(params) {
998
+ console.log('请求参数:', params);
999
+
1000
+ // 模拟网络延迟
1001
+ await new Promise(resolve => setTimeout(resolve, 500));
1002
+
1003
+ // 计算分页
1004
+ const { page, pageSize, filters } = params;
1005
+ let filteredData = [...allMockData];
1006
+
1007
+ // 应用筛选条件
1008
+ if (filters && Object.keys(filters).length > 0) {
1009
+ if (filters.keyword) {
1010
+ filteredData = filteredData.filter(item =>
1011
+ item.username.includes(filters.keyword) ||
1012
+ item.email.includes(filters.keyword)
1013
+ );
1014
+ }
1015
+ if (filters.status) {
1016
+ const statusMap = {
1017
+ 'active': '启用',
1018
+ 'inactive': '禁用'
1019
+ };
1020
+ filteredData = filteredData.filter(item =>
1021
+ item.status === statusMap[filters.status]
1022
+ );
1023
+ }
1024
+ if (filters.department) {
1025
+ // 模拟部门筛选
1026
+ const deptMap = {
1027
+ 'tech': [1, 2, 5, 6],
1028
+ 'product': [3, 7],
1029
+ 'design': [4, 8]
1030
+ };
1031
+ if (deptMap[filters.department]) {
1032
+ filteredData = filteredData.filter(item =>
1033
+ deptMap[filters.department].includes(item.id)
1034
+ );
1035
+ }
1036
+ }
1037
+ if (filters.dateRange && Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
1038
+ // 模拟日期范围筛选
1039
+ filteredData = filteredData.filter(item => {
1040
+ const itemDate = new Date(item.createdAt);
1041
+ const startDate = new Date(filters.dateRange[0]);
1042
+ const endDate = new Date(filters.dateRange[1]);
1043
+ return itemDate >= startDate && itemDate <= endDate;
1044
+ });
1045
+ }
1046
+ }
1047
+
1048
+ const total = filteredData.length;
1049
+ const start = (page - 1) * pageSize;
1050
+ const end = start + pageSize;
1051
+ const data = filteredData.slice(start, end);
1052
+
1053
+ return {
1054
+ data,
1055
+ total,
1056
+ success: true,
1057
+ };
1058
+ },
1059
+ handleFilterSearch(filters) {
1060
+ this.currentFilters = filters;
1061
+ console.log('筛选查询:', filters);
1062
+ },
1063
+ handleFilterReset() {
1064
+ this.currentFilters = {};
1065
+ console.log('重置筛选');
1066
+ },
1067
+ },
1068
+ }),
1069
+ };
1070
+
1071
+ // 自定义工具栏
1072
+ export const 自定义工具栏 = {
1073
+ args: {
1074
+ ...基础示例.args,
1075
+ showCreateButton: false,
1076
+ },
1077
+ render: (args) => ({
1078
+ components: { JhTable },
1079
+ data() {
1080
+ return { args };
1081
+ },
1082
+ template: `
1083
+ <div>
1084
+ <jh-table v-bind="args">
1085
+ <template v-slot:toolbar-actions>
1086
+ <v-btn color="primary" small class="mr-2" @click="handleImport">
1087
+ <v-icon left small>mdi-upload</v-icon>
1088
+ 导入
1089
+ </v-btn>
1090
+ <v-btn color="success" small @click="handleExport">
1091
+ <v-icon left small>mdi-download</v-icon>
1092
+ 导出
1093
+ </v-btn>
1094
+ </template>
1095
+ </jh-table>
1096
+ </div>
1097
+ `,
1098
+ methods: {
1099
+ handleImport() {
1100
+ alert('导入数据');
1101
+ },
1102
+ handleExport() {
1103
+ alert('导出数据');
1104
+ },
1105
+ },
1106
+ }),
1107
+ };
1108
+
1109
+ // 完整CRUD示例
1110
+ export const 完整CRUD示例 = {
1111
+ render: () => ({
1112
+ components: { JhTable },
1113
+ data() {
1114
+ return {
1115
+ headers: [
1116
+ { text: 'ID', value: 'id', width: 80 },
1117
+ { text: '用户名', value: 'username', copyable: true },
1118
+ { text: '邮箱', value: 'email', ellipsis: true, copyable: true },
1119
+ { text: '手机号', value: 'phone' },
1120
+ { text: '状态', value: 'status' },
1121
+ { text: '创建时间', value: 'createdAt', sortable: true },
1122
+ { text: '操作', value: 'action', width: '200px', sortable: false },
1123
+ ],
1124
+ filterFields: [
1125
+ {
1126
+ key: 'keyword',
1127
+ label: '关键词',
1128
+ type: 'text',
1129
+ placeholder: '请输入用户名/邮箱/手机号',
1130
+ },
1131
+ {
1132
+ key: 'status',
1133
+ label: '状态',
1134
+ type: 'select',
1135
+ placeholder: '请选择状态',
1136
+ options: [
1137
+ { text: '全部', value: '' },
1138
+ { text: '启用', value: 'active' },
1139
+ { text: '禁用', value: 'inactive' },
1140
+ { text: '待审核', value: 'pending' },
1141
+ ],
1142
+ },
1143
+ {
1144
+ key: 'department',
1145
+ label: '部门',
1146
+ type: 'select',
1147
+ placeholder: '请选择部门',
1148
+ options: [
1149
+ { text: '全部', value: '' },
1150
+ { text: '技术部', value: 'tech' },
1151
+ { text: '产品部', value: 'product' },
1152
+ { text: '设计部', value: 'design' },
1153
+ { text: '运营部', value: 'operation' },
1154
+ ],
1155
+ },
1156
+ {
1157
+ key: 'role',
1158
+ label: '角色',
1159
+ type: 'select',
1160
+ placeholder: '请选择角色',
1161
+ options: [
1162
+ { text: '全部', value: '' },
1163
+ { text: '管理员', value: 'admin' },
1164
+ { text: '普通用户', value: 'user' },
1165
+ { text: '访客', value: 'guest' },
1166
+ ],
1167
+ },
1168
+ {
1169
+ key: 'dateRange',
1170
+ label: '创建时间',
1171
+ type: 'daterange',
1172
+ placeholder: '请选择日期范围',
1173
+ },
1174
+ ],
1175
+ items: [],
1176
+ loading: false,
1177
+ selectedRows: [],
1178
+ currentFilters: {},
1179
+ actionColumn: {
1180
+ buttons: [
1181
+ {
1182
+ text: '查看',
1183
+ type: 'link',
1184
+ icon: 'mdi-eye',
1185
+ color: 'primary',
1186
+ onClick: (row) => {
1187
+ alert('查看详情: ' + row.username);
1188
+ },
1189
+ },
1190
+ {
1191
+ text: '编辑',
1192
+ type: 'link',
1193
+ icon: 'mdi-pencil',
1194
+ color: 'success',
1195
+ onClick: (row) => {
1196
+ alert('编辑: ' + row.username);
1197
+ },
1198
+ visible: (row) => row.status !== '禁用',
1199
+ },
1200
+ {
1201
+ text: '删除',
1202
+ type: 'link',
1203
+ icon: 'mdi-delete',
1204
+ color: 'error',
1205
+ confirm: '确认删除该用户?',
1206
+ onClick: (row) => {
1207
+ this.handleDelete(row);
1208
+ },
1209
+ },
1210
+ ],
1211
+ },
1212
+ };
1213
+ },
1214
+ mounted() {
1215
+ this.loadData();
1216
+ },
1217
+ template: `
1218
+ <div>
1219
+ <!-- 筛选条件展示 -->
1220
+ <v-alert
1221
+ v-if="Object.keys(currentFilters).length > 0"
1222
+ type="info"
1223
+ dense
1224
+ text
1225
+ class="mb-4"
1226
+ >
1227
+ <div class="d-flex align-center">
1228
+ <span><strong>当前筛选:</strong> {{ formatFilters(currentFilters) }}</span>
1229
+ <v-spacer></v-spacer>
1230
+ <v-btn small text color="primary" @click="clearFilters">
1231
+ 清除筛选
1232
+ </v-btn>
1233
+ </div>
1234
+ </v-alert>
1235
+
1236
+ <!-- 批量操作栏 -->
1237
+ <v-alert
1238
+ v-if="selectedRows.length > 0"
1239
+ type="success"
1240
+ dense
1241
+ text
1242
+ class="mb-4"
1243
+ >
1244
+ <div class="d-flex align-center">
1245
+ <span>已选择 <strong>{{ selectedRows.length }}</strong> 项</span>
1246
+ <v-spacer></v-spacer>
1247
+ <v-btn small text color="error" @click="handleBatchDelete">
1248
+ 批量删除
1249
+ </v-btn>
1250
+ <v-btn small text @click="clearSelection">
1251
+ 取消选择
1252
+ </v-btn>
1253
+ </div>
1254
+ </v-alert>
1255
+
1256
+ <!-- 表格 -->
1257
+ <jh-table
1258
+ ref="tableRef"
1259
+ :headers="headers"
1260
+ :items="items"
1261
+ :loading="loading"
1262
+ :action-column="actionColumn"
1263
+ :show-select="true"
1264
+ :show-filter="true"
1265
+ :filter-fields="filterFields"
1266
+ :filter-collapsible="true"
1267
+ :filter-default-collapsed="true"
1268
+ :filter-default-visible-count="3"
1269
+ @create-click="handleCreate"
1270
+ @selection-change="handleSelectionChange"
1271
+ @refresh="loadData"
1272
+ @filter-search="handleFilterSearch"
1273
+ @filter-reset="handleFilterReset"
1274
+ >
1275
+ <!-- 自定义状态列 -->
1276
+ <template v-slot:item.status="{ item }">
1277
+ <v-chip
1278
+ :color="getStatusColor(item.status)"
1279
+ small
1280
+ label
1281
+ >
1282
+ {{ item.status }}
1283
+ </v-chip>
1284
+ </template>
1285
+ </jh-table>
1286
+ </div>
1287
+ `,
1288
+ methods: {
1289
+ async loadData() {
1290
+ this.loading = true;
1291
+ await new Promise(resolve => setTimeout(resolve, 500));
1292
+ this.items = sampleItems;
1293
+ this.loading = false;
1294
+ },
1295
+ handleCreate() {
1296
+ alert('打开新增表单');
1297
+ },
1298
+ handleDelete(row) {
1299
+ console.log('删除:', row);
1300
+ this.items = this.items.filter(item => item.id !== row.id);
1301
+ },
1302
+ handleBatchDelete() {
1303
+ if (confirm('确认删除选中的 ' + this.selectedRows.length + ' 项?')) {
1304
+ const ids = this.selectedRows.map(row => row.id);
1305
+ this.items = this.items.filter(item => !ids.includes(item.id));
1306
+ this.clearSelection();
1307
+ }
1308
+ },
1309
+ handleSelectionChange({ selectedRows }) {
1310
+ this.selectedRows = selectedRows;
1311
+ },
1312
+ clearSelection() {
1313
+ this.$refs.tableRef.clearSelection();
1314
+ },
1315
+ handleFilterSearch(filters) {
1316
+ this.currentFilters = filters;
1317
+ console.log('筛选查询:', filters);
1318
+ // 这里应该调用 API 进行筛选查询
1319
+ // this.loadData();
1320
+ },
1321
+ handleFilterReset() {
1322
+ this.currentFilters = {};
1323
+ console.log('重置筛选');
1324
+ this.loadData();
1325
+ },
1326
+ clearFilters() {
1327
+ this.currentFilters = {};
1328
+ this.$refs.tableRef.$refs.queryFilterRef?.handleReset();
1329
+ },
1330
+ formatFilters(filters) {
1331
+ const parts = [];
1332
+ if (filters.keyword) parts.push('关键词: ' + filters.keyword);
1333
+ if (filters.status) {
1334
+ const statusMap = { 'active': '启用', 'inactive': '禁用', 'pending': '待审核' };
1335
+ parts.push('状态: ' + statusMap[filters.status]);
1336
+ }
1337
+ if (filters.department) {
1338
+ const deptMap = { 'tech': '技术部', 'product': '产品部', 'design': '设计部', 'operation': '运营部' };
1339
+ parts.push('部门: ' + deptMap[filters.department]);
1340
+ }
1341
+ if (filters.role) {
1342
+ const roleMap = { 'admin': '管理员', 'user': '普通用户', 'guest': '访客' };
1343
+ parts.push('角色: ' + roleMap[filters.role]);
1344
+ }
1345
+ if (filters.dateRange && Array.isArray(filters.dateRange)) {
1346
+ parts.push('创建时间: ' + filters.dateRange.join(' ~ '));
1347
+ }
1348
+ return parts.join(', ');
1349
+ },
1350
+ getStatusColor(status) {
1351
+ const colorMap = {
1352
+ '启用': 'success',
1353
+ '禁用': 'error',
1354
+ '待审核': 'warning'
1355
+ };
1356
+ return colorMap[status] || 'default';
1357
+ },
1358
+ },
1359
+ }),
1360
+ };
1361
+
1362
+
1363
+ export const SelectionAlertRender = {
1364
+ render: () => ({
1365
+ components: { JhTable },
1366
+ data() {
1367
+ return {
1368
+ headers: [
1369
+ { text: '????', value: 'name' },
1370
+ { text: '????', value: 'containers' },
1371
+ { text: '????', value: 'calls' },
1372
+ { text: '???', value: 'owner' },
1373
+ { text: '????', value: 'createdAt' }
1374
+ ],
1375
+ items: [
1376
+ { id: 1, name: 'AppName-0', containers: 8, calls: 1276, owner: '???', createdAt: '2025-11-23' },
1377
+ { id: 2, name: 'AppName-1', containers: 12, calls: 938, owner: '???', createdAt: '2025-11-23' },
1378
+ { id: 3, name: 'AppName-2', containers: 1, calls: 820, owner: '???', createdAt: '2025-11-23' },
1379
+ { id: 4, name: 'AppName-3', containers: 5, calls: 1291, owner: '???', createdAt: '2025-11-23' },
1380
+ { id: 5, name: 'AppName-4', containers: 17, calls: 1796, owner: '???', createdAt: '2025-11-23' }
1381
+ ]
1382
+ };
1383
+ },
1384
+ methods: {
1385
+ renderAlert(h, { selectedRows, onCleanSelected }) {
1386
+ const totalContainers = selectedRows.reduce((sum, row) => sum + row.containers, 0);
1387
+ const totalCalls = selectedRows.reduce((sum, row) => sum + row.calls, 0);
1388
+ return h(
1389
+ 'div',
1390
+ { class: 'd-flex align-center flex-wrap', style: { gap: '12px' } },
1391
+ [
1392
+ h('span', { class: 'font-weight-medium' }, `?? ${selectedRows.length} ?`),
1393
+ h(
1394
+ 'v-btn',
1395
+ {
1396
+ props: { text: true, xSmall: true, color: 'primary' },
1397
+ on: { click: onCleanSelected }
1398
+ },
1399
+ '????'
1400
+ ),
1401
+ h('span', { class: 'grey--text text--darken-1' }, `?????${totalContainers} ?`),
1402
+ h('span', { class: 'grey--text text--darken-1' }, `????${totalCalls} ?`)
1403
+ ]
1404
+ );
1405
+ },
1406
+ renderAlertActions(h, { selectedRows }) {
1407
+ return h('div', { class: 'd-flex align-center', style: { gap: '8px' } }, [
1408
+ h(
1409
+ 'v-btn',
1410
+ {
1411
+ props: { small: true, color: 'error' },
1412
+ on: { click: () => this.handleBatchDelete(selectedRows) }
1413
+ },
1414
+ '????'
1415
+ ),
1416
+ h(
1417
+ 'v-btn',
1418
+ {
1419
+ props: { small: true, outlined: true, color: 'primary' },
1420
+ on: { click: () => this.handleExport(selectedRows) }
1421
+ },
1422
+ '????'
1423
+ )
1424
+ ]);
1425
+ },
1426
+ handleBatchDelete(rows) {
1427
+ alert(`???? ${rows.length} ?`);
1428
+ },
1429
+ handleExport(rows) {
1430
+ alert(`?? ${rows.length} ?`);
1431
+ }
1432
+ },
1433
+ template: `
1434
+ <jh-table
1435
+ :headers="headers"
1436
+ :items="items"
1437
+ show-select
1438
+ :toolbar="false"
1439
+ :table-alert-render="renderAlert"
1440
+ :table-alert-option-render="renderAlertActions"
1441
+ />
1442
+ `
1443
+ })
1444
+ };