mp_objList.html 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319
  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. // 功能说明:把后端 chg/chgRootFuncList 映射成卡片左滑操作按钮,标题取 desc,供 ss-card 左滑动作区使用 by xu 2026-03-06
  766. const enhancedList = formattedList.map((item, index) => {
  767. const rawItem = combinedObjectList[index] || {};
  768. const rawActions = Array.isArray(rawItem.chgRootFuncList) && rawItem.chgRootFuncList.length > 0
  769. ? rawItem.chgRootFuncList
  770. : rawItem.chg
  771. ? [rawItem.chg]
  772. : [];
  773. return {
  774. ...item,
  775. ssObjId: item.ssObjId || rawItem.ssObjId || '',
  776. ssObjName: item.ssObjName || rawItem.ssObjName || '',
  777. swipeActions: rawActions.map(action => ({
  778. ...action,
  779. title: action.desc || action.title || '操作'
  780. }))
  781. };
  782. });
  783. if (isLoadMore) {
  784. // 加载更多:追加到现有列表
  785. this.list = [...this.list, ...enhancedList];
  786. this.originalList = [...this.originalList, ...enhancedList];
  787. } else {
  788. // 首次加载或刷新:替换列表
  789. this.originalList = enhancedList;
  790. this.list = [...this.originalList];
  791. }
  792. console.log('✅ 列表数据处理完成:', this.list.length, '条');
  793. // 更新分页状态
  794. this.currentPage = isLoadMore ? this.currentPage + 1 : 1;
  795. // 根据ssPaging信息判断是否还有更多数据
  796. if (this.ssPaging && this.ssPaging.rowNum !== undefined) {
  797. const totalRecords = this.ssPaging.rowNum;
  798. const currentRecords = this.list.length;
  799. this.hasMore = currentRecords < totalRecords;
  800. console.log('📊 分页信息:', {
  801. totalRecords,
  802. currentRecords,
  803. hasMore: this.hasMore,
  804. currentPage: this.currentPage
  805. });
  806. } else {
  807. // 降级处理:根据当前页数据量判断
  808. this.hasMore = formattedList.length >= this.pageSize;
  809. console.log('⚠️ 使用降级分页判断:', {
  810. returnedCount: formattedList.length,
  811. pageSize: this.pageSize,
  812. hasMore: this.hasMore
  813. });
  814. }
  815. } else {
  816. // 没有更多数据
  817. this.hasMore = false;
  818. console.log('❌ 没有返回数据,设置hasMore为false');
  819. }
  820. // 根据fieldsList生成筛选选项(只在首次加载时生成)
  821. if (!isLoadMore) {
  822. await this.generateFilterOptions();
  823. }
  824. } catch (error) {
  825. console.error('❌ 数据处理失败:', error);
  826. throw error;
  827. }
  828. },
  829. // 获取URL参数
  830. getUrlParams() {
  831. const params = {};
  832. const urlSearchParams = new URLSearchParams(window.location.search);
  833. for (const [key, value] of urlSearchParams) {
  834. params[key] = decodeURIComponent(value);
  835. }
  836. return params;
  837. },
  838. // 处理buttonList按钮点击
  839. handleButtonClick(button) {
  840. console.log('🔘 按钮点击:', button);
  841. // 直接跳转到目标页面
  842. const destPage = button.function?.dest || button.dest;
  843. NavigationManager.goToFromButton(button);
  844. },
  845. // 跳转到目标页面
  846. navigateToPage(button, destPage) {
  847. const urlParams = new URLSearchParams(window.location.search);
  848. // 添加按钮相关参数
  849. if (button.function) {
  850. urlParams.set('dest', destPage);
  851. urlParams.set('title', encodeURIComponent(button.function.desc || button.buttonName));
  852. urlParams.set('service', button.function.servName || button.service || '');
  853. } else {
  854. urlParams.set('dest', destPage);
  855. urlParams.set('title', encodeURIComponent(button.buttonName));
  856. urlParams.set('service', button.service || '');
  857. }
  858. const newUrl = `${destPage}.html?${urlParams.toString()}`;
  859. console.log('� 跳转到:', newUrl);
  860. window.location.href = newUrl;
  861. },
  862. // 根据fieldsList生成筛选选项
  863. async generateFilterOptions() {
  864. if (!this.fieldsList || this.fieldsList.length === 0) {
  865. return;
  866. }
  867. for (const field of this.fieldsList) {
  868. // 如果字段有cbName,生成下拉选项
  869. if (field.cbName) {
  870. try {
  871. const options = await window.getDictOptions(field.cbName, this.dictCache);
  872. this.filterSelectOptions[field.name] = [
  873. { n: `全部${field.desc}`, v: '' },
  874. ...options
  875. ];
  876. } catch (error) {
  877. console.error('获取筛选选项失败:', field.cbName, error);
  878. }
  879. }
  880. }
  881. console.log('🔽 生成筛选选项:', this.filterSelectOptions);
  882. },
  883. // 功能说明:统一处理卡片服务跳转,页面文件名按后端 dest 自动补 mp_,但传给业务页/后端的 dest 仍保持原始值 by xu 2026-03-06
  884. openServicePage(action, item = {}) {
  885. const target = action && typeof action === 'object' ? action : null;
  886. if (!target) {
  887. this.showToast('当前记录缺少操作配置', 'warning');
  888. return;
  889. }
  890. const serviceName = String(target.servName || target.ssServ || '').trim();
  891. const rawDest = String(target.dest || '').trim();
  892. const normalizedDest = rawDest && rawDest.startsWith('mp_') ? rawDest : (rawDest ? `mp_${rawDest}` : '');
  893. const paramName = String(target.param_name || '').trim();
  894. const paramValue = target.param_value;
  895. const ssToken = String(target.ssToken || '').trim();
  896. const paramText = typeof target.parm === 'string' ? target.parm : '';
  897. if (!serviceName) {
  898. this.showToast('缺少服务名', 'warning');
  899. return;
  900. }
  901. if (!normalizedDest) {
  902. this.showToast('缺少目标页面', 'warning');
  903. return;
  904. }
  905. NavigationManager.goTo(normalizedDest, {
  906. title: target.desc || target.title || '处理',
  907. service: serviceName,
  908. dest: rawDest,
  909. ssDest: rawDest,
  910. param: paramText,
  911. playParamName: paramName,
  912. playParamValue: paramValue,
  913. ssToken: ssToken,
  914. ssObjId: item.ssObjId || '',
  915. ssObjName: item.ssObjName || '',
  916. management: this.pageParams.management || '1',
  917. [paramName || 'param_value']: paramValue
  918. });
  919. },
  920. // 卡片点击 - SsCard组件会自动传递item数据
  921. handleCardClick(item) {
  922. console.log('📄 卡片点击事件触发',item);
  923. // 功能说明:卡片点击统一按 play 参数跳转到通用查看页 mp_objplay(兼容 play / service.play 两种结构) by xu 2026-03-04
  924. const play = (item && (item.play || (item.service && item.service.play))) || null;
  925. this.openServicePage(play, item);
  926. },
  927. // 卡片操作 - SsCard组件的按钮点击事件
  928. handleCardAction({ button, item, index }) {
  929. console.log('⚡ 卡片操作:', button, item);
  930. // 功能说明:左滑操作按钮按服务配置跳转(如 chg=变动),不再弹 Toast 占位 by xu 2026-03-06
  931. this.openServicePage(button, item);
  932. },
  933. // 加载更多数据
  934. async loadMore() {
  935. if (!this.hasMore || this.isLoadingMore) {
  936. console.log('🚫 无法加载更多:', { hasMore: this.hasMore, isLoadingMore: this.isLoadingMore });
  937. return;
  938. }
  939. console.log('📄 加载更多数据...');
  940. await this.loadData(this.currentPage + 1, true);
  941. },
  942. // 刷新数据
  943. async refreshData() {
  944. console.log('🔄 刷新数据...');
  945. // 重置分页状态
  946. this.currentPage = 1;
  947. this.hasMore = true;
  948. this.list = [];
  949. this.originalList = [];
  950. try {
  951. await this.loadData(1, false);
  952. this.showToast('刷新成功', 'success');
  953. } catch (error) {
  954. this.showToast('刷新失败', 'error');
  955. }
  956. },
  957. // 获取字段描述
  958. getFieldDesc(fieldName) {
  959. const field = this.fieldsList.find(f => f.name === fieldName);
  960. return field ? field.desc : fieldName;
  961. },
  962. // 功能说明:统一按钮文案映射(兼容 rootFuncList/buttonList 不同字段) by xu 2026-02-28
  963. getButtonText(button) {
  964. if (!button || typeof button !== 'object') return '';
  965. return button.desc || button.title || button.buttonName || button.name || '';
  966. },
  967. // 功能说明:提取有效筛选参数(过滤空串/null/undefined) by xu 2026-02-28
  968. getActiveFilterParams(source) {
  969. const params = {};
  970. const obj = (source && typeof source === 'object') ? source : {};
  971. Object.entries(obj).forEach(([key, value]) => {
  972. if (value === '' || value === null || value === undefined) return;
  973. params[key] = value;
  974. });
  975. return params;
  976. },
  977. // 筛选选择器变化(立即搜索)
  978. handleFilterChange(value) {
  979. console.log('🔽 筛选条件变化,立即搜索:', value);
  980. // 立即执行搜索
  981. this.applyDynamicFilters();
  982. },
  983. // 应用动态筛选(重新调用API)
  984. async applyDynamicFilters() {
  985. try {
  986. console.log('🔍 应用筛选条件:', this.selectedFilters);
  987. // 功能说明:筛选参数先归一化,清理已取消的筛选条件 by xu 2026-02-28
  988. const filterParams = this.getActiveFilterParams(this.selectedFilters);
  989. // 功能说明:筛选后重置分页并走 list 服务 by xu 2026-02-28
  990. this.currentPage = 1;
  991. this.hasMore = true;
  992. this.list = [];
  993. this.originalList = [];
  994. this.selectedFilters = { ...filterParams };
  995. await this.loadData(1, false);
  996. } catch (error) {
  997. console.error('❌ 筛选失败:', error);
  998. this.showToast('筛选失败', 'error');
  999. }
  1000. },
  1001. // 生成项目按钮
  1002. generateItemButtons(service) {
  1003. if (!service || !service.play) return [];
  1004. return [{
  1005. title: service.play.name || '查看',
  1006. icon: 'icon-chakan',
  1007. onclick: () => {
  1008. console.log('点击查看按钮:', service.play);
  1009. // 这里可以跳转到详情页面
  1010. this.showToast(`查看: ${service.play.title}`, 'info');
  1011. }
  1012. }];
  1013. },
  1014. // 设置页面刷新监听
  1015. setupRefreshListener() {
  1016. // 监听子页面返回时的刷新通知
  1017. this.refreshCleanup = NavigationManager.onRefreshNotify((refreshData) => {
  1018. console.log('📢 收到刷新通知,重新加载数据');
  1019. this.refreshData();
  1020. });
  1021. },
  1022. // 设置滚动监听
  1023. setupScrollListener() {
  1024. let isThrottled = false;
  1025. const handleScroll = () => {
  1026. if (isThrottled) return;
  1027. isThrottled = true;
  1028. setTimeout(() => {
  1029. isThrottled = false;
  1030. }, 200); // 节流200ms
  1031. // 检查是否滚动到底部
  1032. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  1033. const windowHeight = window.innerHeight;
  1034. const documentHeight = document.documentElement.scrollHeight;
  1035. // 控制回到顶部按钮显示:滚动超过300px时显示
  1036. this.showBackToTop = scrollTop > 300;
  1037. // 距离底部50px时触发加载更多
  1038. if (scrollTop + windowHeight >= documentHeight - 50) {
  1039. console.log('📄 滚动到底部,尝试加载更多...');
  1040. this.loadMore();
  1041. }
  1042. };
  1043. // 添加滚动监听
  1044. window.addEventListener('scroll', handleScroll);
  1045. // 保存清理函数
  1046. this.scrollCleanup = () => {
  1047. window.removeEventListener('scroll', handleScroll);
  1048. };
  1049. },
  1050. // 返回顶部
  1051. scrollToTop() {
  1052. window.scrollTo({
  1053. top: 0,
  1054. behavior: 'smooth'
  1055. });
  1056. },
  1057. // 处理回到顶部按钮点击
  1058. handleBackToTopClick() {
  1059. // 如果不是长按触发的搜索,则执行返回顶部
  1060. if (!this.isLongPress) {
  1061. this.scrollToTop();
  1062. }
  1063. // 重置长按标志
  1064. this.isLongPress = false;
  1065. },
  1066. // 长按开始
  1067. handleLongPressStart(event) {
  1068. if(this.hasKeyWord){ // 有输入关键字才显示,否则不处理长按
  1069. this.isLongPress = false;
  1070. // 设置长按定时器(500ms)
  1071. this.longPressTimer = setTimeout(() => {
  1072. this.isLongPress = true;
  1073. this.openSearchModal();
  1074. // 震动反馈(如果支持)
  1075. if (navigator.vibrate) {
  1076. navigator.vibrate(50);
  1077. }
  1078. }, 500);
  1079. }
  1080. },
  1081. // 长按结束
  1082. handleLongPressEnd(event) {
  1083. // 清除长按定时器
  1084. if (this.longPressTimer) {
  1085. clearTimeout(this.longPressTimer);
  1086. this.longPressTimer = null;
  1087. }
  1088. },
  1089. // 长按取消(手指移出按钮区域)
  1090. handleLongPressCancel(event) {
  1091. // 清除长按定时器
  1092. if (this.longPressTimer) {
  1093. clearTimeout(this.longPressTimer);
  1094. this.longPressTimer = null;
  1095. }
  1096. this.isLongPress = false;
  1097. },
  1098. // 打开搜索弹窗
  1099. openSearchModal() {
  1100. this.showSearchModal = true;
  1101. this.searchKeyword = '';
  1102. // 延迟聚焦输入框,确保DOM已渲染和动画完成
  1103. this.$nextTick(() => {
  1104. // 使用 setTimeout 确保在移动端也能正常触发键盘
  1105. setTimeout(() => {
  1106. if (this.$refs.searchInput) {
  1107. // 先点击再聚焦,确保移动端键盘弹出
  1108. this.$refs.searchInput.click();
  1109. this.$refs.searchInput.focus();
  1110. console.log('✅ 输入框已聚焦');
  1111. }
  1112. }, 100); // 等待动画完成后再聚焦
  1113. });
  1114. },
  1115. // 关闭搜索弹窗
  1116. closeSearchModal() {
  1117. this.showSearchModal = false;
  1118. this.searchKeyword = '';
  1119. },
  1120. // 执行搜索
  1121. async performSearch() {
  1122. const keyword = this.searchKeyword.trim();
  1123. // 无关键词时,直接关闭弹窗,什么都不做
  1124. if (!keyword) {
  1125. // this.closeSearchModal();
  1126. return;
  1127. }
  1128. console.log('🔍 执行关键词搜索:', keyword);
  1129. // 关闭搜索弹窗
  1130. this.closeSearchModal();
  1131. // 设置 keyword 到筛选条件中
  1132. this.selectedFilters.ssKeyword = keyword;
  1133. // 调用 API 搜索
  1134. await this.applyDynamicFilters();
  1135. },
  1136. // 显示提示
  1137. showToast(message, type = 'info') {
  1138. console.log(`${type.toUpperCase()}: ${message}`);
  1139. // 使用浏览器原生alert,后续可以替换为更好的提示组件
  1140. alert(message);
  1141. }
  1142. }
  1143. })
  1144. })
  1145. </script>
  1146. </body>
  1147. </html>