wui-components-v2 1.1.69 → 1.1.70

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 (45) hide show
  1. package/api/core/index.ts +74 -74
  2. package/api/menu.ts +45 -45
  3. package/api/page.ts +114 -114
  4. package/api/sys.ts +12 -12
  5. package/components/add-address-page/add-address-page.vue +77 -77
  6. package/components/custom-date-picker/custom-date-picker.vue +106 -106
  7. package/components/custom-select-picker/custom-select-picker.vue +95 -95
  8. package/components/demo-block/demo-block.vue +63 -63
  9. package/components/detail-popup/detail-popup.vue +99 -99
  10. package/components/evaluation-page/evaluation-page.vue +196 -196
  11. package/components/fold-card/fold-card.vue +171 -171
  12. package/components/form-control/form-control.vue +661 -661
  13. package/components/global-message/global-message.vue +68 -68
  14. package/components/label-value/label-value.vue +144 -144
  15. package/components/list-top-buttons/list-top-buttons.vue +19 -19
  16. package/components/login-form/login-form.vue +126 -126
  17. package/components/mulselect-picker/mulselect-picker.vue +86 -86
  18. package/components/product-card/product-card.vue +201 -201
  19. package/components/search/search.vue +128 -128
  20. package/components/user-choose/user-choose.vue +1 -1
  21. package/components/wui-enume-select-control/wui-enume-select-control.vue +92 -92
  22. package/components/wui-list/wui-list.vue +235 -235
  23. package/components/wui-menus/wui-menus.vue +247 -247
  24. package/components/wui-menus1/components/navbar.vue +43 -43
  25. package/components/wui-menus1/wui-menus.vue +564 -564
  26. package/components/wui-notify-info/wui-notify-info.vue +280 -280
  27. package/components/wui-search-history-babbar/wui-search-history-babbar.vue +204 -204
  28. package/components/wui-select-list/wui-select-list.vue +310 -310
  29. package/components/wui-select-popup/wui-select-popup.vue +612 -612
  30. package/components/wui-system-settings/wui-system-settings.vue +144 -144
  31. package/components/wui-tabbar/wui-tabbar.vue +106 -106
  32. package/components/wui-tree-page/components/tree-item.vue +238 -238
  33. package/components/wui-user/wui-user.vue +202 -202
  34. package/composables/useCompanyFieldFilter.ts +91 -91
  35. package/composables/useEnumes.ts +2 -2
  36. package/composables/useMenus.ts +193 -193
  37. package/index.ts +83 -83
  38. package/package.json +1 -1
  39. package/static/iconfont/iconfont.css +63 -63
  40. package/store/language.ts +151 -151
  41. package/styles/dark-mode.css +523 -523
  42. package/styles/dark-mode.min.css +1 -1
  43. package/type.ts +2 -2
  44. package/utils/control-tree.ts +2 -2
  45. package/utils/control-type-supportor.ts +148 -148
