jianghu-ui 1.0.1 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jianghu-ui",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "JianghuJS UI Component Library with Storybook, Vue 2, and Vuetify 2",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,13 +24,13 @@ import { Meta } from '@storybook/blocks';
24
24
  <!DOCTYPE html>
25
25
  <html>
26
26
  <head>
27
- <link href="https://jianghujs.github.io/jianghu-ui/cdn/jianghu-ui.css" rel="stylesheet">
27
+ <link href="https://cdn.jsdelivr.net/npm/jianghu-ui@1.0.1/dist/jianghu-ui.css" rel="stylesheet">
28
28
  </head>
29
29
  <body>
30
30
  <div id="app">
31
31
  <!-- 你的应用内容 -->
32
32
  </div>
33
- <script src="https://jianghujs.github.io/jianghu-ui/cdn/jianghu-ui.js"></script>
33
+ <script src="https://cdn.jsdelivr.net/npm/jianghu-ui@1.0.1/dist/jianghu-ui.js"></script>
34
34
  </body>
35
35
  </html>
36
36
  ```
@@ -10,7 +10,8 @@
10
10
  - ✅ **双向绑定**: 支持 v-model 双向数据绑定
11
11
  - ✅ **事件触发**: change 事件在值变化时触发
12
12
  - ✅ **清除功能**: 支持清除已选值
13
- - ✅ **返回完整信息**: 事件返回包含 code 和 name 的完整对象
13
+ - ✅ **返回完整信息**: 事件返回包�� code 和 name 的完整对象
14
+ - ✅ **多种模式**: 支持普通联动选择 (select) 和级联选择 (cascader) 两种模式
14
15
 
15
16
  ## 基本用法
16
17
 
@@ -54,6 +55,8 @@ export default {
54
55
  | --- | --- | --- | --- |
55
56
  | value / v-model | 绑定值,返回包含 code 和 name 的对象 | object | { province: null, city: null, district: null } |
56
57
  | level | 显示层级:1-仅省份,2-省市,3-省市区,4-省市区镇 | number | 3 |
58
+ | type | 显示模式:'select' (默认) 或 'cascader' | string | 'select' |
59
+ | label | 级联模式下的输入框标签 | string | '请选择地区' |
57
60
  | outlined | 是否使用 outlined 样式 | boolean | true |
58
61
  | dense | 是否使用紧凑模式 | boolean | false |
59
62
  | loading | 是否显示加载状态 | boolean | false |
@@ -118,6 +121,19 @@ data 属性需要符合以下格式:
118
121
  ></jh-address-select>
119
122
  ```
120
123
 
124
+ ### 级联选择模式
125
+
126
+ 使用 `type="cascader"` 开启级联选择模式。
127
+
128
+ ```vue
129
+ <jh-address-select
130
+ v-model="address"
131
+ type="cascader"
132
+ label="收货地址"
133
+ :data="addressData"
134
+ ></jh-address-select>
135
+ ```
136
+
121
137
  ### 仅选择到城市
122
138
 
