m-PolicyDocument.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. <template>
  2. <div class="mobile-policy-document">
  3. <MobileHeader title="政策文件" :showMenu="false" @back="goBack" />
  4. <div class="main-content">
  5. <!-- 页面标题与返回首页(移动端仅展示标题) -->
  6. <!-- <div class="page-header">
  7. <h1 class="page-title">政策文件</h1>
  8. </div> -->
  9. <!-- 搜索栏 -->
  10. <div class="search-section">
  11. <div class="search-box">
  12. <div class="search-icon-left">
  13. <img
  14. src="@/assets/Policy/8.png"
  15. alt="搜索"
  16. class="search-icon"
  17. />
  18. </div>
  19. <input
  20. type="text"
  21. placeholder="搜索政策文件..."
  22. class="search-input"
  23. v-model="searchText"
  24. maxlength="100"
  25. @input="handleSearchInput"
  26. @keyup.enter="handleSearch"
  27. />
  28. </div>
  29. </div>
  30. <!-- 分类标签 -->
  31. <div class="category-tabs">
  32. <button
  33. class="tab-btn"
  34. :class="{ active: activeTab === 0 }"
  35. @click="setActiveTab(0)"
  36. >
  37. 全部政策
  38. </button>
  39. <button
  40. class="tab-btn"
  41. :class="{ active: activeTab === 1 }"
  42. @click="setActiveTab(1)"
  43. >
  44. 国家法规
  45. </button>
  46. <button
  47. class="tab-btn"
  48. :class="{ active: activeTab === 2 }"
  49. @click="setActiveTab(2)"
  50. >
  51. 行业法规
  52. </button>
  53. <button
  54. class="tab-btn"
  55. :class="{ active: activeTab === 3 }"
  56. @click="setActiveTab(3)"
  57. >
  58. 地方法规
  59. </button>
  60. <button
  61. class="tab-btn"
  62. :class="{ active: activeTab === 4 }"
  63. @click="setActiveTab(4)"
  64. >
  65. 内部条例
  66. </button>
  67. </div>
  68. <!-- 文档列表 -->
  69. <div
  70. class="document-list"
  71. ref="documentList"
  72. @scroll="handleScroll"
  73. >
  74. <div v-if="loading" class="loading">
  75. <div class="loading-spinner"></div>
  76. <span>加载中...</span>
  77. </div>
  78. <div
  79. v-if="!loading && policyFiles.length === 0"
  80. class="no-data"
  81. >
  82. <span>暂无数据</span>
  83. </div>
  84. <div
  85. v-for="(file, index) in policyFiles"
  86. :key="file.id || index"
  87. class="document-item"
  88. >
  89. <div class="doc-icon">
  90. <img
  91. :src="getFileIcon(file.file_type)"
  92. :alt="getFileTypeName(file.file_type)"
  93. class="file-icon"
  94. />
  95. </div>
  96. <div class="doc-content">
  97. <div class="doc-header">
  98. <h3 class="doc-title">{{ file.policy_name }}</h3>
  99. <span class="doc-date">{{
  100. formatTime(file.publish_time)
  101. }}</span>
  102. </div>
  103. <div class="doc-tags">
  104. <span
  105. v-for="(tag, tagIndex) in getTags(
  106. file.file_tag
  107. )"
  108. :key="tagIndex"
  109. class="tag tag-blue"
  110. >
  111. <img
  112. src="@/assets/Policy/10.png"
  113. alt="标签图标"
  114. class="tag-icon"
  115. />
  116. {{ tag }}
  117. </span>
  118. </div>
  119. <p class="doc-description">{{ file.policy_content }}</p>
  120. <div class="doc-footer">
  121. <div class="doc-info">
  122. <span class="info-item">
  123. <img
  124. src="@/assets/Policy/5.png"
  125. alt="部门"
  126. class="info-icon"
  127. />
  128. {{ file.policy_department }}
  129. </span>
  130. <span class="info-item">
  131. <img
  132. src="@/assets/Policy/6.png"
  133. alt="次数"
  134. class="info-icon"
  135. />
  136. {{ file.view_count }} 次查看
  137. </span>
  138. </div>
  139. <div class="doc-actions">
  140. <button
  141. class="action-btn view-btn"
  142. @click="viewPolicy(file)"
  143. >
  144. 查看详情 &gt;
  145. </button>
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. <div v-if="hasMore && !loading" class="load-more">
  151. <span>上拉加载更多</span>
  152. </div>
  153. </div>
  154. </div>
  155. <div
  156. v-if="previewVisible"
  157. class="preview-modal"
  158. @click.self="closePreview"
  159. >
  160. <div class="preview-content">
  161. <div class="preview-header">
  162. <h3 class="preview-title">{{ previewTitle }}</h3>
  163. <button class="close-btn" @click="closePreview">×</button>
  164. </div>
  165. <div class="preview-body">
  166. <!-- PDF/TXT/其他文件使用自定义渲染器 -->
  167. <MobilePdfViewer
  168. v-if="previewUrl && isPdfType"
  169. :url="previewUrl"
  170. class="preview-frame"
  171. />
  172. <!-- Office文档使用微软在线预览 -->
  173. <iframe
  174. v-else-if="previewUrl"
  175. :src="previewUrl"
  176. class="preview-frame"
  177. frameborder="0"
  178. allowfullscreen
  179. ></iframe>
  180. <div v-else class="preview-placeholder">暂无可预览内容</div>
  181. </div>
  182. </div>
  183. </div>
  184. </div>
  185. </template>
  186. <script setup>
  187. import { ref, computed, onMounted, onUnmounted } from "vue";
  188. import { useRouter } from "vue-router";
  189. import MobileHeader from "@/components/MobileHeader.vue";
  190. import MobilePdfViewer from "@/components/MobilePdfViewer.vue";
  191. import { apis } from "@/request/apis.js";
  192. const router = useRouter();
  193. // 响应式数据(与PC端保持一致)
  194. const searchText = ref("");
  195. const activeTab = ref(0);
  196. const policyFiles = ref([]);
  197. const loading = ref(false);
  198. const page = ref(1);
  199. const pageSize = ref(10);
  200. const hasMore = ref(true);
  201. const documentList = ref(null);
  202. const previewVisible = ref(false);
  203. const previewFile = ref(null);
  204. let searchTimer = null;
  205. let scrollTimer = null;
  206. const goBack = () => {
  207. router.go(-1);
  208. };
  209. const setActiveTab = (tabIndex) => {
  210. activeTab.value = tabIndex;
  211. page.value = 1;
  212. policyFiles.value = [];
  213. hasMore.value = true;
  214. fetchPolicyFiles();
  215. };
  216. const fetchPolicyFiles = async (isLoadMore = false) => {
  217. if (loading.value) return;
  218. loading.value = true;
  219. try {
  220. const params = {
  221. page: page.value,
  222. pageSize: pageSize.value,
  223. search: searchText.value,
  224. policy_type: activeTab.value === 0 ? "" : activeTab.value,
  225. };
  226. const response = await apis.getPolicyFile(params);
  227. if (response && response.data) {
  228. const newFiles = response.data;
  229. if (isLoadMore) {
  230. policyFiles.value = [...policyFiles.value, ...newFiles];
  231. } else {
  232. policyFiles.value = newFiles;
  233. }
  234. hasMore.value = newFiles.length === pageSize.value;
  235. }
  236. } catch (error) {
  237. console.error("获取政策文件失败:", error);
  238. } finally {
  239. loading.value = false;
  240. }
  241. };
  242. const handleSearchInput = () => {
  243. if (searchTimer) clearTimeout(searchTimer);
  244. searchTimer = setTimeout(() => {
  245. page.value = 1;
  246. policyFiles.value = [];
  247. hasMore.value = true;
  248. fetchPolicyFiles();
  249. }, 300);
  250. };
  251. const handleSearch = () => {
  252. if (searchText.value.trim()) {
  253. page.value = 1;
  254. policyFiles.value = [];
  255. hasMore.value = true;
  256. fetchPolicyFiles();
  257. }
  258. };
  259. const viewPolicy = async (file) => {
  260. console.log("查看政策文件:", file);
  261. console.log("文件ID:", file.id, "文件所有字段:", Object.keys(file));
  262. if (!file.policy_file_url) {
  263. alert("文件链接不存在");
  264. return;
  265. }
  266. // 检查ID是否存在
  267. if (!file.id) {
  268. console.error("政策文件ID不存在,跳过次数更新");
  269. } else {
  270. // 前端直接更新查看次数显示
  271. file.view_count = (file.view_count || 0) + 1;
  272. // 更新查看次数到后端
  273. try {
  274. await apis.updatePolicyFileCount({
  275. policy_file_id: file.id,
  276. action_type: 1, // 1-查看
  277. });
  278. console.log("查看次数更新成功");
  279. } catch (error) {
  280. console.error("更新查看次数失败:", error);
  281. // 如果后端更新失败,回滚前端显示
  282. file.view_count = (file.view_count || 1) - 1;
  283. }
  284. }
  285. previewFile.value = file;
  286. previewVisible.value = true;
  287. };
  288. // 图标
  289. import pdfIcon from "@/assets/Policy/2.png";
  290. import wordIcon from "@/assets/Policy/3.png";
  291. import excelIcon from "@/assets/Policy/4.png";
  292. const getFileIcon = (fileType) => {
  293. const iconMap = {
  294. 0: pdfIcon,
  295. 1: wordIcon,
  296. 2: excelIcon,
  297. 3: pdfIcon,
  298. 4: pdfIcon,
  299. 5: pdfIcon,
  300. };
  301. return iconMap[fileType] || pdfIcon;
  302. };
  303. const getFileTypeName = (fileType) => {
  304. const typeMap = {
  305. 0: "PDF",
  306. 1: "Word",
  307. 2: "Excel",
  308. 3: "PPT",
  309. 4: "TXT",
  310. 5: "其他",
  311. };
  312. return typeMap[fileType] || "文件";
  313. };
  314. const previewUrl = computed(() => {
  315. if (!previewFile.value || !previewFile.value.policy_file_url) return "";
  316. const fileType = previewFile.value.file_type;
  317. const url = previewFile.value.policy_file_url;
  318. if (fileType === 0 || fileType === 4 || fileType === 5) {
  319. return url;
  320. }
  321. if (fileType === 1 || fileType === 2 || fileType === 3) {
  322. const encodedUrl = encodeURIComponent(url);
  323. return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`;
  324. }
  325. return url;
  326. });
  327. const previewTitle = computed(
  328. () => previewFile.value?.policy_name || "政策文件预览"
  329. );
  330. const isPdfType = computed(() => {
  331. if (!previewFile.value) return false;
  332. const fileType = previewFile.value.file_type;
  333. // 0: PDF, 4: TXT, 5: 其他 (通常也是PDF或图片)
  334. return fileType === 0 || fileType === 4 || fileType === 5;
  335. });
  336. const closePreview = () => {
  337. previewVisible.value = false;
  338. previewFile.value = null;
  339. };
  340. const getTags = (tagString) => {
  341. if (!tagString) return ["政策文件"];
  342. return tagString
  343. .split(",")
  344. .map((tag) => tag.trim())
  345. .filter((tag) => tag.length > 0);
  346. };
  347. const formatTime = (timestamp) => {
  348. if (!timestamp) return "";
  349. const date = new Date(timestamp * 1000);
  350. const year = date.getFullYear();
  351. const month = String(date.getMonth() + 1).padStart(2, "0");
  352. const day = String(date.getDate()).padStart(2, "0");
  353. return `${year}-${month}-${day}`;
  354. };
  355. const handleScroll = (event) => {
  356. if (scrollTimer) clearTimeout(scrollTimer);
  357. scrollTimer = setTimeout(() => {
  358. const target = event.target;
  359. const scrollTop = target.scrollTop;
  360. const scrollHeight = target.scrollHeight;
  361. const clientHeight = target.clientHeight;
  362. if (
  363. scrollTop + clientHeight >= scrollHeight - 50 &&
  364. hasMore.value &&
  365. !loading.value
  366. ) {
  367. page.value++;
  368. fetchPolicyFiles(true);
  369. }
  370. }, 100);
  371. };
  372. onMounted(() => {
  373. fetchPolicyFiles();
  374. });
  375. onUnmounted(() => {
  376. if (scrollTimer) clearTimeout(scrollTimer);
  377. if (searchTimer) clearTimeout(searchTimer);
  378. });
  379. </script>
  380. <style lang="less" scoped>
  381. .mobile-policy-document {
  382. min-height: 100vh;
  383. background: #ebf3ff;
  384. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  385. display: flex;
  386. flex-direction: column;
  387. }
  388. .main-content {
  389. padding: 12px 0 0 0;
  390. display: flex;
  391. flex-direction: column;
  392. align-items: center;
  393. }
  394. .page-header {
  395. display: flex;
  396. align-items: center;
  397. justify-content: flex-start;
  398. margin-bottom: 10px;
  399. .page-title {
  400. font-size: 18px;
  401. font-weight: 900;
  402. color: #111827;
  403. margin: 0;
  404. }
  405. }
  406. .search-section {
  407. margin-bottom: 10px;
  408. // padding: 0 14px;
  409. width: 100%;
  410. max-width: 450px;
  411. .search-box {
  412. position: relative;
  413. width: 100%;
  414. .search-icon-left {
  415. position: absolute;
  416. left: 12px;
  417. z-index: 10;
  418. top: 50%;
  419. transform: translateY(-50%);
  420. .search-icon {
  421. width: 28px;
  422. height: 28px;
  423. opacity: 0.6;
  424. }
  425. }
  426. .search-input {
  427. width: 100%;
  428. // margin: 0 auto;
  429. height: 62px;
  430. padding: 0 14px 0 42px;
  431. border: 1px solid #e5e7eb;
  432. border-radius: 8px;
  433. font-size: 20px !important;
  434. background: #fff;
  435. outline: none;
  436. transition: all 0.3s ease;
  437. /* 光标优化 */
  438. caret-color: #000;
  439. /* 强制渲染 */
  440. -webkit-appearance: none;
  441. appearance: none;
  442. color: #000 !important;
  443. &:focus {
  444. border-color: #3b82f6;
  445. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  446. }
  447. &::placeholder {
  448. color: #9ca3af;
  449. }
  450. }
  451. }
  452. }
  453. .category-tabs {
  454. display: flex;
  455. overflow-x: auto;
  456. -webkit-overflow-scrolling: touch;
  457. gap: 0;
  458. margin-bottom: 10px;
  459. border-bottom: 1px solid #e5e7eb;
  460. width: 100%;
  461. justify-content: center;
  462. .tab-btn {
  463. padding: 10px 12px;
  464. border: none;
  465. background: transparent;
  466. font-size: 18px;
  467. color: #6b7280;
  468. cursor: pointer;
  469. transition: all 0.3s ease;
  470. position: relative;
  471. border-bottom: 2px solid transparent;
  472. white-space: nowrap;
  473. &:hover {
  474. color: #2563eb;
  475. }
  476. &.active {
  477. color: #2563eb;
  478. border-bottom-color: #2563eb;
  479. }
  480. }
  481. }
  482. .document-list {
  483. width: 100%;
  484. max-width: 450px;
  485. display: flex;
  486. flex-direction: column;
  487. gap: 10px;
  488. min-height: 300px;
  489. max-height: calc(100vh - 180px);
  490. overflow-y: auto;
  491. // padding: 0 14px;
  492. }
  493. .document-item {
  494. background: #fff;
  495. width: 100%;
  496. border-radius: 12px;
  497. padding: 14px;
  498. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  499. transition: all 0.3s ease;
  500. display: flex;
  501. gap: 12px;
  502. border: 1px solid #f1f5f9;
  503. .doc-icon {
  504. flex-shrink: 0;
  505. .file-icon {
  506. width: 22px;
  507. height: 22px;
  508. object-fit: contain;
  509. }
  510. }
  511. .doc-content {
  512. flex: 1;
  513. .doc-header {
  514. display: flex;
  515. justify-content: space-between;
  516. align-items: flex-start;
  517. margin-bottom: 6px;
  518. .doc-title {
  519. font-size: 16px;
  520. font-weight: 600;
  521. color: #1f2937;
  522. margin: 0;
  523. line-height: 1.4;
  524. overflow: hidden;
  525. text-overflow: ellipsis;
  526. display: -webkit-box;
  527. line-clamp: 1;
  528. -webkit-line-clamp: 1;
  529. -webkit-box-orient: vertical;
  530. flex: 1;
  531. margin-right: 10px;
  532. }
  533. .doc-date {
  534. font-size: 12px;
  535. color: #6b7280;
  536. white-space: nowrap;
  537. }
  538. }
  539. .doc-tags {
  540. display: flex;
  541. gap: 8px;
  542. margin-bottom: 6px;
  543. flex-wrap: wrap;
  544. .tag {
  545. padding: 3px 10px;
  546. font-size: 12px;
  547. border-radius: 12px;
  548. font-weight: 500;
  549. display: flex;
  550. align-items: center;
  551. gap: 4px;
  552. background: #eff6ff;
  553. color: #2563eb;
  554. white-space: nowrap;
  555. .tag-icon {
  556. width: 12px;
  557. height: 12px;
  558. flex-shrink: 0;
  559. }
  560. }
  561. }
  562. .doc-description {
  563. font-size: 13px;
  564. color: #6b7280;
  565. line-height: 1.5;
  566. margin: 0 0 6px 0;
  567. display: -webkit-box;
  568. line-clamp: 2;
  569. -webkit-line-clamp: 2;
  570. -webkit-box-orient: vertical;
  571. overflow: hidden;
  572. }
  573. .doc-footer {
  574. display: flex;
  575. justify-content: space-between;
  576. align-items: center;
  577. .doc-info {
  578. display: flex;
  579. gap: 16px;
  580. .info-item {
  581. display: flex;
  582. align-items: center;
  583. gap: 6px;
  584. font-size: 12px;
  585. color: #6b7280;
  586. .info-icon {
  587. width: 14px;
  588. height: 14px;
  589. opacity: 0.6;
  590. }
  591. }
  592. }
  593. .doc-actions {
  594. display: flex;
  595. .action-btn:first-child {
  596. margin-right: 6px;
  597. }
  598. .action-btn {
  599. padding: 4px 0;
  600. border: none;
  601. border-radius: 6px;
  602. font-size: 13px;
  603. cursor: pointer;
  604. transition: all 0.3s ease;
  605. display: flex;
  606. align-items: center;
  607. gap: 6px;
  608. &.view-btn {
  609. color: #3b82f6;
  610. background: transparent;
  611. }
  612. }
  613. }
  614. }
  615. }
  616. }
  617. .loading {
  618. display: flex;
  619. flex-direction: column;
  620. align-items: center;
  621. justify-content: center;
  622. padding: 30px 16px;
  623. color: #6b7280;
  624. font-size: 14px;
  625. .loading-spinner {
  626. border: 4px solid #f3f3f3;
  627. border-top: 4px solid #3b82f6;
  628. border-radius: 50%;
  629. width: 36px;
  630. height: 36px;
  631. animation: spin 1s linear infinite;
  632. margin-bottom: 12px;
  633. }
  634. }
  635. @keyframes spin {
  636. 0% {
  637. transform: rotate(0deg);
  638. }
  639. 100% {
  640. transform: rotate(360deg);
  641. }
  642. }
  643. .no-data {
  644. display: flex;
  645. justify-content: center;
  646. align-items: center;
  647. padding: 40px 16px;
  648. color: #9ca3af;
  649. font-size: 18px;
  650. text-align: center;
  651. }
  652. .load-more {
  653. display: flex;
  654. justify-content: center;
  655. align-items: center;
  656. padding: 16px;
  657. color: #6b7280;
  658. font-size: 13px;
  659. border-top: 1px solid #e5e7eb;
  660. margin-top: 6px;
  661. }
  662. .document-list::-webkit-scrollbar {
  663. width: 4px;
  664. }
  665. .document-list::-webkit-scrollbar-track {
  666. background: #f1f1f1;
  667. border-radius: 3px;
  668. }
  669. .document-list::-webkit-scrollbar-thumb {
  670. background: #c1c1c1;
  671. border-radius: 3px;
  672. }
  673. .document-list::-webkit-scrollbar-thumb:hover {
  674. background: #a8a8a8;
  675. }
  676. .preview-modal {
  677. position: fixed;
  678. inset: 0;
  679. background: rgba(0, 0, 0, 0.65);
  680. display: flex;
  681. justify-content: center;
  682. align-items: center;
  683. padding: 20px;
  684. z-index: 999;
  685. }
  686. .preview-content {
  687. width: 100%;
  688. max-width: 640px;
  689. height: 90vh;
  690. background: #fff;
  691. border-radius: 12px;
  692. display: flex;
  693. flex-direction: column;
  694. overflow: hidden;
  695. }
  696. .preview-header {
  697. display: flex;
  698. justify-content: space-between;
  699. align-items: center;
  700. padding: 14px 16px;
  701. background: #f8fafc;
  702. border-bottom: 1px solid #e5e7eb;
  703. }
  704. .preview-title {
  705. margin: 0;
  706. font-size: 16px;
  707. color: #111827;
  708. font-weight: 600;
  709. }
  710. .close-btn {
  711. border: none;
  712. background: transparent;
  713. font-size: 24px;
  714. cursor: pointer;
  715. color: #6b7280;
  716. line-height: 1;
  717. padding: 4px 8px;
  718. }
  719. .preview-body {
  720. flex: 1;
  721. background: #111827;
  722. display: flex;
  723. align-items: center;
  724. justify-content: center;
  725. overflow: hidden;
  726. /* 防止内容溢出 */
  727. position: relative;
  728. }
  729. .preview-frame {
  730. width: 100%;
  731. height: 100%;
  732. border: none;
  733. }
  734. .preview-placeholder {
  735. color: #fff;
  736. font-size: 16px;
  737. }
  738. </style>