西藏巴青项目

index.vue 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. <template>
  2. <!-- 牦牛行情:顶卡主价;中段走势/预测;底虚拟列表每日行情(见 doc/耗牛行情.md) -->
  3. <view :class="pageRootClass" class="tab-page ym-page" :style="pageStyle">
  4. <scroll-view scroll-y class="ym-scroll" enable-back-to-top :show-scrollbar="false" @scrolltolower="onScrollToLower">
  5. <!-- 1. 主行情卡 -->
  6. <up-card :show-head="false" :show-foot="false" :border="true" :border-radius="16" margin="24rpx 24rpx 0" :body-style="{ padding: '24rpx' }">
  7. <template #body>
  8. <view class="ym-hero">
  9. <up-avatar shape="square" text="市" size="56" font-size="22" bg-color="#22c55e" color="#ffffff" />
  10. <view class="ym-hero__right">
  11. <text class="ym-hero__price">{{ heroPriceLine }}</text>
  12. <text class="ym-hero__sub text-body">{{ heroMarketLine }}</text>
  13. </view>
  14. </view>
  15. </template>
  16. </up-card>
  17. <!-- 2. 走势 / 预测 -->
  18. <up-card :show-head="false" :show-foot="false" :border="true" :border-radius="16" margin="20rpx 24rpx 0" :body-style="{ padding: '20rpx' }">
  19. <template #body>
  20. <up-subsection
  21. :list="subsectionList"
  22. :current="mainSectionIdx"
  23. active-color="#22c55e"
  24. inactive-color="#000000"
  25. :bold="true"
  26. @update:current="onMainSectionUpdate"
  27. />
  28. <view class="ym-panel-inner">
  29. <view class="ym-stats3">
  30. <view class="ym-stat-col">
  31. <text class="ym-stat-label text-body">{{ $t('yakMarketPage.statAvg7') }}</text>
  32. <text class="ym-stat-val text-body">{{ stats7.avg }}</text>
  33. <text class="ym-stat-ph text-body">—</text>
  34. </view>
  35. <view class="ym-stat-col">
  36. <text class="ym-stat-label text-body">{{ $t('yakMarketPage.statLow7') }}</text>
  37. <text class="ym-stat-val ym-stat-val--red text-body">{{ stats7.low }}</text>
  38. <text class="ym-stat-date text-body">{{ stats7.lowDate }}</text>
  39. </view>
  40. <view class="ym-stat-col">
  41. <text class="ym-stat-label text-body">{{ $t('yakMarketPage.statHigh7') }}</text>
  42. <text class="ym-stat-val ym-stat-val--red text-body">{{ stats7.high }}</text>
  43. <text class="ym-stat-date text-body">{{ stats7.highDate }}</text>
  44. </view>
  45. </view>
  46. <view class="ym-meta-row">
  47. <text class="ym-meta text-body">{{ $t('yakMarketPage.metaUpdated', { d: stats7.updateYmd }) }}</text>
  48. <text class="ym-meta text-body">{{ metaUnitLine }}</text>
  49. </view>
  50. <!-- 价格走势 -->
  51. <template v-if="mainSectionIdx === 0">
  52. <view class="ym-chart-host">
  53. <mpvueEcharts
  54. ref="yakPriceEcharts"
  55. :onInit="onYakPriceChartInit"
  56. canvasId="yakMkPriceLine"
  57. :disableTouch="true"
  58. />
  59. </view>
  60. <view class="ym-range-chips">
  61. <view
  62. class="ym-chip"
  63. :class="{ 'ym-chip--on': chartRange === '7' }"
  64. role="button"
  65. @tap="chartRange = '7'"
  66. >
  67. <text class="ym-chip__txt text-body">{{ $t('yakMarketPage.range7') }}</text>
  68. </view>
  69. <view
  70. class="ym-chip"
  71. :class="{ 'ym-chip--on': chartRange === '30' }"
  72. role="button"
  73. @tap="chartRange = '30'"
  74. >
  75. <text class="ym-chip__txt text-body">{{ $t('yakMarketPage.range30') }}</text>
  76. </view>
  77. </view>
  78. </template>
  79. <!-- 价格预测 -->
  80. <template v-else>
  81. <text class="ym-forecast-title text-body">{{ $t('yakMarketPage.forecastBlockTitle') }}</text>
  82. <view class="ym-donuts">
  83. <view v-for="ring in forecastRings" :key="ring.key" class="ym-donut-col">
  84. <view class="ym-donut" :style="donutStyle(ring)">
  85. <view class="ym-donut-hole">
  86. <text class="ym-donut-pct">{{ ring.pct }}%</text>
  87. </view>
  88. </view>
  89. <text class="ym-donut-cap text-body">{{ ring.label }}</text>
  90. </view>
  91. </view>
  92. </template>
  93. </view>
  94. </template>
  95. </up-card>
  96. <!-- 3. 每日行情 -->
  97. <text class="ym-section-title text-title">{{ $t('yakMarketPage.dailyTitle') }}</text>
  98. <view class="ym-list-wrap">
  99. <view v-if="listLoading && !dailyRows.length" class="ym-empty">
  100. <text class="text-body ym-empty__txt">{{ $t('yakMarketPage.loading') }}</text>
  101. </view>
  102. <view v-else-if="!dailyRows.length" class="ym-empty">
  103. <text class="text-body ym-empty__txt">{{ $t('yakMarketPage.emptyDaily') }}</text>
  104. </view>
  105. <up-virtual-list
  106. v-else
  107. ref="ymVList"
  108. class="ym-vlist"
  109. :list-data="dailyRows"
  110. :item-height="slotHeightPx"
  111. :height="listHeightPx"
  112. :buffer="4"
  113. key-field="id"
  114. :scroll-top="vScrollTop"
  115. @update:scrollTop="vScrollTop = $event"
  116. >
  117. <template #default="{ item }">
  118. <view class="ym-vcell">
  119. <up-card :show-head="false" :show-foot="false" :border="true" :border-radius="12" margin="0 24rpx 16rpx" :body-style="{ padding: '20rpx' }">
  120. <template #body>
  121. <view class="ym-row1">
  122. <up-avatar shape="square" :text="item.avatarText" size="48" font-size="18" bg-color="#9ca3af" color="#ffffff" />
  123. <view class="ym-row1__right">
  124. <text class="ym-daily-title text-body">{{ item.marketTitle }}</text>
  125. <text class="ym-daily-sub text-body">{{ item.subLine }}</text>
  126. </view>
  127. </view>
  128. <up-divider :hairline="true" line-color="#e5e7eb" text="" />
  129. <view class="ym-row2">
  130. <text class="ym-daily-range-label text-body">{{ item.rangeLabel }}</text>
  131. <text class="ym-daily-range-val text-body">{{ item.rangePrice }}</text>
  132. </view>
  133. <up-divider :hairline="true" line-color="#e5e7eb" text="" />
  134. <text class="ym-daily-detail text-body">{{ $t('yakMarketPage.lineSlaughter') }}{{ item.slaughter }}</text>
  135. <text class="ym-daily-detail text-body">{{ $t('yakMarketPage.lineFactors') }}{{ item.factors }}</text>
  136. <text class="ym-daily-detail text-body">{{ $t('yakMarketPage.lineTrendPred') }}{{ item.trendPred }}</text>
  137. <text class="ym-daily-detail text-body">{{ $t('yakMarketPage.lineSuggest') }}{{ item.suggest }}</text>
  138. </template>
  139. </up-card>
  140. </view>
  141. </template>
  142. </up-virtual-list>
  143. <view v-if="dailyRows.length && (listLoadingMore || listNoMore)" class="ym-list-foot">
  144. <text class="text-body ym-list-foot__txt">{{
  145. listLoadingMore ? $t('yakMarketPage.loadMore') : $t('yakMarketPage.noMore')
  146. }}</text>
  147. </view>
  148. </view>
  149. <view class="ym-footer-spacer" />
  150. </scroll-view>
  151. </view>
  152. </template>
  153. <script>
  154. import * as echarts from '../../uni_modules/mpvue-echarts/components/echarts.esm.min.js'
  155. import mpvueEcharts from '../../uni_modules/mpvue-echarts/components/echarts.vue'
  156. import UCard from 'uview-plus/components/u-card/u-card.vue'
  157. import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
  158. import USubsection from 'uview-plus/components/u-subsection/u-subsection.vue'
  159. import UDivider from 'uview-plus/components/u-divider/u-divider.vue'
  160. import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
  161. import tabPage from '@/mixins/tabPage'
  162. import pageViewport from '@/mixins/pageViewport'
  163. import {
  164. getYakMarketYesterday,
  165. getYakMarketStats7,
  166. getYakMarketTrend,
  167. getYakMarketForecast,
  168. listYakMarketDaily
  169. } from '@/api/yakMarket'
  170. const ROW_SLOT_RPX = 480
  171. const DASH = '—'
  172. function formatPrice(val) {
  173. if (val == null || val === '') return DASH
  174. const n = Number(val)
  175. if (Number.isNaN(n)) return String(val)
  176. return n % 1 === 0 ? String(n) : n.toFixed(2)
  177. }
  178. function formatShortDate(ymd) {
  179. if (!ymd) return ''
  180. const s = String(ymd).slice(0, 10)
  181. const parts = s.split('-')
  182. if (parts.length >= 3) {
  183. return `${parts[1]}-${parts[2]}`.toLowerCase()
  184. }
  185. return s
  186. }
  187. function formatDateTime(val) {
  188. if (!val) return DASH
  189. const s = String(val)
  190. return s.length >= 19 ? s.slice(0, 19) : s.slice(0, 10)
  191. }
  192. function mapTrendPoints(list) {
  193. return (list || []).map((p) => ({
  194. label: p.quoteDateLabel || formatShortDate(p.quoteDate),
  195. price: p.avgPrice != null ? Number(p.avgPrice) : null
  196. }))
  197. }
  198. export default {
  199. components: {
  200. 'up-card': UCard,
  201. 'up-avatar': UAvatar,
  202. 'up-subsection': USubsection,
  203. 'up-divider': UDivider,
  204. 'up-virtual-list': UVirtualList,
  205. mpvueEcharts
  206. },
  207. mixins: [tabPage, pageViewport],
  208. data() {
  209. return {
  210. navTitleKey: 'yakMarketPage.navTitle',
  211. pageLoading: false,
  212. heroPrice: DASH,
  213. heroUnit: '',
  214. heroMarketName: '',
  215. heroDate: '',
  216. mainSectionIdx: 0,
  217. chartRange: '7',
  218. stats7: {
  219. avg: DASH,
  220. low: DASH,
  221. lowDate: DASH,
  222. high: DASH,
  223. highDate: DASH,
  224. updateYmd: DASH,
  225. priceUnitName: ''
  226. },
  227. forecast: { probRise: 0, probFlat: 0, probFall: 0 },
  228. series7: [],
  229. series30: [],
  230. dailyRows: [],
  231. pageNum: 1,
  232. pageSize: 10,
  233. listTotal: 0,
  234. listLoading: false,
  235. listLoadingMore: false,
  236. slotHeightPx: 220,
  237. listHeightPx: 400,
  238. vScrollTop: 0
  239. }
  240. },
  241. computed: {
  242. subsectionList() {
  243. return [
  244. { name: this.$t('yakMarketPage.tabTrend') },
  245. { name: this.$t('yakMarketPage.tabForecast') }
  246. ]
  247. },
  248. heroPriceLine() {
  249. if (this.heroPrice === DASH) {
  250. return this.$t('yakMarketPage.speciesPriceTpl', {
  251. price: DASH,
  252. unit: this.heroUnit || ''
  253. })
  254. }
  255. return this.$t('yakMarketPage.speciesPriceTpl', {
  256. price: this.heroPrice,
  257. unit: this.heroUnit || ''
  258. })
  259. },
  260. heroMarketLine() {
  261. const name = this.heroMarketName || this.$t('yakMarketPage.defaultMarketName')
  262. return this.$t('yakMarketPage.heroSubTpl', {
  263. name,
  264. date: this.heroDate
  265. })
  266. },
  267. metaUnitLine() {
  268. const unit = this.stats7.priceUnitName || this.heroUnit || DASH
  269. return this.$t('yakMarketPage.metaUnitTpl', { unit })
  270. },
  271. forecastRings() {
  272. const f = this.forecast || {}
  273. return [
  274. { key: 'bull', pct: Math.round(Number(f.probRise) || 0), color: '#ef4444', label: this.$t('yakMarketPage.predBull') },
  275. { key: 'flat', pct: Math.round(Number(f.probFlat) || 0), color: '#3b82f6', label: this.$t('yakMarketPage.predFlat') },
  276. { key: 'bear', pct: Math.round(Number(f.probFall) || 0), color: '#22c55e', label: this.$t('yakMarketPage.predBear') }
  277. ]
  278. },
  279. chartSeries() {
  280. return this.chartRange === '7' ? this.series7 : this.series30
  281. },
  282. listNoMore() {
  283. return this.dailyRows.length > 0 && this.dailyRows.length >= this.listTotal
  284. }
  285. },
  286. watch: {
  287. chartRange() {
  288. if (this.mainSectionIdx !== 0) return
  289. this.refreshChart()
  290. }
  291. },
  292. onShow() {
  293. this.loadPageData()
  294. },
  295. onReady() {
  296. this.applyListHeightFallback()
  297. this.$nextTick(() => {
  298. this.$refs.ymVList?.measureContainerHeight?.()
  299. })
  300. },
  301. methods: {
  302. loadPageData() {
  303. this.pageLoading = true
  304. Promise.all([
  305. this.loadYesterday(),
  306. this.loadStats7(),
  307. this.loadTrend(7),
  308. this.loadTrend(30),
  309. this.loadForecast(),
  310. this.loadDailyList(true)
  311. ]).finally(() => {
  312. this.pageLoading = false
  313. })
  314. },
  315. loadYesterday() {
  316. return getYakMarketYesterday()
  317. .then((res) => {
  318. const d = res.data
  319. if (!d) {
  320. this.heroPrice = DASH
  321. this.heroUnit = ''
  322. this.heroDate = ''
  323. return
  324. }
  325. this.heroPrice = formatPrice(d.avgPrice)
  326. this.heroUnit = d.priceUnitName || ''
  327. this.heroDate = formatShortDate(d.quoteDate)
  328. })
  329. .catch(() => {
  330. this.heroPrice = DASH
  331. this.heroUnit = ''
  332. this.heroDate = ''
  333. })
  334. },
  335. loadStats7() {
  336. return getYakMarketStats7()
  337. .then((res) => {
  338. const d = res.data
  339. if (!d) {
  340. this.stats7 = {
  341. avg: DASH,
  342. low: DASH,
  343. lowDate: DASH,
  344. high: DASH,
  345. highDate: DASH,
  346. updateYmd: DASH,
  347. priceUnitName: ''
  348. }
  349. return
  350. }
  351. this.stats7 = {
  352. avg: formatPrice(d.avgPrice7),
  353. low: formatPrice(d.minPrice7),
  354. lowDate: d.minQuoteDateLabel || formatShortDate(d.minQuoteDate) || DASH,
  355. high: formatPrice(d.maxPrice7),
  356. highDate: d.maxQuoteDateLabel || formatShortDate(d.maxQuoteDate) || DASH,
  357. updateYmd: formatDateTime(d.createTime),
  358. priceUnitName: d.priceUnitName || ''
  359. }
  360. })
  361. .catch(() => {})
  362. },
  363. loadTrend(days) {
  364. return getYakMarketTrend({ days })
  365. .then((res) => {
  366. const points = mapTrendPoints(res.data)
  367. if (days === 7) {
  368. this.series7 = points
  369. } else {
  370. this.series30 = points
  371. }
  372. if (this.mainSectionIdx === 0 && String(this.chartRange) === String(days)) {
  373. this.refreshChart()
  374. }
  375. })
  376. .catch(() => {
  377. if (days === 7) {
  378. this.series7 = []
  379. } else {
  380. this.series30 = []
  381. }
  382. })
  383. },
  384. loadForecast() {
  385. return getYakMarketForecast()
  386. .then((res) => {
  387. const d = res.data || {}
  388. this.forecast = {
  389. probRise: d.probRise,
  390. probFlat: d.probFlat,
  391. probFall: d.probFall
  392. }
  393. })
  394. .catch(() => {
  395. this.forecast = { probRise: 0, probFlat: 0, probFall: 0 }
  396. })
  397. },
  398. loadDailyList(reset = false) {
  399. if (!reset) {
  400. if (this.listLoading || this.listLoadingMore || this.listNoMore) {
  401. return Promise.resolve()
  402. }
  403. this.pageNum += 1
  404. this.listLoadingMore = true
  405. } else {
  406. this.pageNum = 1
  407. this.listLoading = true
  408. }
  409. return listYakMarketDaily({ pageNum: this.pageNum, pageSize: this.pageSize })
  410. .then((res) => {
  411. const rows = (res.rows || []).map((row) => this.mapDailyRow(row))
  412. this.listTotal = res.total != null ? Number(res.total) : 0
  413. this.dailyRows = reset ? rows : this.dailyRows.concat(rows)
  414. })
  415. .catch(() => {
  416. if (reset) {
  417. this.dailyRows = []
  418. this.listTotal = 0
  419. } else {
  420. this.pageNum -= 1
  421. }
  422. })
  423. .finally(() => {
  424. if (reset) {
  425. this.listLoading = false
  426. } else {
  427. this.listLoadingMore = false
  428. }
  429. })
  430. },
  431. mapDailyRow(row) {
  432. const unit = row.priceUnitName || ''
  433. const min = formatPrice(row.priceMin)
  434. const max = formatPrice(row.priceMax)
  435. const dateStr = formatShortDate(row.quoteDate)
  436. const marketName = row.tradeMarketName || this.$t('yakMarketPage.defaultMarketName')
  437. const location = row.marketLocation || ''
  438. const subParts = [location, dateStr].filter(Boolean)
  439. const avatarText = marketName.slice(0, 1) || '市'
  440. return {
  441. id: row.id,
  442. avatarText,
  443. marketTitle: marketName,
  444. subLine: subParts.join(' · ') || DASH,
  445. rangeLabel: this.$t('yakMarketPage.dailyRangeLabel'),
  446. rangePrice: min === DASH || max === DASH ? DASH : `[${min}-${max}]${unit}`,
  447. slaughter: row.supplyStatusName || DASH,
  448. factors: row.changeFactorsText || DASH,
  449. trendPred: row.trendPrediction || DASH,
  450. suggest: row.outboundAdvice || DASH
  451. }
  452. },
  453. onScrollToLower() {
  454. this.loadDailyList(false)
  455. },
  456. refreshChart() {
  457. this.$nextTick(() => {
  458. const inst = this.$refs.yakPriceEcharts
  459. if (inst && inst.chart) {
  460. inst.chart.setOption(this.buildMpvueLineOption(), true)
  461. }
  462. })
  463. },
  464. onMainSectionUpdate(idx) {
  465. this.mainSectionIdx = typeof idx === 'number' ? idx : 0
  466. },
  467. donutStyle(ring) {
  468. const p = Math.min(100, Math.max(0, ring.pct))
  469. const rest = 100 - p
  470. return {
  471. background: `conic-gradient(${ring.color} 0% ${p}%, #e8ecf0 ${p}% 100%)`
  472. }
  473. },
  474. onYakPriceChartInit(canvas, width, height) {
  475. const chart = echarts.init(canvas, null, {
  476. width,
  477. height
  478. })
  479. canvas.setChart(chart)
  480. chart.setOption(this.buildMpvueLineOption())
  481. return chart
  482. },
  483. buildMpvueLineOption() {
  484. const data = this.chartSeries
  485. const dates = data.map((d) => d.label)
  486. const prices = data.map((d) => (d.price != null && !Number.isNaN(d.price) ? d.price : null))
  487. const rotate = dates.length > 10 ? 30 : 0
  488. return {
  489. animation: false,
  490. backgroundColor: '#f0f6fb',
  491. color: ['#22c55e'],
  492. grid: {
  493. left: 10,
  494. right: 14,
  495. top: 28,
  496. bottom: 10,
  497. containLabel: true
  498. },
  499. tooltip: { trigger: 'axis' },
  500. xAxis: [
  501. {
  502. type: 'category',
  503. data: dates,
  504. boundaryGap: false,
  505. axisLine: { lineStyle: { color: '#cbd5e1' } },
  506. axisLabel: { color: '#64748b', fontSize: 10, rotate }
  507. }
  508. ],
  509. yAxis: [
  510. {
  511. type: 'value',
  512. scale: true,
  513. splitLine: { lineStyle: { color: '#e2e8f0' } },
  514. axisLabel: { color: '#64748b', fontSize: 10 }
  515. }
  516. ],
  517. series: [
  518. {
  519. type: 'line',
  520. data: prices,
  521. smooth: true,
  522. symbol: 'circle',
  523. symbolSize: 5,
  524. lineStyle: { width: 2, color: '#22c55e' },
  525. itemStyle: { color: '#22c55e' }
  526. }
  527. ]
  528. }
  529. },
  530. applyListHeightFallback() {
  531. const sys = uni.getSystemInfoSync()
  532. const winH = sys.windowHeight || 600
  533. this.listHeightPx = Math.max(280, Math.floor(winH * 0.42))
  534. this.slotHeightPx = Math.ceil(uni.upx2px(ROW_SLOT_RPX))
  535. }
  536. }
  537. }
  538. </script>
  539. <style lang="scss" scoped>
  540. @import '@/styles/morandi.scss';
  541. @import '@/styles/tab-page.scss';
  542. .ym-page {
  543. display: flex;
  544. flex-direction: column;
  545. min-width: 0;
  546. width: 100%;
  547. height: 100%;
  548. min-height: 100%;
  549. overflow: hidden;
  550. box-sizing: border-box;
  551. background: $morandi-bg-page;
  552. }
  553. .ym-scroll {
  554. flex: 1;
  555. min-height: 0;
  556. min-width: 0;
  557. height: 0;
  558. box-sizing: border-box;
  559. }
  560. .ym-hero {
  561. display: flex;
  562. flex-direction: row;
  563. align-items: center;
  564. gap: 20rpx;
  565. min-width: 0;
  566. }
  567. .ym-hero__right {
  568. flex: 1;
  569. min-width: 0;
  570. display: flex;
  571. flex-direction: column;
  572. gap: 8rpx;
  573. }
  574. .ym-hero__price {
  575. font-size: 34rpx;
  576. font-weight: 700;
  577. color: #ef4444;
  578. line-height: 1.35;
  579. word-break: break-word;
  580. }
  581. .ym-hero__sub {
  582. font-size: 24rpx;
  583. color: $morandi-text-secondary;
  584. text-transform: lowercase;
  585. line-height: 1.45;
  586. }
  587. .ym-panel-inner {
  588. margin-top: 20rpx;
  589. padding: 20rpx;
  590. border-radius: 12rpx;
  591. background: #f0f6fb;
  592. box-sizing: border-box;
  593. }
  594. .ym-stats3 {
  595. display: flex;
  596. flex-direction: row;
  597. align-items: stretch;
  598. min-width: 0;
  599. }
  600. .ym-stat-col {
  601. flex: 1;
  602. min-width: 0;
  603. display: flex;
  604. flex-direction: column;
  605. align-items: center;
  606. text-align: center;
  607. gap: 6rpx;
  608. }
  609. .ym-stat-label {
  610. font-size: 24rpx;
  611. color: $morandi-text-secondary;
  612. }
  613. .ym-stat-val {
  614. font-size: 30rpx;
  615. font-weight: 600;
  616. color: $morandi-text;
  617. }
  618. .ym-stat-val--red {
  619. color: #ef4444;
  620. }
  621. .ym-stat-date,
  622. .ym-stat-ph {
  623. font-size: 22rpx;
  624. color: $morandi-text-muted;
  625. }
  626. .ym-meta-row {
  627. display: flex;
  628. flex-direction: row;
  629. justify-content: space-between;
  630. align-items: center;
  631. margin-top: 16rpx;
  632. min-width: 0;
  633. }
  634. .ym-meta {
  635. font-size: 22rpx;
  636. color: $morandi-text-muted;
  637. }
  638. .ym-chart-host {
  639. width: 100%;
  640. height: 360rpx;
  641. margin-top: 12rpx;
  642. box-sizing: border-box;
  643. border-radius: 8rpx;
  644. overflow: hidden;
  645. background: #f0f6fb;
  646. position: relative;
  647. }
  648. .ym-chart-host :deep(.ec-canvas) {
  649. display: block;
  650. width: 100% !important;
  651. height: 100% !important;
  652. max-width: 100%;
  653. }
  654. .ym-range-chips {
  655. display: flex;
  656. flex-direction: row;
  657. justify-content: center;
  658. gap: 20rpx;
  659. margin-top: 16rpx;
  660. }
  661. .ym-chip {
  662. padding: 8rpx 28rpx;
  663. border-radius: 999rpx;
  664. border: 1rpx solid #cbd5e1;
  665. background: #ffffff;
  666. }
  667. .ym-chip--on {
  668. border-color: #22c55e;
  669. background: rgba(34, 197, 94, 0.12);
  670. }
  671. .ym-chip__txt {
  672. font-size: 24rpx;
  673. color: $morandi-text-secondary;
  674. }
  675. .ym-chip--on .ym-chip__txt {
  676. color: #15803d;
  677. font-weight: 600;
  678. }
  679. .ym-forecast-title {
  680. display: block;
  681. font-size: 24rpx;
  682. color: $morandi-text-muted;
  683. margin-bottom: 16rpx;
  684. }
  685. .ym-donuts {
  686. display: flex;
  687. flex-direction: row;
  688. gap: 10rpx;
  689. justify-content: space-between;
  690. min-width: 0;
  691. }
  692. .ym-donut-col {
  693. flex: 1;
  694. min-width: 0;
  695. display: flex;
  696. flex-direction: column;
  697. align-items: center;
  698. gap: 12rpx;
  699. }
  700. .ym-donut {
  701. width: 120rpx;
  702. height: 120rpx;
  703. border-radius: 50%;
  704. position: relative;
  705. box-sizing: border-box;
  706. }
  707. .ym-donut-hole {
  708. position: absolute;
  709. left: 50%;
  710. top: 50%;
  711. transform: translate(-50%, -50%);
  712. width: 72rpx;
  713. height: 72rpx;
  714. border-radius: 50%;
  715. background: #ffffff;
  716. display: flex;
  717. align-items: center;
  718. justify-content: center;
  719. }
  720. .ym-donut-pct {
  721. font-size: 22rpx;
  722. font-weight: 600;
  723. color: #334155;
  724. }
  725. .ym-donut-cap {
  726. font-size: 24rpx;
  727. color: $morandi-text-secondary;
  728. text-align: center;
  729. }
  730. .ym-section-title {
  731. display: block;
  732. margin: 28rpx 24rpx 12rpx;
  733. font-size: 32rpx;
  734. font-weight: 600;
  735. color: $morandi-text;
  736. }
  737. .ym-list-wrap {
  738. min-width: 0;
  739. padding-bottom: 8rpx;
  740. }
  741. .ym-empty {
  742. padding: 48rpx 24rpx;
  743. text-align: center;
  744. }
  745. .ym-empty__txt {
  746. color: $morandi-text-secondary;
  747. }
  748. .ym-list-foot {
  749. padding: 24rpx 0 8rpx;
  750. text-align: center;
  751. }
  752. .ym-list-foot__txt {
  753. color: $morandi-text-secondary;
  754. font-size: 24rpx;
  755. }
  756. .ym-vlist {
  757. width: 100%;
  758. }
  759. .ym-vcell {
  760. height: 100%;
  761. box-sizing: border-box;
  762. }
  763. .ym-row1 {
  764. display: flex;
  765. flex-direction: row;
  766. align-items: center;
  767. gap: 16rpx;
  768. min-width: 0;
  769. }
  770. .ym-row1__right {
  771. flex: 1;
  772. min-width: 0;
  773. display: flex;
  774. flex-direction: column;
  775. gap: 6rpx;
  776. }
  777. .ym-daily-title {
  778. font-size: 28rpx;
  779. font-weight: 600;
  780. color: $morandi-text;
  781. }
  782. .ym-daily-sub {
  783. font-size: 24rpx;
  784. color: $morandi-text-secondary;
  785. text-transform: lowercase;
  786. }
  787. .ym-row2 {
  788. display: flex;
  789. flex-direction: row;
  790. justify-content: space-between;
  791. align-items: center;
  792. min-width: 0;
  793. padding: 4rpx 0;
  794. }
  795. .ym-daily-range-label {
  796. font-size: 26rpx;
  797. color: $morandi-text-secondary;
  798. }
  799. .ym-daily-range-val {
  800. font-size: 28rpx;
  801. font-weight: 600;
  802. color: #ef4444;
  803. }
  804. .ym-daily-detail {
  805. display: block;
  806. font-size: 24rpx;
  807. line-height: 1.55;
  808. color: $morandi-text-muted;
  809. margin-top: 8rpx;
  810. }
  811. .ym-footer-spacer {
  812. height: 40rpx;
  813. }
  814. .ym-page.lang-bo {
  815. .ym-hero__price {
  816. font-size: 30rpx;
  817. line-height: 1.75;
  818. }
  819. .ym-daily-title {
  820. font-size: 26rpx;
  821. line-height: 1.75;
  822. }
  823. }
  824. </style>