@@ -1,612 +1,612 @@
1
- <script setup lang="ts">
2
- import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
- import { enums, listData, pageConfig, pageKey } from '../../api/page'
4
- import foldCard from '../fold-card/fold-card.vue'
5
- import Search from '../search/search.vue'
6
- import type { Config, Entities, Enums, FoldCardModel } from '../../type'
7
- import { useGlobalToast } from '../../composables/useGlobalToast'
8
- import listTopButtons from '../list-top-buttons/list-top-buttons.vue'
9
-
10
- defineOptions({
11
- name: 'WuiSelectPopup',
12
- })
13
-
14
- const props = withDefaults(
15
- defineProps<{
16
- modelValue: string | string[] // v-model 绑定值
17
- sourceId: string // 数据源ID
18
- title?: string // 标题(用于popup标题)
19
- label?: string // 左侧标签文字(如"基地名称")
20
- extControlType?: string // 选择模式:relselect(单选)、relselect-extdis等
21
- placeholder?: string // 占位文本
22
- foldCardModel?: FoldCardModel // 折叠卡片模式
23
- readonly?: boolean // 只读模式,禁止打开弹窗
24
- clearable?: boolean // 是否显示清空按钮
25
- defaultSelectFirst?: boolean // 是否默认选择第一项
26
- }>(),
27
- {
28
- title: '',
29
- label: '',
30
- extControlType: 'relselect',
31
- placeholder: '请选择',
32
- foldCardModel: 'simple',
33
- readonly: false,
34
- clearable: true,
35
- defaultSelectFirst: false,
36
- }
37
- )
38
- console.log(props, '3333333333333333333')
39
- const emit = defineEmits<{
40
- (e: 'update:modelValue', value: string | string[]): void
41
- (e: 'confirm', value: string | string[]): void
42
- (e: 'cancel'): void
43
- (e: 'open'): void
44
- (e: 'close'): void
45
- }>()
46
-
47
- const globalToast = useGlobalToast()
48
- const Zpaging = ref<any>(null)
49
- const config = ref<Config>({
50
- columns: [],
51
- primaryColumn: {
52
- sourceId: '',
53
- controlType: '',
54
- mstrucId: '',
55
- extControlType: '',
56
- },
57
- secondColumn: {
58
- sourceId: '',
59
- controlType: '',
60
- mstrucId: '',
61
- extControlType: '',
62
- },
63
- primaryCriteria: {
64
- sourceId: '',
65
- controlType: '',
66
- mstrucId: '',
67
- disabled: false,
68
- defaultValue: '',
69
- transDefaultValue: '',
70
- required: false,
71
- extControlType: '',
72
- hidden: false,
73
- },
74
- criterias: [],
75
- buttons: [],
76
- id: '',
77
- fields: [],
78
- title: '',
79
- type: '',
80
- pointSourceId: '',
81
- mstrucId: '',
82
- relationNames: [],
83
- readOnly: false,
84
- displayConfig: [],
85
- showType: 'table',
86
- extShowConfig: {},
87
- extDisplayConfig: {},
88
- })
89
- const datas = ref<Entities[]>([])
90
- const searchData = ref('')
91
- const enumColumn = ref<Enums>({})
92
- const selectData = ref<Record<string, string>>({}) // 选择数据
93
-
94
- // ★ 关键修改:使用独立的 ref 管理显示状态(参考 action-popup)
95
- const show = ref(false)
96
-
97
- // 格式化显示的选中值
98
- const displayValue = computed(() => {
99
- const values = Object.values(selectData.value)
100
- if (!values.length) return ''
101
-
102
- if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
103
- return values[0]?.split('@R@')[1] || ''
104
- }
105
- return values.map(v => v.split('@R@')[1]).join(', ')
106
- })
107
-
108
- // 格式化选中的数据用于checkbox显示
109
- const selectDataFormat = computed(() => {
110
- const newdata: Record<string, string> = {}
111
- if (typeof selectData.value === 'object' && selectData.value !== null) {
112
- Object.values(selectData.value).forEach(item => {
113
- newdata[item.split('@R@')[0]] = item.split('@R@')[0]
114
- })
115
- }
116
- return newdata
117
- })
118
-
119
- // 监听外部modelValue变化,同步到内部选择数据
120
- watch(
121
- () => props.modelValue,
122
- val => {
123
- if (!val || (Array.isArray(val) && !val.length)) {
124
- selectData.value = {}
125
- return
126
- }
127
-
128
- const newData: Record<string, string> = {}
129
- if (Array.isArray(val)) {
130
- val.forEach(item => {
131
- if (item && typeof item === 'string') {
132
- const code = item.split('@R@')[0]
133
- if (code) newData[code] = item
134
- }
135
- })
136
- } else if (typeof val === 'string' && val) {
137
- const code = val.split('@R@')[0]
138
- if (code) newData[code] = val
139
- }
140
- selectData.value = newData
141
- },
142
- { immediate: true }
143
- )
144
-
145
- // 显示地址信息
146
- const showAddressInfo = computed(() => {
147
- return props.extControlType === 'relselect-extdis'
148
- })
149
-
150
- // checkbox类型
151
- const checkboxType = computed(() => {
152
- return ['relselect', 'relselect-extdis'].includes(props.extControlType) ? '' : 'square'
153
- })
154
-
155
- // 格式化地址
156
- const formatAddress = (value: string) => {
157
- return (value?.split(',') || [])[2] || ''
158
- }
159
-
160
- // 标记是否已获取配置
161
- const isConfigLoaded = ref(false)
162
- // ★ 防止重复加载
163
- const isLoading = ref(false)
164
- // ★ 防止 z-paging 初始化时与手动 reload 重复请求
165
- const hasInitialized = ref(false)
166
-
167
- // ★ 是否已完成默认首项选中
168
- const hasDefaultSelected = ref(false)
169
-
170
- // ★ 默认选择第一项:数据加载完成后自动选中第一条并回显
171
- watch(datas, val => {
172
- if (!props.defaultSelectFirst || !val?.length || hasDefaultSelected.value || Object.keys(selectData.value).length > 0)
173
- return
174
- const first = val[0]
175
- selectData.value[first.code] = `${first.code}@R@${first.fieldMap[config.value.primaryColumn.sourceId]}`
176
- const result =
177
- props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis'
178
- ? selectData.value[first.code]
179
- : Object.values(selectData.value)
180
- emit('update:modelValue', result)
181
- hasDefaultSelected.value = true
182
- })
183
-
184
- // 获取页面配置
185
- async function getPageConfig() {
186
- if (isConfigLoaded.value && config.value.id) return // 避免重复请求
187
-
188
- try {
189
- const res = await pageConfig(props.sourceId)
190
- if (res.ltmplConfig) {
191
- config.value = res.ltmplConfig
192
- isConfigLoaded.value = true
193
- await getEnums()
194
- console.log('[WuiSelectPopup] ✅ 配置加载完成', config.value.title)
195
- }
196
- } catch (error) {
197
- console.error('[WuiSelectPopup] ❌ 配置加载失败:', error)
198
- throw error
199
- }
200
- }
201
-
202
- // 获取枚举数据
203
- async function getEnums() {
204
- try {
205
- const params: string[] = []
206
- config.value.criterias?.forEach((item: any) => {
207
- if (item.controlType === 'select' || item.controlType === 'multiselect') {
208
- params.push(`mstrucIds=${item.mstrucId}`)
209
- }
210
- })
211
-
212
- if (!params.length) return
213
- const res = await enums(params.join('&'))
214
- enumColumn.value = res.enumMap || {}
215
- } catch (error) {
216
- console.log('error:', error)
217
- }
218
- }
219
-
220
- // 查询列表数据(完整流程:config -> key -> data)
221
- async function queryList(pageNo: number, pageSize: number) {
222
- // ★ 只防正在进行的重复请求(分页不拦截)
223
- if (pageNo === 1 && isLoading.value) return
224
-
225
- // ★ z-paging 组件初始化时自动触发 @query,popup 未打开时跳过
226
- // (只跳过第一次,后续打开 popup 后的查询正常放行)
227
- if (!show.value && !hasInitialized.value) {
228
- hasInitialized.value = true
229
- Zpaging.value.complete([])
230
- return
231
- }
232
-
233
- if (pageNo === 1) isLoading.value = true
234
-
235
- try {
236
- // 步骤1:确保配置已加载
237
- if (!isConfigLoaded.value) {
238
- console.log('[WuiSelectPopup] 正在加载配置...')
239
- await getPageConfig()
240
- console.log('[WuiSelectPopup] 配置已加载,开始查询数据')
241
- }
242
-
243
- // 步骤2:构建查询参数
244
- const search = uni.getStorageSync('paramsData')?.changedData || {}
245
- const newParams = {
246
- thirdCriteria: search.relValueField3 || '',
247
- secondCriteria: search.relValueField?.toString() || '',
248
- }
249
-
250
- const params = Object.entries(newParams)
251
- .map(([key, value]) => `${key}=${value}`)
252
- .join('&')
253
- let qre = searchData.value ? `${searchData.value}&${params}` : params
254
-
255
- if (config.value.defaultCriteriaValue) {
256
- for (const key in config.value.defaultCriteriaValue) {
257
- if (Object.prototype.hasOwnProperty.call(config.value.defaultCriteriaValue, key)) {
258
- const element = config.value.defaultCriteriaValue[key]
259
- if (!qre.includes(key)) {
260
- qre = `${qre}&${key}=${element}`
261
- }
262
- }
263
- }
264
- }
265
-
266
- // 步骤3:获取查询Key
267
- console.log('[WuiSelectPopup] 请求 pageKey,sourceId:', props.sourceId)
268
- const res = await pageKey(props.sourceId, '', qre)
269
- if (!res?.key) {
270
- throw new Error('未获取到查询Key')
271
- }
272
- console.log('[WuiSelectPopup] 获取到 key:', res.key)
273
-
274
- // 步骤4:使用Key请求数据
275
- const data = await listData(res.key, pageNo, pageSize)
276
- console.log(`[WuiSelectPopup] 获取到数据,第${pageNo}页,共 ${data.entities?.length} 条`)
277
- Zpaging.value.complete(data.entities)
278
- } catch (error) {
279
- console.error('[WuiSelectPopup] ❌ 数据加载失败:', error)
280
- Zpaging.value.complete(false)
281
- } finally {
282
- if (pageNo === 1) isLoading.value = false
283
- }
284
- }
285
-
286
- // 处理选项变化
287
- function handleChange(item: Entities) {
288
- if (selectData.value[item.code]) {
289
- delete selectData.value[item.code]
290
- return
291
- }
292
-
293
- // 单选模式清空已有选择
294
- if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
295
- selectData.value = {}
296
- }
297
-
298
- selectData.value[item.code] = `${item.code}@R@${item.fieldMap[config.value.primaryColumn.sourceId]}`
299
- }
300
-
301
- // 提交确认
302
- function confirm() {
303
- const keys = Object.keys(selectData.value)
304
- if (!keys.length) {
305
- globalToast.warning(`请选择${props.title}`)
306
- return
307
- }
308
-
309
- const payload = Object.values(selectData.value)
310
-
311
- // 单选返回字符串,多选返回数组
312
- let result: string | string[]
313
- if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
314
- result = payload[0]
315
- } else {
316
- result = payload
317
- }
318
-
319
- emit('update:modelValue', result)
320
- emit('confirm', result)
321
- show.value = false
322
- }
323
-
324
- // 取消
325
- function cancel() {
326
- emit('cancel')
327
- show.value = false
328
- }
329
-
330
- // 清空选择
331
- function clear() {
332
- selectData.value = {}
333
- emit(
334
- 'update:modelValue',
335
- props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis' ? '' : []
336
- )
337
- }
338
-
339
- // 搜索提交
340
- function submitSearch(data: any) {
341
- searchData.value = data
342
- Zpaging.value.reload()
343
- }
344
-
345
- // ★ 打开 popup - 直接操作内部 ref
346
- function openPopup(e?: any) {
347
- e?.stopPropagation?.()
348
- if (props.readonly) return
349
- console.log('[WuiSelectPopup] 🎯 点击触发打开 Popup')
350
- show.value = true
351
- emit('open')
352
- }
353
-
354
- // 打开popup时初始化数据
355
- watch(show, async val => {
356
- console.log('[WuiSelectPopup] 📢 show 状态变化:', val)
357
- if (val) {
358
- if (isLoading.value) return // ★ 防重复(正在请求中不重复触发)
359
- await nextTick()
360
- // ★ 不重置 isConfigLoaded(配置只需加载一次)
361
- hasInitialized.value = false // ★ 允许 reload 触发查询
362
- if (Zpaging.value) {
363
- Zpaging.value.reload()
364
- } else {
365
- setTimeout(() => {
366
- Zpaging.value?.reload()
367
- }, 300)
368
- }
369
- } else {
370
- emit('close')
371
- }
372
- })
373
-
374
- // ★ 监听 edit-page 返回后刷新数据(通过 sourceId 过滤,只响应自己的刷新)
375
- function handleRefresh(refreshSourceId: string) {
376
- if (refreshSourceId !== props.sourceId) return
377
- console.log('[WuiSelectPopup] 🔄 收到刷新信号,重新加载数据')
378
- if (show.value && Zpaging.value && !isLoading.value) {
379
- isConfigLoaded.value = false // ★ 刷新时重新加载配置
380
- hasInitialized.value = false // ★ 允许 reload 触发
381
- Zpaging.value.reload()
382
- }
383
- }
384
-
385
- onMounted(() => {
386
- uni.$on('selectPopup:refresh', handleRefresh)
387
- })
388
-
389
- onUnmounted(() => {
390
- uni.$off('selectPopup:refresh', handleRefresh)
391
- uni.removeStorageSync('paramsData')
392
- })
393
-
394
- // 暴露方法给父组件
395
- defineExpose({
396
- clear,
397
- confirm,
398
- cancel,
399
- open: openPopup,
400
- close: () => {
401
- show.value = false
402
- },
403
- })
404
- </script>
405
-
406
- <template>
407
- <view class="wui-select-popup-wrapper dark:bg-[var(--wot-dark-background)]">
408
- <!-- ★ 触发区域 - 原生表单样式(左标签 + 中内容 + 右箭头) -->
409
- <view v-if="!show" class="trigger-cell" @click="openPopup">
410
- <!-- 左侧标签 -->
411
- <text v-if="label" class="cell-label dark:text-white">
412
- {{ label }}
413
- </text>
414
-
415
- <!-- 中间内容区 -->
416
- <view class="cell-content dark:text-white">
417
- <text v-if="displayValue && displayValue !== 'null'" class="cell-value">
418
- {{ displayValue || '' }}
419
- </text>
420
- <text v-else class="cell-placeholder">
421
- {{ placeholder || `请选择${title || label}` }}
422
- </text>
423
- </view>
424
-
425
- <!-- 右侧箭头 -->
426
- <view class="cell-arrow">
427
- <wd-icon v-if="clearable && displayValue" name="close-circle" size="16px" @click.stop="clear" />
428
- <wd-icon name="right" v-else size="16px"></wd-icon>
429
- </view>
430
- </view>
431
-
432
- <!-- ★ 底部弹出的Popup - 修复内容溢出 -->
433
- <wd-popup
434
- v-model="show"
435
- position="bottom"
436
- :z-index="999999"
437
- custom-style="border-radius: 24rpx 24rpx 0 0; overflow: hidden;"
438
- class="dark:bg-[#1b1b1e]"
439
- safe-area-inset-bottom
440
- @close="
441
- () => {
442
- show = false
443
- }
444
- "
445
- >
446
- <view class="select-popup-container dark:bg-[#1b1b1e]">
447
- <!-- 标题栏 - 固定高度不参与滚动 -->
448
- <view
449
- class="popup-header flex items-center justify-between px-4 py-3 border-b border-gray-200 shrink-0 dark:text-white"
450
- >
451
- <text class="text-base font-bold">选择{{ title }}</text>
452
- <wd-icon name="close-circle" size="16px" @click="cancel" />
453
- </view>
454
-
455
- <!-- 列表内容区 - 占据剩余空间并可滚动 -->
456
- <view class="popup-body">
457
- <z-paging
458
- ref="Zpaging"
459
- v-model="datas"
460
- :show-loading-more-when-reload="true"
461
- :fixed="false"
462
- @query="queryList"
463
- >
464
- <template #top>
465
- <Search
466
- :enum-column="enumColumn"
467
- :criterias="config.criterias"
468
- :primary-criteria="config.primaryCriteria"
469
- @submit="submitSearch"
470
- />
471
- <view>
472
- <listTopButtons :buttons="config.buttons" :source-id="sourceId" :page-title="pageTitle" />
473
- </view>
474
- <slot name="top" />
475
- </template>
476
-
477
- <foldCard
478
- v-for="(item, index) in datas"
479
- :key="item.code"
480
- :index="index"
481
- :data="item"
482
- :columns="config.columns"
483
- :primary-column="config.primaryColumn"
484
- :second-column="config.secondColumn"
485
- :model="foldCardModel"
486
- :groups="config"
487
- :source-id="sourceId"
488
- @click="handleChange(item)"
489
- >
490
- <template #select>
491
- <wd-checkbox
492
- :true-value="`${item.code}`"
493
- false-value=""
494
- :model-value="selectDataFormat[item.code]"
495
- :type="checkboxType"
496
- />
497
- </template>
498
-
499
- <template #addressInfo v-if="showAddressInfo">
500
- <div>{{ item.fieldMap[config.extDisplayConfig?.secondColumn?.sourceId] }}</div>
501
- </template>
502
- <template #detailAddress v-if="showAddressInfo">
503
- <div>{{ formatAddress(item.fieldMap[config.extDisplayConfig?.cardShowCols?.[0]?.sourceId]) }}</div>
504
- </template>
505
- </foldCard>
506
-
507
- <template #bottom>
508
- <view class="popup-footer flex justify-center gap-4 p-4 border-t border-gray-200">
509
- <wd-button plain custom-class="flex-1" @click="cancel">取消</wd-button>
510
- <wd-button type="primary" custom-class="flex-1" @click="confirm">确定</wd-button>
511
- </view>
512
- </template>
513
- </z-paging>
514
- </view>
515
- </view>
516
- </wd-popup>
517
- </view>
518
- </template>
519
-
520
- <style scoped lang="scss">
521
- .wui-select-popup-wrapper {
522
- width: 100%;
523
- position: relative;
524
- }
525
-
526
- // ★ 触发区域 - 原生表单样式(类似微信picker)
527
- .trigger-cell {
528
- display: flex;
529
- align-items: center;
530
- justify-content: space-between;
531
- cursor: pointer;
532
- position: relative;
533
- transition: background-color 0.2s;
534
-
535
- &:active {
536
- background-color: #f5f7fa; // 点击反馈
537
- }
538
-
539
- // 左侧标签 - 固定宽度,不换行
540
- .cell-label {
541
- flex-shrink: 0;
542
- margin-right: 24rpx;
543
- white-space: nowrap;
544
- }
545
-
546
- // 中间内容区 - 占据剩余空间
547
- .cell-content {
548
- flex: 1;
549
- min-width: 0; // 允许文本溢出省略
550
-
551
- .cell-value {
552
- text-align: left;
553
- overflow: hidden;
554
- text-overflow: ellipsis;
555
- white-space: nowrap;
556
- display: block;
557
- }
558
-
559
- .cell-placeholder {
560
- color: #c0c4cc; // 灰色占位符
561
- text-align: left;
562
- }
563
- }
564
-
565
- // 右侧箭头
566
- .cell-arrow {
567
- flex-shrink: 0;
568
- margin-left: 16rpx;
569
- display: flex;
570
- align-items: center;
571
-
572
- .arrow-text {
573
- color: #c0c4cc;
574
- transform: scaleX(0.8); // 稍微压窄箭头
575
- }
576
- }
577
- }
578
-
579
- // Popup 容器 - 使用 flex 布局控制溢出
580
- .select-popup-container {
581
- display: flex;
582
- flex-direction: column;
583
- height: 75vh; // 固定高度
584
- max-height: 80vh; // 最大高度
585
- overflow: hidden; // 防止内容溢出
586
- }
587
-
588
- // 标题栏
589
- .popup-header {
590
- flex-shrink: 0; // 不压缩
591
- z-index: 10;
592
- }
593
-
594
- // 内容区域 - 可滚动
595
- .popup-body {
596
- flex: 1; // 占据剩余空间
597
- overflow: hidden; // 超出隐藏,由 z-paging 管理滚动
598
- min-height: 0; // ★ 关键:允许 flex 子元素收缩
599
-
600
- // 让 z-paging 填满整个容器
601
- :deep(.z-paging-content) {
602
- height: 100% !important;
603
- }
604
- }
605
-
606
- // 底部按钮区
607
- .popup-footer {
608
- flex-shrink: 0; // 不压缩
609
- position: sticky;
610
- bottom: 0;
611
- }
612
- </style>
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import { enums, listData, pageConfig, pageKey } from '../../api/page'
4
+ import foldCard from '../fold-card/fold-card.vue'
5
+ import Search from '../search/search.vue'
6
+ import type { Config, Entities, Enums, FoldCardModel } from '../../type'
7
+ import { useGlobalToast } from '../../composables/useGlobalToast'
8
+ import listTopButtons from '../list-top-buttons/list-top-buttons.vue'
9
+
10
+ defineOptions({
11
+ name: 'WuiSelectPopup',
12
+ })
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ modelValue: string | string[] // v-model 绑定值
17
+ sourceId: string // 数据源ID
18
+ title?: string // 标题(用于popup标题)
19
+ label?: string // 左侧标签文字(如"基地名称")
20
+ extControlType?: string // 选择模式:relselect(单选)、relselect-extdis等
21
+ placeholder?: string // 占位文本
22
+ foldCardModel?: FoldCardModel // 折叠卡片模式
23
+ readonly?: boolean // 只读模式,禁止打开弹窗
24
+ clearable?: boolean // 是否显示清空按钮
25
+ defaultSelectFirst?: boolean // 是否默认选择第一项
26
+ }>(),
27
+ {
28
+ title: '',
29
+ label: '',
30
+ extControlType: 'relselect',
31
+ placeholder: '请选择',
32
+ foldCardModel: 'simple',
33
+ readonly: false,
34
+ clearable: true,
35
+ defaultSelectFirst: false,
36
+ }
37
+ )
38
+ console.log(props, '3333333333333333333')
39
+ const emit = defineEmits<{
40
+ (e: 'update:modelValue', value: string | string[]): void
41
+ (e: 'confirm', value: string | string[]): void
42
+ (e: 'cancel'): void
43
+ (e: 'open'): void
44
+ (e: 'close'): void
45
+ }>()
46
+
47
+ const globalToast = useGlobalToast()
48
+ const Zpaging = ref<any>(null)
49
+ const config = ref<Config>({
50
+ columns: [],
51
+ primaryColumn: {
52
+ sourceId: '',
53
+ controlType: '',
54
+ mstrucId: '',
55
+ extControlType: '',
56
+ },
57
+ secondColumn: {
58
+ sourceId: '',
59
+ controlType: '',
60
+ mstrucId: '',
61
+ extControlType: '',
62
+ },
63
+ primaryCriterion: {
64
+ sourceId: '',
65
+ controlType: '',
66
+ mstrucId: '',
67
+ disabled: false,
68
+ defaultValue: '',
69
+ transDefaultValue: '',
70
+ required: false,
71
+ extControlType: '',
72
+ hidden: false,
73
+ },
74
+ criteria: [],
75
+ buttons: [],
76
+ id: '',
77
+ fields: [],
78
+ title: '',
79
+ type: '',
80
+ pointSourceId: '',
81
+ mstrucId: '',
82
+ relationNames: [],
83
+ readOnly: false,
84
+ displayConfig: [],
85
+ showType: 'table',
86
+ extShowConfig: {},
87
+ extDisplayConfig: {},
88
+ })
89
+ const datas = ref<Entities[]>([])
90
+ const searchData = ref('')
91
+ const enumColumn = ref<Enums>({})
92
+ const selectData = ref<Record<string, string>>({}) // 选择数据
93
+
94
+ // ★ 关键修改:使用独立的 ref 管理显示状态(参考 action-popup)
95
+ const show = ref(false)
96
+
97
+ // 格式化显示的选中值
98
+ const displayValue = computed(() => {
99
+ const values = Object.values(selectData.value)
100
+ if (!values.length) return ''
101
+
102
+ if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
103
+ return values[0]?.split('@R@')[1] || ''
104
+ }
105
+ return values.map(v => v.split('@R@')[1]).join(', ')
106
+ })
107
+
108
+ // 格式化选中的数据用于checkbox显示
109
+ const selectDataFormat = computed(() => {
110
+ const newdata: Record<string, string> = {}
111
+ if (typeof selectData.value === 'object' && selectData.value !== null) {
112
+ Object.values(selectData.value).forEach(item => {
113
+ newdata[item.split('@R@')[0]] = item.split('@R@')[0]
114
+ })
115
+ }
116
+ return newdata
117
+ })
118
+
119
+ // 监听外部modelValue变化,同步到内部选择数据
120
+ watch(
121
+ () => props.modelValue,
122
+ val => {
123
+ if (!val || (Array.isArray(val) && !val.length)) {
124
+ selectData.value = {}
125
+ return
126
+ }
127
+
128
+ const newData: Record<string, string> = {}
129
+ if (Array.isArray(val)) {
130
+ val.forEach(item => {
131
+ if (item && typeof item === 'string') {
132
+ const code = item.split('@R@')[0]
133
+ if (code) newData[code] = item
134
+ }
135
+ })
136
+ } else if (typeof val === 'string' && val) {
137
+ const code = val.split('@R@')[0]
138
+ if (code) newData[code] = val
139
+ }
140
+ selectData.value = newData
141
+ },
142
+ { immediate: true }
143
+ )
144
+
145
+ // 显示地址信息
146
+ const showAddressInfo = computed(() => {
147
+ return props.extControlType === 'relselect-extdis'
148
+ })
149
+
150
+ // checkbox类型
151
+ const checkboxType = computed(() => {
152
+ return ['relselect', 'relselect-extdis'].includes(props.extControlType) ? '' : 'square'
153
+ })
154
+
155
+ // 格式化地址
156
+ const formatAddress = (value: string) => {
157
+ return (value?.split(',') || [])[2] || ''
158
+ }
159
+
160
+ // 标记是否已获取配置
161
+ const isConfigLoaded = ref(false)
162
+ // ★ 防止重复加载
163
+ const isLoading = ref(false)
164
+ // ★ 防止 z-paging 初始化时与手动 reload 重复请求
165
+ const hasInitialized = ref(false)
166
+
167
+ // ★ 是否已完成默认首项选中
168
+ const hasDefaultSelected = ref(false)
169
+
170
+ // ★ 默认选择第一项:数据加载完成后自动选中第一条并回显
171
+ watch(datas, val => {
172
+ if (!props.defaultSelectFirst || !val?.length || hasDefaultSelected.value || Object.keys(selectData.value).length > 0)
173
+ return
174
+ const first = val[0]
175
+ selectData.value[first.code] = `${first.code}@R@${first.fieldMap[config.value.primaryColumn.sourceId]}`
176
+ const result =
177
+ props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis'
178
+ ? selectData.value[first.code]
179
+ : Object.values(selectData.value)
180
+ emit('update:modelValue', result)
181
+ hasDefaultSelected.value = true
182
+ })
183
+
184
+ // 获取页面配置
185
+ async function getPageConfig() {
186
+ if (isConfigLoaded.value && config.value.id) return // 避免重复请求
187
+
188
+ try {
189
+ const res = await pageConfig(props.sourceId)
190
+ if (res.ltmplConfig) {
191
+ config.value = res.ltmplConfig
192
+ isConfigLoaded.value = true
193
+ await getEnums()
194
+ console.log('[WuiSelectPopup] ✅ 配置加载完成', config.value.title)
195
+ }
196
+ } catch (error) {
197
+ console.error('[WuiSelectPopup] ❌ 配置加载失败:', error)
198
+ throw error
199
+ }
200
+ }
201
+
202
+ // 获取枚举数据
203
+ async function getEnums() {
204
+ try {
205
+ const params: string[] = []
206
+ config.value.criteria?.forEach((item: any) => {
207
+ if (item.controlType === 'select' || item.controlType === 'multiselect') {
208
+ params.push(`mstrucIds=${item.mstrucId}`)
209
+ }
210
+ })
211
+
212
+ if (!params.length) return
213
+ const res = await enums(params.join('&'))
214
+ enumColumn.value = res.enumMap || {}
215
+ } catch (error) {
216
+ console.log('error:', error)
217
+ }
218
+ }
219
+
220
+ // 查询列表数据(完整流程:config -> key -> data)
221
+ async function queryList(pageNo: number, pageSize: number) {
222
+ // ★ 只防正在进行的重复请求(分页不拦截)
223
+ if (pageNo === 1 && isLoading.value) return
224
+
225
+ // ★ z-paging 组件初始化时自动触发 @query,popup 未打开时跳过
226
+ // (只跳过第一次,后续打开 popup 后的查询正常放行)
227
+ if (!show.value && !hasInitialized.value) {
228
+ hasInitialized.value = true
229
+ Zpaging.value.complete([])
230
+ return
231
+ }
232
+
233
+ if (pageNo === 1) isLoading.value = true
234
+
235
+ try {
236
+ // 步骤1:确保配置已加载
237
+ if (!isConfigLoaded.value) {
238
+ console.log('[WuiSelectPopup] 正在加载配置...')
239
+ await getPageConfig()
240
+ console.log('[WuiSelectPopup] 配置已加载,开始查询数据')
241
+ }
242
+
243
+ // 步骤2:构建查询参数
244
+ const search = uni.getStorageSync('paramsData')?.changedData || {}
245
+ const newParams = {
246
+ thirdCriterion: search.relValueField3 || '',
247
+ secondCriterion: search.relValueField?.toString() || '',
248
+ }
249
+
250
+ const params = Object.entries(newParams)
251
+ .map(([key, value]) => `${key}=${value}`)
252
+ .join('&')
253
+ let qre = searchData.value ? `${searchData.value}&${params}` : params
254
+
255
+ if (config.value.defaultCriteriaValue) {
256
+ for (const key in config.value.defaultCriteriaValue) {
257
+ if (Object.prototype.hasOwnProperty.call(config.value.defaultCriteriaValue, key)) {
258
+ const element = config.value.defaultCriteriaValue[key]
259
+ if (!qre.includes(key)) {
260
+ qre = `${qre}&${key}=${element}`
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // 步骤3:获取查询Key
267
+ console.log('[WuiSelectPopup] 请求 pageKey,sourceId:', props.sourceId)
268
+ const res = await pageKey(props.sourceId, '', qre)
269
+ if (!res?.key) {
270
+ throw new Error('未获取到查询Key')
271
+ }
272
+ console.log('[WuiSelectPopup] 获取到 key:', res.key)
273
+
274
+ // 步骤4:使用Key请求数据
275
+ const data = await listData(res.key, pageNo, pageSize)
276
+ console.log(`[WuiSelectPopup] 获取到数据,第${pageNo}页,共 ${data.entities?.length} 条`)
277
+ Zpaging.value.complete(data.entities)
278
+ } catch (error) {
279
+ console.error('[WuiSelectPopup] ❌ 数据加载失败:', error)
280
+ Zpaging.value.complete(false)
281
+ } finally {
282
+ if (pageNo === 1) isLoading.value = false
283
+ }
284
+ }
285
+
286
+ // 处理选项变化
287
+ function handleChange(item: Entities) {
288
+ if (selectData.value[item.code]) {
289
+ delete selectData.value[item.code]
290
+ return
291
+ }
292
+
293
+ // 单选模式清空已有选择
294
+ if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
295
+ selectData.value = {}
296
+ }
297
+
298
+ selectData.value[item.code] = `${item.code}@R@${item.fieldMap[config.value.primaryColumn.sourceId]}`
299
+ }
300
+
301
+ // 提交确认
302
+ function confirm() {
303
+ const keys = Object.keys(selectData.value)
304
+ if (!keys.length) {
305
+ globalToast.warning(`请选择${props.title}`)
306
+ return
307
+ }
308
+
309
+ const payload = Object.values(selectData.value)
310
+
311
+ // 单选返回字符串,多选返回数组
312
+ let result: string | string[]
313
+ if (props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis') {
314
+ result = payload[0]
315
+ } else {
316
+ result = payload
317
+ }
318
+
319
+ emit('update:modelValue', result)
320
+ emit('confirm', result)
321
+ show.value = false
322
+ }
323
+
324
+ // 取消
325
+ function cancel() {
326
+ emit('cancel')
327
+ show.value = false
328
+ }
329
+
330
+ // 清空选择
331
+ function clear() {
332
+ selectData.value = {}
333
+ emit(
334
+ 'update:modelValue',
335
+ props.extControlType === 'relselect' || props.extControlType === 'relselect-extdis' ? '' : []
336
+ )
337
+ }
338
+
339
+ // 搜索提交
340
+ function submitSearch(data: any) {
341
+ searchData.value = data
342
+ Zpaging.value.reload()
343
+ }
344
+
345
+ // ★ 打开 popup - 直接操作内部 ref
346
+ function openPopup(e?: any) {
347
+ e?.stopPropagation?.()
348
+ if (props.readonly) return
349
+ console.log('[WuiSelectPopup] 🎯 点击触发打开 Popup')
350
+ show.value = true
351
+ emit('open')
352
+ }
353
+
354
+ // 打开popup时初始化数据
355
+ watch(show, async val => {
356
+ console.log('[WuiSelectPopup] 📢 show 状态变化:', val)
357
+ if (val) {
358
+ if (isLoading.value) return // ★ 防重复(正在请求中不重复触发)
359
+ await nextTick()
360
+ // ★ 不重置 isConfigLoaded(配置只需加载一次)
361
+ hasInitialized.value = false // ★ 允许 reload 触发查询
362
+ if (Zpaging.value) {
363
+ Zpaging.value.reload()
364
+ } else {
365
+ setTimeout(() => {
366
+ Zpaging.value?.reload()
367
+ }, 300)
368
+ }
369
+ } else {
370
+ emit('close')
371
+ }
372
+ })
373
+
374
+ // ★ 监听 edit-page 返回后刷新数据(通过 sourceId 过滤,只响应自己的刷新)
375
+ function handleRefresh(refreshSourceId: string) {
376
+ if (refreshSourceId !== props.sourceId) return
377
+ console.log('[WuiSelectPopup] 🔄 收到刷新信号,重新加载数据')
378
+ if (show.value && Zpaging.value && !isLoading.value) {
379
+ isConfigLoaded.value = false // ★ 刷新时重新加载配置
380
+ hasInitialized.value = false // ★ 允许 reload 触发
381
+ Zpaging.value.reload()
382
+ }
383
+ }
384
+
385
+ onMounted(() => {
386
+ uni.$on('selectPopup:refresh', handleRefresh)
387
+ })
388
+
389
+ onUnmounted(() => {
390
+ uni.$off('selectPopup:refresh', handleRefresh)
391
+ uni.removeStorageSync('paramsData')
392
+ })
393
+
394
+ // 暴露方法给父组件
395
+ defineExpose({
396
+ clear,
397
+ confirm,
398
+ cancel,
399
+ open: openPopup,
400
+ close: () => {
401
+ show.value = false
402
+ },
403
+ })
404
+ </script>
405
+
406
+ <template>
407
+ <view class="wui-select-popup-wrapper dark:bg-[var(--wot-dark-background)]">
408
+ <!-- ★ 触发区域 - 原生表单样式(左标签 + 中内容 + 右箭头) -->
409
+ <view v-if="!show" class="trigger-cell" @click="openPopup">
410
+ <!-- 左侧标签 -->
411
+ <text v-if="label" class="cell-label dark:text-white">
412
+ {{ label }}
413
+ </text>
414
+
415
+ <!-- 中间内容区 -->
416
+ <view class="cell-content dark:text-white">
417
+ <text v-if="displayValue && displayValue !== 'null'" class="cell-value">
418
+ {{ displayValue || '' }}
419
+ </text>
420
+ <text v-else class="cell-placeholder">
421
+ {{ placeholder || `请选择${title || label}` }}
422
+ </text>
423
+ </view>
424
+
425
+ <!-- 右侧箭头 -->
426
+ <view class="cell-arrow">
427
+ <wd-icon v-if="clearable && displayValue" name="close-circle" size="16px" @click.stop="clear" />
428
+ <wd-icon name="right" v-else size="16px"></wd-icon>
429
+ </view>
430
+ </view>
431
+
432
+ <!-- ★ 底部弹出的Popup - 修复内容溢出 -->
433
+ <wd-popup
434
+ v-model="show"
435
+ position="bottom"
436
+ :z-index="999999"
437
+ custom-style="border-radius: 24rpx 24rpx 0 0; overflow: hidden;"
438
+ class="dark:bg-[#1b1b1e]"
439
+ safe-area-inset-bottom
440
+ @close="
441
+ () => {
442
+ show = false
443
+ }
444
+ "
445
+ >
446
+ <view class="select-popup-container dark:bg-[#1b1b1e]">
447
+ <!-- 标题栏 - 固定高度不参与滚动 -->
448
+ <view
449
+ class="popup-header flex items-center justify-between px-4 py-3 border-b border-gray-200 shrink-0 dark:text-white"
450
+ >
451
+ <text class="text-base font-bold">选择{{ title }}</text>
452
+ <wd-icon name="close-circle" size="16px" @click="cancel" />
453
+ </view>
454
+
455
+ <!-- 列表内容区 - 占据剩余空间并可滚动 -->
456
+ <view class="popup-body">
457
+ <z-paging
458
+ ref="Zpaging"
459
+ v-model="datas"
460
+ :show-loading-more-when-reload="true"
461
+ :fixed="false"
462
+ @query="queryList"
463
+ >
464
+ <template #top>
465
+ <Search
466
+ :enum-column="enumColumn"
467
+ :criteria="config.criteria"
468
+ :primary-criterion="config.primaryCriterion"
469
+ @submit="submitSearch"
470
+ />
471
+ <view>
472
+ <listTopButtons :buttons="config.buttons" :source-id="sourceId" :page-title="pageTitle" />
473
+ </view>
474
+ <slot name="top" />
475
+ </template>
476
+
477
+ <foldCard
478
+ v-for="(item, index) in datas"
479
+ :key="item.code"
480
+ :index="index"
481
+ :data="item"
482
+ :columns="config.columns"
483
+ :primary-column="config.primaryColumn"
484
+ :second-column="config.secondColumn"
485
+ :model="foldCardModel"
486
+ :groups="config"
487
+ :source-id="sourceId"
488
+ @click="handleChange(item)"
489
+ >
490
+ <template #select>
491
+ <wd-checkbox
492
+ :true-value="`${item.code}`"
493
+ false-value=""
494
+ :model-value="selectDataFormat[item.code]"
495
+ :type="checkboxType"
496
+ />
497
+ </template>
498
+
499
+ <template #addressInfo v-if="showAddressInfo">
500
+ <div>{{ item.fieldMap[config.extDisplayConfig?.secondColumn?.sourceId] }}</div>
501
+ </template>
502
+ <template #detailAddress v-if="showAddressInfo">
503
+ <div>{{ formatAddress(item.fieldMap[config.extDisplayConfig?.cardShowCols?.[0]?.sourceId]) }}</div>
504
+ </template>
505
+ </foldCard>
506
+
507
+ <template #bottom>
508
+ <view class="popup-footer flex justify-center gap-4 p-4 border-t border-gray-200">
509
+ <wd-button plain custom-class="flex-1" @click="cancel">取消</wd-button>
510
+ <wd-button type="primary" custom-class="flex-1" @click="confirm">确定</wd-button>
511
+ </view>
512
+ </template>
513
+ </z-paging>
514
+ </view>
515
+ </view>
516
+ </wd-popup>
517
+ </view>
518
+ </template>
519
+
520
+ <style scoped lang="scss">
521
+ .wui-select-popup-wrapper {
522
+ width: 100%;
523
+ position: relative;
524
+ }
525
+
526
+ // ★ 触发区域 - 原生表单样式(类似微信picker)
527
+ .trigger-cell {
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: space-between;
531
+ cursor: pointer;
532
+ position: relative;
533
+ transition: background-color 0.2s;
534
+
535
+ &:active {
536
+ background-color: #f5f7fa; // 点击反馈
537
+ }
538
+
539
+ // 左侧标签 - 固定宽度,不换行
540
+ .cell-label {
541
+ flex-shrink: 0;
542
+ margin-right: 24rpx;
543
+ white-space: nowrap;
544
+ }
545
+
546
+ // 中间内容区 - 占据剩余空间
547
+ .cell-content {
548
+ flex: 1;
549
+ min-width: 0; // 允许文本溢出省略
550
+
551
+ .cell-value {
552
+ text-align: left;
553
+ overflow: hidden;
554
+ text-overflow: ellipsis;
555
+ white-space: nowrap;
556
+ display: block;
557
+ }
558
+
559
+ .cell-placeholder {
560
+ color: #c0c4cc; // 灰色占位符
561
+ text-align: left;
562
+ }
563
+ }
564
+
565
+ // 右侧箭头
566
+ .cell-arrow {
567
+ flex-shrink: 0;
568
+ margin-left: 16rpx;
569
+ display: flex;
570
+ align-items: center;
571
+
572
+ .arrow-text {
573
+ color: #c0c4cc;
574
+ transform: scaleX(0.8); // 稍微压窄箭头
575
+ }
576
+ }
577
+ }
578
+
579
+ // Popup 容器 - 使用 flex 布局控制溢出
580
+ .select-popup-container {
581
+ display: flex;
582
+ flex-direction: column;
583
+ height: 75vh; // 固定高度
584
+ max-height: 80vh; // 最大高度
585
+ overflow: hidden; // 防止内容溢出
586
+ }
587
+
588
+ // 标题栏
589
+ .popup-header {
590
+ flex-shrink: 0; // 不压缩
591
+ z-index: 10;
592
+ }
593
+
594
+ // 内容区域 - 可滚动
595
+ .popup-body {
596
+ flex: 1; // 占据剩余空间
597
+ overflow: hidden; // 超出隐藏,由 z-paging 管理滚动
598
+ min-height: 0; // ★ 关键:允许 flex 子元素收缩
599
+
600
+ // 让 z-paging 填满整个容器
601
+ :deep(.z-paging-content) {
602
+ height: 100% !important;
603
+ }
604
+ }
605
+
606
+ // 底部按钮区
607
+ .popup-footer {
608
+ flex-shrink: 0; // 不压缩
609
+ position: sticky;
610
+ bottom: 0;
611
+ }
612
+ </style>