cd-personselector 1.0.1 → 1.0.2
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 +151 -175
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/PersonSelector.vue +129 -578
package/src/PersonSelector.vue
CHANGED
|
@@ -8,76 +8,43 @@
|
|
|
8
8
|
@confirm="handleConfirm"
|
|
9
9
|
@close="handleClose"
|
|
10
10
|
>
|
|
11
|
-
<div class="
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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="
|
|
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
|
|
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="
|
|
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="
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
<span class="
|
|
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="
|
|
142
|
-
<div v-if="selectedItems.length === 0" class="empty
|
|
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-
|
|
147
|
-
<div
|
|
148
|
-
|
|
149
|
-
|
|
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="
|
|
158
|
-
<div class="
|
|
159
|
-
<div class="
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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,99 @@ watch(() => props.visible, (newVal) => {
|
|
|
290
208
|
}
|
|
291
209
|
}
|
|
292
210
|
});
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
340
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 点击"显示人员"按钮
|
|
424
277
|
async function handleLoadUsers(node: any, tabKey: string) {
|
|
425
278
|
const nodeId = node.value;
|
|
426
279
|
const treeRef = treeRefs.value[tabKey];
|
|
427
|
-
// 回调函数,父组件调用此函数传入用户数据
|
|
428
280
|
const callback = async (users: User[]) => {
|
|
429
281
|
if (users.length > 0) {
|
|
430
282
|
const userNodes = users.map((user) => {
|
|
431
283
|
const { id, name, ...rest } = user;
|
|
432
|
-
return {
|
|
433
|
-
...rest,
|
|
434
|
-
id,
|
|
435
|
-
name: user.displayName || name,
|
|
436
|
-
isUser: true,
|
|
437
|
-
};
|
|
284
|
+
return { ...rest, id, name: user.displayName || name, isUser: true };
|
|
438
285
|
});
|
|
439
|
-
// 添加到树中
|
|
440
286
|
if (treeRef) {
|
|
441
287
|
treeRef.appendTo(nodeId, userNodes);
|
|
442
288
|
node.data.loaded = true;
|
|
443
289
|
await nextTick();
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
treeRef.setItem(nodeId, { expanded: true });
|
|
447
|
-
} catch (e) {
|
|
448
|
-
console.log('展开节点失败', e);
|
|
449
|
-
}
|
|
290
|
+
try { treeRef.setItem(nodeId, { expanded: true }); } catch { /* ignore */ }
|
|
450
291
|
}
|
|
451
|
-
} else {
|
|
452
|
-
node.data.loaded = true;
|
|
453
|
-
}
|
|
292
|
+
} else { node.data.loaded = true; }
|
|
454
293
|
};
|
|
455
|
-
// 触发事件,让父组件加载数据
|
|
456
294
|
emit('load-users', { tabKey, nodeId, node, callback });
|
|
457
295
|
}
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
selectedIds.value = selectedIds.value.filter(i => i !== id);
|
|
461
|
-
};
|
|
462
|
-
// 清空选择
|
|
463
|
-
const clearSelection = () => {
|
|
464
|
-
selectedIds.value = [];
|
|
465
|
-
};
|
|
466
|
-
// 确认
|
|
296
|
+
const handleRemoveItem = (id: number | string) => { selectedIds.value = selectedIds.value.filter(i => i !== id); };
|
|
297
|
+
const clearSelection = () => { selectedIds.value = []; };
|
|
467
298
|
const handleConfirm = () => {
|
|
468
299
|
emit('update:modelValue', selectedIds.value);
|
|
469
300
|
emit('confirm', selectedItems.value);
|
|
470
301
|
dialogVisible.value = false;
|
|
471
302
|
};
|
|
472
|
-
|
|
473
|
-
const handleClose = () => {
|
|
474
|
-
dialogVisible.value = false;
|
|
475
|
-
};
|
|
476
|
-
// 暴露方法给父组件
|
|
303
|
+
const handleClose = () => { dialogVisible.value = false; };
|
|
477
304
|
defineExpose({
|
|
478
305
|
clearSelection,
|
|
479
306
|
appendUsers: (tabKey: string, nodeId: number | string, users: User[]) => {
|
|
@@ -481,12 +308,7 @@ defineExpose({
|
|
|
481
308
|
if (treeRef && users.length > 0) {
|
|
482
309
|
const userNodes = users.map((user) => {
|
|
483
310
|
const { id, name, ...rest } = user;
|
|
484
|
-
return {
|
|
485
|
-
...rest,
|
|
486
|
-
id,
|
|
487
|
-
name: user.displayName || name,
|
|
488
|
-
isUser: true,
|
|
489
|
-
};
|
|
311
|
+
return { ...rest, id, name: user.displayName || name, isUser: true };
|
|
490
312
|
});
|
|
491
313
|
treeRef.appendTo(nodeId, userNodes);
|
|
492
314
|
}
|
|
@@ -494,326 +316,55 @@ defineExpose({
|
|
|
494
316
|
});
|
|
495
317
|
</script>
|
|
496
318
|
<style scoped>
|
|
497
|
-
.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
.search-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
.
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
.
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
}
|
|
319
|
+
.cd-ps-container { display: flex; flex-direction: column; gap: 16px; min-height: 500px; }
|
|
320
|
+
.cd-ps-tips { display: flex; align-items: center; gap: 8px; padding: 12px; background-color: #e8f4ff; border-radius: 4px; font-size: 13px; color: #0052d9; }
|
|
321
|
+
.cd-ps-search { display: flex; gap: 12px; align-items: center; }
|
|
322
|
+
.cd-ps-org-select { flex-shrink: 0; }
|
|
323
|
+
.cd-ps-search-input { flex: 1; }
|
|
324
|
+
.cd-ps-content { display: flex; gap: 0; height: 450px; border: 1px solid #dfe1e6; border-radius: 4px; overflow: hidden; }
|
|
325
|
+
.cd-ps-left { width: 480px; flex-shrink: 0; border-right: 1px solid #dfe1e6; display: flex; flex-direction: column; background-color: #fafafa; }
|
|
326
|
+
.cd-ps-left :deep(.t-tabs__nav-container) { padding: 0 8px; background-color: #fff; }
|
|
327
|
+
.cd-ps-left :deep(.t-tabs__content) { flex: 1; overflow: hidden; }
|
|
328
|
+
.cd-ps-tree { height: 100%; overflow-y: auto; padding: 8px; }
|
|
329
|
+
.cd-ps-tree::-webkit-scrollbar { width: 6px; }
|
|
330
|
+
.cd-ps-tree::-webkit-scrollbar-track { background: #f1f1f1; }
|
|
331
|
+
.cd-ps-tree::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
|
|
332
|
+
.cd-ps-node { display: flex; align-items: center; gap: 6px; font-size: 14px; width: 100%; }
|
|
333
|
+
:deep(.t-tree__label) { flex: 1; }
|
|
334
|
+
:deep(.t-checkbox__label) { width: 100%; }
|
|
335
|
+
.cd-ps-node-user { font-size: 13px; color: #666; }
|
|
336
|
+
.cd-ps-count { color: #999; font-size: 12px; }
|
|
337
|
+
.cd-ps-load-btn { margin-left: auto; color: #0052d9; font-size: 12px; flex-shrink: 0; }
|
|
338
|
+
.cd-ps-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: #999; }
|
|
339
|
+
.cd-ps-empty p { margin-top: 16px; font-size: 14px; }
|
|
340
|
+
.cd-ps-search-results { display: flex; flex-direction: column; height: 100%; }
|
|
341
|
+
.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; }
|
|
342
|
+
.cd-ps-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; gap: 12px; color: #999; }
|
|
343
|
+
.cd-ps-result-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
344
|
+
.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; }
|
|
345
|
+
.cd-ps-result-item:hover { border-color: #0052d9; background: #f0f5ff; }
|
|
346
|
+
.cd-ps-result-item.cd-ps-selected { border-color: #0052d9; background: #e8f4ff; }
|
|
347
|
+
.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; }
|
|
348
|
+
.cd-ps-result-item .cd-ps-avatar { width: 36px; height: 36px; }
|
|
349
|
+
.cd-ps-avatar-dept { background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); }
|
|
350
|
+
.cd-ps-info { flex: 1; min-width: 0; }
|
|
351
|
+
.cd-ps-name { font-size: 13px; font-weight: 500; color: #333; margin-bottom: 4px; }
|
|
352
|
+
.cd-ps-result-item .cd-ps-name { font-size: 14px; }
|
|
353
|
+
.cd-ps-meta { display: flex; flex-wrap: wrap; gap: 6px; font-size: 11px; color: #999; }
|
|
354
|
+
.cd-ps-result-item .cd-ps-meta { font-size: 12px; }
|
|
355
|
+
.cd-ps-meta span::before { content: '•'; margin-right: 4px; }
|
|
356
|
+
.cd-ps-meta span:first-child::before { display: none; }
|
|
357
|
+
.cd-ps-right { flex: 1; display: flex; flex-direction: column; overflow: hidden; background-color: #fafafa; }
|
|
358
|
+
.cd-ps-right-header { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid #dfe1e6; background-color: #fff; }
|
|
359
|
+
.cd-ps-title { font-weight: 500; font-size: 14px; color: #333; }
|
|
360
|
+
.cd-ps-num { font-size: 13px; color: #0052d9; font-weight: 600; margin-left: 8px; flex: 1; }
|
|
361
|
+
.cd-ps-right-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
362
|
+
.cd-ps-right-list::-webkit-scrollbar { width: 6px; }
|
|
363
|
+
.cd-ps-right-list::-webkit-scrollbar-track { background: #f1f1f1; }
|
|
364
|
+
.cd-ps-right-list::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
|
|
365
|
+
.cd-ps-selected-list { display: flex; flex-direction: column; gap: 8px; }
|
|
366
|
+
.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; }
|
|
367
|
+
.cd-ps-selected-item:hover { border-color: #0052d9; box-shadow: 0 2px 4px rgba(0, 82, 217, 0.1); }
|
|
368
|
+
.cd-ps-dept-item { border-color: #f5af19; }
|
|
369
|
+
.cd-ps-item-info { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
|
|
819
370
|
</style>
|