306 lines
20 KiB
TypeScript
306 lines
20 KiB
TypeScript
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|