初始化
commit
cb20873c2d
|
|
@ -0,0 +1,20 @@
|
|||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</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/329e8442-1371-4537-aa54-9f6e8022bb95
|
||||
|
||||
## 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`
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "紫光汇智CRM系统",
|
||||
"description": "专为销售人员设计的移动端与Web双端CRM系统,包含拓展管理、商机储备、外勤打卡与销售日报等核心功能。",
|
||||
"requestFramePermissions": ["camera", "geolocation", "microphone"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"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",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"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.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Layout from "./components/Layout";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Expansion from "./pages/Expansion";
|
||||
import Opportunities from "./pages/Opportunities";
|
||||
import Work from "./pages/Work";
|
||||
import Profile from "./pages/Profile";
|
||||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="light" storageKey="crm-theme">
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="expansion" element={<Expansion />} />
|
||||
<Route path="opportunities" element={<Opportunities />} />
|
||||
<Route path="work" element={<Work />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||
import { Home, Users, Briefcase, CalendarCheck, User, Moon, Sun } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
const navItems = [
|
||||
{ name: "首页", path: "/", icon: Home },
|
||||
{ name: "拓展", path: "/expansion", icon: Users },
|
||||
{ name: "商机", path: "/opportunities", icon: Briefcase },
|
||||
{ name: "工作", path: "/work", icon: CalendarCheck },
|
||||
{ name: "我的", path: "/profile", icon: User },
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-50 transition-colors duration-300">
|
||||
{/* Desktop Sidebar */}
|
||||
<aside className="hidden w-64 flex-col border-r border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 md:flex transition-colors duration-300">
|
||||
<div className="flex h-16 items-center justify-between border-b border-slate-200 dark:border-slate-800 px-6">
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-violet-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
紫光汇智CRM
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="rounded-full p-2 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 space-y-2 p-4">
|
||||
{navItems.map((item) => {
|
||||
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm font-medium transition-all duration-200",
|
||||
isActive
|
||||
? "bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-400 shadow-sm"
|
||||
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-50"
|
||||
)}
|
||||
>
|
||||
<item.icon className={cn("h-5 w-5 transition-transform", isActive && "scale-110")} />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto pb-20 md:pb-0 relative">
|
||||
<div className="mx-auto max-w-5xl p-4 md:p-8">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile Bottom Nav */}
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 flex h-16 border-t border-slate-200 dark:border-slate-800 bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl pb-safe md:hidden transition-colors duration-300">
|
||||
{navItems.map((item) => {
|
||||
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
"flex flex-1 flex-col items-center justify-center gap-1 text-[10px] font-medium transition-all duration-200",
|
||||
isActive ? "text-violet-600 dark:text-violet-400" : "text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-50"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200",
|
||||
isActive ? "bg-violet-100 dark:bg-violet-500/20" : "bg-transparent"
|
||||
)}>
|
||||
<item.icon className={cn("h-5 w-5", isActive && "fill-violet-100 dark:fill-violet-500/20")} />
|
||||
</div>
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "crm-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
return context;
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-hide {
|
||||
/* IE and Edge */
|
||||
-ms-overflow-style: none;
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -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>,
|
||||
);
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { BarChart3, TrendingUp, Users, CheckCircle2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export default function Dashboard() {
|
||||
const stats = [
|
||||
{ name: "本月新增商机", value: "12", icon: TrendingUp, color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-100 dark:bg-emerald-500/20" },
|
||||
{ name: "跟进中客户", value: "48", icon: Users, color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-100 dark:bg-blue-500/20" },
|
||||
{ name: "已成单项目", value: "3", icon: CheckCircle2, color: "text-violet-600 dark:text-violet-400", bg: "bg-violet-100 dark:bg-violet-500/20" },
|
||||
{ name: "本月打卡天数", value: "20", icon: BarChart3, color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-100 dark:bg-amber-500/20" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">工作台</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">欢迎回来,张三。今天是你入职的第 365 天。</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{stats.map((stat, i) => (
|
||||
<motion.div
|
||||
key={stat.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-5 shadow-sm backdrop-blur-sm transition-all hover:shadow-md dark:hover:bg-slate-900"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-xl ${stat.bg}`}>
|
||||
<stat.icon className={`h-6 w-6 ${stat.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">{stat.name}</p>
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stat.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">待办事项</h2>
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
"跟进 A 公司云桌面扩容项目",
|
||||
"提交本周销售日报",
|
||||
"拜访 B 渠道商王总",
|
||||
"更新 C 项目商机阶段",
|
||||
].map((task, i) => (
|
||||
<li key={i} className="group flex cursor-pointer items-center gap-3 rounded-xl border border-slate-50 dark:border-slate-800/50 p-3 transition-all hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-slate-300 dark:border-slate-600 group-hover:border-violet-500 dark:group-hover:border-violet-400 transition-colors" />
|
||||
<span className="text-sm text-slate-700 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{task}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">最新动态</h2>
|
||||
<div className="space-y-5">
|
||||
{[
|
||||
{ title: "商机阶段更新", desc: "D 项目已推进至方案交流阶段", time: "10分钟前" },
|
||||
{ title: "日报已点评", desc: "主管对你昨天的日报给出了 95 分", time: "2小时前" },
|
||||
{ title: "新渠道录入", desc: "成功录入 E 渠道商信息", time: "昨天" },
|
||||
].map((news, i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className="relative mt-1 flex h-3 w-3 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-20"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white">{news.title}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{news.desc}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400 dark:text-slate-500">{news.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
import { useState } from "react";
|
||||
import { Search, Plus, MapPin, Building2, User, Phone, X, Clock, FileText, Mail, Calendar } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
export default function Expansion() {
|
||||
const [activeTab, setActiveTab] = useState<"sales" | "channel">("sales");
|
||||
const [selectedItem, setSelectedItem] = useState<any | null>(null);
|
||||
|
||||
const salesData = [
|
||||
{
|
||||
id: 1, type: "sales", name: "李四", phone: "13812345678", email: "lisi@example.com",
|
||||
dept: "华东大区", industry: "教育", title: "高级销售", intent: "高", stage: "初步沟通",
|
||||
hasExp: true, inProgress: true, active: true, expectedJoinDate: "2024-05-01",
|
||||
notes: "候选人对提成机制比较关注,在教育行业有5年以上的客户资源积累。"
|
||||
},
|
||||
{
|
||||
id: 2, type: "sales", name: "王五", phone: "13987654321", email: "wangwu@example.com",
|
||||
dept: "华北大区", industry: "医疗", title: "销售经理", intent: "中", stage: "方案交流",
|
||||
hasExp: false, inProgress: false, active: true, expectedJoinDate: "待定",
|
||||
notes: "需要进一步沟通产品线细节,目前在看其他几家竞品的机会。"
|
||||
},
|
||||
];
|
||||
|
||||
const channelData = [
|
||||
{
|
||||
id: 1, type: "channel", name: "某某科技代理商", province: "浙江", industry: "政府",
|
||||
revenue: "500万", size: 50, contact: "张总", contactTitle: "总经理", phone: "13800138000",
|
||||
stage: "合作洽谈", landed: true, expectedSignDate: "2024-04-15",
|
||||
notes: "对方在政务云桌面领域有深厚资源,希望能拿到省级独家代理权。"
|
||||
},
|
||||
{
|
||||
id: 2, type: "channel", name: "云端服务提供商", province: "江苏", industry: "金融",
|
||||
revenue: "1000万", size: 120, contact: "李总", contactTitle: "业务总监", phone: "13900139000",
|
||||
stage: "初步接触", landed: false, expectedSignDate: "待定",
|
||||
notes: "初步接触,对方正在评估多家供应商,对我们的售后响应速度有较高要求。"
|
||||
},
|
||||
];
|
||||
|
||||
const followUpRecords = [
|
||||
{ id: 1, date: "2024-03-15 14:30", type: "电话沟通", content: "初步沟通了合作意向,对方对我们的产品比较感兴趣,约定下周进行详细的产品演示。", user: "张三" },
|
||||
{ id: 2, date: "2024-03-10 10:00", type: "微信沟通", content: "发送了公司介绍和产品白皮书,对方表示会内部评估。", user: "张三" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">拓展管理</h1>
|
||||
<button className="flex items-center gap-2 rounded-xl bg-violet-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-violet-700 active:scale-95 transition-all">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">新增</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex rounded-xl bg-slate-100 dark:bg-slate-900/50 p-1 backdrop-blur-sm border border-slate-200/50 dark:border-slate-800/50">
|
||||
<button
|
||||
onClick={() => setActiveTab("sales")}
|
||||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "sales" ? "bg-white dark:bg-slate-800 text-violet-600 dark:text-violet-400 shadow-sm" : "text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||
}`}
|
||||
>
|
||||
销售人员拓展
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("channel")}
|
||||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "channel" ? "bg-white dark:bg-slate-800 text-violet-600 dark:text-violet-400 shadow-sm" : "text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||
}`}
|
||||
>
|
||||
渠道拓展
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 group-focus-within:text-violet-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索姓名、渠道名称、行业..."
|
||||
className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 py-2.5 pl-10 pr-4 text-sm text-slate-900 dark:text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="space-y-4">
|
||||
{activeTab === "sales" ? (
|
||||
salesData.map((item, i) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="cursor-pointer rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-5 shadow-sm backdrop-blur-sm transition-all hover:shadow-md hover:border-violet-100 dark:hover:border-violet-900/50"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">{item.name}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{item.dept} · {item.title}</p>
|
||||
</div>
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${item.active ? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300'}`}>
|
||||
{item.active ? '在职' : '离职'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-y-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<Building2 className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
{item.industry}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400 dark:text-slate-500">意向:</span>
|
||||
<span className={item.intent === '高' ? 'text-rose-600 dark:text-rose-400 font-medium' : ''}>{item.intent}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400 dark:text-slate-500">阶段:</span>
|
||||
{item.stage}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400 dark:text-slate-500">云桌面经验:</span>
|
||||
{item.hasExp ? '有' : '无'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end border-t border-slate-50 dark:border-slate-800/50 pt-3">
|
||||
<button className="text-sm font-medium text-violet-600 dark:text-violet-400 hover:text-violet-700 dark:hover:text-violet-300 transition-colors">查看详情与跟进</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
) : (
|
||||
channelData.map((item, i) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="cursor-pointer rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-5 shadow-sm backdrop-blur-sm transition-all hover:shadow-md hover:border-violet-100 dark:hover:border-violet-900/50"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">{item.name}</h3>
|
||||
<div className="mt-1 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
<MapPin className="h-3.5 w-3.5" />
|
||||
{item.province}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${item.landed ? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400' : 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400'}`}>
|
||||
{item.landed ? '已落地' : '未落地'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-y-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<Building2 className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
{item.industry}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<User className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
{item.contact}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<Phone className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
{item.phone}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400 dark:text-slate-500">阶段:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-white">{item.stage}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end border-t border-slate-50 dark:border-slate-800/50 pt-3">
|
||||
<button className="text-sm font-medium text-violet-600 dark:text-violet-400 hover:text-violet-700 dark:hover:text-violet-300 transition-colors">查看详情与跟进</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail Slide-over Panel */}
|
||||
<AnimatePresence>
|
||||
{selectedItem && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="fixed inset-0 bg-slate-900/20 dark:bg-slate-900/60 backdrop-blur-sm z-40"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||
className="fixed inset-y-0 right-0 w-full max-w-md bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-200 dark:border-slate-800 z-50 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-100 dark:border-slate-800 px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{selectedItem.type === 'sales' ? '销售拓展详情' : '渠道拓展详情'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="rounded-full p-2 text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
{/* Header Info */}
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white">{selectedItem.name}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selectedItem.type === 'sales' ? `${selectedItem.dept} · ${selectedItem.title}` : `${selectedItem.province} · ${selectedItem.industry}`}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<span className="rounded-full bg-violet-50 dark:bg-violet-500/10 px-2.5 py-1 text-xs font-medium text-violet-600 dark:text-violet-400">
|
||||
{selectedItem.stage}
|
||||
</span>
|
||||
{selectedItem.type === 'sales' ? (
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${selectedItem.active ? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'}`}>
|
||||
{selectedItem.active ? '在职' : '离职'}
|
||||
</span>
|
||||
) : (
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${selectedItem.landed ? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400'}`}>
|
||||
{selectedItem.landed ? '已落地' : '未落地'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Info Grid */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-violet-500" />
|
||||
基本信息
|
||||
</h4>
|
||||
<div className="rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/20 p-4 grid grid-cols-2 gap-4 text-sm">
|
||||
{selectedItem.type === 'sales' ? (
|
||||
<>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Phone className="h-3 w-3"/> 联系电话</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.phone}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Mail className="h-3 w-3"/> 邮箱</p><p className="font-medium text-slate-900 dark:text-white truncate" title={selectedItem.email}>{selectedItem.email}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Building2 className="h-3 w-3"/> 负责行业</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.industry}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1">云桌面经验</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.hasExp ? '有' : '无'}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1">意向度</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.intent}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Calendar className="h-3 w-3"/> 预计入职</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.expectedJoinDate}</p></div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><User className="h-3 w-3"/> 联系人</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.contact} ({selectedItem.contactTitle})</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Phone className="h-3 w-3"/> 联系电话</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.phone}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1">营收规模</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.revenue}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1">公司人数</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.size}人</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Calendar className="h-3 w-3"/> 预计签约</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.expectedSignDate}</p></div>
|
||||
</>
|
||||
)}
|
||||
<div className="col-span-2"><p className="text-slate-500 dark:text-slate-400 mb-1">备注说明</p><p className="font-medium text-slate-900 dark:text-white leading-relaxed">{selectedItem.notes}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-violet-500" />
|
||||
跟进记录
|
||||
</h4>
|
||||
<button className="text-xs font-medium text-violet-600 dark:text-violet-400 hover:text-violet-700">添加记录</button>
|
||||
</div>
|
||||
<div className="relative pl-4 border-l-2 border-slate-100 dark:border-slate-800 space-y-6">
|
||||
{followUpRecords.map((record) => (
|
||||
<div key={record.id} className="relative">
|
||||
<div className="absolute -left-[21px] mt-1.5 h-2.5 w-2.5 rounded-full bg-violet-500 ring-4 ring-white dark:ring-slate-900" />
|
||||
<div className="rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/20 p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-violet-600 dark:text-violet-400 bg-violet-50 dark:bg-violet-500/10 px-2 py-0.5 rounded">{record.type}</span>
|
||||
<span className="text-xs text-slate-400">{record.date}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300 leading-relaxed">{record.content}</p>
|
||||
<p className="text-xs text-slate-400 mt-2">跟进人: {record.user}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-slate-100 dark:border-slate-800 p-4 bg-slate-50 dark:bg-slate-900/50">
|
||||
<div className="flex gap-3">
|
||||
<button className="flex-1 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
|
||||
编辑资料
|
||||
</button>
|
||||
<button className="flex-1 rounded-xl bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
||||
写跟进
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
import { useState } from "react";
|
||||
import { Search, Plus, Filter, ChevronRight, Building, Calendar, DollarSign, Activity, X, Clock, FileText, User, Tag, Link as LinkIcon } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
export default function Opportunities() {
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [selectedItem, setSelectedItem] = useState<any | null>(null);
|
||||
|
||||
const oppData = [
|
||||
{
|
||||
id: "HD-20231024-001",
|
||||
name: "A市第一人民医院云桌面扩容",
|
||||
client: "A市第一人民医院",
|
||||
owner: "张三",
|
||||
amount: "1,200,000",
|
||||
date: "2023-12-15",
|
||||
confidence: 80,
|
||||
stage: "招投标",
|
||||
type: "扩容",
|
||||
pushedToOMS: true,
|
||||
product: "VDI云桌面",
|
||||
source: "渠道推荐",
|
||||
notes: "客户现有500点并发,本次计划扩容300点,主要用于门诊医生工作站。对性能要求较高,需要重点测试3D渲染能力。",
|
||||
},
|
||||
{
|
||||
id: "HB-20231025-002",
|
||||
name: "B大学新校区机房建设",
|
||||
client: "B大学",
|
||||
owner: "李四",
|
||||
amount: "3,500,000",
|
||||
date: "2024-03-01",
|
||||
confidence: 40,
|
||||
stage: "方案交流",
|
||||
type: "新建",
|
||||
pushedToOMS: false,
|
||||
product: "VOI云桌面",
|
||||
source: "市场活动",
|
||||
notes: "新校区规划了5个公共机房,共计800台终端。目前处于方案设计阶段,竞争对手有深信服和锐捷。",
|
||||
},
|
||||
{
|
||||
id: "HN-20231026-003",
|
||||
name: "C集团办公云桌面替换",
|
||||
client: "C集团",
|
||||
owner: "王五",
|
||||
amount: "800,000",
|
||||
date: "2023-11-30",
|
||||
confidence: 90,
|
||||
stage: "商务谈判",
|
||||
type: "新建",
|
||||
pushedToOMS: true,
|
||||
product: "IDV云桌面",
|
||||
source: "主动开发",
|
||||
notes: "替换原有传统PC,客户对数据安全和外设兼容性要求极高。POC测试已通过,目前正在进行价格谈判。",
|
||||
},
|
||||
];
|
||||
|
||||
const followUpRecords = [
|
||||
{ id: 1, date: "2023-10-25 14:30", type: "现场拜访", content: "与信息科主任沟通了扩容需求,确认了具体的点数和预算范围。主任对我们的前期服务比较认可。", user: "张三" },
|
||||
{ id: 2, date: "2023-10-20 10:00", type: "电话沟通", content: "初步了解了医院近期的信息化建设规划,得知有云桌面扩容的意向。", user: "张三" },
|
||||
];
|
||||
|
||||
const getConfidenceColor = (score: number) => {
|
||||
if (score >= 80) return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20";
|
||||
if (score >= 50) return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20";
|
||||
return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">商机储备</h1>
|
||||
<button className="flex items-center gap-2 rounded-xl bg-violet-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-violet-700 active:scale-95 transition-all">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">新增商机</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1 group">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 group-focus-within:text-violet-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索项目名称、客户、编码..."
|
||||
className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 py-2.5 pl-10 pr-4 text-sm text-slate-900 dark:text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
<button className="flex items-center justify-center rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 px-4 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<Filter className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Filters */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{["全部", "初步沟通", "方案交流", "招投标", "商务谈判", "已成交"].map((stage) => (
|
||||
<button
|
||||
key={stage}
|
||||
onClick={() => setFilter(stage)}
|
||||
className={`whitespace-nowrap rounded-full px-4 py-1.5 text-sm font-medium transition-all duration-200 ${
|
||||
filter === stage || (filter === "all" && stage === "全部")
|
||||
? "bg-slate-800 dark:bg-violet-600 text-white shadow-sm"
|
||||
: "bg-white dark:bg-slate-900/50 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||||
}`}
|
||||
>
|
||||
{stage}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="space-y-4">
|
||||
{oppData.map((opp, i) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
key={opp.id}
|
||||
onClick={() => setSelectedItem(opp)}
|
||||
className="group cursor-pointer relative rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-5 shadow-sm backdrop-blur-sm transition-all hover:shadow-md hover:border-violet-100 dark:hover:border-violet-900/50"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="pr-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">{opp.id}</span>
|
||||
{opp.pushedToOMS && (
|
||||
<span className="rounded bg-violet-50 dark:bg-violet-500/10 px-1.5 py-0.5 text-[10px] font-medium text-violet-600 dark:text-violet-400">
|
||||
已推OMS
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="mt-1 text-lg font-semibold text-slate-900 dark:text-white line-clamp-1">{opp.name}</h3>
|
||||
</div>
|
||||
<div className={`flex flex-col items-center justify-center rounded-lg border p-2 ${getConfidenceColor(opp.confidence)}`}>
|
||||
<span className="text-xs font-semibold">{opp.confidence}%</span>
|
||||
<span className="text-[10px] opacity-80">把握度</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-y-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300 col-span-2">
|
||||
<Building className="h-4 w-4 text-slate-400 dark:text-slate-500 shrink-0" />
|
||||
<span className="truncate">{opp.client}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<DollarSign className="h-4 w-4 text-slate-400 dark:text-slate-500 shrink-0" />
|
||||
<span className="font-medium text-slate-900 dark:text-white">¥{opp.amount}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<Calendar className="h-4 w-4 text-slate-400 dark:text-slate-500 shrink-0" />
|
||||
{opp.date}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<Activity className="h-4 w-4 text-slate-400 dark:text-slate-500 shrink-0" />
|
||||
<span className="rounded-full bg-slate-100 dark:bg-slate-800 px-2 py-0.5 text-xs font-medium text-slate-700 dark:text-slate-300">
|
||||
{opp.stage}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400 dark:text-slate-500">类型:</span>
|
||||
{opp.type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 dark:text-slate-600 opacity-0 transition-all group-hover:opacity-100 group-hover:text-violet-500 dark:group-hover:text-violet-400 md:block hidden">
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="mt-4 flex justify-end border-t border-slate-50 dark:border-slate-800/50 pt-3 md:hidden">
|
||||
<button className="flex items-center text-sm font-medium text-violet-600 dark:text-violet-400">
|
||||
查看详情 <ChevronRight className="h-4 w-4 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detail Slide-over Panel */}
|
||||
<AnimatePresence>
|
||||
{selectedItem && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="fixed inset-0 bg-slate-900/20 dark:bg-slate-900/60 backdrop-blur-sm z-40"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||
className="fixed inset-y-0 right-0 w-full max-w-md bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-200 dark:border-slate-800 z-50 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-100 dark:border-slate-800 px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">商机详情</h2>
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="rounded-full p-2 text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
{/* Header Info */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">{selectedItem.id}</span>
|
||||
{selectedItem.pushedToOMS && (
|
||||
<span className="rounded bg-violet-50 dark:bg-violet-500/10 px-1.5 py-0.5 text-[10px] font-medium text-violet-600 dark:text-violet-400">
|
||||
已推OMS
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white leading-tight">{selectedItem.name}</h3>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<span className="rounded-full bg-slate-100 dark:bg-slate-800 px-2.5 py-1 text-xs font-medium text-slate-700 dark:text-slate-300">
|
||||
{selectedItem.stage}
|
||||
</span>
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(selectedItem.confidence)}`}>
|
||||
把握度 {selectedItem.confidence}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Info Grid */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-violet-500" />
|
||||
基本信息
|
||||
</h4>
|
||||
<div className="rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/20 p-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="col-span-2"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Building className="h-3 w-3"/> 客户名称</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.client}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><DollarSign className="h-3 w-3"/> 商机金额</p><p className="font-medium text-rose-600 dark:text-rose-400">¥{selectedItem.amount}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Calendar className="h-3 w-3"/> 预计结单</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.date}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><User className="h-3 w-3"/> 负责人</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.owner}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Tag className="h-3 w-3"/> 商机类型</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.type}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><Activity className="h-3 w-3"/> 产品类别</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.product}</p></div>
|
||||
<div className="col-span-2 sm:col-span-1"><p className="text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><LinkIcon className="h-3 w-3"/> 商机来源</p><p className="font-medium text-slate-900 dark:text-white">{selectedItem.source}</p></div>
|
||||
<div className="col-span-2"><p className="text-slate-500 dark:text-slate-400 mb-1">备注说明</p><p className="font-medium text-slate-900 dark:text-white leading-relaxed">{selectedItem.notes}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-violet-500" />
|
||||
跟进记录
|
||||
</h4>
|
||||
<button className="text-xs font-medium text-violet-600 dark:text-violet-400 hover:text-violet-700">添加记录</button>
|
||||
</div>
|
||||
<div className="relative pl-4 border-l-2 border-slate-100 dark:border-slate-800 space-y-6">
|
||||
{followUpRecords.map((record) => (
|
||||
<div key={record.id} className="relative">
|
||||
<div className="absolute -left-[21px] mt-1.5 h-2.5 w-2.5 rounded-full bg-violet-500 ring-4 ring-white dark:ring-slate-900" />
|
||||
<div className="rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/20 p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-violet-600 dark:text-violet-400 bg-violet-50 dark:bg-violet-500/10 px-2 py-0.5 rounded">{record.type}</span>
|
||||
<span className="text-xs text-slate-400">{record.date}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300 leading-relaxed">{record.content}</p>
|
||||
<p className="text-xs text-slate-400 mt-2">跟进人: {record.user}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-slate-100 dark:border-slate-800 p-4 bg-slate-50 dark:bg-slate-900/50">
|
||||
<div className="flex gap-3">
|
||||
<button className="flex-1 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
|
||||
编辑商机
|
||||
</button>
|
||||
<button className="flex-1 rounded-xl bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
||||
写跟进
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { User, Settings, LogOut, ChevronRight, Bell, Shield, HelpCircle, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "@/components/ThemeProvider";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export default function Profile() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">我的</h1>
|
||||
</header>
|
||||
|
||||
{/* User Info Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-violet-100 dark:bg-violet-500/20 text-2xl font-bold text-violet-600 dark:text-violet-400">
|
||||
张
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white">张三</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">高级销售 · 华东大区</p>
|
||||
</div>
|
||||
<button className="rounded-full bg-slate-50 dark:bg-slate-800 p-2 text-slate-400 dark:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
|
||||
<Settings className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-3 divide-x divide-slate-100 dark:divide-slate-800 border-t border-slate-50 dark:border-slate-800/50 pt-6">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-white">12</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">本月商机</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-white">8</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">本月拓展</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-white">95</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">平均分</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Menu List */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 shadow-sm backdrop-blur-sm overflow-hidden transition-all"
|
||||
>
|
||||
<ul className="divide-y divide-slate-50 dark:divide-slate-800/50">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="flex w-full items-center justify-between p-4 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors md:hidden"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-800">
|
||||
{theme === "dark" ? <Sun className="h-5 w-5 text-amber-500" /> : <Moon className="h-5 w-5 text-indigo-500" />}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{theme === "dark" ? "切换亮色模式" : "切换暗色模式"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-slate-300 dark:text-slate-600" />
|
||||
</button>
|
||||
</li>
|
||||
{[
|
||||
{ icon: User, label: "个人资料", color: "text-blue-500 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-500/10" },
|
||||
{ icon: Bell, label: "消息通知", color: "text-amber-500 dark:text-amber-400", bg: "bg-amber-50 dark:bg-amber-500/10" },
|
||||
{ icon: Shield, label: "账号安全", color: "text-emerald-500 dark:text-emerald-400", bg: "bg-emerald-50 dark:bg-emerald-500/10" },
|
||||
{ icon: HelpCircle, label: "帮助中心", color: "text-violet-500 dark:text-violet-400", bg: "bg-violet-50 dark:bg-violet-500/10" },
|
||||
].map((item, i) => (
|
||||
<li key={i}>
|
||||
<button className="flex w-full items-center justify-between p-4 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-xl ${item.bg}`}>
|
||||
<item.icon className={`h-5 w-5 ${item.color}`} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{item.label}</span>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-slate-300 dark:text-slate-600" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
{/* Logout */}
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-rose-100 dark:border-rose-900/50 bg-rose-50 dark:bg-rose-500/10 p-4 text-sm font-semibold text-rose-600 dark:text-rose-400 transition-all hover:bg-rose-100 dark:hover:bg-rose-500/20 active:scale-[0.98]"
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
退出登录
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
import { useState } from "react";
|
||||
import { MapPin, Camera, Mic, Send, CalendarDays, Clock, CheckCircle2, FileText, ListTodo, Search, Filter, History } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export default function Work() {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [reportText, setReportText] = useState("");
|
||||
|
||||
const handleRecord = () => {
|
||||
setIsRecording(!isRecording);
|
||||
if (!isRecording) {
|
||||
// Mock voice to text
|
||||
setTimeout(() => {
|
||||
setReportText((prev) => prev + (prev ? "\n" : "") + "今天拜访了A市第一人民医院信息科主任,沟通了云桌面扩容需求,对方表示下个月会启动招标流程。");
|
||||
setIsRecording(false);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const historyData = [
|
||||
{
|
||||
id: 1,
|
||||
date: "2024-03-15",
|
||||
type: "日报",
|
||||
content: "1. 拜访了A市第一人民医院信息科主任,沟通了云桌面扩容需求...\n2. 与B大学新校区机房建设项目的负责人进行了电话沟通...",
|
||||
status: "已点评",
|
||||
score: 95,
|
||||
comment: "跟进很紧密,继续保持。A市医院的项目需要重点关注招标时间点。"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: "2024-03-14",
|
||||
type: "外勤打卡",
|
||||
content: "浙江省杭州市西湖区某某街道某某大厦 (C集团总部)",
|
||||
status: "正常",
|
||||
score: null,
|
||||
comment: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: "2024-03-14",
|
||||
type: "日报",
|
||||
content: "1. 整理了C集团办公云桌面替换项目的POC测试报告...\n2. 参加了华东大区周例会...",
|
||||
status: "已阅",
|
||||
score: null,
|
||||
comment: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: "2024-03-13",
|
||||
type: "外勤打卡",
|
||||
content: "上海市浦东新区某某路某某科技园 (D公司)",
|
||||
status: "正常",
|
||||
score: null,
|
||||
comment: null
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">工作台</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
今天是 {format(new Date(), "yyyy年MM月dd日 EEEE")}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
|
||||
{/* Left Column: Today's Actionable Items */}
|
||||
<div className="lg:col-span-7 xl:col-span-8 space-y-6">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="h-6 w-1 bg-violet-600 rounded-full"></div>
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">今日工作</h2>
|
||||
</div>
|
||||
|
||||
{/* Check-in Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-slate-50 dark:border-slate-800/50 pb-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-white">外勤打卡</h3>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 px-2.5 py-1 rounded-full">
|
||||
定位已获取
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white mb-1">当前位置</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl border border-slate-100 dark:border-slate-800">
|
||||
浙江省杭州市西湖区某某街道某某大厦
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white mb-1">备注说明 (选填)</p>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="请输入打卡备注..."
|
||||
className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-3 text-sm text-slate-900 dark:text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 flex flex-col">
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white mb-1">现场照片 (必填)</p>
|
||||
<div className="group flex flex-1 min-h-[120px] w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 transition-all hover:border-violet-400 dark:hover:border-violet-500 hover:bg-violet-50 dark:hover:bg-violet-500/10">
|
||||
<Camera className="mb-2 h-6 w-6 text-slate-400 dark:text-slate-500 group-hover:text-violet-500 transition-colors" />
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors">点击拍照</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-slate-900 dark:bg-white px-4 py-3 text-sm font-semibold text-white dark:text-slate-900 shadow-sm hover:bg-slate-800 dark:hover:bg-slate-100 active:scale-[0.98] transition-all">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
确认打卡
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
{/* Daily Report Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-slate-50 dark:border-slate-800/50 pb-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDays className="h-5 w-5 text-violet-600 dark:text-violet-400" />
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-white">每日表</h3>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 px-2.5 py-1 rounded-full">
|
||||
待提交
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
<FileText className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
今日工作内容
|
||||
</label>
|
||||
<button
|
||||
onClick={handleRecord}
|
||||
className={`flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-all duration-300 ${
|
||||
isRecording ? "bg-rose-100 dark:bg-rose-500/20 text-rose-600 dark:text-rose-400 animate-pulse" : "bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-400 hover:bg-violet-100 dark:hover:bg-violet-500/20"
|
||||
}`}
|
||||
>
|
||||
<Mic className="h-3.5 w-3.5" />
|
||||
{isRecording ? "正在识别..." : "语音输入 (HubMind)"}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={reportText}
|
||||
onChange={(e) => setReportText(e.target.value)}
|
||||
placeholder="请输入今日拜访客户、沟通进展、遇到的问题等..."
|
||||
className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-3 text-sm text-slate-900 dark:text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
<ListTodo className="h-4 w-4 text-slate-400 dark:text-slate-500" />
|
||||
明日工作计划
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="1. 上午拜访...\n2. 下午整理...\n3. 推进..."
|
||||
className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-3 text-sm text-slate-900 dark:text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button className="flex w-full items-center justify-center gap-2 rounded-xl bg-violet-600 px-4 py-3 text-sm font-semibold text-white shadow-sm hover:bg-violet-700 active:scale-[0.98] transition-all">
|
||||
<Send className="h-4 w-4" />
|
||||
提交日报
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: History */}
|
||||
<div className="lg:col-span-5 xl:col-span-4 space-y-6 lg:sticky lg:top-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-1 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">历史记录</h2>
|
||||
</div>
|
||||
<button className="p-2 text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 transition-colors">
|
||||
<Filter className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-h-[calc(100vh-12rem)] overflow-y-auto pr-2 scrollbar-hide">
|
||||
{historyData.map((item, i) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
key={item.id}
|
||||
className="group cursor-pointer rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-4 shadow-sm backdrop-blur-sm transition-all hover:shadow-md hover:border-violet-100 dark:hover:border-violet-900/50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-full ${item.type === '日报' ? 'bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400' : 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'}`}>
|
||||
{item.type === '日报' ? <FileText className="h-4 w-4" /> : <MapPin className="h-4 w-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{item.type}</h3>
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400">{item.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
||||
item.status === '已点评' ? 'bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-400' :
|
||||
item.status === '已阅' ? 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' :
|
||||
'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
{item.score && (
|
||||
<span className="text-[10px] font-bold text-rose-600 dark:text-rose-400">{item.score}分</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pl-11">
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300 line-clamp-2 leading-relaxed">
|
||||
{item.content}
|
||||
</p>
|
||||
|
||||
{item.comment && (
|
||||
<div className="mt-2 rounded-lg bg-slate-50 dark:bg-slate-800/50 p-2.5 border border-slate-100 dark:border-slate-800/50">
|
||||
<p className="text-[10px] font-medium text-slate-900 dark:text-white mb-0.5">主管点评:</p>
|
||||
<p className="text-[10px] text-slate-600 dark:text-slate-400">{item.comment}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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, './src'),
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue