feat: Initialize AI Studio project structure

Sets up the basic project files and dependencies for an AI Studio application. Includes README, `.env.example`, `.gitignore`, basic HTML structure, metadata, `package.json`, Vite configuration, and initial React component setup. This commit provides the foundation for running and deploying the AI Studio app locally.
This commit is contained in:
pazz09adk-glitch
2026-04-29 18:05:24 +08:00
parent 4cf605d82e
commit da191b0dbf
20 changed files with 5455 additions and 8 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

View File

@@ -1,11 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
<h1>Built with AI Studio</h2>
<p>The fastest path from prompt to production with Gemini.</p>
<a href="https://aistudio.google.com/apps">Start building</a>
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/99f30234-252e-4899-b2b5-6d6d1c336581
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6
metadata.json Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "",
"description": "",
"requestFramePermissions": [],
"majorCapabilities": []
}

4738
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}

26
src/App.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { useState } from 'react';
import { Dashboard } from './components/Dashboard';
import { Sidebar } from './components/Sidebar';
import { Header } from './components/Header';
export default function App() {
const [currentView, setCurrentView] = useState('overall');
const viewTitles: Record<string, string> = {
'overall': '全网精细化运营大盘',
'efficiency': '各站点调度与效能监控',
'users': '平台用户与资产池'
};
return (
<div className="flex h-screen bg-slate-50 overflow-hidden font-sans">
<Sidebar currentView={currentView} setCurrentView={setCurrentView} />
<div className="flex flex-col flex-1 overflow-hidden">
<Header title={viewTitles[currentView] || '数据分析'} />
<main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
<Dashboard currentView={currentView} />
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { EfficiencyView } from './views/EfficiencyView';
import { OverallView } from './views/OverallView';
import { UsersView } from './views/UsersView';
export function Dashboard({ currentView }: { currentView: string }) {
return (
<div className="max-w-[1600px] mx-auto pb-10">
{currentView === '效率' && <EfficiencyView />}
{currentView === 'overall' && <OverallView />}
{currentView === 'efficiency' && <EfficiencyView />}
{currentView === 'users' && <UsersView />}
</div>
);
}

39
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Bell, Search, UserCircle, ChevronDown } from 'lucide-react';
export function Header({ title }: { title: string }) {
return (
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 md:px-6 shrink-0 z-10 w-full transition-all">
<div className="flex items-center">
<h1 className="text-lg font-bold text-slate-800 mr-8 tracking-tight">{title}</h1>
<div className="hidden md:flex items-center bg-slate-100 px-3 py-2 rounded-lg w-64 focus-within:ring-2 focus-within:ring-indigo-500/20 border border-transparent focus-within:border-indigo-500/50 transition-all">
<Search className="w-4 h-4 text-slate-400 mr-2 shrink-0" />
<input
type="text"
placeholder="搜索司机、车牌或订单号..."
className="bg-transparent border-none outline-none text-sm text-slate-700 w-full placeholder:text-slate-400"
/>
</div>
</div>
<div className="flex items-center space-x-4 text-slate-500 shrink-0">
<div className="hidden sm:flex text-xs text-indigo-600 bg-indigo-50 border border-indigo-100 px-3 py-1.5 rounded-full font-medium items-center shadow-sm">
<span className="w-2 h-2 rounded-full bg-indigo-500 mr-2 animate-pulse" />
周期: 4/18 - 4/29
</div>
<button className="hover:text-slate-700 transition-colors relative ml-2 p-2 hover:bg-slate-100 rounded-full">
<Bell className="w-5 h-5" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-rose-500 rounded-full border-2 border-white"></span>
</button>
<div className="w-px h-6 bg-slate-200 mx-1"></div>
<button className="flex items-center space-x-2 hover:bg-slate-50 p-1.5 pl-2 pr-3 rounded-xl transition-colors border border-transparent hover:border-slate-200">
<UserCircle className="w-8 h-8 text-indigo-400 bg-indigo-50 rounded-full" />
<div className="text-left hidden sm:block">
<p className="text-sm font-bold text-slate-700 leading-none"></p>
<p className="text-[#94a3b8] text-[11px] mt-1 font-medium"></p>
</div>
<ChevronDown className="w-4 h-4 text-slate-400 ml-1" />
</button>
</div>
</header>
);
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { BarChart3, Calendar, Settings, Fuel, Users, ShieldCheck } from 'lucide-react';
import { cn } from '../lib/utils';
export function Sidebar({ currentView, setCurrentView }: { currentView: string; setCurrentView: (v: string) => void }) {
const menuItems = [
{ id: 'overall', name: '全网运营总览', icon: Calendar },
{ id: 'users', name: '司机与车辆大盘', icon: Users },
{ id: 'efficiency', name: '站点效能监控', icon: BarChart3 },
];
return (
<aside className="w-64 bg-slate-900 border-r border-slate-800 hidden md:flex flex-col text-slate-300 shadow-xl overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-[600px] bg-gradient-to-b from-indigo-500/10 to-transparent pointer-events-none" />
<div className="h-16 flex items-center px-6 border-b border-white/5 shrink-0 z-10 bg-slate-900/50 backdrop-blur-md">
<Fuel className="w-6 h-6 text-indigo-400 mr-2 drop-shadow-md" />
<span className="text-lg font-bold text-white tracking-tight"> BI</span>
</div>
<nav className="flex-1 py-6 px-4 space-y-2 z-10 overflow-y-auto">
<div className="text-xs font-bold text-slate-500/80 uppercase tracking-wider mb-4 px-3"></div>
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => setCurrentView(item.id)}
className={cn(
"flex items-center w-full px-3 py-3 rounded-xl text-sm font-semibold transition-all duration-200 text-left group",
currentView === item.id
? "bg-indigo-500/15 text-indigo-300 shadow-sm border border-indigo-500/30"
: "hover:bg-slate-800/80 hover:text-slate-100 border border-transparent text-slate-400"
)}
>
<item.icon className={cn("w-5 h-5 mr-3 transition-colors", currentView === item.id ? "text-indigo-400" : "text-slate-500 group-hover:text-slate-300")} />
{item.name}
</button>
))}
<div className="mt-8 pt-6 border-t border-white/5">
<div className="text-xs font-bold text-slate-500/80 uppercase tracking-wider mb-2 px-3"></div>
<button className="flex items-center w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors text-left text-slate-400 hover:bg-slate-800 hover:text-slate-100">
<Settings className="w-5 h-5 mr-3 text-slate-500" />
</button>
</div>
</nav>
<div className="p-4 border-t border-white/5 bg-slate-900 z-10 shrink-0">
<div className="bg-gradient-to-br from-slate-800 to-slate-900 border border-slate-700/50 p-4 rounded-xl text-xs relative overflow-hidden shadow-inner">
<div className="absolute top-0 right-0 w-16 h-16 bg-indigo-500/10 rounded-full blur-xl -mr-4 -mt-4"></div>
<p className="text-slate-400 mb-1.5 font-medium"> ()</p>
<p className="font-bold text-slate-200 text-sm">26418 - 429</p>
<div className="mt-3 flex items-center text-[10px] text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded w-fit border border-emerald-400/20">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mr-1.5"></span>
</div>
</div>
</div>
</aside>
);
}

