|
|
@@ -8,7 +8,7 @@
|
|
|
<div class="title">早安,管理员</div>
|
|
|
<div class="subtitle">数据监控中心 | {{ currentDate }}</div>
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- 右侧统计 -->
|
|
|
<div class="right-section">
|
|
|
<div class="stat-item" @click="handleToFulfillerReview">
|
|
|
@@ -122,9 +122,9 @@
|
|
|
<!-- 去掉本月标签 -->
|
|
|
</div>
|
|
|
<div class="rank-list" v-if="fulfillerRankList.length > 0">
|
|
|
- <div
|
|
|
- class="rank-item"
|
|
|
- v-for="(item, index) in fulfillerRankList"
|
|
|
+ <div
|
|
|
+ class="rank-item"
|
|
|
+ v-for="(item, index) in fulfillerRankList"
|
|
|
:key="item.id || index"
|
|
|
>
|
|
|
<div class="item-left">
|
|
|
@@ -135,14 +135,14 @@
|
|
|
<el-avatar class="rank-avatar" :size="40" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
|
|
|
<div class="rank-info">
|
|
|
<div class="rank-name">{{ item.name }}</div>
|
|
|
- <div class="rank-site">{{ item.site }}</div>
|
|
|
+ <div class="rank-site">{{ getStationName(item.site) }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="item-right">
|
|
|
<div class="rank-count">{{ item.count }} 单</div>
|
|
|
<div class="rank-bar-bg">
|
|
|
- <div
|
|
|
- class="rank-bar-fill"
|
|
|
+ <div
|
|
|
+ class="rank-bar-fill"
|
|
|
:class="'bar-color-' + (index + 1)"
|
|
|
:style="{ width: getPercentage(item.count) + '%' }"
|
|
|
></div>
|
|
|
@@ -166,9 +166,9 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="rank-list" v-if="storeRankList.length > 0">
|
|
|
- <div
|
|
|
- class="rank-item"
|
|
|
- v-for="(item, index) in storeRankList"
|
|
|
+ <div
|
|
|
+ class="rank-item"
|
|
|
+ v-for="(item, index) in storeRankList"
|
|
|
:key="item.id || index"
|
|
|
>
|
|
|
<div class="item-left">
|
|
|
@@ -187,10 +187,10 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="item-right store-right">
|
|
|
- <div class="rank-count">¥ {{ Number(item.count).toLocaleString() }}</div>
|
|
|
+ <div class="rank-count">{{ Number(item.count).toLocaleString() }}</div>
|
|
|
<div class="rank-bar-bg">
|
|
|
- <div
|
|
|
- class="rank-bar-fill"
|
|
|
+ <div
|
|
|
+ class="rank-bar-fill"
|
|
|
:class="'store-bar-color-' + (index + 1)"
|
|
|
:style="{ width: getStorePercentage(item.count) + '%' }"
|
|
|
></div>
|
|
|
@@ -211,14 +211,15 @@
|
|
|
import { ref, onMounted, onUnmounted, markRaw } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
import { getAdminCount, listOrder, getFulfillerRank, getStoreRank } from '@/api/system/admin';
|
|
|
-import type { FulfillerRankVO, StoreRankVO } from '@/api/system/admin/types';
|
|
|
+import type { FulfillerRankVO, StoreRankVO, AdminCountVO } from '@/api/system/admin/types';
|
|
|
import { listAllService } from '@/api/service/list';
|
|
|
+import { listAreaStation } from '@/api/system/areaStation';
|
|
|
import { BellFilled, StarFilled, Shop, WalletFilled, List, Bicycle, OfficeBuilding } from '@element-plus/icons-vue';
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
import 'dayjs/locale/zh-cn';
|
|
|
-dayjs.locale('zh-cn');
|
|
|
+dayjs.locale('zh-cn');
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
@@ -250,6 +251,8 @@ const getTrendStr = (current: number, prev: number, checkSign: boolean = true) =
|
|
|
|
|
|
// 获取相关服务记录数组以便于格式化字典
|
|
|
const serviceOptions = ref<any[]>([]);
|
|
|
+// 获取站点列表以解析履约者排名中的站点名称
|
|
|
+const stationOptions = ref<any[]>([]);
|
|
|
|
|
|
// 数据交互及图表实例
|
|
|
const listType = ref(0); // 0: 周, 1: 月
|
|
|
@@ -324,6 +327,23 @@ const fetchServiceList = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/** 请求站点列表以解析排名中的站点名称 */
|
|
|
+const fetchStationList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await listAreaStation();
|
|
|
+ stationOptions.value = res.data || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取站点列表失败', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 根据站点ID获取站点名称 */
|
|
|
+const getStationName = (id: string | number) => {
|
|
|
+ if (!id || stationOptions.value.length === 0) return '未知站点';
|
|
|
+ const station = stationOptions.value.find(s => String(s.id) === String(id));
|
|
|
+ return station ? station.name : '未知站点';
|
|
|
+};
|
|
|
+
|
|
|
/** 请求双图表的趋势及占比数据 */
|
|
|
const fetchChartData = async () => {
|
|
|
try {
|
|
|
@@ -346,7 +366,7 @@ const renderLineChart = (data: any[]) => {
|
|
|
}
|
|
|
|
|
|
const daysCount = listType.value === 0 ? 7 : 30;
|
|
|
-
|
|
|
+
|
|
|
// 自过去 N 天往前推算,初始化日期结构以防断层
|
|
|
const dates: string[] = [];
|
|
|
const statMap: Record<string, { count: number, price: number }> = {};
|
|
|
@@ -457,20 +477,23 @@ const renderPieChart = (data: any[]) => {
|
|
|
}
|
|
|
|
|
|
let totalOrdersCount = data.length;
|
|
|
- const serviceCountMap: Record<number, number> = {};
|
|
|
-
|
|
|
+ const serviceCountMap: Record<string, number> = {};
|
|
|
+
|
|
|
data.forEach(item => {
|
|
|
- serviceCountMap[item.service] = (serviceCountMap[item.service] || 0) + 1;
|
|
|
+ const sId = String(item.service || '');
|
|
|
+ if (sId) {
|
|
|
+ serviceCountMap[sId] = (serviceCountMap[sId] || 0) + 1;
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
- const getServiceNameById = (serviceId: number) => {
|
|
|
- const matchedService = serviceOptions.value.find(s => s.id === serviceId);
|
|
|
+ const getServiceNameById = (serviceId: string) => {
|
|
|
+ const matchedService = serviceOptions.value.find(s => String(s.id) === serviceId);
|
|
|
return matchedService ? matchedService.name : '其他服务';
|
|
|
};
|
|
|
|
|
|
const pieData = Object.keys(serviceCountMap).map(key => ({
|
|
|
- name: getServiceNameById(Number(key)),
|
|
|
- value: serviceCountMap[Number(key)]
|
|
|
+ name: getServiceNameById(key),
|
|
|
+ value: serviceCountMap[key]
|
|
|
}));
|
|
|
|
|
|
const option: echarts.EChartsOption = {
|
|
|
@@ -538,9 +561,10 @@ const resizeCharts = () => {
|
|
|
onMounted(async () => {
|
|
|
initDate();
|
|
|
window.addEventListener('resize', resizeCharts);
|
|
|
-
|
|
|
+
|
|
|
// 按照先后依赖完成视图初始化
|
|
|
await fetchServiceList();
|
|
|
+ fetchStationList();
|
|
|
fetchAdminCount();
|
|
|
fetchChartData();
|
|
|
fetchFulfillerRank();
|
|
|
@@ -567,7 +591,7 @@ onUnmounted(() => {
|
|
|
background-color: #fff;
|
|
|
margin-bottom: 20px;
|
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.02);
|
|
|
-
|
|
|
+
|
|
|
:deep(.el-card__body) {
|
|
|
padding: 24px 32px;
|
|
|
}
|
|
|
@@ -587,7 +611,7 @@ onUnmounted(() => {
|
|
|
margin-bottom: 12px;
|
|
|
letter-spacing: 0.5px;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.subtitle {
|
|
|
font-size: 14px;
|
|
|
color: #86909c;
|
|
|
@@ -629,7 +653,7 @@ onUnmounted(() => {
|
|
|
color: #ff7d00;
|
|
|
background-color: #fff2e8;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
&.primary {
|
|
|
color: #409eff;
|
|
|
background-color: #e6f1fc;
|
|
|
@@ -673,7 +697,7 @@ onUnmounted(() => {
|
|
|
justify-content: space-between;
|
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
|
|
box-sizing: border-box;
|
|
|
-
|
|
|
+
|
|
|
&.bg-blue {
|
|
|
background: linear-gradient(135deg, #35c8fe 0%, #20b2aa 100%);
|
|
|
}
|
|
|
@@ -686,25 +710,25 @@ onUnmounted(() => {
|
|
|
&.bg-green {
|
|
|
background: linear-gradient(135deg, #79e0b1 0%, #85e89d 100%);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.card-title {
|
|
|
font-size: 14px;
|
|
|
font-weight: 500;
|
|
|
opacity: 0.9;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.card-value {
|
|
|
font-size: 28px;
|
|
|
font-weight: bold;
|
|
|
line-height: 1.2;
|
|
|
z-index: 1;
|
|
|
-
|
|
|
+
|
|
|
.currency {
|
|
|
font-size: 18px;
|
|
|
margin-right: 2px;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.card-footer {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
@@ -712,11 +736,11 @@ onUnmounted(() => {
|
|
|
font-size: 12px;
|
|
|
opacity: 0.9;
|
|
|
z-index: 1;
|
|
|
-
|
|
|
+
|
|
|
.footer-text {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.trend-tag {
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
padding: 2px 8px;
|
|
|
@@ -725,7 +749,7 @@ onUnmounted(() => {
|
|
|
display: inline-block;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.card-bg-icon {
|
|
|
position: absolute;
|
|
|
right: -10px;
|
|
|
@@ -747,7 +771,7 @@ onUnmounted(() => {
|
|
|
background-color: #fff;
|
|
|
height: 100%;
|
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.02);
|
|
|
-
|
|
|
+
|
|
|
:deep(.el-card__body) {
|
|
|
padding: 24px;
|
|
|
}
|
|
|
@@ -758,14 +782,14 @@ onUnmounted(() => {
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 24px;
|
|
|
-
|
|
|
+
|
|
|
.chart-title {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
color: #1d2129;
|
|
|
-
|
|
|
+
|
|
|
.title-icon {
|
|
|
width: 4px;
|
|
|
height: 14px;
|
|
|
@@ -805,7 +829,7 @@ onUnmounted(() => {
|
|
|
align-items: center;
|
|
|
padding: 10px 16px;
|
|
|
border-radius: 8px;
|
|
|
-
|
|
|
+
|
|
|
&:nth-child(even) {
|
|
|
background-color: #f7f8fa;
|
|
|
}
|
|
|
@@ -815,10 +839,10 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
-
|
|
|
+
|
|
|
.store-avatar {
|
|
|
background-color: #e8f5e9;
|
|
|
- color: #67c23a;
|
|
|
+ color: #67c23a;
|
|
|
border-radius: 6px;
|
|
|
}
|
|
|
}
|
|
|
@@ -834,7 +858,7 @@ onUnmounted(() => {
|
|
|
font-weight: bold;
|
|
|
color: #fff;
|
|
|
margin-right: 2px;
|
|
|
-
|
|
|
+
|
|
|
&.rank-1 { background-color: #ffc53d; }
|
|
|
&.rank-2 { background-color: #c9cdd4; }
|
|
|
&.rank-3 { background-color: #d28248; }
|
|
|
@@ -846,18 +870,18 @@ onUnmounted(() => {
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
gap: 2px;
|
|
|
-
|
|
|
+
|
|
|
.rank-name {
|
|
|
font-size: 14px;
|
|
|
font-weight: 500;
|
|
|
color: #1d2129;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.rank-site {
|
|
|
font-size: 12px;
|
|
|
color: #86909c;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.rank-score {
|
|
|
font-size: 12px;
|
|
|
color: #e6a23c;
|
|
|
@@ -875,36 +899,36 @@ onUnmounted(() => {
|
|
|
align-items: flex-end;
|
|
|
gap: 6px;
|
|
|
width: 90px;
|
|
|
-
|
|
|
+
|
|
|
.rank-count {
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
color: #1d2129;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
.rank-bar-bg {
|
|
|
width: 100%;
|
|
|
height: 4px;
|
|
|
background-color: #f2f3f5;
|
|
|
border-radius: 2px;
|
|
|
overflow: hidden;
|
|
|
-
|
|
|
+
|
|
|
.rank-bar-fill {
|
|
|
height: 100%;
|
|
|
border-radius: 2px;
|
|
|
transition: width 0.5s ease;
|
|
|
-
|
|
|
+
|
|
|
&.bar-color-1 { background-color: #f56c6c; }
|
|
|
&.bar-color-2 { background-color: #e6a23c; }
|
|
|
&.bar-color-3 { background-color: #409eff; }
|
|
|
&.bar-color-4, &.bar-color-5 { background-color: #909399; }
|
|
|
-
|
|
|
+
|
|
|
&.store-bar-color-1 { background-color: #67c23a; }
|
|
|
&.store-bar-color-2 { background-color: #409eff; }
|
|
|
&.store-bar-color-3, &.store-bar-color-4, &.store-bar-color-5 { background-color: #909399; }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
&.store-right {
|
|
|
width: 100px;
|
|
|
}
|