dman-web-admin/src/views/dashboard/Dashboard.vue
2025-04-08 10:48:26 +08:00

848 lines
23 KiB
Vue

<template>
<page-container>
<div class="dashboard">
<a-row :gutter="{ xs: 8, sm: 16, md: 24 }" class="data-overview">
<!-- 用户数据卡片 -->
<a-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" class="data-card-col">
<a-card class="data-card user-card">
<template #title>
<span class="card-title">
<span class="icon-wrapper user-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="20" height="20">
<path d="M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" fill="currentColor"></path>
</svg>
</span>
总用户
</span>
</template>
<div class="main-stat-row">
<a-statistic
title="总用户数"
:value="dashboardData.total_user_count || 0"
class="main-stat"
:value-style="{ color: '#722ed1' }"
/>
<a-statistic
title="下单用户数"
:value="dashboardData.has_order_user_count || 0"
class="main-stat"
:value-style="{ color: '#722ed1' }"
/>
</div>
<div class="sub-stats">
<a-statistic
title="完成配送用户数"
:value="dashboardData.has_order_completed_user_count || 0"
:value-style="{ color: '#722ed1' }"
/>
<a-statistic
title="复购用户数"
:value="dashboardData.repeat_user_count || 0"
:value-style="{ color: '#722ed1' }"
/>
<a-statistic
title="今日增加用户"
:value="dashboardData.today_user_count || 0"
:value-style="{ color: '#722ed1' }"
/>
</div>
</a-card>
</a-col>
<!-- 配送订单数据卡片 -->
<a-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" class="data-card-col">
<a-card class="data-card order-card">
<template #title>
<span class="card-title">
<span class="icon-wrapper order-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="20" height="20">
<path d="M832 312H696v-16c0-101.6-82.4-184-184-184s-184 82.4-184 184v16H192c-17.7 0-32 14.3-32 32v536c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V344c0-17.7-14.3-32-32-32z m-432-16c0-61.9 50.1-112 112-112s112 50.1 112 112v16H400v-16z m392 544H232V384h96v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h224v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h96v456z" fill="currentColor"></path>
</svg>
</span>
配送订单
</span>
</template>
<div class="main-stat-row">
<a-statistic
title="配送订单数"
:value="dashboardData.order_count || 0"
class="main-stat"
:value-style="{ color: '#fa8c16' }"
/>
<a-statistic
title="完成订单数"
:value="dashboardData.order_completed_count || 0"
class="main-stat"
:value-style="{ color: '#fa8c16' }"
/>
</div>
<div class="sub-stats">
<a-statistic
title="今日订单数"
:value="dashboardData.today_order_count || 0"
:value-style="{ color: '#fa8c16' }"
/>
<a-statistic
title="昨日订单数"
:value="dashboardData.yesterday_order_count || 0"
:value-style="{ color: '#fa8c16' }"
/>
</div>
</a-card>
</a-col>
<!-- 订单金额数据卡片 -->
<a-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" class="data-card-col">
<a-card class="data-card amount-card">
<template #title>
<span class="card-title">
<span class="icon-wrapper amount-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="20" height="20">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"></path>
<path d="M426.6 599.7V521H344c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h82.6v-78.7c0-4.4 3.6-8 8-8h52c4.4 0 8 3.6 8 8V457H592c4.4 0 8 3.6 8 8v48c0 4.4-3.6 8-8 8h-97.4v78.7c0 4.4-3.6 8-8 8h-52c-4.4 0-8-3.6-8-8z" fill="currentColor"></path>
</svg>
</span>
配送订单金额
</span>
</template>
<div class="main-stat-row">
<a-statistic
title="总配送金额"
:value="dashboardData.order_amount || 0"
:precision="1"
prefix="¥"
class="main-stat"
:value-style="{ color: '#52c41a' }"
/>
<a-statistic
title="完成配送金额"
:value="dashboardData.completed_order_amount || 0"
:precision="1"
prefix="¥"
class="main-stat"
:value-style="{ color: '#52c41a' }"
/>
</div>
<div class="sub-stats">
<a-statistic
title="今日订单金额"
:value="dashboardData.today_order_amount || 0"
:precision="1"
prefix="¥"
:value-style="{ color: '#52c41a' }"
/>
<a-statistic
title="昨日订单金额"
:value="dashboardData.yesterday_order_amount || 0"
:precision="1"
prefix="¥"
:value-style="{ color: '#52c41a' }"
/>
</div>
</a-card>
</a-col>
</a-row>
<!-- 每日统计列表 -->
<div class="daily-stats-section">
<div class="section-header">
<h2 class="section-title">
<span class="icon-wrapper stats-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="26" height="26">
<path d="M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" fill="currentColor"></path>
<path d="M288 712h56c4.4 0 8-3.6 8-8V550c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v154c0 4.4 3.6 8 8 8zM440 712h56c4.4 0 8-3.6 8-8V426c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v278c0 4.4 3.6 8 8 8zM592 712h56c4.4 0 8-3.6 8-8V302c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v402c0 4.4 3.6 8 8 8zM744 712h56c4.4 0 8-3.6 8-8V178c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v526c0 4.4 3.6 8 8 8z" fill="currentColor"></path>
</svg>
</span>
每日统计列表
</h2>
</div>
<a-table
:columns="dailyStatsColumns"
:data-source="dailyStatsList"
:pagination="dailyStatsPagination"
:loading="dailyStatsLoading"
row-key="stats_date"
class="daily-stats-table"
@change="handleDailyStatsTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'total_original_amount' || column.key === 'total_final_amount'">
¥{{ record[column.key].toFixed(2) }}
</template>
</template>
</a-table>
</div>
<!-- 配送员列表 -->
<div class="deliveryman-list-section">
<div class="section-header">
<h2 class="section-title">
<span class="icon-wrapper deliveryman-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="26" height="26">
<path d="M959 413.4L935.3 372.2c-2.2-3.8-7.1-5.1-10.9-2.9l-50.7 29.6-78.3-216.2c-8.5-26.5-33.1-44.4-60.9-44.4H301.2c-34.7 0-65.5 22.4-76.2 55.5l-74.6 205.2-50.8-29.6c-3.8-2.2-8.7-0.9-10.9 2.9L65 413.4c-2.2 3.8-0.9 8.6 2.9 10.8l60.4 35.2-14.5 40c-1.2 3.2-1.8 6.6-1.8 10v348.2c0 15.7 11.8 28.4 26.3 28.4h67.6c12.3 0 23-9.3 25.6-22.3l7.7-37.7h545.6l7.7 37.7c2.7 13 13.3 22.3 25.6 22.3h67.6c14.5 0 26.3-12.7 26.3-28.4V509.4c0-3.4-0.6-6.8-1.8-10l-14.5-40 60.3-35.2c3.8-2.2 5.1-7 3-10.8zM264 621c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40z m388 75c0 4.4-3.6 8-8 8H380c-4.4 0-8-3.6-8-8v-84c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v36h168v-36c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v84z m108-75c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zM220 418l72.7-199.9c1.4-3.8 4.9-6.1 8.9-6.1h420.8c4 0 7.5 2.3 8.9 6.1l72.7 199.9H220z" fill="currentColor"></path>
</svg>
</span>
配送员列表
</h2>
<div class="filter-container">
<a-select
v-model:value="selectedCommunity"
placeholder="选择小区筛选"
style="width: 240px"
:loading="communityLoading"
@change="handleCommunityChange"
allowClear
>
<a-select-option :value="0">全部小区</a-select-option>
<a-select-option v-for="item in communityList" :key="item.id" :value="item.id">
{{ item.name }}
</a-select-option>
</a-select>
</div>
</div>
<a-table
:columns="deliverymanColumns"
:data-source="deliverymanList"
:pagination="false"
:loading="loading"
row-key="deliveryman_id"
class="deliveryman-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'today_order_amount' || column.key === 'order_amount'">
¥{{ record[column.key].toFixed(2) }}
</template>
</template>
</a-table>
</div>
</div>
</page-container>
</template>
<script>
import { defineComponent, computed, ref, onMounted } from 'vue'
import { Card, Row, Col, Statistic, Table, Select } from 'ant-design-vue'
import PageContainer from '@/components/PageContainer.vue'
import request from '@/utils/request'
export default defineComponent({
name: 'Dashboard',
components: {
PageContainer,
ACard: Card,
ARow: Row,
ACol: Col,
AStatistic: Statistic,
AStatisticGroup: Statistic.Group,
ATable: Table,
ASelect: Select,
ASelectOption: Select.Option
},
setup() {
const loading = ref(false)
const communityLoading = ref(false)
const selectedCommunity = ref(0)
const communityList = ref([])
const dashboardData = ref({
total_community_count: 0,
total_user_count: 0,
has_order_user_count: 0,
has_paid_user_count: 0,
has_order_completed_user_count: 0,
repeat_user_count: 0,
order_count: 0,
order_pay_count: 0,
order_unpaid_count: 0,
order_completed_count: 0,
pay_rate: 0,
order_amount: 0,
pay_amount: 0,
completed_order_amount: 0,
unpaid_amount: 0,
today_user_count: 0,
yesterday_user_count: 0,
today_order_count: 0,
yesterday_order_count: 0,
today_order_amount: 0,
yesterday_order_amount: 0
})
const deliverymanList = ref([])
const deliverymanColumns = [
{
title: '配送员ID',
dataIndex: 'deliveryman_id',
key: 'deliveryman_id',
width: 80,
align: 'center'
},
{
title: '所属小区',
dataIndex: 'deliveryman_community_name',
key: 'deliveryman_community_name',
width: 150
},
{
title: '配送员名字',
dataIndex: 'deliveryman_auth_name',
key: 'deliveryman_auth_name',
width: 120
},
{
title: '今日订单数',
dataIndex: 'today_order_count',
key: 'today_order_count',
width: 100,
align: 'center'
},
{
title: '今日订单金额',
dataIndex: 'today_order_amount',
key: 'today_order_amount',
width: 120,
align: 'right'
},
{
title: '总订单数',
dataIndex: 'order_count',
key: 'order_count',
width: 100,
align: 'center'
},
{
title: '总订单金额',
dataIndex: 'order_amount',
key: 'order_amount',
width: 120,
align: 'right'
}
]
const dailyStatsList = ref([])
const dailyStatsLoading = ref(false)
const dailyStatsPagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
const dailyStatsColumns = [
{
title: '日期',
dataIndex: 'stats_date',
key: 'stats_date',
width: 120,
align: 'center'
},
{
title: '订单数',
dataIndex: 'total_order_count',
key: 'total_order_count',
width: 100,
align: 'center'
},
{
title: '订单金额',
dataIndex: 'total_original_amount',
key: 'total_original_amount',
width: 120,
align: 'right'
},
{
title: '支付金额',
dataIndex: 'total_final_amount',
key: 'total_final_amount',
width: 120,
align: 'right'
},
{
title: '小区数',
dataIndex: 'total_communities',
key: 'total_communities',
width: 100,
align: 'center'
}
]
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 6) return '夜深了,请注意休息'
if (hour < 9) return '早上好'
if (hour < 12) return '上午好'
if (hour < 14) return '中午好'
if (hour < 17) return '下午好'
if (hour < 19) return '傍晚好'
return '晚上好'
})
const paymentRate = computed(() => {
if (!dashboardData.value.order_count || dashboardData.value.order_count === 0) {
return 0;
}
return dashboardData.value.pay_rate * 100;
});
const fetchDashboardData = async () => {
loading.value = true
try {
const res = await request.get('/api/dashboard')
if (res.code === 200) {
dashboardData.value = res.data
}
} catch (error) {
} finally {
loading.value = false
}
}
const fetchDeliverymanList = async (communityId = null) => {
loading.value = true
try {
const url = '/api/dashboard/deliveryman'
const params = communityId ? { community_id: communityId } : {}
const res = await request.get(url, { params })
if (res.code === 200) {
deliverymanList.value = res.data
}
} catch (error) {
} finally {
loading.value = false
}
}
const fetchCommunityList = async () => {
communityLoading.value = true
try {
const res = await request.get('/api/community?limit=1000', {
params: {
limit: 1000,
skip: 0,
status: "OPENING"
}
})
if (res.code === 200) {
communityList.value = res.data.items || []
}
} catch (error) {
} finally {
communityLoading.value = false
}
}
const fetchDailyStats = async () => {
dailyStatsLoading.value = true
try {
const params = {
skip: (dailyStatsPagination.value.current - 1) * dailyStatsPagination.value.pageSize,
limit: dailyStatsPagination.value.pageSize
}
const res = await request.get('/api/dashboard/order_stats', { params })
if (res.code === 200) {
dailyStatsList.value = res.data.items || []
dailyStatsPagination.value.total = res.data.total || 0
}
} catch (error) {
} finally {
dailyStatsLoading.value = false
}
}
const handleCommunityChange = (value) => {
if (value === 0) {
fetchDeliverymanList()
} else {
fetchDeliverymanList(value)
}
}
const handleDailyStatsTableChange = (pagination) => {
dailyStatsPagination.value.current = pagination.current
dailyStatsPagination.value.pageSize = pagination.pageSize
fetchDailyStats()
}
onMounted(() => {
fetchDashboardData()
fetchDeliverymanList()
fetchCommunityList()
fetchDailyStats()
})
return {
greeting,
dashboardData,
deliverymanList,
deliverymanColumns,
loading,
communityLoading,
communityList,
selectedCommunity,
handleCommunityChange,
paymentRate,
dailyStatsList,
dailyStatsColumns,
dailyStatsPagination,
dailyStatsLoading,
handleDailyStatsTableChange
}
}
})
</script>
<style scoped>
.dashboard {
background: #fff;
}
.data-overview {
margin-bottom: 24px;
}
.data-card {
height: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
&:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
:deep(.ant-card-head) {
min-height: 42px;
padding: 0 12px;
border-bottom: none;
.ant-card-head-title {
padding: 12px 0;
font-weight: 600;
font-size: 15px;
}
}
:deep(.ant-card-body) {
padding: 0 15px 15px;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
z-index: 1;
}
}
.community-card {
:deep(.ant-card-head) {
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
.ant-card-head-title {
color: white;
}
}
&::before {
background-color: #1890ff;
}
}
.user-card {
:deep(.ant-card-head) {
background: linear-gradient(135deg, #722ed1 0%, #eb2f96 100%);
.ant-card-head-title {
color: white;
}
}
&::before {
background-color: #722ed1;
}
}
.order-card {
:deep(.ant-card-head) {
background: linear-gradient(135deg, #fa8c16 0%, #faad14 100%);
.ant-card-head-title {
color: white;
}
}
&::before {
background-color: #fa8c16;
}
}
.amount-card {
:deep(.ant-card-head) {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
.ant-card-head-title {
color: white;
}
}
&::before {
background-color: #52c41a;
}
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
}
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 34px;
height: 34px;
border-radius: 50%;
}
.community-icon {
background-color: rgba(24, 144, 255, 0.2);
color: #1890ff;
}
.user-icon {
background-color: rgba(114, 46, 209, 0.2);
color: #722ed1;
}
.order-icon {
background-color: rgba(250, 140, 22, 0.2);
color: #fa8c16;
}
.amount-icon {
background-color: rgba(82, 196, 26, 0.2);
color: #52c41a;
}
.deliveryman-icon {
background-color: rgba(24, 144, 255, 0.2);
color: #1890ff;
}
.icon {
font-size: 20px;
}
.main-stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
.main-stat {
margin-bottom: 0;
:deep(.ant-statistic-content) {
font-size: 22px;
}
}
}
.main-stat-row .main-stat:last-child :deep(.ant-statistic-content) {
text-align: right;
}
.sub-stats {
display: flex;
justify-content: space-between;
border-top: 1px solid #f0f0f0;
padding-top: 12px;
margin-top: 6px;
flex-wrap: wrap;
:deep(.ant-statistic) {
padding: 0 8px;
min-width: 30%;
margin-bottom: 6px;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
.ant-statistic-title {
font-size: 13px;
margin-bottom: 3px;
color: rgba(0, 0, 0, 0.65);
}
.ant-statistic-content {
font-size: 18px;
font-weight: 500;
}
}
}
.sub-stats :deep(.ant-statistic:last-child) .ant-statistic-content {
text-align: right;
}
.sub-stats :deep(.ant-statistic:last-child) .ant-statistic-title {
text-align: right;
}
.daily-stats-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
margin-bottom: 24px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
background-color: #722ed1;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
.stats-icon {
background-color: rgba(114, 46, 209, 0.2);
color: #722ed1;
}
.daily-stats-table {
:deep(.ant-table-thead > tr > th) {
background-color: #f5f5f5;
font-weight: 500;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f0e6fa;
}
:deep(.ant-table-tbody > tr > td) {
transition: background 0.3s;
}
}
.deliveryman-list-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
background-color: #1890ff;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.section-title {
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
margin: 0;
}
.filter-container {
display: flex;
gap: 16px;
}
.deliveryman-table {
:deep(.ant-table-wrapper) {
}
:deep(.ant-table-container) {
display: flex;
flex-direction: column;
}
:deep(.ant-table) {
}
:deep(.ant-table-content) {
}
:deep(.ant-table-thead > tr > th) {
background-color: #f5f5f5;
font-weight: 500;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #e6f7ff;
}
:deep(.ant-table-tbody > tr > td) {
transition: background 0.3s;
}
}
@media (max-width: 768px) {
.data-overview {
margin-bottom: 16px;
}
.data-card {
margin-bottom: 16px;
}
.main-stat-row {
.main-stat {
:deep(.ant-statistic-content) {
font-size: 20px;
}
}
}
.sub-stats {
:deep(.ant-statistic) {
.ant-statistic-content {
font-size: 16px;
}
}
}
}
</style>