西藏巴青项目

index.vue 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. <template>
  2. <!-- 养殖资讯:顶区固定(搜索 + 横向标签 + 分类入口),下区 up-virtual-list;文案走 i18n,根节点随语言 class 以适配藏文 -->
  3. <view :class="pageRootClass" class="tab-page breed-page" :style="pageStyle">
  4. <view class="breed-top">
  5. <up-search
  6. v-model="searchKeyword"
  7. shape="round"
  8. :placeholder="$t('breedingNewsPage.searchPlaceholder')"
  9. :show-action="false"
  10. :clearabled="true"
  11. bg-color="#f5f2ef"
  12. border-color="#e5ded6"
  13. />
  14. <view class="breed-tabs-row">
  15. <up-tabs
  16. :current="tabCurrentIndex"
  17. class="breed-tabs"
  18. :list="tabsList"
  19. key-name="name"
  20. :scrollable="true"
  21. line-color="#22C55E"
  22. :active-style="{ color: '#15803d', fontWeight: '600' }"
  23. :inactive-style="{ color: '#78716c' }"
  24. @update:current="tabCurrentIndex = $event"
  25. @change="onTabsChange"
  26. />
  27. <view class="breed-tabs-extra" role="button" @click="popupVisible = true">
  28. <up-icon name="grid-fill" color="#22C55E" :size="22" />
  29. </view>
  30. </view>
  31. </view>
  32. <view class="breed-body">
  33. <view v-if="listLoading && !articles.length" class="breed-empty">
  34. <text class="text-body breed-empty__txt">{{ $t('breedingNewsPage.loading') }}</text>
  35. </view>
  36. <view v-else-if="!articles.length" class="breed-empty">
  37. <text class="text-body breed-empty__txt">{{ $t('breedingNewsPage.empty') }}</text>
  38. </view>
  39. <view v-else class="breed-list-wrap">
  40. <up-virtual-list
  41. ref="breedVList"
  42. class="breed-vlist"
  43. :list-data="articles"
  44. :item-height="slotHeightPx"
  45. :height="listHeightPx"
  46. :buffer="6"
  47. key-field="id"
  48. :scroll-top="vScrollTop"
  49. @update:scrollTop="vScrollTop = $event"
  50. @scroll="onVirtualListScroll"
  51. >
  52. <template #default="{ item }">
  53. <view class="breed-row-cell">
  54. <view class="breed-row" :style="{ height: rowBodyPx + 'px' }">
  55. <view class="breed-row__left" role="button" @click="openNewsDetail(item)">
  56. <text class="breed-row__title">{{ item.title }}</text>
  57. <text class="text-body breed-row__summary">{{ item.introduction || $t('breedingNewsPage.noIntro') }}</text>
  58. <view class="breed-row__meta">
  59. <text class="breed-row__meta-tag text-body">{{ tabTitle(item.type) }}</text>
  60. <text class="breed-row__meta-date text-body">{{ item.publishTime || $t('breedingNewsPage.noDate') }}</text>
  61. </view>
  62. </view>
  63. <view class="breed-row__right">
  64. <!-- up-lazy-load:进入视区再加载;点击用 uni.previewImage 放大 -->
  65. <up-lazy-load
  66. class="breed-lazy-cover"
  67. role="button"
  68. :image="articleCover(item)"
  69. height="120"
  70. :border-radius="12"
  71. img-mode="aspectFill"
  72. :threshold="200"
  73. :index="item.id"
  74. @click="() => onPreviewCover(item)"
  75. />
  76. </view>
  77. </view>
  78. </view>
  79. </template>
  80. </up-virtual-list>
  81. <view v-if="listLoadingMore || listNoMore" class="breed-list-foot">
  82. <text class="text-body breed-list-foot__txt">
  83. {{ listLoadingMore ? $t('breedingNewsPage.loadingMore') : $t('breedingNewsPage.noMore') }}
  84. </text>
  85. </view>
  86. </view>
  87. </view>
  88. <up-popup
  89. :show="popupVisible"
  90. mode="bottom"
  91. :round="20"
  92. :safe-area-inset-bottom="true"
  93. :close-on-click-overlay="true"
  94. @update:show="popupVisible = $event"
  95. >
  96. <view class="breed-popup">
  97. <view class="breed-popup__handle" />
  98. <scroll-view scroll-y class="breed-popup__scroll" :show-scrollbar="false">
  99. <view v-for="grp in popupGroups" :key="grp.key" class="breed-popup__block">
  100. <text class="breed-popup__group-title text-title">{{ grp.title || $t(grp.titleKey) }}</text>
  101. <view class="breed-popup__chips">
  102. <view
  103. v-for="tid in grp.tabIds"
  104. :key="tid"
  105. class="breed-chip"
  106. :class="{ 'breed-chip--on': selectedTabId === tid }"
  107. role="button"
  108. @click="onPickChip(tid)"
  109. >
  110. <text class="breed-chip__txt text-body">{{ tabTitle(tid) }}</text>
  111. </view>
  112. </view>
  113. </view>
  114. </scroll-view>
  115. </view>
  116. </up-popup>
  117. </view>
  118. </template>
  119. <script>
  120. import USearch from 'uview-plus/components/u-search/u-search.vue'
  121. import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
  122. import UPopup from 'uview-plus/components/u-popup/u-popup.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 { putNewsDetailPayload } from '@/utils/newsDetailCache'
  129. import { listInformationCategoryTree } from '@/api/category/informationCategory'
  130. import { listFarmingNews } from '@/api/farmingNews'
  131. /** 列表分页(接口默认 10,上限 50) */
  132. const LIST_PAGE_SIZE = 20
  133. /** 养殖资讯模块(与资讯类别接口 moduleId 一致) */
  134. const FARMING_NEWS_MODULE_ID = '01'
  135. /** 接口不可用时的静态兜底(visible=1 叶子 code,不含 001013) */
  136. const FALLBACK_TABS = [
  137. { id: '001001', name: '繁育作业' },
  138. { id: '001002', name: '饲养工作' },
  139. { id: '001003', name: '免疫程序' },
  140. { id: '001004', name: '环境调控' },
  141. { id: '001005', name: '饲料配方' },
  142. { id: '001006', name: '牦牛投喂' },
  143. { id: '001007', name: '设备操作' },
  144. { id: '001008', name: '设备保养' },
  145. { id: '001009', name: '设备排障' },
  146. { id: '001010', name: '设备维修' },
  147. { id: '001011', name: '牦牛生长' },
  148. { id: '001012', name: '牦牛出栏' },
  149. { id: '002001', name: '高新技术' },
  150. { id: '002002', name: '农业科技' },
  151. { id: '002003', name: '社会发展' },
  152. { id: '002004', name: '基础研究' },
  153. { id: '003001', name: '涉农政策' },
  154. { id: '003002', name: '产业项目' },
  155. { id: '003003', name: '惠农补贴' },
  156. { id: '003004', name: '共富项目' }
  157. ]
  158. const FALLBACK_POPUP_GROUPS = [
  159. { key: '001', titleKey: 'breedingNewsPage.groupStandard', tabIds: FALLBACK_TABS.slice(0, 12).map((t) => t.id) },
  160. { key: '002', titleKey: 'breedingNewsPage.groupTech', tabIds: FALLBACK_TABS.slice(12, 16).map((t) => t.id) },
  161. { key: '003', titleKey: 'breedingNewsPage.groupPolicy', tabIds: FALLBACK_TABS.slice(16, 20).map((t) => t.id) }
  162. ]
  163. /** 列表行内容区(rpx),不含与下一行的间距;虚拟列表单项高度 = 内容 + 间隔 */
  164. const ROW_BODY_RPX = 260
  165. const ROW_GAP_RPX = 20
  166. const BREED_BODY_PAD_RPX = 40
  167. const COVER = '/static/ai/hero.png'
  168. /** 资讯详情分包路径(英文目录名) */
  169. const NEWS_DETAIL_PATH = '/package-a/news-detail/index'
  170. export default {
  171. components: {
  172. 'up-search': USearch,
  173. 'up-tabs': UTabs,
  174. 'up-popup': UPopup,
  175. 'up-icon': UIcon,
  176. 'up-lazy-load': ULazyLoad,
  177. 'up-virtual-list': UVirtualList
  178. },
  179. mixins: [tabPage],
  180. data() {
  181. return {
  182. navTitleKey: 'breedingNewsPage.navTitle',
  183. searchKeyword: '',
  184. tabCurrentIndex: 0,
  185. popupVisible: false,
  186. categoryTabs: [],
  187. popupGroupsData: [],
  188. articles: [],
  189. listLoading: false,
  190. listLoadingMore: false,
  191. listTotal: 0,
  192. pageNum: 1,
  193. pageSize: LIST_PAGE_SIZE,
  194. _searchTimer: null,
  195. loadMoreTimer: null,
  196. rowBodyPx: 120,
  197. marginPx: 10,
  198. slotHeightPx: 130,
  199. listHeightPx: 400,
  200. pageHeightPx: 0,
  201. vScrollTop: 0,
  202. coverSrc: COVER
  203. }
  204. },
  205. computed: {
  206. /** u-tabs:id 为资讯类别叶子 code,name 为接口 name */
  207. tabsList() {
  208. return this.categoryTabs.length ? this.categoryTabs : FALLBACK_TABS
  209. },
  210. selectedTabId() {
  211. const row = this.tabsList[this.tabCurrentIndex]
  212. return row ? row.id : ''
  213. },
  214. popupGroups() {
  215. return this.popupGroupsData.length ? this.popupGroupsData : FALLBACK_POPUP_GROUPS
  216. },
  217. /** 已加载条数 ≥ total 时无更多 */
  218. listNoMore() {
  219. return this.listTotal > 0 && this.articles.length >= this.listTotal
  220. },
  221. pageStyle() {
  222. if (this.pageHeightPx > 0) {
  223. return { height: `${this.pageHeightPx}px` }
  224. }
  225. return {}
  226. }
  227. },
  228. watch: {
  229. searchKeyword() {
  230. this.vScrollTop = 0
  231. clearTimeout(this._searchTimer)
  232. this._searchTimer = setTimeout(() => {
  233. this.loadNewsList(true)
  234. }, 300)
  235. },
  236. tabCurrentIndex() {
  237. this.vScrollTop = 0
  238. this.loadNewsList(true)
  239. },
  240. listLoadingMore() {
  241. this.$nextTick(() => this.calcLayoutHeights())
  242. },
  243. listNoMore() {
  244. this.$nextTick(() => this.calcLayoutHeights())
  245. }
  246. },
  247. created() {
  248. this.loadCategoryTabs().finally(() => {
  249. this.loadNewsList(true)
  250. })
  251. },
  252. onReady() {
  253. try {
  254. this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
  255. this.rowBodyPx = Math.max(100, Math.ceil(uni.upx2px(ROW_BODY_RPX)))
  256. this.slotHeightPx = this.rowBodyPx + this.marginPx
  257. } catch (e) {
  258. this.marginPx = 10
  259. this.rowBodyPx = 120
  260. this.slotHeightPx = 130
  261. }
  262. this.applyListHeightFallback()
  263. this.$nextTick(() => {
  264. this.calcLayoutHeights()
  265. })
  266. },
  267. onShow() {
  268. this.$nextTick(() => this.calcLayoutHeights())
  269. },
  270. onUnload() {
  271. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  272. if (this._searchTimer) clearTimeout(this._searchTimer)
  273. },
  274. methods: {
  275. loadCategoryTabs() {
  276. return listInformationCategoryTree({ moduleId: FARMING_NEWS_MODULE_ID })
  277. .then((res) => {
  278. const tree = res.data || []
  279. const tabs = []
  280. const groups = []
  281. tree.forEach((root) => {
  282. const children = root.children || []
  283. groups.push({
  284. key: root.code,
  285. title: root.name,
  286. tabIds: children.map((c) => c.code)
  287. })
  288. children.forEach((child) => {
  289. tabs.push({ id: child.code, name: child.name })
  290. })
  291. })
  292. this.categoryTabs = tabs
  293. this.popupGroupsData = groups
  294. if (this.tabCurrentIndex >= tabs.length) {
  295. this.tabCurrentIndex = 0
  296. }
  297. })
  298. .catch(() => {
  299. this.categoryTabs = []
  300. this.popupGroupsData = []
  301. })
  302. .finally(() => {
  303. this.$nextTick(() => this.calcLayoutHeights())
  304. })
  305. },
  306. mapNewsRow(row, index) {
  307. const type = row.type || ''
  308. const publishTime = row.publishTime || ''
  309. const title = row.title || ''
  310. return {
  311. id: `${type}-${publishTime}-${index}-${title}`,
  312. title,
  313. introduction: row.introduction || '',
  314. type,
  315. coverFileUrl: row.coverFileUrl || '',
  316. contentFileUrl: row.contentFileUrl || '',
  317. publishTime
  318. }
  319. },
  320. loadNewsList(reset = false) {
  321. const type = this.selectedTabId
  322. if (!type) {
  323. this.articles = []
  324. this.listTotal = 0
  325. return Promise.resolve()
  326. }
  327. if (!reset) {
  328. if (this.listLoading || this.listLoadingMore || this.listNoMore) {
  329. return Promise.resolve()
  330. }
  331. this.pageNum += 1
  332. this.listLoadingMore = true
  333. } else {
  334. this.pageNum = 1
  335. this.listLoading = true
  336. }
  337. const params = {
  338. type,
  339. pageNum: this.pageNum,
  340. pageSize: this.pageSize
  341. }
  342. const titleKw = (this.searchKeyword || '').trim()
  343. if (titleKw) {
  344. params.title = titleKw
  345. }
  346. const baseIndex = reset ? 0 : this.articles.length
  347. return listFarmingNews(params)
  348. .then((res) => {
  349. const rows = res.rows || []
  350. this.listTotal = res.total != null ? Number(res.total) : 0
  351. const mapped = rows.map((row, i) => this.mapNewsRow(row, baseIndex + i))
  352. this.articles = reset ? mapped : this.articles.concat(mapped)
  353. this.$nextTick(() => {
  354. this.calcLayoutHeights()
  355. })
  356. })
  357. .catch(() => {
  358. if (reset) {
  359. this.articles = []
  360. this.listTotal = 0
  361. } else {
  362. this.pageNum -= 1
  363. }
  364. })
  365. .finally(() => {
  366. if (reset) {
  367. this.listLoading = false
  368. } else {
  369. this.listLoadingMore = false
  370. }
  371. })
  372. },
  373. /** 首屏内容未填满可视区时自动补页 */
  374. tryAutoLoadMore() {
  375. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  376. const totalH = this.articles.length * this.slotHeightPx
  377. const viewH = this.listHeightPx
  378. if (totalH > 0 && totalH <= viewH) {
  379. this.loadNewsList(false)
  380. }
  381. },
  382. /**
  383. * up-virtual-list scroll:接近底部时 pageNum++ 加载下一页
  384. * 回调为 scrollTop 数值(非原生 event)
  385. */
  386. onVirtualListScroll(scrollTop) {
  387. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  388. this.loadMoreTimer = setTimeout(() => {
  389. this.loadMoreTimer = null
  390. this.tryLoadMoreOnScroll(typeof scrollTop === 'number' ? scrollTop : 0)
  391. }, 180)
  392. },
  393. tryLoadMoreOnScroll(scrollTop) {
  394. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  395. const totalH = this.articles.length * this.slotHeightPx
  396. const viewH = this.listHeightPx
  397. const threshold = Math.max(this.slotHeightPx * 2, 120)
  398. if (totalH <= viewH) {
  399. this.loadNewsList(false)
  400. return
  401. }
  402. if (scrollTop + viewH >= totalH - threshold) {
  403. this.loadNewsList(false)
  404. }
  405. },
  406. applyListHeightFallback() {
  407. const contentH = this.getViewportContentHeight()
  408. const topFallback = Math.ceil(uni.upx2px(220))
  409. const bodyPad = Math.ceil(uni.upx2px(BREED_BODY_PAD_RPX))
  410. this.pageHeightPx = contentH
  411. this.listHeightPx = Math.max(240, contentH - topFallback - bodyPad)
  412. },
  413. getNavigationBarHeight() {
  414. const sys = uni.getSystemInfoSync()
  415. const statusBar = sys.statusBarHeight || 0
  416. // #ifdef MP-WEIXIN
  417. try {
  418. const menu = uni.getMenuButtonBoundingClientRect()
  419. if (menu && menu.height) {
  420. return statusBar + (menu.top - statusBar) * 2 + menu.height
  421. }
  422. } catch (e) {
  423. /* noop */
  424. }
  425. // #endif
  426. return statusBar + 44
  427. },
  428. getViewportContentHeight() {
  429. const sys = uni.getSystemInfoSync()
  430. const navH = this.getNavigationBarHeight()
  431. // #ifdef H5
  432. if (typeof window !== 'undefined' && window.innerHeight) {
  433. return Math.max(320, window.innerHeight - navH)
  434. }
  435. // #endif
  436. const screenH = sys.screenHeight || sys.windowHeight || 600
  437. return Math.max(320, screenH - navH)
  438. },
  439. calcLayoutHeights() {
  440. const contentH = this.getViewportContentHeight()
  441. this.pageHeightPx = contentH
  442. const applyListHeight = (bodyH) => {
  443. let listH = Math.max(240, Math.floor(bodyH))
  444. if (!this.articles.length) {
  445. this.listHeightPx = listH
  446. return
  447. }
  448. uni.createSelectorQuery()
  449. .in(this)
  450. .select('.breed-list-foot')
  451. .boundingClientRect((footRect) => {
  452. if (footRect && footRect.height > 0) {
  453. listH -= Math.ceil(footRect.height)
  454. }
  455. this.listHeightPx = Math.max(240, listH)
  456. this.$nextTick(() => {
  457. this.$refs.breedVList?.measureContainerHeight?.()
  458. this.tryAutoLoadMore()
  459. })
  460. })
  461. .exec()
  462. }
  463. uni.createSelectorQuery()
  464. .in(this)
  465. .select('.breed-body')
  466. .boundingClientRect((bodyRect) => {
  467. if (bodyRect && bodyRect.height > 0) {
  468. applyListHeight(bodyRect.height)
  469. return
  470. }
  471. uni.createSelectorQuery()
  472. .in(this)
  473. .select('.breed-top')
  474. .boundingClientRect((topRect) => {
  475. const topH =
  476. topRect && topRect.height > 0 ? Math.ceil(topRect.height) : Math.ceil(uni.upx2px(220))
  477. const bodyPad = Math.ceil(uni.upx2px(BREED_BODY_PAD_RPX))
  478. applyListHeight(Math.max(240, contentH - topH - bodyPad))
  479. })
  480. .exec()
  481. })
  482. .exec()
  483. },
  484. tabTitle(tabId) {
  485. const row = this.tabsList.find((t) => t.id === tabId)
  486. return row ? row.name : String(tabId || '')
  487. },
  488. /** 列表项封面 */
  489. articleCover(item) {
  490. if (item.coverFileUrl) {
  491. return resolveResourceUrl(item.coverFileUrl)
  492. }
  493. return this.coverSrc
  494. },
  495. /** 点击缩略图:系统预览放大 */
  496. onPreviewCover(item) {
  497. const url = this.articleCover(item)
  498. if (!url) return
  499. uni.previewImage({
  500. urls: [url],
  501. current: 0
  502. })
  503. },
  504. /** 进入资讯详情(英文路径页),携带 id / 日期 / 序号供详情展示 */
  505. openNewsDetail(item) {
  506. const payload = {
  507. title: item.title || '',
  508. introduction: item.introduction || '',
  509. type: item.type || '',
  510. typeLabel: this.tabTitle(item.type),
  511. coverFileUrl: item.coverFileUrl || '',
  512. contentFileUrl: item.contentFileUrl || '',
  513. publishTime: item.publishTime || '',
  514. listKind: 'breeding'
  515. }
  516. const cacheKey = putNewsDetailPayload(payload)
  517. let url = `${NEWS_DETAIL_PATH}?kind=breeding`
  518. if (cacheKey) {
  519. url += `&cacheKey=${encodeURIComponent(cacheKey)}`
  520. } else {
  521. url += [
  522. `&title=${encodeURIComponent(payload.title)}`,
  523. `&date=${encodeURIComponent(payload.publishTime)}`,
  524. `&type=${encodeURIComponent(payload.type)}`,
  525. `&typeLabel=${encodeURIComponent(payload.typeLabel)}`,
  526. `&introduction=${encodeURIComponent(payload.introduction)}`,
  527. `&coverFileUrl=${encodeURIComponent(payload.coverFileUrl)}`,
  528. `&contentFileUrl=${encodeURIComponent(payload.contentFileUrl)}`
  529. ].join('')
  530. }
  531. uni.navigateTo({ url })
  532. },
  533. onTabsChange(_item, index) {
  534. this.tabCurrentIndex = typeof index === 'number' ? index : this.tabCurrentIndex
  535. },
  536. /** 弹层中点选子类:与顶部 tabs 同一套 id */
  537. onPickChip(tabId) {
  538. const idx = this.tabsList.findIndex((t) => t.id === tabId)
  539. if (idx >= 0) this.tabCurrentIndex = idx
  540. this.popupVisible = false
  541. this.vScrollTop = 0
  542. }
  543. }
  544. }
  545. </script>
  546. <style lang="scss" scoped>
  547. @import '@/styles/morandi.scss';
  548. @import '@/styles/tab-page.scss';
  549. .breed-page {
  550. display: flex;
  551. flex-direction: column;
  552. min-width: 0;
  553. width: 100%;
  554. height: 100%;
  555. min-height: 100%;
  556. overflow: hidden;
  557. box-sizing: border-box;
  558. background: $morandi-bg-page;
  559. }
  560. .breed-top {
  561. flex-shrink: 0;
  562. display: flex;
  563. flex-direction: column;
  564. gap: 20rpx;
  565. min-width: 0;
  566. padding: 20rpx 24rpx 16rpx;
  567. box-sizing: border-box;
  568. background: $morandi-bg-page;
  569. border-bottom: 1rpx solid $morandi-border-soft;
  570. }
  571. .breed-tabs-row {
  572. display: flex;
  573. flex-direction: row;
  574. align-items: stretch;
  575. min-width: 0;
  576. gap: 12rpx;
  577. }
  578. .breed-tabs {
  579. flex: 1;
  580. min-width: 0;
  581. }
  582. .breed-tabs-extra {
  583. flex-shrink: 0;
  584. display: flex;
  585. flex-direction: column;
  586. align-items: center;
  587. justify-content: center;
  588. gap: 4rpx;
  589. padding: 8rpx 12rpx;
  590. border-radius: 16rpx;
  591. background: $morandi-bg-card-inner;
  592. border: 1rpx solid $morandi-border;
  593. box-sizing: border-box;
  594. }
  595. .breed-tabs-extra__txt {
  596. font-size: 20rpx;
  597. color: $morandi-text-muted;
  598. text-align: center;
  599. max-width: 88rpx;
  600. word-break: break-word;
  601. overflow-wrap: anywhere;
  602. }
  603. .breed-body {
  604. // flex: 1;
  605. min-height: 0;
  606. height: calc(100vh - 100px);
  607. min-width: 0;
  608. display: flex;
  609. flex-direction: column;
  610. padding: 16rpx 24rpx 24rpx;
  611. box-sizing: border-box;
  612. overflow: hidden;
  613. }
  614. .breed-list-wrap {
  615. flex: 1;
  616. min-height: 0;
  617. display: flex;
  618. flex-direction: column;
  619. min-width: 0;
  620. }
  621. .breed-vlist {
  622. flex: 1;
  623. min-height: 0;
  624. width: 100%;
  625. height: 100%;
  626. }
  627. .breed-list-foot {
  628. flex-shrink: 0;
  629. display: flex;
  630. align-items: center;
  631. justify-content: center;
  632. padding: 16rpx 0 8rpx;
  633. box-sizing: border-box;
  634. }
  635. .breed-list-foot__txt {
  636. font-size: 24rpx;
  637. color: $morandi-text-muted;
  638. text-align: center;
  639. }
  640. .breed-empty {
  641. flex: 1;
  642. display: flex;
  643. align-items: center;
  644. justify-content: center;
  645. min-height: 0;
  646. padding: 48rpx 24rpx;
  647. box-sizing: border-box;
  648. }
  649. .breed-empty__txt {
  650. color: $morandi-text-muted;
  651. text-align: center;
  652. }
  653. .breed-row-cell {
  654. height: 100%;
  655. box-sizing: border-box;
  656. display: flex;
  657. flex-direction: column;
  658. justify-content: flex-start;
  659. }
  660. .breed-row {
  661. display: flex;
  662. flex-direction: row;
  663. align-items: stretch;
  664. gap: 20rpx;
  665. min-width: 0;
  666. padding: 20rpx;
  667. box-sizing: border-box;
  668. border-radius: 16rpx;
  669. background: $morandi-bg-card;
  670. border: 1rpx solid $morandi-border;
  671. flex-shrink: 0;
  672. }
  673. .breed-row__left {
  674. flex: 1;
  675. min-width: 0;
  676. display: flex;
  677. flex-direction: column;
  678. gap: 12rpx;
  679. }
  680. .breed-row__title {
  681. font-size: 32rpx;
  682. font-weight: 600;
  683. line-height: 1.45;
  684. color: $morandi-text;
  685. word-break: break-word;
  686. overflow-wrap: anywhere;
  687. }
  688. .breed-row__summary {
  689. font-size: 24rpx;
  690. line-height: 1.55;
  691. color: $morandi-text-secondary;
  692. word-break: break-word;
  693. overflow-wrap: anywhere;
  694. }
  695. .breed-row__meta {
  696. display: flex;
  697. flex-direction: row;
  698. flex-wrap: wrap;
  699. align-items: center;
  700. gap: 12rpx 20rpx;
  701. min-width: 0;
  702. }
  703. .breed-row__meta-tag {
  704. font-size: 22rpx;
  705. color: $morandi-accent-soft;
  706. }
  707. .breed-row__meta-date {
  708. font-size: 22rpx;
  709. color: $morandi-text-soft;
  710. }
  711. .breed-row__right {
  712. flex-shrink: 0;
  713. align-self: center;
  714. width: 160rpx;
  715. min-width: 0;
  716. }
  717. .breed-lazy-cover {
  718. width: 100%;
  719. display: block;
  720. }
  721. /* 藏文:标题/摘要略小字号、行高与字间距(与全站 app-lang 协调) */
  722. .breed-page.lang-bo {
  723. .breed-row__title {
  724. font-size: 30rpx;
  725. line-height: 1.75;
  726. letter-spacing: 2rpx;
  727. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  728. }
  729. .breed-row__summary {
  730. font-size: 22rpx;
  731. line-height: 1.75;
  732. letter-spacing: 2rpx;
  733. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  734. }
  735. .breed-row__meta-tag,
  736. .breed-row__meta-date {
  737. font-size: 20rpx;
  738. line-height: 1.75;
  739. letter-spacing: 2rpx;
  740. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  741. }
  742. .breed-tabs-extra__txt {
  743. font-size: 18rpx;
  744. line-height: 1.75;
  745. letter-spacing: 2rpx;
  746. }
  747. .breed-chip__txt {
  748. font-size: 20rpx;
  749. line-height: 1.75;
  750. letter-spacing: 2rpx;
  751. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  752. }
  753. }
  754. .breed-popup {
  755. display: flex;
  756. flex-direction: column;
  757. min-width: 0;
  758. max-height: 72vh;
  759. padding: 12rpx 24rpx 24rpx;
  760. box-sizing: border-box;
  761. }
  762. .breed-popup__handle {
  763. align-self: center;
  764. width: 72rpx;
  765. height: 8rpx;
  766. border-radius: 999rpx;
  767. background: $morandi-border-strong;
  768. margin-bottom: 16rpx;
  769. }
  770. .breed-popup__scroll {
  771. flex: 1;
  772. min-height: 0;
  773. max-height: 68vh;
  774. }
  775. .breed-popup__block {
  776. display: flex;
  777. flex-direction: column;
  778. gap: 16rpx;
  779. margin-bottom: 28rpx;
  780. min-width: 0;
  781. }
  782. .breed-popup__group-title {
  783. color: $morandi-text;
  784. word-break: break-word;
  785. overflow-wrap: anywhere;
  786. }
  787. .breed-popup__chips {
  788. display: grid;
  789. grid-template-columns: repeat(4, minmax(0, 1fr));
  790. gap: 12rpx;
  791. min-width: 0;
  792. }
  793. .breed-chip {
  794. min-width: 0;
  795. padding: 14rpx 8rpx;
  796. box-sizing: border-box;
  797. border-radius: 12rpx;
  798. border: 1rpx solid $morandi-border-strong;
  799. background: $morandi-bg-card-inner;
  800. display: flex;
  801. align-items: center;
  802. justify-content: center;
  803. }
  804. .breed-chip--on {
  805. border-color: #22c55e;
  806. background: rgba(34, 197, 94, 0.08);
  807. }
  808. .breed-chip__txt {
  809. font-size: 22rpx;
  810. color: $morandi-text-secondary;
  811. text-align: center;
  812. word-break: break-word;
  813. overflow-wrap: anywhere;
  814. }
  815. .breed-chip--on .breed-chip__txt {
  816. color: #15803d;
  817. font-weight: 600;
  818. }
  819. </style>