西藏巴青项目

index.vue 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <template>
  2. <!-- 农技课堂:上区搜索 + 主 Tab + 子类 Chip;下区虚拟列表,每行两列卡片(视频课程 / 实战培训 底部样式不同) -->
  3. <view :class="pageRootClass" class="tab-page ac-page" :style="pageStyle">
  4. <view class="ac-top">
  5. <up-search
  6. v-model="searchKeyword"
  7. shape="round"
  8. :placeholder="$t('agriClassroomPage.searchPlaceholder')"
  9. :show-action="false"
  10. :clearabled="true"
  11. bg-color="#f5f2ef"
  12. border-color="#e5ded6"
  13. />
  14. <up-tabs
  15. :current="mainTabIndex"
  16. class="ac-tabs"
  17. :list="mainTabsList"
  18. key-name="name"
  19. :scrollable="false"
  20. line-color="#22C55E"
  21. :active-style="{ color: '#15803d', fontWeight: '600' }"
  22. :inactive-style="{ color: '#78716c' }"
  23. @update:current="onMainTabUpdate"
  24. />
  25. <!-- 第三行:子类 Chip,圆角线框;未选中灰,选中绿框绿字 -->
  26. <view class="ac-chips" role="tablist">
  27. <view
  28. v-for="code in subTypeCodes"
  29. :key="code"
  30. class="ac-chip"
  31. :class="{ 'ac-chip--on': chipSelected(code) }"
  32. role="button"
  33. @click="onPickSub(code)"
  34. >
  35. <text class="text-body ac-chip__txt">{{ subLabel(code) }}</text>
  36. </view>
  37. </view>
  38. </view>
  39. <view class="ac-body">
  40. <view v-if="listLoading && !pairedRows.length" class="ac-empty">
  41. <text class="text-body ac-empty__txt">{{ $t('agriClassroomPage.loading') }}</text>
  42. </view>
  43. <view v-else-if="!pairedRows.length" class="ac-empty">
  44. <text class="text-body ac-empty__txt">{{ $t('agriClassroomPage.empty') }}</text>
  45. </view>
  46. <view v-else class="ac-list-wrap">
  47. <up-virtual-list
  48. ref="acVList"
  49. class="ac-vlist"
  50. :list-data="pairedRows"
  51. :item-height="slotHeightPx"
  52. :height="listHeightPx"
  53. :buffer="4"
  54. key-field="id"
  55. :scroll-top="vScrollTop"
  56. @update:scrollTop="vScrollTop = $event"
  57. @scroll="onVirtualListScroll"
  58. >
  59. <template #default="{ item }">
  60. <view class="ac-pair-cell">
  61. <view class="ac-pair" :style="{ height: rowBodyPx + 'px' }">
  62. <view class="ac-pair__col">
  63. <view v-if="item.left" class="ac-card" role="button" @click="openCourseDetail(item.left)">
  64. <up-lazy-load
  65. class="ac-card__img"
  66. :image="articleCover(item.left)"
  67. height="120"
  68. :border-radius="12"
  69. img-mode="aspectFill"
  70. :threshold="200"
  71. :index="item.left.id"
  72. @click.stop="() => onPreviewCover(item.left)"
  73. />
  74. <text class="ac-card__title">{{ item.left.title }}</text>
  75. <view v-if="item.left.type === 'video'" class="ac-card__foot ac-card__foot--video">
  76. <text class="text-body ac-card__meta">{{ videoSubLabel(item.left.typeCode) }}</text>
  77. <text class="text-body ac-card__meta ac-card__meta--muted">{{ item.left.publishTime || $t('agriClassroomPage.noDate') }}</text>
  78. </view>
  79. <view v-else class="ac-card__foot ac-card__foot--train">
  80. <text class="text-body ac-card__meta">{{ trainingSubLabel(item.left.typeCode) }}</text>
  81. <up-icon name="account-fill" color="#22C55E" :size="16" />
  82. <text class="text-body ac-card__meta ac-card__meta--num">
  83. {{ item.left.enrolled }}/{{ item.left.total }}
  84. </text>
  85. </view>
  86. </view>
  87. </view>
  88. <view class="ac-pair__col">
  89. <view v-if="item.right" class="ac-card" role="button" @click="openCourseDetail(item.right)">
  90. <up-lazy-load
  91. class="ac-card__img"
  92. :image="articleCover(item.right)"
  93. height="120"
  94. :border-radius="12"
  95. img-mode="aspectFill"
  96. :threshold="200"
  97. :index="item.right.id"
  98. @click.stop="() => onPreviewCover(item.right)"
  99. />
  100. <text class="ac-card__title">{{ item.right.title }}</text>
  101. <view v-if="item.right.type === 'video'" class="ac-card__foot ac-card__foot--video">
  102. <text class="text-body ac-card__meta">{{ videoSubLabel(item.right.typeCode) }}</text>
  103. <text class="text-body ac-card__meta ac-card__meta--muted">{{ item.right.publishTime || $t('agriClassroomPage.noDate') }}</text>
  104. </view>
  105. <view v-else class="ac-card__foot ac-card__foot--train">
  106. <text class="text-body ac-card__meta">{{ trainingSubLabel(item.right.typeCode) }}</text>
  107. <up-icon name="account-fill" color="#22C55E" :size="16" />
  108. <text class="text-body ac-card__meta ac-card__meta--num">
  109. {{ item.right.enrolled }}/{{ item.right.total }}
  110. </text>
  111. </view>
  112. </view>
  113. </view>
  114. </view>
  115. </view>
  116. </template>
  117. </up-virtual-list>
  118. </view>
  119. </view>
  120. </view>
  121. </template>
  122. <script>
  123. import USearch from 'uview-plus/components/u-search/u-search.vue'
  124. import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
  125. import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
  126. import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
  127. import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
  128. import tabPage from '@/mixins/tabPage'
  129. import { resolveResourceUrl } from '@/utils/resourceUrl'
  130. import { listAgriculturalCourse } from '@/api/agriculturalCourse'
  131. /** 视频课程 course_topic(005001–005004) */
  132. const VIDEO_TYPE_CODES = ['005001', '005002', '005003', '005004']
  133. /** 实战培训 training_status(006001–006004) */
  134. const TRAINING_TYPE_CODES = ['006001', '006002', '006003', '006004']
  135. const LIST_PAGE_SIZE = 20
  136. const ROW_BODY_RPX = 320
  137. const ROW_GAP_RPX = 20
  138. const AC_BODY_PAD_RPX = 40
  139. const COVER = '/static/ai/hero.png'
  140. /** 视频课程详情 */
  141. const COURSE_DETAIL_PATH = '/package-a/course-detail/index'
  142. /** 实战培训详情(英文路径) */
  143. const TRAINING_DETAIL_PATH = '/package-a/training-detail/index'
  144. export default {
  145. components: {
  146. 'up-search': USearch,
  147. 'up-tabs': UTabs,
  148. 'up-icon': UIcon,
  149. 'up-lazy-load': ULazyLoad,
  150. 'up-virtual-list': UVirtualList
  151. },
  152. mixins: [tabPage],
  153. data() {
  154. return {
  155. navTitleKey: 'agriClassroomPage.navTitle',
  156. searchKeyword: '',
  157. /** 0 视频课程 1 实战培训 */
  158. mainTabIndex: 0,
  159. selectedVideoType: '005001',
  160. selectedTrainingType: '006002',
  161. allCards: [],
  162. listLoading: false,
  163. listLoadingMore: false,
  164. listTotal: 0,
  165. pageNum: 1,
  166. pageSize: LIST_PAGE_SIZE,
  167. _searchTimer: null,
  168. loadMoreTimer: null,
  169. rowBodyPx: 140,
  170. marginPx: 10,
  171. slotHeightPx: 150,
  172. listHeightPx: 400,
  173. pageHeightPx: 0,
  174. vScrollTop: 0,
  175. coverSrc: COVER
  176. }
  177. },
  178. computed: {
  179. mainTabsList() {
  180. return [
  181. { name: this.$t('agriClassroomPage.mainVideo'), id: 'video' },
  182. { name: this.$t('agriClassroomPage.mainTraining'), id: 'training' }
  183. ]
  184. },
  185. isVideoMain() {
  186. return this.mainTabIndex === 0
  187. },
  188. subTypeCodes() {
  189. return this.isVideoMain ? VIDEO_TYPE_CODES : TRAINING_TYPE_CODES
  190. },
  191. selectedTypeCode() {
  192. return this.isVideoMain ? this.selectedVideoType : this.selectedTrainingType
  193. },
  194. listNoMore() {
  195. return this.listTotal > 0 && this.allCards.length >= this.listTotal
  196. },
  197. pageStyle() {
  198. if (this.pageHeightPx > 0) {
  199. return { height: `${this.pageHeightPx}px` }
  200. }
  201. return {}
  202. },
  203. /** 虚拟列表每行 2 个卡片 */
  204. pairedRows() {
  205. const cards = this.allCards
  206. const rows = []
  207. for (let i = 0; i < cards.length; i += 2) {
  208. const L = cards[i]
  209. const R = cards[i + 1] || null
  210. rows.push({
  211. id: `${L.id}_${R ? R.id : 'x'}`,
  212. left: L,
  213. right: R
  214. })
  215. }
  216. return rows
  217. }
  218. },
  219. watch: {
  220. searchKeyword() {
  221. this.vScrollTop = 0
  222. clearTimeout(this._searchTimer)
  223. this._searchTimer = setTimeout(() => {
  224. this.loadCourseList(true)
  225. }, 300)
  226. },
  227. selectedVideoType() {
  228. if (this.isVideoMain) {
  229. this.vScrollTop = 0
  230. this.loadCourseList(true)
  231. }
  232. },
  233. selectedTrainingType() {
  234. if (!this.isVideoMain) {
  235. this.vScrollTop = 0
  236. this.loadCourseList(true)
  237. }
  238. }
  239. },
  240. created() {
  241. this.loadCourseList(true)
  242. },
  243. onReady() {
  244. try {
  245. this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
  246. this.rowBodyPx = Math.max(120, Math.ceil(uni.upx2px(ROW_BODY_RPX)))
  247. this.slotHeightPx = this.rowBodyPx + this.marginPx
  248. } catch (e) {
  249. this.marginPx = 10
  250. this.rowBodyPx = 140
  251. this.slotHeightPx = 150
  252. }
  253. this.applyListHeightFallback()
  254. this.$nextTick(() => this.calcLayoutHeights())
  255. },
  256. onShow() {
  257. this.$nextTick(() => this.calcLayoutHeights())
  258. },
  259. onUnload() {
  260. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  261. if (this._searchTimer) clearTimeout(this._searchTimer)
  262. },
  263. methods: {
  264. isTrainingType(typeCode) {
  265. return String(typeCode || '').startsWith('006')
  266. },
  267. mapCourseRow(row) {
  268. const typeCode = row.type || ''
  269. const isTraining = this.isTrainingType(typeCode)
  270. return {
  271. id: String(row.id != null ? row.id : ''),
  272. type: isTraining ? 'training' : 'video',
  273. typeCode,
  274. title: row.title || '',
  275. introduction: row.introduction || '',
  276. coverFileUrl: row.coverFileUrl || '',
  277. contentFileUrl: row.contentFileUrl || '',
  278. publishTime: row.publishTime || '',
  279. enrolled: row.actualEnrolledCount != null ? Number(row.actualEnrolledCount) : 0,
  280. total: row.plannedHeadCount != null ? Number(row.plannedHeadCount) : 0,
  281. registrationStartTime: row.registrationStartTime || '',
  282. registrationEndTime: row.registrationEndTime || '',
  283. trainingTime: row.trainingTime || '',
  284. expectedOutcome: row.expectedOutcome || ''
  285. }
  286. },
  287. loadCourseList(reset = false) {
  288. const type = this.selectedTypeCode
  289. if (!type) {
  290. this.allCards = []
  291. this.listTotal = 0
  292. return Promise.resolve()
  293. }
  294. if (!reset) {
  295. if (this.listLoading || this.listLoadingMore || this.listNoMore) {
  296. return Promise.resolve()
  297. }
  298. this.pageNum += 1
  299. this.listLoadingMore = true
  300. } else {
  301. this.pageNum = 1
  302. this.listLoading = true
  303. }
  304. const params = {
  305. type,
  306. pageNum: this.pageNum,
  307. pageSize: this.pageSize
  308. }
  309. const titleKw = (this.searchKeyword || '').trim()
  310. if (titleKw) {
  311. params.title = titleKw
  312. }
  313. return listAgriculturalCourse(params)
  314. .then((res) => {
  315. const rows = res.rows || []
  316. this.listTotal = res.total != null ? Number(res.total) : 0
  317. const mapped = rows.map((row) => this.mapCourseRow(row))
  318. this.allCards = reset ? mapped : this.allCards.concat(mapped)
  319. this.$nextTick(() => {
  320. this.calcLayoutHeights()
  321. if (this.allCards.length < this.listTotal) {
  322. this.tryAutoLoadMore()
  323. }
  324. })
  325. })
  326. .catch(() => {
  327. if (reset) {
  328. this.allCards = []
  329. this.listTotal = 0
  330. } else {
  331. this.pageNum -= 1
  332. }
  333. })
  334. .finally(() => {
  335. if (reset) {
  336. this.listLoading = false
  337. } else {
  338. this.listLoadingMore = false
  339. }
  340. })
  341. },
  342. tryAutoLoadMore() {
  343. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  344. const totalH = this.pairedRows.length * this.slotHeightPx
  345. const viewH = this.listHeightPx
  346. if (totalH > 0 && totalH <= viewH) {
  347. this.loadCourseList(false)
  348. }
  349. },
  350. onVirtualListScroll(scrollTop) {
  351. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  352. this.loadMoreTimer = setTimeout(() => {
  353. this.loadMoreTimer = null
  354. this.tryLoadMoreOnScroll(typeof scrollTop === 'number' ? scrollTop : 0)
  355. }, 180)
  356. },
  357. tryLoadMoreOnScroll(scrollTop) {
  358. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  359. const totalH = this.pairedRows.length * this.slotHeightPx
  360. const viewH = this.listHeightPx
  361. const threshold = Math.max(this.slotHeightPx * 2, 120)
  362. if (totalH <= viewH) {
  363. this.loadCourseList(false)
  364. return
  365. }
  366. if (scrollTop + viewH >= totalH - threshold) {
  367. this.loadCourseList(false)
  368. }
  369. },
  370. getNavigationBarHeight() {
  371. const sys = uni.getSystemInfoSync()
  372. const statusBar = sys.statusBarHeight || 0
  373. // #ifdef MP-WEIXIN
  374. try {
  375. const menu = uni.getMenuButtonBoundingClientRect()
  376. if (menu && menu.height) {
  377. return statusBar + (menu.top - statusBar) * 2 + menu.height
  378. }
  379. } catch (e) {
  380. /* noop */
  381. }
  382. // #endif
  383. return statusBar + 44
  384. },
  385. getViewportContentHeight() {
  386. const sys = uni.getSystemInfoSync()
  387. const navH = this.getNavigationBarHeight()
  388. // #ifdef H5
  389. if (typeof window !== 'undefined' && window.innerHeight) {
  390. return Math.max(320, window.innerHeight - navH)
  391. }
  392. // #endif
  393. const screenH = sys.screenHeight || sys.windowHeight || 600
  394. return Math.max(320, screenH - navH)
  395. },
  396. applyListHeightFallback() {
  397. const contentH = this.getViewportContentHeight()
  398. const topFallback = Math.ceil(uni.upx2px(280))
  399. const bodyPad = Math.ceil(uni.upx2px(AC_BODY_PAD_RPX))
  400. this.pageHeightPx = contentH
  401. this.listHeightPx = Math.max(240, contentH - topFallback - bodyPad)
  402. },
  403. calcLayoutHeights() {
  404. const contentH = this.getViewportContentHeight()
  405. const bodyPad = Math.ceil(uni.upx2px(AC_BODY_PAD_RPX))
  406. this.pageHeightPx = contentH
  407. uni.createSelectorQuery()
  408. .in(this)
  409. .select('.ac-top')
  410. .boundingClientRect((topRect) => {
  411. const acTopH =
  412. topRect && topRect.height > 0 ? Math.ceil(topRect.height) : Math.ceil(uni.upx2px(280))
  413. this.listHeightPx = Math.max(240, contentH - acTopH - bodyPad)
  414. this.$nextTick(() => {
  415. this.$refs.acVList?.measureContainerHeight?.()
  416. })
  417. })
  418. .exec()
  419. },
  420. subLabel(typeCode) {
  421. return this.isVideoMain ? this.videoSubLabel(typeCode) : this.trainingSubLabel(typeCode)
  422. },
  423. chipSelected(typeCode) {
  424. return this.selectedTypeCode === typeCode
  425. },
  426. onPickSub(typeCode) {
  427. if (this.isVideoMain) {
  428. this.selectedVideoType = typeCode
  429. } else {
  430. this.selectedTrainingType = typeCode
  431. }
  432. },
  433. typeCodeIndex(typeCode, codes) {
  434. const idx = codes.indexOf(typeCode)
  435. return idx >= 0 ? idx : 0
  436. },
  437. videoSubLabel(typeCode) {
  438. const idx = this.typeCodeIndex(typeCode, VIDEO_TYPE_CODES)
  439. return this.$t(`agriClassroomPage.videoSubs.s${idx}`)
  440. },
  441. trainingSubLabel(typeCode) {
  442. const idx = this.typeCodeIndex(typeCode, TRAINING_TYPE_CODES)
  443. return this.$t(`agriClassroomPage.trainingSubs.s${idx}`)
  444. },
  445. articleCover(card) {
  446. if (card.coverFileUrl) {
  447. return resolveResourceUrl(card.coverFileUrl)
  448. }
  449. return this.coverSrc
  450. },
  451. onPreviewCover(card) {
  452. const url = this.articleCover(card)
  453. if (!url) return
  454. uni.previewImage({ urls: [url], current: 0 })
  455. },
  456. openCourseDetail(card) {
  457. if (!card) return
  458. if (card.type === 'training') {
  459. const q = [
  460. `id=${encodeURIComponent(card.id)}`,
  461. `date=${encodeURIComponent(card.publishTime || '')}`,
  462. `title=${encodeURIComponent(card.title || '')}`,
  463. `type=${encodeURIComponent(card.typeCode || '')}`,
  464. `enrolled=${encodeURIComponent(card.enrolled)}`,
  465. `total=${encodeURIComponent(card.total)}`,
  466. `introduction=${encodeURIComponent(card.introduction || '')}`,
  467. `trainingTime=${encodeURIComponent(card.trainingTime || '')}`,
  468. `expectedOutcome=${encodeURIComponent(card.expectedOutcome || '')}`,
  469. `cover=${encodeURIComponent(card.coverFileUrl || '')}`,
  470. `regStart=${encodeURIComponent(card.registrationStartTime || '')}`,
  471. `regEnd=${encodeURIComponent(card.registrationEndTime || '')}`
  472. ].join('&')
  473. uni.navigateTo({ url: `${TRAINING_DETAIL_PATH}?${q}` })
  474. return
  475. }
  476. const q = [
  477. `id=${encodeURIComponent(card.id)}`,
  478. `date=${encodeURIComponent(card.publishTime || '')}`,
  479. `title=${encodeURIComponent(card.title || '')}`,
  480. `type=${encodeURIComponent(card.typeCode || '')}`,
  481. `introduction=${encodeURIComponent(card.introduction || '')}`,
  482. `src=${encodeURIComponent(card.contentFileUrl || '')}`,
  483. `cover=${encodeURIComponent(card.coverFileUrl || '')}`,
  484. 'mode=video'
  485. ].join('&')
  486. uni.navigateTo({ url: `${COURSE_DETAIL_PATH}?${q}` })
  487. },
  488. /** 主 Tab 切换:恢复各 Tab 默认子类并重新拉列表 */
  489. onMainTabUpdate(idx) {
  490. const i = typeof idx === 'number' ? idx : 0
  491. if (this.mainTabIndex === i) return
  492. this.mainTabIndex = i
  493. this.vScrollTop = 0
  494. if (i === 0) {
  495. this.selectedVideoType = '005001'
  496. } else {
  497. this.selectedTrainingType = '006002'
  498. }
  499. this.loadCourseList(true)
  500. }
  501. }
  502. }
  503. </script>
  504. <style lang="scss" scoped>
  505. @import '@/styles/morandi.scss';
  506. @import '@/styles/tab-page.scss';
  507. .ac-page {
  508. display: flex;
  509. flex-direction: column;
  510. min-width: 0;
  511. width: 100%;
  512. height: 100%;
  513. min-height: 100%;
  514. overflow: hidden;
  515. box-sizing: border-box;
  516. background: $morandi-bg-page;
  517. }
  518. .ac-top {
  519. flex-shrink: 0;
  520. display: flex;
  521. flex-direction: column;
  522. gap: 16rpx;
  523. min-width: 0;
  524. padding: 20rpx 24rpx 16rpx;
  525. box-sizing: border-box;
  526. background: $morandi-bg-page;
  527. border-bottom: 1rpx solid $morandi-border-soft;
  528. }
  529. .ac-tabs {
  530. width: 100%;
  531. min-width: 0;
  532. }
  533. .ac-chips {
  534. display: flex;
  535. flex-direction: row;
  536. flex-wrap: wrap;
  537. min-width: 0;
  538. justify-content: space-between;
  539. }
  540. .ac-chip {
  541. padding: 2rpx 30rpx;
  542. box-sizing: border-box;
  543. border-radius: 999rpx;
  544. border: 1rpx solid $morandi-border-strong;
  545. background: $morandi-bg-card-inner;
  546. }
  547. .ac-chip--on {
  548. border-color: #22c55e;
  549. background: rgba(34, 197, 94, 0.08);
  550. }
  551. .ac-chip__txt {
  552. font-size: 24rpx;
  553. color: $morandi-text-secondary;
  554. text-align: center;
  555. word-break: break-word;
  556. overflow-wrap: anywhere;
  557. }
  558. .ac-chip--on .ac-chip__txt {
  559. color: #15803d;
  560. font-weight: 600;
  561. }
  562. .ac-body {
  563. flex: 1;
  564. min-height: 0;
  565. height: 0;
  566. min-width: 0;
  567. display: flex;
  568. flex-direction: column;
  569. padding: 16rpx 24rpx 24rpx;
  570. box-sizing: border-box;
  571. overflow: hidden;
  572. }
  573. .ac-list-wrap {
  574. flex: 1;
  575. min-height: 0;
  576. display: flex;
  577. flex-direction: column;
  578. min-width: 0;
  579. }
  580. .ac-vlist {
  581. flex: 1;
  582. min-height: 0;
  583. width: 100%;
  584. height: 100%;
  585. }
  586. .ac-empty {
  587. flex: 1;
  588. display: flex;
  589. align-items: center;
  590. justify-content: center;
  591. min-height: 0;
  592. padding: 48rpx 24rpx;
  593. box-sizing: border-box;
  594. }
  595. .ac-empty__txt {
  596. color: $morandi-text-muted;
  597. text-align: center;
  598. }
  599. .ac-pair-cell {
  600. height: 100%;
  601. box-sizing: border-box;
  602. display: flex;
  603. flex-direction: column;
  604. justify-content: flex-start;
  605. }
  606. /* 一行两列,列间距 20rpx(文档) */
  607. .ac-pair {
  608. display: flex;
  609. flex-direction: row;
  610. align-items: stretch;
  611. gap: 20rpx;
  612. min-width: 0;
  613. box-sizing: border-box;
  614. }
  615. .ac-pair__col {
  616. flex: 1;
  617. min-width: 0;
  618. display: flex;
  619. flex-direction: column;
  620. }
  621. .ac-card {
  622. flex: 1;
  623. min-height: 0;
  624. min-width: 0;
  625. display: flex;
  626. flex-direction: column;
  627. gap: 12rpx;
  628. padding: 12rpx;
  629. box-sizing: border-box;
  630. border-radius: 16rpx;
  631. background: $morandi-bg-card;
  632. border: 1rpx solid $morandi-border;
  633. }
  634. .ac-card__img {
  635. width: 100%;
  636. display: block;
  637. }
  638. .ac-card__title {
  639. font-size: 30rpx;
  640. font-weight: 600;
  641. line-height: 1.4;
  642. color: #111827;
  643. word-break: break-word;
  644. overflow-wrap: anywhere;
  645. }
  646. .ac-card__foot {
  647. display: flex;
  648. flex-direction: row;
  649. flex-wrap: wrap;
  650. align-items: center;
  651. gap: 8rpx 12rpx;
  652. min-width: 0;
  653. }
  654. .ac-card__foot--video {
  655. justify-content: flex-start;
  656. }
  657. .ac-card__foot--train {
  658. justify-content: flex-start;
  659. }
  660. .ac-card__meta {
  661. font-size: 22rpx;
  662. line-height: 1.45;
  663. color: $morandi-accent-soft;
  664. }
  665. .ac-card__meta--muted {
  666. color: $morandi-text-soft;
  667. }
  668. .ac-card__meta--num {
  669. color: $morandi-text-secondary;
  670. }
  671. .ac-page.lang-bo {
  672. .ac-chip__txt {
  673. font-size: 22rpx;
  674. line-height: 1.75;
  675. letter-spacing: 2rpx;
  676. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  677. }
  678. .ac-card__title {
  679. font-size: 28rpx;
  680. line-height: 1.75;
  681. letter-spacing: 2rpx;
  682. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  683. }
  684. .ac-card__meta {
  685. font-size: 20rpx;
  686. line-height: 1.75;
  687. letter-spacing: 2rpx;
  688. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  689. }
  690. }
  691. </style>