cd-personselector 1.0.0

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.
@@ -0,0 +1,819 @@
1
+ <template>
2
+ <t-dialog
3
+ v-model:visible="dialogVisible"
4
+ header="选择人员"
5
+ :width="dialogWidth"
6
+ :footer="true"
7
+ destroy-on-close
8
+ @confirm="handleConfirm"
9
+ @close="handleClose"
10
+ >
11
+ <div class="person-selector">
12
+ <!-- 头部说明 -->
13
+ <div v-if="tips" class="selector-tips">
14
+ <t-icon name="info-circle" />
15
+ <span>{{ tips }}</span>
16
+ </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
+ />
32
+ </t-select>
33
+ </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>
45
+ </t-input>
46
+ </div>
47
+ </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">
55
+ <span>搜索结果</span>
56
+ <t-button size="small" variant="text" @click="clearSearch">返回</t-button>
57
+ </div>
58
+ <div v-if="searchLoading" class="search-loading">
59
+ <t-loading />
60
+ <span>搜索中...</span>
61
+ </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>
65
+ </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
+ >
74
+ <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">
81
+ <span v-if="user.position">{{ user.position }}</span>
82
+ <span v-if="user.department">{{ user.department }}</span>
83
+ <span v-if="user.phone">{{ user.phone }}</span>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <!-- 正常 Tab 模式 -->
90
+ <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">
98
+ <t-tree
99
+ :ref="(el: any) => setTreeRef(tab.key, el)"
100
+ v-if="internalTreeData[tab.key]?.length > 0"
101
+ :data="internalTreeData[tab.key]"
102
+ :keys="{ value: 'id', label: 'name', children: 'children' }"
103
+ hover
104
+ checkable
105
+ :expand-all="false"
106
+ :value="selectedIds"
107
+ @change="handleTreeCheck"
108
+ >
109
+ <template #label="{ node }">
110
+ <div class="tree-node-label" :class="{ 'is-user': node.data.isUser }">
111
+ <t-icon :name="node.data.isUser ? 'user' : (tab.icon || 'folder')" />
112
+ <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>
123
+ </div>
124
+ </template>
125
+ </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>
129
+ </div>
130
+ </div>
131
+ </t-tab-panel>
132
+ </t-tabs>
133
+ </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>
139
+ <t-button v-if="selectedItems.length > 0" size="small" variant="text" @click="clearSelection">清空</t-button>
140
+ </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>
145
+ </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 }">
155
+ <t-icon :name="item.isUser ? 'user' : 'folder'" />
156
+ </div>
157
+ <div class="user-details">
158
+ <div class="user-name">{{ item.displayName || item.name }}</div>
159
+ <div class="user-meta">
160
+ <span v-if="item.isUser && item.position">{{ item.position }}</span>
161
+ <span v-if="item.isUser && item.department">{{ item.department }}</span>
162
+ <span v-if="!item.isUser">{{ item.typeName || '部门' }}</span>
163
+ <span v-if="!item.isUser && item.userCount">{{ item.userCount }}人</span>
164
+ </div>
165
+ </div>
166
+ </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>
176
+ </t-button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </t-dialog>
184
+ </template>
185
+ <script setup lang="ts">
186
+ import { ref, computed, watch, nextTick } from 'vue';
187
+ // 类型定义
188
+ interface User {
189
+ id: number | string;
190
+ name: string;
191
+ displayName?: string;
192
+ phone?: string;
193
+ email?: string;
194
+ position?: string;
195
+ department?: string;
196
+ isUser?: boolean;
197
+ [key: string]: any;
198
+ }
199
+ interface TreeNode {
200
+ id: number | string;
201
+ name: string;
202
+ children?: TreeNode[];
203
+ userCount?: number;
204
+ isUser?: boolean;
205
+ loaded?: boolean;
206
+ [key: string]: any;
207
+ }
208
+ interface Organization {
209
+ id: number | string;
210
+ name: string;
211
+ displayName?: string;
212
+ }
213
+ // Tab 配置接口
214
+ interface TabConfig {
215
+ key: string;
216
+ name: string;
217
+ icon?: string;
218
+ tree: TreeNode[];
219
+ }
220
+ // Props
221
+ interface Props {
222
+ visible: boolean;
223
+ tabs?: TabConfig[];
224
+ organizations?: Organization[];
225
+ modelValue?: (number | string)[];
226
+ dialogWidth?: string;
227
+ tips?: string;
228
+ showSearch?: boolean;
229
+ }
230
+ const props = withDefaults(defineProps<Props>(), {
231
+ visible: false,
232
+ tabs: () => [],
233
+ organizations: () => [],
234
+ modelValue: () => [],
235
+ dialogWidth: '900px',
236
+ tips: '',
237
+ showSearch: true,
238
+ });
239
+ // Emits
240
+ const emit = defineEmits<{
241
+ 'update:visible': [value: boolean];
242
+ 'update:modelValue': [value: (number | string)[]];
243
+ 'confirm': [items: any[]];
244
+ 'load-users': [params: { tabKey: string; nodeId: number | string; node: any; callback: (users: User[]) => void }];
245
+ 'search': [params: { keyword: string; orgId?: number | string; callback: (users: User[]) => void }];
246
+ }>();
247
+ // 内部状态
248
+ const dialogVisible = ref(props.visible);
249
+ const activeTab = ref(props.tabs?.[0]?.key || '');
250
+ const selectedOrgId = ref<number | string | null>(null);
251
+ const searchKeyword = ref('');
252
+ const selectedIds = ref<(number | string)[]>([]);
253
+ const treeRefs = ref<Record<string, any>>({});
254
+ // 搜索相关状态
255
+ const isSearchMode = ref(false);
256
+ const searchLoading = ref(false);
257
+ const searchResults = ref<User[]>([]);
258
+ // 暴露给模板使用
259
+ const tabs = computed(() => props.tabs || []);
260
+ const organizations = computed(() => props.organizations || []);
261
+ const tips = computed(() => props.tips || '');
262
+ const showSearch = computed(() => props.showSearch);
263
+ const dialogWidth = computed(() => props.dialogWidth);
264
+ // 内部树数据(用于动态添加用户节点)
265
+ 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
+ }
279
+ }
280
+ },
281
+ { immediate: true, deep: true }
282
+ );
283
+ // 监听外部 visible 变化
284
+ watch(() => props.visible, (newVal) => {
285
+ dialogVisible.value = newVal;
286
+ if (newVal) {
287
+ selectedIds.value = props.modelValue ? [...props.modelValue] : [];
288
+ if (props.organizations.length > 0 && !selectedOrgId.value) {
289
+ selectedOrgId.value = props.organizations[0].id;
290
+ }
291
+ }
292
+ });
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
+ // 递归查找节点
304
+ function findNodeById(tree: TreeNode[], id: number | string): TreeNode | null {
305
+ for (const node of tree) {
306
+ if (node.id === id) return node;
307
+ if (node.children) {
308
+ const found = findNodeById(node.children, id);
309
+ if (found) return found;
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+ // 已选择的项目
315
+ const selectedItems = computed(() => {
316
+ const items: any[] = [];
317
+ selectedIds.value.forEach(id => {
318
+ // 先在搜索结果中查找
319
+ const searchUser = searchResults.value.find(u => u.id === id);
320
+ if (searchUser) {
321
+ items.push({ ...searchUser, typeName: '搜索结果' });
322
+ return;
323
+ }
324
+ // 在所有 tab 的树中查找
325
+ for (const tab of props.tabs) {
326
+ const treeData = internalTreeData.value[tab.key] || [];
327
+ const node = findNodeById(treeData, id);
328
+ if (node) {
329
+ items.push({
330
+ ...node,
331
+ typeName: tab.name
332
+ });
333
+ break;
334
+ }
335
+ }
336
+ });
337
+ return items;
338
+ });
339
+ // Tab 切换处理
340
+ const handleTabChange = () => {
341
+ searchKeyword.value = '';
342
+ };
343
+ // 组织变化处理
344
+ const handleOrgChange = () => {
345
+ // 可以触发重新加载数据
346
+ };
347
+ // 防抖定时器
348
+ let searchTimer: ReturnType<typeof setTimeout> | null = null;
349
+ // 搜索输入处理(带防抖)
350
+ const handleSearchInput = () => {
351
+ if (searchTimer) {
352
+ clearTimeout(searchTimer);
353
+ }
354
+ if (!searchKeyword.value.trim()) {
355
+ clearSearch();
356
+ return;
357
+ }
358
+ // 立即显示搜索模式和loading
359
+ isSearchMode.value = true;
360
+ searchLoading.value = true;
361
+ // 300ms 防抖
362
+ searchTimer = setTimeout(() => {
363
+ doSearch();
364
+ }, 300);
365
+ };
366
+ // 执行搜索
367
+ const doSearch = () => {
368
+ searchResults.value = [];
369
+ const callback = (users: User[]) => {
370
+ searchResults.value = users.map(u => ({ ...u, isUser: true }));
371
+ searchLoading.value = false;
372
+ };
373
+ emit('search', {
374
+ keyword: searchKeyword.value,
375
+ orgId: selectedOrgId.value || undefined,
376
+ callback
377
+ });
378
+ };
379
+ // 清除搜索,返回 tab 模式
380
+ const clearSearch = () => {
381
+ if (searchTimer) {
382
+ clearTimeout(searchTimer);
383
+ searchTimer = null;
384
+ }
385
+ isSearchMode.value = false;
386
+ searchKeyword.value = '';
387
+ searchResults.value = [];
388
+ searchLoading.value = false;
389
+ };
390
+ // 切换搜索结果中的选中状态
391
+ const toggleSearchResultSelect = (user: User) => {
392
+ 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
+ }
398
+ };
399
+ // 获取当前 tab 树中所有节点的 id
400
+ function getAllNodeIds(tree: TreeNode[]): (number | string)[] {
401
+ const ids: (number | string)[] = [];
402
+ 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
+ }
409
+ };
410
+ traverse(tree);
411
+ return ids;
412
+ }
413
+ // 处理树节点 checkbox 变化
414
+ const handleTreeCheck = (checkedIds: (number | string)[]) => {
415
+ // 获取当前 tab 的所有节点 id
416
+ const currentTreeData = internalTreeData.value[activeTab.value] || [];
417
+ const currentTreeNodeIds = getAllNodeIds(currentTreeData);
418
+ // 保留不属于当前 tab 的选中项
419
+ const otherSelectedIds = selectedIds.value.filter(id => !currentTreeNodeIds.includes(id));
420
+ // 合并:其他 tab 的选中项 + 当前 tab 的选中项
421
+ selectedIds.value = [...otherSelectedIds, ...checkedIds];
422
+ };
423
+ // 点击"显示人员"按钮
424
+ async function handleLoadUsers(node: any, tabKey: string) {
425
+ const nodeId = node.value;
426
+ const treeRef = treeRefs.value[tabKey];
427
+ // 回调函数,父组件调用此函数传入用户数据
428
+ const callback = async (users: User[]) => {
429
+ if (users.length > 0) {
430
+ const userNodes = users.map((user) => {
431
+ const { id, name, ...rest } = user;
432
+ return {
433
+ ...rest,
434
+ id,
435
+ name: user.displayName || name,
436
+ isUser: true,
437
+ };
438
+ });
439
+ // 添加到树中
440
+ if (treeRef) {
441
+ treeRef.appendTo(nodeId, userNodes);
442
+ node.data.loaded = true;
443
+ await nextTick();
444
+ // 使用 treeRef 的 setItem 方法展开节点
445
+ try {
446
+ treeRef.setItem(nodeId, { expanded: true });
447
+ } catch (e) {
448
+ console.log('展开节点失败', e);
449
+ }
450
+ }
451
+ } else {
452
+ node.data.loaded = true;
453
+ }
454
+ };
455
+ // 触发事件,让父组件加载数据
456
+ emit('load-users', { tabKey, nodeId, node, callback });
457
+ }
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
+ // 确认
467
+ const handleConfirm = () => {
468
+ emit('update:modelValue', selectedIds.value);
469
+ emit('confirm', selectedItems.value);
470
+ dialogVisible.value = false;
471
+ };
472
+ // 关闭
473
+ const handleClose = () => {
474
+ dialogVisible.value = false;
475
+ };
476
+ // 暴露方法给父组件
477
+ defineExpose({
478
+ clearSelection,
479
+ appendUsers: (tabKey: string, nodeId: number | string, users: User[]) => {
480
+ const treeRef = treeRefs.value[tabKey];
481
+ if (treeRef && users.length > 0) {
482
+ const userNodes = users.map((user) => {
483
+ const { id, name, ...rest } = user;
484
+ return {
485
+ ...rest,
486
+ id,
487
+ name: user.displayName || name,
488
+ isUser: true,
489
+ };
490
+ });
491
+ treeRef.appendTo(nodeId, userNodes);
492
+ }
493
+ },
494
+ });
495
+ </script>
496
+ <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
+ }
819
+ </style>