af-mobile-client-vue3 1.2.11 → 1.2.13

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.
@@ -1,1000 +1,1000 @@
1
- <script setup lang="ts">
2
- import XBadge from '@af-mobile-client-vue3/components/data/XBadge/index.vue'
3
- import XCellListFilter from '@af-mobile-client-vue3/components/data/XCellListFilter/index.vue'
4
- import { getConfigByName, query } from '@af-mobile-client-vue3/services/api/common'
5
- import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
6
- import { getRangeByType } from '@af-mobile-client-vue3/utils/queryFormDefaultRangePicker'
7
- import { executeStrFunctionByContext } from '@af-mobile-client-vue3/utils/runEvalFunction'
8
- import LoadError from '@af-mobile-client-vue3/views/common/LoadError.vue'
9
- import {
10
- showConfirmDialog,
11
- BackTop as VanBackTop,
12
- Button as VanButton,
13
- Col as VanCol,
14
- Icon as VanIcon,
15
- List as VanList,
16
- Popover as VanPopover,
17
- PullRefresh as VanPullRefresh,
18
- Row as VanRow,
19
- Search as VanSearch,
20
- Space as VanSpace,
21
- Tag as VanTag,
22
- } from 'vant'
23
- import { computed, defineEmits, defineProps, getCurrentInstance, onBeforeMount, ref, useSlots, watch } from 'vue'
24
- import { useRouter } from 'vue-router'
25
-
26
- const { configName, serviceName, fixQueryForm, customAdd, customEdit } = withDefaults(defineProps<{
27
- configName?: string
28
- fixQueryForm?: object
29
- idKey?: string
30
- serviceName?: string
31
- scanOptions?: {
32
- show?: boolean // 是否显示扫码按钮
33
- type?: string | string[] // 显示类型:可以是单个类型或类型数组 'nfc' / ['scan', 'nfc']
34
- defaultMode?: string // 默认模式
35
- }
36
- // 是否自定义新增、编辑按钮
37
- customAdd?: boolean
38
- customEdit?: boolean
39
- }>(), {
40
- configName: '',
41
- fixQueryForm: null,
42
- idKey: 'o_id',
43
- serviceName: undefined,
44
- scanOptions: undefined,
45
- customAdd: false,
46
- customEdit: false,
47
- })
48
-
49
- const emit = defineEmits<{
50
- (e: 'toDetail', item: any): void
51
- (e: 'update', item: any): void
52
- (e: 'deleteRow', item: any): void
53
- (e: 'add'): void
54
- (e: string, item: any): void
55
- (e: 'updateCondition', params: any): void
56
- }>()
57
-
58
- const userState = useUserStore().getLogin()
59
-
60
- const router = useRouter()
61
-
62
- const orderVal = ref(undefined)
63
-
64
- const sortordVal = ref(undefined)
65
-
66
- // 配置内容
67
- const configContent = ref({})
68
-
69
- // 表单加载后是否立即执行查询
70
- const isInitQuery = ref(false)
71
-
72
- // 主列
73
- const mainColumns = ref([])
74
-
75
- // 副标题列
76
- const subTitleColumns = ref([])
77
-
78
- // 详情列
79
- const detailColumns = ref([])
80
-
81
- // 标签列
82
- const tagList = ref([])
83
-
84
- // 标题按钮列
85
- const btnList = ref([])
86
-
87
- // 底部列
88
- const footColumns = ref([])
89
-
90
- // 所有的复杂操作列
91
- const allActions = ref([])
92
-
93
- // 复杂操作列前三项
94
- const mainActions = ref([])
95
-
96
- // 复杂操作列其余项
97
- const otherActions = ref([])
98
-
99
- // 数据集
100
- const list = ref([])
101
-
102
- // 每个 Popover 的显示状态
103
- const showPopover = ref<boolean[]>(list.value.map(() => false))
104
-
105
- // 排序集
106
- const orderList = ref([])
107
-
108
- // 表单查询数组
109
- const formQueryList = ref([])
110
-
111
- // 总数据
112
- const totalCount = ref(0)
113
-
114
- // 当前页数
115
- const pageNo = ref(1)
116
-
117
- // 每页数量
118
- const pageSize = 20
119
-
120
- const searchValue = ref('')
121
-
122
- // 数据加载状态
123
- const loading = ref(false)
124
- const refreshing = ref(false)
125
- const finished = ref(false)
126
- const isError = ref(false)
127
- const finishedText = ref('加载完成')
128
- // 避免查询多次
129
- const isLastPage = ref(false)
130
-
131
- // 条件参数(查询框)
132
- const conditionParams = ref(undefined)
133
-
134
- // 查询参数(配置自带的默认值)
135
- const queryDefaultParams = ref({})
136
-
137
- // 主要按钮的状态
138
- const buttonState = ref(undefined)
139
-
140
- // 新增or查询的表单配置
141
- const groupFormItems = ref({})
142
- const title = ref('')
143
-
144
- const slots = useSlots()
145
-
146
- // 当前组件实例(不推荐使用,可能会在后续的版本更迭中调整,暂时用来绑定函数的上下文)
147
- const currInst = getCurrentInstance()
148
-
149
- onBeforeMount(() => {
150
- initComponent()
151
- })
152
-
153
- // 组件初始化
154
- function initComponent() {
155
- getConfigByName(configName, (result) => {
156
- groupFormItems.value = result
157
- title.value = result?.title
158
- const isQuery = result.createdQuery
159
- for (let i = 0; i < result.columnJson.length; i++) {
160
- const item = result.columnJson[i]
161
- item.span = item.flexSpan
162
- if (item.slotType === 'badge')
163
- item.dictName = item.slotKeyMap
164
-
165
- if (item.mobileColumnType === 'mobile_header_column') {
166
- mainColumns.value.push(item)
167
- }
168
- else if (item.mobileColumnType === 'mobile_subtitle_column') {
169
- subTitleColumns.value.push(item)
170
- }
171
- else if (item.mobileColumnType === 'mobile_details_column') {
172
- detailColumns.value.push(item)
173
- }
174
- else if (item.mobileColumnType === 'mobile_footer_column') {
175
- footColumns.value.push(item)
176
- }
177
- else if (item.mobileColumnType === 'mobile_tag_column') {
178
- tagList.value.push(item)
179
- }
180
- else if (item.slotType === 'action' && item.actionArr) {
181
- for (let j = 0; j < item.actionArr.length; j++) {
182
- allActions.value.push({
183
- text: item.actionArr[j].text,
184
- func: item.actionArr[j].func,
185
- customFunction: item.actionArr[j].customFunction,
186
- })
187
- }
188
- }
189
-
190
- if (item.btnIcon)
191
- btnList.value.push(item)
192
-
193
- if (result.showSortIcon && item.sortable) {
194
- orderList.value.push({
195
- title: item.title,
196
- value: item.dataIndex,
197
- })
198
- }
199
- }
200
- configContent.value = result
201
- formQueryList.value = result.formJson
202
-
203
- if (result.buttonState) {
204
- buttonState.value = result.buttonState
205
- if (buttonState.value.edit && buttonState.value.edit === true)
206
- allActions.value.push({ text: '修改', func: 'updateRow' })
207
- if (buttonState.value.delete && buttonState.value.delete === true)
208
- allActions.value.push({ text: '删除', func: 'deleteRow' })
209
- }
210
- splitArrayAt(allActions.value, 3)
211
-
212
- // 初始化条件参数(从表单默认值中获取)
213
- initConditionParams(result.formJson, isQuery)
214
- }, serviceName)
215
- }
216
-
217
- // 初始化条件参数
218
- function initConditionParams(formItems, isQuery) {
219
- const defaultParams = {}
220
- let hasDefaults: boolean
221
-
222
- // 从表单配置中获取所有默认值
223
- formItems.forEach((item) => {
224
- if (item.model) {
225
- // 根据查询模式获取对应的默认值
226
- if (item.queryFormDefault !== undefined && item.queryFormDefault !== null) {
227
- if (item.type === 'rangePicker' && item.queryType === 'BETWEEN') {
228
- defaultParams[item.model] = getRangeByType(item.queryFormDefault, false)
229
- }
230
- else if (['treeSelect', 'select', 'checkbox'].includes(item.type) && ['curOrgId', 'curDepId', 'curUserId'].includes(item.queryFormDefault)) {
231
- if (item.queryFormDefault === 'curOrgId') {
232
- defaultParams[item.model] = item.type === 'select' ? userState.f.resources.orgid : [userState.f.resources.orgid]
233
- }
234
- if (item.queryFormDefault === 'curDepId') {
235
- defaultParams[item.model] = item.type === 'select' ? userState.f.resources.depids : [userState.f.resources.depids]
236
- }
237
- if (item.queryFormDefault === 'curUserId') {
238
- defaultParams[item.model] = item.type === 'select' ? userState.f.resources.id : [userState.f.resources.id]
239
- }
240
- }
241
- else {
242
- defaultParams[item.model] = item.queryFormDefault
243
- }
244
- }
245
- }
246
- })
247
- // eslint-disable-next-line prefer-const
248
- hasDefaults = true
249
-
250
- // 如果有默认值,则设置到条件参数中并立即执行查询
251
- queryDefaultParams.value = defaultParams
252
- isInitQuery.value = isQuery
253
- if (hasDefaults && isInitQuery.value) {
254
- // 延迟执行第一次查询,确保组件完全加载
255
- setTimeout(() => {
256
- loading.value = true
257
- onLoad()
258
- }, 100)
259
- }
260
- }
261
-
262
- // 刷新数据
263
- function onRefresh() {
264
- isError.value = false
265
- setTimeout(() => {
266
- // 重新加载数据
267
- // 将 loading 设置为 true,表示处于加载状态
268
- refreshing.value = true
269
- finishedText.value = '加载完成'
270
- finished.value = false
271
- loading.value = true
272
- onLoad()
273
- }, 100)
274
- }
275
-
276
- // 加载数据
277
- function onLoad() {
278
- if (refreshing.value) {
279
- list.value = []
280
- pageNo.value = 1
281
- isLastPage.value = false
282
- }
283
- if (!isLastPage.value) {
284
- let searchVal = searchValue.value
285
- if (searchVal === '')
286
- searchVal = undefined
287
-
288
- // 确保conditionParams不是undefined
289
- if (conditionParams.value === undefined)
290
- conditionParams.value = {}
291
- const mergedParams = mergeParams(queryDefaultParams.value, conditionParams.value)
292
-
293
- // 输出查询条件,便于调试
294
- console.log('查询条件:', {
295
- pageNo: pageNo.value,
296
- pageSize,
297
- conditionParams: {
298
- $queryValue: searchVal,
299
- ...fixQueryForm,
300
- ...mergedParams,
301
- },
302
- })
303
-
304
- query({
305
- queryParamsName: configName,
306
- pageNo: pageNo.value,
307
- pageSize,
308
- conditionParams: {
309
- $queryValue: searchVal,
310
- ...fixQueryForm,
311
- ...mergedParams,
312
- },
313
- sortField: orderVal?.value,
314
- sortOrder: sortordVal?.value,
315
- }, serviceName).then((res: any) => {
316
- totalCount.value = res.totalCount
317
- if (res.data.length === 0)
318
- isLastPage.value = true
319
-
320
- for (const item of res.data)
321
- list.value.push(item)
322
- if (list.value.length >= res.totalCount)
323
- finished.value = true
324
- else
325
- pageNo.value = pageNo.value + 1
326
- }).catch(() => {
327
- finishedText.value = ''
328
- finished.value = true
329
- isError.value = true
330
- }).finally(() => {
331
- // 加载状态结束
332
- loading.value = false
333
- refreshing.value = false
334
- })
335
- }
336
- }
337
-
338
- // 合并参数
339
- function mergeParams(defaultParams: object, overrideParams: object) {
340
- const result = { ...defaultParams }
341
- for (const [key, value] of Object.entries(overrideParams)) {
342
- // 只有当overrideParams中的值不是undefined或空字符串时才覆盖
343
- if (value !== undefined) {
344
- result[key] = value
345
- }
346
- }
347
- return result
348
- }
349
-
350
- // 区分主要操作列与其他操作列
351
- function splitArrayAt<T>(array: T[], index: number) {
352
- mainActions.value = array.slice(0, index)
353
- otherActions.value = array.slice(index)
354
- }
355
-
356
- // 新增:动态获取按钮分组
357
- function getActionGroups(item: any, index: number) {
358
- // 先过滤出当前行可见的按钮
359
- const visibleActions = allActions.value.filter(action =>
360
- evaluateCustomFunction(action.customFunction, item, index),
361
- )
362
- // 前3个为主按钮,其余为更多按钮,保持逆序
363
- return {
364
- main: visibleActions.slice(0, 3).reverse(),
365
- more: visibleActions.slice(3).reverse(),
366
- }
367
- }
368
-
369
- watch(() => searchValue.value, (newVal) => {
370
- if (newVal === '')
371
- onRefresh()
372
- })
373
-
374
- // 配置中心->表单项展示函数
375
- function handleFunctionStyle(funcString, record) {
376
- if (!funcString) {
377
- return {}
378
- }
379
-
380
- try {
381
- // 同步执行函数
382
- const obj = executeStrFunctionByContext(currInst, funcString, [record])
383
- // 如果返回的是对象,则直接返回
384
- if (obj && typeof obj === 'object') {
385
- return obj
386
- }
387
- // 其他情况返回空对象
388
- return {}
389
- }
390
- catch (error) {
391
- console.error('Error in handleFunctionStyle:', error)
392
- return {}
393
- }
394
- }
395
-
396
- // 逆序排列主要按钮
397
- const reversedMainActions = computed(() => {
398
- return [...mainActions.value].reverse()
399
- })
400
-
401
- // 设置 Popover 的事件
402
- function onSelectMenu(item: any, event: any) {
403
- if (event.text === '删除') {
404
- showConfirmDialog({
405
- title: '删除',
406
- message:
407
- '请确认是否删除!!!',
408
- }).then(() => {
409
- emit(event.func, item)
410
- }).catch(() => {
411
- })
412
- }
413
- else if (event.text === '修改') {
414
- if (customEdit) {
415
- emit('update', item)
416
- }
417
- else {
418
- // 默认行为 - 导航到XForm页面
419
- router.push({
420
- name: 'XForm',
421
- // params: { id: item },
422
- query: {
423
- groupFormItems: JSON.stringify(groupFormItems.value),
424
- serviceName,
425
- formData: JSON.stringify(item),
426
- mode: '修改',
427
- },
428
- })
429
- }
430
- }
431
- else {
432
- emit(event.func, item)
433
- }
434
- }
435
-
436
- // 抛出新增按钮的事件
437
- function addOption() {
438
- if (customAdd) {
439
- emit('add')
440
- }
441
- else {
442
- // 默认行为 - 导航到XForm页面
443
- router.push({
444
- name: 'XForm',
445
- query: {
446
- groupFormItems: JSON.stringify(groupFormItems.value),
447
- serviceName,
448
- formData: JSON.stringify({}),
449
- mode: '新增',
450
- },
451
- })
452
- }
453
- }
454
-
455
- // 处理按钮点击
456
- function handleButtonClick(btn, item) {
457
- emit(`${btn.btnIcon}`, item)
458
- }
459
-
460
- // 处理自定义函数
461
- function evaluateCustomFunction(funcString: string | undefined, record: any, index: number): boolean {
462
- try {
463
- // 如果 customFunction 不存在,返回 true 表示正常显示
464
- if (!funcString || funcString === '')
465
- return true
466
-
467
- // 匹配参数名、函数体
468
- const innerFuncRegex = /function\s*\((\w+)\s*,\s*(\w+)\)\s*\{([\s\S]*)\}/
469
- const matches = funcString.match(innerFuncRegex)
470
-
471
- if (!matches)
472
- return true
473
-
474
- const [, param1, param2, functionBody] = matches
475
-
476
- // eslint-disable-next-line no-new-func
477
- const func = new Function(param1, param2, functionBody)
478
- return func(record, index)
479
- }
480
- catch (error) {
481
- console.error('Error evaluating custom function:', error)
482
- return true
483
- }
484
- }
485
-
486
- /**
487
- * 函数描述: 传入自定义条件进行查询(传入字段必须在琉璃中配置生成查询项才会生效)
488
- * @param {string} params - 查询条件map 例: { os_id:1 }
489
- * // 小提示:此处传入的条件会覆盖掉fixQueryForm(固定查询条件参数)
490
- */
491
- function updateConditionAndRefresh(params: any) {
492
- if (params) {
493
- // 遍历参数,更新对应的表单值
494
- Object.entries(params).forEach(([key, value]) => {
495
- // 找到对应的表单项
496
- conditionParams.value[key] = value
497
- })
498
- }
499
-
500
- // 触发刷新
501
- onRefresh()
502
- }
503
-
504
- // 暴露方法给父组件
505
- defineExpose({
506
- updateConditionAndRefresh,
507
- })
508
- </script>
509
-
510
- <template>
511
- <div id="XCellList">
512
- <VanRow class="filter-condition">
513
- <!-- 左侧动态插槽区域 -->
514
- <template v-for="(_, name) in slots" :key="name">
515
- <template v-if="typeof name === 'string' && name.startsWith('search-left-')">
516
- <div class="filter-icon-box">
517
- <slot :name="name" />
518
- </div>
519
- </template>
520
- </template>
521
- <VanCol class="search-col">
522
- <VanSearch
523
- v-model="searchValue"
524
- class="title-search"
525
- clearable
526
- placeholder="综合查询框..."
527
- shape="round"
528
- @search="onRefresh"
529
- />
530
- </VanCol>
531
- <!-- 右侧动态插槽区域 -->
532
- <template v-for="(_, name) in slots" :key="name">
533
- <template v-if="typeof name === 'string' && name.startsWith('search-right-')">
534
- <div class="filter-icon-box">
535
- <slot :name="name" />
536
- </div>
537
- </template>
538
- </template>
539
- <VanCol class="search-icon">
540
- <XCellListFilter
541
- v-model:sortord-val="sortordVal"
542
- v-model:order-val="orderVal"
543
- v-model:condition-params="conditionParams"
544
- :fix-query-form="fixQueryForm"
545
- :service-name="serviceName"
546
- :order-list="orderList"
547
- :form-query="formQueryList"
548
- :button-state="buttonState"
549
- :scan-options="scanOptions"
550
- @on-refresh="onRefresh"
551
- @add-option="addOption"
552
- />
553
- </VanCol>
554
- </VanRow>
555
- <slot name="search-after" />
556
- <div class="main">
557
- <VanPullRefresh v-model="refreshing" :success-text="finishedText" head-height="70" @refresh="onRefresh">
558
- <template v-if="!isError">
559
- <VanList
560
- v-model:loading="loading"
561
- class="list_main"
562
- :finished="finished"
563
- finished-text="已加载全部内容"
564
- :immediate-check="isInitQuery"
565
- @load="onLoad"
566
- >
567
- <div v-for="(item, index) in list" :key="`card_${index}`" class="card_item_main">
568
- <VanRow gutter="20" class="card_item_header" align="center" @click="emit('toDetail', item)">
569
- <VanCol :span="24">
570
- <div class="title-row">
571
- <div v-for="(column) in mainColumns" :key="`main_${column.dataIndex}`" class="main-title">
572
- <p
573
- class="card_item_title"
574
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
575
- >
576
- {{ item[column.dataIndex] ?? '--' }}
577
- </p>
578
- </div>
579
- <div v-for="(column) in subTitleColumns" :key="`subtitle_${column.dataIndex}`" class="sub-title">
580
- <p
581
- class="card_item_subtitle"
582
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
583
- >
584
- <XBadge
585
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
586
- :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
587
- :service-name="serviceName"
588
- />
589
- </p>
590
- </div>
591
- <div class="action-buttons">
592
- <VanButton
593
- v-for="btn in btnList"
594
- :key="btn.dataIndex"
595
- class="action-button"
596
- :icon="btn.btnIcon"
597
- size="small"
598
- @click.stop="handleButtonClick(btn, item)"
599
- />
600
- </div>
601
- </div>
602
- </VanCol>
603
- </VanRow>
604
- <VanRow gutter="20" class="card_item_details" @click="emit('toDetail', item)">
605
- <VanCol v-for="column of detailColumns" :key="`details_${column.dataIndex}`" :span="column.span">
606
- <p>
607
- {{ (column.showLabel === undefined || column.showLabel) ? `${column.title}: ` : '' }}
608
- <XBadge
609
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
610
- :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
611
- :service-name="serviceName"
612
- />
613
- </p>
614
- </VanCol>
615
- </VanRow>
616
- <VanRow v-if="tagList.length > 0" gutter="20" class="tag-row" @click="emit('toDetail', item)">
617
- <VanCol :span="24">
618
- <div class="tag-container">
619
- <div class="tag-wrapper">
620
- <template
621
- v-for="column of tagList"
622
- :key="`tag_${column.dataIndex}`"
623
- >
624
- <VanTag
625
- :text-color="column.tagColor"
626
- :color="column.tagBorderColor"
627
- size="large"
628
- class="tag-item"
629
- >
630
- <div class="tag-content">
631
- <VanIcon v-if="column.tagIcon" :name="column.tagIcon" class="tag-icon" />
632
- <span v-if="column.showLabel === undefined || column.showLabel" class="tag-title">{{ `${column.title}: ` }}</span>
633
- <XBadge
634
- :dict-name="column.dictName"
635
- :dict-value="item[column.dataIndex]"
636
- :service-name="serviceName"
637
- />
638
- </div>
639
- </VanTag>
640
- </template>
641
- </div>
642
- </div>
643
- </VanCol>
644
- </VanRow>
645
- <VanRow
646
- v-if="footColumns && footColumns.length > 0"
647
- gutter="20"
648
- class="card_item_footer"
649
- @click="emit('toDetail', item)"
650
- >
651
- <VanCol v-for="column of footColumns" :key="`foot_${column.dataIndex}`" :span="12">
652
- <p>
653
- <span :style="handleFunctionStyle(column.styleFunctionForTitle, item)">
654
- {{ (column.showLabel === undefined || column.showLabel) ? `${column.title}: ` : '' }}
655
- </span>
656
- <XBadge
657
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
658
- :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
659
- :service-name="serviceName"
660
- />
661
- </p>
662
- </VanCol>
663
- </VanRow>
664
- <!-- 添加详情插槽 -->
665
- <slot name="item-detail" :item="item" />
666
- <VanRow v-if="allActions.length > 0" gutter="20" class="card_item_bottom">
667
- <VanCol span="4">
668
- <VanPopover
669
- v-if="getActionGroups(item, index).more.length"
670
- v-model:show="showPopover[index]"
671
- placement="bottom-start"
672
- theme="dark"
673
- overlay
674
- :actions="getActionGroups(item, index).more"
675
- @select="onSelectMenu(item, $event)"
676
- >
677
- <template #reference>
678
- <div class="more-button">
679
- <span>⋯</span>
680
- </div>
681
- </template>
682
- </VanPopover>
683
- </VanCol>
684
- <VanCol span="20">
685
- <VanRow justify="end">
686
- <VanSpace>
687
- <VanButton
688
- v-for="button in getActionGroups(item, index).main"
689
- :key="button.func"
690
- type="primary"
691
- size="normal"
692
- class="action-btn"
693
- @click="onSelectMenu(item, button)"
694
- >
695
- {{ button.text }}
696
- </VanButton>
697
- </VanSpace>
698
- </VanRow>
699
- </VanCol>
700
- </VanRow>
701
- </div>
702
- </VanList>
703
- </template>
704
- <template v-else>
705
- <LoadError />
706
- </template>
707
- </VanPullRefresh>
708
- <VanBackTop />
709
- </div>
710
- </div>
711
- </template>
712
-
713
- <style scoped lang="less">
714
- #XCellList {
715
- height: calc(100vh - var(--van-nav-bar-height) - 20px);
716
- display: flex;
717
- flex-direction: column;
718
- --van-search-padding: 3px;
719
- --van-dropdown-menu-title-padding: 3px;
720
- --van-cell-vertical-padding: 0px;
721
- --van-text-color-2: rgb(75, 85, 99);
722
- --van-button-normal-font-size: 13px;
723
- .main {
724
- flex: 1;
725
- min-height: 0;
726
- overflow-y: auto;
727
- background-color: var(--van-background);
728
- padding: var(--van-padding-base) var(--van-padding-sm);
729
-
730
- p {
731
- white-space: nowrap;
732
- overflow: hidden;
733
- text-overflow: ellipsis;
734
- margin: 0;
735
- }
736
-
737
- .card_item_main {
738
- background-color: var(--van-background-2);
739
- border-radius: var(--van-radius-lg);
740
- margin: 0 0 var(--van-padding-xs) 0;
741
- padding: var(--van-padding-sm);
742
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
743
- transition: all 0.3s ease;
744
- border: 1px solid rgba(0, 0, 0, 0.04);
745
-
746
- &:active {
747
- transform: scale(0.98);
748
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
749
- }
750
-
751
- .card_item_header {
752
- margin-bottom: var(--van-padding-base);
753
-
754
- .title-row {
755
- display: flex;
756
- align-items: center;
757
- margin-bottom: 2px;
758
- width: 100%;
759
-
760
- .main-title {
761
- display: inline-flex;
762
- align-items: center;
763
- .card_item_title {
764
- font-size: var(--van-font-size-lg);
765
- font-weight: 700;
766
- color: var(--van-text-color);
767
- margin: 0;
768
- }
769
- }
770
-
771
- .sub-title {
772
- display: inline-flex;
773
- align-items: center;
774
- margin-left: 8px;
775
- .card_item_subtitle {
776
- font-size: var(--van-font-size-md);
777
- color: var(--van-text-color-2);
778
- }
779
- }
780
-
781
- .action-buttons {
782
- display: flex;
783
- align-items: center;
784
- margin-left: auto;
785
- .action-button {
786
- margin-left: 6px;
787
- width: 32px;
788
- height: 32px;
789
- padding: 0;
790
- border: none;
791
- color: var(--van-primary-color);
792
- background-color: rgba(25, 137, 250, 0.1);
793
- border-radius: 6px;
794
- font-size: var(--van-font-size-lg);
795
- display: flex;
796
- align-items: center;
797
- justify-content: center;
798
- transition: all 0.2s ease;
799
- &:active {
800
- opacity: 0.7;
801
- background-color: rgba(25, 137, 250, 0.2);
802
- transform: scale(0.95);
803
- }
804
- }
805
- }
806
- }
807
- }
808
-
809
- .tag-row {
810
- margin-bottom: var(--van-padding-base);
811
- }
812
-
813
- .tag-container {
814
- width: 100%;
815
- .tag-wrapper {
816
- display: flex;
817
- flex-wrap: wrap;
818
- padding: 0 !important;
819
- align-items: center;
820
- width: 100%;
821
- margin: 0 -4px;
822
-
823
- .tag-item {
824
- width: auto;
825
- font-size: var(--van-font-size-sm);
826
- margin: 4px 4px;
827
- :deep(.van-tag) {
828
- width: fit-content;
829
- display: inline-flex;
830
- align-items: center;
831
- padding: 2px 8px;
832
- border-radius: 4px;
833
- }
834
- .tag-content {
835
- display: flex;
836
- align-items: center;
837
- //white-space: nowrap;
838
- }
839
- .tag-icon {
840
- margin-right: 4px;
841
- font-size: 14px;
842
- display: inline-flex;
843
- align-items: center;
844
- }
845
- .tag-title {
846
- font-weight: normal;
847
- }
848
- }
849
- }
850
- }
851
-
852
- .card_item_details {
853
- font-size: var(--van-font-size-md);
854
- color: var(--van-text-color-2);
855
- padding: 4px 0;
856
-
857
- .van-col {
858
- margin-bottom: 4px;
859
- p {
860
- display: flex;
861
- align-items: center;
862
- gap: 4px;
863
-
864
- span {
865
- flex: 1;
866
- min-width: 0;
867
- overflow: hidden;
868
- text-overflow: ellipsis;
869
- white-space: nowrap;
870
- }
871
-
872
- :deep(.van-badge) {
873
- flex-shrink: 0;
874
- }
875
- }
876
- }
877
- }
878
-
879
- .card_item_footer {
880
- font-size: var(--van-font-size-md);
881
- color: var(--van-text-color-2);
882
- padding: 4px 0;
883
- .van-col:last-child {
884
- text-align: right;
885
- }
886
- .van-col:first-child {
887
- text-align: left;
888
- }
889
- }
890
-
891
- .card_item_bottom {
892
- margin-top: 8px;
893
- padding-top: 10px;
894
- border-top: 1px solid rgba(0, 0, 0, 0.06);
895
-
896
- .more-button {
897
- width: 28px;
898
- height: 28px;
899
- display: flex;
900
- align-items: center;
901
- justify-content: center;
902
- color: var(--van-text-color-2);
903
- cursor: pointer;
904
- font-size: var(--van-font-size-lg);
905
- background-color: var(--van-background);
906
- border-radius: 6px;
907
- transition: all 0.2s ease;
908
- border: 1px solid rgba(0, 0, 0, 0.06);
909
- span {
910
- line-height: 1;
911
- margin-top: -2px;
912
- }
913
- &:active {
914
- opacity: 0.7;
915
- background-color: var(--van-background-2);
916
- transform: scale(0.95);
917
- }
918
- }
919
-
920
- .action-btn {
921
- --van-button-primary-border-color: #1890ff;
922
- --van-button-primary-background: #1890ff;
923
- min-width: 76px;
924
- height: 40px;
925
- border-radius: 10px;
926
- // font-size: var(--van-font-size-md);
927
- transition: all 0.2s ease;
928
- &:active {
929
- transform: scale(0.95);
930
- }
931
- }
932
- }
933
- }
934
- }
935
-
936
- .filter-condition {
937
- display: flex;
938
- align-items: center;
939
- padding: 8px 12px;
940
- background-color: #fff;
941
- gap: 1px;
942
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
943
- position: sticky;
944
- top: 0;
945
- z-index: 10;
946
- :deep(.van-search) {
947
- width: 100%;
948
- padding: var(--van-search-padding);
949
- background-color: transparent;
950
- }
951
- :deep(.van-search__content) {
952
- border-radius: 8px;
953
- background-color: var(--van-background);
954
- padding: 4px 12px;
955
- border: 1px solid rgba(0, 0, 0, 0.01);
956
- height: 40px;
957
- }
958
- :deep(.van-field__left-icon) {
959
- color: var(--van-text-color-2);
960
- }
961
- :deep(.van-cell) {
962
- background-color: transparent;
963
- }
964
- :deep(.van-field__control::placeholder) {
965
- color: var(--van-text-color-2);
966
- font-size: 14px;
967
- }
968
- .van-col {
969
- display: flex;
970
- align-items: center;
971
- &.search-col {
972
- flex: 1;
973
- min-width: 0;
974
- }
975
- &.search-icon {
976
- flex-shrink: 0;
977
- padding: 0;
978
- }
979
- }
980
- .filter-icon-box {
981
- display: flex;
982
- align-items: center;
983
- justify-content: center;
984
- width: 36px;
985
- height: 36px;
986
- border-radius: 8px;
987
- background-color: var(--van-background);
988
- cursor: pointer;
989
- position: relative;
990
- z-index: 1;
991
- border: 1px solid rgba(0, 0, 0, 0.06);
992
- transition: all 0.2s ease;
993
- &:active {
994
- opacity: 0.7;
995
- transform: scale(0.95);
996
- }
997
- }
998
- }
999
- }
1000
- </style>
1
+ <script setup lang="ts">
2
+ import XBadge from '@af-mobile-client-vue3/components/data/XBadge/index.vue'
3
+ import XCellListFilter from '@af-mobile-client-vue3/components/data/XCellListFilter/index.vue'
4
+ import { getConfigByName, query } from '@af-mobile-client-vue3/services/api/common'
5
+ import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
6
+ import { getRangeByType } from '@af-mobile-client-vue3/utils/queryFormDefaultRangePicker'
7
+ import { executeStrFunctionByContext } from '@af-mobile-client-vue3/utils/runEvalFunction'
8
+ import LoadError from '@af-mobile-client-vue3/views/common/LoadError.vue'
9
+ import {
10
+ showConfirmDialog,
11
+ BackTop as VanBackTop,
12
+ Button as VanButton,
13
+ Col as VanCol,
14
+ Icon as VanIcon,
15
+ List as VanList,
16
+ Popover as VanPopover,
17
+ PullRefresh as VanPullRefresh,
18
+ Row as VanRow,
19
+ Search as VanSearch,
20
+ Space as VanSpace,
21
+ Tag as VanTag,
22
+ } from 'vant'
23
+ import { computed, defineEmits, defineProps, getCurrentInstance, onBeforeMount, ref, useSlots, watch } from 'vue'
24
+ import { useRouter } from 'vue-router'
25
+
26
+ const { configName, serviceName, fixQueryForm, customAdd, customEdit } = withDefaults(defineProps<{
27
+ configName?: string
28
+ fixQueryForm?: object
29
+ idKey?: string
30
+ serviceName?: string
31
+ scanOptions?: {
32
+ show?: boolean // 是否显示扫码按钮
33
+ type?: string | string[] // 显示类型:可以是单个类型或类型数组 'nfc' / ['scan', 'nfc']
34
+ defaultMode?: string // 默认模式
35
+ }
36
+ // 是否自定义新增、编辑按钮
37
+ customAdd?: boolean
38
+ customEdit?: boolean
39
+ }>(), {
40
+ configName: '',
41
+ fixQueryForm: null,
42
+ idKey: 'o_id',
43
+ serviceName: undefined,
44
+ scanOptions: undefined,
45
+ customAdd: false,
46
+ customEdit: false,
47
+ })
48
+
49
+ const emit = defineEmits<{
50
+ (e: 'toDetail', item: any): void
51
+ (e: 'update', item: any): void
52
+ (e: 'deleteRow', item: any): void
53
+ (e: 'add'): void
54
+ (e: string, item: any): void
55
+ (e: 'updateCondition', params: any): void
56
+ }>()
57
+
58
+ const userState = useUserStore().getLogin()
59
+
60
+ const router = useRouter()
61
+
62
+ const orderVal = ref(undefined)
63
+
64
+ const sortordVal = ref(undefined)
65
+
66
+ // 配置内容
67
+ const configContent = ref({})
68
+
69
+ // 表单加载后是否立即执行查询
70
+ const isInitQuery = ref(false)
71
+
72
+ // 主列
73
+ const mainColumns = ref([])
74
+
75
+ // 副标题列
76
+ const subTitleColumns = ref([])
77
+
78
+ // 详情列
79
+ const detailColumns = ref([])
80
+
81
+ // 标签列
82
+ const tagList = ref([])
83
+
84
+ // 标题按钮列
85
+ const btnList = ref([])
86
+
87
+ // 底部列
88
+ const footColumns = ref([])
89
+
90
+ // 所有的复杂操作列
91
+ const allActions = ref([])
92
+
93
+ // 复杂操作列前三项
94
+ const mainActions = ref([])
95
+
96
+ // 复杂操作列其余项
97
+ const otherActions = ref([])
98
+
99
+ // 数据集
100
+ const list = ref([])
101
+
102
+ // 每个 Popover 的显示状态
103
+ const showPopover = ref<boolean[]>(list.value.map(() => false))
104
+
105
+ // 排序集
106
+ const orderList = ref([])
107
+
108
+ // 表单查询数组
109
+ const formQueryList = ref([])
110
+
111
+ // 总数据
112
+ const totalCount = ref(0)
113
+
114
+ // 当前页数
115
+ const pageNo = ref(1)
116
+
117
+ // 每页数量
118
+ const pageSize = 20
119
+
120
+ const searchValue = ref('')
121
+
122
+ // 数据加载状态
123
+ const loading = ref(false)
124
+ const refreshing = ref(false)
125
+ const finished = ref(false)
126
+ const isError = ref(false)
127
+ const finishedText = ref('加载完成')
128
+ // 避免查询多次
129
+ const isLastPage = ref(false)
130
+
131
+ // 条件参数(查询框)
132
+ const conditionParams = ref(undefined)
133
+
134
+ // 查询参数(配置自带的默认值)
135
+ const queryDefaultParams = ref({})
136
+
137
+ // 主要按钮的状态
138
+ const buttonState = ref(undefined)
139
+
140
+ // 新增or查询的表单配置
141
+ const groupFormItems = ref({})
142
+ const title = ref('')
143
+
144
+ const slots = useSlots()
145
+
146
+ // 当前组件实例(不推荐使用,可能会在后续的版本更迭中调整,暂时用来绑定函数的上下文)
147
+ const currInst = getCurrentInstance()
148
+
149
+ onBeforeMount(() => {
150
+ initComponent()
151
+ })
152
+
153
+ // 组件初始化
154
+ function initComponent() {
155
+ getConfigByName(configName, (result) => {
156
+ groupFormItems.value = result
157
+ title.value = result?.title
158
+ const isQuery = result.createdQuery
159
+ for (let i = 0; i < result.columnJson.length; i++) {
160
+ const item = result.columnJson[i]
161
+ item.span = item.flexSpan
162
+ if (item.slotType === 'badge')
163
+ item.dictName = item.slotKeyMap
164
+
165
+ if (item.mobileColumnType === 'mobile_header_column') {
166
+ mainColumns.value.push(item)
167
+ }
168
+ else if (item.mobileColumnType === 'mobile_subtitle_column') {
169
+ subTitleColumns.value.push(item)
170
+ }
171
+ else if (item.mobileColumnType === 'mobile_details_column') {
172
+ detailColumns.value.push(item)
173
+ }
174
+ else if (item.mobileColumnType === 'mobile_footer_column') {
175
+ footColumns.value.push(item)
176
+ }
177
+ else if (item.mobileColumnType === 'mobile_tag_column') {
178
+ tagList.value.push(item)
179
+ }
180
+ else if (item.slotType === 'action' && item.actionArr) {
181
+ for (let j = 0; j < item.actionArr.length; j++) {
182
+ allActions.value.push({
183
+ text: item.actionArr[j].text,
184
+ func: item.actionArr[j].func,
185
+ customFunction: item.actionArr[j].customFunction,
186
+ })
187
+ }
188
+ }
189
+
190
+ if (item.btnIcon)
191
+ btnList.value.push(item)
192
+
193
+ if (result.showSortIcon && item.sortable) {
194
+ orderList.value.push({
195
+ title: item.title,
196
+ value: item.dataIndex,
197
+ })
198
+ }
199
+ }
200
+ configContent.value = result
201
+ formQueryList.value = result.formJson
202
+
203
+ if (result.buttonState) {
204
+ buttonState.value = result.buttonState
205
+ if (buttonState.value.edit && buttonState.value.edit === true)
206
+ allActions.value.push({ text: '修改', func: 'updateRow' })
207
+ if (buttonState.value.delete && buttonState.value.delete === true)
208
+ allActions.value.push({ text: '删除', func: 'deleteRow' })
209
+ }
210
+ splitArrayAt(allActions.value, 3)
211
+
212
+ // 初始化条件参数(从表单默认值中获取)
213
+ initConditionParams(result.formJson, isQuery)
214
+ }, serviceName)
215
+ }
216
+
217
+ // 初始化条件参数
218
+ function initConditionParams(formItems, isQuery) {
219
+ const defaultParams = {}
220
+ let hasDefaults: boolean
221
+
222
+ // 从表单配置中获取所有默认值
223
+ formItems.forEach((item) => {
224
+ if (item.model) {
225
+ // 根据查询模式获取对应的默认值
226
+ if (item.queryFormDefault !== undefined && item.queryFormDefault !== null) {
227
+ if (item.type === 'rangePicker' && item.queryType === 'BETWEEN') {
228
+ defaultParams[item.model] = getRangeByType(item.queryFormDefault, false)
229
+ }
230
+ else if (['treeSelect', 'select', 'checkbox'].includes(item.type) && ['curOrgId', 'curDepId', 'curUserId'].includes(item.queryFormDefault)) {
231
+ if (item.queryFormDefault === 'curOrgId') {
232
+ defaultParams[item.model] = item.type === 'select' ? userState.f.resources.orgid : [userState.f.resources.orgid]
233
+ }
234
+ if (item.queryFormDefault === 'curDepId') {
235
+ defaultParams[item.model] = item.type === 'select' ? userState.f.resources.depids : [userState.f.resources.depids]
236
+ }
237
+ if (item.queryFormDefault === 'curUserId') {
238
+ defaultParams[item.model] = item.type === 'select' ? userState.f.resources.id : [userState.f.resources.id]
239
+ }
240
+ }
241
+ else {
242
+ defaultParams[item.model] = item.queryFormDefault
243
+ }
244
+ }
245
+ }
246
+ })
247
+ // eslint-disable-next-line prefer-const
248
+ hasDefaults = true
249
+
250
+ // 如果有默认值,则设置到条件参数中并立即执行查询
251
+ queryDefaultParams.value = defaultParams
252
+ isInitQuery.value = isQuery
253
+ if (hasDefaults && isInitQuery.value) {
254
+ // 延迟执行第一次查询,确保组件完全加载
255
+ setTimeout(() => {
256
+ loading.value = true
257
+ onLoad()
258
+ }, 100)
259
+ }
260
+ }
261
+
262
+ // 刷新数据
263
+ function onRefresh() {
264
+ isError.value = false
265
+ setTimeout(() => {
266
+ // 重新加载数据
267
+ // 将 loading 设置为 true,表示处于加载状态
268
+ refreshing.value = true
269
+ finishedText.value = '加载完成'
270
+ finished.value = false
271
+ loading.value = true
272
+ onLoad()
273
+ }, 100)
274
+ }
275
+
276
+ // 加载数据
277
+ function onLoad() {
278
+ if (refreshing.value) {
279
+ list.value = []
280
+ pageNo.value = 1
281
+ isLastPage.value = false
282
+ }
283
+ if (!isLastPage.value) {
284
+ let searchVal = searchValue.value
285
+ if (searchVal === '')
286
+ searchVal = undefined
287
+
288
+ // 确保conditionParams不是undefined
289
+ if (conditionParams.value === undefined)
290
+ conditionParams.value = {}
291
+ const mergedParams = mergeParams(queryDefaultParams.value, conditionParams.value)
292
+
293
+ // 输出查询条件,便于调试
294
+ console.log('查询条件:', {
295
+ pageNo: pageNo.value,
296
+ pageSize,
297
+ conditionParams: {
298
+ $queryValue: searchVal,
299
+ ...fixQueryForm,
300
+ ...mergedParams,
301
+ },
302
+ })
303
+
304
+ query({
305
+ queryParamsName: configName,
306
+ pageNo: pageNo.value,
307
+ pageSize,
308
+ conditionParams: {
309
+ $queryValue: searchVal,
310
+ ...fixQueryForm,
311
+ ...mergedParams,
312
+ },
313
+ sortField: orderVal?.value,
314
+ sortOrder: sortordVal?.value,
315
+ }, serviceName).then((res: any) => {
316
+ totalCount.value = res.totalCount
317
+ if (res.data.length === 0)
318
+ isLastPage.value = true
319
+
320
+ for (const item of res.data)
321
+ list.value.push(item)
322
+ if (list.value.length >= res.totalCount)
323
+ finished.value = true
324
+ else
325
+ pageNo.value = pageNo.value + 1
326
+ }).catch(() => {
327
+ finishedText.value = ''
328
+ finished.value = true
329
+ isError.value = true
330
+ }).finally(() => {
331
+ // 加载状态结束
332
+ loading.value = false
333
+ refreshing.value = false
334
+ })
335
+ }
336
+ }
337
+
338
+ // 合并参数
339
+ function mergeParams(defaultParams: object, overrideParams: object) {
340
+ const result = { ...defaultParams }
341
+ for (const [key, value] of Object.entries(overrideParams)) {
342
+ // 只有当overrideParams中的值不是undefined或空字符串时才覆盖
343
+ if (value !== undefined) {
344
+ result[key] = value
345
+ }
346
+ }
347
+ return result
348
+ }
349
+
350
+ // 区分主要操作列与其他操作列
351
+ function splitArrayAt<T>(array: T[], index: number) {
352
+ mainActions.value = array.slice(0, index)
353
+ otherActions.value = array.slice(index)
354
+ }
355
+
356
+ // 新增:动态获取按钮分组
357
+ function getActionGroups(item: any, index: number) {
358
+ // 先过滤出当前行可见的按钮
359
+ const visibleActions = allActions.value.filter(action =>
360
+ evaluateCustomFunction(action.customFunction, item, index),
361
+ )
362
+ // 前3个为主按钮,其余为更多按钮,保持逆序
363
+ return {
364
+ main: visibleActions.slice(0, 3).reverse(),
365
+ more: visibleActions.slice(3).reverse(),
366
+ }
367
+ }
368
+
369
+ watch(() => searchValue.value, (newVal) => {
370
+ if (newVal === '')
371
+ onRefresh()
372
+ })
373
+
374
+ // 配置中心->表单项展示函数
375
+ function handleFunctionStyle(funcString, record) {
376
+ if (!funcString) {
377
+ return {}
378
+ }
379
+
380
+ try {
381
+ // 同步执行函数
382
+ const obj = executeStrFunctionByContext(currInst, funcString, [record])
383
+ // 如果返回的是对象,则直接返回
384
+ if (obj && typeof obj === 'object') {
385
+ return obj
386
+ }
387
+ // 其他情况返回空对象
388
+ return {}
389
+ }
390
+ catch (error) {
391
+ console.error('Error in handleFunctionStyle:', error)
392
+ return {}
393
+ }
394
+ }
395
+
396
+ // 逆序排列主要按钮
397
+ const reversedMainActions = computed(() => {
398
+ return [...mainActions.value].reverse()
399
+ })
400
+
401
+ // 设置 Popover 的事件
402
+ function onSelectMenu(item: any, event: any) {
403
+ if (event.text === '删除') {
404
+ showConfirmDialog({
405
+ title: '删除',
406
+ message:
407
+ '请确认是否删除!!!',
408
+ }).then(() => {
409
+ emit(event.func, item)
410
+ }).catch(() => {
411
+ })
412
+ }
413
+ else if (event.text === '修改') {
414
+ if (customEdit) {
415
+ emit('update', item)
416
+ }
417
+ else {
418
+ // 默认行为 - 导航到XForm页面
419
+ router.push({
420
+ name: 'XForm',
421
+ // params: { id: item },
422
+ query: {
423
+ groupFormItems: JSON.stringify(groupFormItems.value),
424
+ serviceName,
425
+ formData: JSON.stringify(item),
426
+ mode: '修改',
427
+ },
428
+ })
429
+ }
430
+ }
431
+ else {
432
+ emit(event.func, item)
433
+ }
434
+ }
435
+
436
+ // 抛出新增按钮的事件
437
+ function addOption() {
438
+ if (customAdd) {
439
+ emit('add')
440
+ }
441
+ else {
442
+ // 默认行为 - 导航到XForm页面
443
+ router.push({
444
+ name: 'XForm',
445
+ query: {
446
+ groupFormItems: JSON.stringify(groupFormItems.value),
447
+ serviceName,
448
+ formData: JSON.stringify({}),
449
+ mode: '新增',
450
+ },
451
+ })
452
+ }
453
+ }
454
+
455
+ // 处理按钮点击
456
+ function handleButtonClick(btn, item) {
457
+ emit(`${btn.btnIcon}`, item)
458
+ }
459
+
460
+ // 处理自定义函数
461
+ function evaluateCustomFunction(funcString: string | undefined, record: any, index: number): boolean {
462
+ try {
463
+ // 如果 customFunction 不存在,返回 true 表示正常显示
464
+ if (!funcString || funcString === '')
465
+ return true
466
+
467
+ // 匹配参数名、函数体
468
+ const innerFuncRegex = /function\s*\((\w+)\s*,\s*(\w+)\)\s*\{([\s\S]*)\}/
469
+ const matches = funcString.match(innerFuncRegex)
470
+
471
+ if (!matches)
472
+ return true
473
+
474
+ const [, param1, param2, functionBody] = matches
475
+
476
+ // eslint-disable-next-line no-new-func
477
+ const func = new Function(param1, param2, functionBody)
478
+ return func(record, index)
479
+ }
480
+ catch (error) {
481
+ console.error('Error evaluating custom function:', error)
482
+ return true
483
+ }
484
+ }
485
+
486
+ /**
487
+ * 函数描述: 传入自定义条件进行查询(传入字段必须在琉璃中配置生成查询项才会生效)
488
+ * @param {string} params - 查询条件map 例: { os_id:1 }
489
+ * // 小提示:此处传入的条件会覆盖掉fixQueryForm(固定查询条件参数)
490
+ */
491
+ function updateConditionAndRefresh(params: any) {
492
+ if (params) {
493
+ // 遍历参数,更新对应的表单值
494
+ Object.entries(params).forEach(([key, value]) => {
495
+ // 找到对应的表单项
496
+ conditionParams.value[key] = value
497
+ })
498
+ }
499
+
500
+ // 触发刷新
501
+ onRefresh()
502
+ }
503
+
504
+ // 暴露方法给父组件
505
+ defineExpose({
506
+ updateConditionAndRefresh,
507
+ })
508
+ </script>
509
+
510
+ <template>
511
+ <div id="XCellList">
512
+ <VanRow class="filter-condition">
513
+ <!-- 左侧动态插槽区域 -->
514
+ <template v-for="(_, name) in slots" :key="name">
515
+ <template v-if="typeof name === 'string' && name.startsWith('search-left-')">
516
+ <div class="filter-icon-box">
517
+ <slot :name="name" />
518
+ </div>
519
+ </template>
520
+ </template>
521
+ <VanCol class="search-col">
522
+ <VanSearch
523
+ v-model="searchValue"
524
+ class="title-search"
525
+ clearable
526
+ placeholder="综合查询框..."
527
+ shape="round"
528
+ @search="onRefresh"
529
+ />
530
+ </VanCol>
531
+ <!-- 右侧动态插槽区域 -->
532
+ <template v-for="(_, name) in slots" :key="name">
533
+ <template v-if="typeof name === 'string' && name.startsWith('search-right-')">
534
+ <div class="filter-icon-box">
535
+ <slot :name="name" />
536
+ </div>
537
+ </template>
538
+ </template>
539
+ <VanCol class="search-icon">
540
+ <XCellListFilter
541
+ v-model:sortord-val="sortordVal"
542
+ v-model:order-val="orderVal"
543
+ v-model:condition-params="conditionParams"
544
+ :fix-query-form="fixQueryForm"
545
+ :service-name="serviceName"
546
+ :order-list="orderList"
547
+ :form-query="formQueryList"
548
+ :button-state="buttonState"
549
+ :scan-options="scanOptions"
550
+ @on-refresh="onRefresh"
551
+ @add-option="addOption"
552
+ />
553
+ </VanCol>
554
+ </VanRow>
555
+ <slot name="search-after" />
556
+ <div class="main">
557
+ <VanPullRefresh v-model="refreshing" :success-text="finishedText" head-height="70" @refresh="onRefresh">
558
+ <template v-if="!isError">
559
+ <VanList
560
+ v-model:loading="loading"
561
+ class="list_main"
562
+ :finished="finished"
563
+ finished-text="已加载全部内容"
564
+ :immediate-check="isInitQuery"
565
+ @load="onLoad"
566
+ >
567
+ <div v-for="(item, index) in list" :key="`card_${index}`" class="card_item_main">
568
+ <VanRow gutter="20" class="card_item_header" align="center" @click="emit('toDetail', item)">
569
+ <VanCol :span="24">
570
+ <div class="title-row">
571
+ <div v-for="(column) in mainColumns" :key="`main_${column.dataIndex}`" class="main-title">
572
+ <p
573
+ class="card_item_title"
574
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
575
+ >
576
+ {{ item[column.dataIndex] ?? '--' }}
577
+ </p>
578
+ </div>
579
+ <div v-for="(column) in subTitleColumns" :key="`subtitle_${column.dataIndex}`" class="sub-title">
580
+ <p
581
+ class="card_item_subtitle"
582
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
583
+ >
584
+ <XBadge
585
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
586
+ :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
587
+ :service-name="serviceName"
588
+ />
589
+ </p>
590
+ </div>
591
+ <div class="action-buttons">
592
+ <VanButton
593
+ v-for="btn in btnList"
594
+ :key="btn.dataIndex"
595
+ class="action-button"
596
+ :icon="btn.btnIcon"
597
+ size="small"
598
+ @click.stop="handleButtonClick(btn, item)"
599
+ />
600
+ </div>
601
+ </div>
602
+ </VanCol>
603
+ </VanRow>
604
+ <VanRow gutter="20" class="card_item_details" @click="emit('toDetail', item)">
605
+ <VanCol v-for="column of detailColumns" :key="`details_${column.dataIndex}`" :span="column.span">
606
+ <p>
607
+ {{ (column.showLabel === undefined || column.showLabel) ? `${column.title}: ` : '' }}
608
+ <XBadge
609
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
610
+ :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
611
+ :service-name="serviceName"
612
+ />
613
+ </p>
614
+ </VanCol>
615
+ </VanRow>
616
+ <VanRow v-if="tagList.length > 0" gutter="20" class="tag-row" @click="emit('toDetail', item)">
617
+ <VanCol :span="24">
618
+ <div class="tag-container">
619
+ <div class="tag-wrapper">
620
+ <template
621
+ v-for="column of tagList"
622
+ :key="`tag_${column.dataIndex}`"
623
+ >
624
+ <VanTag
625
+ :text-color="column.tagColor"
626
+ :color="column.tagBorderColor"
627
+ size="large"
628
+ class="tag-item"
629
+ >
630
+ <div class="tag-content">
631
+ <VanIcon v-if="column.tagIcon" :name="column.tagIcon" class="tag-icon" />
632
+ <span v-if="column.showLabel === undefined || column.showLabel" class="tag-title">{{ `${column.title}: ` }}</span>
633
+ <XBadge
634
+ :dict-name="column.dictName"
635
+ :dict-value="item[column.dataIndex]"
636
+ :service-name="serviceName"
637
+ />
638
+ </div>
639
+ </VanTag>
640
+ </template>
641
+ </div>
642
+ </div>
643
+ </VanCol>
644
+ </VanRow>
645
+ <VanRow
646
+ v-if="footColumns && footColumns.length > 0"
647
+ gutter="20"
648
+ class="card_item_footer"
649
+ @click="emit('toDetail', item)"
650
+ >
651
+ <VanCol v-for="column of footColumns" :key="`foot_${column.dataIndex}`" :span="12">
652
+ <p>
653
+ <span :style="handleFunctionStyle(column.styleFunctionForTitle, item)">
654
+ {{ (column.showLabel === undefined || column.showLabel) ? `${column.title}: ` : '' }}
655
+ </span>
656
+ <XBadge
657
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
658
+ :dict-name="column.dictName" :dict-value="item[column.dataIndex]"
659
+ :service-name="serviceName"
660
+ />
661
+ </p>
662
+ </VanCol>
663
+ </VanRow>
664
+ <!-- 添加详情插槽 -->
665
+ <slot name="item-detail" :item="item" />
666
+ <VanRow v-if="allActions.length > 0" gutter="20" class="card_item_bottom">
667
+ <VanCol span="4">
668
+ <VanPopover
669
+ v-if="getActionGroups(item, index).more.length"
670
+ v-model:show="showPopover[index]"
671
+ placement="bottom-start"
672
+ theme="dark"
673
+ overlay
674
+ :actions="getActionGroups(item, index).more"
675
+ @select="onSelectMenu(item, $event)"
676
+ >
677
+ <template #reference>
678
+ <div class="more-button">
679
+ <span>⋯</span>
680
+ </div>
681
+ </template>
682
+ </VanPopover>
683
+ </VanCol>
684
+ <VanCol span="20">
685
+ <VanRow justify="end">
686
+ <VanSpace>
687
+ <VanButton
688
+ v-for="button in getActionGroups(item, index).main"
689
+ :key="button.func"
690
+ type="primary"
691
+ size="normal"
692
+ class="action-btn"
693
+ @click="onSelectMenu(item, button)"
694
+ >
695
+ {{ button.text }}
696
+ </VanButton>
697
+ </VanSpace>
698
+ </VanRow>
699
+ </VanCol>
700
+ </VanRow>
701
+ </div>
702
+ </VanList>
703
+ </template>
704
+ <template v-else>
705
+ <LoadError />
706
+ </template>
707
+ </VanPullRefresh>
708
+ <VanBackTop />
709
+ </div>
710
+ </div>
711
+ </template>
712
+
713
+ <style scoped lang="less">
714
+ #XCellList {
715
+ height: calc(100vh - var(--van-nav-bar-height) - 20px);
716
+ display: flex;
717
+ flex-direction: column;
718
+ --van-search-padding: 3px;
719
+ --van-dropdown-menu-title-padding: 3px;
720
+ --van-cell-horizontal-padding: 0px;
721
+ --van-text-color-2: rgb(75, 85, 99);
722
+ --van-button-normal-font-size: 13px;
723
+ .main {
724
+ flex: 1;
725
+ min-height: 0;
726
+ overflow-y: auto;
727
+ background-color: var(--van-background);
728
+ padding: var(--van-padding-base) var(--van-padding-sm);
729
+
730
+ p {
731
+ white-space: nowrap;
732
+ overflow: hidden;
733
+ text-overflow: ellipsis;
734
+ margin: 0;
735
+ }
736
+
737
+ .card_item_main {
738
+ background-color: var(--van-background-2);
739
+ border-radius: var(--van-radius-lg);
740
+ margin: 0 0 var(--van-padding-xs) 0;
741
+ padding: var(--van-padding-sm);
742
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
743
+ transition: all 0.3s ease;
744
+ border: 1px solid rgba(0, 0, 0, 0.04);
745
+
746
+ &:active {
747
+ transform: scale(0.98);
748
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
749
+ }
750
+
751
+ .card_item_header {
752
+ margin-bottom: var(--van-padding-base);
753
+
754
+ .title-row {
755
+ display: flex;
756
+ align-items: center;
757
+ margin-bottom: 2px;
758
+ width: 100%;
759
+
760
+ .main-title {
761
+ display: inline-flex;
762
+ align-items: center;
763
+ .card_item_title {
764
+ font-size: var(--van-font-size-lg);
765
+ font-weight: 700;
766
+ color: var(--van-text-color);
767
+ margin: 0;
768
+ }
769
+ }
770
+
771
+ .sub-title {
772
+ display: inline-flex;
773
+ align-items: center;
774
+ margin-left: 8px;
775
+ .card_item_subtitle {
776
+ font-size: var(--van-font-size-md);
777
+ color: var(--van-text-color-2);
778
+ }
779
+ }
780
+
781
+ .action-buttons {
782
+ display: flex;
783
+ align-items: center;
784
+ margin-left: auto;
785
+ .action-button {
786
+ margin-left: 6px;
787
+ width: 32px;
788
+ height: 32px;
789
+ padding: 0;
790
+ border: none;
791
+ color: var(--van-primary-color);
792
+ background-color: rgba(25, 137, 250, 0.1);
793
+ border-radius: 6px;
794
+ font-size: var(--van-font-size-lg);
795
+ display: flex;
796
+ align-items: center;
797
+ justify-content: center;
798
+ transition: all 0.2s ease;
799
+ &:active {
800
+ opacity: 0.7;
801
+ background-color: rgba(25, 137, 250, 0.2);
802
+ transform: scale(0.95);
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+
809
+ .tag-row {
810
+ margin-bottom: var(--van-padding-base);
811
+ }
812
+
813
+ .tag-container {
814
+ width: 100%;
815
+ .tag-wrapper {
816
+ display: flex;
817
+ flex-wrap: wrap;
818
+ padding: 0 !important;
819
+ align-items: center;
820
+ width: 100%;
821
+ margin: 0 -4px;
822
+
823
+ .tag-item {
824
+ width: auto;
825
+ font-size: var(--van-font-size-md);
826
+ margin: 4px 4px;
827
+ :deep(.van-tag) {
828
+ width: fit-content;
829
+ display: inline-flex;
830
+ align-items: center;
831
+ padding: 2px 8px;
832
+ border-radius: 4px;
833
+ }
834
+ .tag-content {
835
+ display: flex;
836
+ align-items: center;
837
+ //white-space: nowrap;
838
+ }
839
+ .tag-icon {
840
+ margin-right: 4px;
841
+ font-size: 14px;
842
+ display: inline-flex;
843
+ align-items: center;
844
+ }
845
+ .tag-title {
846
+ font-weight: normal;
847
+ }
848
+ }
849
+ }
850
+ }
851
+
852
+ .card_item_details {
853
+ font-size: var(--van-font-size-md);
854
+ color: var(--van-text-color-2);
855
+ padding: 4px 0;
856
+
857
+ .van-col {
858
+ margin-bottom: 4px;
859
+ p {
860
+ display: flex;
861
+ align-items: center;
862
+ gap: 4px;
863
+
864
+ span {
865
+ flex: 1;
866
+ min-width: 0;
867
+ overflow: hidden;
868
+ text-overflow: ellipsis;
869
+ white-space: nowrap;
870
+ }
871
+
872
+ :deep(.van-badge) {
873
+ flex-shrink: 0;
874
+ }
875
+ }
876
+ }
877
+ }
878
+
879
+ .card_item_footer {
880
+ font-size: var(--van-font-size-md);
881
+ color: var(--van-text-color-2);
882
+ padding: 4px 0;
883
+ .van-col:last-child {
884
+ text-align: right;
885
+ }
886
+ .van-col:first-child {
887
+ text-align: left;
888
+ }
889
+ }
890
+
891
+ .card_item_bottom {
892
+ margin-top: 8px;
893
+ padding-top: 10px;
894
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
895
+
896
+ .more-button {
897
+ width: 28px;
898
+ height: 28px;
899
+ display: flex;
900
+ align-items: center;
901
+ justify-content: center;
902
+ color: var(--van-text-color-2);
903
+ cursor: pointer;
904
+ font-size: var(--van-font-size-lg);
905
+ background-color: var(--van-background);
906
+ border-radius: 6px;
907
+ transition: all 0.2s ease;
908
+ border: 1px solid rgba(0, 0, 0, 0.06);
909
+ span {
910
+ line-height: 1;
911
+ margin-top: -2px;
912
+ }
913
+ &:active {
914
+ opacity: 0.7;
915
+ background-color: var(--van-background-2);
916
+ transform: scale(0.95);
917
+ }
918
+ }
919
+
920
+ .action-btn {
921
+ --van-button-primary-border-color: #1890ff;
922
+ --van-button-primary-background: #1890ff;
923
+ min-width: 76px;
924
+ height: 40px;
925
+ border-radius: 10px;
926
+ // font-size: var(--van-font-size-md);
927
+ transition: all 0.2s ease;
928
+ &:active {
929
+ transform: scale(0.95);
930
+ }
931
+ }
932
+ }
933
+ }
934
+ }
935
+
936
+ .filter-condition {
937
+ display: flex;
938
+ align-items: center;
939
+ padding: 8px 12px;
940
+ background-color: #fff;
941
+ gap: 1px;
942
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
943
+ position: sticky;
944
+ top: 0;
945
+ z-index: 10;
946
+ :deep(.van-search) {
947
+ width: 100%;
948
+ padding: var(--van-search-padding);
949
+ background-color: transparent;
950
+ }
951
+ :deep(.van-search__content) {
952
+ border-radius: 8px;
953
+ background-color: var(--van-background);
954
+ padding: 4px 12px;
955
+ border: 1px solid rgba(0, 0, 0, 0.01);
956
+ height: 40px;
957
+ }
958
+ :deep(.van-field__left-icon) {
959
+ color: var(--van-text-color-2);
960
+ }
961
+ :deep(.van-cell) {
962
+ background-color: transparent;
963
+ }
964
+ :deep(.van-field__control::placeholder) {
965
+ color: var(--van-text-color-2);
966
+ font-size: 14px;
967
+ }
968
+ .van-col {
969
+ display: flex;
970
+ align-items: center;
971
+ &.search-col {
972
+ flex: 1;
973
+ min-width: 0;
974
+ }
975
+ &.search-icon {
976
+ flex-shrink: 0;
977
+ padding: 0;
978
+ }
979
+ }
980
+ .filter-icon-box {
981
+ display: flex;
982
+ align-items: center;
983
+ justify-content: center;
984
+ width: 36px;
985
+ height: 36px;
986
+ border-radius: 8px;
987
+ background-color: var(--van-background);
988
+ cursor: pointer;
989
+ position: relative;
990
+ z-index: 1;
991
+ border: 1px solid rgba(0, 0, 0, 0.06);
992
+ transition: all 0.2s ease;
993
+ &:active {
994
+ opacity: 0.7;
995
+ transform: scale(0.95);
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ </style>