mp_cobjList.html 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  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. <script src="/js/mp_base/base.js"></script>
  8. <style>
  9. #app {
  10. background: #f5f5f5;
  11. min-height: 100vh;
  12. }
  13. [v-cloak] {
  14. display: none !important;
  15. }
  16. .search-filter-container {
  17. background: #f5f5f5;
  18. padding: 15px;
  19. position: sticky;
  20. top: 0;
  21. z-index: 100;
  22. display: flex;
  23. flex-wrap: wrap;
  24. gap: 10px;
  25. align-items: center;
  26. /* 搜索条件右对齐,和 mp_objList 保持一致 by xu 2026-03-06 */
  27. justify-content: flex-end;
  28. }
  29. .search-filter-container .ss-select-container,
  30. .search-filter-container .input,
  31. .search-filter-container .ss-search-date-picker {
  32. border-radius: 6px;
  33. overflow: hidden;
  34. }
  35. .loading-container,
  36. .error-container {
  37. display: flex;
  38. flex-direction: column;
  39. align-items: center;
  40. justify-content: center;
  41. min-height: 220px;
  42. color: #666;
  43. padding: 24px;
  44. text-align: center;
  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. .error-text {
  60. color: #d03050;
  61. line-height: 1.7;
  62. }
  63. .list-container {
  64. padding: 0 15px 8px;
  65. }
  66. .empty-state {
  67. text-align: center;
  68. padding: 60px 20px;
  69. color: #999;
  70. }
  71. .empty-icon {
  72. font-size: 48px;
  73. margin-bottom: 15px;
  74. }
  75. .empty-text {
  76. font-size: 16px;
  77. }
  78. .card-content .card-header {
  79. display: flex;
  80. align-items: center;
  81. gap: 8px;
  82. margin-bottom: 10px;
  83. }
  84. .card-content .card-title {
  85. font-size: 16px;
  86. font-weight: bold;
  87. color: #333;
  88. line-height: 1.5;
  89. word-break: break-all;
  90. }
  91. .draft-tag {
  92. flex: 0 0 auto;
  93. padding: 2px 8px;
  94. border-radius: 999px;
  95. background: rgba(64, 172, 109, 0.12);
  96. color: #2a8f58;
  97. font-size: 12px;
  98. }
  99. .card-content .card-description {
  100. font-size: 14px;
  101. color: #666;
  102. margin-bottom: 8px;
  103. line-height: 1.6;
  104. word-break: break-all;
  105. }
  106. .card-content .attribute-group {
  107. display: flex;
  108. flex-wrap: wrap;
  109. column-gap: 10px;
  110. }
  111. .card-content .attribute-item {
  112. display: flex;
  113. margin-bottom: 5px;
  114. max-width: 100%;
  115. }
  116. .card-content .attr-label {
  117. font-size: 13px;
  118. color: #999;
  119. flex: 0 0 auto;
  120. }
  121. .card-content .attr-value {
  122. font-size: 13px;
  123. color: #333;
  124. word-break: break-all;
  125. }
  126. .load-more-container {
  127. text-align: center;
  128. padding: 20px;
  129. color: #999;
  130. font-size: 14px;
  131. }
  132. .load-more-loading {
  133. color: #007aff;
  134. }
  135. .load-more-end {
  136. color: #999;
  137. }
  138. .load-more-tip {
  139. color: #ccc;
  140. }
  141. .back-to-top-btn {
  142. width: 50px;
  143. height: 50px;
  144. border-radius: 50%;
  145. background: rgba(87, 93, 109, 0.5);
  146. display: flex;
  147. justify-content: center;
  148. align-items: center;
  149. position: fixed;
  150. bottom: 200px;
  151. right: 15px;
  152. cursor: pointer;
  153. transition: all 0.3s ease;
  154. z-index: 999;
  155. }
  156. .back-to-top-btn:active {
  157. transform: scale(0.9);
  158. }
  159. .back-to-top-inner {
  160. width: 42px;
  161. height: 42px;
  162. border-radius: 50%;
  163. background: rgba(87, 93, 109, 0.5);
  164. display: flex;
  165. justify-content: center;
  166. align-items: center;
  167. }
  168. .search-modal-mask {
  169. position: fixed;
  170. top: 0;
  171. left: 0;
  172. right: 0;
  173. bottom: 0;
  174. background: rgba(0, 0, 0, 0.5);
  175. z-index: 1000;
  176. display: flex;
  177. align-items: flex-end;
  178. }
  179. .search-modal-content {
  180. width: 100%;
  181. background: white;
  182. animation: slideUp 0.3s ease-out;
  183. position: relative;
  184. }
  185. @keyframes slideUp {
  186. from { transform: translateY(100%); }
  187. to { transform: translateY(0); }
  188. }
  189. .search-input-container {
  190. display: flex;
  191. align-items: center;
  192. height: 50px;
  193. background: white;
  194. border-top: 1px solid #e5e5e5;
  195. }
  196. .search-input {
  197. flex: 1;
  198. height: 100%;
  199. border: none;
  200. outline: none;
  201. padding: 0 15px;
  202. font-size: 16px;
  203. background: transparent;
  204. }
  205. .search-divider {
  206. width: 1px;
  207. height: 24px;
  208. background: #e5e5e5;
  209. }
  210. .search-icon-btn {
  211. width: 56px;
  212. height: 100%;
  213. border: none;
  214. background: white;
  215. display: flex;
  216. align-items: center;
  217. justify-content: center;
  218. }
  219. .search-icon-btn:active {
  220. background: #f5f5f5;
  221. }
  222. </style>
  223. </head>
  224. <body>
  225. <div id="app" v-cloak>
  226. <!-- 二级列表筛选条:兼容 PC 的搜索字段、范围按钮和根功能按钮 by xu 2026-03-06 -->
  227. <div class="search-filter-container">
  228. <template v-for="field in searchFieldList" :key="field.name">
  229. <ss-select
  230. v-if="field.cbName"
  231. v-model="selectedFilters[field.name]"
  232. :placeholder="field.desc || '请选择'"
  233. :options="filterSelectOptions[field.name] || []"
  234. @change="handleFilterChange"
  235. ></ss-select>
  236. <ss-search-date-picker
  237. v-else-if="isDateField(field)"
  238. v-model="selectedFilters[field.name]"
  239. :name="field.name"
  240. :placeholder="field.desc || ''"
  241. width="140px"
  242. @change="applyFilters"
  243. ></ss-search-date-picker>
  244. <ss-search-input
  245. v-else
  246. v-model="selectedFilters[field.name]"
  247. :name="field.name"
  248. :placeholder="field.desc || ''"
  249. width="120px"
  250. @search="applyFilters"
  251. ></ss-search-input>
  252. </template>
  253. <ss-search-button
  254. :text="getCurrentScopeText()"
  255. :options="scopeButtonOptions"
  256. ></ss-search-button>
  257. <ss-search-button
  258. v-if="hasKeyWord"
  259. text="关键词"
  260. @click="openSearchModal"
  261. ></ss-search-button>
  262. <ss-search-button
  263. v-for="(button, index) in rootFuncList"
  264. :key="`${getButtonText(button)}-${index}`"
  265. :text="getButtonText(button)"
  266. @click="handleRootButtonClick(button)"
  267. ></ss-search-button>
  268. </div>
  269. <div v-if="loading" class="loading-container">
  270. <div class="loading-spinner"></div>
  271. <div class="loading-text">加载中...</div>
  272. </div>
  273. <div v-else-if="errorMessage" class="error-container">
  274. <div class="error-text">{{ errorMessage }}</div>
  275. </div>
  276. <div v-else class="list-container">
  277. <div v-if="list.length === 0" class="empty-state">
  278. <div class="empty-icon">📋</div>
  279. <div class="empty-text">暂无数据</div>
  280. </div>
  281. <div v-else>
  282. <!-- 二级列表卡片:兼容 draftList/objList 与 catList/title 结构 by xu 2026-03-06 -->
  283. <ss-card
  284. v-for="(item, index) in list"
  285. :key="`${index}-${item.ssObjId || item.id || item.firstDisplay || ''}`"
  286. :item="item"
  287. @click="handleCardClick(item)"
  288. @button-click="handleCardAction"
  289. >
  290. <div class="card-content">
  291. <div class="card-header" v-if="item.firstDisplay || item.isDraft">
  292. <span v-if="item.isDraft" class="draft-tag">草稿</span>
  293. <div class="card-title">{{ item.firstDisplay || '未命名记录' }}</div>
  294. </div>
  295. <div class="card-description" v-if="item.secondDisplay">
  296. {{ item.secondDisplay }}
  297. </div>
  298. <div class="card-attributes" v-if="item.thirdDisplay && item.thirdDisplay.length > 0">
  299. <div
  300. v-for="(group, groupIndex) in item.thirdDisplay"
  301. :key="groupIndex"
  302. class="attribute-group"
  303. >
  304. <div
  305. v-for="(attr, attrIndex) in group"
  306. :key="attrIndex"
  307. class="attribute-item"
  308. >
  309. <span class="attr-label">{{ getAttrLabel(attr) }}:</span>
  310. <span class="attr-value">{{ attr.displayValue }}</span>
  311. </div>
  312. </div>
  313. </div>
  314. </div>
  315. </ss-card>
  316. </div>
  317. </div>
  318. <div class="load-more-container" v-if="list.length > 0">
  319. <div v-if="isLoadingMore" class="load-more-loading">
  320. <span>正在加载更多...</span>
  321. </div>
  322. <div v-else-if="!hasMore" class="load-more-end">
  323. <span>没有更多数据了</span>
  324. </div>
  325. <div v-else class="load-more-tip">
  326. <span>滚动到底部加载更多</span>
  327. </div>
  328. </div>
  329. <div
  330. v-if="showBackToTop"
  331. class="back-to-top-btn"
  332. @touchstart="handleLongPressStart"
  333. @touchend="handleLongPressEnd"
  334. @touchcancel="handleLongPressCancel"
  335. @click.prevent="handleBackToTopClick"
  336. >
  337. <div class="back-to-top-inner">
  338. <Icon name="icon-huidaodingbu" size="40" color="#fff"></Icon>
  339. </div>
  340. </div>
  341. <div v-if="showSearchModal" class="search-modal-mask" @click="closeSearchModal">
  342. <div class="search-modal-content" @click.stop>
  343. <div class="search-input-container">
  344. <input
  345. ref="searchInput"
  346. v-model="searchKeyword"
  347. type="search"
  348. inputmode="search"
  349. class="search-input"
  350. placeholder="请输入关键词"
  351. @keyup.enter="performSearch"
  352. autocomplete="off"
  353. />
  354. <div class="search-divider"></div>
  355. <button class="search-icon-btn" @click="performSearch">
  356. <Icon name="icon-chazhao" size="24" color="#575d6d"></Icon>
  357. </button>
  358. </div>
  359. </div>
  360. </div>
  361. </div>
  362. <script>
  363. window.SS.ready(function () {
  364. window.SS.dom.initializeFormApp({
  365. el: '#app',
  366. data() {
  367. return {
  368. loading: true,
  369. errorMessage: '',
  370. list: [],
  371. currentPage: 1,
  372. pageSize: 12,
  373. hasMore: true,
  374. isLoadingMore: false,
  375. pageParams: {},
  376. service: '',
  377. ssSearchCobjServName: '',
  378. requestParentViewObject: '',
  379. ssObjName: '',
  380. ssObjId: '',
  381. ssPobjIdName: '',
  382. management: '1',
  383. searchFieldList: [],
  384. rootFuncList: [],
  385. selectedFilters: {},
  386. filterSelectOptions: {},
  387. dictCache: new Map(),
  388. ssPaging: null,
  389. hasKeyWord: false,
  390. scopeButtonOptions: [],
  391. showBackToTop: false,
  392. showSearchModal: false,
  393. searchKeyword: '',
  394. longPressTimer: null,
  395. isLongPress: false,
  396. requestSeq: 0,
  397. };
  398. },
  399. mounted() {
  400. this.setupScopeButtons();
  401. this.initPage();
  402. this.setupRefreshListener();
  403. this.setupScrollListener();
  404. },
  405. beforeUnmount() {
  406. if (this.refreshCleanup) {
  407. this.refreshCleanup();
  408. }
  409. if (this.scrollCleanup) {
  410. this.scrollCleanup();
  411. }
  412. },
  413. methods: {
  414. // 二级列表初始化:同时兼容直接传 cobj 服务和先走 init 再拿 ssSearchCobjServName 的两种入口 by xu 2026-03-06
  415. async initPage() {
  416. try {
  417. this.pageParams = this.getUrlParams();
  418. this.service = String(this.pageParams.service || '').trim();
  419. this.ssSearchCobjServName = String(this.pageParams.ssSearchCobjServName || '').trim();
  420. this.requestParentViewObject = String(this.pageParams.requestParentViewObject || '').trim();
  421. this.ssObjName = String(this.pageParams.ssObjName || this.pageParams.ssobjname || '').trim();
  422. this.ssObjId = String(this.pageParams.ssObjId || this.pageParams.ssobjid || '').trim();
  423. this.ssPobjIdName = String(this.pageParams.ssPobjIdName || '').trim();
  424. this.management = String(this.pageParams.management || '1');
  425. this.searchKeyword = String(this.pageParams.ssKeyword || '').trim();
  426. this.setupScopeButtons();
  427. if (this.searchKeyword) {
  428. this.selectedFilters = {
  429. ...this.selectedFilters,
  430. ssKeyword: this.searchKeyword,
  431. };
  432. }
  433. if (this.ssSearchCobjServName) {
  434. await this.loadData(1, false);
  435. return;
  436. }
  437. if (!this.service) {
  438. this.errorMessage = '缺少服务名,无法加载二级列表';
  439. return;
  440. }
  441. await this.resolveInitService();
  442. } catch (error) {
  443. console.error('❌ 二级列表初始化失败:', error);
  444. this.errorMessage = '页面初始化失败,请稍后重试';
  445. } finally {
  446. this.loading = false;
  447. }
  448. },
  449. async resolveInitService() {
  450. const reqId = ++this.requestSeq;
  451. this.loading = true;
  452. this.errorMessage = '';
  453. try {
  454. const requestParams = this.buildRequestParams(1);
  455. const result = await request.post(
  456. `/service?ssServ=${this.service}&management=${encodeURIComponent(this.management)}&isReady=1`,
  457. requestParams,
  458. {
  459. loading: false,
  460. formData: true,
  461. }
  462. );
  463. if (reqId !== this.requestSeq) return;
  464. const rawData = result && result.data ? result.data : {};
  465. if (this.isBizError(rawData)) {
  466. this.errorMessage = this.formatBizError(rawData);
  467. return;
  468. }
  469. const payload = this.unwrapResponseData(rawData);
  470. const resolvedCobjService = String(payload.ssSearchCobjServName || '').trim();
  471. if (resolvedCobjService) {
  472. this.ssSearchCobjServName = resolvedCobjService;
  473. if (!this.requestParentViewObject) {
  474. this.requestParentViewObject = String(payload.requestParentViewObject || '').trim();
  475. }
  476. if (!this.ssPobjIdName) {
  477. this.ssPobjIdName = String(payload.ssPobjIdName || '').trim();
  478. }
  479. if (!this.ssObjName) {
  480. this.ssObjName = String(payload.ssObjName || payload.objName || '').trim();
  481. }
  482. if (!this.ssObjId) {
  483. this.ssObjId = String(payload.ssObjId || payload.objId || '').trim();
  484. }
  485. await this.loadData(1, false);
  486. return;
  487. }
  488. await this.applyApiData(payload, false);
  489. } catch (error) {
  490. console.error('❌ 初始化服务请求失败:', error);
  491. this.errorMessage = '初始化失败,请稍后重试';
  492. }
  493. },
  494. async loadData(pageNo = 1, isLoadMore = false) {
  495. const serviceName = String(this.ssSearchCobjServName || this.service || '').trim();
  496. if (!serviceName) {
  497. this.errorMessage = '缺少二级列表服务名';
  498. return;
  499. }
  500. if (isLoadMore && this.isLoadingMore) return;
  501. if (!isLoadMore && this.loading && this.list.length > 0) return;
  502. const reqId = ++this.requestSeq;
  503. this.errorMessage = '';
  504. if (isLoadMore) {
  505. this.isLoadingMore = true;
  506. } else {
  507. this.loading = true;
  508. }
  509. try {
  510. const requestParams = this.buildRequestParams(pageNo);
  511. const result = await request.post(
  512. `/service?ssServ=${serviceName}&management=${encodeURIComponent(this.management)}&isReady=1`,
  513. requestParams,
  514. {
  515. loading: false,
  516. formData: true,
  517. }
  518. );
  519. if (reqId !== this.requestSeq) return;
  520. const rawData = result && result.data ? result.data : {};
  521. if (this.isBizError(rawData)) {
  522. this.errorMessage = this.formatBizError(rawData);
  523. return;
  524. }
  525. const payload = this.unwrapResponseData(rawData);
  526. await this.applyApiData(payload, isLoadMore);
  527. } catch (error) {
  528. console.error('❌ 二级列表加载失败:', error);
  529. this.errorMessage = '加载失败,请稍后重试';
  530. } finally {
  531. if (reqId === this.requestSeq) {
  532. this.loading = false;
  533. this.isLoadingMore = false;
  534. }
  535. }
  536. },
  537. async applyApiData(payload, isLoadMore = false) {
  538. const data = payload && typeof payload === 'object' ? payload : {};
  539. if (Array.isArray(data.searchFieldList)) {
  540. this.searchFieldList = data.searchFieldList;
  541. }
  542. if (Array.isArray(data.rootFuncList) || Array.isArray(data.buttonList)) {
  543. this.rootFuncList = data.rootFuncList || data.buttonList || [];
  544. }
  545. if (Object.prototype.hasOwnProperty.call(data, 'hasKeyWord') || Object.prototype.hasOwnProperty.call(data, 'hasKeyword')) {
  546. this.hasKeyWord = !!(data.hasKeyWord || data.hasKeyword);
  547. }
  548. if (data.ssPaging && typeof data.ssPaging === 'object') {
  549. this.ssPaging = {
  550. pageNo: Number(data.ssPaging.pageNo || 1),
  551. rowNumPer: Number(data.ssPaging.rowNumPer || this.pageSize || 12),
  552. rowNum: Number(data.ssPaging.rowNum || 0),
  553. };
  554. if (this.ssPaging.rowNumPer > 0) {
  555. this.pageSize = this.ssPaging.rowNumPer;
  556. }
  557. }
  558. this.ensureSelectedFilters();
  559. await this.generateFilterOptions();
  560. const draftList = Array.isArray(data.draftList)
  561. ? data.draftList.map(item => ({ ...item, __isDraft: true }))
  562. : [];
  563. const objList = Array.isArray(data.objList)
  564. ? data.objList.map(item => ({ ...item, __isDraft: false }))
  565. : Array.isArray(data.objectList)
  566. ? data.objectList.map(item => ({ ...item, __isDraft: false }))
  567. : [];
  568. const formattedList = await this.formatCobjItems(draftList.concat(objList));
  569. if (isLoadMore) {
  570. this.list = this.list.concat(formattedList);
  571. this.currentPage = Number(this.currentPage || 1) + 1;
  572. } else {
  573. this.list = formattedList;
  574. this.currentPage = Number(this.ssPaging && this.ssPaging.pageNo ? this.ssPaging.pageNo : 1);
  575. }
  576. if (this.ssPaging && this.ssPaging.rowNum) {
  577. this.hasMore = this.list.length < Number(this.ssPaging.rowNum || 0);
  578. } else {
  579. this.hasMore = formattedList.length >= this.pageSize;
  580. }
  581. this.errorMessage = '';
  582. },
  583. async formatCobjItems(rawList) {
  584. const list = Array.isArray(rawList) ? rawList : [];
  585. const formattedList = await window.formatObjectList(list, this.dictCache);
  586. return formattedList.map((item, index) => {
  587. const rawItem = list[index] || {};
  588. const nextItem = {
  589. ...item,
  590. isDraft: !!rawItem.__isDraft,
  591. buttons: [],
  592. };
  593. if (Array.isArray(rawItem.catList) && rawItem.catList.length) {
  594. nextItem.thirdDisplay = [
  595. rawItem.catList
  596. .map(catItem => {
  597. if (!catItem || typeof catItem !== 'object') return null;
  598. const rawValue = catItem.val;
  599. let displayValue = rawValue === undefined || rawValue === null ? '' : String(rawValue);
  600. if (catItem.fmt && window.H5FieldFormatter && typeof window.H5FieldFormatter.formatDate === 'function') {
  601. displayValue = window.H5FieldFormatter.formatDate(rawValue, catItem.fmt) || displayValue;
  602. }
  603. return {
  604. field: {
  605. desc: String(catItem.desc || ''),
  606. },
  607. value: rawValue,
  608. displayValue,
  609. };
  610. })
  611. .filter(Boolean),
  612. ].filter(group => group.length > 0);
  613. }
  614. if (rawItem.chg) {
  615. nextItem.buttons = [{
  616. title: '变动',
  617. onclick: () => this.handleServiceAction(rawItem.chg),
  618. }];
  619. }
  620. return nextItem;
  621. });
  622. },
  623. unwrapResponseData(data) {
  624. if (!data || typeof data !== 'object') return {};
  625. return data.ssData && typeof data.ssData === 'object' ? data.ssData : data;
  626. },
  627. isBizError(data) {
  628. return !!(data && typeof data === 'object' && Object.prototype.hasOwnProperty.call(data, 'ssCode') && Object.prototype.hasOwnProperty.call(data, 'ssMsg') && !data.ssData);
  629. },
  630. formatBizError(data) {
  631. return `错误(${data.ssCode}):${data.ssMsg == null ? '' : data.ssMsg}`;
  632. },
  633. getUrlParams() {
  634. return NavigationManager.getUrlParam();
  635. },
  636. buildRequestParams(pageNo = 1) {
  637. const params = {
  638. pageNo,
  639. rowNumPer: this.pageSize,
  640. management: this.management,
  641. isReady: '1',
  642. ...this.getActiveFilterParams(this.selectedFilters),
  643. };
  644. if (this.ssObjId) {
  645. const objIdKey = String(this.ssPobjIdName || '').trim() || (this.ssObjName ? `${this.ssObjName}id` : 'ssObjId');
  646. params[objIdKey] = this.ssObjId;
  647. }
  648. if (this.requestParentViewObject) {
  649. params.requestParentViewObject = this.requestParentViewObject;
  650. }
  651. return params;
  652. },
  653. getActiveFilterParams(source) {
  654. const params = {};
  655. const data = source && typeof source === 'object' ? source : {};
  656. Object.entries(data).forEach(([key, value]) => {
  657. if (value === '' || value === null || value === undefined) return;
  658. params[key] = value;
  659. });
  660. return params;
  661. },
  662. ensureSelectedFilters() {
  663. const nextFilters = { ...this.selectedFilters };
  664. (this.searchFieldList || []).forEach(field => {
  665. if (!field || !field.name) return;
  666. if (nextFilters[field.name] !== undefined) return;
  667. const pageValue = this.pageParams[field.name];
  668. nextFilters[field.name] = pageValue !== undefined ? String(pageValue) : '';
  669. });
  670. if (this.hasKeyWord) {
  671. if (nextFilters.ssKeyword === undefined) {
  672. nextFilters.ssKeyword = this.searchKeyword || '';
  673. }
  674. this.searchKeyword = String(nextFilters.ssKeyword || '');
  675. }
  676. this.selectedFilters = nextFilters;
  677. },
  678. async generateFilterOptions() {
  679. for (const field of this.searchFieldList || []) {
  680. if (!field || !field.name || !field.cbName) continue;
  681. if (Array.isArray(this.filterSelectOptions[field.name]) && this.filterSelectOptions[field.name].length > 0) continue;
  682. try {
  683. const options = await window.getDictOptions(field.cbName, this.dictCache);
  684. this.filterSelectOptions = {
  685. ...this.filterSelectOptions,
  686. [field.name]: [{ n: `全部${field.desc || ''}`, v: '' }].concat(options || []),
  687. };
  688. } catch (error) {
  689. console.error('❌ 获取筛选选项失败:', field.cbName, error);
  690. }
  691. }
  692. },
  693. isDateField(field) {
  694. const type = Number(field && field.type);
  695. return type === 3 || type === 11;
  696. },
  697. getFieldDesc(fieldName) {
  698. const field = (this.searchFieldList || []).find(item => item && item.name === fieldName);
  699. return field ? field.desc : fieldName;
  700. },
  701. getAttrLabel(attr) {
  702. if (!attr || typeof attr !== 'object') return '';
  703. return attr.field && attr.field.desc ? attr.field.desc : '';
  704. },
  705. getButtonText(button) {
  706. if (!button || typeof button !== 'object') return '';
  707. return button.desc || button.title || button.buttonName || button.name || button.function?.desc || '';
  708. },
  709. setupScopeButtons() {
  710. const scopeList = [
  711. { id: '99', text: '所有' },
  712. { id: '2', text: '管理' },
  713. { id: '1', text: '创建' },
  714. { id: '3', text: '已办' },
  715. { id: '55', text: '停用' },
  716. ];
  717. this.scopeButtonOptions = scopeList.map(item => ({
  718. text: item.text,
  719. onclick: () => this.switchScope(item.id),
  720. }));
  721. },
  722. getCurrentScopeText() {
  723. const scopeMap = {
  724. '99': '所有',
  725. '2': '管理',
  726. '1': '创建',
  727. '3': '已办',
  728. '55': '停用',
  729. };
  730. return scopeMap[String(this.management || '1')] || '所有';
  731. },
  732. async switchScope(scopeId) {
  733. this.management = String(scopeId || '1');
  734. await this.applyFilters();
  735. },
  736. handleFilterChange() {
  737. this.applyFilters();
  738. },
  739. async applyFilters() {
  740. this.currentPage = 1;
  741. this.hasMore = true;
  742. this.list = [];
  743. await this.loadData(1, false);
  744. },
  745. handleRootButtonClick(button) {
  746. if (!NavigationManager.goToFromButton(button)) {
  747. this.handleServiceAction(button.function || button);
  748. }
  749. },
  750. handleServiceAction(serviceAction) {
  751. if (!serviceAction || typeof serviceAction !== 'object') {
  752. this.showToast('当前操作缺少配置', 'warning');
  753. return;
  754. }
  755. const navButton = {
  756. dest: serviceAction.dest || serviceAction.ssDest || '',
  757. servName: serviceAction.servName || serviceAction.ssServ || serviceAction.service || '',
  758. service: serviceAction.servName || serviceAction.ssServ || serviceAction.service || '',
  759. title: serviceAction.desc || serviceAction.title || '',
  760. desc: serviceAction.desc || serviceAction.title || '',
  761. };
  762. if (NavigationManager.goToFromButton(navButton)) {
  763. return;
  764. }
  765. this.showToast('暂不支持当前操作跳转', 'warning');
  766. },
  767. handleCardClick(item) {
  768. const play = (item && (item.play || (item.service && item.service.play))) || null;
  769. if (!play) {
  770. this.showToast('当前记录缺少查看配置', 'warning');
  771. return;
  772. }
  773. const serviceName = String(play.servName || play.ssServ || '').trim();
  774. const destName = String(play.dest || 'objPlay').trim();
  775. const paramName = String(play.param_name || '').trim();
  776. const paramValue = play.param_value;
  777. const ssToken = String(play.ssToken || '').trim();
  778. const paramText = typeof play.parm === 'string' ? play.parm : '';
  779. if (!serviceName) {
  780. this.showToast('缺少查看服务名', 'warning');
  781. return;
  782. }
  783. NavigationManager.goTo('mp_objplay', {
  784. title: play.desc || play.title || '查看',
  785. service: serviceName,
  786. dest: destName,
  787. ssDest: destName,
  788. param: paramText,
  789. playParamName: paramName,
  790. playParamValue: paramValue,
  791. ssToken,
  792. ssObjId: item.ssObjId || this.ssObjId || '',
  793. ssObjName: item.ssObjName || this.ssObjName || '',
  794. });
  795. },
  796. handleCardAction() {
  797. },
  798. async loadMore() {
  799. if (!this.hasMore || this.isLoadingMore) return;
  800. await this.loadData(Number(this.currentPage || 1) + 1, true);
  801. },
  802. setupRefreshListener() {
  803. this.refreshCleanup = NavigationManager.onRefreshNotify(() => {
  804. this.applyFilters();
  805. });
  806. },
  807. setupScrollListener() {
  808. let timer = null;
  809. const handleScroll = () => {
  810. clearTimeout(timer);
  811. timer = setTimeout(() => {
  812. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  813. const windowHeight = window.innerHeight;
  814. const documentHeight = document.documentElement.scrollHeight;
  815. this.showBackToTop = scrollTop > 300;
  816. if (scrollTop + windowHeight >= documentHeight - 50) {
  817. this.loadMore();
  818. }
  819. }, 200);
  820. };
  821. window.addEventListener('scroll', handleScroll);
  822. this.scrollCleanup = () => {
  823. window.removeEventListener('scroll', handleScroll);
  824. };
  825. },
  826. scrollToTop() {
  827. window.scrollTo({ top: 0, behavior: 'smooth' });
  828. },
  829. handleBackToTopClick() {
  830. if (!this.isLongPress) {
  831. this.scrollToTop();
  832. }
  833. this.isLongPress = false;
  834. },
  835. handleLongPressStart() {
  836. if (!this.hasKeyWord) return;
  837. this.isLongPress = false;
  838. this.longPressTimer = setTimeout(() => {
  839. this.isLongPress = true;
  840. this.openSearchModal();
  841. if (navigator.vibrate) {
  842. navigator.vibrate(50);
  843. }
  844. }, 500);
  845. },
  846. handleLongPressEnd() {
  847. if (this.longPressTimer) {
  848. clearTimeout(this.longPressTimer);
  849. this.longPressTimer = null;
  850. }
  851. },
  852. handleLongPressCancel() {
  853. if (this.longPressTimer) {
  854. clearTimeout(this.longPressTimer);
  855. this.longPressTimer = null;
  856. }
  857. this.isLongPress = false;
  858. },
  859. openSearchModal() {
  860. this.showSearchModal = true;
  861. this.$nextTick(() => {
  862. setTimeout(() => {
  863. if (this.$refs.searchInput) {
  864. this.$refs.searchInput.click();
  865. this.$refs.searchInput.focus();
  866. }
  867. }, 100);
  868. });
  869. },
  870. closeSearchModal() {
  871. this.showSearchModal = false;
  872. },
  873. async performSearch() {
  874. const keyword = String(this.searchKeyword || '').trim();
  875. this.selectedFilters = {
  876. ...this.selectedFilters,
  877. ssKeyword: keyword,
  878. };
  879. this.closeSearchModal();
  880. await this.applyFilters();
  881. },
  882. showToast(message, type = 'info') {
  883. console.log(`${type.toUpperCase()}: ${message}`);
  884. alert(message);
  885. },
  886. },
  887. });
  888. });
  889. </script>
  890. </body>
  891. </html>