@hbdlzy/ui-core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -3
- package/components.manifest.json +239 -24
- package/dist/index.cjs +1 -1
- package/dist/index.js +3005 -13
- package/dist/style.css +1 -0
- package/package.json +5 -3
- package/src/components/BaseCard/BaseCard.types.ts +38 -0
- package/src/components/BaseCard/BaseCard.vue +250 -0
- package/src/components/BaseCard/README.md +164 -0
- package/src/components/BaseCard/index.ts +5 -0
- package/src/components/BaseEChart/BaseEChart.types.ts +35 -0
- package/src/components/BaseEChart/BaseEChart.vue +327 -0
- package/src/components/BaseEChart/README.md +136 -0
- package/src/components/BaseEChart/index.ts +5 -0
- package/src/components/BaseExportButton/BaseExportButton.vue +1 -1
- package/src/components/BaseExportButton/README.md +104 -8
- package/src/components/BaseTable/BaseTable.types.ts +174 -0
- package/src/components/BaseTable/BaseTable.vue +809 -0
- package/src/components/BaseTable/README.md +398 -0
- package/src/components/BaseTable/index.ts +25 -0
- package/src/components/OutlinedCascader/OutlinedCascader.types.ts +28 -0
- package/src/components/OutlinedCascader/OutlinedCascader.vue +250 -0
- package/src/components/OutlinedCascader/README.md +91 -0
- package/src/components/OutlinedCascader/index.ts +10 -0
- package/src/components/OutlinedDatePicker/OutlinedDatePicker.types.ts +40 -0
- package/src/components/OutlinedDatePicker/OutlinedDatePicker.vue +365 -0
- package/src/components/OutlinedDatePicker/README.md +137 -0
- package/src/components/OutlinedDatePicker/index.ts +11 -0
- package/src/components/OutlinedDateTimePicker/OutlinedDateTimePicker.types.ts +34 -0
- package/src/components/OutlinedDateTimePicker/OutlinedDateTimePicker.vue +421 -0
- package/src/components/OutlinedDateTimePicker/README.md +88 -0
- package/src/components/OutlinedDateTimePicker/index.ts +11 -0
- package/src/components/OutlinedInput/OutlinedInput.types.ts +32 -0
- package/src/components/OutlinedInput/OutlinedInput.vue +307 -0
- package/src/components/OutlinedInput/README.md +154 -0
- package/src/components/OutlinedInput/index.ts +10 -0
- package/src/components/OutlinedSelect/OutlinedSelect.types.ts +48 -0
- package/src/components/OutlinedSelect/OutlinedSelect.vue +359 -0
- package/src/components/OutlinedSelect/README.md +166 -0
- package/src/components/OutlinedSelect/index.ts +12 -0
- package/src/components/OutlinedTimePicker/OutlinedTimePicker.types.ts +36 -0
- package/src/components/OutlinedTimePicker/OutlinedTimePicker.vue +303 -0
- package/src/components/OutlinedTimePicker/README.md +93 -0
- package/src/components/OutlinedTimePicker/index.ts +10 -0
- package/src/components/OutlinedTreeSelect/OutlinedTreeSelect.types.ts +56 -0
- package/src/components/OutlinedTreeSelect/OutlinedTreeSelect.vue +354 -0
- package/src/components/OutlinedTreeSelect/README.md +107 -0
- package/src/components/OutlinedTreeSelect/index.ts +12 -0
- package/src/echarts/index.ts +12 -0
- package/src/excel/exportExcel.ts +36 -10
- package/src/index.ts +29 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# BaseTable
|
|
2
|
+
|
|
3
|
+
`BaseTable` 用来统一封装项目里高频出现的表格壳子、分页栏、远程请求、操作列、图片列、标签列和常见表格暴露方法,避免页面层重复写同一套表格基础代码。
|
|
4
|
+
|
|
5
|
+
## 解决的问题
|
|
6
|
+
|
|
7
|
+
- 页面层不再重复封装 `el-table + el-pagination`
|
|
8
|
+
- 同时支持本地数据模式和远程请求模式
|
|
9
|
+
- 统一收敛勾选列、索引列、操作列、图片列、标签列、输入列
|
|
10
|
+
- 支持列配置驱动和插槽扩展,既能快用,也能保留复杂场景灵活性
|
|
11
|
+
- 提供 `load`、`refresh`、`setData`、`clearSelection` 等常用暴露方法
|
|
12
|
+
|
|
13
|
+
## 适用场景
|
|
14
|
+
|
|
15
|
+
- 列表页、管理页、日志页、明细页
|
|
16
|
+
- 需要统一分页和表格风格的页面
|
|
17
|
+
- 同一项目里会重复出现的通用管理表格
|
|
18
|
+
- 页面层希望把请求逻辑和列配置分开维护
|
|
19
|
+
|
|
20
|
+
## 不适用场景
|
|
21
|
+
|
|
22
|
+
- 高度强业务定制的复杂表格编辑器
|
|
23
|
+
- 需要虚拟滚动、大数据量渲染专项优化的场景
|
|
24
|
+
- 完全脱离 Element Plus Table 行为的表格交互
|
|
25
|
+
|
|
26
|
+
## 基础用法
|
|
27
|
+
|
|
28
|
+
### 本地数据模式
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<template>
|
|
32
|
+
<BaseTable
|
|
33
|
+
:columns="columns"
|
|
34
|
+
:data="rows"
|
|
35
|
+
:show-toolbar="false"
|
|
36
|
+
:show-pagination="false"
|
|
37
|
+
border
|
|
38
|
+
height="100%"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { BaseTable, type BaseTableColumn } from '@hbdlzy/ui-core'
|
|
44
|
+
|
|
45
|
+
interface RowItem {
|
|
46
|
+
id: number
|
|
47
|
+
name: string
|
|
48
|
+
status: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const columns: BaseTableColumn<RowItem>[] = [
|
|
52
|
+
{
|
|
53
|
+
label: '名称',
|
|
54
|
+
prop: 'name'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: '状态',
|
|
58
|
+
prop: 'status',
|
|
59
|
+
kind: 'tag',
|
|
60
|
+
options: [
|
|
61
|
+
{ label: '启用', value: 'enabled', tagType: 'success' },
|
|
62
|
+
{ label: '停用', value: 'disabled', tagType: 'info' }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const rows: RowItem[] = [
|
|
68
|
+
{ id: 1, name: '策略 A', status: 'enabled' },
|
|
69
|
+
{ id: 2, name: '策略 B', status: 'disabled' }
|
|
70
|
+
]
|
|
71
|
+
</script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 远程请求模式
|
|
75
|
+
|
|
76
|
+
`BaseTable` 不直接接 `listUrl`,而是统一改成 `request` 回调。这样组件本身不依赖任何业务接口封装。
|
|
77
|
+
|
|
78
|
+
```vue
|
|
79
|
+
<template>
|
|
80
|
+
<BaseTable
|
|
81
|
+
ref="tableRef"
|
|
82
|
+
:columns="columns"
|
|
83
|
+
:request="requestTableData"
|
|
84
|
+
:request-params="filters"
|
|
85
|
+
:has-index="true"
|
|
86
|
+
border
|
|
87
|
+
@row-action="handleRowAction"
|
|
88
|
+
>
|
|
89
|
+
<template #toolbar>
|
|
90
|
+
<el-form inline>
|
|
91
|
+
<el-form-item label="名称">
|
|
92
|
+
<el-input v-model="filters.keyword" placeholder="请输入名称" />
|
|
93
|
+
</el-form-item>
|
|
94
|
+
<el-form-item>
|
|
95
|
+
<el-button type="primary" @click="tableRef?.resetPage()">查询</el-button>
|
|
96
|
+
</el-form-item>
|
|
97
|
+
</el-form>
|
|
98
|
+
</template>
|
|
99
|
+
</BaseTable>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { ref } from 'vue'
|
|
104
|
+
import { BaseTable, type BaseTableColumn, type BaseTableExpose } from '@hbdlzy/ui-core'
|
|
105
|
+
|
|
106
|
+
interface RowItem {
|
|
107
|
+
id: number
|
|
108
|
+
name: string
|
|
109
|
+
status: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tableRef = ref<BaseTableExpose<RowItem> | null>(null)
|
|
113
|
+
|
|
114
|
+
const filters = ref({
|
|
115
|
+
keyword: ''
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const columns: BaseTableColumn<RowItem>[] = [
|
|
119
|
+
{
|
|
120
|
+
label: '名称',
|
|
121
|
+
prop: 'name',
|
|
122
|
+
kind: 'link'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: '状态',
|
|
126
|
+
prop: 'status'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: '操作',
|
|
130
|
+
prop: 'action',
|
|
131
|
+
kind: 'actions',
|
|
132
|
+
width: 180,
|
|
133
|
+
fixed: 'right',
|
|
134
|
+
actions: [
|
|
135
|
+
{ label: '编辑', type: 'edit' },
|
|
136
|
+
{ label: '删除', type: 'delete', buttonType: 'danger' }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
const requestTableData = async (params: Record<string, unknown>) => {
|
|
142
|
+
const res = await fetch('/api/demo/list', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: JSON.stringify(params)
|
|
145
|
+
}).then((response) => response.json())
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
records: res.data.records,
|
|
149
|
+
total: res.data.total
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handleRowAction = ({ row, action }: { row: RowItem; action: { type: string } }) => {
|
|
154
|
+
if (action.type === 'edit') {
|
|
155
|
+
console.log('edit', row)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 请求结果适配
|
|
162
|
+
|
|
163
|
+
如果接口返回结构不是常见的 `records/list/items + total`,可以直接传 `resultAdapter` 做结果归一化。
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<script setup lang="ts">
|
|
167
|
+
import { BaseTable } from '@hbdlzy/ui-core'
|
|
168
|
+
|
|
169
|
+
const resultAdapter = (result: any) => {
|
|
170
|
+
return {
|
|
171
|
+
rows: result?.payload?.rows || [],
|
|
172
|
+
total: Number(result?.payload?.count || 0)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<BaseTable
|
|
179
|
+
:columns="columns"
|
|
180
|
+
:request="requestTableData"
|
|
181
|
+
:result-adapter="resultAdapter"
|
|
182
|
+
/>
|
|
183
|
+
</template>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
这样无论接口原始返回是:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
{
|
|
190
|
+
payload: {
|
|
191
|
+
rows: [],
|
|
192
|
+
count: 36
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
还是别的结构,页面层都可以自己决定如何映射成:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
{
|
|
201
|
+
rows: [],
|
|
202
|
+
total: 36
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 远程排序和参数映射
|
|
207
|
+
|
|
208
|
+
如果接口需要排序字段和排序方向,可以直接用内置映射能力。
|
|
209
|
+
|
|
210
|
+
```vue
|
|
211
|
+
<script setup lang="ts">
|
|
212
|
+
import { BaseTable, type BaseTableColumn } from '@hbdlzy/ui-core'
|
|
213
|
+
|
|
214
|
+
interface RowItem {
|
|
215
|
+
id: number
|
|
216
|
+
name: string
|
|
217
|
+
createdAt: string
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const columns: BaseTableColumn<RowItem>[] = [
|
|
221
|
+
{
|
|
222
|
+
label: '名称',
|
|
223
|
+
prop: 'name',
|
|
224
|
+
sortable: true
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
label: '创建时间',
|
|
228
|
+
prop: 'createdAt',
|
|
229
|
+
sortField: 'created_time',
|
|
230
|
+
sortable: true
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<BaseTable
|
|
237
|
+
:columns="columns"
|
|
238
|
+
:request="requestTableData"
|
|
239
|
+
:default-sort="{ prop: 'createdAt', order: 'descending' }"
|
|
240
|
+
sort-field-key="orderBy"
|
|
241
|
+
sort-order-key="orderDirection"
|
|
242
|
+
/>
|
|
243
|
+
</template>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
上面这个配置会把请求参数自动映射成:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
{
|
|
250
|
+
pageNum: 1,
|
|
251
|
+
pageSize: 20,
|
|
252
|
+
orderBy: 'created_time',
|
|
253
|
+
orderDirection: 'desc'
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
如果接口参数更特殊,可以直接传 `sortMapper` 自定义:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const sortMapper = (payload: { field?: string; direction: string | null }) => {
|
|
261
|
+
if (!payload.field || !payload.direction) {
|
|
262
|
+
return {}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
sortList: [
|
|
267
|
+
{
|
|
268
|
+
property: payload.field,
|
|
269
|
+
direction: payload.direction === 'asc'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## 列插槽
|
|
277
|
+
|
|
278
|
+
如果某一列需要完全自定义渲染,优先使用命名插槽。
|
|
279
|
+
|
|
280
|
+
- 默认列插槽名:`cell-${prop}`
|
|
281
|
+
- 默认表头插槽名:`header-${prop}`
|
|
282
|
+
|
|
283
|
+
```vue
|
|
284
|
+
<template>
|
|
285
|
+
<BaseTable :columns="columns" :data="rows" :show-toolbar="false" :show-pagination="false">
|
|
286
|
+
<template #cell-status="{ row }">
|
|
287
|
+
<el-switch :model-value="row.status === 'enabled'" />
|
|
288
|
+
</template>
|
|
289
|
+
</BaseTable>
|
|
290
|
+
</template>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Props
|
|
294
|
+
|
|
295
|
+
- `columns`: 列配置数组
|
|
296
|
+
- `data`: 本地数据源
|
|
297
|
+
- `request`: 远程请求回调
|
|
298
|
+
- `requestParams`: 请求附加参数
|
|
299
|
+
- `resultAdapter`: 请求结果适配器,用于把接口原始返回映射成 `{ rows, total }`
|
|
300
|
+
- `autoLoad`: 有 `request` 时是否挂载后自动请求
|
|
301
|
+
- `reloadOnParamsChange`: `requestParams` 改变时是否自动重载
|
|
302
|
+
- `rowKey`: 行主键,默认 `id`
|
|
303
|
+
- `height`: 表格高度
|
|
304
|
+
- `border`: 是否显示边框
|
|
305
|
+
- `stripe`: 是否显示斑马纹
|
|
306
|
+
- `showToolbar`: 是否显示顶部工具栏容器
|
|
307
|
+
- `showPagination`: 是否显示分页
|
|
308
|
+
- `hasSelection`: 是否显示多选列
|
|
309
|
+
- `hasIndex`: 是否显示序号列
|
|
310
|
+
- `pagination`: 分页配置,支持 `currentPage`、`pageSize`、`total`、`pageSizes`
|
|
311
|
+
- `currentPageKey`: 远程请求时页码字段名,默认 `pageNum`
|
|
312
|
+
- `pageSizeKey`: 远程请求时每页条数字段名,默认 `pageSize`
|
|
313
|
+
- `defaultSort`: 默认排序状态
|
|
314
|
+
- `reloadOnSortChange`: 排序变化时是否自动重载远程数据
|
|
315
|
+
- `sortFieldKey`: 排序字段请求参数名,默认 `sortField`
|
|
316
|
+
- `sortOrderKey`: 排序方向请求参数名,默认 `sortOrder`
|
|
317
|
+
- `sortOrderMap`: 排序方向值映射,默认 `{ ascending: 'asc', descending: 'desc' }`
|
|
318
|
+
- `sortMapper`: 自定义排序参数映射器,优先级高于 `sortFieldKey/sortOrderKey`
|
|
319
|
+
- `emptyText`: 空状态文案
|
|
320
|
+
|
|
321
|
+
## Column 能力
|
|
322
|
+
|
|
323
|
+
列配置里当前支持这些高频模式:
|
|
324
|
+
|
|
325
|
+
- 默认文本列
|
|
326
|
+
- `kind: 'link'`
|
|
327
|
+
点击型文本列
|
|
328
|
+
- `kind: 'actions'`
|
|
329
|
+
操作列,配合 `actions`
|
|
330
|
+
- `kind: 'image'`
|
|
331
|
+
图片列
|
|
332
|
+
- `kind: 'tag'`
|
|
333
|
+
标签映射列,配合 `options`
|
|
334
|
+
- `kind: 'html'`
|
|
335
|
+
HTML 内容列
|
|
336
|
+
- `kind: 'input'`
|
|
337
|
+
行内输入列
|
|
338
|
+
|
|
339
|
+
常用字段:
|
|
340
|
+
|
|
341
|
+
- `label`
|
|
342
|
+
- `prop`
|
|
343
|
+
- `width`
|
|
344
|
+
- `minWidth`
|
|
345
|
+
- `align`
|
|
346
|
+
- `fixed`
|
|
347
|
+
- `sortable`
|
|
348
|
+
- `sortField`
|
|
349
|
+
- `showOverflowTooltip`
|
|
350
|
+
- `actions`
|
|
351
|
+
- `options`
|
|
352
|
+
- `formatter`
|
|
353
|
+
- `clickHandler`
|
|
354
|
+
- `inputHandler`
|
|
355
|
+
- `slotName`
|
|
356
|
+
- `headerSlotName`
|
|
357
|
+
|
|
358
|
+
## Events
|
|
359
|
+
|
|
360
|
+
- `selection-change`: 勾选行变化
|
|
361
|
+
- `sort-change`: 排序变化
|
|
362
|
+
返回 `prop`、`order`、`field`、`direction`
|
|
363
|
+
- `row-action`: 点击操作列按钮
|
|
364
|
+
- `cell-click`: 点击链接型单元格
|
|
365
|
+
- `cell-input`: 行内输入变化
|
|
366
|
+
- `page-change`: 页码变化
|
|
367
|
+
- `size-change`: 每页条数变化
|
|
368
|
+
- `update:pagination`: 分页状态变化
|
|
369
|
+
- `loaded`: 远程数据加载完成
|
|
370
|
+
- `request-error`: 远程请求失败
|
|
371
|
+
|
|
372
|
+
## Expose
|
|
373
|
+
|
|
374
|
+
- `load(data?)`: 传数组时直接灌入本地数据,不传时走远程请求
|
|
375
|
+
- `refresh()`: 按当前分页和参数重新请求
|
|
376
|
+
- `setData(data)`: 直接替换当前表格数据
|
|
377
|
+
- `resetPage()`: 页码重置到第一页并重新加载
|
|
378
|
+
- `clearSelection()`: 清空勾选
|
|
379
|
+
- `toggleRowSelection(row, selected?)`: 手动切换某一行勾选状态
|
|
380
|
+
- `getSelectionRows()`: 获取当前勾选行
|
|
381
|
+
- `getRows()`: 获取当前表格行数据
|
|
382
|
+
|
|
383
|
+
## 从旧 TableComponent 迁移的建议
|
|
384
|
+
|
|
385
|
+
- 把旧的 `listUrl`、`paramsData` 改成 `request + requestParams`
|
|
386
|
+
- 如果旧接口返回结构不统一,优先通过 `resultAdapter` 适配,不要改公共组件内部解析逻辑
|
|
387
|
+
- 把旧的 `sortKey`、`sortList` 映射逻辑改成 `sortField`、`sortFieldKey`、`sortOrderKey` 或 `sortMapper`
|
|
388
|
+
- 把旧的 `@handleAction` 改成 `@row-action`
|
|
389
|
+
- 把旧的顶部筛选表单内容移动到 `toolbar` 插槽
|
|
390
|
+
- 业务字典渲染优先用 `kind: 'tag' + options`
|
|
391
|
+
- 如果某列逻辑太特殊,优先用 `cell-${prop}` 插槽,不要继续把业务逻辑塞回公共组件
|
|
392
|
+
|
|
393
|
+
## 推荐约定
|
|
394
|
+
|
|
395
|
+
- 页面层负责请求参数和接口编排,`BaseTable` 只负责通用表格能力
|
|
396
|
+
- 默认优先使用列配置驱动,只有复杂单元格才用插槽
|
|
397
|
+
- 通用操作列尽量统一成 `row-action`
|
|
398
|
+
- 复杂筛选表单不要内置到表格组件里,统一通过 `toolbar` 插槽扩展
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import BaseTable from './BaseTable.vue'
|
|
2
|
+
|
|
3
|
+
export default BaseTable
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
BaseTableAction,
|
|
7
|
+
BaseTableAlign,
|
|
8
|
+
BaseTableCellPayload,
|
|
9
|
+
BaseTableColumn,
|
|
10
|
+
BaseTableColumnKind,
|
|
11
|
+
BaseTableCssValue,
|
|
12
|
+
BaseTableExpose,
|
|
13
|
+
BaseTableLoadedPayload,
|
|
14
|
+
BaseTableNormalizedResult,
|
|
15
|
+
BaseTableOption,
|
|
16
|
+
BaseTablePagination,
|
|
17
|
+
BaseTableProps,
|
|
18
|
+
BaseTableResultAdapter,
|
|
19
|
+
BaseTableRequestHandler,
|
|
20
|
+
BaseTableRequestParams,
|
|
21
|
+
BaseTableRequestResult,
|
|
22
|
+
BaseTableRowActionPayload,
|
|
23
|
+
BaseTableSortOrder,
|
|
24
|
+
BaseTableSortPayload
|
|
25
|
+
} from './BaseTable.types'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type OutlinedCascaderCssValue = string | number
|
|
2
|
+
|
|
3
|
+
export type OutlinedCascaderValue = Array<string | number>
|
|
4
|
+
|
|
5
|
+
export interface OutlinedCascaderProps {
|
|
6
|
+
value?: OutlinedCascaderValue
|
|
7
|
+
options?: Record<string, unknown>[]
|
|
8
|
+
clearable?: boolean
|
|
9
|
+
propsValue?: Record<string, unknown>
|
|
10
|
+
placeholder?: string
|
|
11
|
+
label?: string
|
|
12
|
+
popperClass?: string
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
inputHeight?: number
|
|
15
|
+
isBorder?: boolean
|
|
16
|
+
filterable?: boolean
|
|
17
|
+
levels?: boolean
|
|
18
|
+
marginBottom?: OutlinedCascaderCssValue
|
|
19
|
+
paddingTop?: OutlinedCascaderCssValue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OutlinedCascaderExpose {
|
|
23
|
+
focus: () => void
|
|
24
|
+
blur: () => void
|
|
25
|
+
togglePopperVisible: () => void
|
|
26
|
+
clear: () => void
|
|
27
|
+
getCascaderRef: () => unknown | null
|
|
28
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="outlined-cascader"
|
|
4
|
+
:class="[containerClasses, rootClass]"
|
|
5
|
+
:style="[containerStyle, rootStyle]"
|
|
6
|
+
>
|
|
7
|
+
<el-cascader
|
|
8
|
+
ref="cascaderRef"
|
|
9
|
+
v-bind="cascaderAttrs"
|
|
10
|
+
class="outlined-cascader__control"
|
|
11
|
+
:model-value="cascaderValue"
|
|
12
|
+
:options="options"
|
|
13
|
+
:props="propsValue"
|
|
14
|
+
:clearable="clearable"
|
|
15
|
+
:placeholder="placeholder"
|
|
16
|
+
:filterable="filterable"
|
|
17
|
+
:show-all-levels="levels"
|
|
18
|
+
:disabled="disabled"
|
|
19
|
+
:popper-class="popperClass"
|
|
20
|
+
:style="cascaderStyle"
|
|
21
|
+
@update:model-value="handleModelValueUpdate"
|
|
22
|
+
@focus="handleFocus"
|
|
23
|
+
@blur="handleBlur"
|
|
24
|
+
@change="handleChange"
|
|
25
|
+
@visible-change="handleVisibleChange"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<div
|
|
29
|
+
v-if="floatingLabel"
|
|
30
|
+
class="outlined-cascader__label"
|
|
31
|
+
:class="labelClasses"
|
|
32
|
+
>
|
|
33
|
+
{{ floatingLabel }}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
import { computed, ref, useAttrs, watch } from 'vue'
|
|
40
|
+
import type {
|
|
41
|
+
OutlinedCascaderCssValue,
|
|
42
|
+
OutlinedCascaderExpose,
|
|
43
|
+
OutlinedCascaderProps,
|
|
44
|
+
OutlinedCascaderValue
|
|
45
|
+
} from './OutlinedCascader.types'
|
|
46
|
+
|
|
47
|
+
defineOptions({
|
|
48
|
+
name: 'OutlinedCascader',
|
|
49
|
+
inheritAttrs: false
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const props = withDefaults(defineProps<OutlinedCascaderProps>(), {
|
|
53
|
+
value: () => [],
|
|
54
|
+
options: () => [],
|
|
55
|
+
clearable: false,
|
|
56
|
+
propsValue: undefined,
|
|
57
|
+
placeholder: '',
|
|
58
|
+
label: '',
|
|
59
|
+
popperClass: '',
|
|
60
|
+
disabled: false,
|
|
61
|
+
inputHeight: 40,
|
|
62
|
+
isBorder: false,
|
|
63
|
+
filterable: true,
|
|
64
|
+
levels: true,
|
|
65
|
+
marginBottom: 20,
|
|
66
|
+
paddingTop: 20
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const emit = defineEmits<{
|
|
70
|
+
(event: 'input', value: OutlinedCascaderValue): void
|
|
71
|
+
(event: 'update:value', value: OutlinedCascaderValue): void
|
|
72
|
+
(event: 'change', value: OutlinedCascaderValue): void
|
|
73
|
+
(event: 'focus', value: FocusEvent): void
|
|
74
|
+
(event: 'blur', value: FocusEvent): void
|
|
75
|
+
(event: 'visible-change', value: boolean): void
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
const attrs = useAttrs()
|
|
79
|
+
const cascaderRef = ref<any>(null)
|
|
80
|
+
const cascaderValue = ref<OutlinedCascaderValue>(normalizeValue(props.value))
|
|
81
|
+
const focused = ref(false)
|
|
82
|
+
const blurred = ref(false)
|
|
83
|
+
const visible = ref(false)
|
|
84
|
+
|
|
85
|
+
watch(
|
|
86
|
+
() => props.value,
|
|
87
|
+
(value) => {
|
|
88
|
+
cascaderValue.value = normalizeValue(value)
|
|
89
|
+
},
|
|
90
|
+
{ immediate: true }
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const rootClass = computed(() => attrs.class)
|
|
94
|
+
const rootStyle = computed(() => attrs.style)
|
|
95
|
+
|
|
96
|
+
const cascaderAttrs = computed(() => {
|
|
97
|
+
const { class: _class, style: _style, ...rest } = attrs
|
|
98
|
+
return rest
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const containerClasses = computed(() => ({
|
|
102
|
+
'outlined-cascader--bordered': props.isBorder
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
const containerStyle = computed(() => ({
|
|
106
|
+
marginBottom: toCssValue(props.marginBottom),
|
|
107
|
+
paddingTop: toCssValue(props.paddingTop)
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
const cascaderStyle = computed(() => ({
|
|
111
|
+
width: '100%',
|
|
112
|
+
height: `${props.inputHeight}px`
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const floatingLabel = computed(() => props.label || props.placeholder || '')
|
|
116
|
+
|
|
117
|
+
const hasContent = computed(() => cascaderValue.value.length > 0)
|
|
118
|
+
|
|
119
|
+
const labelClasses = computed(() => ({
|
|
120
|
+
'outlined-cascader__label--floating': focused.value || hasContent.value,
|
|
121
|
+
'outlined-cascader__label--placeholder': !focused.value && !hasContent.value,
|
|
122
|
+
'outlined-cascader__label--filled': hasContent.value && !focused.value
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
function handleModelValueUpdate(value: OutlinedCascaderValue) {
|
|
126
|
+
const normalizedValue = normalizeValue(value)
|
|
127
|
+
cascaderValue.value = normalizedValue
|
|
128
|
+
emitModelValue(normalizedValue)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleChange(value: OutlinedCascaderValue) {
|
|
132
|
+
const normalizedValue = normalizeValue(value)
|
|
133
|
+
cascaderValue.value = normalizedValue
|
|
134
|
+
emit('change', normalizedValue)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleFocus(event: FocusEvent) {
|
|
138
|
+
blurred.value = false
|
|
139
|
+
focused.value = true
|
|
140
|
+
emit('focus', event)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleBlur(event: FocusEvent) {
|
|
144
|
+
blurred.value = true
|
|
145
|
+
|
|
146
|
+
if (!visible.value) {
|
|
147
|
+
focused.value = false
|
|
148
|
+
emitModelValue(cascaderValue.value)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
emit('blur', event)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleVisibleChange(nextVisible: boolean) {
|
|
155
|
+
visible.value = nextVisible
|
|
156
|
+
|
|
157
|
+
if (!nextVisible && blurred.value) {
|
|
158
|
+
focused.value = false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
emit('visible-change', nextVisible)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function emitModelValue(value: OutlinedCascaderValue) {
|
|
165
|
+
emit('input', value)
|
|
166
|
+
emit('update:value', value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeValue(value: OutlinedCascaderValue | undefined) {
|
|
170
|
+
return Array.isArray(value) ? value : []
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toCssValue(value: OutlinedCascaderCssValue | undefined) {
|
|
174
|
+
if (typeof value === 'number') {
|
|
175
|
+
return `${value}px`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
defineExpose<OutlinedCascaderExpose>({
|
|
182
|
+
focus: () => {
|
|
183
|
+
cascaderRef.value?.togglePopperVisible?.(true)
|
|
184
|
+
},
|
|
185
|
+
blur: () => {
|
|
186
|
+
cascaderRef.value?.togglePopperVisible?.(false)
|
|
187
|
+
},
|
|
188
|
+
togglePopperVisible: () => {
|
|
189
|
+
cascaderRef.value?.togglePopperVisible?.()
|
|
190
|
+
},
|
|
191
|
+
clear: () => {
|
|
192
|
+
cascaderValue.value = []
|
|
193
|
+
emitModelValue([])
|
|
194
|
+
emit('change', [])
|
|
195
|
+
},
|
|
196
|
+
getCascaderRef: () => cascaderRef.value
|
|
197
|
+
})
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<style scoped>
|
|
201
|
+
.outlined-cascader {
|
|
202
|
+
position: relative;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.outlined-cascader__label {
|
|
206
|
+
position: absolute;
|
|
207
|
+
left: -2px;
|
|
208
|
+
padding: 2px 5px;
|
|
209
|
+
border-radius: 2px;
|
|
210
|
+
background-color: #ffffff;
|
|
211
|
+
font-size: 12px;
|
|
212
|
+
line-height: 1;
|
|
213
|
+
transition: color 200ms cubic-bezier(0, 0, 0.2, 1), transform 200ms cubic-bezier(0, 0, 0.2, 1);
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.outlined-cascader__label--floating {
|
|
218
|
+
top: 18px;
|
|
219
|
+
z-index: 1;
|
|
220
|
+
opacity: 1;
|
|
221
|
+
transform: translate(14px, -6px) scale(1);
|
|
222
|
+
color: var(--el-color-primary);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.outlined-cascader__label--placeholder {
|
|
226
|
+
top: 50%;
|
|
227
|
+
left: 1px;
|
|
228
|
+
z-index: -1;
|
|
229
|
+
opacity: 0;
|
|
230
|
+
transform: translate(14px, 6px) scale(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.outlined-cascader__label--filled {
|
|
234
|
+
color: var(--el-text-color-regular);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
:deep(.outlined-cascader__control .el-input) {
|
|
238
|
+
height: 100%;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
:deep(.outlined-cascader--bordered .el-input__wrapper) {
|
|
242
|
+
box-shadow: none;
|
|
243
|
+
border-radius: 0;
|
|
244
|
+
border-bottom: 1px solid #cccccc;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
:deep(.outlined-cascader--bordered .el-input__wrapper.is-focus) {
|
|
248
|
+
border-bottom-color: var(--el-color-primary);
|
|
249
|
+
}
|
|
250
|
+
</style>
|