widget.qw 1.0.80 → 1.0.82
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/.env.development +9 -9
- package/.env.production +8 -8
- package/README.md +75 -75
- package/build/style.css +471 -471
- package/build/widget.qw.es.js +56 -56
- package/build/widget.qw.umd.js +56 -56
- package/index.html +17 -17
- package/package.json +47 -47
- package/src/App.vue +26 -26
- package/src/api/index.js +165 -165
- package/src/components/AuditBar.vue +396 -396
- package/src/components/BillCard.vue +164 -164
- package/src/components/CascaderPicker.vue +132 -132
- package/src/components/CascaderPop.vue +371 -371
- package/src/components/CheckGroup.vue +113 -113
- package/src/components/DatetimePicker/DatetimePop.vue +342 -342
- package/src/components/DatetimePicker/index.vue +124 -124
- package/src/components/DayDropdown.vue +56 -56
- package/src/components/FilePicker.vue +169 -169
- package/src/components/Input.vue +80 -80
- package/src/components/MonthDropdown.vue +51 -51
- package/src/components/MultiPicker.vue +123 -123
- package/src/components/ObjsEditor.vue +334 -334
- package/src/components/SecretNotify.js +5 -5
- package/src/components/Sheet.vue +92 -92
- package/src/components/SingleApiPicker.vue +108 -108
- package/src/components/SinglePicker.vue +102 -102
- package/src/components/SingleUserSelector.vue +425 -425
- package/src/components/Switch.vue +64 -64
- package/src/components/TreePicker.vue +113 -113
- package/src/components/UserPicker.vue +106 -106
- package/src/components/UserProfile.vue +129 -129
- package/src/components/UsersPicker.vue +118 -118
- package/src/components/YearDropdown.vue +59 -59
- package/src/components/data_selector.vue +303 -303
- package/src/components/image_picker.vue +123 -123
- package/src/components/images_picker.vue +195 -195
- package/src/components/index.js +162 -162
- package/src/components/mult_list_selector.vue +155 -155
- package/src/components/subdepartment_selector.vue +481 -481
- package/src/components/user_selector.vue +639 -639
- package/src/components/widget/DataPop.vue +95 -95
- package/src/components/widget/TreePop.vue +88 -88
- package/src/components/widget/UserPop.vue +347 -347
- package/src/env.d.ts +8 -8
- package/src/main.js +117 -117
- package/src/router/index.ts +173 -173
- package/src/util/array_util.js +32 -32
- package/src/util/auth_util.js +72 -72
- package/src/util/bool_util.js +5 -5
- package/src/util/bus.js +1 -1
- package/src/util/cache_util.js +18 -18
- package/src/util/errer_code.js +6 -6
- package/src/util/eval_util.js +19 -19
- package/src/util/icon_util.js +36 -36
- package/src/util/image_util.js +27 -27
- package/src/util/index.js +57 -57
- package/src/util/num_util.js +70 -70
- package/src/util/obj_util.js +28 -28
- package/src/util/request.js +73 -73
- package/src/util/request_json.js +71 -71
- package/src/util/request_json_mute.js +65 -65
- package/src/util/request_upload.js +79 -79
- package/src/util/route_util.js +31 -31
- package/src/util/str_util.js +143 -143
- package/src/util/time_util.js +406 -406
- package/src/util/toast_util.js +24 -24
- package/src/util/tree_util.js +153 -153
- package/src/util/uuid_util.js +9 -9
- package/src/util/validate.js +182 -182
- package/src/util/vue_filter.js +223 -223
- package/src/views/auditbar/index.vue +78 -78
- package/src/views/billcard/index.vue +48 -48
- package/src/views/cascaderpop/index.vue +90 -90
- package/src/views/checkgroup/index.vue +35 -35
- package/src/views/dataSelector/index.vue +48 -48
- package/src/views/datetimepicker/index.vue +34 -34
- package/src/views/daydropdown/index.vue +31 -31
- package/src/views/filepicker/index.vue +32 -32
- package/src/views/imagepicker/index.vue +31 -31
- package/src/views/imagespicker/index.vue +37 -31
- package/src/views/input/index.vue +35 -35
- package/src/views/monthdropdown/index.vue +31 -31
- package/src/views/multListSelector/index.vue +61 -61
- package/src/views/multipicker/index.vue +36 -36
- package/src/views/objseditor/index.vue +277 -277
- package/src/views/productSelector/index.vue +35 -35
- package/src/views/projectpicker/index.vue +41 -41
- package/src/views/secretnotify/index.vue +27 -27
- package/src/views/sheet/index.vue +45 -45
- package/src/views/singlepicker/index.vue +35 -35
- package/src/views/subdepartmentSelector/index.vue +40 -40
- package/src/views/switch/index.vue +34 -34
- package/src/views/treepicker/index.vue +41 -41
- package/src/views/userSelector/index.vue +54 -54
- package/src/views/userSelectorNew/index.vue +45 -45
- package/src/views/userpicker/index.vue +43 -43
- package/src/views/userprofile/index.vue +30 -30
- package/src/views/userspicker/index.vue +43 -43
- package/src/views/yeardropdown/index.vue +32 -32
- package/src/vm/index.js +1 -1
- package/tsconfig.json +19 -19
- package/tsconfig.node.json +8 -8
- package/vite.config.ts +122 -122
|
@@ -1,639 +1,639 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="user-selector">
|
|
3
|
-
<van-field
|
|
4
|
-
v-if="!isGone"
|
|
5
|
-
:model-value="selectedUserInfo"
|
|
6
|
-
clickable
|
|
7
|
-
:label="props.label"
|
|
8
|
-
:placeholder="props.placeholder"
|
|
9
|
-
readonly
|
|
10
|
-
@click="showPicker = !props.readonly"
|
|
11
|
-
:right-icon="props.readonly ? '' : 'arrow'"
|
|
12
|
-
:required="props.required"
|
|
13
|
-
:rules="props.rules"
|
|
14
|
-
/>
|
|
15
|
-
|
|
16
|
-
<van-popup v-model:show="showPicker" position="bottom" class="user-popup">
|
|
17
|
-
<div class="popup-container">
|
|
18
|
-
<div class="picker-header">
|
|
19
|
-
<div class="header-left">
|
|
20
|
-
<van-button
|
|
21
|
-
v-if="currentLevel > 0"
|
|
22
|
-
@click="goBack"
|
|
23
|
-
icon="arrow-left"
|
|
24
|
-
size="small"
|
|
25
|
-
class="back-button"
|
|
26
|
-
/>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<!-- 移除header-center包装,直接使用current-path -->
|
|
30
|
-
<div class="current-path">{{ currentPath }}</div>
|
|
31
|
-
|
|
32
|
-
<div class="header-right">
|
|
33
|
-
<van-button size="small" @click="handleClear" class="cancel-btn"
|
|
34
|
-
>清空</van-button
|
|
35
|
-
>
|
|
36
|
-
<van-button
|
|
37
|
-
type="primary"
|
|
38
|
-
size="small"
|
|
39
|
-
@click="confirmSelection"
|
|
40
|
-
class="confirm-btn"
|
|
41
|
-
>确定</van-button
|
|
42
|
-
>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div v-if="mode === 'multiple'" class="select-all-container">
|
|
47
|
-
<van-checkbox
|
|
48
|
-
shape="square"
|
|
49
|
-
v-model="isAllChecked"
|
|
50
|
-
@click="toggleAllSelect"
|
|
51
|
-
label-position="left"
|
|
52
|
-
>
|
|
53
|
-
全选
|
|
54
|
-
</van-checkbox>
|
|
55
|
-
</div>
|
|
56
|
-
<div class="search-container">
|
|
57
|
-
<van-search
|
|
58
|
-
v-model="searchInput"
|
|
59
|
-
placeholder="输入工号查询"
|
|
60
|
-
clearable
|
|
61
|
-
@clear="handleClearSearch"
|
|
62
|
-
/>
|
|
63
|
-
<van-button
|
|
64
|
-
size="small"
|
|
65
|
-
type="primary"
|
|
66
|
-
@click="searchUserById"
|
|
67
|
-
class="search-btn"
|
|
68
|
-
>搜索</van-button
|
|
69
|
-
>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="scroll-container">
|
|
72
|
-
<van-cell-group v-if="!searchUserId && currentDepartments.length > 0">
|
|
73
|
-
<van-cell
|
|
74
|
-
v-for="dept in currentDepartments"
|
|
75
|
-
:key="dept.id"
|
|
76
|
-
:title="dept.name"
|
|
77
|
-
clickable
|
|
78
|
-
@click="selectDepartment(dept)"
|
|
79
|
-
>
|
|
80
|
-
<template #right-icon>
|
|
81
|
-
<van-icon v-if="dept.subDepartments?.length" name="arrow" />
|
|
82
|
-
</template>
|
|
83
|
-
</van-cell>
|
|
84
|
-
</van-cell-group>
|
|
85
|
-
|
|
86
|
-
<van-checkbox-group
|
|
87
|
-
v-model="tempSelectedUsers"
|
|
88
|
-
v-if="mode === 'multiple'"
|
|
89
|
-
>
|
|
90
|
-
<van-cell-group>
|
|
91
|
-
<van-cell
|
|
92
|
-
v-for="user in userData.users"
|
|
93
|
-
:key="user.userid"
|
|
94
|
-
:title="user.name"
|
|
95
|
-
clickable
|
|
96
|
-
@click="toggleUserSelect(user.userid)"
|
|
97
|
-
>
|
|
98
|
-
<template #right-icon>
|
|
99
|
-
<van-checkbox
|
|
100
|
-
:name="user.userid"
|
|
101
|
-
shape="square"
|
|
102
|
-
@click.stop
|
|
103
|
-
/>
|
|
104
|
-
<van-loading v-if="userData.loading" size="20px" />
|
|
105
|
-
</template>
|
|
106
|
-
</van-cell>
|
|
107
|
-
</van-cell-group>
|
|
108
|
-
</van-checkbox-group>
|
|
109
|
-
|
|
110
|
-
<van-radio-group v-model="tempSelectedUser" v-else>
|
|
111
|
-
<van-cell-group>
|
|
112
|
-
<van-cell
|
|
113
|
-
v-for="user in userData.users"
|
|
114
|
-
:key="user.userid"
|
|
115
|
-
:title="user.name"
|
|
116
|
-
clickable
|
|
117
|
-
@click="tempSelectedUser = user.userid"
|
|
118
|
-
>
|
|
119
|
-
<template #right-icon>
|
|
120
|
-
<van-radio :name="user.userid" />
|
|
121
|
-
<van-loading v-if="userData.loading" size="20px" />
|
|
122
|
-
</template>
|
|
123
|
-
</van-cell>
|
|
124
|
-
</van-cell-group>
|
|
125
|
-
</van-radio-group>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
</van-popup>
|
|
129
|
-
</div>
|
|
130
|
-
</template>
|
|
131
|
-
|
|
132
|
-
<script setup>
|
|
133
|
-
import { ref, computed, onMounted, watch } from "vue";
|
|
134
|
-
import util from "../util";
|
|
135
|
-
|
|
136
|
-
const props = defineProps({
|
|
137
|
-
modelValue: [Array, String, Number],
|
|
138
|
-
label: {
|
|
139
|
-
type: String,
|
|
140
|
-
default: "选择人员",
|
|
141
|
-
},
|
|
142
|
-
placeholder: {
|
|
143
|
-
type: String,
|
|
144
|
-
default: "请选择人员",
|
|
145
|
-
},
|
|
146
|
-
mode: {
|
|
147
|
-
type: String,
|
|
148
|
-
default: "single",
|
|
149
|
-
validator: (v) => ["single", "multiple"].includes(v),
|
|
150
|
-
},
|
|
151
|
-
required: {
|
|
152
|
-
type: Boolean,
|
|
153
|
-
default: true,
|
|
154
|
-
},
|
|
155
|
-
readonly: {
|
|
156
|
-
type: Boolean,
|
|
157
|
-
default: false,
|
|
158
|
-
},
|
|
159
|
-
rules: {
|
|
160
|
-
type: Array,
|
|
161
|
-
default: () => [],
|
|
162
|
-
},
|
|
163
|
-
currentDept: {
|
|
164
|
-
// 新增 currentDept 参数
|
|
165
|
-
type: String,
|
|
166
|
-
default: "",
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const { isRequired, isReadonly, isGone, isDisabled } = util.props2auth(props);
|
|
171
|
-
|
|
172
|
-
// 用户加载逻辑
|
|
173
|
-
const userData = ref({
|
|
174
|
-
users: [],
|
|
175
|
-
loading: false,
|
|
176
|
-
error: null,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
const emit = defineEmits(["update:modelValue"]);
|
|
180
|
-
|
|
181
|
-
// 状态管理
|
|
182
|
-
const departmentTree = ref([]);
|
|
183
|
-
const currentDepartments = ref([]);
|
|
184
|
-
const selectionPath = ref([]);
|
|
185
|
-
const searchInput = ref("");
|
|
186
|
-
const searchUserId = ref("");
|
|
187
|
-
const showPicker = ref(false);
|
|
188
|
-
const selectedUsers = ref([]);
|
|
189
|
-
|
|
190
|
-
// 临时选择状态
|
|
191
|
-
const tempSelectedUser = ref(null);
|
|
192
|
-
const tempSelectedUsers = ref([]);
|
|
193
|
-
const isAllChecked = ref(false);
|
|
194
|
-
const isInternalChange = ref(false); // 标志位,用于区分是内部选择还是外部修改
|
|
195
|
-
|
|
196
|
-
const onShow = () => {
|
|
197
|
-
if (isReadonly.value) return;
|
|
198
|
-
|
|
199
|
-
showPicker.value = true;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// 修复返回功能的核心修改点
|
|
203
|
-
const getCurrentDepartments = () => {
|
|
204
|
-
if (selectionPath.value.length === 0) return departmentTree.value;
|
|
205
|
-
let current = departmentTree.value;
|
|
206
|
-
for (const dept of selectionPath.value) {
|
|
207
|
-
const found = current.find((d) => d.id === dept.id);
|
|
208
|
-
current = found?.subDepartments || [];
|
|
209
|
-
}
|
|
210
|
-
return current;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const goBack = () => {
|
|
214
|
-
if (currentLevel.value === 0) return;
|
|
215
|
-
selectionPath.value.pop();
|
|
216
|
-
currentDepartments.value = getCurrentDepartments();
|
|
217
|
-
// 修复:返回时加载对应部门的用户
|
|
218
|
-
const targetDeptId =
|
|
219
|
-
selectionPath.value.slice(-1)[0]?.id || departmentTree.value[0]?.id;
|
|
220
|
-
if (targetDeptId) loadUsers(targetDeptId);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// 初始化方法
|
|
224
|
-
const initTempSelection = () => {
|
|
225
|
-
if (props.mode === "multiple") {
|
|
226
|
-
tempSelectedUsers.value = Array.isArray(props.modelValue)
|
|
227
|
-
? [...props.modelValue]
|
|
228
|
-
: [];
|
|
229
|
-
} else {
|
|
230
|
-
tempSelectedUser.value = props.modelValue || null;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// 添加模块级缓存对象
|
|
235
|
-
// 在模块顶部添加全局缓存
|
|
236
|
-
const departmentCache = ref(null); // 模块级缓存
|
|
237
|
-
const userCache = new Map();
|
|
238
|
-
const pendingPromises = new Map(); // 进行中的请求
|
|
239
|
-
|
|
240
|
-
// 部门树加载(添加共享缓存)
|
|
241
|
-
const fetchAllDepartments = async () => {
|
|
242
|
-
console.log("---------departmentCache---------", departmentCache);
|
|
243
|
-
if (departmentCache.value) {
|
|
244
|
-
departmentTree.value = departmentCache.value;
|
|
245
|
-
currentDepartments.value = departmentCache.value;
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const response = await util.get_subdepartments({ departmentId: 1 });
|
|
251
|
-
// console.log('---------------userSelector:get_subdepartments--------------------------',response);
|
|
252
|
-
if (response.code === 200) {
|
|
253
|
-
departmentCache.value = response.data.subDepartments || [];
|
|
254
|
-
departmentTree.value = departmentCache.value;
|
|
255
|
-
currentDepartments.value = departmentTree.value;
|
|
256
|
-
// console.log('---------departmentCache2---------',departmentCache);
|
|
257
|
-
}
|
|
258
|
-
} catch (error) {
|
|
259
|
-
console.error("初始化部门数据失败:", error);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// 用户加载逻辑(优化缓存处理)
|
|
264
|
-
const loadUsers = async (departmentId) => {
|
|
265
|
-
console.log("---------userCache---------", userCache);
|
|
266
|
-
if (userCache.has(departmentId)) {
|
|
267
|
-
console.log("---------userCacheHasDepartmentId---------", userCache);
|
|
268
|
-
userData.value.users = userCache.get(departmentId);
|
|
269
|
-
updateAllCheckedState();
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 防止重复请求
|
|
274
|
-
if (pendingPromises.has(departmentId)) {
|
|
275
|
-
await pendingPromises.get(departmentId);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
// 删除 departmentCache 的声明
|
|
281
|
-
// const departmentCache = ref(null);
|
|
282
|
-
|
|
283
|
-
const fetchAllDepartments = async () => {
|
|
284
|
-
try {
|
|
285
|
-
const response = await util.get_subdepartments({ departmentId: 1 });
|
|
286
|
-
if (response.code === 200) {
|
|
287
|
-
departmentTree.value = response.data.subDepartments || [];
|
|
288
|
-
currentDepartments.value = departmentTree.value;
|
|
289
|
-
}
|
|
290
|
-
} catch (error) {
|
|
291
|
-
console.error("初始化部门数据失败:", error);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
userData.value.loading = true;
|
|
295
|
-
const promise = util.get_subdepartments_users({ departmentId });
|
|
296
|
-
pendingPromises.set(departmentId, promise);
|
|
297
|
-
|
|
298
|
-
const response = await promise;
|
|
299
|
-
console.log(
|
|
300
|
-
"---------------userSelector:get_subdepartments_users--------------------------",
|
|
301
|
-
response
|
|
302
|
-
);
|
|
303
|
-
if (response.code === 200) {
|
|
304
|
-
const users = response.data.users || [];
|
|
305
|
-
userCache.set(departmentId, users);
|
|
306
|
-
userData.value.users = users;
|
|
307
|
-
updateAllCheckedState();
|
|
308
|
-
}
|
|
309
|
-
} catch (error) {
|
|
310
|
-
userData.value.error = error;
|
|
311
|
-
} finally {
|
|
312
|
-
userData.value.loading = false;
|
|
313
|
-
pendingPromises.delete(departmentId);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
// 部门选择处理
|
|
318
|
-
const selectDepartment = (dept) => {
|
|
319
|
-
selectionPath.value.push(dept);
|
|
320
|
-
currentDepartments.value = dept.subDepartments || [];
|
|
321
|
-
loadUsers(dept.id);
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// 计算属性
|
|
325
|
-
const currentPath = computed(
|
|
326
|
-
() => selectionPath.value.map((d) => d.name).join(" / ") || "请选择部门"
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
const currentLevel = computed(() => selectionPath.value.length);
|
|
330
|
-
|
|
331
|
-
const selectedUserInfo = computed(() => {
|
|
332
|
-
if (props.mode === "multiple") {
|
|
333
|
-
return selectedUsers.value.length > 0
|
|
334
|
-
? `${selectedUsers.value.map((u) => u.name).join(", ")}`
|
|
335
|
-
: "";
|
|
336
|
-
}
|
|
337
|
-
return selectedUsers.value[0] ? `${selectedUsers.value[0].name}` : "";
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// 全选逻辑
|
|
341
|
-
const toggleAllSelect = () => {
|
|
342
|
-
if (isAllChecked.value) {
|
|
343
|
-
tempSelectedUsers.value = userData.value.users.map((u) => u.userid);
|
|
344
|
-
} else {
|
|
345
|
-
tempSelectedUsers.value = [];
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const updateAllCheckedState = () => {
|
|
350
|
-
const currentUserIds = userData.value.users.map((u) => u.userid);
|
|
351
|
-
isAllChecked.value =
|
|
352
|
-
currentUserIds.length > 0 &&
|
|
353
|
-
currentUserIds.every((id) => tempSelectedUsers.value.includes(id));
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
// 用户选择切换
|
|
357
|
-
const toggleUserSelect = (userId) => {
|
|
358
|
-
const index = tempSelectedUsers.value.indexOf(userId);
|
|
359
|
-
if (index > -1) {
|
|
360
|
-
tempSelectedUsers.value.splice(index, 1);
|
|
361
|
-
} else {
|
|
362
|
-
tempSelectedUsers.value.push(userId);
|
|
363
|
-
}
|
|
364
|
-
updateAllCheckedState();
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// 确认选择
|
|
368
|
-
const confirmSelection = () => {
|
|
369
|
-
isInternalChange.value = true; // 标记为内部选择
|
|
370
|
-
if (props.mode === "multiple") {
|
|
371
|
-
// 合并当前选择的用户与已存在的selectedUsers
|
|
372
|
-
const currentSelected = userData.value.users.filter((u) =>
|
|
373
|
-
tempSelectedUsers.value.includes(u.userid)
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
// 使用Set去重
|
|
377
|
-
const uniqueUsers = new Set([...selectedUsers.value, ...currentSelected]);
|
|
378
|
-
selectedUsers.value = Array.from(uniqueUsers);
|
|
379
|
-
|
|
380
|
-
// 提取所有用户ID并emit
|
|
381
|
-
const allUserIds = selectedUsers.value.map((u) => u.userid);
|
|
382
|
-
emit("update:modelValue", allUserIds);
|
|
383
|
-
} else {
|
|
384
|
-
const selected = userData.value.users.find(
|
|
385
|
-
(u) => u.userid === tempSelectedUser.value
|
|
386
|
-
);
|
|
387
|
-
selectedUsers.value = selected ? [selected] : [];
|
|
388
|
-
emit("update:modelValue", tempSelectedUser.value);
|
|
389
|
-
}
|
|
390
|
-
showPicker.value = false;
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const handleClear = () => {
|
|
394
|
-
tempSelectedUsers.value = [""];
|
|
395
|
-
selectedUsers.value = [""];
|
|
396
|
-
isAllChecked.value = false;
|
|
397
|
-
emit("update:modelValue", null);
|
|
398
|
-
showPicker.value = false;
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
// 添加部门路径查找方法
|
|
402
|
-
function findDeptPath(deptId, tree, path = []) {
|
|
403
|
-
for (const dept of tree) {
|
|
404
|
-
if (dept.id === deptId) {
|
|
405
|
-
return [...path, dept];
|
|
406
|
-
}
|
|
407
|
-
if (dept.subDepartments?.length) {
|
|
408
|
-
const found = findDeptPath(deptId, dept.subDepartments, [...path, dept]);
|
|
409
|
-
if (found) return found;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
// 添加当前部门处理逻辑
|
|
415
|
-
const handleCurrentDeptChange = async (deptId) => {
|
|
416
|
-
const path = findDeptPath(deptId, departmentTree.value);
|
|
417
|
-
if (path) {
|
|
418
|
-
selectionPath.value = path;
|
|
419
|
-
currentDepartments.value = getCurrentDepartments();
|
|
420
|
-
loadUsers(deptId);
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// 添加重置到根部门方法
|
|
425
|
-
const resetToRootDepartment = () => {
|
|
426
|
-
selectionPath.value = [];
|
|
427
|
-
currentDepartments.value = departmentTree.value;
|
|
428
|
-
loadUsers(1); // 假设根部门ID为1
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// 监听currentDept变化
|
|
432
|
-
watch(
|
|
433
|
-
() => props.currentDept,
|
|
434
|
-
async (newVal) => {
|
|
435
|
-
if (!departmentCache.value) await fetchAllDepartments();
|
|
436
|
-
|
|
437
|
-
if (newVal) {
|
|
438
|
-
await handleCurrentDeptChange(newVal);
|
|
439
|
-
} else {
|
|
440
|
-
resetToRootDepartment();
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
{ immediate: true }
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
// 监听弹窗显示状态
|
|
447
|
-
watch(showPicker, (val) => {
|
|
448
|
-
if (val) {
|
|
449
|
-
initTempSelection();
|
|
450
|
-
if (props.mode === "multiple") {
|
|
451
|
-
updateAllCheckedState();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// 监听 modelValue 变化
|
|
457
|
-
watch(
|
|
458
|
-
() => props.modelValue,
|
|
459
|
-
async (newVal, oldVal) => {
|
|
460
|
-
// console.log('watchModelValue',isInternalChange);
|
|
461
|
-
if (newVal !== oldVal && !isInternalChange.value) {
|
|
462
|
-
if (props.mode === "multiple") {
|
|
463
|
-
console.log(
|
|
464
|
-
"-----------------------watchModelValue-------------------",
|
|
465
|
-
newVal
|
|
466
|
-
);
|
|
467
|
-
// 初始化时转换数组为部门分组格式
|
|
468
|
-
const initValues = Array.isArray(newVal) ? newVal : [];
|
|
469
|
-
tempSelectedUsers.value = initValues.reduce((acc, userId) => {
|
|
470
|
-
const deptId = selectedUsers.value.find(
|
|
471
|
-
(u) => u.userid === userId
|
|
472
|
-
)?.mainDepartment;
|
|
473
|
-
if (deptId) {
|
|
474
|
-
acc[deptId] = [...(acc[deptId] || []), userId];
|
|
475
|
-
}
|
|
476
|
-
return acc;
|
|
477
|
-
}, {});
|
|
478
|
-
// 获取所有用户信息
|
|
479
|
-
if (Array.isArray(newVal) && newVal.length > 0) {
|
|
480
|
-
const users = [];
|
|
481
|
-
for (const userId of newVal) {
|
|
482
|
-
console.log(
|
|
483
|
-
"-----------------------watchModelValue:userid-------------------",
|
|
484
|
-
userId,
|
|
485
|
-
newVal
|
|
486
|
-
);
|
|
487
|
-
const user = await getUserInfoById(userId);
|
|
488
|
-
if (user) users.push(user);
|
|
489
|
-
}
|
|
490
|
-
selectedUsers.value = users;
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
tempSelectedUser.value = newVal || null;
|
|
494
|
-
// 获取单个用户信息
|
|
495
|
-
if (newVal) {
|
|
496
|
-
const user = await getUserInfoById(newVal);
|
|
497
|
-
if (user) selectedUsers.value = [user];
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// 重置标志位
|
|
503
|
-
if (isInternalChange.value) {
|
|
504
|
-
isInternalChange.value = false;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
// 获取用户信息
|
|
510
|
-
const getUserInfoById = async (userId) => {
|
|
511
|
-
try {
|
|
512
|
-
const response = await util.get_wxuser_by_id({ userId });
|
|
513
|
-
if (response.code === 200) {
|
|
514
|
-
return response.data;
|
|
515
|
-
}
|
|
516
|
-
} catch (error) {
|
|
517
|
-
console.error("获取用户信息失败:", error);
|
|
518
|
-
}
|
|
519
|
-
return null;
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const searchUserById = async () => {
|
|
523
|
-
searchUserId.value = searchInput.value;
|
|
524
|
-
if (!searchUserId.value) return;
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
userData.value.loading = true;
|
|
528
|
-
const user = await getUserInfoById(searchUserId.value);
|
|
529
|
-
if (user) {
|
|
530
|
-
userData.value.users = [user];
|
|
531
|
-
// 更新currentPath显示用户部门
|
|
532
|
-
console.log("=========", user);
|
|
533
|
-
|
|
534
|
-
const deptPath = await findDeptPath(
|
|
535
|
-
user.mainDepartment,
|
|
536
|
-
departmentTree.value
|
|
537
|
-
);
|
|
538
|
-
if (deptPath) {
|
|
539
|
-
selectionPath.value = deptPath;
|
|
540
|
-
}
|
|
541
|
-
// 设置标志位,查询后只显示人员列表
|
|
542
|
-
currentDepartments.value = [];
|
|
543
|
-
} else {
|
|
544
|
-
userData.value.users = [];
|
|
545
|
-
}
|
|
546
|
-
} catch (error) {
|
|
547
|
-
console.error("查询用户失败:", error);
|
|
548
|
-
} finally {
|
|
549
|
-
userData.value.loading = false;
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
const handleClearSearch = () => {
|
|
554
|
-
searchUserId.value = "";
|
|
555
|
-
if (selectionPath.value.length > 0) {
|
|
556
|
-
loadUsers(selectionPath.value.slice(-1)[0].id);
|
|
557
|
-
} else {
|
|
558
|
-
loadUsers(departmentTree.value[0]?.id || 1);
|
|
559
|
-
}
|
|
560
|
-
};
|
|
561
|
-
</script>
|
|
562
|
-
|
|
563
|
-
<style scoped>
|
|
564
|
-
.user-popup {
|
|
565
|
-
height: 60vh;
|
|
566
|
-
width: 100%;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
.picker-header {
|
|
570
|
-
padding: 10px;
|
|
571
|
-
border-bottom: 1px solid #ebedf0;
|
|
572
|
-
display: flex;
|
|
573
|
-
justify-content: space-between;
|
|
574
|
-
align-items: center;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
.scroll-container {
|
|
578
|
-
height: calc(80vh - 150px);
|
|
579
|
-
overflow-y: auto;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
.search-container {
|
|
583
|
-
padding: 10px;
|
|
584
|
-
background: #fff;
|
|
585
|
-
display: flex;
|
|
586
|
-
align-items: center;
|
|
587
|
-
gap: 8px;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
:deep(.search-container .van-search) {
|
|
591
|
-
flex: 1;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.search-btn {
|
|
595
|
-
flex-shrink: 0;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.select-all-container {
|
|
599
|
-
font-size: 14px;
|
|
600
|
-
padding: 0 16px;
|
|
601
|
-
margin: 10px 0;
|
|
602
|
-
display: flex;
|
|
603
|
-
justify-content: flex-end;
|
|
604
|
-
align-items: center;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
:deep(.van-checkbox__icon--square) {
|
|
608
|
-
border-radius: 4px;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
:deep(.van-checkbox__label) {
|
|
612
|
-
margin-left: 8px;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
:deep(.van-radio-group) .van-cell,
|
|
616
|
-
:deep(.van-checkbox-group) .van-cell {
|
|
617
|
-
align-items: center;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
.current-path {
|
|
621
|
-
flex: 1;
|
|
622
|
-
font-size: 14px;
|
|
623
|
-
color: #323233;
|
|
624
|
-
white-space: nowrap;
|
|
625
|
-
overflow: hidden;
|
|
626
|
-
text-overflow: ellipsis;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
.header-right {
|
|
630
|
-
display: flex;
|
|
631
|
-
gap: 8px;
|
|
632
|
-
margin-left: auto;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
.cancel-btn {
|
|
636
|
-
color: #969799;
|
|
637
|
-
margin-right: 5px;
|
|
638
|
-
}
|
|
639
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="user-selector">
|
|
3
|
+
<van-field
|
|
4
|
+
v-if="!isGone"
|
|
5
|
+
:model-value="selectedUserInfo"
|
|
6
|
+
clickable
|
|
7
|
+
:label="props.label"
|
|
8
|
+
:placeholder="props.placeholder"
|
|
9
|
+
readonly
|
|
10
|
+
@click="showPicker = !props.readonly"
|
|
11
|
+
:right-icon="props.readonly ? '' : 'arrow'"
|
|
12
|
+
:required="props.required"
|
|
13
|
+
:rules="props.rules"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<van-popup v-model:show="showPicker" position="bottom" class="user-popup">
|
|
17
|
+
<div class="popup-container">
|
|
18
|
+
<div class="picker-header">
|
|
19
|
+
<div class="header-left">
|
|
20
|
+
<van-button
|
|
21
|
+
v-if="currentLevel > 0"
|
|
22
|
+
@click="goBack"
|
|
23
|
+
icon="arrow-left"
|
|
24
|
+
size="small"
|
|
25
|
+
class="back-button"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- 移除header-center包装,直接使用current-path -->
|
|
30
|
+
<div class="current-path">{{ currentPath }}</div>
|
|
31
|
+
|
|
32
|
+
<div class="header-right">
|
|
33
|
+
<van-button size="small" @click="handleClear" class="cancel-btn"
|
|
34
|
+
>清空</van-button
|
|
35
|
+
>
|
|
36
|
+
<van-button
|
|
37
|
+
type="primary"
|
|
38
|
+
size="small"
|
|
39
|
+
@click="confirmSelection"
|
|
40
|
+
class="confirm-btn"
|
|
41
|
+
>确定</van-button
|
|
42
|
+
>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div v-if="mode === 'multiple'" class="select-all-container">
|
|
47
|
+
<van-checkbox
|
|
48
|
+
shape="square"
|
|
49
|
+
v-model="isAllChecked"
|
|
50
|
+
@click="toggleAllSelect"
|
|
51
|
+
label-position="left"
|
|
52
|
+
>
|
|
53
|
+
全选
|
|
54
|
+
</van-checkbox>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="search-container">
|
|
57
|
+
<van-search
|
|
58
|
+
v-model="searchInput"
|
|
59
|
+
placeholder="输入工号查询"
|
|
60
|
+
clearable
|
|
61
|
+
@clear="handleClearSearch"
|
|
62
|
+
/>
|
|
63
|
+
<van-button
|
|
64
|
+
size="small"
|
|
65
|
+
type="primary"
|
|
66
|
+
@click="searchUserById"
|
|
67
|
+
class="search-btn"
|
|
68
|
+
>搜索</van-button
|
|
69
|
+
>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="scroll-container">
|
|
72
|
+
<van-cell-group v-if="!searchUserId && currentDepartments.length > 0">
|
|
73
|
+
<van-cell
|
|
74
|
+
v-for="dept in currentDepartments"
|
|
75
|
+
:key="dept.id"
|
|
76
|
+
:title="dept.name"
|
|
77
|
+
clickable
|
|
78
|
+
@click="selectDepartment(dept)"
|
|
79
|
+
>
|
|
80
|
+
<template #right-icon>
|
|
81
|
+
<van-icon v-if="dept.subDepartments?.length" name="arrow" />
|
|
82
|
+
</template>
|
|
83
|
+
</van-cell>
|
|
84
|
+
</van-cell-group>
|
|
85
|
+
|
|
86
|
+
<van-checkbox-group
|
|
87
|
+
v-model="tempSelectedUsers"
|
|
88
|
+
v-if="mode === 'multiple'"
|
|
89
|
+
>
|
|
90
|
+
<van-cell-group>
|
|
91
|
+
<van-cell
|
|
92
|
+
v-for="user in userData.users"
|
|
93
|
+
:key="user.userid"
|
|
94
|
+
:title="user.name"
|
|
95
|
+
clickable
|
|
96
|
+
@click="toggleUserSelect(user.userid)"
|
|
97
|
+
>
|
|
98
|
+
<template #right-icon>
|
|
99
|
+
<van-checkbox
|
|
100
|
+
:name="user.userid"
|
|
101
|
+
shape="square"
|
|
102
|
+
@click.stop
|
|
103
|
+
/>
|
|
104
|
+
<van-loading v-if="userData.loading" size="20px" />
|
|
105
|
+
</template>
|
|
106
|
+
</van-cell>
|
|
107
|
+
</van-cell-group>
|
|
108
|
+
</van-checkbox-group>
|
|
109
|
+
|
|
110
|
+
<van-radio-group v-model="tempSelectedUser" v-else>
|
|
111
|
+
<van-cell-group>
|
|
112
|
+
<van-cell
|
|
113
|
+
v-for="user in userData.users"
|
|
114
|
+
:key="user.userid"
|
|
115
|
+
:title="user.name"
|
|
116
|
+
clickable
|
|
117
|
+
@click="tempSelectedUser = user.userid"
|
|
118
|
+
>
|
|
119
|
+
<template #right-icon>
|
|
120
|
+
<van-radio :name="user.userid" />
|
|
121
|
+
<van-loading v-if="userData.loading" size="20px" />
|
|
122
|
+
</template>
|
|
123
|
+
</van-cell>
|
|
124
|
+
</van-cell-group>
|
|
125
|
+
</van-radio-group>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</van-popup>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup>
|
|
133
|
+
import { ref, computed, onMounted, watch } from "vue";
|
|
134
|
+
import util from "../util";
|
|
135
|
+
|
|
136
|
+
const props = defineProps({
|
|
137
|
+
modelValue: [Array, String, Number],
|
|
138
|
+
label: {
|
|
139
|
+
type: String,
|
|
140
|
+
default: "选择人员",
|
|
141
|
+
},
|
|
142
|
+
placeholder: {
|
|
143
|
+
type: String,
|
|
144
|
+
default: "请选择人员",
|
|
145
|
+
},
|
|
146
|
+
mode: {
|
|
147
|
+
type: String,
|
|
148
|
+
default: "single",
|
|
149
|
+
validator: (v) => ["single", "multiple"].includes(v),
|
|
150
|
+
},
|
|
151
|
+
required: {
|
|
152
|
+
type: Boolean,
|
|
153
|
+
default: true,
|
|
154
|
+
},
|
|
155
|
+
readonly: {
|
|
156
|
+
type: Boolean,
|
|
157
|
+
default: false,
|
|
158
|
+
},
|
|
159
|
+
rules: {
|
|
160
|
+
type: Array,
|
|
161
|
+
default: () => [],
|
|
162
|
+
},
|
|
163
|
+
currentDept: {
|
|
164
|
+
// 新增 currentDept 参数
|
|
165
|
+
type: String,
|
|
166
|
+
default: "",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const { isRequired, isReadonly, isGone, isDisabled } = util.props2auth(props);
|
|
171
|
+
|
|
172
|
+
// 用户加载逻辑
|
|
173
|
+
const userData = ref({
|
|
174
|
+
users: [],
|
|
175
|
+
loading: false,
|
|
176
|
+
error: null,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
180
|
+
|
|
181
|
+
// 状态管理
|
|
182
|
+
const departmentTree = ref([]);
|
|
183
|
+
const currentDepartments = ref([]);
|
|
184
|
+
const selectionPath = ref([]);
|
|
185
|
+
const searchInput = ref("");
|
|
186
|
+
const searchUserId = ref("");
|
|
187
|
+
const showPicker = ref(false);
|
|
188
|
+
const selectedUsers = ref([]);
|
|
189
|
+
|
|
190
|
+
// 临时选择状态
|
|
191
|
+
const tempSelectedUser = ref(null);
|
|
192
|
+
const tempSelectedUsers = ref([]);
|
|
193
|
+
const isAllChecked = ref(false);
|
|
194
|
+
const isInternalChange = ref(false); // 标志位,用于区分是内部选择还是外部修改
|
|
195
|
+
|
|
196
|
+
const onShow = () => {
|
|
197
|
+
if (isReadonly.value) return;
|
|
198
|
+
|
|
199
|
+
showPicker.value = true;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// 修复返回功能的核心修改点
|
|
203
|
+
const getCurrentDepartments = () => {
|
|
204
|
+
if (selectionPath.value.length === 0) return departmentTree.value;
|
|
205
|
+
let current = departmentTree.value;
|
|
206
|
+
for (const dept of selectionPath.value) {
|
|
207
|
+
const found = current.find((d) => d.id === dept.id);
|
|
208
|
+
current = found?.subDepartments || [];
|
|
209
|
+
}
|
|
210
|
+
return current;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const goBack = () => {
|
|
214
|
+
if (currentLevel.value === 0) return;
|
|
215
|
+
selectionPath.value.pop();
|
|
216
|
+
currentDepartments.value = getCurrentDepartments();
|
|
217
|
+
// 修复:返回时加载对应部门的用户
|
|
218
|
+
const targetDeptId =
|
|
219
|
+
selectionPath.value.slice(-1)[0]?.id || departmentTree.value[0]?.id;
|
|
220
|
+
if (targetDeptId) loadUsers(targetDeptId);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// 初始化方法
|
|
224
|
+
const initTempSelection = () => {
|
|
225
|
+
if (props.mode === "multiple") {
|
|
226
|
+
tempSelectedUsers.value = Array.isArray(props.modelValue)
|
|
227
|
+
? [...props.modelValue]
|
|
228
|
+
: [];
|
|
229
|
+
} else {
|
|
230
|
+
tempSelectedUser.value = props.modelValue || null;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// 添加模块级缓存对象
|
|
235
|
+
// 在模块顶部添加全局缓存
|
|
236
|
+
const departmentCache = ref(null); // 模块级缓存
|
|
237
|
+
const userCache = new Map();
|
|
238
|
+
const pendingPromises = new Map(); // 进行中的请求
|
|
239
|
+
|
|
240
|
+
// 部门树加载(添加共享缓存)
|
|
241
|
+
const fetchAllDepartments = async () => {
|
|
242
|
+
console.log("---------departmentCache---------", departmentCache);
|
|
243
|
+
if (departmentCache.value) {
|
|
244
|
+
departmentTree.value = departmentCache.value;
|
|
245
|
+
currentDepartments.value = departmentCache.value;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await util.get_subdepartments({ departmentId: 1 });
|
|
251
|
+
// console.log('---------------userSelector:get_subdepartments--------------------------',response);
|
|
252
|
+
if (response.code === 200) {
|
|
253
|
+
departmentCache.value = response.data.subDepartments || [];
|
|
254
|
+
departmentTree.value = departmentCache.value;
|
|
255
|
+
currentDepartments.value = departmentTree.value;
|
|
256
|
+
// console.log('---------departmentCache2---------',departmentCache);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error("初始化部门数据失败:", error);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// 用户加载逻辑(优化缓存处理)
|
|
264
|
+
const loadUsers = async (departmentId) => {
|
|
265
|
+
console.log("---------userCache---------", userCache);
|
|
266
|
+
if (userCache.has(departmentId)) {
|
|
267
|
+
console.log("---------userCacheHasDepartmentId---------", userCache);
|
|
268
|
+
userData.value.users = userCache.get(departmentId);
|
|
269
|
+
updateAllCheckedState();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 防止重复请求
|
|
274
|
+
if (pendingPromises.has(departmentId)) {
|
|
275
|
+
await pendingPromises.get(departmentId);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// 删除 departmentCache 的声明
|
|
281
|
+
// const departmentCache = ref(null);
|
|
282
|
+
|
|
283
|
+
const fetchAllDepartments = async () => {
|
|
284
|
+
try {
|
|
285
|
+
const response = await util.get_subdepartments({ departmentId: 1 });
|
|
286
|
+
if (response.code === 200) {
|
|
287
|
+
departmentTree.value = response.data.subDepartments || [];
|
|
288
|
+
currentDepartments.value = departmentTree.value;
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error("初始化部门数据失败:", error);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
userData.value.loading = true;
|
|
295
|
+
const promise = util.get_subdepartments_users({ departmentId });
|
|
296
|
+
pendingPromises.set(departmentId, promise);
|
|
297
|
+
|
|
298
|
+
const response = await promise;
|
|
299
|
+
console.log(
|
|
300
|
+
"---------------userSelector:get_subdepartments_users--------------------------",
|
|
301
|
+
response
|
|
302
|
+
);
|
|
303
|
+
if (response.code === 200) {
|
|
304
|
+
const users = response.data.users || [];
|
|
305
|
+
userCache.set(departmentId, users);
|
|
306
|
+
userData.value.users = users;
|
|
307
|
+
updateAllCheckedState();
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
userData.value.error = error;
|
|
311
|
+
} finally {
|
|
312
|
+
userData.value.loading = false;
|
|
313
|
+
pendingPromises.delete(departmentId);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// 部门选择处理
|
|
318
|
+
const selectDepartment = (dept) => {
|
|
319
|
+
selectionPath.value.push(dept);
|
|
320
|
+
currentDepartments.value = dept.subDepartments || [];
|
|
321
|
+
loadUsers(dept.id);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// 计算属性
|
|
325
|
+
const currentPath = computed(
|
|
326
|
+
() => selectionPath.value.map((d) => d.name).join(" / ") || "请选择部门"
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const currentLevel = computed(() => selectionPath.value.length);
|
|
330
|
+
|
|
331
|
+
const selectedUserInfo = computed(() => {
|
|
332
|
+
if (props.mode === "multiple") {
|
|
333
|
+
return selectedUsers.value.length > 0
|
|
334
|
+
? `${selectedUsers.value.map((u) => u.name).join(", ")}`
|
|
335
|
+
: "";
|
|
336
|
+
}
|
|
337
|
+
return selectedUsers.value[0] ? `${selectedUsers.value[0].name}` : "";
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 全选逻辑
|
|
341
|
+
const toggleAllSelect = () => {
|
|
342
|
+
if (isAllChecked.value) {
|
|
343
|
+
tempSelectedUsers.value = userData.value.users.map((u) => u.userid);
|
|
344
|
+
} else {
|
|
345
|
+
tempSelectedUsers.value = [];
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const updateAllCheckedState = () => {
|
|
350
|
+
const currentUserIds = userData.value.users.map((u) => u.userid);
|
|
351
|
+
isAllChecked.value =
|
|
352
|
+
currentUserIds.length > 0 &&
|
|
353
|
+
currentUserIds.every((id) => tempSelectedUsers.value.includes(id));
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// 用户选择切换
|
|
357
|
+
const toggleUserSelect = (userId) => {
|
|
358
|
+
const index = tempSelectedUsers.value.indexOf(userId);
|
|
359
|
+
if (index > -1) {
|
|
360
|
+
tempSelectedUsers.value.splice(index, 1);
|
|
361
|
+
} else {
|
|
362
|
+
tempSelectedUsers.value.push(userId);
|
|
363
|
+
}
|
|
364
|
+
updateAllCheckedState();
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// 确认选择
|
|
368
|
+
const confirmSelection = () => {
|
|
369
|
+
isInternalChange.value = true; // 标记为内部选择
|
|
370
|
+
if (props.mode === "multiple") {
|
|
371
|
+
// 合并当前选择的用户与已存在的selectedUsers
|
|
372
|
+
const currentSelected = userData.value.users.filter((u) =>
|
|
373
|
+
tempSelectedUsers.value.includes(u.userid)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// 使用Set去重
|
|
377
|
+
const uniqueUsers = new Set([...selectedUsers.value, ...currentSelected]);
|
|
378
|
+
selectedUsers.value = Array.from(uniqueUsers);
|
|
379
|
+
|
|
380
|
+
// 提取所有用户ID并emit
|
|
381
|
+
const allUserIds = selectedUsers.value.map((u) => u.userid);
|
|
382
|
+
emit("update:modelValue", allUserIds);
|
|
383
|
+
} else {
|
|
384
|
+
const selected = userData.value.users.find(
|
|
385
|
+
(u) => u.userid === tempSelectedUser.value
|
|
386
|
+
);
|
|
387
|
+
selectedUsers.value = selected ? [selected] : [];
|
|
388
|
+
emit("update:modelValue", tempSelectedUser.value);
|
|
389
|
+
}
|
|
390
|
+
showPicker.value = false;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const handleClear = () => {
|
|
394
|
+
tempSelectedUsers.value = [""];
|
|
395
|
+
selectedUsers.value = [""];
|
|
396
|
+
isAllChecked.value = false;
|
|
397
|
+
emit("update:modelValue", null);
|
|
398
|
+
showPicker.value = false;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// 添加部门路径查找方法
|
|
402
|
+
function findDeptPath(deptId, tree, path = []) {
|
|
403
|
+
for (const dept of tree) {
|
|
404
|
+
if (dept.id === deptId) {
|
|
405
|
+
return [...path, dept];
|
|
406
|
+
}
|
|
407
|
+
if (dept.subDepartments?.length) {
|
|
408
|
+
const found = findDeptPath(deptId, dept.subDepartments, [...path, dept]);
|
|
409
|
+
if (found) return found;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
// 添加当前部门处理逻辑
|
|
415
|
+
const handleCurrentDeptChange = async (deptId) => {
|
|
416
|
+
const path = findDeptPath(deptId, departmentTree.value);
|
|
417
|
+
if (path) {
|
|
418
|
+
selectionPath.value = path;
|
|
419
|
+
currentDepartments.value = getCurrentDepartments();
|
|
420
|
+
loadUsers(deptId);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// 添加重置到根部门方法
|
|
425
|
+
const resetToRootDepartment = () => {
|
|
426
|
+
selectionPath.value = [];
|
|
427
|
+
currentDepartments.value = departmentTree.value;
|
|
428
|
+
loadUsers(1); // 假设根部门ID为1
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// 监听currentDept变化
|
|
432
|
+
watch(
|
|
433
|
+
() => props.currentDept,
|
|
434
|
+
async (newVal) => {
|
|
435
|
+
if (!departmentCache.value) await fetchAllDepartments();
|
|
436
|
+
|
|
437
|
+
if (newVal) {
|
|
438
|
+
await handleCurrentDeptChange(newVal);
|
|
439
|
+
} else {
|
|
440
|
+
resetToRootDepartment();
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{ immediate: true }
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// 监听弹窗显示状态
|
|
447
|
+
watch(showPicker, (val) => {
|
|
448
|
+
if (val) {
|
|
449
|
+
initTempSelection();
|
|
450
|
+
if (props.mode === "multiple") {
|
|
451
|
+
updateAllCheckedState();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// 监听 modelValue 变化
|
|
457
|
+
watch(
|
|
458
|
+
() => props.modelValue,
|
|
459
|
+
async (newVal, oldVal) => {
|
|
460
|
+
// console.log('watchModelValue',isInternalChange);
|
|
461
|
+
if (newVal !== oldVal && !isInternalChange.value) {
|
|
462
|
+
if (props.mode === "multiple") {
|
|
463
|
+
console.log(
|
|
464
|
+
"-----------------------watchModelValue-------------------",
|
|
465
|
+
newVal
|
|
466
|
+
);
|
|
467
|
+
// 初始化时转换数组为部门分组格式
|
|
468
|
+
const initValues = Array.isArray(newVal) ? newVal : [];
|
|
469
|
+
tempSelectedUsers.value = initValues.reduce((acc, userId) => {
|
|
470
|
+
const deptId = selectedUsers.value.find(
|
|
471
|
+
(u) => u.userid === userId
|
|
472
|
+
)?.mainDepartment;
|
|
473
|
+
if (deptId) {
|
|
474
|
+
acc[deptId] = [...(acc[deptId] || []), userId];
|
|
475
|
+
}
|
|
476
|
+
return acc;
|
|
477
|
+
}, {});
|
|
478
|
+
// 获取所有用户信息
|
|
479
|
+
if (Array.isArray(newVal) && newVal.length > 0) {
|
|
480
|
+
const users = [];
|
|
481
|
+
for (const userId of newVal) {
|
|
482
|
+
console.log(
|
|
483
|
+
"-----------------------watchModelValue:userid-------------------",
|
|
484
|
+
userId,
|
|
485
|
+
newVal
|
|
486
|
+
);
|
|
487
|
+
const user = await getUserInfoById(userId);
|
|
488
|
+
if (user) users.push(user);
|
|
489
|
+
}
|
|
490
|
+
selectedUsers.value = users;
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
tempSelectedUser.value = newVal || null;
|
|
494
|
+
// 获取单个用户信息
|
|
495
|
+
if (newVal) {
|
|
496
|
+
const user = await getUserInfoById(newVal);
|
|
497
|
+
if (user) selectedUsers.value = [user];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 重置标志位
|
|
503
|
+
if (isInternalChange.value) {
|
|
504
|
+
isInternalChange.value = false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// 获取用户信息
|
|
510
|
+
const getUserInfoById = async (userId) => {
|
|
511
|
+
try {
|
|
512
|
+
const response = await util.get_wxuser_by_id({ userId });
|
|
513
|
+
if (response.code === 200) {
|
|
514
|
+
return response.data;
|
|
515
|
+
}
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error("获取用户信息失败:", error);
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const searchUserById = async () => {
|
|
523
|
+
searchUserId.value = searchInput.value;
|
|
524
|
+
if (!searchUserId.value) return;
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
userData.value.loading = true;
|
|
528
|
+
const user = await getUserInfoById(searchUserId.value);
|
|
529
|
+
if (user) {
|
|
530
|
+
userData.value.users = [user];
|
|
531
|
+
// 更新currentPath显示用户部门
|
|
532
|
+
console.log("=========", user);
|
|
533
|
+
|
|
534
|
+
const deptPath = await findDeptPath(
|
|
535
|
+
user.mainDepartment,
|
|
536
|
+
departmentTree.value
|
|
537
|
+
);
|
|
538
|
+
if (deptPath) {
|
|
539
|
+
selectionPath.value = deptPath;
|
|
540
|
+
}
|
|
541
|
+
// 设置标志位,查询后只显示人员列表
|
|
542
|
+
currentDepartments.value = [];
|
|
543
|
+
} else {
|
|
544
|
+
userData.value.users = [];
|
|
545
|
+
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error("查询用户失败:", error);
|
|
548
|
+
} finally {
|
|
549
|
+
userData.value.loading = false;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const handleClearSearch = () => {
|
|
554
|
+
searchUserId.value = "";
|
|
555
|
+
if (selectionPath.value.length > 0) {
|
|
556
|
+
loadUsers(selectionPath.value.slice(-1)[0].id);
|
|
557
|
+
} else {
|
|
558
|
+
loadUsers(departmentTree.value[0]?.id || 1);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
</script>
|
|
562
|
+
|
|
563
|
+
<style scoped>
|
|
564
|
+
.user-popup {
|
|
565
|
+
height: 60vh;
|
|
566
|
+
width: 100%;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.picker-header {
|
|
570
|
+
padding: 10px;
|
|
571
|
+
border-bottom: 1px solid #ebedf0;
|
|
572
|
+
display: flex;
|
|
573
|
+
justify-content: space-between;
|
|
574
|
+
align-items: center;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.scroll-container {
|
|
578
|
+
height: calc(80vh - 150px);
|
|
579
|
+
overflow-y: auto;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.search-container {
|
|
583
|
+
padding: 10px;
|
|
584
|
+
background: #fff;
|
|
585
|
+
display: flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
gap: 8px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
:deep(.search-container .van-search) {
|
|
591
|
+
flex: 1;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.search-btn {
|
|
595
|
+
flex-shrink: 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.select-all-container {
|
|
599
|
+
font-size: 14px;
|
|
600
|
+
padding: 0 16px;
|
|
601
|
+
margin: 10px 0;
|
|
602
|
+
display: flex;
|
|
603
|
+
justify-content: flex-end;
|
|
604
|
+
align-items: center;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
:deep(.van-checkbox__icon--square) {
|
|
608
|
+
border-radius: 4px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
:deep(.van-checkbox__label) {
|
|
612
|
+
margin-left: 8px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
:deep(.van-radio-group) .van-cell,
|
|
616
|
+
:deep(.van-checkbox-group) .van-cell {
|
|
617
|
+
align-items: center;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.current-path {
|
|
621
|
+
flex: 1;
|
|
622
|
+
font-size: 14px;
|
|
623
|
+
color: #323233;
|
|
624
|
+
white-space: nowrap;
|
|
625
|
+
overflow: hidden;
|
|
626
|
+
text-overflow: ellipsis;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.header-right {
|
|
630
|
+
display: flex;
|
|
631
|
+
gap: 8px;
|
|
632
|
+
margin-left: auto;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.cancel-btn {
|
|
636
|
+
color: #969799;
|
|
637
|
+
margin-right: 5px;
|
|
638
|
+
}
|
|
639
|
+
</style>
|