list.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <template>
  2. <view class="universal-list-page">
  3. <!-- 筛选表单和操作区域 -->
  4. <view class="filter-section" v-if="fieldsList.length > 0 || showScopeFilter || buttonList.length > 0">
  5. <view class="filter-row">
  6. <!-- 有 cbName 的字段用 SsSelect 下拉选择器 -->
  7. <SsSelect
  8. v-for="field in fieldsList"
  9. :key="field.name"
  10. v-model="searchParams[field.name]"
  11. :placeholder="field.desc"
  12. :options="fieldOptions[field.name] || []"
  13. :loading="fieldLoading[field.name]"
  14. width="auto"
  15. minWidth="200rpx"
  16. class="filter-select"
  17. @change="(value) => handleFieldChange(field.name, value)"
  18. />
  19. <!-- 管理类别筛选(条件:isReady=="1" && !isMultipleObject) -->
  20. <SsSearchButton
  21. v-if="showScopeFilter"
  22. :text="scopeText"
  23. :options="scopeOptions"
  24. @change="handleScopeChange"
  25. />
  26. <!-- 操作按钮 -->
  27. <SsSearchButton
  28. v-for="(btn, index) in buttonList"
  29. :key="index"
  30. :text="btn.buttonName"
  31. @click="handleButtonClick(btn)"
  32. />
  33. </view>
  34. </view>
  35. <!-- 列表区域 -->
  36. <view class="list-section">
  37. <view v-if="loading && list.length === 0" class="loading-state">
  38. <text>加载中...</text>
  39. </view>
  40. <view v-else-if="list.length === 0" class="empty-state">
  41. <text>暂无数据</text>
  42. </view>
  43. <view v-else>
  44. <SsCard
  45. v-for="(item, index) in list"
  46. :key="item.uniqueId || index"
  47. :item="item"
  48. @click="handleItemClick(item)"
  49. >
  50. <!-- 动态渲染卡片内容 -->
  51. <view class="card-content">
  52. <!-- 主标题 (first) -->
  53. <view class="card-header" v-if="item.first">
  54. <view class="card-title">
  55. {{ formatFieldValueWrapper(item.first) }}
  56. </view>
  57. </view>
  58. <!-- 描述 (second) -->
  59. <view class="card-description" v-if="item.second">
  60. {{ formatFieldValueWrapper(item.second) }}
  61. </view>
  62. <!-- 属性列表 (third) -->
  63. <view class="card-attributes" v-if="item.third && item.third.length > 0">
  64. <view
  65. v-for="(group, groupIndex) in item.third"
  66. :key="groupIndex"
  67. class="attribute-group"
  68. >
  69. <view
  70. v-for="(attr, attrIndex) in group"
  71. :key="attrIndex"
  72. class="attribute-item"
  73. >
  74. <text class="attr-label">{{ attr.field.desc }}:</text>
  75. <text class="attr-value">{{ formatFieldValueWrapper(attr) }}</text>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. </SsCard>
  81. </view>
  82. </view>
  83. <!-- 加载更多提示 -->
  84. <view v-if="loading && list.length > 0" class="load-more-tip">
  85. <text>加载中...</text>
  86. </view>
  87. <view v-else-if="!hasMore && list.length > 0" class="no-more-tip">
  88. <text>没有更多数据了</text>
  89. </view>
  90. </view>
  91. </template>
  92. <script setup>
  93. import { computed, ref } from 'vue'
  94. import { onReachBottom, onLoad } from '@dcloudio/uni-app'
  95. import SsSearchButton from '@/components/SsSearchButton/index.vue'
  96. import SsCard from '@/components/SsCard/index.vue'
  97. import SsSelect from '@/components/SsSelect/index.vue'
  98. import { commonApi } from '@/api/common'
  99. import { formatFieldValue, getDictTranslation } from '@/utils/fieldFormatter'
  100. import { goTo } from '@/utils/navigation'
  101. // 页面参数(从路由传入)
  102. const pageConfig = ref({
  103. ssServ: '', // 服务名称
  104. title: '', // 页面标题
  105. baseParams: {}, // 基础查询参数
  106. })
  107. // 列表数据
  108. const list = ref([])
  109. const buttonList = ref([])
  110. const fieldsList = ref([])
  111. const loading = ref(false)
  112. const hasMore = ref(true)
  113. const currentPage = ref(1)
  114. const pageSize = ref(10)
  115. // 搜索相关
  116. const searchParams = ref({management:99})
  117. const fieldOptions = ref({}) // 存储每个字段的选项
  118. const fieldLoading = ref({}) // 存储每个字段的加载状态
  119. // 管理类别相关
  120. const showScopeFilter = ref(false) // 是否显示管理类别筛选
  121. const scopeOptions = ref([
  122. { text: '所有', value: 99 },
  123. { text: '管理', value: 2 },
  124. { text: '创建', value: 1 },
  125. { text: '已办', value: 3 },
  126. { text: '停用', value: 55 }
  127. ])
  128. const scopeText = computed(() => {
  129. const selectedOption = scopeOptions.value.find(option => option.value === searchParams.value.management)
  130. return selectedOption ? selectedOption.text : '所有'
  131. })
  132. // 字典缓存
  133. const dictCache = ref(new Map())
  134. /**
  135. * 格式化字段值的包装函数
  136. */
  137. const formatFieldValueWrapper = (fieldObj) => {
  138. return formatFieldValue(fieldObj, dictCache.value, handleDictTranslation)
  139. }
  140. /**
  141. * 处理字典翻译的包装函数
  142. */
  143. const handleDictTranslation = (cbName, value, cacheKey) => {
  144. getDictTranslation(cbName, value, cacheKey, dictCache.value, () => {
  145. // 触发页面更新
  146. list.value = [...list.value]
  147. })
  148. }
  149. /**
  150. * 加载字段选项
  151. */
  152. const loadFieldOptions = async (fields) => {
  153. for (const field of fields) {
  154. if (field.cbName) {
  155. // 有 cbName 的字段需要加载下拉选项
  156. fieldLoading.value[field.name] = true
  157. try {
  158. const result = await commonApi.getDictOptionsByCbName(field.cbName)
  159. console.log(`字段 ${field.name} 字典数据:`, result)
  160. if (result.data && result.data.result) {
  161. const resultData = result.data.result
  162. // 转换为 SsSelect 需要的格式:[{n:"显示名称", v:"值"}]
  163. const options = Object.keys(resultData).map(key => ({
  164. n: resultData[key], // 显示名称
  165. v: key // 值
  166. }))
  167. // 在前面插入清空选项
  168. fieldOptions.value[field.name] = [
  169. { n: "全部", v: "" }, // 清空条件的选项
  170. ...options
  171. ]
  172. }
  173. } catch (error) {
  174. console.error(`加载字段 ${field.name} 选项失败:`, error)
  175. fieldOptions.value[field.name] = []
  176. } finally {
  177. fieldLoading.value[field.name] = false
  178. }
  179. } else {
  180. // 没有 cbName 的字段暂时不处理(文本输入、日期等)
  181. fieldOptions.value[field.name] = []
  182. }
  183. }
  184. }
  185. /**
  186. * 加载数据
  187. */
  188. const loadData = async (pageNo = 1, isLoadMore = false) => {
  189. if (loading.value) return
  190. loading.value = true
  191. try {
  192. const params = {
  193. pageNo,
  194. rowNumPer: pageSize.value,
  195. ...pageConfig.value.baseParams,
  196. ...searchParams.value
  197. }
  198. const result = await commonApi.universalQuery(pageConfig.value.ssServ, params)
  199. console.log(`通用列表查询结果 [${pageConfig.value.ssServ}]:`, result)
  200. if (result.data) {
  201. const {
  202. objectList = [],
  203. ssPaging,
  204. buttonList: buttons = [],
  205. fieldsList: fields = [],
  206. isReady = "0",
  207. isMultipleObject = false
  208. } = result.data
  209. if (isLoadMore) {
  210. list.value = [...list.value, ...objectList]
  211. } else {
  212. list.value = objectList
  213. buttonList.value = buttons
  214. fieldsList.value = fields
  215. // 判断是否显示管理类别筛选:isReady=="1" && !isMultipleObject
  216. showScopeFilter.value = isReady == "1" && !isMultipleObject && true
  217. console.log('管理类别筛选判断:', { isReady, isMultipleObject, showScopeFilter: showScopeFilter.value })
  218. // 加载字段选项
  219. await loadFieldOptions(fields)
  220. }
  221. // 更新分页信息
  222. if (ssPaging) {
  223. currentPage.value = ssPaging.pageNo
  224. hasMore.value = (ssPaging.pageNo * ssPaging.rowNumPer) < ssPaging.rowNum
  225. }
  226. }
  227. } catch (error) {
  228. console.error('加载数据失败:', error)
  229. uni.showToast({
  230. title: '加载失败',
  231. icon: 'error'
  232. })
  233. } finally {
  234. loading.value = false
  235. }
  236. }
  237. /**
  238. * 加载更多数据
  239. */
  240. const loadMore = async () => {
  241. if (!hasMore.value || loading.value) return
  242. await loadData(currentPage.value + 1, true)
  243. }
  244. /**
  245. * 搜索字段值改变
  246. */
  247. const handleFieldChange = (fieldName, value) => {
  248. searchParams.value[fieldName] = value
  249. console.log(`搜索字段 ${fieldName} 改变:`, value)
  250. // 立即执行搜索
  251. handleSearch()
  252. }
  253. /**
  254. * 管理类别改变
  255. */
  256. const handleScopeChange = (option) => {
  257. searchParams.value.management = option.value
  258. console.log('管理类别改变:', option)
  259. // 立即执行搜索
  260. handleSearch()
  261. }
  262. /**
  263. * 执行搜索
  264. */
  265. const handleSearch = () => {
  266. console.log('执行搜索,参数:', searchParams.value)
  267. // 重置页码并重新加载数据
  268. currentPage.value = 1
  269. loadData(1, false)
  270. }
  271. /**
  272. * 操作按钮点击
  273. */
  274. const handleButtonClick = (button) => {
  275. console.log('操作按钮点击2:', button)
  276. const parts = button.function.dest.split('_')
  277. const dir = parts[0]
  278. const mobilePage = `/pages/${dir}/${button.function.dest}`
  279. goTo(mobilePage)
  280. }
  281. /**
  282. * 列表项点击
  283. */
  284. const handleItemClick = (item) => {
  285. // 如果有service配置,执行相应操作
  286. if (item.service && item.service.play) {
  287. // const { service: serviceName, param } = item.service.play
  288. // console.log(`执行服务: ${serviceName}`, param)
  289. goTo(`/pages/clyy/clyy_play`)
  290. // TODO: 根据service配置跳转到相应页面
  291. }
  292. }
  293. // 使用onLoad获取页面参数
  294. onLoad((options) => {
  295. console.log('页面参数:', options)
  296. // 解析参数
  297. pageConfig.value = {
  298. ssServ: options.ssServ || '',
  299. title: options.title || '通用列表',
  300. baseParams: options.baseParams ? JSON.parse(decodeURIComponent(options.baseParams)) : {}
  301. }
  302. console.log('解析后的页面配置:', pageConfig.value)
  303. // 加载数据
  304. if (pageConfig.value.ssServ) {
  305. loadData()
  306. } else {
  307. console.error('缺少必要参数: ssServ')
  308. uni.showToast({
  309. title: '参数错误',
  310. icon: 'error'
  311. })
  312. }
  313. })
  314. // 使用uni-app的页面生命周期监听滚动到底部
  315. onReachBottom(() => {
  316. loadMore()
  317. })
  318. </script>
  319. <style lang="scss" scoped>
  320. ss-select{
  321. width: auto !important;
  322. }
  323. .universal-list-page {
  324. min-height: 100vh;
  325. background-color: #f5f5f5;
  326. }
  327. .page-header {
  328. background: #fff;
  329. padding: 20rpx 30rpx;
  330. border-bottom: 1rpx solid #eee;
  331. .page-title {
  332. font-size: 36rpx;
  333. font-weight: bold;
  334. color: #333;
  335. }
  336. }
  337. .filter-section {
  338. padding: 20rpx 30rpx;
  339. // margin-bottom: 20rpx;
  340. .filter-row {
  341. display: flex;
  342. justify-content: flex-end;
  343. align-items: flex-end;
  344. flex-wrap: wrap;
  345. gap: 20rpx;
  346. }
  347. // SsSelect 在筛选区域的样式
  348. .filter-select {
  349. :deep(.ss-select) {
  350. height:36px;
  351. border: 1rpx solid #ddd;
  352. border-radius: 8rpx;
  353. padding: 15rpx 20rpx;
  354. background: #fff;
  355. min-height: auto;
  356. }
  357. }
  358. }
  359. .list-section {
  360. padding: 0 30rpx;
  361. }
  362. .card-content {
  363. .card-header {
  364. margin-bottom: 20rpx;
  365. .card-title {
  366. font-size: 32rpx;
  367. font-weight: bold;
  368. color: #333;
  369. }
  370. }
  371. .card-description {
  372. font-size: 28rpx;
  373. color: #666;
  374. margin-bottom: 15rpx;
  375. }
  376. .attribute-group {
  377. display: flex;
  378. flex-wrap: wrap;
  379. column-gap: 20rpx;
  380. }
  381. .attribute-item {
  382. display: flex;
  383. margin-bottom: 10rpx;
  384. .attr-label {
  385. font-size: 26rpx;
  386. color: #999;
  387. }
  388. .attr-value {
  389. font-size: 26rpx;
  390. color: #333;
  391. flex: 1;
  392. }
  393. }
  394. }
  395. .loading-state,
  396. .empty-state {
  397. text-align: center;
  398. padding: 100rpx 0;
  399. color: #999;
  400. }
  401. .load-more-tip,
  402. .no-more-tip {
  403. text-align: center;
  404. padding: 30rpx 0;
  405. color: #999;
  406. font-size: 24rpx;
  407. }
  408. </style>