| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <template>
- <div class="department-efficiency-view">
- <header class="page-header">
- <h1 class="page-title">综合运营分析</h1>
- <p class="page-description">分析各部门的平均销售额与渠道商品多样性。</p>
- </header>
- <OrderDateRangeFilter
- :max-date="maxDate"
- default-period="day"
- storage-key="dtm-shop-date-filter"
- @change="handleFilterChange"
- />
- <section class="chart-area">
- <OrderLoadingPanel
- v-if="barChart.loading"
- title="正在加载部门效率数据"
- detail="正在读取各部门平均销售额"
- />
- <div v-else-if="barChart.message" :class="['status-overlay', { error: barChart.isError }]">
- <p>{{ barChart.title }}</p>
- <p class="error-message">{{ barChart.message }}</p>
- </div>
- <div ref="barChartRef" class="chart-canvas"></div>
- </section>
- <section class="chart-area">
- <OrderLoadingPanel
- v-if="pieChart.loading"
- title="正在加载渠道多样性数据"
- detail="正在读取各渠道商品覆盖结构"
- />
- <div v-else-if="pieChart.message" :class="['status-overlay', { error: pieChart.isError }]">
- <p>{{ pieChart.title }}</p>
- <p class="error-message">{{ pieChart.message }}</p>
- </div>
- <div ref="pieChartRef" class="chart-canvas"></div>
- </section>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts'
- import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
- import OrderDateRangeFilter from '../components/OrderDateRangeFilter.vue'
- import { getShopChannelDiversity, getShopDepartmentEfficiency, getShopMaxDate } from '@/api/order'
- const EMPTY_DATA_HINTS = ['未上传', '请先上传', '暂无数据', '无数据', 'not found', 'no data']
- function createChartState(defaultTitle) {
- return {
- loading: true,
- title: defaultTitle,
- message: '',
- isError: false
- }
- }
- export default {
- name: 'OrderEfficiency',
- components: {
- OrderLoadingPanel,
- OrderDateRangeFilter
- },
- data() {
- return {
- barChartInstance: null,
- pieChartInstance: null,
- maxDate: '',
- currentQuery: {},
- barChart: createChartState('部门效率数据暂不可用'),
- pieChart: createChartState('渠道多样性数据暂不可用')
- }
- },
- mounted() {
- this.initFilter()
- window.addEventListener('resize', this.handleResize)
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.handleResize)
- this.disposeCharts()
- },
- methods: {
- async loadCharts() {
- await Promise.all([this.initBarChart(), this.initPieChart()])
- },
- async initFilter() {
- try {
- this.maxDate = await getShopMaxDate()
- } catch (error) {
- const today = new Date()
- this.maxDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
- }
- },
- handleFilterChange(params) {
- this.currentQuery = params || {}
- this.loadCharts()
- },
- disposeCharts() {
- if (this.barChartInstance) {
- this.barChartInstance.dispose()
- this.barChartInstance = null
- }
- if (this.pieChartInstance) {
- this.pieChartInstance.dispose()
- this.pieChartInstance = null
- }
- },
- resetChartState(target, title) {
- target.loading = true
- target.title = title
- target.message = ''
- target.isError = false
- },
- getErrorMessage(error, fallback) {
- return (
- error?.response?.data?.message ||
- error?.response?.data?.msg ||
- error?.message ||
- fallback
- )
- },
- isEmptyDataMessage(message) {
- const normalized = String(message || '').toLowerCase()
- return EMPTY_DATA_HINTS.some(item => normalized.includes(item.toLowerCase()))
- },
- setChartStatus(target, title, message, isError = false) {
- target.title = title
- target.message = message
- target.isError = isError
- },
- normalizeBarData(response) {
- if (!response || response.success !== true || !response.data) {
- throw new Error((response && response.message) || '部门效率数据加载失败')
- }
- const rows = Object.entries(response.data)
- .map(([name, value]) => ({
- name,
- value: Number(value)
- }))
- .filter(item => Number.isFinite(item.value))
- if (!rows.length) {
- throw new Error('请先上传店铺价值 CSV 文件')
- }
- return rows
- },
- normalizePieData(response) {
- if (!response || response.success !== true || !response.data) {
- throw new Error((response && response.message) || '渠道多样性数据加载失败')
- }
- const rows = Object.entries(response.data)
- .map(([name, value]) => ({
- name,
- value: Number(value)
- }))
- .filter(item => Number.isFinite(item.value) && item.value > 0)
- if (!rows.length) {
- throw new Error('请先上传店铺价值 CSV 文件')
- }
- return rows
- },
- initBarChartInstance() {
- const chartEl = this.$refs.barChartRef
- if (!chartEl) return null
- this.barChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
- return this.barChartInstance
- },
- initPieChartInstance() {
- const chartEl = this.$refs.pieChartRef
- if (!chartEl) return null
- this.pieChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
- return this.pieChartInstance
- },
- async initBarChart() {
- this.resetChartState(this.barChart, '部门效率数据暂不可用')
- try {
- const response = await getShopDepartmentEfficiency({
- startDate: this.currentQuery.startDate,
- endDate: this.currentQuery.endDate
- })
- const chartData = this.normalizeBarData(response)
- await this.$nextTick()
- const chart = this.initBarChartInstance()
- if (!chart) return
- chart.setOption({
- title: { text: '部门效率分析', left: 'center' },
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: '{b}<br/>平均销售额: {c} 元'
- },
- grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
- xAxis: {
- type: 'category',
- data: chartData.map(item => item.name),
- axisLabel: { rotate: 30, interval: 0 }
- },
- yAxis: {
- type: 'value',
- name: '平均销售额(元)'
- },
- series: [{
- name: '平均销售额',
- type: 'bar',
- data: chartData.map(item => Number(item.value.toFixed(2))),
- barWidth: '40%',
- itemStyle: {
- borderRadius: [5, 5, 0, 0],
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#83bff6' },
- { offset: 1, color: '#188df0' }
- ])
- }
- }]
- }, true)
- } catch (error) {
- const message = this.getErrorMessage(error, '部门效率数据加载失败')
- this.setChartStatus(
- this.barChart,
- this.isEmptyDataMessage(message) ? '暂无部门效率数据' : '部门效率数据加载失败',
- this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
- !this.isEmptyDataMessage(message)
- )
- } finally {
- this.barChart.loading = false
- }
- },
- async initPieChart() {
- this.resetChartState(this.pieChart, '渠道多样性数据暂不可用')
- try {
- const response = await getShopChannelDiversity({
- startDate: this.currentQuery.startDate,
- endDate: this.currentQuery.endDate
- })
- const chartData = this.normalizePieData(response)
- await this.$nextTick()
- const chart = this.initPieChartInstance()
- if (!chart) return
- chart.setOption({
- title: { text: '渠道商品多样性', left: 'center' },
- tooltip: { trigger: 'item', formatter: '{a}<br/>{b}: {c} ({d}%)' },
- legend: { orient: 'vertical', left: 'left', top: '10%' },
- series: [{
- name: '渠道',
- type: 'pie',
- radius: [20, 140],
- center: ['50%', '60%'],
- roseType: 'area',
- itemStyle: { borderRadius: 5 },
- data: chartData
- }]
- }, true)
- } catch (error) {
- const message = this.getErrorMessage(error, '渠道多样性数据加载失败')
- this.setChartStatus(
- this.pieChart,
- this.isEmptyDataMessage(message) ? '暂无渠道多样性数据' : '渠道多样性数据加载失败',
- this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
- !this.isEmptyDataMessage(message)
- )
- } finally {
- this.pieChart.loading = false
- }
- },
- handleResize() {
- if (this.barChartInstance) {
- this.barChartInstance.resize()
- }
- if (this.pieChartInstance) {
- this.pieChartInstance.resize()
- }
- }
- }
- }
- </script>
- <style scoped>
- .department-efficiency-view {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- }
- .page-header {
- padding: 15px 20px;
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- }
- .page-title {
- margin: 0;
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .page-description {
- margin: 4px 0 0;
- font-size: 14px;
- color: #666;
- }
- .chart-area {
- position: relative;
- padding: 20px;
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- min-height: 540px;
- }
- .chart-canvas {
- width: 100%;
- height: 500px;
- }
- .status-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- padding: 24px;
- text-align: center;
- background-color: rgba(255, 255, 255, 0.88);
- border-radius: 8px;
- z-index: 10;
- color: #555;
- font-size: 16px;
- }
- .status-overlay.error {
- color: #f56c6c;
- }
- .error-message {
- margin-top: 8px;
- font-size: 14px;
- color: #999;
- }
- </style>
|