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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cd-personselector",
3
- "version": "1.3.2",
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
+ }
@@ -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
- <div v-if="isSearchMode" class="cd-ps-search-results">
28
- <div class="cd-ps-search-header">
29
- <span>搜索结果</span>
30
- <t-button size="small" variant="text" @click="clearSearch">返回</t-button>
31
- </div>
32
- <div v-if="searchLoading" class="cd-ps-loading">
33
- <t-loading /><span>搜索中...</span>
34
- </div>
35
- <div v-else-if="searchResults.length === 0" class="cd-ps-empty">
36
- <t-icon name="search" size="48px" style="color: #ddd;" /><p>未找到匹配的人员</p>
37
- </div>
38
- <div v-else class="cd-ps-result-list">
39
- <div v-for="user in searchResults" :key="user.id" class="cd-ps-result-item" :class="{ 'cd-ps-selected': selectedIds.includes(user.id) }" @click="toggleSearchResultSelect(user)">
40
- <t-checkbox :checked="selectedIds.includes(user.id)" @click.stop />
41
- <div class="cd-ps-avatar"><t-icon name="user" /></div>
42
- <div class="cd-ps-info">
43
- <div class="cd-ps-name">{{ user.displayName || user.name }}</div>
44
- <div class="cd-ps-meta">
45
- <span v-if="user.position">{{ user.position }}</span>
46
- <span v-if="user.department">{{ user.department }}</span>
47
- <span v-if="user.phone">{{ user.phone }}</span>
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
- </div>
51
- </div>
52
- </div>
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 && !node.data.children?.length && !node.data.loaded" size="small" variant="text" class="cd-ps-load-btn" @click.stop="handleLoadUsers(node, tab.key)">显示人员</t-button>
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 searchResults = ref<User[]>([]);
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 handleTabChange = () => { searchKeyword.value = ''; };
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
- emit('org-change', orgId);
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
- searchResults.value = [];
305
+ searchResultsMap.value = {};
291
306
  const callback = (users: User[]) => {
292
- searchResults.value = users.map(u => ({ ...u, isUser: true }));
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
- searchResults.value = [];
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
- addSelectedItem(user.id, { ...user, isUser: true, typeName: '搜索结果', orgId: selectedOrgId.value });
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; }