widget.qw 1.0.24 → 1.0.25

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.
Files changed (101) hide show
  1. package/.env.development +9 -9
  2. package/.env.production +8 -8
  3. package/README.md +73 -73
  4. package/build/style.css +468 -482
  5. package/build/widget.qw.es.js +1628 -2374
  6. package/build/widget.qw.umd.js +1540 -2286
  7. package/index.html +17 -17
  8. package/package.json +47 -47
  9. package/src/App.vue +26 -26
  10. package/src/api/index.js +149 -141
  11. package/src/components/AuditBar.vue +340 -340
  12. package/src/components/BillCard.vue +155 -155
  13. package/src/components/CascaderPicker.vue +87 -87
  14. package/src/components/CascaderPop.vue +319 -330
  15. package/src/components/CheckGroup.vue +113 -113
  16. package/src/components/DatetimePicker/DatetimePop.vue +342 -342
  17. package/src/components/DatetimePicker/index.vue +112 -112
  18. package/src/components/DayDropdown.vue +56 -56
  19. package/src/components/FilePicker.vue +113 -113
  20. package/src/components/Input.vue +71 -71
  21. package/src/components/MonthDropdown.vue +51 -51
  22. package/src/components/MultiPicker.vue +123 -123
  23. package/src/components/SecretNotify.js +5 -5
  24. package/src/components/Sheet.vue +92 -92
  25. package/src/components/SingleApiPicker.vue +112 -112
  26. package/src/components/SinglePicker.vue +102 -102
  27. package/src/components/SingleUserSelector.vue +425 -425
  28. package/src/components/Switch.vue +64 -64
  29. package/src/components/TreePicker.vue +113 -113
  30. package/src/components/UserPicker.vue +106 -106
  31. package/src/components/UserProfile.vue +131 -131
  32. package/src/components/UsersPicker.vue +118 -118
  33. package/src/components/YearDropdown.vue +59 -59
  34. package/src/components/data_selector.vue +280 -280
  35. package/src/components/image_picker.vue +114 -114
  36. package/src/components/images_picker.vue +139 -139
  37. package/src/components/index.js +157 -157
  38. package/src/components/mult_list_selector.vue +155 -155
  39. package/src/components/subdepartment_selector.vue +481 -481
  40. package/src/components/user_selector.vue +639 -639
  41. package/src/components/widget/DataPop.vue +95 -95
  42. package/src/components/widget/TreePop.vue +88 -88
  43. package/src/components/widget/UserPop.vue +347 -347
  44. package/src/main.js +117 -117
  45. package/src/router/index.ts +168 -168
  46. package/src/util/array_util.js +32 -32
  47. package/src/util/auth_util.js +72 -72
  48. package/src/util/bool_util.js +5 -5
  49. package/src/util/bus.js +1 -1
  50. package/src/util/cache_util.js +18 -18
  51. package/src/util/errer_code.js +6 -6
  52. package/src/util/eval_util.js +19 -19
  53. package/src/util/icon_util.js +36 -36
  54. package/src/util/image_util.js +27 -27
  55. package/src/util/index.js +55 -55
  56. package/src/util/num_util.js +70 -70
  57. package/src/util/obj_util.js +28 -28
  58. package/src/util/request.js +73 -73
  59. package/src/util/request_json.js +71 -71
  60. package/src/util/request_json_mute.js +65 -65
  61. package/src/util/request_upload.js +79 -79
  62. package/src/util/route_util.js +31 -31
  63. package/src/util/str_util.js +143 -143
  64. package/src/util/time_util.js +406 -406
  65. package/src/util/toast_util.js +24 -24
  66. package/src/util/tree_util.js +153 -153
  67. package/src/util/validate.js +182 -182
  68. package/src/util/vue_filter.js +223 -223
  69. package/src/views/auditbar/index.vue +65 -65
  70. package/src/views/billcard/index.vue +45 -45
  71. package/src/views/cascaderpicker/index copy.vue +0 -0
  72. package/src/views/cascaderpicker/index.vue +0 -0
  73. package/src/views/cascaderpop/index.vue +90 -90
  74. package/src/views/checkgroup/index.vue +35 -35
  75. package/src/views/dataSelector/index.vue +48 -48
  76. package/src/views/datetimepicker/index.vue +34 -34
  77. package/src/views/daydropdown/index.vue +31 -31
  78. package/src/views/filepicker/index.vue +31 -31
  79. package/src/views/imagepicker/index.vue +31 -31
  80. package/src/views/imagespicker/index.vue +31 -31
  81. package/src/views/input/index.vue +34 -34
  82. package/src/views/monthdropdown/index.vue +31 -31
  83. package/src/views/multListSelector/index.vue +61 -61
  84. package/src/views/multipicker/index.vue +36 -36
  85. package/src/views/productSelector/index.vue +35 -35
  86. package/src/views/projectdropdown/index.vue +31 -31
  87. package/src/views/projectpicker/index.vue +41 -41
  88. package/src/views/secretnotify/index.vue +27 -27
  89. package/src/views/sheet/index.vue +45 -45
  90. package/src/views/singlepicker/index.vue +35 -35
  91. package/src/views/subdepartmentSelector/index.vue +40 -40
  92. package/src/views/switch/index.vue +34 -34
  93. package/src/views/treepicker/index.vue +41 -41
  94. package/src/views/userSelector/index.vue +54 -54
  95. package/src/views/userSelectorNew/index.vue +45 -45
  96. package/src/views/userpicker/index.vue +43 -43
  97. package/src/views/userprofile/index.vue +30 -30
  98. package/src/views/userspicker/index.vue +43 -43
  99. package/src/views/yeardropdown/index.vue +32 -32
  100. package/src/vm/index.js +1 -1
  101. 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>