widget.qw 1.0.81 → 1.0.83

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