| | |
| | | <span>{{ currentTime }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 内容区 --> |
| | | <div class="content-container"> |
| | | <!-- 左侧 --> |
| | |
| | | <TitleBox> |
| | | <template #left>年度趋势</template> |
| | | <template #right> |
| | | <div class="date-picker-box"> |
| | | <img src="~@/assets/img/home/date-icon.png" class="date-icon" /> |
| | | <span>2026</span> |
| | | <div |
| | | @click="() => $refs.yearTrendPicker.focus()" |
| | | class="date-picker-box relative cursor-pointer" |
| | | style="overflow: hidden;" |
| | | > |
| | | <el-date-picker |
| | | v-model="yearTrendDate" |
| | | ref="yearTrendPicker" |
| | | type="year" |
| | | placeholder="选择年" |
| | | value-format="yyyy" |
| | | class="custom-date-picker absolute top-0 left-0 w-full h-full opacity-0" |
| | | style="opacity: 0; position: absolute; z-index: -1;" |
| | | :clearable="false" |
| | | @change="handleYearTrendChange" |
| | | > |
| | | </el-date-picker> |
| | | <div class="flex a-center pointer-events-none" style="pointer-events: none;"> |
| | | <img src="~@/assets/img/home/date-icon.png" class="date-icon" /> |
| | | <span>{{ yearTrendDate || '2026' }}</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | |
| | | <TitleBox> |
| | | <template #left>总体订单统计</template> |
| | | <template #right> |
| | | <div class="date-range-picker-box"> |
| | | <div class="date-item"> |
| | | <img |
| | | src="~@/assets/img/home/date-icon.png" |
| | | class="date-icon" |
| | | /> |
| | | <span>2026-01-30</span> |
| | | </div> |
| | | <div class="date-item"> |
| | | <img |
| | | src="~@/assets/img/home/date-icon.png" |
| | | class="date-icon" |
| | | /> |
| | | <span>2026-05-30</span> |
| | | <div |
| | | @click="() => $refs.totalOrderPicker.focus()" |
| | | class="date-range-picker-box relative cursor-pointer" |
| | | style="overflow: hidden;" |
| | | > |
| | | <el-date-picker |
| | | v-model="totalOrderDateRange" |
| | | ref="totalOrderPicker" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="yyyy-MM-dd" |
| | | class="custom-date-range-picker" |
| | | style="opacity: 0; position: absolute; z-index: -1;" |
| | | :clearable="false" |
| | | @change="handleTotalOrderChange" |
| | | > |
| | | </el-date-picker> |
| | | <div class="flex pointer-events-none" style="pointer-events: none; display: flex; gap: 20px;"> |
| | | <div class="date-item"> |
| | | <img |
| | | src="~@/assets/img/home/date-icon.png" |
| | | class="date-icon" |
| | | /> |
| | | <span>{{ totalOrderDateRange ? totalOrderDateRange[0] : '2026-01-30' }}</span> |
| | | </div> |
| | | <div class="date-item"> |
| | | <img |
| | | src="~@/assets/img/home/date-icon.png" |
| | | class="date-icon" |
| | | /> |
| | | <span>{{ totalOrderDateRange ? totalOrderDateRange[1] : '2026-05-30' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <TitleBox> |
| | | <template #left>当日各时段订单情况</template> |
| | | <template #right> |
| | | <div class="time-picker-box"> |
| | | <img src="~@/assets/img/home/history-icon.png" class="time-icon" /> |
| | | <span>16~24时</span> |
| | | <div |
| | | @click="() => $refs.dayOrderPicker.focus()" |
| | | class="time-picker-box relative cursor-pointer" |
| | | > |
| | | <el-time-picker |
| | | is-range |
| | | v-model="dayOrderTimeRange" |
| | | ref="dayOrderPicker" |
| | | range-separator="~" |
| | | start-placeholder="开始" |
| | | end-placeholder="结束" |
| | | placeholder="选择时间范围" |
| | | value-format="HH" |
| | | format="HH时" |
| | | class="custom-time-picker" |
| | | style="opacity: 0; position: absolute; z-index: -1; width: 100%; right: 0;" |
| | | :clearable="false" |
| | | @blur="handleDayOrderTimeBlur" |
| | | > |
| | | </el-time-picker> |
| | | <div class="flex a-center pointer-events-none" style="pointer-events: none;"> |
| | | <img |
| | | src="~@/assets/img/home/history-icon.png" |
| | | class="time-icon" |
| | | /> |
| | | <span>{{ displayDayOrderTimeRange ? displayDayOrderTimeRange[0] + '~' + displayDayOrderTimeRange[1] + '时' : '16~24时' }}</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | |
| | | <!-- 右侧 --> |
| | | <div class="right-content"> |
| | | <!-- 司机接单排名 --> |
| | | <div class="driver-order-rank"></div> |
| | | <!-- 乘客排名 --> |
| | | <div class="driver-order-rank"> |
| | | <TitleBox> |
| | | <template #left>司机接单排名</template> |
| | | <template #right> |
| | | <div class="filter-btn-group"> |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: driverRankFilter === 'day' }" |
| | | @click="driverRankFilter = 'day'" |
| | | > |
| | | 今日 |
| | | </div> |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: driverRankFilter === 'week' }" |
| | | @click="driverRankFilter = 'week'" |
| | | > |
| | | 本周 |
| | | </div> |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: driverRankFilter === 'month' }" |
| | | @click="driverRankFilter = 'month'" |
| | | > |
| | | 本月 |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | | <div class="driver-rank-content"> |
| | | <!-- 前三名 --> |
| | | <div class="top-three"> |
| | | <!-- 第二名 --> |
| | | <div class="top-card card-2" v-if="driverRankList[1]"> |
| | | <div class="order-count">{{ formatNumber(driverRankList[1].count) }}</div> |
| | | <div class="plate-number">{{ driverRankList[1].plate }}</div> |
| | | </div> |
| | | <!-- 第一名 --> |
| | | <div class="top-card card-1" v-if="driverRankList[0]"> |
| | | <div class="order-count">{{ formatNumber(driverRankList[0].count) }}</div> |
| | | <div class="plate-number">{{ driverRankList[0].plate }}</div> |
| | | </div> |
| | | <!-- 第三名 --> |
| | | <div class="top-card card-3" v-if="driverRankList[2]"> |
| | | <div class="order-count">{{ formatNumber(driverRankList[2].count) }}</div> |
| | | <div class="plate-number">{{ driverRankList[2].plate }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第4-10名 --> |
| | | <div class="other-rank-list"> |
| | | <div |
| | | class="other-rank-item" |
| | | v-for="(item, index) in driverRankList.slice(3)" |
| | | :key="index + 3" |
| | | > |
| | | <div class="left-top-triangle"></div> |
| | | <div class="rank-index-hex">{{ index + 4 }}</div> |
| | | <div class="plate-number">{{ item.plate }}</div> |
| | | <div class="progress-bar-container"> |
| | | <div |
| | | class="progress-bar" |
| | | :style="{ |
| | | width: (item.count / maxDriverOrderCount) * 100 + '%', |
| | | }" |
| | | ></div> |
| | | </div> |
| | | <div class="order-count">{{ formatNumber(item.count) }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 乘客排名 --> |
| | | <div class="passenger-rank"> |
| | | <TitleBox> |
| | | <template #left>乘客排名</template> |
| | | <template #right> |
| | | <div class="filter-btn-group"> |
| | | <div |
| | | class="filter-btn" |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: passengerRankFilter === 'day' }" |
| | | @click="passengerRankFilter = 'day'" |
| | | > |
| | | 今日 |
| | | </div> |
| | | <div |
| | | class="filter-btn" |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: passengerRankFilter === 'week' }" |
| | | @click="passengerRankFilter = 'week'" |
| | | > |
| | | 本周 |
| | | </div> |
| | | <div |
| | | class="filter-btn" |
| | | <div |
| | | class="filter-btn" |
| | | :class="{ active: passengerRankFilter === 'month' }" |
| | | @click="passengerRankFilter = 'month'" |
| | | > |
| | |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | | |
| | | |
| | | <div class="rank-list-container"> |
| | | <div class="rank-header"> |
| | | <span class="header-index"></span> |
| | |
| | | <span class="header-orders">完成订单数</span> |
| | | </div> |
| | | <div class="rank-list"> |
| | | <div |
| | | class="rank-item" |
| | | v-for="(item, index) in passengerRankList" |
| | | <div |
| | | class="rank-item" |
| | | v-for="(item, index) in passengerRankList" |
| | | :key="index" |
| | | :class="'bg-' + (index < 3 ? index + 1 : 4)" |
| | | > |
| | | <div class="rank-index-wrapper"> |
| | | TOP <span class="rank-number" :class="'rank-' + (index < 3 ? index + 1 : 4)">{{ index + 1 }}</span> |
| | | TOP |
| | | <span |
| | | class="rank-number" |
| | | :class="'rank-' + (index < 3 ? index + 1 : 4)" |
| | | >{{ index + 1 }}</span |
| | | > |
| | | </div> |
| | | <div class="item-phone">{{ item.phone }}</div> |
| | | <div class="item-count">{{ item.count }}</div> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { formatTime } from "@/utils/time"; |
| | | import { formatTime, formatNumber } from "@/utils/utils"; |
| | | import TitleBox from "@/components/TitleBox.vue"; |
| | | import { initYearTrendChart } from "./charts/yearTrend"; |
| | | import { initTotalOrderChart } from "./charts/totalOrder"; |
| | |
| | | newPassengerFilter: "month", |
| | | /** @type {string} 订单量筛选类型: 'day' 日订单量 | 'month' 月订单量 */ |
| | | orderVolumeFilter: "month", |
| | | /** @type {string} 司机接单排名筛选类型: 'day' | 'week' | 'month' */ |
| | | driverRankFilter: "day", |
| | | /** @type {Array} 司机接单排名数据 */ |
| | | driverRankList: [], |
| | | /** @type {string} 乘客排名筛选类型: 'day' | 'week' | 'month' */ |
| | | passengerRankFilter: "day", |
| | | /** @type {Array} 乘客排名数据 */ |
| | |
| | | orderVolumeChartInstance: null, |
| | | /** @type {Object|null} 当日各时段订单情况图表实例 */ |
| | | dayOrderVolumeChartInstance: null, |
| | | /** @type {string} 年度趋势选择年份 */ |
| | | yearTrendDate: "2026", |
| | | /** @type {Array<string>} 总体订单统计时间段 */ |
| | | totalOrderDateRange: ["2026-01-30", "2026-05-30"], |
| | | /** @type {Array<string>} 单日各时段订单情况时间段 */ |
| | | dayOrderTimeRange: ["16", "24"], |
| | | /** @type {Array<string>} 实际用于显示的单日各时段订单情况时间段 */ |
| | | displayDayOrderTimeRange: ["16", "24"], |
| | | }; |
| | | }, |
| | | computed: {}, |
| | | computed: { |
| | | /** |
| | | * @returns {number} 司机排名列表中最大的接单次数,用于计算进度条比例 |
| | | */ |
| | | maxDriverOrderCount() { |
| | | if (!this.driverRankList || this.driverRankList.length === 0) return 1; |
| | | return Math.max(...this.driverRankList.map((item) => item.count)); |
| | | } |
| | | }, |
| | | watch: { |
| | | /** |
| | | * 监听新增乘客的筛选条件,改变时重新渲染图表 |
| | |
| | | this.initOrderVolumeChart(); |
| | | }, |
| | | /** |
| | | * 监听司机排名的筛选条件,改变时更新数据 |
| | | */ |
| | | driverRankFilter() { |
| | | this.fetchDriverRankData(); |
| | | }, |
| | | /** |
| | | * 监听乘客排名的筛选条件,改变时更新数据 |
| | | */ |
| | | passengerRankFilter() { |
| | | this.fetchPassengerRankData(); |
| | | }, |
| | | /** |
| | | * 监听当日各时段订单的时间变化,因为 el-time-picker 会在滚动时实时触发 change |
| | | * 我们改为监听其 visible 状态(通过 watch 或 blur 等),或者只监听用户主动操作完成后的数据变化 |
| | | */ |
| | | dayOrderTimeRange(newVal, oldVal) { |
| | | // 在 Vue 的 watch 中不做任何事,将逻辑移交给 Element UI 隐藏时间选择器的 blur 事件 |
| | | } |
| | | }, |
| | | created() { |
| | |
| | | } |
| | | }, |
| | | mounted() { |
| | | // 首次挂载时传入默认静态数据,防止 X 轴默认 16~24 却只有 12 条数据导致的图表不匹配 |
| | | const defaultDayOrderData = [85, 130, 185, 145, 120, 180, 170, 160, 215]; |
| | | |
| | | this.initChart(); |
| | | this.initTotalOrderChart(); |
| | | this.initNewPassengerChart(); |
| | | this.initOrderVolumeChart(); |
| | | this.initDayOrderVolumeChart(); |
| | | this.initDayOrderVolumeChart(defaultDayOrderData); |
| | | this.fetchDriverRankData(); |
| | | this.fetchPassengerRankData(); |
| | | |
| | | |
| | | window.addEventListener("resize", this.handleResize); |
| | | }, |
| | | destroyed() { |
| | | window.removeEventListener("resize", this.handleResize); |
| | | }, |
| | | methods: { |
| | | formatNumber, |
| | | handleYearTrendChange(val) { |
| | | console.log('年度趋势年份改变:', val); |
| | | // 模拟数据改变 |
| | | const newData = Array.from({ length: 12 }, () => Math.floor(Math.random() * 3000) + 1000); |
| | | this.initChart(newData); |
| | | }, |
| | | handleTotalOrderChange(val) { |
| | | console.log('总体订单时间段改变:', val); |
| | | // 模拟数据改变 |
| | | this.orderStats = this.orderStats.map(item => ({ |
| | | ...item, |
| | | value: Math.floor(Math.random() * 800) + 200, |
| | | percent: Math.floor(Math.random() * 20 + 5) + '%' |
| | | })); |
| | | this.initTotalOrderChart(); |
| | | }, |
| | | /** |
| | | * 更新顶部时间显示 |
| | | */ |
| | |
| | | }, |
| | | /** |
| | | * 初始化年度趋势图表 |
| | | * @param {Array} seriesData 模拟的新数据,如果为空则使用默认值 |
| | | */ |
| | | initChart() { |
| | | initChart(seriesData) { |
| | | if (this.yearTrendChartInstance) { |
| | | this.yearTrendChartInstance.dispose(); |
| | | } |
| | | this.yearTrendChartInstance = initYearTrendChart( |
| | | this.$refs.yearTrendChart |
| | | this.$refs.yearTrendChart, |
| | | this.yearTrendDate, |
| | | seriesData |
| | | ); |
| | | }, |
| | | /** |
| | |
| | | }, |
| | | /** |
| | | * 初始化当日各时段订单情况图表数据 |
| | | * @param {Array} seriesData 模拟的新数据,如果为空则使用默认值 |
| | | */ |
| | | initDayOrderVolumeChart() { |
| | | initDayOrderVolumeChart(seriesData) { |
| | | if (this.dayOrderVolumeChartInstance) { |
| | | this.dayOrderVolumeChartInstance.dispose(); |
| | | } |
| | | |
| | | const xData = [ |
| | | "01", |
| | | "02", |
| | | "03", |
| | | "04", |
| | | "05", |
| | | "06", |
| | | "07", |
| | | "08", |
| | | "09", |
| | | "10", |
| | | "11", |
| | | "12", |
| | | ]; |
| | | const seriesData = [ |
| | | 85, 130, 185, 145, 120, 180, 170, 160, 215, 120, 85, 85, |
| | | ]; |
| | | // 动态生成 X 轴的小时数据 |
| | | let startHour = 1; |
| | | let endHour = 12; |
| | | |
| | | if (this.dayOrderTimeRange && this.dayOrderTimeRange.length === 2) { |
| | | startHour = parseInt(this.dayOrderTimeRange[0]); |
| | | endHour = parseInt(this.dayOrderTimeRange[1]); |
| | | } |
| | | |
| | | const xData = []; |
| | | const actualSeriesData = seriesData || []; |
| | | |
| | | // 如果没有传模拟数据,或者修改了时间范围,生成一些新的模拟数据 |
| | | if (!seriesData) { |
| | | for (let i = startHour; i <= endHour; i++) { |
| | | xData.push(String(i).padStart(2, '0')); |
| | | actualSeriesData.push(Math.floor(Math.random() * 200) + 50); |
| | | } |
| | | } else { |
| | | // 如果传入了固定数据(例如第一次初始化时) |
| | | for (let i = startHour; i <= endHour; i++) { |
| | | xData.push(String(i).padStart(2, '0')); |
| | | } |
| | | } |
| | | |
| | | this.dayOrderVolumeChartInstance = initDayOrderVolumeChart( |
| | | this.$refs.dayOrderVolumeChart, |
| | | xData, |
| | | seriesData |
| | | actualSeriesData |
| | | ); |
| | | }, |
| | | handleDayOrderTimeBlur(component) { |
| | | // Element UI 时间范围选择器失去焦点时(即选择完成点击面板外部或确认后)触发 |
| | | const val = this.dayOrderTimeRange; |
| | | if (val && val.join(',') !== this.displayDayOrderTimeRange.join(',')) { |
| | | this.handleDayOrderTimeChange(val); |
| | | } |
| | | }, |
| | | handleDayOrderTimeChange(val) { |
| | | console.log('单日各时段订单时间段确认:', val); |
| | | if (val) { |
| | | this.displayDayOrderTimeRange = val; |
| | | } |
| | | this.initDayOrderVolumeChart(); // 调用时会自动根据新的 dayOrderTimeRange 生成随机数据 |
| | | }, |
| | | /** |
| | | * 获取司机接单排名数据 |
| | | */ |
| | | fetchDriverRankData() { |
| | | // 模拟接口请求数据 |
| | | let multiplier = |
| | | this.driverRankFilter === "month" |
| | | ? 30 |
| | | : this.driverRankFilter === "week" |
| | | ? 7 |
| | | : 1; |
| | | // 模拟 10 条数据 |
| | | this.driverRankList = [ |
| | | { plate: "豫GD99832", count: 1294 * multiplier }, |
| | | { plate: "豫GD99832", count: 889 * multiplier }, |
| | | { plate: "豫GD99832", count: 438 * multiplier }, |
| | | ...Array.from({ length: 7 }, (_, i) => ({ |
| | | plate: "豫GD99832", |
| | | count: Math.floor(3234 - i * 50) * multiplier, |
| | | })), |
| | | ]; |
| | | }, |
| | | /** |
| | | * 获取乘客排名数据 |
| | | */ |
| | | fetchPassengerRankData() { |
| | | // 模拟接口请求数据 |
| | | let multiplier = this.passengerRankFilter === 'month' ? 30 : this.passengerRankFilter === 'week' ? 7 : 1; |
| | | let multiplier = |
| | | this.passengerRankFilter === "month" |
| | | ? 30 |
| | | : this.passengerRankFilter === "week" |
| | | ? 7 |
| | | : 1; |
| | | this.passengerRankList = Array.from({ length: 8 }, (_, i) => ({ |
| | | phone: '15215268013', |
| | | phone: "15215268013", |
| | | count: Math.floor((88 - i * 5) * multiplier), |
| | | orders: Math.floor((335 - i * 15) * multiplier) |
| | | orders: Math.floor((335 - i * 15) * multiplier), |
| | | })); |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | |
| | | @import "./styles/new-passenger.less"; |
| | | @import "./styles/order-volume.less"; |
| | | @import "./styles/day-order-volume.less"; |
| | | @import "./styles/driver-order-rank.less"; |
| | | @import "./styles/passenger-rank.less"; |
| | | </style> |