123
139
  ```vue
@@ -243,8 +259,9 @@ export default {
243
259
  4. **禁用状态**: 未选择上级时,下级选择器会自动禁用
244
260
  5. **响应式布局**: 组件会根据 level 自动调整栅格布局(level=1 时占 12 列,level=2 时占 6 列,level=3 时占 4 列,level=4 时占 3 列)
245
261
  6. **返回值格式**: v-model 和事件返回的值包含 code 和 name 两个字段,方便获取编码和名称
262
+ 7. **级联模式**: 在 cascader 模式下,点击省份/城市/区县会展示下一级列表,直到选择完指定 level 级数后自动收起菜单
246
263
 
247
264
  ## 相关链接
248
265
 
249
266
  - [Vuetify Autocomplete](https://vuetifyjs.com/en/components/autocompletes/)
250
- - [中国行政区划代码](http://www.mca.gov.cn/article/sj/xzqh/)
267
+ - [中国行政区划代码](http://www.mca.gov.cn/article/sj/xzqh/)
@@ -31,6 +31,23 @@ export default {
31
31
  defaultValue: { summary: '3' },
32
32
  },
33
33
  },
34
+ type: {
35
+ control: { type: 'select' },
36
+ options: ['select', 'cascader'],
37
+ description: '显示模式:select-普通联动,cascader-级联选择',
38
+ table: {
39
+ type: { summary: 'string' },
40
+ defaultValue: { summary: 'select' },
41
+ },
42
+ },
43
+ label: {
44
+ control: 'text',
45
+ description: '级联模式下的输入框标签',
46
+ table: {
47
+ type: { summary: 'string' },
48
+ defaultValue: { summary: '请选择地区' },
49
+ },
50
+ },
34
51
  outlined: {
35
52
  control: 'boolean',
36
53
  description: '是否使用 `outlined` 样式',
@@ -105,6 +122,8 @@ const Template = (args) => ({
105
122
  <jh-address-select
106
123
  v-model="value"
107
124
  :level="args.level"
125
+ :type="args.type"
126
+ :label="args.label"
108
127
  :outlined="args.outlined"
109
128
  :dense="args.dense"
110
129
  :loading="args.loading"
@@ -123,6 +142,8 @@ const Template = (args) => ({
123
142
  const baseArgs = {
124
143
  value: { province: null, city: null, district: null, town: null },
125
144
  level: 3,
145
+ type: 'select',
146
+ label: '请选择地区',
126
147
  outlined: true,
127
148
  dense: true,
128
149
  loading: false,
@@ -144,6 +165,21 @@ Default.parameters = {
144
165
  },
145
166
  };
146
167
 
168
+ export const CascaderMode = Template.bind({});
169
+ CascaderMode.storyName = "级联选择模式";
170
+ CascaderMode.args = {
171
+ ...baseArgs,
172
+ type: 'cascader',
173
+ label: '请选择收货地址',
174
+ };
175
+ CascaderMode.parameters = {
176
+ docs: {
177
+ description: {
178
+ story: '使用 `type="cascader"` 开启级联选择模式,在一个下拉菜单中完成多级选择。',
179
+ },
180
+ },
181
+ };
182
+
147
183
  export const WithInitialValue = Template.bind({});
148
184
  WithInitialValue.storyName = "带初始值";
149
185
  WithInitialValue.args = {
@@ -157,7 +193,7 @@ WithInitialValue.args = {
157
193
  WithInitialValue.parameters = {
158
194
  docs: {
159
195
  description: {
160
- story: '设置 `v-model` 的初始值可以使组件在加载时就显示已选定的地址。返回值包含 `code` 和 `name` 两个字段。',
196
+ story: '设置 `v-model` 的初始值可以使组件在加载时就显示已选定的地址。返回值包含 `code` 和 `name` ���个字段。',
161
197
  },
162
198
  },
163
199
  };
@@ -278,5 +314,4 @@ Level4WithInitialValue.parameters = {
278
314
  story: '四级联动并设置初始值,展示完整的省市区镇选择功能。',
279
315
  },
280
316
  },
281
- };
282
-
317
+ };
@@ -1,86 +1,158 @@
1
1
  <template>
2
- <v-row dense>
3
- <v-col cols="12" :md="gridCol">
4
- <v-autocomplete
5
- class="jh-v-input"
6
- v-model="internalValue.province"
7
- :items="provinces"
8
- :label="labels.province"
9
- :outlined="outlined"
10
- :dense="dense"
11
- :filled="filled"
12
- :single-line="singleLine"
13
- :loading="loading"
14
- item-text="name"
15
- item-value="code"
16
- clearable
17
- hide-details
18
- hide-no-data
19
- prepend-inner-icon="mdi-map-outline"
20
- @change="handleProvinceChange"
21
- ></v-autocomplete>
22
- </v-col>
23
-
24
- <v-col v-if="level >= 2" cols="12" :md="gridCol">
25
- <v-autocomplete
26
- class="jh-v-input"
27
- v-model="internalValue.city"
28
- :items="cities"
29
- :label="labels.city"
30
- :disabled="!internalValue.province"
31
- :outlined="outlined"
32
- :dense="dense"
33
- :filled="filled"
34
- :single-line="singleLine"
35
- :loading="loading"
36
- item-text="name"
37
- item-value="code"
38
- clearable
39
- prepend-inner-icon="mdi-city-variant-outline"
40
- @change="handleCityChange"
41
- ></v-autocomplete>
42
- </v-col>
2
+ <div class="jh-address-select">
3
+ <template v-if="type === 'select'">
4
+ <v-row dense>
5
+ <v-col cols="12" :md="gridCol">
6
+ <v-autocomplete
7
+ class="jh-v-input"
8
+ v-model="internalValue.province"
9
+ :items="provinces"
10
+ :label="labels.province"
11
+ :outlined="outlined"
12
+ :dense="dense"
13
+ :filled="filled"
14
+ :single-line="singleLine"
15
+ :loading="loading"
16
+ item-text="name"
17
+ item-value="code"
18
+ clearable
19
+ hide-details
20
+ hide-no-data
21
+ prepend-inner-icon="mdi-map-outline"
22
+ @change="handleProvinceChange"
23
+ ></v-autocomplete>
24
+ </v-col>
25
+
26
+ <v-col v-if="level >= 2" cols="12" :md="gridCol">
27
+ <v-autocomplete
28
+ class="jh-v-input"
29
+ v-model="internalValue.city"
30
+ :items="cities"
31
+ :label="labels.city"
32
+ :disabled="!internalValue.province"
33
+ :outlined="outlined"
34
+ :dense="dense"
35
+ :filled="filled"
36
+ :single-line="singleLine"
37
+ :loading="loading"
38
+ item-text="name"
39
+ item-value="code"
40
+ clearable
41
+ prepend-inner-icon="mdi-city-variant-outline"
42
+ @change="handleCityChange"
43
+ ></v-autocomplete>
44
+ </v-col>
45
+
46
+ <v-col v-if="level >= 3" cols="12" :md="gridCol">
47
+ <v-autocomplete
48
+ class="jh-v-input"
49
+ v-model="internalValue.district"
50
+ :items="districts"
51
+ :label="labels.district"
52
+ :disabled="!internalValue.city"
53
+ :outlined="outlined"
54
+ :dense="dense"
55
+ :filled="filled"
56
+ :single-line="singleLine"
57
+ :loading="loading"
58
+ item-text="name"
59
+ item-value="code"
60
+ clearable
61
+ prepend-inner-icon="mdi-home-city-outline"
62
+ @change="handleDistrictChange"
63
+ ></v-autocomplete>
64
+ </v-col>
65
+
66
+ <v-col v-if="level >= 4" cols="12" :md="gridCol">
67
+ <v-autocomplete
68
+ class="jh-v-input"
69
+ v-model="internalValue.town"
70
+ :items="towns"
71
+ :label="labels.town"
72
+ :disabled="!internalValue.district"
73
+ :outlined="outlined"
74
+ :dense="dense"
75
+ :filled="filled"
76
+ :single-line="singleLine"
77
+ :loading="loading"
78
+ item-text="name"
79
+ item-value="code"
80
+ clearable
81
+ prepend-inner-icon="mdi-home-variant-outline"
82
+ @change="emitChange"
83
+ ></v-autocomplete>
84
+ </v-col>
85
+ </v-row>
86
+ </template>
43
87
 
44
- <v-col v-if="level >= 3" cols="12" :md="gridCol">
45
- <v-autocomplete
46
- class="jh-v-input"
47
- v-model="internalValue.district"
48
- :items="districts"
49
- :label="labels.district"
50
- :disabled="!internalValue.city"
51
- :outlined="outlined"
52
- :dense="dense"
53
- :filled="filled"
54
- :single-line="singleLine"
55
- :loading="loading"
56
- item-text="name"
57
- item-value="code"
58
- clearable
59
- prepend-inner-icon="mdi-home-city-outline"
60
- @change="handleDistrictChange"
61
- ></v-autocomplete>
62
- </v-col>
88
+ <template v-else-if="type === 'cascader'">
89
+ <v-menu
90
+ v-model="menuVisible"
91
+ :close-on-content-click="false"
92
+ offset-y
93
+ max-width="100%"
94
+ transition="scale-transition"
95
+ >
96
+ <template v-slot:activator="{ on, attrs }">
97
+ <v-text-field
98
+ class="jh-v-input"
99
+ v-bind="attrs"
100
+ v-on="on"
101
+ :value="displayText"
102
+ :label="label"
103
+ :outlined="outlined"
104
+ :dense="dense"
105
+ :filled="filled"
106
+ :single-line="singleLine"
107
+ :loading="loading"
108
+ readonly
109
+ append-icon="mdi-menu-down"
110
+ clearable
111
+ hide-details
112
+ @click:clear="clearValue"
113
+ ></v-text-field>
114
+ </template>
115
+ <v-card class="jh-cascader-card">
116
+ <div class="jh-cascader-container">
117
+ <div class="jh-cascader-column" v-if="provinces.length">
118
+ <v-list dense class="pa-0">
119
+ <v-list-item v-for="item in provinces" :key="item.code" @click="onProvinceClick(item)" :class="{'v-item--active v-list-item--active primary--text': internalValue.province === item.code}">
120
+ <v-list-item-content><v-list-item-title :title="item.name">{{ item.name }}</v-list-item-title></v-list-item-content>
121
+ <v-list-item-action v-if="level > 1"><v-icon small>mdi-chevron-right</v-icon></v-list-item-action>
122
+ </v-list-item>
123
+ </v-list>
124
+ </div>
63
125
 
64
- <v-col v-if="level >= 4" cols="12" :md="gridCol">
65
- <v-autocomplete
66
- class="jh-v-input"
67
- v-model="internalValue.town"
68
- :items="towns"
69
- :label="labels.town"
70
- :disabled="!internalValue.district"
71
- :outlined="outlined"
72
- :dense="dense"
73
- :filled="filled"
74
- :single-line="singleLine"
75
- :loading="loading"
76
- item-text="name"
77
- item-value="code"
78
- clearable
79
- prepend-inner-icon="mdi-home-variant-outline"
80
- @change="emitChange"
81
- ></v-autocomplete>
82
- </v-col>
83
- </v-row>
126
+ <div v-if="level >= 2" class="jh-cascader-column">
127
+ <v-list dense class="pa-0" v-if="cities.length">
128
+ <v-list-item v-for="item in cities" :key="item.code" @click="onCityClick(item)" :class="{'v-item--active v-list-item--active primary--text': internalValue.city === item.code}">
129
+ <v-list-item-content><v-list-item-title :title="item.name">{{ item.name }}</v-list-item-title></v-list-item-content>
130
+ <v-list-item-action v-if="level > 2"><v-icon small>mdi-chevron-right</v-icon></v-list-item-action>
131
+ </v-list-item>
132
+ </v-list>
133
+ </div>
134
+
135
+ <div v-if="level >= 3" class="jh-cascader-column">
136
+ <v-list dense class="pa-0" v-if="districts.length">
137
+ <v-list-item v-for="item in districts" :key="item.code" @click="onDistrictClick(item)" :class="{'v-item--active v-list-item--active primary--text': internalValue.district === item.code}">
138
+ <v-list-item-content><v-list-item-title :title="item.name">{{ item.name }}</v-list-item-title></v-list-item-content>
139
+ <v-list-item-action v-if="level > 3"><v-icon small>mdi-chevron-right</v-icon></v-list-item-action>
140
+ </v-list-item>
141
+ </v-list>
142
+ </div>
143
+
144
+ <div v-if="level >= 4" class="jh-cascader-column">
145
+ <v-list dense class="pa-0" v-if="towns.length">
146
+ <v-list-item v-for="item in towns" :key="item.code" @click="onTownClick(item)" :class="{'v-item--active v-list-item--active primary--text': internalValue.town === item.code}">
147
+ <v-list-item-content><v-list-item-title :title="item.name">{{ item.name }}</v-list-item-title></v-list-item-content>
148
+ </v-list-item>
149
+ </v-list>
150
+ </div>
151
+ </div>
152
+ </v-card>
153
+ </v-menu>
154
+ </template>
155
+ </div>
84
156
  </template>
85
157
 
86
158
  <script>
@@ -96,6 +168,8 @@ export default {
96
168
  dense: { type: Boolean, default: true },
97
169
  filled: { type: Boolean, default: true },
98
170
  singleLine: { type: Boolean, default: true },
171
+ type: { type: String, default: 'select' }, // select | cascader
172
+ label: { type: String, default: '请选择地区' },
99
173
 
100
174
  loading: { type: Boolean, default: false },
101
175
  labels: {
@@ -121,7 +195,8 @@ export default {
121
195
  internalValue: { ...this.value },
122
196
  cities: [],
123
197
  districts: [],
124
- towns: []
198
+ towns: [],
199
+ menuVisible: false,
125
200
  };
126
201
  },
127
202
  computed: {
@@ -134,6 +209,28 @@ export default {
134
209
  if (this.level === 3) return 4;
135
210
  return 3;
136
211
  },
212
+ displayText() {
213
+ if (!this.internalValue.province) return '';
214
+ const p = this.provinces.find(x => x.code === this.internalValue.province);
215
+ let text = p ? p.name : '';
216
+
217
+ if (this.level >= 2 && this.internalValue.city) {
218
+ const c = this.cities.find(x => x.code === this.internalValue.city);
219
+ if (c) text += ` / ${c.name}`;
220
+ }
221
+
222
+ if (this.level >= 3 && this.internalValue.district) {
223
+ const d = this.districts.find(x => x.code === this.internalValue.district);
224
+ if (d) text += ` / ${d.name}`;
225
+ }
226
+
227
+ if (this.level >= 4 && this.internalValue.town) {
228
+ const t = this.towns.find(x => x.code === this.internalValue.town);
229
+ if (t) text += ` / ${t.name}`;
230
+ }
231
+
232
+ return text;
233
+ },
137
234
  fullValue() {
138
235
  const result = {
139
236
  province: null,
@@ -252,10 +349,61 @@ export default {
252
349
  emitChange() {
253
350
  this.$emit('input', { ...this.fullValue });
254
351
  this.$emit('change', { ...this.fullValue });
352
+ },
353
+ onProvinceClick(item) {
354
+ this.internalValue.province = item.code;
355
+ this.handleProvinceChange(item.code);
356
+ if (this.level === 1) this.menuVisible = false;
357
+ },
358
+ onCityClick(item) {
359
+ this.internalValue.city = item.code;
360
+ this.handleCityChange(item.code);
361
+ if (this.level === 2) this.menuVisible = false;
362
+ },
363
+ onDistrictClick(item) {
364
+ this.internalValue.district = item.code;
365
+ this.handleDistrictChange(item.code);
366
+ if (this.level === 3) this.menuVisible = false;
367
+ },
368
+ onTownClick(item) {
369
+ this.internalValue.town = item.code;
370
+ this.emitChange();
371
+ if (this.level === 4) this.menuVisible = false;
372
+ },
373
+ clearValue() {
374
+ this.internalValue = { province: null, city: null, district: null, town: null };
375
+ this.cities = [];
376
+ this.districts = [];
377
+ this.towns = [];
378
+ this.emitChange();
255
379
  }
256
380
  }
257
381
  };
258
382
  </script>
259
383
 
260
384
  <style scoped>
385
+ .jh-cascader-container {
386
+ display: flex;
387
+ overflow-x: auto;
388
+ white-space: nowrap;
389
+ }
390
+ .jh-cascader-column {
391
+ min-width: 180px;
392
+ max-width: 250px;
393
+ max-height: 400px;
394
+ overflow-y: auto;
395
+ border-right: 1px solid #eee;
396
+ }
397
+ .jh-cascader-column:last-child {
398
+ border-right: none;
399
+ }
400
+ /* 优化列表项内容防止过早截断 */
401
+ .jh-cascader-column .v-list-item__content {
402
+ overflow: visible;
403
+ }
404
+ .jh-cascader-column .v-list-item__title {
405
+ white-space: nowrap;
406
+ overflow: hidden;
407
+ text-overflow: ellipsis;
408
+ }
261
409
  </style>
@@ -117,6 +117,17 @@
117
117
  clearable
118
118
  ></v-text-field>
119
119
 
120
+ <!-- 导出按钮 -->
121
+ <v-btn
122
+ v-if="toolbarConfig.export"
123
+ icon
124
+ small
125
+ @click="handleExport"
126
+ title="导出"
127
+ >
128
+ <v-icon>mdi-export-variant</v-icon>
129
+ </v-btn>
130
+
120
131
  <!-- 刷新按钮 -->
121
132
  <v-btn
122
133
  v-if="toolbarConfig.refresh"
@@ -244,7 +255,7 @@
244
255
  :single-select="singleSelectComputed"
245
256
  :value="selectedItems"
246
257
  :item-key="rowKey"
247
- :dense="dense"
258
+ :dense="tableDense"
248
259
  :multi-sort="multiSort"
249
260
  :must-sort="mustSort"
250
261
  :sort-by="internalSortBy"
@@ -964,6 +975,7 @@ export default {
964
975
  setting: true,
965
976
  density: true,
966
977
  fullscreen: false,
978
+ export: false, // Default to false, can be enabled via prop
967
979
  ...this.toolbar
968
980
  };
969
981
  }
@@ -972,7 +984,8 @@ export default {
972
984
  refresh: true,
973
985
  setting: true,
974
986
  density: true,
975
- fullscreen: false
987
+ fullscreen: false,
988
+ export: false // Default to false
976
989
  };
977
990
  },
978
991
  // 可见的表头
@@ -992,6 +1005,15 @@ export default {
992
1005
  }
993
1006
  ];
994
1007
  },
1008
+ tableDense() {
1009
+ if (this.currentDensity === 'compact') {
1010
+ return true;
1011
+ }
1012
+ // For 'medium' and 'default', we don't use the dense prop, but a custom class.
1013
+ // But we still respect the component's `dense` prop as a baseline.
1014
+ return this.dense;
1015
+ },
1016
+
995
1017
  // 密度样式类
996
1018
  densityClass() {
997
1019
  return {
@@ -1463,6 +1485,10 @@ export default {
1463
1485
  this.emitColumnStateChange();
1464
1486
  this.$forceUpdate();
1465
1487
  },
1488
+ // 导出表格
1489
+ handleExport() {
1490
+ this.$emit('export');
1491
+ },
1466
1492
  // 刷新表格
1467
1493
  async handleRefresh() {
1468
1494
  this.$emit('refresh');
@@ -2000,6 +2026,16 @@ export default {
2000
2026
  flex: 1;
2001
2027
  }
2002
2028
 
2029
+ /* --- 密度调整 --- */
2030
+ /* 中等密度 */
2031
+ .jh-pro-table ::v-deep .jh-table-medium.v-data-table > .v-data-table__wrapper > table > thead > tr > th {
2032
+ height: 40px;
2033
+ }
2034
+ .jh-pro-table ::v-deep .jh-table-medium.v-data-table > .v-data-table__wrapper > table > tbody > tr > td {
2035
+ height: 40px;
2036
+ }
2037
+
2038
+
2003
2039
  .jh-pro-table-header-right {
2004
2040
  display: flex;
2005
2041
  align-items: center;