index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <template>
  2. <view class="page-container">
  3. <!-- 顶部标签导航 -->
  4. <view class="header-tabs">
  5. <view
  6. class="tab-item"
  7. :class="{ active: currentTab === 0 }"
  8. @click="handleTabChange(0)"
  9. >
  10. <text>农技知识</text>
  11. <view class="tab-line" :class="{ active: currentTab === 0 }"></view>
  12. </view>
  13. <view
  14. class="tab-item"
  15. :class="{ active: currentTab === 1 }"
  16. @click="handleTabChange(1)"
  17. >
  18. <text>政策解读</text>
  19. <view class="tab-line" :class="{ active: currentTab === 1 }"></view>
  20. </view>
  21. </view>
  22. <!-- 内容区域 -->
  23. <scroll-view
  24. scroll-y
  25. class="content-container"
  26. @scrolltolower="loadMore"
  27. @refresherrefresh="onRefresh"
  28. refresher-enabled
  29. :refresher-triggered="isRefreshing"
  30. >
  31. <!-- 主内容区域 -->
  32. <view class="cards-section">
  33. <!-- 农技知识内容 -->
  34. <block v-if="currentTab === 0">
  35. <navigator
  36. class="content-card"
  37. v-for="(item, index) in techList"
  38. :key="index"
  39. :url="`/pages/knowledge/detail?id=${item.id}&type=tech`"
  40. hover-class="card-hover"
  41. >
  42. <view class="content-thumbnail">
  43. <image
  44. :src="item.image"
  45. mode="aspectFill"
  46. class="thumbnail-image"
  47. lazy-load
  48. />
  49. <!-- 仅视频类型显示时长和播放按钮 -->
  50. <template v-if="item.contentType === 'video'">
  51. <view class="video-duration">{{ item.duration }}</view>
  52. <view class="play-button">
  53. <view class="play-icon"></view>
  54. </view>
  55. <view class="content-type-tag video">视频</view>
  56. </template>
  57. <!-- 文章类型显示标签 -->
  58. <template v-else>
  59. <view class="content-type-tag article">文章</view>
  60. </template>
  61. </view>
  62. <view class="content-info">
  63. <text class="content-title">{{ item.title }}</text>
  64. <view class="info-row">
  65. <text class="source-name">{{ item.source }}</text>
  66. <view class="view-count">
  67. <!-- <text class="iconfont icon-eye"></text> -->
  68. <image src="../../static/icons/yidu.png" mode="aspectFit"></image>
  69. <text>已读 {{ item.viewCount }}</text>
  70. </view>
  71. </view>
  72. </view>
  73. </navigator>
  74. </block>
  75. <!-- 政策解读内容 -->
  76. <block v-if="currentTab === 1">
  77. <navigator
  78. class="content-card"
  79. v-for="(item, index) in policyList"
  80. :key="index"
  81. :url="`/pages/knowledge/detail?id=${item.id}&type=policy`"
  82. hover-class="card-hover"
  83. >
  84. <view class="content-thumbnail">
  85. <image
  86. :src="item.image"
  87. mode="aspectFill"
  88. class="thumbnail-image"
  89. lazy-load
  90. />
  91. <!-- 仅视频类型显示时长和播放按钮 -->
  92. <template v-if="item.contentType === 'video'">
  93. <view class="video-duration">{{ item.duration }}</view>
  94. <view class="play-button">
  95. <view class="play-icon"></view>
  96. </view>
  97. <view class="content-type-tag video">视频</view>
  98. </template>
  99. <!-- 文章类型显示标签 -->
  100. <template v-else>
  101. <view class="content-type-tag article">文章</view>
  102. </template>
  103. </view>
  104. <view class="content-info">
  105. <text class="content-title">{{ item.title }}</text>
  106. <view class="info-row">
  107. <text class="source-name">{{ item.source }}</text>
  108. <view class="view-count">
  109. <image src="../../static/icons/yidu.png" mode="aspectFit"></image>
  110. <text>已读 {{ item.viewCount }}</text>
  111. </view>
  112. </view>
  113. </view>
  114. </navigator>
  115. </block>
  116. <!-- 无数据状态 -->
  117. <view class="empty-state" v-if="(currentTab === 0 && techList.length === 0) || (currentTab === 1 && policyList.length === 0)">
  118. <image src="/static/images/empty.png" mode="aspectFit" class="empty-image"></image>
  119. <text class="empty-text">暂无数据</text>
  120. </view>
  121. <!-- 加载状态 -->
  122. <view class="loading-state" v-if="loading">
  123. <text class="loading-text">加载中...</text>
  124. </view>
  125. <!-- 底部加载完成提示 -->
  126. <view class="load-all" v-if="(currentTab === 0 && techLoadAll) || (currentTab === 1 && policyLoadAll)">
  127. <text class="load-all-text">— 已经到底啦 —</text>
  128. </view>
  129. </view>
  130. <!-- 底部安全区域 -->
  131. <view style="height: 120rpx;"></view>
  132. </scroll-view>
  133. <!-- AI问答悬浮按钮 -->
  134. <view class="assistant-btn" @click="navigateToAI" hover-class="btn-hover">
  135. <image src="/static/icons/ai.png" class="assistant-icon" mode="aspectFit"></image>
  136. <text class="btn-text">问农小禹</text>
  137. </view>
  138. </view>
  139. </template>
  140. <script>
  141. import {getTechList,getPolicyList} from '@/api/services/knowledge.js';
  142. export default {
  143. data() {
  144. return {
  145. // 当前标签
  146. currentTab: 0,
  147. // 农技知识列表
  148. techList: [],
  149. // 政策解读列表
  150. policyList: [],
  151. // 分页参数
  152. techPage: 1,
  153. policyPage: 1,
  154. pageSize: 10,
  155. // 加载状态
  156. loading: false,
  157. isRefreshing: false,
  158. // 是否全部加载完成
  159. techLoadAll: false,
  160. policyLoadAll: false
  161. }
  162. },
  163. mounted() {
  164. console.log("process.env.NODE_ENV",process.env.NODE_ENV);
  165. // 初始化数据加载
  166. this.loadInitialData();
  167. },
  168. methods: {
  169. // 初始化数据加载
  170. loadInitialData() {
  171. if (this.currentTab === 0) {
  172. this.loadTechList();
  173. } else {
  174. this.loadPolicyList();
  175. }
  176. },
  177. // 处理Tab切换
  178. handleTabChange(index) {
  179. if (this.currentTab === index) {
  180. return;
  181. }
  182. this.currentTab = index;
  183. // 切换标签时,如果对应列表为空,则加载数据
  184. if (index === 0 && this.techList.length === 0) {
  185. this.loadTechList();
  186. } else if (index === 1 && this.policyList.length === 0) {
  187. this.loadPolicyList();
  188. }
  189. },
  190. // 下拉刷新
  191. async onRefresh() {
  192. this.isRefreshing = true;
  193. if (this.currentTab === 0) {
  194. this.techPage = 1;
  195. this.techLoadAll = false;
  196. await this.loadTechList(true);
  197. } else {
  198. this.policyPage = 1;
  199. this.policyLoadAll = false;
  200. await this.loadPolicyList(true);
  201. }
  202. this.isRefreshing = false;
  203. },
  204. // 加载更多
  205. loadMore() {
  206. if (this.loading) return;
  207. if (this.currentTab === 0 && !this.techLoadAll) {
  208. this.techPage++;
  209. this.loadTechList();
  210. } else if (this.currentTab === 1 && !this.policyLoadAll) {
  211. this.policyPage++;
  212. this.loadPolicyList();
  213. }
  214. },
  215. // 加载农技知识列表
  216. async loadTechList(refresh = false) {
  217. if (this.loading) return;
  218. this.loading = true;
  219. try {
  220. const result = await getTechList({params: {
  221. pageNum: this.techPage,
  222. pageSize: this.pageSize
  223. }});
  224. console.log("result",result);
  225. // 请求成功
  226. if (result.data.code === 200 && result.data.rows) {
  227. const newData = result.data.rows;
  228. // 如果是刷新或第一页,则替换列表
  229. if (refresh || this.techPage === 1) {
  230. this.techList = newData;
  231. } else {
  232. // 否则追加到列表
  233. this.techList = [...this.techList, ...newData];
  234. }
  235. // 判断是否加载完全部数据
  236. if (newData.length < this.pageSize) {
  237. this.techLoadAll = true;
  238. }
  239. console.log("this.techList",this.techList);
  240. } else {
  241. // 请求返回错误
  242. uni.showToast({
  243. title: result.msg || '加载失败',
  244. icon: 'none'
  245. });
  246. // 如果不是第一页,则减回页码
  247. if (this.techPage > 1) {
  248. this.techPage--;
  249. }
  250. }
  251. } catch (error) {
  252. console.error('加载农技知识列表失败:', error);
  253. uni.showToast({
  254. title: '网络异常,请稍后重试',
  255. icon: 'none'
  256. });
  257. // 如果不是第一页,则减回页码
  258. if (this.techPage > 1) {
  259. this.techPage--;
  260. }
  261. } finally {
  262. this.loading = false;
  263. }
  264. },
  265. // 加载政策解读列表
  266. async loadPolicyList(refresh = false) {
  267. if (this.loading) return;
  268. this.loading = true;
  269. try {
  270. const result = await getPolicyList({
  271. params: {
  272. pageNum: this.policyPage,
  273. pageSize: this.pageSize
  274. }
  275. })
  276. // 请求成功
  277. if (result.data.code === 200 && result.data.rows) {
  278. const newData = result.data.rows;
  279. // 如果是刷新或第一页,则替换列表
  280. if (refresh || this.policyPage === 1) {
  281. this.policyList = newData;
  282. } else {
  283. // 否则追加到列表
  284. this.policyList = [...this.policyList, ...newData];
  285. }
  286. // 判断是否加载完全部数据
  287. if (newData.length < this.pageSize) {
  288. this.policyLoadAll = true;
  289. }
  290. } else {
  291. // 请求返回错误
  292. uni.showToast({
  293. title: result.msg || '加载失败',
  294. icon: 'none'
  295. });
  296. // 如果不是第一页,则减回页码
  297. if (this.policyPage > 1) {
  298. this.policyPage--;
  299. }
  300. }
  301. } catch (error) {
  302. console.error('加载政策解读列表失败:', error);
  303. uni.showToast({
  304. title: '网络异常,请稍后重试',
  305. icon: 'none'
  306. });
  307. // 如果不是第一页,则减回页码
  308. if (this.policyPage > 1) {
  309. this.policyPage--;
  310. }
  311. } finally {
  312. this.loading = false;
  313. }
  314. },
  315. // 跳转AI问答
  316. navigateToAI() {
  317. uni.navigateTo({
  318. url: '/pages/knowledge/ai-chat/index'
  319. })
  320. }
  321. }
  322. }
  323. </script>
  324. <style scoped>
  325. .icon-robot:before {
  326. content: "\e64b";
  327. }
  328. .icon-eye:before {
  329. content: "\e614";
  330. }
  331. /* 页面容器
  332. .page-container {
  333. min-height: 100vh;
  334. background-color: #f7f8fa;
  335. position: relative;
  336. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  337. }
  338. /* 头部标签样式 */
  339. .header-tabs {
  340. display: flex;
  341. background-color: #fff;
  342. padding: 0 30rpx;
  343. position: relative;
  344. border-bottom: 1rpx solid #f2f2f2;
  345. }
  346. .tab-item {
  347. position: relative;
  348. padding: 20rpx 30rpx;
  349. margin-right: 50rpx;
  350. font-size: 30rpx;
  351. color: #666;
  352. transition: all 0.3s ease;
  353. }
  354. .tab-item.active {
  355. color: #333;
  356. font-weight: 600;
  357. }
  358. .tab-line {
  359. position: absolute;
  360. bottom: 0;
  361. left: 50%;
  362. width: 0;
  363. height: 6rpx;
  364. background-color: #4dc971;
  365. border-radius: 3rpx;
  366. transition: all 0.3s ease;
  367. transform: translateX(-50%);
  368. }
  369. .tab-line.active {
  370. width: 40rpx;
  371. }
  372. /* 内容区域样式 */
  373. .content-container {
  374. height: calc(100vh - 88rpx);
  375. box-sizing: border-box;
  376. }
  377. .cards-section {
  378. padding: 12rpx;
  379. }
  380. /* 内容卡片通用样式 */
  381. .content-card {
  382. background-color: #fff;
  383. border-radius: 16rpx;
  384. margin-bottom: 24rpx;
  385. overflow: hidden;
  386. transition: all 0.2s ease;
  387. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
  388. position: relative;
  389. }
  390. .card-hover {
  391. transform: translateY(-2rpx);
  392. box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
  393. }
  394. /* 缩略图 */
  395. .content-thumbnail {
  396. position: relative;
  397. width: 100%;
  398. height: 300rpx;
  399. background-color: #f0f0f0;
  400. }
  401. .thumbnail-image {
  402. width: 100%;
  403. height: 100%;
  404. object-fit: cover;
  405. }
  406. /* 播放按钮 */
  407. .play-button {
  408. position: absolute;
  409. top: 50%;
  410. left: 50%;
  411. transform: translate(-50%, -50%);
  412. width: 70rpx;
  413. height: 70rpx;
  414. background-color: rgba(0, 0, 0, 0.5);
  415. border-radius: 50%;
  416. display: flex;
  417. align-items: center;
  418. justify-content: center;
  419. z-index: 2;
  420. }
  421. .play-icon {
  422. width: 0;
  423. height: 0;
  424. border-style: solid;
  425. border-width: 9rpx 0 9rpx 16rpx;
  426. border-color: transparent transparent transparent #ffffff;
  427. margin-left: 4rpx;
  428. }
  429. /* 视频时长 */
  430. .video-duration {
  431. position: absolute;
  432. bottom: 10rpx;
  433. right: 10rpx;
  434. padding: 3rpx 8rpx;
  435. background-color: rgba(0, 0, 0, 0.6);
  436. color: #fff;
  437. font-size: 18rpx;
  438. border-radius: 10rpx;
  439. }
  440. /* 内容类型标签 */
  441. .content-type-tag {
  442. position: absolute;
  443. top: 10rpx;
  444. left: 10rpx;
  445. padding: 4rpx 10rpx;
  446. font-size: 20rpx;
  447. border-radius: 6rpx;
  448. z-index: 2;
  449. }
  450. .content-type-tag.video {
  451. background-color: #4dc971;
  452. color: #fff;
  453. }
  454. .content-type-tag.article {
  455. background-color: #3498db;
  456. color: #fff;
  457. }
  458. /* 内容信息 */
  459. .content-info {
  460. padding: 16rpx;
  461. position: relative;
  462. }
  463. .content-title {
  464. font-size: 30rpx;
  465. font-weight: 600;
  466. color: #333;
  467. line-height: 1.4;
  468. margin-bottom: 10rpx;
  469. display: -webkit-box;
  470. -webkit-box-orient: vertical;
  471. -webkit-line-clamp: 2;
  472. overflow: hidden;
  473. }
  474. .source-name {
  475. color: #999;
  476. font-size: 22rpx;
  477. }
  478. .info-row {
  479. display: flex;
  480. justify-content: space-between;
  481. align-items: center;
  482. }
  483. .view-count {
  484. color: #c0c0c0;
  485. font-size: 20rpx;
  486. display: flex;
  487. align-items: center;
  488. }
  489. .view-count image {
  490. padding-right: 6px;
  491. width: 16px; /* 设置图标宽度 */
  492. height: 16px; /* 设置图标高度 */
  493. vertical-align: middle;
  494. }
  495. .view-count .iconfont {
  496. margin-right: 4rpx;
  497. }
  498. /* 空状态 */
  499. .empty-state {
  500. display: flex;
  501. flex-direction: column;
  502. align-items: center;
  503. justify-content: center;
  504. padding: 120rpx 0;
  505. }
  506. .empty-image {
  507. width: 180rpx;
  508. height: 180rpx;
  509. margin-bottom: 16rpx;
  510. }
  511. .empty-text {
  512. font-size: 26rpx;
  513. color: #999;
  514. }
  515. /* 加载中状态 */
  516. .loading-state {
  517. text-align: center;
  518. padding: 20rpx 0;
  519. }
  520. .loading-text {
  521. font-size: 24rpx;
  522. color: #999;
  523. }
  524. /* 加载全部状态 */
  525. .load-all {
  526. text-align: center;
  527. padding: 30rpx 0;
  528. }
  529. .load-all-text {
  530. font-size: 24rpx;
  531. color: #999;
  532. }
  533. /* AI按钮 */
  534. .assistant-btn {
  535. position: fixed;
  536. right: 30rpx;
  537. bottom: 100rpx;
  538. height: 80rpx;
  539. border-radius: 40rpx;
  540. background: linear-gradient(135deg, #42b983, #3db160);
  541. display: flex;
  542. align-items: center;
  543. padding: 0 24rpx;
  544. box-shadow: 0 6rpx 20rpx rgba(66, 185, 131, 0.35);
  545. z-index: 99;
  546. transition: all 0.25s ease;
  547. }
  548. .assistant-icon {
  549. width: 68rpx;
  550. height: 68rpx;
  551. margin-right: 10rpx;
  552. }
  553. .btn-hover {
  554. transform: scale(1.05);
  555. box-shadow: 0 8rpx 24rpx rgba(66, 185, 131, 0.45);
  556. }
  557. .btn-text {
  558. font-size: 28rpx;
  559. color: #fff;
  560. font-weight: 500;
  561. }
  562. </style>