cd-personselector 1.3.2 → 1.3.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/dist/index.js +1 -1
- package/dist/index.mjs +428 -400
- package/dist/src/PersonSelector.vue.d.ts +16 -2
- package/dist/style.css +1 -1
- package/package.json +7 -6
- package/src/PersonSelector.vue +87 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cd-personselector",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "人员选择器组件 - 支持多Tab、树形结构、搜索、懒加载、输入框选择模式",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"dist",
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "vite",
|
|
24
|
+
"build": "vite build",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
22
27
|
"keywords": [
|
|
23
28
|
"vue",
|
|
24
29
|
"vue3",
|
|
@@ -46,9 +51,5 @@
|
|
|
46
51
|
},
|
|
47
52
|
"dependencies": {
|
|
48
53
|
"remixicon": "^4.8.0"
|
|
49
|
-
},
|
|
50
|
-
"scripts": {
|
|
51
|
-
"dev": "vite",
|
|
52
|
-
"build": "vite build"
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
|
+
}
|
package/src/PersonSelector.vue
CHANGED
|
@@ -17,42 +17,44 @@
|
|
|
17
17
|
</t-select>
|
|
18
18
|
</div>
|
|
19
19
|
<div class="cd-ps-search-input">
|
|
20
|
-
<t-input v-model="searchKeyword" placeholder="输入手机号/工号/姓名/部门/职位搜索" clearable @input="handleSearchInput" @clear="clearSearch">
|
|
20
|
+
<t-input v-model="searchKeyword" placeholder="输入手机号/工号/姓名/部门/职位搜索" clearable :inputProps="{ autocomplete: 'off' }" @input="handleSearchInput" @clear="clearSearch">
|
|
21
21
|
<template #prefix-icon><t-icon name="search" /></template>
|
|
22
22
|
</t-input>
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="cd-ps-content">
|
|
26
26
|
<div class="cd-ps-left">
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
<t-tabs v-model="activeTab" @change="handleTabChange">
|
|
28
|
+
<t-tab-panel v-for="tab in tabs" :key="tab.key" :value="tab.key" :label="getTabLabel(tab)">
|
|
29
|
+
<!-- 搜索模式 -->
|
|
30
|
+
<template v-if="isSearchMode">
|
|
31
|
+
<div v-if="searchLoading" class="cd-ps-loading">
|
|
32
|
+
<t-loading /><span>搜索中...</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div v-else-if="!searchResultsMap[tab.key] || searchResultsMap[tab.key].length === 0" class="cd-ps-empty">
|
|
35
|
+
<t-icon name="search" size="48px" style="color: #ddd;" /><p>暂无结果</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div v-else class="cd-ps-result-list">
|
|
38
|
+
<div v-for="user in searchResultsMap[tab.key]" :key="user.id" class="cd-ps-result-item" :class="{ 'cd-ps-selected': selectedIds.includes(user.id), 'cd-ps-dept-result': !user.isUser }" @click="toggleSearchResultSelect(user)">
|
|
39
|
+
<t-checkbox :checked="selectedIds.includes(user.id)" @click.stop />
|
|
40
|
+
<div class="cd-ps-avatar" :class="{ 'cd-ps-avatar-dept': !user.isUser }">
|
|
41
|
+
<t-icon :name="user.isUser ? 'user' : (user.isPost ? 'assignment-checked' : 'folder')" />
|
|
42
|
+
</div>
|
|
43
|
+
<div class="cd-ps-info">
|
|
44
|
+
<div class="cd-ps-name">{{ user.displayName || user.name }}</div>
|
|
45
|
+
<div class="cd-ps-meta">
|
|
46
|
+
<span v-if="!user.isUser" class="cd-ps-type-tag">{{ user.isPost ? '职位' : '部门' }}</span>
|
|
47
|
+
<span v-if="user.position">{{ user.position }}</span>
|
|
48
|
+
<span v-if="user.department">{{ user.department }}</span>
|
|
49
|
+
<span v-if="user.phone">{{ user.phone }}</span>
|
|
50
|
+
<span v-if="user.userCount">{{ user.userCount }}人</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
48
53
|
</div>
|
|
49
54
|
</div>
|
|
50
|
-
</
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<t-tabs v-else v-model="activeTab" @change="handleTabChange">
|
|
54
|
-
<t-tab-panel v-for="tab in tabs" :key="tab.key" :value="tab.key" :label="tab.name">
|
|
55
|
-
<div class="cd-ps-tree">
|
|
55
|
+
</template>
|
|
56
|
+
<!-- 非搜索模式:树 -->
|
|
57
|
+
<div v-else class="cd-ps-tree">
|
|
56
58
|
<t-tree
|
|
57
59
|
:ref="(el: any) => setTreeRef(tab.key, el)"
|
|
58
60
|
v-if="internalTreeData[tab.key]?.length > 0"
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
<span v-if="node.data.isUser && node.data.position" class="cd-ps-position">{{ node.data.position }}</span>
|
|
69
71
|
</span>
|
|
70
72
|
<span v-if="node.data.userCount && !node.data.isUser" class="cd-ps-count">({{ node.data.userCount }})</span>
|
|
71
|
-
<t-button v-if="!node.data.isUser
|
|
73
|
+
<t-button v-if="!node.data.isUser" size="small" variant="text" class="cd-ps-load-btn" @click.stop="handleLoadUsers(node, tab.key)">{{ node.data.loaded ? '刷新人员' : '显示人员' }}</t-button>
|
|
72
74
|
</div>
|
|
73
75
|
</template>
|
|
74
76
|
</t-tree>
|
|
@@ -177,7 +179,8 @@ const emit = defineEmits<{
|
|
|
177
179
|
'confirm': [items: any[]];
|
|
178
180
|
'load-users': [params: { tabKey: string; nodeId: number | string; node: any; callback: (users: User[]) => void }];
|
|
179
181
|
'search': [params: { keyword: string; orgId?: number | string; callback: (users: User[]) => void }];
|
|
180
|
-
'org-change': [orgId: number | string];
|
|
182
|
+
'org-change': [params: { orgId: number | string; tabKey: string; callback?: () => void }];
|
|
183
|
+
'tab-change': [params: { tabKey: string }];
|
|
181
184
|
}>();
|
|
182
185
|
const dialogVisible = ref(props.visible);
|
|
183
186
|
const activeTab = ref(props.tabs?.[0]?.key || '');
|
|
@@ -188,7 +191,7 @@ const selectedItemsMap = ref<Map<number | string, any>>(new Map()); // 缓存已
|
|
|
188
191
|
const treeRefs = ref<Record<string, any>>({});
|
|
189
192
|
const isSearchMode = ref(false);
|
|
190
193
|
const searchLoading = ref(false);
|
|
191
|
-
const
|
|
194
|
+
const searchResultsMap = ref<Record<string, User[]>>({});
|
|
192
195
|
const tabs = computed(() => props.tabs || []);
|
|
193
196
|
const organizations = computed(() => props.organizations || []);
|
|
194
197
|
const tips = computed(() => props.tips || '');
|
|
@@ -269,14 +272,26 @@ function removeSelectedItem(id: number | string) {
|
|
|
269
272
|
selectedIds.value = selectedIds.value.filter(i => i !== id);
|
|
270
273
|
selectedItemsMap.value.delete(id);
|
|
271
274
|
}
|
|
272
|
-
const
|
|
275
|
+
const getTabLabel = (tab: TabConfig) => {
|
|
276
|
+
if (!isSearchMode.value) return tab.name;
|
|
277
|
+
const count = searchResultsMap.value[tab.key]?.length ?? 0;
|
|
278
|
+
return count > 0 ? `${tab.name} (${count})` : tab.name;
|
|
279
|
+
};
|
|
280
|
+
const handleTabChange = (tabKey: string) => {
|
|
281
|
+
if (isSearchMode.value) return;
|
|
282
|
+
searchKeyword.value = '';
|
|
283
|
+
emit('tab-change', { tabKey });
|
|
284
|
+
};
|
|
273
285
|
const handleOrgChange = (orgId: number | string) => {
|
|
274
286
|
// 清空左侧树数据,等待父组件重新传入
|
|
275
287
|
const newData: Record<string, TreeNode[]> = {};
|
|
276
288
|
props.tabs.forEach((tab) => { newData[tab.key] = []; });
|
|
277
289
|
internalTreeData.value = newData;
|
|
278
290
|
// 触发事件,让父组件重新加载数据
|
|
279
|
-
|
|
291
|
+
const callback = () => {
|
|
292
|
+
// 父组件更新完成后的回调,可以在这里做一些后续处理
|
|
293
|
+
};
|
|
294
|
+
emit('org-change', { orgId, tabKey: activeTab.value, callback });
|
|
280
295
|
};
|
|
281
296
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
282
297
|
const handleSearchInput = () => {
|
|
@@ -287,9 +302,16 @@ const handleSearchInput = () => {
|
|
|
287
302
|
searchTimer = setTimeout(() => doSearch(), 300);
|
|
288
303
|
};
|
|
289
304
|
const doSearch = () => {
|
|
290
|
-
|
|
305
|
+
searchResultsMap.value = {};
|
|
291
306
|
const callback = (users: User[]) => {
|
|
292
|
-
|
|
307
|
+
const map: Record<string, User[]> = {};
|
|
308
|
+
users.forEach(u => {
|
|
309
|
+
const normalized = { ...u, isUser: u.isUser !== false };
|
|
310
|
+
const key = u.nodeType || (u.isUser !== false ? 'user' : 'department');
|
|
311
|
+
if (!map[key]) map[key] = [];
|
|
312
|
+
map[key].push(normalized);
|
|
313
|
+
});
|
|
314
|
+
searchResultsMap.value = map;
|
|
293
315
|
searchLoading.value = false;
|
|
294
316
|
};
|
|
295
317
|
emit('search', { keyword: searchKeyword.value, orgId: selectedOrgId.value || undefined, callback });
|
|
@@ -298,7 +320,7 @@ const clearSearch = () => {
|
|
|
298
320
|
if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
|
|
299
321
|
isSearchMode.value = false;
|
|
300
322
|
searchKeyword.value = '';
|
|
301
|
-
|
|
323
|
+
searchResultsMap.value = {};
|
|
302
324
|
searchLoading.value = false;
|
|
303
325
|
};
|
|
304
326
|
const toggleSearchResultSelect = (user: User) => {
|
|
@@ -310,7 +332,8 @@ const toggleSearchResultSelect = (user: User) => {
|
|
|
310
332
|
selectedIds.value = [];
|
|
311
333
|
selectedItemsMap.value.clear();
|
|
312
334
|
}
|
|
313
|
-
|
|
335
|
+
const typeName = !user.isUser ? (user.isPost ? '职位' : '部门') : '搜索结果';
|
|
336
|
+
addSelectedItem(user.id, { ...user, typeName, orgId: selectedOrgId.value });
|
|
314
337
|
if (!props.multiple) {
|
|
315
338
|
handleConfirm();
|
|
316
339
|
}
|
|
@@ -370,6 +393,23 @@ function addChildrenToNode(tree: TreeNode[], parentId: number | string, children
|
|
|
370
393
|
async function handleLoadUsers(node: any, tabKey: string) {
|
|
371
394
|
const nodeId = node.value;
|
|
372
395
|
const treeRef = treeRefs.value[tabKey];
|
|
396
|
+
// 如果已加载过,先移除旧的用户子节点
|
|
397
|
+
if (node.data.loaded) {
|
|
398
|
+
const treeData = internalTreeData.value[tabKey];
|
|
399
|
+
if (treeData) {
|
|
400
|
+
const targetNode = findNodeById(treeData, nodeId);
|
|
401
|
+
if (targetNode && targetNode.children) {
|
|
402
|
+
targetNode.children = targetNode.children.filter((c: any) => !c.isUser);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (treeRef) {
|
|
406
|
+
const children = treeRef.getItem(nodeId)?.children || [];
|
|
407
|
+
children.filter((c: any) => c.data?.isUser).forEach((c: any) => {
|
|
408
|
+
try { treeRef.remove(c.value); } catch { /* ignore */ }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
node.data.loaded = false;
|
|
412
|
+
}
|
|
373
413
|
const callback = async (users: User[]) => {
|
|
374
414
|
if (users.length > 0) {
|
|
375
415
|
const userNodes = users.map((user) => {
|
|
@@ -428,9 +468,12 @@ defineExpose({
|
|
|
428
468
|
.cd-ps-org-select { flex-shrink: 0; }
|
|
429
469
|
.cd-ps-search-input { flex: 1; }
|
|
430
470
|
.cd-ps-content { display: flex; gap: 0; height: 450px; border: 1px solid #dfe1e6; border-radius: 4px; overflow: hidden; }
|
|
431
|
-
.cd-ps-left { width: 480px; flex-shrink: 0; border-right: 1px solid #dfe1e6; display: flex; flex-direction: column; background-color: #fafafa; }
|
|
471
|
+
.cd-ps-left { width: 480px; flex-shrink: 0; border-right: 1px solid #dfe1e6; display: flex; flex-direction: column; background-color: #fafafa; min-height: 0; overflow: hidden; }
|
|
472
|
+
.cd-ps-left :deep(.t-tabs) { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
473
|
+
.cd-ps-left :deep(.t-tabs__header) { flex-shrink: 0; }
|
|
432
474
|
.cd-ps-left :deep(.t-tabs__nav-container) { padding: 0 8px; background-color: #fff; }
|
|
433
|
-
.cd-ps-left :deep(.t-tabs__content) { flex: 1; overflow: hidden; }
|
|
475
|
+
.cd-ps-left :deep(.t-tabs__content) { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
476
|
+
.cd-ps-left :deep(.t-tab-panel) { flex: 1; min-height: 0; overflow: hidden; }
|
|
434
477
|
.cd-ps-tree { height: 100%; overflow-y: auto; padding: 8px; }
|
|
435
478
|
.cd-ps-tree::-webkit-scrollbar { width: 6px; }
|
|
436
479
|
.cd-ps-tree::-webkit-scrollbar-track { background: #f1f1f1; }
|
|
@@ -452,6 +495,9 @@ defineExpose({
|
|
|
452
495
|
.cd-ps-result-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: #fff; border-radius: 4px; border: 1px solid #e5e7eb; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
|
|
453
496
|
.cd-ps-result-item:hover { border-color: #0052d9; background: #f0f5ff; }
|
|
454
497
|
.cd-ps-result-item.cd-ps-selected { border-color: #0052d9; background: #e8f4ff; }
|
|
498
|
+
.cd-ps-dept-result { border-color: #f5af19; }
|
|
499
|
+
.cd-ps-dept-result:hover { border-color: #f5af19; background: #fffbe6; }
|
|
500
|
+
.cd-ps-dept-result.cd-ps-selected { border-color: #f5af19; background: #fff7e6; }
|
|
455
501
|
.cd-ps-avatar { width: 32px; height: 32px; border-radius: 50%; background: #667eea; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; flex-shrink: 0; }
|
|
456
502
|
.cd-ps-result-item .cd-ps-avatar { width: 36px; height: 36px; }
|
|
457
503
|
.cd-ps-avatar-dept { background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); }
|
|
@@ -462,6 +508,8 @@ defineExpose({
|
|
|
462
508
|
.cd-ps-result-item .cd-ps-meta { font-size: 12px; }
|
|
463
509
|
.cd-ps-meta span::before { content: '•'; margin-right: 4px; }
|
|
464
510
|
.cd-ps-meta span:first-child::before { display: none; }
|
|
511
|
+
.cd-ps-type-tag { color: #f5af19; font-weight: 500; }
|
|
512
|
+
.cd-ps-type-tag::before { display: none !important; }
|
|
465
513
|
.cd-ps-right { flex: 1; display: flex; flex-direction: column; overflow: hidden; background-color: #fafafa; }
|
|
466
514
|
.cd-ps-right-header { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid #dfe1e6; background-color: #fff; }
|
|
467
515
|
.cd-ps-title { font-weight: 500; font-size: 14px; color: #333; }
|