126 lines
6.7 KiB
Vue
126 lines
6.7 KiB
Vue
<template>
|
|
<div class="dashboard">
|
|
<a-spin :loading="loading" class="dashboard-spin">
|
|
<a-grid :cols="{ xs: 1, sm: 2, lg: 4 }" :col-gap="16" :row-gap="16">
|
|
<a-grid-item v-for="item in cards" :key="item.key">
|
|
<a-card :bordered="false" class="metric-card">
|
|
<a-statistic :title="item.label" :value="overview[item.key]" :precision="item.money ? 2 : 0"
|
|
:prefix="item.money ? '¥' : undefined" show-group-separator />
|
|
<div class="metric-hint">{{ item.hint }}</div>
|
|
</a-card>
|
|
</a-grid-item>
|
|
</a-grid>
|
|
|
|
<a-card title="快捷操作" :bordered="false" class="section-card">
|
|
<a-grid :cols="{ xs: 2, sm: 3, md: 4, lg: 6 }" :col-gap="12" :row-gap="12">
|
|
<a-grid-item v-for="action in availableActions" :key="action.route">
|
|
<a-button long class="quick-action" @click="router.push({ name: action.route })">
|
|
<template #icon><component :is="action.icon" /></template>
|
|
{{ action.label }}
|
|
</a-button>
|
|
</a-grid-item>
|
|
</a-grid>
|
|
</a-card>
|
|
|
|
<a-grid :cols="{ xs: 1, lg: 2 }" :col-gap="16" :row-gap="16" class="section-card">
|
|
<a-grid-item><a-card title="近 7 日配送订单趋势" :bordered="false"><Chart :option="orderTrendOption" height="320px" /></a-card></a-grid-item>
|
|
<a-grid-item><a-card title="订单状态分布" :bordered="false"><Chart :option="orderStatusOption" height="320px" /></a-card></a-grid-item>
|
|
<a-grid-item><a-card title="气瓶状态分布" :bordered="false"><Chart :option="productStatusOption" height="320px" /></a-card></a-grid-item>
|
|
<a-grid-item><a-card title="支付渠道实收金额" :bordered="false"><Chart :option="paymentChannelOption" height="320px" /></a-card></a-grid-item>
|
|
</a-grid>
|
|
</a-spin>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Message } from '@arco-design/web-vue';
|
|
import { IconApps, IconFile, IconPlus, IconSettings, IconStorage, IconUser } from '@arco-design/web-vue/es/icon';
|
|
import type { EChartsOption } from 'echarts';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { platformApi, type DashboardOverview } from '@/api/platform';
|
|
import { useUserStore } from '@/store';
|
|
|
|
type CountKey = 'gas_basic_count' | 'delivery_basic_count' | 'staff_count' | 'user_count' |
|
|
'product_count' | 'active_contract_count' | 'today_order_count' | 'today_order_amount' |
|
|
'pending_ticket_count' | 'paid_amount';
|
|
|
|
const emptyOverview = (): DashboardOverview => ({
|
|
gas_basic_count: 0, delivery_basic_count: 0, staff_count: 0, user_count: 0,
|
|
product_count: 0, active_contract_count: 0, today_order_count: 0,
|
|
today_order_amount: 0, pending_ticket_count: 0, paid_amount: 0,
|
|
order_statuses: [], product_statuses: [], payment_channels: [], recent_orders: [],
|
|
});
|
|
const loading = ref(false);
|
|
const overview = ref<DashboardOverview>(emptyOverview());
|
|
const router = useRouter();
|
|
const userStore = useUserStore();
|
|
const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] = [
|
|
{ key: 'gas_basic_count', label: '启用气站', hint: '当前正常运营' },
|
|
{ key: 'delivery_basic_count', label: '启用配送点', hint: '跨组织汇总' },
|
|
{ key: 'staff_count', label: '在岗人员', hint: '当前可接单' },
|
|
{ key: 'user_count', label: '启用客户', hint: '有效业主账户' },
|
|
{ key: 'product_count', label: '在册气瓶', hint: '已启用资产' },
|
|
{ key: 'active_contract_count', label: '生效合同', hint: '当前履约中' },
|
|
{ key: 'today_order_count', label: '今日订单', hint: '自然日新增' },
|
|
{ key: 'today_order_amount', label: '今日应付金额', hint: '订单口径', money: true },
|
|
{ key: 'pending_ticket_count', label: '待受理工单', hint: '客服待办' },
|
|
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
|
|
];
|
|
const actions = [
|
|
{ label: '新建气站', route: 'gas-basic', menu: 'gas_basic', icon: IconPlus },
|
|
{ label: '配送订单', route: 'gasorder-orders', menu: 'gasorder_basic', icon: IconFile },
|
|
{ label: '气瓶档案', route: 'product-info', menu: 'product_info', icon: IconStorage },
|
|
{ label: '用户管理', route: 'user-account', menu: 'user_account', icon: IconUser },
|
|
{ label: '商城订单', route: 'ec-orders', menu: 'ec_order', icon: IconApps },
|
|
{ label: '角色权限', route: 'platform-roles', menu: 'platform_role', icon: IconSettings },
|
|
];
|
|
const availableActions = computed(() => actions.filter(
|
|
(action) => userStore.role === 'root' || userStore.menuCodes.includes(action.menu),
|
|
));
|
|
const centsToYuan = (value: number) => value / 100;
|
|
const pieOption = (data: { name: string; value: number }[]): EChartsOption => ({
|
|
tooltip: { trigger: 'item' }, legend: { bottom: 0 },
|
|
series: [{ type: 'pie', radius: ['42%', '68%'], center: ['50%', '44%'], data, label: { formatter: '{b}\\n{c}' } }],
|
|
});
|
|
const orderStatusOption = computed(() => pieOption(overview.value.order_statuses));
|
|
const productStatusOption = computed(() => pieOption(overview.value.product_statuses));
|
|
const orderTrendOption = computed<EChartsOption>(() => ({
|
|
tooltip: { trigger: 'axis' }, legend: { data: ['订单数', '订单金额'] },
|
|
grid: { left: 48, right: 58, bottom: 34 },
|
|
xAxis: { type: 'category', data: overview.value.recent_orders.map((item) => item.date.slice(5)) },
|
|
yAxis: [{ type: 'value', name: '单' }, { type: 'value', name: '元' }],
|
|
series: [
|
|
{ name: '订单数', type: 'bar', data: overview.value.recent_orders.map((item) => item.order_count) },
|
|
{ name: '订单金额', type: 'line', yAxisIndex: 1, smooth: true, data: overview.value.recent_orders.map((item) => centsToYuan(item.order_amount)) },
|
|
],
|
|
}));
|
|
const paymentChannelOption = computed<EChartsOption>(() => ({
|
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: 70, right: 24, bottom: 28 },
|
|
xAxis: { type: 'value', name: '元' },
|
|
yAxis: { type: 'category', data: overview.value.payment_channels.map((item) => item.name || '未知渠道') },
|
|
series: [{ type: 'bar', data: overview.value.payment_channels.map((item) => centsToYuan(item.value)) }],
|
|
}));
|
|
|
|
onMounted(async () => {
|
|
loading.value = true;
|
|
try {
|
|
const data = await platformApi.overview();
|
|
overview.value = { ...data, today_order_amount: centsToYuan(data.today_order_amount), paid_amount: centsToYuan(data.paid_amount) };
|
|
} catch (error) {
|
|
Message.error((error as Error).message);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.dashboard { padding: 20px; }
|
|
.dashboard-spin { width: 100%; }
|
|
.metric-card { min-height: 120px; }
|
|
.metric-hint { margin-top: 8px; color: var(--color-text-3); font-size: 12px; }
|
|
.section-card { margin-top: 16px; }
|
|
.quick-action { height: 52px; justify-content: flex-start; padding: 0 18px; }
|
|
</style>
|