157 lines
8.0 KiB
Vue
157 lines
8.0 KiB
Vue
<template>
|
||
<div class="dashboard">
|
||
<a-spin :loading="loading" class="dashboard-spin">
|
||
<a-alert v-if="errorMessage" type="error" show-icon class="dashboard-alert">
|
||
{{ errorMessage }}
|
||
<template #action><a-button size="small" @click="loadOverview">重新加载</a-button></template>
|
||
</a-alert>
|
||
<div class="dashboard-meta">统计口径以服务端为准 · 更新时间:{{ updatedAt || '尚未加载' }}</div>
|
||
<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"
|
||
show-group-separator
|
||
>
|
||
<template v-if="item.money" #prefix>¥</template>
|
||
</a-statistic>
|
||
<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 errorMessage = ref('');
|
||
const updatedAt = ref('');
|
||
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: 'organization-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 safeArray = <T,>(value: T[] | null | undefined): T[] =>
|
||
Array.isArray(value) ? value : [];
|
||
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(safeArray(overview.value.order_statuses)));
|
||
const productStatusOption = computed(() => pieOption(safeArray(overview.value.product_statuses)));
|
||
const orderTrendOption = computed<EChartsOption>(() => ({
|
||
tooltip: { trigger: 'axis' }, legend: { data: ['订单数', '订单金额'] },
|
||
grid: { left: 48, right: 58, bottom: 34 },
|
||
xAxis: { type: 'category', data: safeArray(overview.value.recent_orders).map((item) => item.date.slice(5)) },
|
||
yAxis: [{ type: 'value', name: '单' }, { type: 'value', name: '元' }],
|
||
series: [
|
||
{ name: '订单数', type: 'bar', data: safeArray(overview.value.recent_orders).map((item) => item.order_count) },
|
||
{ name: '订单金额', type: 'line', yAxisIndex: 1, smooth: true, data: safeArray(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: safeArray(overview.value.payment_channels).map((item) => item.name || '未知渠道') },
|
||
series: [{ type: 'bar', data: safeArray(overview.value.payment_channels).map((item) => centsToYuan(item.value)) }],
|
||
}));
|
||
|
||
async function loadOverview() {
|
||
loading.value = true;
|
||
errorMessage.value = '';
|
||
try {
|
||
const data = await platformApi.overview();
|
||
overview.value = {
|
||
...emptyOverview(),
|
||
...data,
|
||
today_order_amount: centsToYuan(data.today_order_amount ?? 0),
|
||
paid_amount: centsToYuan(data.paid_amount ?? 0),
|
||
order_statuses: safeArray(data.order_statuses),
|
||
product_statuses: safeArray(data.product_statuses),
|
||
payment_channels: safeArray(data.payment_channels),
|
||
recent_orders: safeArray(data.recent_orders),
|
||
};
|
||
updatedAt.value = new Date().toLocaleString('zh-CN', { hour12: false });
|
||
} catch (error) {
|
||
errorMessage.value = (error as Error).message;
|
||
Message.error(errorMessage.value);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(loadOverview);
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.dashboard { padding: 20px; }
|
||
.dashboard-spin { width: 100%; }
|
||
.dashboard-alert { margin-bottom: 16px; }
|
||
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
|
||
.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>
|