西藏巴青项目

index.vue 21KB

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