@ebiz/designer-components 0.0.18-beta.27 → 0.0.18-beta.28

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": "@ebiz/designer-components",
3
- "version": "0.0.18-beta.27",
3
+ "version": "0.0.18-beta.28",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -6,7 +6,6 @@
6
6
 
7
7
  import axios from 'axios'
8
8
  import { TinyNotify } from '@opentiny/vue'
9
-
10
9
  // 从环境变量获取API基础URL
11
10
  const API_BASE_URL = 'http://' + window.location.host + '/api'
12
11
 
@@ -41,9 +41,7 @@ const props = defineProps({
41
41
  */
42
42
  queryParams: {
43
43
  type: Object,
44
- default: () => ({
45
- name: ''
46
- })
44
+ default: () => ({})
47
45
  },
48
46
  /**
49
47
  * 是否多选
@@ -64,7 +62,7 @@ const props = defineProps({
64
62
  */
65
63
  clearable: {
66
64
  type: Boolean,
67
- default: true
65
+ default: false
68
66
  },
69
67
  /**
70
68
  * 是否禁用
@@ -142,29 +140,24 @@ const focus = () => {
142
140
 
143
141
  defineExpose({
144
142
  focus,
145
- selectRef,
146
- options
143
+ selectRef
147
144
  });
148
145
 
149
146
  // 远程搜索处理函数
150
147
  const handleRemoteSearch = async (keyword) => {
151
148
  loading.value = true;
149
+ console.log('handleRemoteSearch', keyword);
152
150
  try {
153
151
  const params = {
154
152
  queryParams: {
155
153
  ...queryParams.value,
156
- name: keyword
154
+ keyword
157
155
  }
158
156
  };
159
- const res = await dataService.fetch(params, {
160
- key: props.apiConfig.key,
161
- apiId: props.apiConfig.apiId,
162
- apiType: 'MULTIPLE_DATA_SEARCH'
163
- });
157
+ const res = await dataService.fetch(params, props.apiConfig);
164
158
  const { labelField, valueField } = props.optionsConfig;
165
159
 
166
160
  options.value = res.data.map(item => ({
167
- ...item,
168
161
  label: labelField ? item[labelField] : (item.label || item.name),
169
162
  value: valueField ? item[valueField] : (item.value || item.id)
170
163
  }));
@@ -213,7 +206,6 @@ onMounted(async () => {
213
206
  const { labelField, valueField } = props.optionsConfig;
214
207
 
215
208
  options.value = res.data.map(item => ({
216
- ...item,
217
209
  label: labelField ? item[labelField] : (item.label || item.name),
218
210
  value: valueField ? item[valueField] : (item.value || item.id)
219
211
  }));
@@ -5,6 +5,8 @@
5
5
  :value="modelValue"
6
6
  :expanded="expandedModel"
7
7
  :actived="activedModel"
8
+ :transition="transition"
9
+ :disable-check="disableCheck"
8
10
  @change="handleChange"
9
11
  @expand="handleExpand"
10
12
  @active="handleActive"
@@ -63,6 +65,16 @@ const props = defineProps({
63
65
  items: {
64
66
  type: Array,
65
67
  default: () => []
68
+ },
69
+ // 是否启用过渡动画
70
+ transition: {
71
+ type: Boolean,
72
+ default: false
73
+ },
74
+ // 自定义节点禁用状态,返回true表示禁用
75
+ disableCheck: {
76
+ type: Function,
77
+ default: null
66
78
  }
67
79
  });
68
80
 
@@ -0,0 +1,517 @@
1
+ <template>
2
+ <div class="ebiz-tree-selector">
3
+ <div class="selected-items" v-if="modelValue && modelValue.length">
4
+ <div v-for="(item, index) in modelValue" :key="index" class="selected-item">
5
+ <span class="item-text">{{ item.label }}</span>
6
+ <span class="item-remove" @click.stop="removeItem(index)">×</span>
7
+ </div>
8
+ </div>
9
+ <t-button @click="showDialog" icon="add">
10
+ <t-icon name="add"></t-icon>添加
11
+ </t-button>
12
+
13
+ <EbizDialog
14
+ v-model:visible="dialogVisible"
15
+ :header="{
16
+ text: '选择人员/部门',
17
+ icon: 'folder-open'
18
+ }"
19
+ width="800px"
20
+ placement="center"
21
+ confirmBtn="确定"
22
+ cancelBtn="取消"
23
+ @confirm="handleConfirm"
24
+ @cancel="handleCancel"
25
+ >
26
+ <div class="selector-container">
27
+ <!-- 左侧选择区域 -->
28
+ <div class="left-panel">
29
+ <!-- 顶部搜索区域 -->
30
+ <div class="search-box">
31
+ <t-input v-model="searchText" placeholder="搜索成员、部门或标签" clearable>
32
+ <template #suffix-icon>
33
+ <t-icon name="search"></t-icon>
34
+ </template>
35
+ </t-input>
36
+ </div>
37
+
38
+ <!-- 选项卡 -->
39
+ <t-tabs v-model="activeTab" class="selector-tabs">
40
+ <t-tab-panel value="organization" label="组织架构"></t-tab-panel>
41
+ <t-tab-panel value="department" label="部门"></t-tab-panel>
42
+ <t-tab-panel value="position" label="岗位"></t-tab-panel>
43
+ <t-tab-panel value="employee" label="员工"></t-tab-panel>
44
+ </t-tabs>
45
+
46
+ <!-- 树形结构区域 -->
47
+ <div class="tree-content">
48
+ <div v-if="loading" class="loading-container">
49
+ <t-loading />
50
+ </div>
51
+ <EbizTree
52
+ v-else-if="activeTab === 'organization'"
53
+ checkable
54
+ :items="filteredData.organization"
55
+ v-model="checkedNodes"
56
+ :disable-check="disableCheck"
57
+ />
58
+ <EbizTree
59
+ v-else-if="activeTab === 'department'"
60
+ checkable
61
+ :items="filteredData.department"
62
+ v-model="checkedNodes"
63
+ :disable-check="disableCheck"
64
+ />
65
+ <EbizTree
66
+ v-else-if="activeTab === 'position'"
67
+ checkable
68
+ :items="filteredData.position"
69
+ v-model="checkedNodes"
70
+ :disable-check="disableCheck"
71
+ />
72
+ <EbizTree
73
+ v-else-if="activeTab === 'employee'"
74
+ checkable
75
+ :items="filteredData.employee"
76
+ v-model="checkedNodes"
77
+ :disable-check="disableCheck"
78
+ />
79
+ </div>
80
+ </div>
81
+
82
+ <!-- 右侧已选区域 -->
83
+ <div class="right-panel">
84
+ <div class="selected-title">已选择的部门、成员</div>
85
+ <div class="selected-count">共 {{ selectPreview.length }} 项</div>
86
+ <div class="selected-list">
87
+ <div v-for="(item, index) in selectPreview" :key="index" class="selected-list-item">
88
+ <t-icon :name="getIconForType(item.type)" class="item-icon" />
89
+ <span class="selected-label">{{ item.label }}</span>
90
+ <t-icon name="close-circle" class="remove-icon" @click="removePreviewItem(index)" />
91
+ </div>
92
+ <div v-if="selectPreview.length === 0" class="no-selection">
93
+ <t-icon name="info-circle" />
94
+ <span>请在左侧选择部门或成员</span>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </EbizDialog>
100
+ </div>
101
+ </template>
102
+
103
+ <script setup>
104
+ import { ref, computed, watch, onMounted } from 'vue';
105
+ import { Button as TButton, Icon as TIcon, Input as TInput, Tabs as TTabs, TabPanel as TTabPanel, Loading as TLoading, MessagePlugin } from 'tdesign-vue-next';
106
+ import request from '../apiService/simpleDataService';
107
+ import EbizDialog from './TdesignDialog.vue';
108
+ import EbizTree from './EbizTree.vue';
109
+
110
+ const props = defineProps({
111
+ // 选中的数据,支持v-model
112
+ modelValue: {
113
+ type: Array,
114
+ default: () => []
115
+ },
116
+ // 树形数据,可以是单个数组或分类对象
117
+ data: {
118
+ type: [Array, Object],
119
+ default: () => ([])
120
+ },
121
+ // 禁用选择的节点
122
+ disableCheck: {
123
+ type: Function,
124
+ default: null
125
+ },
126
+ // 标题
127
+ title: {
128
+ type: String,
129
+ default: '选择人员/部门'
130
+ }
131
+ });
132
+
133
+ const emit = defineEmits(['update:modelValue', 'change']);
134
+
135
+ // 弹窗显示状态
136
+ const dialogVisible = ref(false);
137
+ // 搜索文本
138
+ const searchText = ref('');
139
+ // 选中的节点
140
+ const checkedNodes = ref([]);
141
+ // 当前活动的选项卡
142
+ const activeTab = ref('organization');
143
+ // 弹窗内预览的选中项
144
+ const selectPreview = ref([]);
145
+ // 加载状态
146
+ const loading = ref(false);
147
+ // API返回的数据
148
+ const apiData = ref(null);
149
+
150
+ // 固定的API请求URL和参数
151
+ const API_URL = '/permissions/tree';
152
+ const API_PARAMS = {
153
+ type: 'all',
154
+ includeDisabled: false
155
+ };
156
+
157
+ // 根据数据结构处理树形数据
158
+ const treeData = computed(() => {
159
+ // 如果已有API数据,则优先使用API数据
160
+ if (apiData.value) {
161
+ return apiData.value;
162
+ }
163
+
164
+ // 否则使用props传入的数据
165
+ if (Array.isArray(props.data)) {
166
+ return {
167
+ organization: props.data,
168
+ department: [],
169
+ position: [],
170
+ employee: []
171
+ };
172
+ } else {
173
+ return {
174
+ organization: props.data.organization || [],
175
+ department: props.data.department || [],
176
+ position: props.data.position || [],
177
+ employee: props.data.employee || []
178
+ };
179
+ }
180
+ });
181
+
182
+ // 过滤后的树形数据
183
+ const filteredData = computed(() => {
184
+ if (!searchText.value) {
185
+ return treeData.value;
186
+ }
187
+
188
+ return {
189
+ organization: filterTreeData(treeData.value.organization, searchText.value),
190
+ department: filterTreeData(treeData.value.department, searchText.value),
191
+ position: filterTreeData(treeData.value.position, searchText.value),
192
+ employee: filterTreeData(treeData.value.employee, searchText.value)
193
+ };
194
+ });
195
+
196
+ // 获取API数据
197
+ const fetchApiData = async () => {
198
+ loading.value = true;
199
+ try {
200
+ const response = await request.fetch({}, {}, API_URL);
201
+ console.log(response, 201);
202
+ // 处理返回数据转换
203
+ if (response && response.code === 0) {
204
+ const responseData = response.data || {};
205
+
206
+ // 转换API返回数据为组件需要的格式
207
+ apiData.value = {
208
+ organization: responseData.items || [],
209
+ department: responseData.items || [], // 可根据实际需求自定义
210
+ position: responseData.positions || [],
211
+ employee: responseData.users || []
212
+ };
213
+ } else {
214
+ throw new Error('API返回数据格式不正确');
215
+ }
216
+ } catch (error) {
217
+ console.error('获取树数据失败:', error);
218
+ MessagePlugin.error('获取数据失败,请稍后重试');
219
+
220
+ // 如果API请求失败,使用props.data作为备选数据
221
+ apiData.value = null;
222
+ } finally {
223
+ loading.value = false;
224
+ }
225
+ };
226
+
227
+ // 过滤树形数据
228
+ function filterTreeData(data, keyword) {
229
+ if (!data || !data.length) {
230
+ return [];
231
+ }
232
+
233
+ return data.filter(node => {
234
+ // 如果节点名称包含关键字,则保留该节点
235
+ if (node.label.toLowerCase().includes(keyword.toLowerCase())) {
236
+ return true;
237
+ }
238
+
239
+ // 递归过滤子节点
240
+ if (node.children && node.children.length) {
241
+ const filteredChildren = filterTreeData(node.children, keyword);
242
+ if (filteredChildren.length) {
243
+ // 复制节点并替换children
244
+ const clonedNode = { ...node, children: filteredChildren };
245
+ return true;
246
+ }
247
+ }
248
+
249
+ return false;
250
+ });
251
+ }
252
+
253
+ // 根据类型获取对应图标
254
+ const getIconForType = (type) => {
255
+ const iconMap = {
256
+ organization: 'internet',
257
+ department: 'folder',
258
+ position: 'user-talk',
259
+ employee: 'user'
260
+ };
261
+ return iconMap[type] || 'user';
262
+ };
263
+
264
+ // 显示对话框
265
+ function showDialog() {
266
+ // 获取API数据
267
+ fetchApiData();
268
+
269
+ // 重置弹窗内的预览数据
270
+ selectPreview.value = props.modelValue.map(item => ({
271
+ ...item,
272
+ type: item.type || 'employee' // 默认为员工类型
273
+ }));
274
+
275
+ // 重置选中的节点
276
+ checkedNodes.value = props.modelValue.map(item => item.value);
277
+ dialogVisible.value = true;
278
+ }
279
+
280
+ // 确认选择
281
+ function handleConfirm() {
282
+ emit('update:modelValue', selectPreview.value.map(item => ({
283
+ label: item.label,
284
+ value: item.value,
285
+ type: item.type
286
+ })));
287
+ emit('change', selectPreview.value);
288
+ dialogVisible.value = false;
289
+ }
290
+
291
+ // 取消选择
292
+ function handleCancel() {
293
+ dialogVisible.value = false;
294
+ }
295
+
296
+ // 移除已选择预览中的项目
297
+ function removePreviewItem(index) {
298
+ const removedItem = selectPreview.value[index];
299
+ selectPreview.value.splice(index, 1);
300
+ checkedNodes.value = checkedNodes.value.filter(value => value !== removedItem.value);
301
+ }
302
+
303
+ // 移除选中项
304
+ function removeItem(index) {
305
+ const newValue = [...props.modelValue];
306
+ newValue.splice(index, 1);
307
+ emit('update:modelValue', newValue);
308
+ emit('change', newValue);
309
+ }
310
+
311
+ // 获取所有树数据的扁平结构
312
+ const flattenAllTrees = computed(() => {
313
+ let result = [];
314
+ const flattenTree = (nodes, type) => {
315
+ if (!nodes) return;
316
+ nodes.forEach(node => {
317
+ result.push({
318
+ ...node,
319
+ type: node.type || type
320
+ });
321
+ if (node.children && node.children.length) {
322
+ flattenTree(node.children, type);
323
+ }
324
+ });
325
+ };
326
+
327
+ flattenTree(treeData.value.organization, 'organization');
328
+ flattenTree(treeData.value.department, 'department');
329
+ flattenTree(treeData.value.position, 'position');
330
+ flattenTree(treeData.value.employee, 'employee');
331
+
332
+ return result;
333
+ });
334
+
335
+ // 监听选中节点变化,同步到预览
336
+ watch(checkedNodes, (newValues) => {
337
+ // 提取所有树中的节点
338
+ const flatNodes = flattenAllTrees.value;
339
+
340
+ // 找到所有选中的节点
341
+ const selectedItems = newValues
342
+ .map(value => flatNodes.find(node => node.value === value))
343
+ .filter(Boolean);
344
+
345
+ // 更新预览
346
+ selectPreview.value = selectedItems;
347
+ }, { deep: true });
348
+
349
+ // 监听modelValue变化,同步到选中节点
350
+ watch(() => props.modelValue, (newValue) => {
351
+ if (!dialogVisible.value) return;
352
+
353
+ checkedNodes.value = newValue.map(item => item.value);
354
+ selectPreview.value = newValue.map(item => ({
355
+ ...item,
356
+ type: item.type || 'employee'
357
+ }));
358
+ }, { deep: true });
359
+
360
+ // 组件挂载时,预加载数据
361
+ onMounted(() => {
362
+ // 预加载不再需要,点击添加按钮时才加载
363
+ showDialog()
364
+ console.log(dialogVisible.value);
365
+ });
366
+ </script>
367
+
368
+ <style scoped>
369
+ .ebiz-tree-selector {
370
+ display: flex;
371
+ flex-wrap: wrap;
372
+ align-items: center;
373
+ gap: 8px;
374
+ min-height: 32px;
375
+ }
376
+
377
+ .selected-items {
378
+ display: flex;
379
+ flex-wrap: wrap;
380
+ gap: 4px;
381
+ }
382
+
383
+ .selected-item {
384
+ display: flex;
385
+ align-items: center;
386
+ padding: 4px 8px;
387
+ background-color: #f0f0f0;
388
+ border-radius: 4px;
389
+ font-size: 14px;
390
+ }
391
+
392
+ .item-text {
393
+ margin-right: 4px;
394
+ }
395
+
396
+ .item-remove {
397
+ cursor: pointer;
398
+ color: #999;
399
+ font-size: 16px;
400
+ }
401
+
402
+ .item-remove:hover {
403
+ color: #ff4d4f;
404
+ }
405
+
406
+ .add-button {
407
+ display: flex;
408
+ align-items: center;
409
+ }
410
+
411
+ .selector-container {
412
+ display: flex;
413
+ height: 500px;
414
+ }
415
+
416
+ .left-panel {
417
+ flex: 1;
418
+ display: flex;
419
+ flex-direction: column;
420
+ border-right: 1px solid #e0e0e0;
421
+ padding-right: 16px;
422
+ }
423
+
424
+ .right-panel {
425
+ width: 280px;
426
+ display: flex;
427
+ flex-direction: column;
428
+ padding-left: 16px;
429
+ }
430
+
431
+ .search-box {
432
+ margin-bottom: 12px;
433
+ }
434
+
435
+ .selector-tabs {
436
+ margin-bottom: 12px;
437
+ }
438
+
439
+ .tree-content {
440
+ flex: 1;
441
+ overflow: auto;
442
+ border: 1px solid #e0e0e0;
443
+ border-radius: 4px;
444
+ padding: 12px;
445
+ position: relative;
446
+ }
447
+
448
+ .loading-container {
449
+ position: absolute;
450
+ top: 0;
451
+ left: 0;
452
+ right: 0;
453
+ bottom: 0;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ background-color: rgba(255, 255, 255, 0.7);
458
+ }
459
+
460
+ .selected-title {
461
+ font-weight: bold;
462
+ margin-bottom: 8px;
463
+ }
464
+
465
+ .selected-count {
466
+ color: #999;
467
+ font-size: 12px;
468
+ margin-bottom: 12px;
469
+ }
470
+
471
+ .selected-list {
472
+ flex: 1;
473
+ overflow: auto;
474
+ border: 1px solid #e0e0e0;
475
+ border-radius: 4px;
476
+ padding: 8px;
477
+ }
478
+
479
+ .selected-list-item {
480
+ display: flex;
481
+ align-items: center;
482
+ padding: 8px;
483
+ border-bottom: 1px solid #f0f0f0;
484
+ }
485
+
486
+ .selected-list-item:last-child {
487
+ border-bottom: none;
488
+ }
489
+
490
+ .item-icon {
491
+ color: #0052d9;
492
+ margin-right: 8px;
493
+ }
494
+
495
+ .selected-label {
496
+ flex: 1;
497
+ }
498
+
499
+ .remove-icon {
500
+ color: #999;
501
+ cursor: pointer;
502
+ }
503
+
504
+ .remove-icon:hover {
505
+ color: #ff4d4f;
506
+ }
507
+
508
+ .no-selection {
509
+ display: flex;
510
+ flex-direction: column;
511
+ align-items: center;
512
+ justify-content: center;
513
+ height: 100%;
514
+ color: #999;
515
+ gap: 8px;
516
+ }
517
+ </style>