mp_objList.html 49 KB

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