This commit is contained in:
aaron 2025-03-18 09:30:23 +08:00
parent 674b67065a
commit 5479099dba
3 changed files with 499 additions and 128 deletions

20
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "beefast-admin",
"version": "0.1.0",
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@vue/cli-service": "^5.0.8",
"ant-design-vue": "^3.2.20",
"axios": "^1.7.9",
@ -56,10 +57,9 @@
"license": "MIT"
},
"node_modules/@ant-design/icons-vue": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-6.1.0.tgz",
"integrity": "sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==",
"license": "MIT",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
"integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.2.1"
@ -1484,6 +1484,18 @@
"vue": ">=3.2.0"
}
},
"node_modules/ant-design-vue/node_modules/@ant-design/icons-vue": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-6.1.0.tgz",
"integrity": "sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.2.1"
},
"peerDependencies": {
"vue": ">=3.0.3"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@vue/cli-service": "^5.0.8",
"ant-design-vue": "^3.2.20",
"axios": "^1.7.9",

View File

@ -1,107 +1,182 @@
<template>
<page-container>
<div class="dashboard">
<div class="welcome-card">
<h1>欢迎使用蜂快运营管理系统</h1>
<p>{{ greeting }}</p>
</div>
<a-row :gutter="24" class="data-overview">
<!-- 用户数据卡片 -->
<a-col :span="8">
<a-card class="data-card">
<template #title>
<span class="card-title">
<team-outlined /> 用户数据
</span>
</template>
<a-statistic-group>
<a-statistic
title="总用户数"
:value="dashboardData.total_user_count"
class="main-stat"
/>
<div class="sub-stats">
<a-statistic
title="今日新增"
:value="dashboardData.today_user_count"
:value-style="{ color: '#3f8600' }"
/>
<a-statistic
title="昨日新增"
:value="dashboardData.yesterday_user_count"
/>
</div>
</a-statistic-group>
</a-card>
</a-col>
<!-- 订单数据卡片 -->
<a-col :span="8">
<a-card class="data-card">
<template #title>
<span class="card-title">
<shopping-outlined /> 配送订单
</span>
</template>
<a-statistic-group>
<a-statistic
title="总订单数"
:value="dashboardData.total_order_count"
class="main-stat"
/>
<div class="sub-stats">
<a-statistic
title="今日新增"
:value="dashboardData.today_order_count"
:value-style="{ color: '#3f8600' }"
/>
<a-statistic
title="昨日新增"
:value="dashboardData.yesterday_order_count"
/>
</div>
</a-statistic-group>
</a-card>
</a-col>
<!-- 小区数据卡片 -->
<a-col :span="8">
<a-card class="data-card">
<a-col :span="6">
<a-card class="data-card community-card">
<template #title>
<span class="card-title">
<home-outlined /> 小区数据
<span class="icon-wrapper community-icon">
<svg class="icon" viewBox="0 0 1024 1024" width="24" height="24">
<path d="M946.5 505L560.1 118.8l-25.9-25.9c-12.3-12.2-32.1-12.2-44.4 0L77.5 505c-12.3 12.3-18.9 28.6-18.8 46 0.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8 12.1-12.1 18.7-28.2 18.7-45.3 0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204z m217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z" fill="currentColor"></path>
</svg>
</span>
小区数据
</span>
</template>
<a-statistic-group>
<a-statistic
title="开通小区数"
:value="dashboardData.total_community_count || 0"
class="main-stat"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<!-- 用户数据卡片 -->
<a-col :span="6">
<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="24" height="24">
<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>
<a-statistic
title="总用户数"
:value="dashboardData.total_user_count || 0"
class="main-stat"
:value-style="{ color: '#722ed1' }"
/>
<div class="sub-stats">
<a-statistic
title="总小区数"
:value="dashboardData.total_community_count"
class="main-stat"
title="已下单用户"
:value="dashboardData.has_order_user_count || 0"
/>
<div class="sub-stats">
<a-statistic
title="今日新增"
:value="dashboardData.today_community_count"
:value-style="{ color: '#3f8600' }"
/>
<a-statistic
title="昨日新增"
:value="dashboardData.yesterday_community_count"
/>
</div>
</a-statistic-group>
<a-statistic
title="付费用户"
:value="dashboardData.has_paid_user_count || 0"
:value-style="{ color: '#722ed1' }"
/>
</div>
</a-card>
</a-col>
<!-- 配送订单数据卡片 -->
<a-col :span="6">
<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="24" height="24">
<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>
<a-statistic
title="配送订单数"
:value="dashboardData.order_count || 0"
class="main-stat"
:value-style="{ color: '#fa8c16' }"
/>
<div class="sub-stats">
<a-statistic
title="付费订单数"
:value="dashboardData.order_pay_count || 0"
:value-style="{ color: '#fa8c16' }"
/>
<a-statistic
title="付费率"
:value="paymentRate"
:precision="2"
suffix="%"
:value-style="{ color: '#fa8c16' }"
/>
</div>
</a-card>
</a-col>
<!-- 订单金额数据卡片 -->
<a-col :span="6">
<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="24" height="24">
<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>
<a-statistic
title="配送订单总金额"
:value="dashboardData.order_amount || 0"
:precision="2"
prefix="¥"
class="main-stat"
:value-style="{ color: '#52c41a' }"
/>
<div class="sub-stats">
<a-statistic
title="付费订单总金额"
:value="dashboardData.pay_amount || 0"
:precision="2"
prefix="¥"
:value-style="{ color: '#52c41a' }"
/>
</div>
</a-card>
</a-col>
</a-row>
<!-- 配送员列表 -->
<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 } from 'ant-design-vue'
import { TeamOutlined, ShoppingOutlined, HomeOutlined } from '@ant-design/icons-vue'
import { Card, Row, Col, Statistic, Table, Select } from 'ant-design-vue'
import PageContainer from '@/components/PageContainer.vue'
import request from '@/utils/request'
@ -114,22 +189,79 @@ export default defineComponent({
ACol: Col,
AStatistic: Statistic,
AStatisticGroup: Statistic.Group,
TeamOutlined,
ShoppingOutlined,
HomeOutlined
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_user_count: 0,
today_user_count: 0,
yesterday_user_count: 0,
total_order_count: 0,
today_order_count: 0,
yesterday_order_count: 0,
total_community_count: 0,
today_community_count: 0,
yesterday_community_count: 0
total_user_count: 0,
has_order_user_count: 0,
has_paid_user_count: 0,
order_count: 0,
order_pay_count: 0,
pay_rate: 0,
order_amount: 0,
pay_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_name',
key: 'deliveryman_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 greeting = computed(() => {
const hour = new Date().getHours()
@ -142,7 +274,15 @@ export default defineComponent({
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) {
@ -150,60 +290,181 @@ export default defineComponent({
}
} catch (error) {
console.error('获取数据看板数据失败:', 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) {
console.error('获取配送员列表失败:', error)
} finally {
loading.value = false
}
}
const fetchCommunityList = async () => {
communityLoading.value = true
try {
const res = await request.get('/api/community', {
params: {
limit: 1000,
skip: 0,
status: "OPENING"
}
})
if (res.code === 200) {
communityList.value = res.data.items || []
}
} catch (error) {
console.error('获取小区列表失败:', error)
} finally {
communityLoading.value = false
}
}
const handleCommunityChange = (value) => {
if (value === 0) {
fetchDeliverymanList()
} else {
fetchDeliverymanList(value)
}
}
onMounted(() => {
fetchDashboardData()
fetchDeliverymanList()
fetchCommunityList()
})
return {
greeting,
dashboardData
dashboardData,
deliverymanList,
deliverymanColumns,
loading,
communityLoading,
communityList,
selectedCommunity,
handleCommunityChange,
paymentRate
}
}
})
</script>
<style scoped>
.dashboard {
.dashboard {
padding: 24px;
background: #fff;
min-height: 400px;
}
.welcome-card {
text-align: center;
padding: 24px 0;
margin-bottom: 24px;
}
.welcome-card h1 {
font-size: 28px;
color: #1890ff;
margin-bottom: 16px;
}
.welcome-card p {
font-size: 16px;
color: rgba(0, 0, 0, 0.65);
}
.data-overview {
margin-top: 24px;
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: 48px;
padding: 0 16px;
border-bottom: none;
.ant-card-head-title {
padding: 12px 0;
padding: 16px 0;
font-weight: 600;
}
}
:deep(.ant-card-body) {
padding: 0 20px 20px;
}
&::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 {
@ -211,11 +472,44 @@ export default defineComponent({
align-items: center;
gap: 8px;
font-size: 16px;
.anticon {
font-size: 20px;
color: #1890ff;
}
}
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
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: 24px;
}
.main-stat {
@ -223,18 +517,22 @@ export default defineComponent({
:deep(.ant-statistic-title) {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
color: rgba(0, 0, 0, 0.65);
margin-bottom: 8px;
}
:deep(.ant-statistic-content) {
font-size: 24px;
color: #1890ff;
font-size: 30px;
font-weight: 600;
}
}
.sub-stats {
display: flex;
justify-content: space-between;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
margin-top: 8px;
:deep(.ant-statistic) {
padding: 0 12px;
@ -250,11 +548,71 @@ export default defineComponent({
.ant-statistic-title {
font-size: 14px;
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.65);
}
.ant-statistic-content {
font-size: 20px;
font-weight: 500;
}
}
}
.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;
}
.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 {
margin-top: 16px;
: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;
}
}
</style>