| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812 |
- <template>
- <div class="screen-page ts-page" :class="{ 'is-loading': loading }">
- <div v-if="loadError" class="ts-error" @click="onRetry">
- {{ loadError }}(点击重试)
- </div>
- <!-- <div class="ts-year-bar">
- <label class="ts-year-bar__label">统计年份</label>
- <select
- v-model="statYear"
- class="ts-year-bar__select"
- :disabled="loading"
- @change="onYearChange"
- >
- <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
- </select>
- <span v-if="statDate" class="ts-year-bar__date">统计日 {{ statDate }}</span>
- <button
- type="button"
- class="ts-year-bar__refresh"
- :disabled="loading || refreshing"
- @click="onManualRefresh"
- >
- {{ refreshing ? '刷新中…' : '刷新' }}
- </button>
- </div> -->
- <!-- 左栏:牦牛交易 -->
- <div class="screen-page--home-column">
- <div class="screen-page--home-column-top">
- <div class="top_title">交易总览 ཚོང་འདོན་ཀྱི་རྒྱུན་བསྡུས།</div>
- <div class="ts-overview-grid">
- <div
- v-for="item in tradeOverviewItems"
- :key="item.key"
- class="ts-overview-cell"
- :class="{ 'ts-overview-cell--placeholder': item.placeholder }"
- >
- <div class="content_title"><div>{{ item.label }}</div></div>
- <div class="content_num">
- <strong v-if="item.placeholder" class="content_num__value">—</strong>
- <ScreenScrollNumber
- v-else
- class="content_num__value"
- :value="item.rawValue"
- :format="item.format"
- />
- <span v-if="item.unit" class="content_num__unit">{{ item.unit }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="screen-page--home-column-flex">
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">产地行情 ཐོག་མའི་གནས་ཚད།</div>
- <div class="flex_content flex_content--quote">
- <div class="quote-summary">
- <div class="quote-summary__item">
- <div class="quote-summary__label">7日均价</div>
- <div class="quote-summary__val">
- <strong>{{ formatPrice(originQuote?.avgPrice7d) }}</strong>
- <span class="quote-summary__unit">{{ originQuote?.priceUnitLabel || '' }}</span>
- </div>
- </div>
- <div class="quote-summary__item">
- <div class="quote-summary__label">7日最低价</div>
- <div class="quote-summary__val">
- <strong>{{ formatPrice(originQuote?.minPrice7d) }}</strong>
- </div>
- </div>
- <div class="quote-summary__item">
- <div class="quote-summary__label">7日最高价</div>
- <div class="quote-summary__val">
- <strong>{{ formatPrice(originQuote?.maxPrice7d) }}</strong>
- </div>
- </div>
- <div class="quote-summary__item">
- <div class="quote-summary__label">最近行情日</div>
- <div class="quote-summary__val quote-summary__val--date">
- <strong>{{ originQuote?.lastQuoteDate || '—' }}</strong>
- </div>
- </div>
- </div>
- <div class="quote-chart">
- <ScreenChart :option="originQuoteChartOption" />
- </div>
- </div>
- </div>
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">牦牛交易数额趋势 གཡག་ཚོང་གི་འབོར་གྱི་འགྲོ་སྟངས།</div>
- <div class="flex_content">
- <ScreenChart :option="monthlyTrendChartOption" />
- </div>
- </div>
- </div>
- <div class="screen-page--home-column-flex">
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">牦牛销售去向 གཡག་ཚོང་འདོན་གྱི་ཕྱིར་སྐྱོད།</div>
- <div class="flex_content">
- <ScreenChart :option="destinationChartOption" />
- </div>
- </div>
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">品质等级占比 སྤུན་ཤེས་རྩིས་ཀྱི་ཆ་འདུམ།</div>
- <div class="flex_content">
- <ScreenChart :option="qualityGradeChartOption" />
- </div>
- </div>
- </div>
- </div>
- <!-- 右栏:农资商城 -->
- <div class="screen-page--home-column" :class="{ 'ts-mall-unavailable': !mallStatsAvailable }">
- <div class="screen-page--home-column-right ts-mall-panel">
- <div class="top_title">
- 农资品类销售 ཞིང་ལས་རྒྱུ་ཆ་རིགས་ཀྱི་ཚོང་འདོན།
- </div>
- <div class="ts-mall-top">
- <div class="ts-mall-top__pie">
- <ScreenChart :option="categorySalesChartOption" />
- </div>
- <div class="ts-mall-top__hot">
- <div class="ts-mall-hot-title">热销农资 Top5</div>
- <ul v-if="hotCategoryItems.length" class="ts-mall-hot-list">
- <li v-for="item in hotCategoryItems" :key="item.rank" class="ts-mall-hot-list__row">
- <span class="ts-mall-hot-list__rank">{{ item.rank }}</span>
- <span class="ts-mall-hot-list__name" :title="item.categoryName">{{ item.categoryName }}</span>
- <span class="ts-mall-hot-list__qty">{{ formatNum(item.qty) }} 件</span>
- </li>
- </ul>
- <span v-else class="ts-placeholder ts-placeholder--sm">{{ mallEmptyHint }}</span>
- </div>
- </div>
- </div>
- <div class="screen-page--home-column-flex">
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">商城订单趋势 ཚོང་ཁང་གི་བཀོད་རྩིས་འགྲོ་སྟངས།</div>
- <div class="flex_content">
- <ScreenChart :option="mallOrderTrendChartOption" />
- </div>
- </div>
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">店铺入驻 ཚོང་ཁང་གི་ཞུགས་སྒོ།</div>
- <div class="flex_content">
- <ScreenChart :option="shopEntryChartOption" />
- </div>
- </div>
- </div>
- <div class="screen-page--home-column-flex">
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">消费区域排名(前5) ཟས་སྤྱོད་ས་ཁུལ་གོ་རྒྱ་(གོང་ལྔ)</div>
- <div class="flex_content">
- <ScreenChart :option="regionRankChartOption" />
- </div>
- </div>
- <div class="screen-page--home-column-flex-item">
- <div class="flex_title">消费者评价 སྤྱོད་མིའི་དཔྱད་པ།</div>
- <div class="flex_content">
- <ScreenChart :option="reviewWordCloudChartOption" />
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, onMounted, onUnmounted, ref } from 'vue'
- import ScreenChart from '@/components/ScreenChart.vue'
- import ScreenScrollNumber from '@/components/ScreenScrollNumber.vue'
- import { getTradeSalesDashboard } from '@/api/tradeSales'
- import {
- buildCategorySalesPieOption,
- buildMallOrderTrendOption,
- buildOriginQuoteOption,
- buildQualityGradePieOption,
- buildRegionRankBarOption,
- buildReviewWordCloudOption,
- buildSalesDestinationBarOption,
- buildShopEntryPieOption,
- buildTradeMonthlyOption
- } from './chartOptions'
- /** 自动轮播刷新周期(对齐功能需求 §7.3,默认 3 分钟) */
- const DASHBOARD_REFRESH_MS = 3 * 60 * 1000
- const loading = ref(false)
- const refreshing = ref(false)
- const loadError = ref('')
- const statYear = ref(String(new Date().getFullYear()))
- const statDate = ref('')
- const availableYears = ref([new Date().getFullYear()])
- const tradeOverview = ref(null)
- const originQuote = ref(null)
- const tradeMonthlyTrend = ref([])
- const salesDestination = ref([])
- const qualityGrade = ref(null)
- const mallStatsAvailable = ref(false)
- const categorySales = ref(null)
- const hotCategoryRank = ref(null)
- const mallOrderTrend = ref(null)
- const shopEntry = ref(null)
- const regionRank = ref(null)
- const reviewWordCloud = ref(null)
- let dashboardTimer = null
- const mallAvailable = computed(() => mallStatsAvailable.value)
- const originQuoteChartOption = computed(() => buildOriginQuoteOption(originQuote.value))
- const monthlyTrendChartOption = computed(() => buildTradeMonthlyOption(tradeMonthlyTrend.value))
- const destinationChartOption = computed(() => buildSalesDestinationBarOption(salesDestination.value))
- const qualityGradeChartOption = computed(() => buildQualityGradePieOption(qualityGrade.value))
- const categorySalesChartOption = computed(() =>
- buildCategorySalesPieOption(categorySales.value, mallAvailable.value)
- )
- const mallOrderTrendChartOption = computed(() =>
- buildMallOrderTrendOption(mallOrderTrend.value, mallAvailable.value)
- )
- const shopEntryChartOption = computed(() => buildShopEntryPieOption(shopEntry.value, mallAvailable.value))
- const regionRankChartOption = computed(() => buildRegionRankBarOption(regionRank.value, mallAvailable.value))
- const reviewWordCloudChartOption = computed(() =>
- buildReviewWordCloudOption(reviewWordCloud.value, mallAvailable.value)
- )
- const hotCategoryItems = computed(() => {
- const items = hotCategoryRank.value?.items || []
- return [...items].sort((a, b) => (a.rank || 0) - (b.rank || 0))
- })
- const mallEmptyHint = computed(() => (mallStatsAvailable.value ? '暂无热销数据' : '商城统计未接入'))
- /** 右栏品类/热销标题:展示统计年 Y(对齐 v1.2 categorySales.statYear) */
- const categoryStatYearLabel = computed(() => {
- const y =
- categorySales.value?.statYear ??
- hotCategoryRank.value?.statYear ??
- statYear.value
- if (y === null || y === undefined || y === '') {
- return ''
- }
- return `${y}年`
- })
- const tradeOverviewItems = computed(() => {
- const o = tradeOverview.value
- return [
- {
- key: 'orderCount',
- label: '牦牛交易订单数',
- unit: '单',
- rawValue: o?.orderCount,
- format: 'int',
- placeholder: false
- },
- {
- key: 'tradeHeads',
- label: '牦牛交易量',
- unit: '头',
- rawValue: o?.tradeHeads,
- format: 'int',
- placeholder: false
- },
- {
- key: 'tradeAmount',
- label: '牦牛交易总额',
- unit: '元',
- rawValue: o?.tradeAmount,
- format: 'money',
- placeholder: false
- },
- {
- key: 'supplierCount',
- label: '入驻商铺数',
- unit: '家',
- rawValue: o?.supplierCount,
- format: 'int',
- placeholder: false
- },
- {
- key: 'agriOrderCount',
- label: '农资订单量',
- unit: '单',
- rawValue: o?.agriOrderCount,
- format: 'int',
- placeholder: o?.agriOrderCount == null
- },
- {
- key: 'agriSalesAmount',
- label: '农资销售额',
- unit: '元',
- rawValue: o?.agriSalesAmount,
- format: 'money',
- placeholder: o?.agriSalesAmount == null
- }
- ]
- })
- function formatNum(val) {
- if (val === null || val === undefined || val === '') {
- return '0'
- }
- const n = Number(val)
- if (Number.isNaN(n)) {
- return '0'
- }
- return n.toLocaleString('zh-CN')
- }
- function formatPrice(val) {
- if (val === null || val === undefined || val === '') {
- return '—'
- }
- const n = Number(val)
- if (Number.isNaN(n)) {
- return '—'
- }
- return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
- }
- function resetDashboard() {
- tradeOverview.value = null
- originQuote.value = null
- tradeMonthlyTrend.value = []
- salesDestination.value = []
- qualityGrade.value = null
- mallStatsAvailable.value = false
- categorySales.value = null
- hotCategoryRank.value = null
- mallOrderTrend.value = null
- shopEntry.value = null
- regionRank.value = null
- reviewWordCloud.value = null
- }
- function applyDashboard(data) {
- if (!data) {
- return
- }
- if (data.statYear) {
- statYear.value = String(data.statYear)
- }
- statDate.value = data.statDate || ''
- availableYears.value = data.availableYears?.length
- ? [...data.availableYears]
- : [Number(statYear.value)]
- tradeOverview.value = data.tradeOverview || null
- originQuote.value = data.originQuote || null
- tradeMonthlyTrend.value = data.tradeMonthlyTrend || []
- salesDestination.value = data.salesDestination || []
- qualityGrade.value = data.qualityGrade || null
- mallStatsAvailable.value = data.mallStatsAvailable || false
- categorySales.value = data.categorySales || null
- hotCategoryRank.value = data.hotCategoryRank || null
- mallOrderTrend.value = data.mallOrderTrend || null
- shopEntry.value = data.shopEntry || null
- regionRank.value = data.regionRank || null
- reviewWordCloud.value = data.reviewWordCloud || null
- }
- async function fetchDashboard(forceRefresh = false) {
- if (forceRefresh) {
- if (refreshing.value) {
- return
- }
- refreshing.value = true
- } else {
- loading.value = true
- resetDashboard()
- }
- loadError.value = ''
- try {
- const res = await getTradeSalesDashboard(statYear.value, forceRefresh)
- applyDashboard(res.data)
- } catch (e) {
- loadError.value = e?.message || '看板数据加载失败'
- } finally {
- loading.value = false
- refreshing.value = false
- }
- }
- function onYearChange() {
- fetchDashboard()
- }
- function onManualRefresh() {
- fetchDashboard(true)
- }
- function onRetry() {
- fetchDashboard()
- }
- function startDashboardTimer() {
- dashboardTimer = setInterval(() => {
- fetchDashboard(true)
- }, DASHBOARD_REFRESH_MS)
- }
- onMounted(() => {
- fetchDashboard()
- startDashboardTimer()
- })
- onUnmounted(() => {
- if (dashboardTimer) {
- clearInterval(dashboardTimer)
- dashboardTimer = null
- }
- })
- </script>
- <style scoped>
- .ts-page {
- position: relative;
- width: 100%;
- height: 100%;
- box-sizing: border-box;
- padding: 20px;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- .ts-page.is-loading {
- opacity: 0.92;
- }
- .ts-error {
- position: absolute;
- top: 4px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 10;
- padding: 4px 12px;
- font-size: 12px;
- color: #ffb4b4;
- background: rgba(80, 20, 20, 0.75);
- border-radius: 4px;
- cursor: pointer;
- }
- .ts-year-bar {
- position: absolute;
- top: 8px;
- right: 24px;
- z-index: 5;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .ts-year-bar__label {
- font-size: 12px;
- color: var(--screen-text-secondary, #a8d4c8);
- }
- .ts-year-bar__select {
- min-width: 88px;
- height: 28px;
- padding: 0 8px;
- font-size: 13px;
- color: #e8eef5;
- background: rgba(4, 48, 40, 0.85);
- border: 1px solid var(--screen-line-dim, rgba(61, 217, 176, 0.42));
- border-radius: 4px;
- outline: none;
- }
- .ts-year-bar__date {
- font-size: 11px;
- color: rgba(168, 212, 200, 0.75);
- }
- .ts-year-bar__refresh {
- height: 28px;
- padding: 0 10px;
- font-size: 12px;
- color: #e8eef5;
- background: rgba(4, 48, 40, 0.85);
- border: 1px solid var(--screen-line-dim, rgba(61, 217, 176, 0.42));
- border-radius: 4px;
- cursor: pointer;
- }
- .ts-year-bar__refresh:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- .screen-page--home-column {
- width: 617px;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- }
- .screen-page--home-column-top {
- width: 617px;
- height: 201px;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- background: url('../../assets/sale/sale.png') no-repeat center center;
- background-size: 100% 100%;
- }
- .screen-page--home-column-right {
- width: 617px;
- height: 201px;
- background: url('../../assets/pro/pro.png') no-repeat center center;
- background-size: 100% 100%;
- }
- .ts-mall-panel {
- display: flex;
- flex-direction: column;
- }
- .ts-mall-stat-year {
- font-size: 12px;
- color: rgba(168, 212, 200, 0.8);
- margin-left: 4px;
- }
- .ts-mall-top {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: row;
- gap: 8px;
- padding: 0 12px 10px 28px;
- box-sizing: border-box;
- }
- .ts-mall-top__pie {
- flex: 1.1;
- min-width: 0;
- min-height: 0;
- }
- .ts-mall-top__hot {
- flex: 0.9;
- min-width: 0;
- display: flex;
- flex-direction: column;
- padding: 4px 6px;
- background: rgba(4, 48, 40, 0.35);
- border-radius: 4px;
- border: 1px solid rgba(61, 217, 176, 0.18);
- }
- .ts-mall-hot-title {
- font-size: 11px;
- color: #a8d4c8;
- margin-bottom: 4px;
- flex-shrink: 0;
- }
- .ts-mall-hot-list {
- list-style: none;
- margin: 0;
- padding: 0;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- .ts-mall-hot-list__row {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 10px;
- color: #e8eef5;
- line-height: 1.6;
- }
- .ts-mall-hot-list__rank {
- width: 16px;
- text-align: center;
- color: #ecd27b;
- flex-shrink: 0;
- }
- .ts-mall-hot-list__name {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #a8d4c8;
- }
- .ts-mall-hot-list__qty {
- flex-shrink: 0;
- color: #5ef0c8;
- font-weight: 600;
- }
- .ts-placeholder {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 24px;
- text-align: center;
- font-size: 14px;
- color: rgba(168, 212, 200, 0.75);
- }
- .ts-placeholder--sm {
- font-size: 13px;
- }
- .screen-page--home-column-flex {
- width: 617px;
- height: 223px;
- display: flex;
- justify-content: space-between;
- }
- .screen-page--home-column-flex-item {
- width: 304px;
- height: 223px;
- background: url('../../assets/home/mini_box.png') no-repeat center center;
- background-size: 100% 100%;
- }
- .top_title {
- width: 100%;
- height: 35px;
- box-sizing: border-box;
- font-size: 18px;
- color: #fff;
- line-height: 35px;
- padding-left: 100px;
- margin-bottom: 8px;
- flex-shrink: 0;
- }
- .ts-overview-grid {
- flex: 1;
- min-height: 0;
- box-sizing: border-box;
- padding: 6px 20px 14px 32px;
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: repeat(2, 1fr);
- gap: 4px 40px;
- }
- .ts-overview-cell {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- justify-content: center;
- min-height: 0;
- width: 100%;
- text-align: right;
- box-sizing: border-box;
- padding-right: 10px;
- }
- .ts-overview-cell .content_title {
- width: 100%;
- text-align: right;
- }
- .ts-overview-cell .content_num {
- width: 100%;
- display: flex;
- align-items: baseline;
- justify-content: flex-end;
- gap: 6px;
- margin: 4px 0 0;
- line-height: 1.3;
- }
- .content_num__value {
- flex: 1;
- min-width: 0;
- text-align: right;
- font-size: 24px;
- font-weight: 600;
- background: linear-gradient(to bottom, #98e9aa, #ecd27b);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- .content_num :deep(.screen-scroll-number) {
- flex: 1;
- min-width: 0;
- justify-content: flex-end;
- font-size: 24px;
- font-weight: 600;
- }
- .content_num :deep(.screen-scroll-number__strip),
- .content_num :deep(.screen-scroll-number__sep) {
- background: linear-gradient(to bottom, #98e9aa, #ecd27b);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- .content_num__unit {
- flex-shrink: 0;
- font-size: 12px;
- color: #a8d4c8;
- line-height: 1.2;
- text-align: right;
- }
- .ts-overview-cell--placeholder .content_num__value {
- font-size: 18px;
- background: none;
- -webkit-background-clip: unset;
- background-clip: unset;
- color: #6a9f90;
- }
- .content_title {
- font-size: 11px;
- color: #a8d4c8;
- line-height: 1.3;
- }
- .flex_title {
- width: 100%;
- box-sizing: border-box;
- height: 35px;
- font-size: 16px;
- color: #fff;
- line-height: 35px;
- padding-left: 45px;
- margin-bottom: 5px;
- }
- .flex_content {
- width: 100%;
- height: 180px;
- box-sizing: border-box;
- padding: 5px;
- }
- .flex_content--quote {
- display: flex;
- flex-direction: column;
- padding: 4px 6px;
- }
- .quote-summary {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 4px;
- min-height: 42px;
- flex-shrink: 0;
- }
- .quote-summary__item {
- flex: 1 1 calc(50% - 4px);
- min-width: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: rgba(4, 48, 40, 0.45);
- border-radius: 4px;
- border: 1px solid rgba(61, 217, 176, 0.2);
- padding: 2px 0;
- }
- .quote-summary__label {
- font-size: 9px;
- color: #a8d4c8;
- }
- .quote-summary__val {
- font-size: 10px;
- color: #fff;
- }
- .quote-summary__val strong {
- font-size: 12px;
- background: linear-gradient(to bottom, #98e9aa, #ecd27b);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- .quote-summary__val--date strong {
- font-size: 10px;
- }
- .quote-summary__unit {
- font-size: 9px;
- color: #a8d4c8;
- margin-left: 2px;
- }
- .quote-chart {
- flex: 1;
- min-height: 0;
- }
- </style>
|