统计 优化
This commit is contained in:
@@ -22,8 +22,6 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
MapPin,
|
MapPin,
|
||||||
Users,
|
|
||||||
Shield,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Store,
|
Store,
|
||||||
@@ -507,54 +505,6 @@ export default function MallsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">商场总数</CardTitle>
|
|
||||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{malls.length}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
正常运营 {malls.filter((m) => m.status === "1").length} 个
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">商户总数</CardTitle>
|
|
||||||
<Store className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">0</div>
|
|
||||||
<p className="text-xs text-muted-foreground">分布在 {malls.length} 个商场</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">设备总数</CardTitle>
|
|
||||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">0</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
平均每商场 0 台
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">经销商覆盖</CardTitle>
|
|
||||||
<Users className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{new Set(malls.map((m) => m.mallUser)).size}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">商场管理员数量</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索和筛选 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
@@ -13,9 +13,8 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "../ui/dialog"
|
} from "../ui/dialog"
|
||||||
import { Plus, Search, Filter, Download, Calendar, AlertTriangle, Store, Eye, Building2, MapPin, ChevronLeft, ChevronRight } from "lucide-react"
|
import { Plus, Search, Filter, Download, Store, Eye, Building2, MapPin, ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
import { apiGet, apiPost, apiPut } from "../../lib/services/api"
|
import { apiGet, apiPost, apiPut } from "../../lib/services/api"
|
||||||
|
|
||||||
// 总商户数据类型
|
// 总商户数据类型
|
||||||
@@ -382,16 +381,6 @@ export default function MerchantsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMerchantStatusBadge = (merchant: any) => {
|
|
||||||
if (merchant.expiredCount > 0) {
|
|
||||||
return <Badge variant="destructive">有设备过期</Badge>
|
|
||||||
} else if (merchant.expiringCount > 0) {
|
|
||||||
return <Badge className="bg-yellow-100 text-yellow-800">有设备即将到期</Badge>
|
|
||||||
} else {
|
|
||||||
return <Badge className="bg-green-100 text-green-800">设备正常</Badge>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开编辑对话框
|
// 打开编辑对话框
|
||||||
const handleEditMerchant = (merchant: any) => {
|
const handleEditMerchant = (merchant: any) => {
|
||||||
setEditMerchant({
|
setEditMerchant({
|
||||||
@@ -529,64 +518,6 @@ export default function MerchantsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">商户总数</p>
|
|
||||||
<p className="text-2xl font-bold">{total}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
|
|
||||||
<Store className="h-4 w-4 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">设备正常</p>
|
|
||||||
<p className="text-2xl font-bold text-green-600">128</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
|
|
||||||
<Calendar className="h-4 w-4 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">有设备即将到期</p>
|
|
||||||
<p className="text-2xl font-bold text-yellow-600">23</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">有设备过期</p>
|
|
||||||
<p className="text-2xl font-bold text-red-600">5</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-red-100 rounded-full flex items-center justify-center">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters and Search */}
|
{/* Filters and Search */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -56,9 +56,25 @@ interface DealerDistributionData {
|
|||||||
totalCount: number
|
totalCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定义设备状态分布数据接口
|
||||||
|
interface DeviceDistribution {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义月度工单趋势数据接口
|
||||||
|
interface WorkOrderTrend {
|
||||||
|
month: string
|
||||||
|
completed: number
|
||||||
|
pending: number
|
||||||
|
total: number | string // total 可能是字符串或数字
|
||||||
|
}
|
||||||
|
|
||||||
export default function StatisticsPage() {
|
export default function StatisticsPage() {
|
||||||
const [statisticsData, setStatisticsData] = useState<StatisticsData | null>(null)
|
const [statisticsData, setStatisticsData] = useState<StatisticsData | null>(null)
|
||||||
const [dealerDistributionData, setDealerDistributionData] = useState<DealerDistribution[]>([])
|
const [dealerDistributionData, setDealerDistributionData] = useState<DealerDistribution[]>([])
|
||||||
|
const [deviceDistributionData, setDeviceDistributionData] = useState<DeviceDistribution[]>([])
|
||||||
|
const [workOrderTrendData, setWorkOrderTrendData] = useState<WorkOrderTrend[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const userData = getUserData()
|
const userData = getUserData()
|
||||||
|
|
||||||
@@ -68,6 +84,8 @@ export default function StatisticsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStatistics()
|
fetchStatistics()
|
||||||
fetchDealerDistribution()
|
fetchDealerDistribution()
|
||||||
|
fetchDeviceDistribution()
|
||||||
|
fetchWorkOrderTrend()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchStatistics = async () => {
|
const fetchStatistics = async () => {
|
||||||
@@ -95,6 +113,28 @@ export default function StatisticsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchDeviceDistribution = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiGet<{ code: number; msg: string; data: DeviceDistribution[] }>('/back/statistics/deviceDistribution')
|
||||||
|
if (response.code === 200) {
|
||||||
|
setDeviceDistributionData(response.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备状态分布数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchWorkOrderTrend = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiGet<{ code: number; msg: string; data: WorkOrderTrend[] }>('/back/statistics/workOrderCompletionTrend')
|
||||||
|
if (response.code === 200) {
|
||||||
|
setWorkOrderTrendData(response.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取月度工单趋势数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
totalEquipment: statisticsData?.equipmentTotal || 0,
|
totalEquipment: statisticsData?.equipmentTotal || 0,
|
||||||
activeWorkOrders: statisticsData?.completedWorkOrders || 0,
|
activeWorkOrders: statisticsData?.completedWorkOrders || 0,
|
||||||
@@ -153,22 +193,32 @@ export default function StatisticsPage() {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备状态分布数据
|
// 根据状态名称获取颜色
|
||||||
const equipmentStatusData = [
|
const getColorByStatusName = (name: string): string => {
|
||||||
{ name: "正常运行", value: 1156, color: "#10B981" },
|
const colorMap: { [key: string]: string } = {
|
||||||
{ name: "即将到期", value: 67, color: "#F59E0B" },
|
'正常': '#10B981',
|
||||||
{ name: "已过期", value: 25, color: "#EF4444" },
|
'正常运行': '#10B981',
|
||||||
]
|
'即将到期': '#F59E0B',
|
||||||
|
'已过期': '#EF4444',
|
||||||
|
'过期': '#EF4444',
|
||||||
|
}
|
||||||
|
return colorMap[name] || '#6B7280' // 默认灰色
|
||||||
|
}
|
||||||
|
|
||||||
// 月度工单完成情况
|
// 设备状态分布数据 - 从接口获取并添加颜色
|
||||||
const monthlyWorkOrderData = [
|
const equipmentStatusData = deviceDistributionData.map(item => ({
|
||||||
{ month: "1月", completed: 145, pending: 23, total: 168 },
|
name: item.name,
|
||||||
{ month: "2月", completed: 132, pending: 18, total: 150 },
|
value: item.value,
|
||||||
{ month: "3月", completed: 156, pending: 25, total: 181 },
|
color: getColorByStatusName(item.name)
|
||||||
{ month: "4月", completed: 142, pending: 19, total: 161 },
|
}))
|
||||||
{ month: "5月", completed: 167, pending: 22, total: 189 },
|
|
||||||
{ month: "6月", completed: 158, pending: 27, total: 185 },
|
// 月度工单完成情况 - 从接口获取
|
||||||
]
|
const monthlyWorkOrderData = workOrderTrendData.map(item => ({
|
||||||
|
month: item.month,
|
||||||
|
completed: typeof item.completed === 'string' ? Number(item.completed) : item.completed,
|
||||||
|
pending: typeof item.pending === 'string' ? Number(item.pending) : item.pending,
|
||||||
|
total: typeof item.total === 'string' ? Number(item.total) : item.total,
|
||||||
|
}))
|
||||||
|
|
||||||
// provinceData 已从接口获取,存储在 dealerDistributionData 中
|
// provinceData 已从接口获取,存储在 dealerDistributionData 中
|
||||||
|
|
||||||
|
|||||||
@@ -16,20 +16,16 @@ import { Label } from "../ui/label"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"
|
||||||
import {
|
import {
|
||||||
Users,
|
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
MapPin,
|
MapPin,
|
||||||
Shield,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
User,
|
User,
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Phone,
|
Phone,
|
||||||
Award,
|
|
||||||
Wrench,
|
Wrench,
|
||||||
CheckCircle,
|
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { apiPost, apiGet, apiPut, apiDelete } from "../../lib/services/api"
|
import { apiPost, apiGet, apiPut, apiDelete } from "../../lib/services/api"
|
||||||
import { getProvinces } from '../../lib/services/region'
|
import { getProvinces } from '../../lib/services/region'
|
||||||
@@ -805,60 +801,6 @@ export default function WorkersPage() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">工人总数</CardTitle>
|
|
||||||
<Users className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{workers.length}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
在职 {workers.filter((w) => w.status === "active").length} 人
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">高级技师</CardTitle>
|
|
||||||
<Award className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{workers.filter((w) => w.skillLevel === "高级技师").length}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
占比 {workers.length > 0 ? Math.round((workers.filter((w) => w.skillLevel === "高级技师").length / workers.length) * 100) : 0}%
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">完成工单</CardTitle>
|
|
||||||
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{workers.reduce((sum, w) => sum + w.completedOrders, 0)}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
平均每人 {workers.length > 0 ? Math.round(workers.reduce((sum, w) => sum + w.completedOrders, 0) / workers.length) : 0} 单
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">平均评分</CardTitle>
|
|
||||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{workers.filter((w) => w.rating > 0).length > 0
|
|
||||||
? (workers.reduce((sum, w) => sum + w.rating, 0) / workers.filter((w) => w.rating > 0).length).toFixed(1)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">满分 5.0 分</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索和筛选 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user