291 lines
17 KiB
TypeScript
291 lines
17 KiB
TypeScript
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|