Update workflow
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter } from 'lucide-react';
|
||||
import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter, ChevronDown, BarChart3, HelpCircle, Mail } from 'lucide-react';
|
||||
import { Card, Button } from '../components/UI';
|
||||
import { mockAssistants, getDashboardStats } from '../services/mockData';
|
||||
|
||||
@@ -12,84 +13,133 @@ export const DashboardPage: React.FC = () => {
|
||||
}, [timeRange, selectedAssistantId]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">首页概览</h1>
|
||||
<div className="min-h-full flex flex-col justify-center animate-in fade-in py-1">
|
||||
<div className="w-full max-w-5xl mx-auto space-y-4 px-2">
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 bg-card/40 backdrop-blur-md p-2 rounded-lg border border-border/50">
|
||||
<div className="flex items-center px-2">
|
||||
<Filter className="h-4 w-4 text-primary mr-2" />
|
||||
<select
|
||||
className="bg-transparent text-sm font-medium focus:outline-none text-foreground [&>option]:bg-background"
|
||||
value={selectedAssistantId}
|
||||
onChange={(e) => setSelectedAssistantId(e.target.value)}
|
||||
>
|
||||
<option value="all">所有小助手</option>
|
||||
{mockAssistants.map(a => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* 1. Utility Row (Top Navigation Actions) */}
|
||||
<div className="flex justify-end items-center gap-2 border-b border-white/[0.03] pb-2">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all">
|
||||
<HelpCircle className="w-3 h-3 mr-1.5 opacity-70" /> 文档
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all">
|
||||
<Mail className="w-3 h-3 mr-1.5 opacity-70" /> 联系方式
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 2. Welcome Row */}
|
||||
<div className="flex flex-col space-y-0.5 text-center md:text-left pt-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">
|
||||
欢迎, <span className="text-primary">Admin User</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground flex items-center justify-center md:justify-start text-[11px]">
|
||||
系统状态:
|
||||
<span className="flex items-center ml-2 text-green-400 text-[10px] font-mono bg-green-400/10 px-1.5 py-0.5 rounded-full border border-green-400/20">
|
||||
<span className="w-1 h-1 rounded-full bg-green-400 animate-pulse mr-1"></span>
|
||||
HEALTHY
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 3. Section Header: Title + Filters aligned perfectly */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between border-b border-white/5 pb-3 pt-1 gap-3">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="p-1 bg-primary/10 rounded-lg">
|
||||
<BarChart3 className="h-3.5 w-3.5 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-sm font-bold text-white tracking-wide leading-none">用量标准</h2>
|
||||
<span className="text-[8px] font-mono text-muted-foreground uppercase tracking-[0.2em] opacity-40 mt-1">Metrics Overview</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-border/50 hidden sm:block"></div>
|
||||
<div className="flex bg-muted/50 rounded-md p-1">
|
||||
{(['week', 'month', 'year'] as const).map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => setTimeRange(r)}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-sm transition-all ${timeRange === r ? 'bg-primary text-primary-foreground shadow-[0_0_10px_rgba(6,182,212,0.3)]' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
{r === 'week' ? '近一周' : r === 'month' ? '近一个月' : '近一年'}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Filters Group (Aligned Right) */}
|
||||
<div className="flex items-center gap-1.5 bg-black/20 p-0.5 rounded-lg border border-white/5 shadow-inner scale-95 origin-right">
|
||||
{/* Assistant Selector */}
|
||||
<div className="relative group min-w-[130px]">
|
||||
<div className="absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<Filter className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
<select
|
||||
className="w-full bg-transparent border-0 rounded-lg pl-7 pr-6 py-1 text-[10px] font-bold focus:outline-none appearance-none cursor-pointer transition-all text-white/80"
|
||||
value={selectedAssistantId}
|
||||
onChange={(e) => setSelectedAssistantId(e.target.value)}
|
||||
>
|
||||
<option value="all" className="bg-background">所有小助手</option>
|
||||
{mockAssistants.map(a => (
|
||||
<option key={a.id} value={a.id} className="bg-background">{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 h-2.5 w-2.5 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
|
||||
<div className="h-3 w-px bg-white/10 mx-0.5"></div>
|
||||
|
||||
{/* Time Range Selector */}
|
||||
<div className="flex gap-0.5">
|
||||
{(['week', 'month', 'year'] as const).map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => setTimeRange(r)}
|
||||
className={`px-2.5 py-0.5 text-[9px] font-black uppercase tracking-tight rounded transition-all ${timeRange === r ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
{r === 'week' ? '周' : r === 'month' ? '月' : '年'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="通话数量"
|
||||
value={stats.totalCalls.toString()}
|
||||
icon={<Phone className="h-4 w-4 text-primary" />}
|
||||
trend="+12.5% 较上期"
|
||||
/>
|
||||
<StatCard
|
||||
title="接通率"
|
||||
value={`${stats.answerRate}%`}
|
||||
icon={<CheckCircle className="h-4 w-4 text-green-400" />}
|
||||
trend="+2.1% 较上期"
|
||||
/>
|
||||
<StatCard
|
||||
title="平均通话时长"
|
||||
value={stats.avgDuration}
|
||||
icon={<Clock className="h-4 w-4 text-blue-400" />}
|
||||
trend="-0.5% 较上期"
|
||||
/>
|
||||
<StatCard
|
||||
title="转人工数量"
|
||||
value={stats.humanTransferCount.toString()}
|
||||
icon={<UserCheck className="h-4 w-4 text-purple-400" />}
|
||||
trend="+5% 较上期"
|
||||
/>
|
||||
</div>
|
||||
{/* 4. Metrics Grid (Cards) */}
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="通话数量"
|
||||
value={stats.totalCalls.toString()}
|
||||
icon={<Phone className="h-3.5 w-3.5 text-primary" />}
|
||||
trend="+12.5% UP"
|
||||
/>
|
||||
<StatCard
|
||||
title="接通率"
|
||||
value={`${stats.answerRate}%`}
|
||||
icon={<CheckCircle className="h-3.5 w-3.5 text-green-400" />}
|
||||
trend="+2.1% UP"
|
||||
/>
|
||||
<StatCard
|
||||
title="平均时长"
|
||||
value={stats.avgDuration}
|
||||
icon={<Clock className="h-3.5 w-3.5 text-blue-400" />}
|
||||
trend="-0.5% LOW"
|
||||
/>
|
||||
<StatCard
|
||||
title="转人工数"
|
||||
value={stats.humanTransferCount.toString()}
|
||||
icon={<UserCheck className="h-3.5 w-3.5 text-purple-400" />}
|
||||
trend="+5% STABLE"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid gap-4 md:grid-cols-1">
|
||||
<Card className="p-6 border-primary/20 bg-card/30">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-medium leading-none flex items-center">
|
||||
<Activity className="h-5 w-5 text-primary mr-2" />
|
||||
通话趋势图
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">展示选定时间范围内的通话量变化</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[300px] w-full">
|
||||
<SimpleAreaChart data={stats.trend} />
|
||||
</div>
|
||||
</Card>
|
||||
{/* 5. Charts Section */}
|
||||
<div className="pt-1">
|
||||
<Card className="p-5 border-white/5 bg-card/20 backdrop-blur-sm overflow-hidden shadow-xl">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between mb-6 gap-3">
|
||||
<div className="space-y-0.5 text-center md:text-left">
|
||||
<h3 className="text-base font-bold leading-none flex items-center justify-center md:justify-start text-white">
|
||||
<Activity className="h-4 w-4 text-primary mr-2" />
|
||||
通话趋势 (Performance Insight)
|
||||
</h3>
|
||||
<p className="text-[10px] text-muted-foreground font-mono opacity-40">REAL-TIME DATA PROCESSING PIPELINE ENABLED</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-6 w-6 rounded-full border border-primary/20 flex items-center justify-center animate-spin-slow">
|
||||
<div className="h-1 w-1 rounded-full bg-primary shadow-[0_0_8px_rgba(6,182,212,0.8)]"></div>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono text-primary animate-pulse tracking-widest uppercase">Streaming</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[250px] w-full">
|
||||
<SimpleAreaChart data={stats.trend} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -98,14 +148,20 @@ export const DashboardPage: React.FC = () => {
|
||||
// --- Sub Components ---
|
||||
|
||||
const StatCard: React.FC<{ title: string; value: string; icon: React.ReactNode; trend?: string }> = ({ title, value, icon, trend }) => (
|
||||
<Card className="p-6 border-border/40 hover:border-primary/50 transition-colors">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">{title}</h3>
|
||||
{icon}
|
||||
<Card className="p-4 border-white/5 bg-card/30 hover:border-primary/40 hover:bg-card/50 transition-all duration-300 group flex flex-col justify-between min-h-[110px] shadow-lg">
|
||||
<div className="flex flex-row items-center justify-between space-y-0">
|
||||
<h3 className="text-[9px] font-mono font-bold text-muted-foreground uppercase tracking-[0.15em]">{title}</h3>
|
||||
<div className="p-1.5 bg-white/5 rounded-lg group-hover:bg-primary/20 transition-all group-hover:scale-110">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-end">
|
||||
<div className="text-2xl font-bold tracking-tight text-foreground">{value}</div>
|
||||
{trend && <p className="text-xs text-muted-foreground mt-1">{trend}</p>}
|
||||
<div className="mt-2">
|
||||
<div className="text-2xl font-black tracking-tight text-white group-hover:text-primary transition-colors">{value}</div>
|
||||
{trend && (
|
||||
<p className={`text-[8px] font-bold font-mono mt-1 flex items-center ${trend.includes('+') ? 'text-green-400' : 'text-red-400'}`}>
|
||||
<span className="bg-white/5 px-1 rounded-sm mr-1 opacity-70">{trend}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
@@ -131,9 +187,16 @@ const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> =
|
||||
return (
|
||||
<div className="w-full h-full relative">
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full overflow-visible" preserveAspectRatio="none">
|
||||
{/* Tech Grid Lines */}
|
||||
<line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} stroke="hsl(var(--border))" strokeWidth="1" />
|
||||
<line x1={padding} y1={padding} x2={width - padding} y2={padding} stroke="hsl(var(--border))" strokeWidth="1" strokeDasharray="4 4" opacity="0.3" />
|
||||
{/* Grid Lines */}
|
||||
<line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} stroke="rgba(255,255,255,0.03)" strokeWidth="1" />
|
||||
{[0.25, 0.5, 0.75].map(v => (
|
||||
<line
|
||||
key={v}
|
||||
x1={padding} y1={height - padding - ((height - padding * 2) * v)}
|
||||
x2={width - padding} y2={height - padding - ((height - padding * 2) * v)}
|
||||
stroke="rgba(255,255,255,0.02)" strokeWidth="1" strokeDasharray="8 6"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Area Fill Gradient */}
|
||||
<defs>
|
||||
@@ -141,9 +204,8 @@ const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> =
|
||||
<stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
{/* Glow Filter */}
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feGaussianBlur stdDeviation="5" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
@@ -151,10 +213,8 @@ const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> =
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Fill Area */}
|
||||
<polygon points={fillPath} fill="url(#chartGradient)" />
|
||||
|
||||
{/* Main Line with Glow */}
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
@@ -163,22 +223,23 @@ const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> =
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
filter="url(#glow)"
|
||||
className="drop-shadow-sm"
|
||||
/>
|
||||
|
||||
{/* Data Points */}
|
||||
{data.length < 20 && data.map((d, i) => {
|
||||
{data.length < 32 && data.map((d, i) => {
|
||||
const x = (i / (data.length - 1)) * (width - padding * 2) + padding;
|
||||
const y = height - (d.value / maxValue) * (height - padding * 2) - padding;
|
||||
return (
|
||||
<circle key={i} cx={x} cy={y} r="4" fill="hsl(var(--background))" stroke="hsl(var(--primary))" strokeWidth="2" />
|
||||
<g key={i} className="group/dot">
|
||||
<circle cx={x} cy={y} r="3.5" fill="hsl(var(--background))" stroke="hsl(var(--primary))" strokeWidth="2" className="transition-all duration-300 group-hover/dot:r-5 group-hover/dot:stroke-white" />
|
||||
<circle cx={x} cy={y} r="10" fill="hsl(var(--primary))" fillOpacity="0" className="cursor-pointer" />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* X-Axis Labels */}
|
||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-[2%] text-xs text-muted-foreground pointer-events-none font-mono">
|
||||
{data.filter((_, i) => i % Math.ceil(data.length / 6) === 0).map((d, i) => (
|
||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-[3%] text-[8px] text-muted-foreground pointer-events-none font-mono opacity-30 mt-2">
|
||||
{data.filter((_, i) => i % Math.ceil(data.length / 7) === 0).map((d, i) => (
|
||||
<span key={i}>{d.label}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user