cd-personselector 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.
@@ -8,76 +8,43 @@
8
8
  @confirm="handleConfirm"
9
9
  @close="handleClose"
10
10
  >
11
- <div class="person-selector">
12
- <!-- 头部说明 -->
13
- <div v-if="tips" class="selector-tips">
11
+ <div class="cd-ps-container">
12
+ <div v-if="tips" class="cd-ps-tips">
14
13
  <t-icon name="info-circle" />
15
14
  <span>{{ tips }}</span>
16
15
  </div>
17
- <!-- 搜索区域 -->
18
- <div v-if="showSearch" class="search-area">
19
- <div v-if="organizations.length > 0" class="org-select">
20
- <t-select
21
- v-model="selectedOrgId"
22
- placeholder="选择组织"
23
- style="width: 200px"
24
- @change="handleOrgChange"
25
- >
26
- <t-option
27
- v-for="org in organizations"
28
- :key="org.id"
29
- :value="org.id"
30
- :label="org.displayName || org.name"
31
- />
16
+ <div v-if="showSearch" class="cd-ps-search">
17
+ <div v-if="organizations.length > 0" class="cd-ps-org-select">
18
+ <t-select v-model="selectedOrgId" placeholder="选择组织" style="width: 200px" @change="handleOrgChange">
19
+ <t-option v-for="org in organizations" :key="org.id" :value="org.id" :label="org.displayName || org.name" />
32
20
  </t-select>
33
21
  </div>
34
- <div class="search-input">
35
- <t-input
36
- v-model="searchKeyword"
37
- placeholder="搜索"
38
- clearable
39
- @input="handleSearchInput"
40
- @clear="clearSearch"
41
- >
42
- <template #prefix-icon>
43
- <t-icon name="search" />
44
- </template>
22
+ <div class="cd-ps-search-input">
23
+ <t-input v-model="searchKeyword" placeholder="搜索" clearable @input="handleSearchInput" @clear="clearSearch">
24
+ <template #prefix-icon><t-icon name="search" /></template>
45
25
  </t-input>
46
26
  </div>
47
27
  </div>
48
- <!-- 主内容区域 -->
49
- <div class="content-area">
50
- <!-- 左侧:Tab + 树形结构 / 搜索结果 -->
51
- <div class="left-panel tree-panel">
52
- <!-- 搜索结果模式 -->
53
- <div v-if="isSearchMode" class="search-results">
54
- <div class="search-results-header">
28
+ <div class="cd-ps-content">
29
+ <div class="cd-ps-left">
30
+ <div v-if="isSearchMode" class="cd-ps-search-results">
31
+ <div class="cd-ps-search-header">
55
32
  <span>搜索结果</span>
56
33
  <t-button size="small" variant="text" @click="clearSearch">返回</t-button>
57
34
  </div>
58
- <div v-if="searchLoading" class="search-loading">
59
- <t-loading />
60
- <span>搜索中...</span>
35
+ <div v-if="searchLoading" class="cd-ps-loading">
36
+ <t-loading /><span>搜索中...</span>
61
37
  </div>
62
- <div v-else-if="searchResults.length === 0" class="empty-search">
63
- <t-icon name="search" size="48px" style="color: #ddd;" />
64
- <p>未找到匹配的人员</p>
38
+ <div v-else-if="searchResults.length === 0" class="cd-ps-empty">
39
+ <t-icon name="search" size="48px" style="color: #ddd;" /><p>未找到匹配的人员</p>
65
40
  </div>
66
- <div v-else class="search-result-list">
67
- <div
68
- v-for="user in searchResults"
69
- :key="user.id"
70
- class="search-result-item"
71
- :class="{ 'is-selected': selectedIds.includes(user.id) }"
72
- @click="toggleSearchResultSelect(user)"
73
- >
41
+ <div v-else class="cd-ps-result-list">
42
+ <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)">
74
43
  <t-checkbox :checked="selectedIds.includes(user.id)" @click.stop />
75
- <div class="user-avatar">
76
- <t-icon name="user" />
77
- </div>
78
- <div class="user-details">
79
- <div class="user-name">{{ user.displayName || user.name }}</div>
80
- <div class="user-meta">
44
+ <div class="cd-ps-avatar"><t-icon name="user" /></div>
45
+ <div class="cd-ps-info">
46
+ <div class="cd-ps-name">{{ user.displayName || user.name }}</div>
47
+ <div class="cd-ps-meta">
81
48
  <span v-if="user.position">{{ user.position }}</span>
82
49
  <span v-if="user.department">{{ user.department }}</span>
83
50
  <span v-if="user.phone">{{ user.phone }}</span>
@@ -86,77 +53,51 @@
86
53
  </div>
87
54
  </div>
88
55
  </div>