View File

@@ -0,0 +1,36 @@
import { cn } from '../../lib/utils';
import { LucideIcon } from 'lucide-react';
interface StatCardProps {
title: string;
value: string | number;
subValue?: string;
icon: LucideIcon;
trend?: number;
highlight?: boolean;
}
export function StatCard({ title, value, subValue, icon: Icon, trend, highlight }: StatCardProps) {
return (
<div className={cn("bg-white rounded-xl border border-slate-100 p-5 shadow-sm transition-all hover:shadow-md", highlight && "ring-1 ring-indigo-500/50")}>
<div className="flex justify-between items-start">
<div>
<p className="text-sm font-medium text-slate-500 mb-1">{title}</p>
<h3 className="text-2xl font-extrabold text-slate-800 tracking-tight">{value}</h3>
</div>
<div className={cn("p-2.5 rounded-xl bg-slate-50", highlight && "bg-indigo-50 text-indigo-600")}>
<Icon className={cn("w-5 h-5 text-slate-400", highlight && "text-indigo-600")} />
</div>
</div>
<div className="mt-4 flex items-center text-sm">
{trend !== undefined && (
<span className={cn("font-semibold mr-2 flex items-center", trend > 0 ? "text-emerald-500" : "text-rose-500")}>
{trend > 0 ? "+" : ""}{trend}%
{trend > 0 ? <span className="ml-1 text-emerald-500"></span> : <span className="ml-1 text-rose-500"></span>}
</span>
)}
<span className="text-slate-400 font-medium text-xs">{subValue}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
import React from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Legend, Cell } from 'recharts';
import { StatCard } from '../ui/StatCard';
import { Activity, Target, TrendingUp, Zap, Clock, Radio, AlertCircle } from 'lucide-react';
const stationData = [
{ name: '嘉兴嘉燃', 消耗: 420, 剩余: 580, 上限: 1000 },
{ name: '嘉兴嘉锦', 消耗: 380, 剩余: 420, 上限: 800 },
{ name: '如皋华神', 消耗: 160, 剩余: 340, 上限: 500 },
{ name: '花桥中石油', 消耗: 80, 剩余: 220, 上限: 300 },
{ name: '常熟嘉化', 消耗: 45, 剩余: 155, 上限: 200 }
];
const stationGrowth = [
{ date: '4/18', 嘉燃: 10, 嘉锦: 3, 其他: 0 },
{ date: '4/19', 嘉燃: 6, 嘉锦: 3, 其他: 0 },
{ date: '4/20', 嘉燃: 7, 嘉锦: 0, 其他: 0 },
{ date: '4/21', 嘉燃: 5, 嘉锦: 3, 其他: 0 },
{ date: '4/22', 嘉燃: 7, 嘉锦: 3, 其他: 1 },
{ date: '4/23', 嘉燃: 7, 嘉锦: 15, 其他: 7 },
{ date: '4/24', 嘉燃: 9, 嘉锦: 14, 其他: 6 },
{ date: '4/25', 嘉燃: 6, 嘉锦: 12, 其他: 4 },
{ date: '4/26', 嘉燃: 6, 嘉锦: 13, 其他: 3 },
{ date: '4/27', 嘉燃: 6, 嘉锦: 13, 其他: 3 },
{ date: '4/28', 嘉燃: 7, 嘉锦: 13, 其他: 2 },
{ date: '4/29', 嘉燃: 7, 嘉锦: 13, 其他: 2 },
];
export function EfficiencyView() {
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="bg-gradient-to-r from-indigo-50 to-blue-50 border border-indigo-100 rounded-xl p-5 flex items-start shadow-sm">
<div className="bg-indigo-100/80 p-2 rounded-lg mr-4 shrink-0">
<Radio className="w-5 h-5 text-indigo-600" />
</div>
<div>
<h4 className="text-sm font-bold text-indigo-900 flex items-center">
(IoT)
<span className="ml-3 px-2 py-0.5 rounded text-[10px] bg-indigo-200 text-indigo-800 font-bold tracking-wider">PHASE 2</span>
</h4>
<p className="text-sm text-indigo-700 mt-1.5 leading-relaxed">
<span className="font-semibold text-indigo-900 border-b border-indigo-200"></span>线<b></b><span className="font-semibold text-indigo-900 border-b border-indigo-200"></span>
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
title="全网综合日均加注量预估"
value="1,085 kg"
subValue="主要覆盖嘉兴、南通等核心枢纽网的测算值"
icon={Activity}
highlight
/>
<StatCard
title="全网各站总负荷上限满载"
value="2,800 kg"
subValue="所有加注站理论最高吞吐物理上限汇总"
icon={Zap}
/>
<StatCard
title="系统优化全站有效工时"
value="~4.5 小时"
subValue="主要由预约时限测算出的无效排队节省时间"
icon={Clock}
trend={-45}
/>
<StatCard
title="平均承载饱和度 (预估)"
value="38.5 %"
subValue="预留了充足的峰值缓冲池与调配拓展空间"
icon={Target}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="text-lg font-bold text-slate-800"></h3>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
<div className="flex bg-indigo-50 text-indigo-600 px-3 py-1.5 rounded-lg text-sm font-semibold items-center border border-indigo-100">
<TrendingUp className="w-4 h-4 mr-1.5" />
线
</div>
</div>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={stationGrowth} margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<defs>
<linearGradient id="colorJr" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#0ea5e9" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#0ea5e9" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorJj" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} />
<CartesianGrid vertical={false} stroke="#f1f5f9" strokeDasharray="4 4" />
<Tooltip cursor={{stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '4 4'}} contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
<Legend verticalAlign="top" height={36} iconType="circle" wrapperStyle={{ fontSize: '12px' }} />
<Area type="monotone" dataKey="嘉燃" stackId="1" stroke="#0ea5e9" strokeWidth={2} fillOpacity={1} fill="url(#colorJr)" />
<Area type="monotone" dataKey="嘉锦" stackId="1" stroke="#8b5cf6" strokeWidth={2} fillOpacity={1} fill="url(#colorJj)" />
<Area type="monotone" dataKey="其他" stackId="1" stroke="#f59e0b" strokeWidth={2} fillOpacity={0.1} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm flex flex-col justify-between relative overflow-hidden">
<div className="absolute top-0 right-0 p-6 opacity-5 pointer-events-none">
<Zap className="w-32 h-32" />
</div>
<div className="mb-6 relative z-10">
<div className="flex flex-col xl:flex-row xl:justify-between xl:items-start gap-3">
<div>
<h3 className="text-lg font-bold text-slate-800"> (kg)</h3>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
<span className="flex items-center text-[10px] lg:text-xs font-bold bg-amber-50 text-amber-600 px-2 py-1 rounded-md border border-amber-200 w-fit shrink-0">
<AlertCircle className="w-3 h-3 mr-1.5" />
IoT硬件直连筹备中
</span>
</div>
</div>
<div className="flex-1 min-h-[300px] relative z-10">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stationData} layout="vertical" margin={{ top: 0, right: 30, left: 20, bottom: 0 }}>
<XAxis type="number" hide />
<YAxis dataKey="name" type="category" axisLine={false} tickLine={false} tick={{fill: '#475569', fontSize: 12, fontWeight: 500}} width={80} />
<Tooltip cursor={{fill: '#f8fafc'}} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.05)' }}/>
<Legend verticalAlign="top" height={36} iconType="circle" wrapperStyle={{ fontSize: '12px' }} />
<Bar dataKey="消耗" stackId="a" fill="#4f46e5" radius={[0, 0, 0, 0]} barSize={24} name="当前消耗测算负荷" />
<Bar dataKey="剩余" stackId="a" fill="#e2e8f0" radius={[0, 4, 4, 0]} barSize={24} name="未饱和推演备用空间" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { StatCard } from '../ui/StatCard';
import { Calendar, Network, MapPin, UserCheck } from 'lucide-react';
const weeklyTrend = [
{ name: '3/16-3/20', 总量: 17 },
{ name: '3/21-3/27', 总量: 47 },
{ name: '3/28-4/6', 总量: 55 },
{ name: '4/7-4/11', 总量: 40 },
{ name: '4/12-4/17', 总量: 49 },
{ name: '4/18-4/24', 总量: 106 },
{ name: '4/25-4/29', 总量: 110 },
];
const dailyBreakdown = [
{ date: '4/18', 嘉燃: 10, 嘉锦: 3, 如皋: 0, 其他: 0 },
{ date: '4/19', 嘉燃: 6, 嘉锦: 3, 如皋: 0, 其他: 0 },
{ date: '4/20', 嘉燃: 7, 嘉锦: 0, 如皋: 0, 其他: 0 },
{ date: '4/21', 嘉燃: 5, 嘉锦: 3, 如皋: 0, 其他: 0 },
{ date: '4/22', 嘉燃: 7, 嘉锦: 3, 如皋: 1, 其他: 0 },
{ date: '4/23', 嘉燃: 7, 嘉锦: 15, 如皋: 4, 其他: 3 },
{ date: '4/24', 嘉燃: 9, 嘉锦: 14, 如皋: 4, 其他: 2 },
{ date: '4/25', 嘉燃: 6, 嘉锦: 12, 如皋: 1, 其他: 3 },
{ date: '4/26', 嘉燃: 6, 嘉锦: 13, 如皋: 1, 其他: 2 },
{ date: '4/27', 嘉燃: 6, 嘉锦: 13, 如皋: 2, 其他: 1 },
{ date: '4/28', 嘉燃: 7, 嘉锦: 13, 如皋: 1, 其他: 1 },
{ date: '4/29', 嘉燃: 7, 嘉锦: 13, 如皋: 2, 其他: 0 },
];
const activeStations = [
{ name: '嘉兴嘉燃', isNew: false },
{ name: '嘉兴嘉锦', isNew: false },
{ name: '如皋华神', isNew: false },
{ name: '常熟嘉化', isNew: true },
{ name: '桐乡绿能', isNew: true },
{ name: '花桥中石油', isNew: true },
{ name: '嘉善站前路', isNew: true },
];
export function OverallView() {
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
title="系统累计预约总单量"
value="424 单"
subValue="全网7周累计历史总量"
icon={Network}
highlight
/>
<StatCard
title="本周(4/25-4/29)新增单量"
value="110 单"
subValue="嘉锦占比过半,增势迅猛"
icon={Calendar}
trend={116}
/>
<StatCard
title="现阶段活跃运营站点"
value="7 个"
subValue="期间新增桐乡、花桥、前路加氢站"
icon={MapPin}
trend={75}
/>
<StatCard
title="近期活跃司机数量"
value="45 人"
subValue="4/25-4/29 期间内活跃"
icon={UserCheck}
/>
</div>
<div className="bg-white border border-indigo-100/50 rounded-xl p-4 shadow-sm flex flex-col md:flex-row md:items-center gap-3">
<div className="flex items-center text-sm font-bold text-slate-700 mr-2 shrink-0">
<MapPin className="w-4 h-4 mr-1.5 text-indigo-500" />
({activeStations.length}):
</div>
<div className="flex flex-wrap gap-2">
{activeStations.map(station => (
<div key={station.name} className="flex items-center bg-slate-50 border border-slate-200 px-3 py-1.5 rounded-lg text-sm transition-colors hover:border-indigo-200 hover:bg-indigo-50/50">
<span className="text-slate-700 font-medium">{station.name}</span>
{station.isNew && (
<span className="ml-2 px-1.5 py-0.5 rounded text-[10px] font-bold bg-rose-100 text-rose-600 border border-rose-200">NEW</span>
)}
</div>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
<div className="mb-6">
<h3 className="text-lg font-bold text-slate-800">6</h3>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={weeklyTrend} margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid vertical={false} stroke="#f1f5f9" strokeDasharray="3 3"/>
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 11}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} />
<Tooltip cursor={{stroke: '#e2e8f0', strokeWidth: 2}} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}/>
<Line type="monotone" dataKey="总量" stroke="#0ea5e9" strokeWidth={4} dot={{r: 6, strokeWidth: 3, fill: '#fff'}} activeDot={{r: 8, strokeWidth: 0, fill: '#0284c7'}} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
<div className="mb-6">
<h3 className="text-lg font-bold text-slate-800"></h3>
<p className="text-sm text-slate-500 mt-1">线</p>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={dailyBreakdown} margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid vertical={false} stroke="#f1f5f9" strokeDasharray="3 3" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} />
<Tooltip cursor={{fill: '#f8fafc'}} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.05)' }}/>
<Legend verticalAlign="top" height={36} iconType="circle" wrapperStyle={{ fontSize: '12px' }} />
<Bar dataKey="嘉燃" stackId="a" fill="#0ea5e9" radius={[0, 0, 4, 4]} />
<Bar dataKey="嘉锦" stackId="a" fill="#8b5cf6" />
<Bar dataKey="如皋" stackId="a" fill="#f59e0b" />
<Bar dataKey="其他" stackId="a" fill="#94a3b8" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, BarChart, Bar, Cell } from 'recharts';
import { StatCard } from '../ui/StatCard';
import { Users, Truck, Smartphone } from 'lucide-react';
const driverGrowth = [
{ date: '4/18', 活跃APP: 57, 绑定车辆: 114 },
{ date: '4/19', 活跃APP: 57, 绑定车辆: 114 },
{ date: '4/20', 活跃APP: 58, 绑定车辆: 114 },
{ date: '4/21', 活跃APP: 59, 绑定车辆: 117 },
{ date: '4/22', 活跃APP: 61, 绑定车辆: 121 },
{ date: '4/23', 活跃APP: 74, 绑定车辆: 128 },
{ date: '4/24', 活跃APP: 82, 绑定车辆: 135 },
{ date: '4/25', 活跃APP: 84, 绑定车辆: 139 },
{ date: '4/26', 活跃APP: 86, 绑定车辆: 139 },
{ date: '4/27', 活跃APP: 89, 绑定车辆: 140 },
{ date: '4/28', 活跃APP: 91, 绑定车辆: 141 },
{ date: '4/29', 活跃APP: 93, 绑定车辆: 142 },
];
const driverByStation = [
{ name: '如皋华神', value: 40 },
{ name: '嘉兴嘉燃', value: 32 },
{ name: '嘉兴嘉锦', value: 32 },
{ name: '桐乡绿能', value: 4 },
{ name: '花桥/嘉善等', value: 7 },
];
export function UsersView() {
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
title="累计入池APP用户总数"
value="93 人"
subValue="近期活跃司机达到45人"
icon={Smartphone}
highlight
/>
<StatCard
title="累计绑定车辆司机总数"
value="142 人"
subValue="系统全量历史转化留存"
icon={Truck}
/>
<StatCard
title="总核验车辆绑定率"
value="95 %"
subValue="公司自营与核心车队"
icon={Users}
trend={5}
/>
<StatCard
title="本周最佳外拓来源区"
value="长三角外围"
subValue="如皋及花桥累计贡献非标散户6个"
icon={Users}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
<div className="mb-6">
<h3 className="text-lg font-bold text-slate-800">APP渗透与车辆绑定追踪</h3>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={driverGrowth} margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid vertical={false} stroke="#f1f5f9" strokeDasharray="3 3"/>
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#64748b', fontSize: 12}} />
<Tooltip cursor={{stroke: '#e2e8f0', strokeWidth: 2}} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0' }}/>
<Legend verticalAlign="top" height={36} iconType="circle" wrapperStyle={{ fontSize: '12px' }} />
<Line type="monotone" dataKey="绑定车辆" stroke="#6366f1" strokeWidth={3} dot={{r: 4}} activeDot={{r: 6}} />
<Line type="monotone" dataKey="活跃APP" stroke="#14b8a6" strokeWidth={3} dot={{r: 4}} activeDot={{r: 6}} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
<div className="mb-6">
<h3 className="text-lg font-bold text-slate-800"></h3>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={driverByStation} layout="vertical" margin={{ top: 0, right: 30, left: 20, bottom: 0 }}>
<XAxis type="number" hide />
<YAxis dataKey="name" type="category" axisLine={false} tickLine={false} tick={{fill: '#475569', fontSize: 12, fontWeight: 500}} width={80} />
<Tooltip cursor={{fill: '#f8fafc'}} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0' }}/>
<Bar dataKey="value" radius={[0, 6, 6, 0]} barSize={28}>
{driverByStation.map((entry, index) => (
<Cell key={`cell-${index}`} fill={index === 0 ? '#f59e0b' : '#94a3b8'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
}

1
src/index.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

24
vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});