| | |
| | | <template> |
| | | <div class="main_home"> |
| | | <div class="home_header"> |
| | | <div class="mb10 fs17">下午好,{{ userInfo.realname }}</div> |
| | | <div class="fs13"> |
| | | 今天是 {{ nowDate }} {{ nowWeek }},欢迎访问高铁站行李寄存管理系统 |
| | | <div class="data-board"> |
| | | <div class="header"> |
| | | <h1 class="title">经营数据看板</h1> |
| | | <div class="filter"> |
| | | <el-button-group> |
| | | <el-button :type="currentDateType === 0 ? 'primary' : 'default'" @click="onDateTypeChange(0)">今日</el-button> |
| | | <el-button :type="currentDateType === 1 ? 'primary' : 'default'" @click="onDateTypeChange(1)">近7日</el-button> |
| | | <el-button :type="currentDateType === 2 ? 'primary' : 'default'" @click="onDateTypeChange(2)">近30日</el-button> |
| | | <el-button :type="currentDateType === 3 ? 'primary' : 'default'" @click="onDateTypeChange(3)">半年</el-button> |
| | | <el-button :type="currentDateType === 4 ? 'primary' : 'default'" @click="onDateTypeChange(4)">一年</el-button> |
| | | </el-button-group> |
| | | <el-select v-model="currentShopId" filterable placeholder="请选择寄存点" style="width: 200px; margin: 0 10px;"> |
| | | <el-option |
| | | v-for="shop in shopList" |
| | | :key="shop.id" |
| | | :label="shop.name" |
| | | :value="shop.id" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-button type="primary" @click="onSearch">查询</el-button> |
| | | <el-button @click="onReset">重置</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="stats-row"> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">入驻门店总数</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.shopCount || 0 }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">门店入驻率</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.shopSettlementRate || 0 }}%</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">认证司机总数</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.driverCount || 0 }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">司机通过率</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.driverPassRate || 0 }}%</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">累计会员总数</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.memberCount || 0 }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">周期总订单数</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.orderCount || 0 }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">周期营收总金额</div> |
| | | <div class="stat-value" v-if="overviewData">¥{{ (overviewData.totalRevenue || 0).toFixed(2) }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <div class="stat-label">周期退款单数</div> |
| | | <div class="stat-value" v-if="overviewData">{{ overviewData.refundOrderCount || 0 }}</div> |
| | | <div class="stat-value" v-else>-</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="charts-row"> |
| | | <div class="chart-card"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">订单行李类型占比</span> |
| | | <el-button type="text" size="mini" @click="exportPieChart">导出</el-button> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="pieChart" class="pie-chart"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">新增会员走势</span> |
| | | <div class="date-selectors"> |
| | | <el-select v-model="memberYear" placeholder="选择年份" @change="onMemberYearChange"> |
| | | <el-option |
| | | v-for="year in memberYearOptions" |
| | | :key="year" |
| | | :label="year + '年'" |
| | | :value="year" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-select v-model="memberMonth" placeholder="选择月份" :disabled="!memberYear" @change="getMemberTrendData" clearable> |
| | | <el-option |
| | | v-for="month in memberMonthOptions" |
| | | :key="month" |
| | | :label="month + '月'" |
| | | :value="month" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="memberChart" class="member-chart"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="charts-row"> |
| | | <div class="chart-card"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">订单量趋势</span> |
| | | <div class="date-selectors"> |
| | | <el-select v-model="orderYear" placeholder="选择年份" @change="onOrderYearChange"> |
| | | <el-option |
| | | v-for="year in orderYearOptions" |
| | | :key="year" |
| | | :label="year + '年'" |
| | | :value="year" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-select v-model="orderMonth" placeholder="选择月份" :disabled="!orderYear" @change="getOrderTrendData" clearable> |
| | | <el-option |
| | | v-for="month in orderMonthOptions" |
| | | :key="month" |
| | | :label="month + '月'" |
| | | :value="month" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="orderChart" class="order-chart"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">营收增长曲线</span> |
| | | <div class="date-selectors"> |
| | | <el-select v-model="revenueYear" placeholder="选择年份" @change="onRevenueYearChange"> |
| | | <el-option |
| | | v-for="year in revenueYearOptions" |
| | | :key="year" |
| | | :label="year + '年'" |
| | | :value="year" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-select v-model="revenueMonth" placeholder="选择月份" :disabled="!revenueYear" @change="getRevenueTrendData" clearable> |
| | | <el-option |
| | | v-for="month in revenueMonthOptions" |
| | | :key="month" |
| | | :label="month + '月'" |
| | | :value="month" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="revenueChart" class="revenue-chart"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 平台财务总览 --> |
| | | <div class="chart-card"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">平台财务总览</span> |
| | | <div class="filter-group"> |
| | | <el-date-picker type="daterange" range-separator="至" start-placeholder="开始月份" end-placeholder="结束月份" v-model="financeDateRange" value-format="yyyy-MM" style="margin-right: 10px" :picker-options="financePickerOptions" clearable></el-date-picker> |
| | | <el-button type="primary" size="mini" @click="getFinanceOverviewData">查询</el-button> |
| | | <el-button size="mini" @click="resetFinanceDate">重置</el-button> |
| | | <el-button type="success" size="mini" @click="exportFinanceData">导出</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <el-table :data="financeList" stripe> |
| | | <el-table-column prop="date" label="年月" min-width="100px"></el-table-column> |
| | | <el-table-column prop="storageRevenue" label="寄存订单营收" min-width="120px"></el-table-column> |
| | | <el-table-column prop="deliveryRevenue" label="寄送订单营收" min-width="120px"></el-table-column> |
| | | <el-table-column prop="totalRevenue" label="平台总营收" min-width="120px"></el-table-column> |
| | | <el-table-column prop="shopFeeTotal" label="门店分成总额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="driverFeeTotal" label="司机分成总额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="refundAmount" label="退款总金额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="netRevenue" label="平台净营收" min-width="120px"></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="header" style="margin-top: 20px;"> |
| | | <h1 class="title">业绩排名分析</h1> |
| | | <div class="filter"> |
| | | <el-select v-model="shopTopYear" placeholder="选择年份" style="width: 120px; margin-right: 10px;" @change="onShopTopYearChange"> |
| | | <el-option |
| | | v-for="year in shopTopYearOptions" |
| | | :key="year" |
| | | :label="year + '年'" |
| | | :value="year" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-select v-model="shopTopMonth" placeholder="选择月份" style="width: 120px;" :disabled="!shopTopYear" @change="onShopTopMonthChange" clearable> |
| | | <el-option |
| | | v-for="month in shopTopMonthOptions" |
| | | :key="month" |
| | | :label="month + '月'" |
| | | :value="month" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 业绩排名分析 --> |
| | | <div class="charts-row" style="margin-top: 20px; margin-bottom: 0;"> |
| | | <div class="chart-card" style="width: calc(50% - 20px);"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">门店业绩TOP10</span> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <el-table :data="storeTopList" stripe> |
| | | <el-table-column type="index" label="排名" width="60px"></el-table-column> |
| | | <el-table-column prop="shopName" label="门店名称" min-width="120px"></el-table-column> |
| | | <el-table-column prop="completedCount" label="总完成订单量" min-width="120px"></el-table-column> |
| | | <el-table-column prop="totalRevenue" label="总营收金额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="shopFeeTotal" label="门店分成总额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="refundCount" label="退款单数" min-width="120px"></el-table-column> |
| | | <el-table-column prop="penaltyAmount" label="责任扣款总额" min-width="120px"></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card" style="width: calc(50% - 20px);"> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">司机业绩TOP10</span> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <el-table :data="driverTopList" stripe> |
| | | <el-table-column type="index" label="排名" width="60px"></el-table-column> |
| | | <el-table-column prop="driverName" label="司机姓名" min-width="120px"></el-table-column> |
| | | <el-table-column prop="completedCount" label="总完成订单量" min-width="120px"></el-table-column> |
| | | <el-table-column prop="totalRevenue" label="总营收金额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="driverFeeTotal" label="司机分成总额" min-width="120px"></el-table-column> |
| | | <el-table-column prop="refundCount" label="退款单数" min-width="120px"></el-table-column> |
| | | <el-table-column prop="penaltyAmount" label="责任扣款总额" min-width="120px"></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import dayjs from 'dayjs' |
| | | import { overview, memberTrend, orderTrend, revenueTrend, financeOverview, exportExcel, luggageTypeExport, shopTop, driverTop } from '@/api/business/dataBoard' |
| | | import { fetchList } from '@/api/business/shopInfo' |
| | | import * as echarts from 'echarts' |
| | | import { weeks } from '@/utils/config' |
| | | const colors = ['#52a4f7', '#7678f7', '#5fc6d5'] |
| | | |
| | | export default { |
| | | components: { |
| | | }, |
| | | name: 'DataBoard', |
| | | data () { |
| | | const currentYear = new Date().getFullYear() |
| | | const currentMonth = new Date().getMonth() + 1 |
| | | const startOfYear = `${currentYear}-01` |
| | | const endOfYear = `${currentYear}-${String(currentMonth).padStart(2, '0')}` |
| | | return { |
| | | searchForm: { |
| | | timeType: null, |
| | | timeName: '全部' |
| | | overviewData: {}, |
| | | currentDateType: 0, |
| | | currentShopId: null, |
| | | shopList: [], |
| | | pieChartInstance: null, |
| | | currentYear, |
| | | currentMonth, |
| | | memberYear: currentYear, |
| | | memberMonth: null, |
| | | memberYearOptions: [], |
| | | memberMonthOptions: [], |
| | | memberTrendData: [], |
| | | memberChartInstance: null, |
| | | orderYear: currentYear, |
| | | orderMonth: null, |
| | | orderYearOptions: [], |
| | | orderMonthOptions: [], |
| | | orderTrendData: [], |
| | | orderChartInstance: null, |
| | | revenueYear: currentYear, |
| | | revenueMonth: null, |
| | | revenueYearOptions: [], |
| | | revenueMonthOptions: [], |
| | | revenueTrendData: [], |
| | | revenueChartInstance: null, |
| | | shopTopYear: currentYear, |
| | | shopTopMonth: null, |
| | | shopTopYearOptions: [], |
| | | shopTopMonthOptions: [], |
| | | driverTopList: [], |
| | | financeDateRange: [startOfYear, endOfYear], |
| | | financePickerOptions: { |
| | | disabledDate: (time) => { |
| | | // 禁用未来日期 |
| | | if (time.getTime() > Date.now()) { |
| | | return true |
| | | } |
| | | // 限制日期范围不超过2年 |
| | | if (this.financeMinDate) { |
| | | const twoYears = 2 * 365 * 24 * 60 * 60 * 1000 |
| | | const maxTime = this.financeMinDate.getTime() + twoYears |
| | | if (time.getTime() > maxTime) { |
| | | return true |
| | | } |
| | | } |
| | | return false |
| | | }, |
| | | colors, |
| | | nowDate: '', |
| | | nowWeek: '', |
| | | headerData: {}, |
| | | headerData1: {}, |
| | | staticData0: {}, |
| | | staticData01: {}, |
| | | staticData1: {}, |
| | | staticData2: {}, |
| | | staticData3: {}, |
| | | staticData4: {}, |
| | | manningRatio: [] |
| | | onPick: ({ maxDate, minDate }) => { |
| | | this.financeMinDate = minDate |
| | | if (maxDate) { |
| | | this.financeMinDate = null |
| | | } |
| | | } |
| | | }, |
| | | computed: { |
| | | userInfo () { |
| | | return this.$store.state.userInfo |
| | | financeMinDate: null, |
| | | financeList: [], |
| | | storeTopList: [], |
| | | driverTopList: [ |
| | | { driverName: '张伟大', phone: '18356981111', totalOrder: '100', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '100', deductionTotal: '90720.00' }, |
| | | { driverName: '李明', phone: '18356982222', totalOrder: '80', totalIncome: '32432.00', driverShare: '32432.00', refundCount: '80', deductionTotal: '32432.00' }, |
| | | { driverName: '张杰', phone: '18356981111', totalOrder: '70', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '70', deductionTotal: '90720.00' }, |
| | | { driverName: '孙浩', phone: '18356982222', totalOrder: '70', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '70', deductionTotal: '90720.00' }, |
| | | { driverName: '李梦', phone: '18356982222', totalOrder: '70', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '70', deductionTotal: '90720.00' }, |
| | | { driverName: '刘梓贤', phone: '18356982222', totalOrder: '40', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '40', deductionTotal: '90720.00' }, |
| | | { driverName: '王浩然', phone: '18356981111', totalOrder: '40', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '40', deductionTotal: '90720.00' }, |
| | | { driverName: '张伟大', phone: '18356982222', totalOrder: '40', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '40', deductionTotal: '90720.00' }, |
| | | { driverName: '李明', phone: '18356981111', totalOrder: '40', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '40', deductionTotal: '90720.00' }, |
| | | { driverName: '张杰', phone: '18356982222', totalOrder: '40', totalIncome: '90720.00', driverShare: '90720.00', refundCount: '40', deductionTotal: '90720.00' } |
| | | ] |
| | | } |
| | | }, |
| | | created () { |
| | | this.getOverviewData() |
| | | this.getShopList() |
| | | this.initMemberDateOptions() |
| | | this.getMemberTrendData() |
| | | this.initOrderDateOptions() |
| | | this.getOrderTrendData() |
| | | this.initRevenueDateOptions() |
| | | this.getRevenueTrendData() |
| | | this.initShopTopDateOptions() |
| | | this.getShopTopData() |
| | | this.getDriverTopData() |
| | | this.getFinanceOverviewData() |
| | | }, |
| | | mounted () { |
| | | this.updateDate() |
| | | // this.initData() |
| | | this.initPieChart() |
| | | this.renderPieChart() |
| | | this.initMemberChart() |
| | | this.renderMemberChart() |
| | | this.initOrderChart() |
| | | this.renderOrderChart() |
| | | this.initRevenueChart() |
| | | this.renderRevenueChart() |
| | | window.addEventListener('resize', this.handleResize) |
| | | }, |
| | | beforeDestroy() { |
| | | window.removeEventListener('resize', this.handleResize) |
| | | if (this.pieChartInstance) { |
| | | this.pieChartInstance.dispose() |
| | | } |
| | | if (this.memberChartInstance) { |
| | | this.memberChartInstance.dispose() |
| | | } |
| | | if (this.orderChartInstance) { |
| | | this.orderChartInstance.dispose() |
| | | } |
| | | if (this.revenueChartInstance) { |
| | | this.revenueChartInstance.dispose() |
| | | } |
| | | }, |
| | | methods: { |
| | | updateDate () { |
| | | this.nowDate = dayjs().format('YYYY年M月D日') |
| | | this.nowWeek = weeks[new Date().getDay()] |
| | | async getOverviewData(dateType = 0, shopId = null) { |
| | | const response = await overview({ dateType, shopId }) |
| | | this.overviewData = response || null |
| | | this.$nextTick(() => { |
| | | this.renderPieChart() |
| | | }) |
| | | }, |
| | | onDateTypeChange(dateType) { |
| | | this.currentDateType = dateType |
| | | this.getOverviewData(dateType, this.currentShopId) |
| | | }, |
| | | onSearch() { |
| | | this.getOverviewData(this.currentDateType, this.currentShopId) |
| | | }, |
| | | onReset() { |
| | | this.currentDateType = 0 |
| | | this.currentShopId = null |
| | | this.getOverviewData(0, null) |
| | | }, |
| | | async getShopList() { |
| | | const response = await fetchList({ |
| | | capacity: 99999, |
| | | page: 1, |
| | | model: { |
| | | versionType: 0, |
| | | auditStatus: 3 |
| | | } |
| | | }) |
| | | this.shopList = response.records || [] |
| | | }, |
| | | initPieChart() { |
| | | this.pieChartInstance = echarts.init(this.$refs.pieChart) |
| | | }, |
| | | renderPieChart() { |
| | | if (!this.pieChartInstance) { |
| | | this.initPieChart() |
| | | } |
| | | |
| | | const data = this.overviewData.luggageTypeList || [] |
| | | const chartData = data.map(item => ({ |
| | | value: item.orderCount, |
| | | name: item.luggageName, |
| | | luggageCount: item.luggageCount |
| | | })) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: (params) => { |
| | | return `${params.name}<br/>行李数: ${params.data.luggageCount}` |
| | | } |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '行李类型', |
| | | type: 'pie', |
| | | radius: ['40%', '70%'], |
| | | avoidLabelOverlap: true, |
| | | itemStyle: { |
| | | borderRadius: 10, |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: true, |
| | | position: 'outside', |
| | | formatter: '{b}' |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: 16, |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | length: 30, |
| | | length2: 20, |
| | | smooth: true |
| | | }, |
| | | data: chartData |
| | | } |
| | | ] |
| | | } |
| | | |
| | | this.pieChartInstance.setOption(option) |
| | | }, |
| | | handleResize() { |
| | | if (this.pieChartInstance) { |
| | | this.pieChartInstance.resize() |
| | | } |
| | | if (this.memberChartInstance) { |
| | | this.memberChartInstance.resize() |
| | | } |
| | | if (this.orderChartInstance) { |
| | | this.orderChartInstance.resize() |
| | | } |
| | | if (this.revenueChartInstance) { |
| | | this.revenueChartInstance.resize() |
| | | } |
| | | }, |
| | | async exportPieChart() { |
| | | try { |
| | | const dateType = this.currentDateType |
| | | const shopId = this.currentShopId |
| | | const params = { dateType, shopId } |
| | | |
| | | const response = await luggageTypeExport(params) |
| | | |
| | | // 创建下载链接 |
| | | const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | |
| | | // 生成文件名 |
| | | const dateTypeNames = ['今日', '近七天', '近30天', '近半年', '近一年'] |
| | | const shopName = this.shopList.find(shop => shop.id === shopId)?.name || '全部寄存点' |
| | | const fileName = `订单行李类型占比_${dateTypeNames[dateType]}_${shopName}.xlsx` |
| | | |
| | | link.setAttribute('download', fileName) |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | window.URL.revokeObjectURL(url) |
| | | |
| | | this.$message.success('导出成功') |
| | | } catch (error) { |
| | | console.error('导出行李类型数据失败:', error) |
| | | this.$message.error('导出失败,请稍后重试') |
| | | } |
| | | }, |
| | | initMemberDateOptions() { |
| | | // 生成年份选项,最多到当前年 |
| | | const startYear = this.currentYear - 5 |
| | | this.memberYearOptions = [] |
| | | for (let year = startYear; year <= this.currentYear; year++) { |
| | | this.memberYearOptions.push(year) |
| | | } |
| | | // 初始化月份选项 |
| | | this.updateMemberMonthOptions() |
| | | }, |
| | | updateMemberMonthOptions() { |
| | | this.memberMonthOptions = [] |
| | | const maxMonth = this.memberYear === this.currentYear ? this.currentMonth : 12 |
| | | for (let month = 1; month <= maxMonth; month++) { |
| | | this.memberMonthOptions.push(month) |
| | | } |
| | | // 如果当前选择的月份超过最大允许值,自动调整 |
| | | if (this.memberMonth > maxMonth) { |
| | | this.memberMonth = maxMonth |
| | | } |
| | | }, |
| | | onMemberYearChange() { |
| | | this.updateMemberMonthOptions() |
| | | this.getMemberTrendData() |
| | | }, |
| | | initOrderDateOptions() { |
| | | // 生成年份选项,最多到当前年 |
| | | const startYear = this.currentYear - 5 |
| | | this.orderYearOptions = [] |
| | | for (let year = startYear; year <= this.currentYear; year++) { |
| | | this.orderYearOptions.push(year) |
| | | } |
| | | // 初始化月份选项 |
| | | this.updateOrderMonthOptions() |
| | | }, |
| | | updateOrderMonthOptions() { |
| | | this.orderMonthOptions = [] |
| | | const maxMonth = this.orderYear === this.currentYear ? this.currentMonth : 12 |
| | | for (let month = 1; month <= maxMonth; month++) { |
| | | this.orderMonthOptions.push(month) |
| | | } |
| | | // 如果当前选择的月份超过最大允许值,自动调整 |
| | | if (this.orderMonth && this.orderMonth > maxMonth) { |
| | | this.orderMonth = maxMonth |
| | | } |
| | | }, |
| | | onOrderYearChange() { |
| | | this.updateOrderMonthOptions() |
| | | this.getOrderTrendData() |
| | | }, |
| | | async getOrderTrendData() { |
| | | const params = {} |
| | | if (this.orderMonth) { |
| | | params.month = `${this.orderYear}-${String(this.orderMonth).padStart(2, '0')}` |
| | | } else { |
| | | params.year = this.orderYear |
| | | } |
| | | const response = await orderTrend(params) |
| | | this.orderTrendData = response || [] |
| | | this.$nextTick(() => { |
| | | this.renderOrderChart() |
| | | }) |
| | | }, |
| | | initRevenueDateOptions() { |
| | | // 生成年份选项,最多到当前年 |
| | | const startYear = this.currentYear - 5 |
| | | this.revenueYearOptions = [] |
| | | for (let year = startYear; year <= this.currentYear; year++) { |
| | | this.revenueYearOptions.push(year) |
| | | } |
| | | // 初始化月份选项 |
| | | this.updateRevenueMonthOptions() |
| | | }, |
| | | updateRevenueMonthOptions() { |
| | | this.revenueMonthOptions = [] |
| | | const maxMonth = this.revenueYear === this.currentYear ? this.currentMonth : 12 |
| | | for (let month = 1; month <= maxMonth; month++) { |
| | | this.revenueMonthOptions.push(month) |
| | | } |
| | | // 如果当前选择的月份超过最大允许值,自动调整 |
| | | if (this.revenueMonth && this.revenueMonth > maxMonth) { |
| | | this.revenueMonth = maxMonth |
| | | } |
| | | }, |
| | | onRevenueYearChange() { |
| | | this.updateRevenueMonthOptions() |
| | | this.getRevenueTrendData() |
| | | }, |
| | | async getRevenueTrendData() { |
| | | const params = {} |
| | | if (this.revenueMonth) { |
| | | params.month = `${this.revenueYear}-${String(this.revenueMonth).padStart(2, '0')}` |
| | | } else { |
| | | params.year = this.revenueYear |
| | | } |
| | | const response = await revenueTrend(params) |
| | | this.revenueTrendData = response || [] |
| | | this.$nextTick(() => { |
| | | this.renderRevenueChart() |
| | | }) |
| | | }, |
| | | async getFinanceOverviewData() { |
| | | if (!this.financeDateRange || this.financeDateRange.length !== 2) { |
| | | // 如果日期范围为空,使用默认范围(今年1月到当前月) |
| | | const currentYear = new Date().getFullYear() |
| | | const currentMonth = new Date().getMonth() + 1 |
| | | const startOfYear = `${currentYear}-01` |
| | | const endOfYear = `${currentYear}-${String(currentMonth).padStart(2, '0')}` |
| | | this.financeDateRange = [startOfYear, endOfYear] |
| | | } |
| | | const params = { |
| | | startDate: this.financeDateRange[0], |
| | | endDate: this.financeDateRange[1] |
| | | } |
| | | const response = await financeOverview(params) |
| | | this.financeList = response || [] |
| | | }, |
| | | resetFinanceDate() { |
| | | const currentYear = new Date().getFullYear() |
| | | const currentMonth = new Date().getMonth() + 1 |
| | | const startOfYear = `${currentYear}-01` |
| | | const endOfYear = `${currentYear}-${String(currentMonth).padStart(2, '0')}` |
| | | this.financeDateRange = [startOfYear, endOfYear] |
| | | this.getFinanceOverviewData() |
| | | }, |
| | | initShopTopDateOptions() { |
| | | // 生成年份选项,最多到当前年 |
| | | const startYear = this.currentYear - 5 |
| | | this.shopTopYearOptions = [] |
| | | for (let year = startYear; year <= this.currentYear; year++) { |
| | | this.shopTopYearOptions.push(year) |
| | | } |
| | | // 初始化月份选项 |
| | | this.updateShopTopMonthOptions() |
| | | }, |
| | | updateShopTopMonthOptions() { |
| | | this.shopTopMonthOptions = [] |
| | | const maxMonth = this.shopTopYear === this.currentYear ? this.currentMonth : 12 |
| | | for (let month = 1; month <= maxMonth; month++) { |
| | | this.shopTopMonthOptions.push(month) |
| | | } |
| | | // 如果当前选择的月份超过最大允许值,自动调整 |
| | | if (this.shopTopMonth && this.shopTopMonth > maxMonth) { |
| | | this.shopTopMonth = maxMonth |
| | | } |
| | | }, |
| | | onShopTopYearChange() { |
| | | this.updateShopTopMonthOptions() |
| | | // 同时刷新门店和司机业绩数据 |
| | | this.getShopTopData() |
| | | this.getDriverTopData() |
| | | }, |
| | | onShopTopMonthChange() { |
| | | // 同时刷新门店和司机业绩数据 |
| | | this.getShopTopData() |
| | | this.getDriverTopData() |
| | | }, |
| | | async exportFinanceData() { |
| | | try { |
| | | // 确保有有效的日期范围 |
| | | let startDate, endDate |
| | | if (!this.financeDateRange || this.financeDateRange.length !== 2) { |
| | | const currentYear = new Date().getFullYear() |
| | | const currentMonth = new Date().getMonth() + 1 |
| | | startDate = `${currentYear}-01` |
| | | endDate = `${currentYear}-${String(currentMonth).padStart(2, '0')}` |
| | | } else { |
| | | startDate = this.financeDateRange[0] |
| | | endDate = this.financeDateRange[1] |
| | | } |
| | | |
| | | const params = { |
| | | startDate, |
| | | endDate, |
| | | type: 'finance' // 导出类型标识 |
| | | } |
| | | |
| | | const response = await exportExcel(params) |
| | | |
| | | // 创建下载链接 |
| | | const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.setAttribute('download', `财务总览_${startDate}_至_${endDate}.xlsx`) |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | window.URL.revokeObjectURL(url) |
| | | |
| | | this.$message.success('导出成功') |
| | | } catch (error) { |
| | | console.error('导出财务数据失败:', error) |
| | | this.$message.error('导出失败,请稍后重试') |
| | | } |
| | | }, |
| | | async getShopTopData() { |
| | | try { |
| | | const params = {} |
| | | if (this.shopTopMonth) { |
| | | params.month = `${this.shopTopYear}-${String(this.shopTopMonth).padStart(2, '0')}` |
| | | } else { |
| | | params.year = this.shopTopYear |
| | | } |
| | | const response = await shopTop(params) |
| | | this.storeTopList = response || [] |
| | | } catch (error) { |
| | | console.error('获取门店业绩TOP10数据失败:', error) |
| | | } |
| | | }, |
| | | async getDriverTopData() { |
| | | try { |
| | | const params = {} |
| | | if (this.shopTopMonth) { |
| | | params.month = `${this.shopTopYear}-${String(this.shopTopMonth).padStart(2, '0')}` |
| | | } else { |
| | | params.year = this.shopTopYear |
| | | } |
| | | const response = await driverTop(params) |
| | | this.driverTopList = response || [] |
| | | } catch (error) { |
| | | console.error('获取司机业绩TOP10数据失败:', error) |
| | | } |
| | | }, |
| | | async getMemberTrendData() { |
| | | const params = {} |
| | | if (this.memberMonth) { |
| | | params.month = `${this.memberYear}-${String(this.memberMonth).padStart(2, '0')}` |
| | | } else { |
| | | params.year = this.memberYear |
| | | } |
| | | const response = await memberTrend(params) |
| | | console.log('会员走势数据:', response) |
| | | this.memberTrendData = response || [] |
| | | this.$nextTick(() => { |
| | | this.renderMemberChart() |
| | | }) |
| | | }, |
| | | initMemberChart() { |
| | | this.memberChartInstance = echarts.init(this.$refs.memberChart) |
| | | }, |
| | | renderMemberChart() { |
| | | if (!this.memberChartInstance) { |
| | | this.initMemberChart() |
| | | } |
| | | |
| | | const xAxisData = this.memberTrendData.map(item => item.date || item.name) |
| | | const seriesData = this.memberTrendData.map(item => item.count) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: [ |
| | | { |
| | | type: 'category', |
| | | data: xAxisData, |
| | | axisTick: { |
| | | alignWithLabel: true |
| | | } |
| | | } |
| | | ], |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: '新增会员数' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: '新增会员', |
| | | type: 'bar', |
| | | barWidth: '60%', |
| | | data: seriesData |
| | | } |
| | | ] |
| | | } |
| | | |
| | | this.memberChartInstance.setOption(option) |
| | | }, |
| | | initOrderChart() { |
| | | this.orderChartInstance = echarts.init(this.$refs.orderChart) |
| | | }, |
| | | renderOrderChart() { |
| | | if (!this.orderChartInstance) { |
| | | this.initOrderChart() |
| | | } |
| | | |
| | | const xAxisData = this.orderTrendData.map(item => item.date) |
| | | const localData = this.orderTrendData.map(item => item.localCount) |
| | | const remoteData = this.orderTrendData.map(item => item.remoteCount) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross' |
| | | }, |
| | | formatter: (params) => { |
| | | let result = params[0].name + '<br/>' |
| | | params.forEach(param => { |
| | | result += param.marker + param.seriesName + ': ' + Math.round(param.value) + '<br/>' |
| | | }) |
| | | return result |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['就地寄存', '同城寄送'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: [ |
| | | { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: xAxisData |
| | | } |
| | | ], |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: '订单数量', |
| | | axisLabel: { |
| | | formatter: (value) => Math.round(value) |
| | | }, |
| | | axisPointer: { |
| | | label: { |
| | | formatter: (params) => Math.round(params.value) |
| | | } |
| | | } |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: '就地寄存', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | data: localData |
| | | }, |
| | | { |
| | | name: '同城寄送', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | data: remoteData |
| | | } |
| | | ] |
| | | } |
| | | |
| | | this.orderChartInstance.setOption(option) |
| | | }, |
| | | initRevenueChart() { |
| | | this.revenueChartInstance = echarts.init(this.$refs.revenueChart) |
| | | }, |
| | | renderRevenueChart() { |
| | | if (!this.revenueChartInstance) { |
| | | this.initRevenueChart() |
| | | } |
| | | |
| | | const xAxisData = this.revenueTrendData.map(item => item.date) |
| | | const localData = this.revenueTrendData.map(item => item.localRevenue) |
| | | const remoteData = this.revenueTrendData.map(item => item.remoteRevenue) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross' |
| | | }, |
| | | formatter: (params) => { |
| | | let result = params[0].axisValue + '<br/>' |
| | | params.forEach(item => { |
| | | result += `${item.marker}${item.seriesName}: ¥${item.value.toFixed(2)}<br/>` |
| | | }) |
| | | return result |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['就地寄存', '同城寄送'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: [ |
| | | { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: xAxisData |
| | | } |
| | | ], |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: '营收金额(元)', |
| | | axisLabel: { |
| | | formatter: '¥{value}' |
| | | } |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: '就地寄存', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | data: localData |
| | | }, |
| | | { |
| | | name: '同城寄送', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | data: remoteData |
| | | } |
| | | ] |
| | | } |
| | | |
| | | this.revenueChartInstance.setOption(option) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | |
| | | ::v-deep .el-input--small .el-input__inner { |
| | | height: 30px !important; // 这里就是修改默认高度 |
| | | width: 120px; |
| | | } |
| | | div { |
| | | <style scoped> |
| | | .data-board { |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | background: #f5f7fa; |
| | | height: calc(100vh - 60px); |
| | | overflow-y: scroll; |
| | | } |
| | | |
| | | .home_title { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | color: #222222; |
| | | line-height: 22px; |
| | | } |
| | | .mb50{ |
| | | margin-bottom: 50px; |
| | | } |
| | | .main { |
| | | display: flex; |
| | | position: relative; |
| | | z-index: 99; |
| | | |
| | | .app_content { |
| | | flex: 1; |
| | | |
| | | .static_card { |
| | | height: 187px; |
| | | color: #fff; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .card { |
| | | flex: 1; |
| | | height: 187px; |
| | | background: linear-gradient(270deg, #29aeff 0%, #207ff7 100%); |
| | | box-shadow: 0px 2px 10px 0px rgba(32, 127, 247, 0.4); |
| | | border-radius: 8px; |
| | | margin-right: 14px; |
| | | |
| | | &:nth-of-type(2) { |
| | | background: linear-gradient(270deg, #8383ff 0%, #6b6eff 100%); |
| | | } |
| | | |
| | | &:nth-of-type(3) { |
| | | background: linear-gradient(270deg, #42d49d 0%, #12bb8b 100%); |
| | | } |
| | | |
| | | &:nth-of-type(4) { |
| | | margin-right: 0; |
| | | background: linear-gradient(270deg, #c430dee3 0%, #cd04b9cf 100%); |
| | | //linear-gradient(270deg, #de3049b8 0%, #cd0421d4 100%); |
| | | //background: linear-gradient(270deg, #30d3de 0%, #04b7cd 100%); |
| | | } |
| | | |
| | | .header { |
| | | height: 103px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | /* justify-content: space-between; */ |
| | | align-items: center; |
| | | padding: 20px 20px 12px; |
| | | border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
| | | |
| | | img { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | |
| | | .num { |
| | | font-weight: 600; |
| | | font-size: 30px; |
| | | margin-top: 12px; |
| | | .title { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | height: 82px; |
| | | .filter { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | font-size: 13px; |
| | | padding: 15px 20px 20px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .funcs { |
| | | height: 149px; |
| | | padding: 20px; |
| | | background: #fff; |
| | | margin: 10px 0; |
| | | |
| | | .list { |
| | | display: flex; |
| | | padding-top: 20px; |
| | | |
| | | .item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 24px; |
| | | cursor: pointer; |
| | | |
| | | img { |
| | | width: 44px; |
| | | height: 44px; |
| | | margin-left: 30px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | .static_wrap { |
| | | .wrap { |
| | | background: #fff; |
| | | padding: 20px 20px 10px; |
| | | border-radius: 2px; |
| | | border: 1px solid #eeeeee; |
| | | height: 280px; |
| | | flex: 1; |
| | | margin-top: 10px; |
| | | .stats-row { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | justify-content: space-between; |
| | | margin-bottom: 20px; |
| | | &:nth-of-type(2n) { |
| | | //margin-left: 10px; |
| | | } |
| | | .echart { |
| | | width: 100%; |
| | | height: 190px; |
| | | position: relative; |
| | | .stat-card { |
| | | width: calc(100% / 4 - 15px); |
| | | background: white; |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | border-radius: 8px; |
| | | margin-top: 20px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 10px; |
| | | } |
| | | .stat-value { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | .charts-row { |
| | | display: flex; |
| | | div{ |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | .chart-card { |
| | | flex: 1; |
| | | height: 190px; |
| | | background: white; |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .header { |
| | | .chart-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 30px; |
| | | |
| | | .more { |
| | | font-size: 13px; |
| | | color: #999999; |
| | | } |
| | | .chart-title { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .static1 { |
| | | .content { |
| | | .chart-tags { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | .chart-content { |
| | | /* height: 300px; */ |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | align-items: center; |
| | | height: 100%; |
| | | |
| | | .echart_wrap { |
| | | position: relative; |
| | | |
| | | .pie_text { |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-50%, -50%); |
| | | z-index: 999; |
| | | } |
| | | .chart-placeholder { |
| | | width: 100%; |
| | | text-align: center; |
| | | } |
| | | |
| | | .echart { |
| | | width: 150px; |
| | | height: 150px; |
| | | .pie-chart { |
| | | width: 100%; |
| | | height: 300px; |
| | | } |
| | | |
| | | .list { |
| | | margin-left: 36px; |
| | | .date-selectors { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .item { |
| | | .date-selectors .el-select { |
| | | width: 100px; |
| | | } |
| | | |
| | | .member-chart { |
| | | width: 100%; |
| | | height: 300px; |
| | | } |
| | | |
| | | .order-chart { |
| | | width: 100%; |
| | | height: 300px; |
| | | } |
| | | |
| | | .revenue-chart { |
| | | width: 100%; |
| | | height: 300px; |
| | | } |
| | | |
| | | .filter-group { |
| | | display: flex; |
| | | align-items: center; |
| | | margin: 8px 0; |
| | | |
| | | .icon { |
| | | width: 16px; |
| | | height: 16px; |
| | | border-radius: 50%; |
| | | margin-right: 6px; |
| | | background: linear-gradient(270deg, #29aeff 0%, #207ff7 100%); |
| | | gap: 10px; |
| | | } |
| | | |
| | | .text { |
| | | margin-right: 6px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | .chart-content { |
| | | padding-top: 20px; |
| | | } |
| | | |
| | | .main_home { |
| | | background: #f4f7fc; |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | overflow: auto; |
| | | padding: 92px 20px 20px; |
| | | .home_header { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 200px; |
| | | padding: 20px; |
| | | color: #fff; |
| | | background: linear-gradient(180deg, #076ae5 0%, rgba(32, 127, 247, 0) 100%); |
| | | .el-table { |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .el-table th { |
| | | background-color: #f5f7fa; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | </style> |