89
- <!-- 正常 Tab 模式 -->
90
56
  <t-tabs v-else v-model="activeTab" @change="handleTabChange">
91
- <t-tab-panel
92
- v-for="tab in tabs"
93
- :key="tab.key"
94
- :value="tab.key"
95
- :label="tab.name"
96
- >
97
- <div class="tree-container">
57
+ <t-tab-panel v-for="tab in tabs" :key="tab.key" :value="tab.key" :label="tab.name">
58
+ <div class="cd-ps-tree">
98
59
  <t-tree
99
60
  :ref="(el: any) => setTreeRef(tab.key, el)"
100
61
  v-if="internalTreeData[tab.key]?.length > 0"
101
62
  :data="internalTreeData[tab.key]"
102
63
  :keys="{ value: 'id', label: 'name', children: 'children' }"
103
- hover
104
- checkable
105
- :expand-all="false"
106
- :value="selectedIds"
107
- @change="handleTreeCheck"
64
+ hover checkable :expand-all="false" :value="selectedIds" @change="handleTreeCheck"
108
65
  >
109
66
  <template #label="{ node }">
110
- <div class="tree-node-label" :class="{ 'is-user': node.data.isUser }">
67
+ <div class="cd-ps-node" :class="{ 'cd-ps-node-user': node.data.isUser }">
111
68
  <t-icon :name="node.data.isUser ? 'user' : (tab.icon || 'folder')" />
112
69
  <span>{{ node.label }}</span>
113
- <span v-if="node.data.userCount && !node.data.isUser" class="user-count">({{ node.data.userCount }})</span>
114
- <t-button
115
- v-if="!node.data.isUser && !node.data.children?.length && !node.data.loaded"
116
- size="small"
117
- variant="text"
118
- class="load-users-btn"
119
- @click.stop="handleLoadUsers(node, tab.key)"
120
- >
121
- 显示人员
122
- </t-button>
70
+ <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>
123
72
  </div>
124
73
  </template>
125
74
  </t-tree>
126
- <div v-else class="empty-tree">
127
- <t-icon :name="tab.icon || 'folder-open'" size="48px" style="color: #ddd;" />
128
- <p>暂无数据</p>
75
+ <div v-else class="cd-ps-empty">
76
+ <t-icon :name="tab.icon || 'folder-open'" size="48px" style="color: #ddd;" /><p>暂无数据</p>
129
77
  </div>
130
78
  </div>
131
79
  </t-tab-panel>
132
80
  </t-tabs>
133
81
  </div>
134
- <!-- 右侧:已选择 -->
135
- <div class="right-panel">
136
- <div class="selected-header">
137
- <span class="header-title">已选择</span>
138
- <span class="header-count">{{ selectedItems.length }} 项</span>
82
+ <div class="cd-ps-right">
83
+ <div class="cd-ps-right-header">
84
+ <span class="cd-ps-title">已选择</span>
85
+ <span class="cd-ps-num">{{ selectedItems.length }} 项</span>
139
86
  <t-button v-if="selectedItems.length > 0" size="small" variant="text" @click="clearSelection">清空</t-button>
140
87
  </div>
141
- <div class="selected-list-container">
142
- <div v-if="selectedItems.length === 0" class="empty-selected">
143
- <t-icon name="user-checked" size="64px" style="color: #ddd;" />
144
- <p>暂无选择</p>
88
+ <div class="cd-ps-right-list">
89
+ <div v-if="selectedItems.length === 0" class="cd-ps-empty">
90
+ <t-icon name="user-checked" size="64px" style="color: #ddd;" /><p>暂无选择</p>
145
91
  </div>
146
- <div v-else class="selected-user-list">
147
- <div
148
- v-for="item in selectedItems"
149
- :key="item.id"
150
- class="selected-user-item"
151
- :class="{ 'selected-dept-item': !item.isUser }"
152
- >
153
- <div class="user-info">
154
- <div class="user-avatar" :class="{ 'dept-avatar': !item.isUser }">
92
+ <div v-else class="cd-ps-selected-list">
93
+ <div v-for="item in selectedItems" :key="item.id" class="cd-ps-selected-item" :class="{ 'cd-ps-dept-item': !item.isUser }">
94
+ <div class="cd-ps-item-info">
95
+ <div class="cd-ps-avatar" :class="{ 'cd-ps-avatar-dept': !item.isUser }">
155
96
  <t-icon :name="item.isUser ? 'user' : 'folder'" />
156
97
  </div>
157
- <div class="user-details">
158
- <div class="user-name">{{ item.displayName || item.name }}</div>
159
- <div class="user-meta">
98
+ <div class="cd-ps-info">
99
+ <div class="cd-ps-name">{{ item.displayName || item.name }}</div>
100
+ <div class="cd-ps-meta">
160
101
  <span v-if="item.isUser && item.position">{{ item.position }}</span>
161
102
  <span v-if="item.isUser && item.department">{{ item.department }}</span>
162
103
  <span v-if="!item.isUser">{{ item.typeName || '部门' }}</span>
@@ -164,15 +105,8 @@
164
105
  </div>
165
106
  </div>
166
107
  </div>
167
- <t-button
168
- size="small"
169
- variant="text"
170
- shape="circle"
171
- @click="handleRemoveItem(item.id)"
172
- >
173
- <template #icon>
174
- <t-icon name="close" />
175
- </template>
108
+ <t-button size="small" variant="text" shape="circle" @click="handleRemoveItem(item.id)">
109
+ <template #icon><t-icon name="close" /></template>
176
110
  </t-button>
177
111
  </div>
178
112
  </div>
@@ -184,7 +118,6 @@
184
118
  </template>
185
119
  <script setup lang="ts">
186
120
  import { ref, computed, watch, nextTick } from 'vue';
187
- // 类型定义
188
121
  interface User {
189
122
  id: number | string;
190
123
  name: string;
@@ -210,14 +143,12 @@ interface Organization {
210
143
  name: string;
211
144
  displayName?: string;
212
145
  }
213
- // Tab 配置接口
214
146
  interface TabConfig {
215
147
  key: string;
216
148
  name: string;
217
149
  icon?: string;
218
150
  tree: TreeNode[];
219
151
  }
220
- // Props
221
152
  interface Props {
222
153
  visible: boolean;
223
154
  tabs?: TabConfig[];
@@ -236,7 +167,6 @@ const props = withDefaults(defineProps<Props>(), {
236
167
  tips: '',
237
168
  showSearch: true,
238
169
  });
239
- // Emits
240
170
  const emit = defineEmits<{
241
171
  'update:visible': [value: boolean];
242
172
  'update:modelValue': [value: (number | string)[]];
@@ -244,43 +174,31 @@ const emit = defineEmits<{
244
174
  'load-users': [params: { tabKey: string; nodeId: number | string; node: any; callback: (users: User[]) => void }];
245
175
  'search': [params: { keyword: string; orgId?: number | string; callback: (users: User[]) => void }];
246
176
  }>();
247
- // 内部状态
248
177
  const dialogVisible = ref(props.visible);
249
178
  const activeTab = ref(props.tabs?.[0]?.key || '');
250
179
  const selectedOrgId = ref<number | string | null>(null);
251
180
  const searchKeyword = ref('');
252
181
  const selectedIds = ref<(number | string)[]>([]);
253
182
  const treeRefs = ref<Record<string, any>>({});
254
- // 搜索相关状态
255
183
  const isSearchMode = ref(false);
256
184
  const searchLoading = ref(false);
257
185
  const searchResults = ref<User[]>([]);
258
- // 暴露给模板使用
259
186
  const tabs = computed(() => props.tabs || []);
260
187
  const organizations = computed(() => props.organizations || []);
261
188
  const tips = computed(() => props.tips || '');
262
189
  const showSearch = computed(() => props.showSearch);
263
190
  const dialogWidth = computed(() => props.dialogWidth);
264
- // 内部树数据(用于动态添加用户节点)
265
191
  const internalTreeData = ref<Record<string, TreeNode[]>>({});
266
- // 初始化内部树数据
267
- watch(
268
- () => props.tabs,
269
- (newTabs) => {
270
- if (newTabs && newTabs.length > 0) {
271
- const newData: Record<string, TreeNode[]> = {};
272
- newTabs.forEach((tab) => {
273
- newData[tab.key] = JSON.parse(JSON.stringify(tab.tree));
274
- });
275
- internalTreeData.value = newData;
276
- if (!activeTab.value || !newTabs.find(t => t.key === activeTab.value)) {
277
- activeTab.value = newTabs[0].key;
278
- }
192
+ watch(() => props.tabs, (newTabs) => {
193
+ if (newTabs && newTabs.length > 0) {
194
+ const newData: Record<string, TreeNode[]> = {};
195
+ newTabs.forEach((tab) => { newData[tab.key] = JSON.parse(JSON.stringify(tab.tree)); });
196
+ internalTreeData.value = newData;
197
+ if (!activeTab.value || !newTabs.find(t => t.key === activeTab.value)) {
198
+ activeTab.value = newTabs[0].key;
279
199
  }
280
- },
281
- { immediate: true, deep: true }
282
- );
283
- // 监听外部 visible 变化
200
+ }
201
+ }, { immediate: true, deep: true });
284
202
  watch(() => props.visible, (newVal) => {
285
203
  dialogVisible.value = newVal;
286
204
  if (newVal) {
@@ -290,190 +208,128 @@ watch(() => props.visible, (newVal) => {
290
208
  }
291
209
  }
292
210
  });
293
- // 监听内部 dialogVisible 变化
294
- watch(dialogVisible, (newVal) => {
295
- emit('update:visible', newVal);
296
- });
297
- // 设置树组件 ref
298
- function setTreeRef(key: string, el: any) {
299
- if (el) {
300
- treeRefs.value[key] = el;
301
- }
302
- }
303
- // 递归查找节点
211
+ watch(dialogVisible, (newVal) => { emit('update:visible', newVal); });
212
+ function setTreeRef(key: string, el: any) { if (el) treeRefs.value[key] = el; }
304
213
  function findNodeById(tree: TreeNode[], id: number | string): TreeNode | null {
305
214
  for (const node of tree) {
306
215
  if (node.id === id) return node;
307
- if (node.children) {
308
- const found = findNodeById(node.children, id);
309
- if (found) return found;
310
- }
216
+ if (node.children) { const found = findNodeById(node.children, id); if (found) return found; }
311
217
  }
312
218
  return null;
313
219
  }
314
- // 已选择的项目
315
220
  const selectedItems = computed(() => {
316
221
  const items: any[] = [];
317
222
  selectedIds.value.forEach(id => {
318
- // 先在搜索结果中查找
319
223
  const searchUser = searchResults.value.find(u => u.id === id);
320
- if (searchUser) {
321
- items.push({ ...searchUser, typeName: '搜索结果' });
322
- return;
323
- }
324
- // 在所有 tab 的树中查找
224
+ if (searchUser) { items.push({ ...searchUser, typeName: '搜索结果' }); return; }
325
225
  for (const tab of props.tabs) {
326
226
  const treeData = internalTreeData.value[tab.key] || [];
327
227
  const node = findNodeById(treeData, id);
328
- if (node) {
329
- items.push({
330
- ...node,
331
- typeName: tab.name
332
- });
333
- break;
334
- }
228
+ if (node) { items.push({ ...node, typeName: tab.name }); break; }
335
229
  }
336
230
  });
337
231
  return items;
338
232
  });
339
- // Tab 切换处理
340
- const handleTabChange = () => {
341
- searchKeyword.value = '';
342
- };
343
- // 组织变化处理
344
- const handleOrgChange = () => {
345
- // 可以触发重新加载数据
346
- };
347
- // 防抖定时器
233
+ const handleTabChange = () => { searchKeyword.value = ''; };
234
+ const handleOrgChange = () => {};
348
235
  let searchTimer: ReturnType<typeof setTimeout> | null = null;
349
- // 搜索输入处理(带防抖)
350
236
  const handleSearchInput = () => {
351
- if (searchTimer) {
352
- clearTimeout(searchTimer);
353
- }
354
- if (!searchKeyword.value.trim()) {
355
- clearSearch();
356
- return;
357
- }
358
- // 立即显示搜索模式和loading
237
+ if (searchTimer) clearTimeout(searchTimer);
238
+ if (!searchKeyword.value.trim()) { clearSearch(); return; }
359
239
  isSearchMode.value = true;
360
240
  searchLoading.value = true;
361
- // 300ms 防抖
362
- searchTimer = setTimeout(() => {
363
- doSearch();
364
- }, 300);
241
+ searchTimer = setTimeout(() => doSearch(), 300);
365
242
  };
366
- // 执行搜索
367
243
  const doSearch = () => {
368
244
  searchResults.value = [];
369
245
  const callback = (users: User[]) => {
370
246
  searchResults.value = users.map(u => ({ ...u, isUser: true }));
371
247
  searchLoading.value = false;
372
248
  };
373
- emit('search', {
374
- keyword: searchKeyword.value,
375
- orgId: selectedOrgId.value || undefined,
376
- callback
377
- });
249
+ emit('search', { keyword: searchKeyword.value, orgId: selectedOrgId.value || undefined, callback });
378
250
  };
379
- // 清除搜索,返回 tab 模式
380
251
  const clearSearch = () => {
381
- if (searchTimer) {
382
- clearTimeout(searchTimer);
383
- searchTimer = null;
384
- }
252
+ if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
385
253
  isSearchMode.value = false;
386
254
  searchKeyword.value = '';
387
255
  searchResults.value = [];
388
256
  searchLoading.value = false;
389
257
  };
390
- // 切换搜索结果中的选中状态
391
258
  const toggleSearchResultSelect = (user: User) => {
392
259
  const idx = selectedIds.value.indexOf(user.id);
393
- if (idx > -1) {
394
- selectedIds.value.splice(idx, 1);
395
- } else {
396
- selectedIds.value.push(user.id);
397
- }
260
+ if (idx > -1) selectedIds.value.splice(idx, 1);
261
+ else selectedIds.value.push(user.id);
398
262
  };
399
- // 获取当前 tab 树中所有节点的 id
400
263
  function getAllNodeIds(tree: TreeNode[]): (number | string)[] {
401
264
  const ids: (number | string)[] = [];
402
265
  const traverse = (nodes: TreeNode[]) => {
403
- for (const node of nodes) {
404
- ids.push(node.id);
405
- if (node.children) {
406
- traverse(node.children);
407
- }
408
- }
266
+ for (const node of nodes) { ids.push(node.id); if (node.children) traverse(node.children); }
409
267
  };
410
268
  traverse(tree);
411
269
  return ids;
412
270
  }
413
- // 处理树节点 checkbox 变化
414
271
  const handleTreeCheck = (checkedIds: (number | string)[]) => {
415
- // 获取当前 tab 的所有节点 id
416
272
  const currentTreeData = internalTreeData.value[activeTab.value] || [];
417
273
  const currentTreeNodeIds = getAllNodeIds(currentTreeData);
418
- // 保留不属于当前 tab 的选中项
419
274
  const otherSelectedIds = selectedIds.value.filter(id => !currentTreeNodeIds.includes(id));
420
- // 合并:其他 tab 的选中项 + 当前 tab 的选中项
421
275
  selectedIds.value = [...otherSelectedIds, ...checkedIds];
422
276
  };
423
- // 点击"显示人员"按钮
277
+ // 递归更新 internalTreeData 中的节点
278
+ function addChildrenToNode(tree: TreeNode[], parentId: number | string, children: TreeNode[]): boolean {
279
+ for (const node of tree) {
280
+ if (node.id === parentId) {
281
+ node.children = node.children || [];
282
+ node.children.push(...children);
283
+ node.loaded = true;
284
+ return true;
285
+ }
286
+ if (node.children && addChildrenToNode(node.children, parentId, children)) {
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
424
292
  async function handleLoadUsers(node: any, tabKey: string) {
425
293
  const nodeId = node.value;
426
294
  const treeRef = treeRefs.value[tabKey];
427
- // 回调函数,父组件调用此函数传入用户数据
428
295
  const callback = async (users: User[]) => {
429
296
  if (users.length > 0) {
430
297
  const userNodes = users.map((user) => {
431
298
  const { id, name, ...rest } = user;
432
- return {
433
- ...rest,
434
- id,
435
- name: user.displayName || name,
436
- isUser: true,
437
- };
299
+ return { ...rest, id, name: user.displayName || name, isUser: true };
438
300
  });
439
- // 添加到树中
301
+ // 同步更新 internalTreeData
302
+ const treeData = internalTreeData.value[tabKey];
303
+ if (treeData) {
304
+ addChildrenToNode(treeData, nodeId, userNodes);
305
+ }
306
+ // 更新 UI 树
440
307
  if (treeRef) {
441
308
  treeRef.appendTo(nodeId, userNodes);
442
309
  node.data.loaded = true;
443
310
  await nextTick();
444
- // 使用 treeRef setItem 方法展开节点
445
- try {
446
- treeRef.setItem(nodeId, { expanded: true });
447
- } catch (e) {
448
- console.log('展开节点失败', e);
449
- }
311
+ try { treeRef.setItem(nodeId, { expanded: true }); } catch { /* ignore */ }
450
312
  }
451
- } else {
313
+ } else {
452
314
  node.data.loaded = true;
315
+ // 标记节点已加载
316
+ const treeData = internalTreeData.value[tabKey];
317
+ if (treeData) {
318
+ const targetNode = findNodeById(treeData, nodeId);
319
+ if (targetNode) targetNode.loaded = true;
320
+ }
453
321
  }
454
322
  };
455
- // 触发事件,让父组件加载数据
456
323
  emit('load-users', { tabKey, nodeId, node, callback });
457
324
  }
458
- // 移除选中项
459
- const handleRemoveItem = (id: number | string) => {
460
- selectedIds.value = selectedIds.value.filter(i => i !== id);
461
- };
462
- // 清空选择
463
- const clearSelection = () => {
464
- selectedIds.value = [];
465
- };
466
- // 确认
325
+ const handleRemoveItem = (id: number | string) => { selectedIds.value = selectedIds.value.filter(i => i !== id); };
326
+ const clearSelection = () => { selectedIds.value = []; };
467
327
  const handleConfirm = () => {
468
328
  emit('update:modelValue', selectedIds.value);
469
329
  emit('confirm', selectedItems.value);
470
330
  dialogVisible.value = false;
471
331
  };
472
- // 关闭
473
- const handleClose = () => {
474
- dialogVisible.value = false;
475
- };
476
- // 暴露方法给父组件
332
+ const handleClose = () => { dialogVisible.value = false; };
477
333
  defineExpose({
478
334
  clearSelection,
479
335
  appendUsers: (tabKey: string, nodeId: number | string, users: User[]) => {
@@ -481,12 +337,7 @@ defineExpose({
481
337
  if (treeRef && users.length > 0) {
482
338
  const userNodes = users.map((user) => {
483
339
  const { id, name, ...rest } = user;
484
- return {
485
- ...rest,
486
- id,
487
- name: user.displayName || name,
488
- isUser: true,
489
- };
340
+ return { ...rest, id, name: user.displayName || name, isUser: true };
490
341
  });
491
342
  treeRef.appendTo(nodeId, userNodes);
492
343
  }
@@ -494,326 +345,55 @@ defineExpose({
494
345
  });
495
346
  </script>
496
347
  <style scoped>
497
- .person-selector {
498
- display: flex;
499
- flex-direction: column;
500
- gap: 16px;
501
- min-height: 500px;
502
- }
503
- .selector-tips {
504
- display: flex;
505
- align-items: center;
506
- gap: 8px;
507
- padding: 12px;
508
- background-color: #e8f4ff;
509
- border-radius: 4px;
510
- font-size: 13px;
511
- color: #0052d9;
512
- }
513
- .search-area {
514
- display: flex;
515
- gap: 12px;
516
- align-items: center;
517
- }
518
- .search-area .org-select {
519
- flex-shrink: 0;
520
- }
521
- .search-area .search-input {
522
- flex: 1;
523
- }
524
- .content-area {
525
- display: flex;
526
- gap: 0;
527
- height: 450px;
528
- border: 1px solid #dfe1e6;
529
- border-radius: 4px;
530
- overflow: hidden;
531
- }
532
- .tree-panel {
533
- width: 480px;
534
- flex-shrink: 0;
535
- border-right: 1px solid #dfe1e6;
536
- display: flex;
537
- flex-direction: column;
538
- background-color: #fafafa;
539
- }
540
- .tree-panel :deep(.t-tabs__nav-container) {
541
- padding: 0 8px;
542
- background-color: #fff;
543
- }
544
- .tree-panel :deep(.t-tabs__content) {
545
- flex: 1;
546
- overflow: hidden;
547
- }
548
- .tree-container {
549
- height: 100%;
550
- overflow-y: auto;
551
- padding: 8px;
552
- }
553
- .tree-container::-webkit-scrollbar {
554
- width: 6px;
555
- }
556
- .tree-container::-webkit-scrollbar-track {
557
- background: #f1f1f1;
558
- }
559
- .tree-container::-webkit-scrollbar-thumb {
560
- background: #c1c1c1;
561
- border-radius: 3px;
562
- }
563
- .tree-node-label {
564
- display: flex;
565
- align-items: center;
566
- gap: 6px;
567
- font-size: 14px;
568
- width: 100%;
569
- }
570
- :deep(.t-tree__label) {
571
- flex: 1;
572
- }
573
- :deep(.t-checkbox__label) {
574
- width: 100%;
575
- }
576
- .tree-node-label.is-user {
577
- font-size: 13px;
578
- color: #666;
579
- }
580
- .tree-node-label .user-count {
581
- color: #999;
582
- font-size: 12px;
583
- }
584
- .tree-node-label .load-users-btn {
585
- margin-left: auto;
586
- color: #0052d9;
587
- font-size: 12px;
588
- flex-shrink: 0;
589
- }
590
- .empty-tree {
591
- display: flex;
592
- flex-direction: column;
593
- align-items: center;
594
- justify-content: center;
595
- padding: 60px 20px;
596
- color: #999;
597
- }
598
- .empty-tree p {
599
- margin-top: 16px;
600
- font-size: 14px;
601
- }
602
- /* 搜索结果样式 */
603
- .search-results {
604
- display: flex;
605
- flex-direction: column;
606
- height: 100%;
607
- }
608
- .search-results-header {
609
- display: flex;
610
- justify-content: space-between;
611
- align-items: center;
612
- padding: 12px 16px;
613
- background: #fff;
614
- border-bottom: 1px solid #dfe1e6;
615
- font-weight: 500;
616
- }
617
- .search-loading {
618
- display: flex;
619
- flex-direction: column;
620
- align-items: center;
621
- justify-content: center;
622
- padding: 60px 20px;
623
- gap: 12px;
624
- color: #999;
625
- }
626
- .empty-search {
627
- display: flex;
628
- flex-direction: column;
629
- align-items: center;
630
- justify-content: center;
631
- padding: 60px 20px;
632
- color: #999;
633
- }
634
- .empty-search p {
635
- margin-top: 16px;
636
- font-size: 14px;
637
- }
638
- .search-result-list {
639
- flex: 1;
640
- overflow-y: auto;
641
- padding: 8px;
642
- }
643
- .search-result-item {
644
- display: flex;
645
- align-items: center;
646
- gap: 10px;
647
- padding: 10px 12px;
648
- background: #fff;
649
- border-radius: 4px;
650
- border: 1px solid #e5e7eb;
651
- margin-bottom: 8px;
652
- cursor: pointer;
653
- transition: all 0.2s;
654
- }
655
- .search-result-item:hover {
656
- border-color: #0052d9;
657
- background: #f0f5ff;
658
- }
659
- .search-result-item.is-selected {
660
- border-color: #0052d9;
661
- background: #e8f4ff;
662
- }
663
- .search-result-item .user-avatar {
664
- width: 36px;
665
- height: 36px;
666
- border-radius: 50%;
667
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
668
- display: flex;
669
- align-items: center;
670
- justify-content: center;
671
- color: #fff;
672
- flex-shrink: 0;
673
- }
674
- .search-result-item .user-details {
675
- flex: 1;
676
- min-width: 0;
677
- }
678
- .search-result-item .user-name {
679
- font-size: 14px;
680
- font-weight: 500;
681
- color: #333;
682
- margin-bottom: 4px;
683
- }
684
- .search-result-item .user-meta {
685
- display: flex;
686
- flex-wrap: wrap;
687
- gap: 6px;
688
- font-size: 12px;
689
- color: #999;
690
- }
691
- .search-result-item .user-meta span::before {
692
- content: '•';
693
- margin-right: 4px;
694
- }
695
- .search-result-item .user-meta span:first-child::before {
696
- display: none;
697
- }
698
- .right-panel {
699
- flex: 1;
700
- display: flex;
701
- flex-direction: column;
702
- overflow: hidden;
703
- background-color: #fafafa;
704
- }
705
- .selected-header {
706
- display: flex;
707
- align-items: center;
708
- padding: 12px 16px;
709
- border-bottom: 1px solid #dfe1e6;
710
- background-color: #fff;
711
- }
712
- .selected-header .header-title {
713
- font-weight: 500;
714
- font-size: 14px;
715
- color: #333;
716
- }
717
- .selected-header .header-count {
718
- font-size: 13px;
719
- color: #0052d9;
720
- font-weight: 600;
721
- margin-left: 8px;
722
- flex: 1;
723
- }
724
- .selected-list-container {
725
- flex: 1;
726
- overflow-y: auto;
727
- padding: 8px;
728
- }
729
- .selected-list-container::-webkit-scrollbar {
730
- width: 6px;
731
- }
732
- .selected-list-container::-webkit-scrollbar-track {
733
- background: #f1f1f1;
734
- }
735
- .selected-list-container::-webkit-scrollbar-thumb {
736
- background: #c1c1c1;
737
- border-radius: 3px;
738
- }
739
- .empty-selected {
740
- display: flex;
741
- flex-direction: column;
742
- align-items: center;
743
- justify-content: center;
744
- padding: 60px 20px;
745
- color: #999;
746
- }
747
- .empty-selected p {
748
- margin-top: 16px;
749
- font-size: 14px;
750
- }
751
- .selected-user-list {
752
- display: flex;
753
- flex-direction: column;
754
- gap: 8px;
755
- }
756
- .selected-user-item {
757
- display: flex;
758
- align-items: center;
759
- justify-content: space-between;
760
- padding: 10px 12px;
761
- background: #fff;
762
- border-radius: 4px;
763
- border: 1px solid #e5e7eb;
764
- transition: all 0.2s;
765
- }
766
- .selected-user-item:hover {
767
- border-color: #0052d9;
768
- box-shadow: 0 2px 4px rgba(0, 82, 217, 0.1);
769
- }
770
- .selected-user-item .user-info {
771
- flex: 1;
772
- min-width: 0;
773
- display: flex;
774
- align-items: center;
775
- gap: 10px;
776
- }
777
- .user-avatar {
778
- width: 32px;
779
- height: 32px;
780
- border-radius: 50%;
781
- background: #667eea;
782
- display: flex;
783
- align-items: center;
784
- justify-content: center;
785
- color: #fff;
786
- font-size: 16px;
787
- flex-shrink: 0;
788
- }
789
- .dept-avatar {
790
- background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
791
- }
792
- .selected-dept-item {
793
- border-color: #f5af19;
794
- }
795
- .selected-user-item .user-details {
796
- flex: 1;
797
- min-width: 0;
798
- }
799
- .selected-user-item .user-name {
800
- font-size: 13px;
801
- font-weight: 500;
802
- color: #333;
803
- margin-bottom: 4px;
804
- }
805
- .selected-user-item .user-meta {
806
- display: flex;
807
- flex-wrap: wrap;
808
- gap: 6px;
809
- font-size: 11px;
810
- color: #999;
811
- }
812
- .selected-user-item .user-meta span::before {
813
- content: '•';
814
- margin-right: 4px;
815
- }
816
- .selected-user-item .user-meta span:first-child::before {
817
- display: none;
818
- }
348
+ .cd-ps-container { display: flex; flex-direction: column; gap: 16px; min-height: 500px; }
349
+ .cd-ps-tips { display: flex; align-items: center; gap: 8px; padding: 12px; background-color: #e8f4ff; border-radius: 4px; font-size: 13px; color: #0052d9; }
350
+ .cd-ps-search { display: flex; gap: 12px; align-items: center; }
351
+ .cd-ps-org-select { flex-shrink: 0; }
352
+ .cd-ps-search-input { flex: 1; }
353
+ .cd-ps-content { display: flex; gap: 0; height: 450px; border: 1px solid #dfe1e6; border-radius: 4px; overflow: hidden; }
354
+ .cd-ps-left { width: 480px; flex-shrink: 0; border-right: 1px solid #dfe1e6; display: flex; flex-direction: column; background-color: #fafafa; }
355
+ .cd-ps-left :deep(.t-tabs__nav-container) { padding: 0 8px; background-color: #fff; }
356
+ .cd-ps-left :deep(.t-tabs__content) { flex: 1; overflow: hidden; }
357
+ .cd-ps-tree { height: 100%; overflow-y: auto; padding: 8px; }
358
+ .cd-ps-tree::-webkit-scrollbar { width: 6px; }
359
+ .cd-ps-tree::-webkit-scrollbar-track { background: #f1f1f1; }
360
+ .cd-ps-tree::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
361
+ .cd-ps-node { display: flex; align-items: center; gap: 6px; font-size: 14px; width: 100%; }
362
+ :deep(.t-tree__label) { flex: 1; }
363
+ :deep(.t-checkbox__label) { width: 100%; }
364
+ .cd-ps-node-user { font-size: 13px; color: #666; }
365
+ .cd-ps-count { color: #999; font-size: 12px; }
366
+ .cd-ps-load-btn { margin-left: auto; color: #0052d9; font-size: 12px; flex-shrink: 0; }
367
+ .cd-ps-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: #999; }
368
+ .cd-ps-empty p { margin-top: 16px; font-size: 14px; }
369
+ .cd-ps-search-results { display: flex; flex-direction: column; height: 100%; }
370
+ .cd-ps-search-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: #fff; border-bottom: 1px solid #dfe1e6; font-weight: 500; }
371
+ .cd-ps-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; gap: 12px; color: #999; }
372
+ .cd-ps-result-list { flex: 1; overflow-y: auto; padding: 8px; }
373
+ .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; }
374
+ .cd-ps-result-item:hover { border-color: #0052d9; background: #f0f5ff; }
375
+ .cd-ps-result-item.cd-ps-selected { border-color: #0052d9; background: #e8f4ff; }
376
+ .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; }
377
+ .cd-ps-result-item .cd-ps-avatar { width: 36px; height: 36px; }
378
+ .cd-ps-avatar-dept { background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); }
379
+ .cd-ps-info { flex: 1; min-width: 0; }
380
+ .cd-ps-name { font-size: 13px; font-weight: 500; color: #333; margin-bottom: 4px; }
381
+ .cd-ps-result-item .cd-ps-name { font-size: 14px; }
382
+ .cd-ps-meta { display: flex; flex-wrap: wrap; gap: 6px; font-size: 11px; color: #999; }
383
+ .cd-ps-result-item .cd-ps-meta { font-size: 12px; }
384
+ .cd-ps-meta span::before { content: '•'; margin-right: 4px; }
385
+ .cd-ps-meta span:first-child::before { display: none; }
386
+ .cd-ps-right { flex: 1; display: flex; flex-direction: column; overflow: hidden; background-color: #fafafa; }
387
+ .cd-ps-right-header { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid #dfe1e6; background-color: #fff; }
388
+ .cd-ps-title { font-weight: 500; font-size: 14px; color: #333; }
389
+ .cd-ps-num { font-size: 13px; color: #0052d9; font-weight: 600; margin-left: 8px; flex: 1; }
390
+ .cd-ps-right-list { flex: 1; overflow-y: auto; padding: 8px; }
391
+ .cd-ps-right-list::-webkit-scrollbar { width: 6px; }
392
+ .cd-ps-right-list::-webkit-scrollbar-track { background: #f1f1f1; }
393
+ .cd-ps-right-list::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
394
+ .cd-ps-selected-list { display: flex; flex-direction: column; gap: 8px; }
395
+ .cd-ps-selected-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; background: #fff; border-radius: 4px; border: 1px solid #e5e7eb; transition: all 0.2s; }
396
+ .cd-ps-selected-item:hover { border-color: #0052d9; box-shadow: 0 2px 4px rgba(0, 82, 217, 0.1); }
397
+ .cd-ps-dept-item { border-color: #f5af19; }
398
+ .cd-ps-item-info { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
819
399
  </style>