mp_objList.html 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  6. <title>列表</title>
  7. <!-- 引入基础依赖(统一由 base.js 动态注入其他依赖) -->
  8. <script src="/js/mp_base/base.js"></script>
  9. <style>
  10. #app {
  11. background: #f5f5f5;
  12. min-height: 100vh;
  13. }
  14. /* 防止Vue模板闪烁 */
  15. [v-cloak] {
  16. display: none !important;
  17. }
  18. /* 搜索筛选区域 */
  19. .search-filter-container {
  20. background: #f5f5f5;
  21. padding: 15px;
  22. position: sticky;
  23. top: 0;
  24. z-index: 100;
  25. display: flex;
  26. justify-content: flex-end;
  27. flex-wrap: wrap;
  28. gap: 10px;
  29. }
  30. .search-filter-container .ss-select-container{
  31. border: 1px solid #ccc;
  32. border-radius: 4px;
  33. padding: 0 10px;
  34. height: 34px;
  35. box-sizing: border-box;
  36. }
  37. /* 加载状态 */
  38. .loading-container {
  39. display: flex;
  40. flex-direction: column;
  41. align-items: center;
  42. justify-content: center;
  43. height: 200px;
  44. color: #666;
  45. }
  46. .loading-spinner {
  47. width: 40px;
  48. height: 40px;
  49. border: 4px solid #f3f3f3;
  50. border-top: 4px solid #40ac6d;
  51. border-radius: 50%;
  52. animation: spin 1s linear infinite;
  53. margin-bottom: 15px;
  54. }
  55. @keyframes spin {
  56. 0% { transform: rotate(0deg); }
  57. 100% { transform: rotate(360deg); }
  58. }
  59. .loading-text {
  60. font-size: 14px;
  61. }
  62. /* 列表容器 */
  63. .list-container {
  64. padding:0 15px;
  65. }
  66. /* 空状态 */
  67. .empty-state {
  68. text-align: center;
  69. padding: 60px 20px;
  70. color: #999;
  71. }
  72. .empty-icon {
  73. font-size: 48px;
  74. margin-bottom: 15px;
  75. }
  76. .empty-text {
  77. font-size: 16px;
  78. }
  79. /* 卡片内容样式 - 按照小程序list.vue转换 */
  80. .card-content .card-header {
  81. margin-bottom: 10px; /* 20rpx -> 10px */
  82. }
  83. .card-content .card-title {
  84. font-size: 16px; /* 32rpx -> 16px */
  85. font-weight: bold;
  86. color: #333;
  87. }
  88. .card-content .card-description {
  89. font-size: 14px; /* 28rpx -> 14px */
  90. color: #666;
  91. margin-bottom: 8px; /* 15rpx -> 8px */
  92. }
  93. .card-content .attribute-group {
  94. display: flex;
  95. flex-wrap: wrap;
  96. column-gap: 10px; /* 20rpx -> 10px */
  97. }
  98. .card-content .attribute-item {
  99. display: flex;
  100. margin-bottom: 5px; /* 10rpx -> 5px */
  101. }
  102. .card-content .attr-label {
  103. font-size: 13px; /* 26rpx -> 13px */
  104. color: #999;
  105. }
  106. .card-content .attr-value {
  107. font-size: 13px; /* 26rpx -> 13px */
  108. color: #333;
  109. flex: 1;
  110. }
  111. /* 状态文本样式 */
  112. .status-text {
  113. font-weight: bold;
  114. }
  115. /* 加载更多提示样式 */
  116. .load-more-container {
  117. text-align: center;
  118. padding: 20px;
  119. color: #999;
  120. font-size: 14px;
  121. }
  122. .load-more-loading {
  123. color: #007aff;
  124. }
  125. .load-more-end {
  126. color: #999;
  127. }
  128. .load-more-tip {
  129. color: #ccc;
  130. }
  131. /* 回到顶部按钮 */
  132. .back-to-top-btn {
  133. width: 50px;
  134. height: 50px;
  135. border-radius: 50%;
  136. background: rgba(87, 93, 109, 0.5);
  137. display: flex;
  138. justify-content: center;
  139. align-items: center;
  140. position: fixed;
  141. bottom: 200px;
  142. right: 15px;
  143. cursor: pointer;
  144. transition: all 0.3s ease;
  145. z-index: 999;
  146. }
  147. .back-to-top-btn:active {
  148. transform: scale(0.9);
  149. }
  150. .back-to-top-inner {
  151. width: 42px;
  152. height: 42px;
  153. border-radius: 50%;
  154. background: rgba(87, 93, 109, 0.5);
  155. display: flex;
  156. justify-content: center;
  157. align-items: center;
  158. }
  159. /* 搜索弹窗 */
  160. .search-modal-mask {
  161. position: fixed;
  162. top: 0;
  163. left: 0;
  164. right: 0;
  165. bottom: 0;
  166. background: rgba(0, 0, 0, 0.5);
  167. z-index: 1000;
  168. display: flex;
  169. align-items: flex-end;
  170. }
  171. .search-modal-content {
  172. width: 100%;
  173. background: white;
  174. animation: slideUp 0.3s ease-out;
  175. /* 让键盘弹起时自动上移 */
  176. position: relative;
  177. }
  178. @keyframes slideUp {
  179. from {
  180. transform: translateY(100%);
  181. }
  182. to {
  183. transform: translateY(0);
  184. }
  185. }
  186. .search-input-container {
  187. display: flex;
  188. align-items: center;
  189. height: 50px;
  190. background: white;
  191. border-top: 1px solid #e5e5e5;
  192. /* 适配iPhone底部安全区域 */
  193. /* padding-bottom: env(safe-area-inset-bottom); */
  194. }
  195. .search-input {
  196. flex: 1;
  197. height: 100%;
  198. border: none;
  199. padding: 0 15px;
  200. font-size: 16px;
  201. outline: none;
  202. background: transparent;
  203. }
  204. .search-divider {
  205. width: 1px;
  206. height: 30px;
  207. background: #d5d8dc;
  208. }
  209. .search-icon-btn {
  210. width: 60px;
  211. height: 100%;
  212. display: flex;
  213. align-items: center;
  214. justify-content: center;
  215. cursor: pointer;
  216. background: transparent;
  217. border: none;
  218. padding: 0;
  219. }
  220. .search-icon-btn:active {
  221. background: #f5f5f5;
  222. }
  223. </style>
  224. </head>
  225. <body>
  226. <div id="app" v-cloak>
  227. <!-- 搜索和筛选区域 -->
  228. <div class="search-filter-container">
  229. <!-- 动态下拉选择器 -->
  230. <template v-for="(options, fieldName) in filterSelectOptions" :key="fieldName">
  231. <ss-select
  232. v-model="selectedFilters[fieldName]"
  233. :placeholder="`选择${getFieldDesc(fieldName)}`"
  234. :options="options"
  235. @change="handleFilterChange"
  236. >
  237. </ss-select>
  238. </template>
  239. <!-- 完全由buttonList决定的动态按钮组 -->
  240. <ss-search-button
  241. v-for="(button, index) in buttonList"
  242. :key="index"
  243. :text="getButtonText(button)"
  244. @click="handleButtonClick(button)"
  245. >
  246. </ss-search-button>
  247. </div>
  248. <!-- 加载状态 -->
  249. <div v-if="loading" class="loading-container">
  250. <div class="loading-spinner"></div>
  251. <div class="loading-text">加载中...</div>
  252. </div>
  253. <!-- 列表区域 -->
  254. <div v-else class="list-container">
  255. <!-- 空状态 -->
  256. <div v-if="list.length === 0" class="empty-state">
  257. <div class="empty-icon">📋</div>
  258. <div class="empty-text">暂无数据</div>
  259. </div>
  260. <!-- 数据列表 -->
  261. <div v-else>
  262. <ss-card
  263. v-for="(item, index) in list"
  264. :key="index"
  265. :item="item"
  266. @click="handleCardClick(item)"
  267. @button-click="handleCardAction"
  268. >
  269. <!-- 卡片内容 - 按照小程序list.vue的结构,使用API数据 -->
  270. <div class="card-content">
  271. <!-- 主标题 (first) -->
  272. <div class="card-header" v-if="item.firstDisplay">
  273. <div class="card-title">{{ item.firstDisplay }}</div>
  274. </div>
  275. <!-- 描述 (second) -->
  276. <div class="card-description" v-if="item.secondDisplay">
  277. {{ item.secondDisplay }}
  278. </div>
  279. <!-- 属性列表 (third) -->
  280. <div class="card-attributes" v-if="item.thirdDisplay && item.thirdDisplay.length > 0">
  281. <div
  282. v-for="(group, groupIndex) in item.thirdDisplay"
  283. :key="groupIndex"
  284. class="attribute-group"
  285. >
  286. <div
  287. v-for="(attr, attrIndex) in group"
  288. :key="attrIndex"
  289. class="attribute-item"
  290. >
  291. <span class="attr-label">{{ attr.field.desc }}:</span>
  292. <span class="attr-value">{{ attr.displayValue }}</span>
  293. </div>
  294. </div>
  295. </div>
  296. </div>
  297. </ss-card>
  298. </div>
  299. </div>
  300. <!-- 加载更多提示 -->
  301. <div class="load-more-container" v-if="list.length > 0">
  302. <div v-if="isLoadingMore" class="load-more-loading">
  303. <span>正在加载更多...</span>
  304. </div>
  305. <div v-else-if="!hasMore" class="load-more-end">
  306. <span>没有更多数据了</span>
  307. </div>
  308. <div v-else class="load-more-tip">
  309. <span>滚动到底部加载更多</span>
  310. </div>
  311. </div>
  312. <!-- 回到顶部按钮 -->
  313. <div
  314. v-if="showBackToTop"
  315. class="back-to-top-btn"
  316. @touchstart="handleLongPressStart"
  317. @touchend="handleLongPressEnd"
  318. @touchcancel="handleLongPressCancel"
  319. @click.prevent="handleBackToTopClick"
  320. >
  321. <div class="back-to-top-inner">
  322. <Icon name="icon-huidaodingbu" size="40" color="#fff"></Icon>
  323. </div>
  324. </div>
  325. <!-- 搜索弹窗 -->
  326. <div v-if="showSearchModal" class="search-modal-mask" @click="closeSearchModal">
  327. <div class="search-modal-content" @click.stop>
  328. <div class="search-input-container">
  329. <input
  330. ref="searchInput"
  331. v-model="searchKeyword"
  332. type="search"
  333. inputmode="search"
  334. class="search-input"
  335. placeholder="请输入关键词"
  336. @keyup.enter="performSearch"
  337. autocomplete="off"
  338. />
  339. <div class="search-divider"></div>
  340. <button class="search-icon-btn" @click="performSearch">
  341. <Icon name="icon-chazhao" size="24" color="#575d6d"></Icon>
  342. </button>
  343. </div>
  344. </div>
  345. </div>
  346. </div>
  347. <script>
  348. // 等待SS框架加载完成
  349. window.SS.ready(function () {
  350. // 使用SS框架的方式创建Vue实例
  351. window.SS.dom.initializeFormApp({
  352. el: '#app',
  353. data() {
  354. return {
  355. // 加载状态
  356. loading: true,
  357. // 列表数据
  358. list: [],
  359. originalList: [], // 原始数据,用于筛选
  360. // 分页相关
  361. currentPage: 1,
  362. pageSize: 10,
  363. hasMore: true,
  364. isLoadingMore: false,
  365. // 页面参数
  366. pageParams: {},
  367. service: '', // init服务名称
  368. // 功能说明:对接PC两段式接口(init 返回 home/list 服务名) by xu 2026-02-28
  369. ssSearchPobjHomeServName: '',
  370. ssSearchPobjListServName: '',
  371. // API返回的动态配置
  372. buttonList: [],
  373. fieldsList: [],
  374. ssPaging: null,
  375. // 动态搜索筛选选项
  376. filterOptions: [],
  377. sortOptions: [],
  378. // 下拉选择器数据
  379. selectedFilters: {}, // 动态筛选条件
  380. filterSelectOptions: {}, // 各个筛选字段的选项
  381. // 字典缓存
  382. dictCache: new Map(),
  383. // 显示字段配置 - 根据service动态设置
  384. displayFields: [],
  385. // 卡片操作按钮
  386. cardActions: [
  387. { text: '查看', name: 'view' },
  388. { text: '编辑', name: 'edit' }
  389. ],
  390. // 回到顶部按钮
  391. showBackToTop: false,
  392. // 是否存在关键词搜索
  393. hasKeyWord:false,
  394. // 搜索相关
  395. showSearchModal: false,
  396. searchKeyword: '',
  397. // 长按相关
  398. longPressTimer: null,
  399. isLongPress: false,
  400. }
  401. },
  402. mounted() {
  403. // 页面加载时初始化
  404. this.initPage();
  405. // 监听页面刷新通知
  406. this.setupRefreshListener();
  407. // 监听滚动事件,实现滚动加载
  408. this.setupScrollListener();
  409. // 开发模式:加载Mock数据(测试用)
  410. // this.loadMockData();
  411. },
  412. beforeUnmount() {
  413. // 清理刷新监听器
  414. if (this.refreshCleanup) {
  415. this.refreshCleanup();
  416. }
  417. // 清理滚动监听器
  418. if (this.scrollCleanup) {
  419. this.scrollCleanup();
  420. }
  421. },
  422. methods: {
  423. // 加载Mock数据(用于开发测试)
  424. loadMockData() {
  425. console.log('🎭 加载Mock数据...');
  426. const mockData = [
  427. {
  428. firstDisplay: '张三 - 2024级计算机科学与技术1班',
  429. secondDisplay: '学号:2024001 | 手机:138****1234',
  430. thirdDisplay: [
  431. [
  432. { field: { desc: '性别' }, displayValue: '男' },
  433. { field: { desc: '年龄' }, displayValue: '20' },
  434. { field: { desc: '状态' }, displayValue: '在读' }
  435. ],
  436. [
  437. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  438. { field: { desc: '辅导员' }, displayValue: '王老师' }
  439. ]
  440. ]
  441. },
  442. {
  443. firstDisplay: '李四 - 2024级软件工程2班',
  444. secondDisplay: '学号:2024002 | 手机:139****5678',
  445. thirdDisplay: [
  446. [
  447. { field: { desc: '性别' }, displayValue: '女' },
  448. { field: { desc: '年龄' }, displayValue: '19' },
  449. { field: { desc: '状态' }, displayValue: '在读' }
  450. ],
  451. [
  452. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  453. { field: { desc: '辅导员' }, displayValue: '李老师' }
  454. ]
  455. ]
  456. },
  457. {
  458. firstDisplay: '王五 - 2023级人工智能1班',
  459. secondDisplay: '学号:2023003 | 手机:136****9012',
  460. thirdDisplay: [
  461. [
  462. { field: { desc: '性别' }, displayValue: '男' },
  463. { field: { desc: '年龄' }, displayValue: '21' },
  464. { field: { desc: '状态' }, displayValue: '在读' }
  465. ],
  466. [
  467. { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
  468. { field: { desc: '辅导员' }, displayValue: '赵老师' }
  469. ]
  470. ]
  471. },
  472. {
  473. firstDisplay: '赵六 - 2024级数据科学与大数据技术1班',
  474. secondDisplay: '学号:2024004 | 手机:137****3456',
  475. thirdDisplay: [
  476. [
  477. { field: { desc: '性别' }, displayValue: '女' },
  478. { field: { desc: '年龄' }, displayValue: '20' },
  479. { field: { desc: '状态' }, displayValue: '在读' }
  480. ],
  481. [
  482. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  483. { field: { desc: '辅导员' }, displayValue: '刘老师' }
  484. ]
  485. ]
  486. },
  487. {
  488. firstDisplay: '钱七 - 2023级网络工程1班',
  489. secondDisplay: '学号:2023005 | 手机:135****7890',
  490. thirdDisplay: [
  491. [
  492. { field: { desc: '性别' }, displayValue: '男' },
  493. { field: { desc: '年龄' }, displayValue: '21' },
  494. { field: { desc: '状态' }, displayValue: '休学' }
  495. ],
  496. [
  497. { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
  498. { field: { desc: '辅导员' }, displayValue: '周老师' }
  499. ]
  500. ]
  501. },
  502. {
  503. firstDisplay: '孙八 - 2024级信息安全1班',
  504. secondDisplay: '学号:2024006 | 手机:133****2468',
  505. thirdDisplay: [
  506. [
  507. { field: { desc: '性别' }, displayValue: '女' },
  508. { field: { desc: '年龄' }, displayValue: '19' },
  509. { field: { desc: '状态' }, displayValue: '在读' }
  510. ],
  511. [
  512. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  513. { field: { desc: '辅导员' }, displayValue: '吴老师' }
  514. ]
  515. ]
  516. },
  517. {
  518. firstDisplay: '周九 - 2023级物联网工程1班',
  519. secondDisplay: '学号:2023007 | 手机:188****1357',
  520. thirdDisplay: [
  521. [
  522. { field: { desc: '性别' }, displayValue: '男' },
  523. { field: { desc: '年龄' }, displayValue: '22' },
  524. { field: { desc: '状态' }, displayValue: '在读' }
  525. ],
  526. [
  527. { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
  528. { field: { desc: '辅导员' }, displayValue: '郑老师' }
  529. ]
  530. ]
  531. },
  532. {
  533. firstDisplay: '吴十 - 2024级云计算1班',
  534. secondDisplay: '学号:2024008 | 手机:189****2468',
  535. thirdDisplay: [
  536. [
  537. { field: { desc: '性别' }, displayValue: '女' },
  538. { field: { desc: '年龄' }, displayValue: '20' },
  539. { field: { desc: '状态' }, displayValue: '在读' }
  540. ],
  541. [
  542. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  543. { field: { desc: '辅导员' }, displayValue: '冯老师' }
  544. ]
  545. ]
  546. },
  547. {
  548. firstDisplay: '郑十一 - 2023级区块链工程1班',
  549. secondDisplay: '学号:2023009 | 手机:180****3691',
  550. thirdDisplay: [
  551. [
  552. { field: { desc: '性别' }, displayValue: '男' },
  553. { field: { desc: '年龄' }, displayValue: '21' },
  554. { field: { desc: '状态' }, displayValue: '在读' }
  555. ],
  556. [
  557. { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
  558. { field: { desc: '辅导员' }, displayValue: '陈老师' }
  559. ]
  560. ]
  561. },
  562. {
  563. firstDisplay: '王十二 - 2024级电子商务1班',
  564. secondDisplay: '学号:2024010 | 手机:181****4802',
  565. thirdDisplay: [
  566. [
  567. { field: { desc: '性别' }, displayValue: '女' },
  568. { field: { desc: '年龄' }, displayValue: '19' },
  569. { field: { desc: '状态' }, displayValue: '在读' }
  570. ],
  571. [
  572. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  573. { field: { desc: '辅导员' }, displayValue: '褚老师' }
  574. ]
  575. ]
  576. },
  577. {
  578. firstDisplay: '陈十三 - 2023级金融科技1班',
  579. secondDisplay: '学号:2023011 | 手机:182****5913',
  580. thirdDisplay: [
  581. [
  582. { field: { desc: '性别' }, displayValue: '男' },
  583. { field: { desc: '年龄' }, displayValue: '22' },
  584. { field: { desc: '状态' }, displayValue: '毕业' }
  585. ],
  586. [
  587. { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
  588. { field: { desc: '辅导员' }, displayValue: '卫老师' }
  589. ]
  590. ]
  591. },
  592. {
  593. firstDisplay: '刘十四 - 2024级数字媒体技术1班',
  594. secondDisplay: '学号:2024012 | 手机:183****6024',
  595. thirdDisplay: [
  596. [
  597. { field: { desc: '性别' }, displayValue: '女' },
  598. { field: { desc: '年龄' }, displayValue: '20' },
  599. { field: { desc: '状态' }, displayValue: '在读' }
  600. ],
  601. [
  602. { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
  603. { field: { desc: '辅导员' }, displayValue: '蒋老师' }
  604. ]
  605. ]
  606. }
  607. ];
  608. // 设置数据
  609. this.list = mockData;
  610. this.originalList = [...mockData];
  611. this.loading = false;
  612. this.hasMore = false;
  613. console.log('✅ Mock数据加载完成,共', mockData.length, '条');
  614. },
  615. // 初始化页面
  616. async initPage() {
  617. try {
  618. console.log('🔄 初始化列表页面...');
  619. // 获取URL参数
  620. this.pageParams = this.getUrlParams();
  621. this.service = this.pageParams.service || 'default';
  622. console.log('📋 页面参数:', this.pageParams);
  623. // 功能说明:先调 init,再按 init 返回的 home/list 服务继续拉取数据 by xu 2026-02-28
  624. await this.loadInitAndHomeData();
  625. } catch (error) {
  626. console.log('❌ 页面初始化失败:', error);
  627. // this.showToast('页面初始化失败', 'error');
  628. } finally {
  629. this.loading = false;
  630. }
  631. },
  632. // 功能说明:调用 init 接口并解析 ssSearchPobjHomeServName/ssSearchPobjListServName by xu 2026-02-28
  633. async loadInitAndHomeData() {
  634. const initService = (this.service || '').trim();
  635. if (!initService) {
  636. await this.loadData(1, false);
  637. return;
  638. }
  639. const initParams = {
  640. pageNo: 1,
  641. rowNumPer: this.pageSize,
  642. management: '1',
  643. isReady: '1'
  644. };
  645. const initResult = await request.post(
  646. `/service?ssServ=${initService}&management=1&isReady=1`,
  647. initParams,
  648. {
  649. loading: false,
  650. formData: true
  651. }
  652. );
  653. const initPayload = this.unwrapResponseData(initResult?.data);
  654. this.ssSearchPobjHomeServName = String(initPayload?.ssSearchPobjHomeServName || '').trim();
  655. this.ssSearchPobjListServName = String(initPayload?.ssSearchPobjListServName || '').trim();
  656. console.log('✅ init返回服务名:', {
  657. initService,
  658. ssSearchPobjHomeServName: this.ssSearchPobjHomeServName,
  659. ssSearchPobjListServName: this.ssSearchPobjListServName
  660. });
  661. const homeService = this.ssSearchPobjHomeServName || initService;
  662. await this.loadDataByService(homeService, 1, false);
  663. },
  664. // 功能说明:统一处理 /service 返回结构(兼容 {ssData} 与平铺结构) by xu 2026-02-28
  665. unwrapResponseData(data) {
  666. if (!data || typeof data !== 'object') {
  667. return {};
  668. }
  669. if (data.ssData && typeof data.ssData === 'object') {
  670. return data.ssData;
  671. }
  672. return data;
  673. },
  674. // 功能说明:封装按指定 ssServ 拉取列表数据(home/list 共用) by xu 2026-02-28
  675. async loadDataByService(ssServ, pageNo = 1, isLoadMore = false) {
  676. const serviceName = String(ssServ || '').trim();
  677. if (!serviceName) {
  678. console.warn('⚠️ 缺少服务名,跳过请求');
  679. return;
  680. }
  681. // 防止重复加载:只有在非首次加载且正在加载时才阻止
  682. if (this.loading && !isLoadMore && this.list.length > 0) return;
  683. if (this.isLoadingMore && isLoadMore) return;
  684. try {
  685. console.log(`🔄 加载列表数据... 服务: ${serviceName}, 页码: ${pageNo}, 加载更多: ${isLoadMore}`);
  686. if (isLoadMore) {
  687. this.isLoadingMore = true;
  688. } else {
  689. this.loading = true;
  690. }
  691. const requestParams = {
  692. pageNo: pageNo,
  693. rowNumPer: this.pageSize,
  694. management: '1',
  695. isReady: '1',
  696. // 功能说明:请求参数只保留有效筛选项(避免空值/旧值污染查询) by xu 2026-02-28
  697. ...this.getActiveFilterParams(this.selectedFilters)
  698. };
  699. const result = await request.post(
  700. `/service?ssServ=${serviceName}&management=1&isReady=1`,
  701. requestParams,
  702. {
  703. loading: false,
  704. formData: true
  705. }
  706. );
  707. console.log('✅ API响应数据:', result);
  708. if (result && result.data) {
  709. await this.processApiData(result.data, isLoadMore);
  710. } else {
  711. console.warn('⚠️ API返回数据格式异常:', result);
  712. }
  713. } catch (error) {
  714. console.error('❌ 数据加载失败:', error);
  715. } finally {
  716. if (isLoadMore) {
  717. this.isLoadingMore = false;
  718. } else {
  719. this.loading = false;
  720. }
  721. }
  722. },
  723. // 加载数据
  724. async loadData(pageNo = 1, isLoadMore = false) {
  725. // 功能说明:翻页/筛选/搜索统一走 list 服务;未返回时回退 home/init by xu 2026-02-28
  726. const listService = this.ssSearchPobjListServName || this.ssSearchPobjHomeServName || this.service;
  727. await this.loadDataByService(listService, pageNo, isLoadMore);
  728. },
  729. // 处理API返回的数据
  730. async processApiData(data, isLoadMore = false) {
  731. try {
  732. const payload = this.unwrapResponseData(data);
  733. const objectList = Array.isArray(payload.objectList)
  734. ? payload.objectList
  735. : Array.isArray(payload.objList)
  736. ? payload.objList
  737. : [];
  738. const draftList = Array.isArray(payload.draftList) ? payload.draftList : [];
  739. const combinedObjectList = draftList.concat(objectList);
  740. console.log('🔄 处理API数据...', {
  741. isLoadMore,
  742. objectListLength: combinedObjectList.length,
  743. fromObjList: Array.isArray(payload.objList)
  744. });
  745. // 保存API返回的配置信息
  746. if (!isLoadMore) {
  747. // 功能说明:列表接口通常不返回按钮/搜索字段,缺省时保留首屏(home)已加载配置,避免筛选后按钮消失 by xu 2026-02-28
  748. if (Array.isArray(payload.buttonList) || Array.isArray(payload.rootFuncList)) {
  749. this.buttonList = payload.buttonList || payload.rootFuncList || [];
  750. }
  751. if (Array.isArray(payload.fieldsList) || Array.isArray(payload.searchFieldList)) {
  752. this.fieldsList = payload.fieldsList || payload.searchFieldList || [];
  753. }
  754. }
  755. // ssPaging信息每次都要更新,因为包含当前页信息
  756. this.ssPaging = payload.ssPaging || this.ssPaging || null;
  757. // 功能说明:list接口未返回 hasKeyword 时沿用首屏值,避免长按搜索入口被错误隐藏 by xu 2026-02-28
  758. if (Object.prototype.hasOwnProperty.call(payload, 'hasKeyWord') || Object.prototype.hasOwnProperty.call(payload, 'hasKeyword')) {
  759. this.hasKeyWord = payload.hasKeyWord || payload.hasKeyword || false;
  760. }
  761. // 处理objectList数据
  762. if (combinedObjectList.length > 0) {
  763. // 使用field-formatter.js格式化列表数据
  764. const formattedList = await window.formatObjectList(combinedObjectList, this.dictCache);
  765. if (isLoadMore) {
  766. // 加载更多:追加到现有列表
  767. this.list = [...this.list, ...formattedList];
  768. this.originalList = [...this.originalList, ...formattedList];
  769. } else {
  770. // 首次加载或刷新:替换列表
  771. this.originalList = formattedList;
  772. this.list = [...this.originalList];
  773. }
  774. console.log('✅ 列表数据处理完成:', this.list.length, '条');
  775. // 更新分页状态
  776. this.currentPage = isLoadMore ? this.currentPage + 1 : 1;
  777. // 根据ssPaging信息判断是否还有更多数据
  778. if (this.ssPaging && this.ssPaging.rowNum !== undefined) {
  779. const totalRecords = this.ssPaging.rowNum;
  780. const currentRecords = this.list.length;
  781. this.hasMore = currentRecords < totalRecords;
  782. console.log('📊 分页信息:', {
  783. totalRecords,
  784. currentRecords,
  785. hasMore: this.hasMore,
  786. currentPage: this.currentPage
  787. });
  788. } else {
  789. // 降级处理:根据当前页数据量判断
  790. this.hasMore = formattedList.length >= this.pageSize;
  791. console.log('⚠️ 使用降级分页判断:', {
  792. returnedCount: formattedList.length,
  793. pageSize: this.pageSize,
  794. hasMore: this.hasMore
  795. });
  796. }
  797. } else {
  798. // 没有更多数据
  799. this.hasMore = false;
  800. console.log('❌ 没有返回数据,设置hasMore为false');
  801. }
  802. // 根据fieldsList生成筛选选项(只在首次加载时生成)
  803. if (!isLoadMore) {
  804. await this.generateFilterOptions();
  805. }
  806. } catch (error) {
  807. console.error('❌ 数据处理失败:', error);
  808. throw error;
  809. }
  810. },
  811. // 获取URL参数
  812. getUrlParams() {
  813. const params = {};
  814. const urlSearchParams = new URLSearchParams(window.location.search);
  815. for (const [key, value] of urlSearchParams) {
  816. params[key] = decodeURIComponent(value);
  817. }
  818. return params;
  819. },
  820. // 处理buttonList按钮点击
  821. handleButtonClick(button) {
  822. console.log('🔘 按钮点击:', button);
  823. // 直接跳转到目标页面
  824. const destPage = button.function?.dest || button.dest;
  825. NavigationManager.goToFromButton(button);
  826. },
  827. // 跳转到目标页面
  828. navigateToPage(button, destPage) {
  829. const urlParams = new URLSearchParams(window.location.search);
  830. // 添加按钮相关参数
  831. if (button.function) {
  832. urlParams.set('dest', destPage);
  833. urlParams.set('title', encodeURIComponent(button.function.desc || button.buttonName));
  834. urlParams.set('service', button.function.servName || button.service || '');
  835. } else {
  836. urlParams.set('dest', destPage);
  837. urlParams.set('title', encodeURIComponent(button.buttonName));
  838. urlParams.set('service', button.service || '');
  839. }
  840. const newUrl = `${destPage}.html?${urlParams.toString()}`;
  841. console.log('� 跳转到:', newUrl);
  842. window.location.href = newUrl;
  843. },
  844. // 根据fieldsList生成筛选选项
  845. async generateFilterOptions() {
  846. if (!this.fieldsList || this.fieldsList.length === 0) {
  847. return;
  848. }
  849. for (const field of this.fieldsList) {
  850. // 如果字段有cbName,生成下拉选项
  851. if (field.cbName) {
  852. try {
  853. const options = await window.getDictOptions(field.cbName, this.dictCache);
  854. this.filterSelectOptions[field.name] = [
  855. { n: `全部${field.desc}`, v: '' },
  856. ...options
  857. ];
  858. } catch (error) {
  859. console.error('获取筛选选项失败:', field.cbName, error);
  860. }
  861. }
  862. }
  863. console.log('🔽 生成筛选选项:', this.filterSelectOptions);
  864. },
  865. // 卡片点击 - SsCard组件会自动传递item数据
  866. handleCardClick(item) {
  867. console.log('📄 卡片点击事件触发',item);
  868. },
  869. // 卡片操作 - SsCard组件的按钮点击事件
  870. handleCardAction({ button, item, index }) {
  871. console.log('⚡ 卡片操作:', button, item);
  872. if (button && button.onclick) {
  873. // 执行按钮的onclick回调
  874. button.onclick();
  875. }
  876. this.showToast(`执行操作: ${button.title}`, 'info');
  877. },
  878. // 加载更多数据
  879. async loadMore() {
  880. if (!this.hasMore || this.isLoadingMore) {
  881. console.log('🚫 无法加载更多:', { hasMore: this.hasMore, isLoadingMore: this.isLoadingMore });
  882. return;
  883. }
  884. console.log('📄 加载更多数据...');
  885. await this.loadData(this.currentPage + 1, true);
  886. },
  887. // 刷新数据
  888. async refreshData() {
  889. console.log('🔄 刷新数据...');
  890. // 重置分页状态
  891. this.currentPage = 1;
  892. this.hasMore = true;
  893. this.list = [];
  894. this.originalList = [];
  895. try {
  896. await this.loadData(1, false);
  897. this.showToast('刷新成功', 'success');
  898. } catch (error) {
  899. this.showToast('刷新失败', 'error');
  900. }
  901. },
  902. // 获取字段描述
  903. getFieldDesc(fieldName) {
  904. const field = this.fieldsList.find(f => f.name === fieldName);
  905. return field ? field.desc : fieldName;
  906. },
  907. // 功能说明:统一按钮文案映射(兼容 rootFuncList/buttonList 不同字段) by xu 2026-02-28
  908. getButtonText(button) {
  909. if (!button || typeof button !== 'object') return '';
  910. return button.desc || button.title || button.buttonName || button.name || '';
  911. },
  912. // 功能说明:提取有效筛选参数(过滤空串/null/undefined) by xu 2026-02-28
  913. getActiveFilterParams(source) {
  914. const params = {};
  915. const obj = (source && typeof source === 'object') ? source : {};
  916. Object.entries(obj).forEach(([key, value]) => {
  917. if (value === '' || value === null || value === undefined) return;
  918. params[key] = value;
  919. });
  920. return params;
  921. },
  922. // 筛选选择器变化(立即搜索)
  923. handleFilterChange(value) {
  924. console.log('🔽 筛选条件变化,立即搜索:', value);
  925. // 立即执行搜索
  926. this.applyDynamicFilters();
  927. },
  928. // 应用动态筛选(重新调用API)
  929. async applyDynamicFilters() {
  930. try {
  931. console.log('🔍 应用筛选条件:', this.selectedFilters);
  932. // 功能说明:筛选参数先归一化,清理已取消的筛选条件 by xu 2026-02-28
  933. const filterParams = this.getActiveFilterParams(this.selectedFilters);
  934. // 功能说明:筛选后重置分页并走 list 服务 by xu 2026-02-28
  935. this.currentPage = 1;
  936. this.hasMore = true;
  937. this.list = [];
  938. this.originalList = [];
  939. this.selectedFilters = { ...filterParams };
  940. await this.loadData(1, false);
  941. } catch (error) {
  942. console.error('❌ 筛选失败:', error);
  943. this.showToast('筛选失败', 'error');
  944. }
  945. },
  946. // 生成项目按钮
  947. generateItemButtons(service) {
  948. if (!service || !service.play) return [];
  949. return [{
  950. title: service.play.name || '查看',
  951. icon: 'icon-chakan',
  952. onclick: () => {
  953. console.log('点击查看按钮:', service.play);
  954. // 这里可以跳转到详情页面
  955. this.showToast(`查看: ${service.play.title}`, 'info');
  956. }
  957. }];
  958. },
  959. // 设置页面刷新监听
  960. setupRefreshListener() {
  961. // 监听子页面返回时的刷新通知
  962. this.refreshCleanup = NavigationManager.onRefreshNotify((refreshData) => {
  963. console.log('📢 收到刷新通知,重新加载数据');
  964. this.refreshData();
  965. });
  966. },
  967. // 设置滚动监听
  968. setupScrollListener() {
  969. let isThrottled = false;
  970. const handleScroll = () => {
  971. if (isThrottled) return;
  972. isThrottled = true;
  973. setTimeout(() => {
  974. isThrottled = false;
  975. }, 200); // 节流200ms
  976. // 检查是否滚动到底部
  977. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  978. const windowHeight = window.innerHeight;
  979. const documentHeight = document.documentElement.scrollHeight;
  980. // 控制回到顶部按钮显示:滚动超过300px时显示
  981. this.showBackToTop = scrollTop > 300;
  982. // 距离底部50px时触发加载更多
  983. if (scrollTop + windowHeight >= documentHeight - 50) {
  984. console.log('📄 滚动到底部,尝试加载更多...');
  985. this.loadMore();
  986. }
  987. };
  988. // 添加滚动监听
  989. window.addEventListener('scroll', handleScroll);
  990. // 保存清理函数
  991. this.scrollCleanup = () => {
  992. window.removeEventListener('scroll', handleScroll);
  993. };
  994. },
  995. // 返回顶部
  996. scrollToTop() {
  997. window.scrollTo({
  998. top: 0,
  999. behavior: 'smooth'
  1000. });
  1001. },
  1002. // 处理回到顶部按钮点击
  1003. handleBackToTopClick() {
  1004. // 如果不是长按触发的搜索,则执行返回顶部
  1005. if (!this.isLongPress) {
  1006. this.scrollToTop();
  1007. }
  1008. // 重置长按标志
  1009. this.isLongPress = false;
  1010. },
  1011. // 长按开始
  1012. handleLongPressStart(event) {
  1013. if(this.hasKeyWord){ // 有输入关键字才显示,否则不处理长按
  1014. this.isLongPress = false;
  1015. // 设置长按定时器(500ms)
  1016. this.longPressTimer = setTimeout(() => {
  1017. this.isLongPress = true;
  1018. this.openSearchModal();
  1019. // 震动反馈(如果支持)
  1020. if (navigator.vibrate) {
  1021. navigator.vibrate(50);
  1022. }
  1023. }, 500);
  1024. }
  1025. },
  1026. // 长按结束
  1027. handleLongPressEnd(event) {
  1028. // 清除长按定时器
  1029. if (this.longPressTimer) {
  1030. clearTimeout(this.longPressTimer);
  1031. this.longPressTimer = null;
  1032. }
  1033. },
  1034. // 长按取消(手指移出按钮区域)
  1035. handleLongPressCancel(event) {
  1036. // 清除长按定时器
  1037. if (this.longPressTimer) {
  1038. clearTimeout(this.longPressTimer);
  1039. this.longPressTimer = null;
  1040. }
  1041. this.isLongPress = false;
  1042. },
  1043. // 打开搜索弹窗
  1044. openSearchModal() {
  1045. this.showSearchModal = true;
  1046. this.searchKeyword = '';
  1047. // 延迟聚焦输入框,确保DOM已渲染和动画完成
  1048. this.$nextTick(() => {
  1049. // 使用 setTimeout 确保在移动端也能正常触发键盘
  1050. setTimeout(() => {
  1051. if (this.$refs.searchInput) {
  1052. // 先点击再聚焦,确保移动端键盘弹出
  1053. this.$refs.searchInput.click();
  1054. this.$refs.searchInput.focus();
  1055. console.log('✅ 输入框已聚焦');
  1056. }
  1057. }, 100); // 等待动画完成后再聚焦
  1058. });
  1059. },
  1060. // 关闭搜索弹窗
  1061. closeSearchModal() {
  1062. this.showSearchModal = false;
  1063. this.searchKeyword = '';
  1064. },
  1065. // 执行搜索
  1066. async performSearch() {
  1067. const keyword = this.searchKeyword.trim();
  1068. // 无关键词时,直接关闭弹窗,什么都不做
  1069. if (!keyword) {
  1070. // this.closeSearchModal();
  1071. return;
  1072. }
  1073. console.log('🔍 执行关键词搜索:', keyword);
  1074. // 关闭搜索弹窗
  1075. this.closeSearchModal();
  1076. // 设置 keyword 到筛选条件中
  1077. this.selectedFilters.ssKeyword = keyword;
  1078. // 调用 API 搜索
  1079. await this.applyDynamicFilters();
  1080. },
  1081. // 显示提示
  1082. showToast(message, type = 'info') {
  1083. console.log(`${type.toUpperCase()}: ${message}`);
  1084. // 使用浏览器原生alert,后续可以替换为更好的提示组件
  1085. alert(message);
  1086. }
  1087. }
  1088. })
  1089. })
  1090. </script>
  1091. </body>
  1092. </html>