Spaces:
Paused
Paused
| import React, { useState, useEffect } from 'react'; | |
| import { X, Users, Settings, Download, Save, LogIn, Cloud, RefreshCw, ChevronDown, ChevronUp, Link as LinkIcon, Image as ImageIcon, MessageSquare } from 'lucide-react'; | |
| import { Language, LeadData, AdminConfig, UserAccount, FeedbackItem } from '../types'; | |
| import { TRANSLATIONS } from '../constants/translations'; | |
| interface AdminDashboardProps { | |
| isVisible: boolean; | |
| onClose: () => void; | |
| language: Language; | |
| leads: LeadData[]; | |
| feedback?: FeedbackItem[]; | |
| config: AdminConfig; | |
| allUsers: UserAccount[]; | |
| currentUser?: UserAccount; | |
| onUpdateConfig: (newConfig: AdminConfig) => void; | |
| onUpdateLeadStatus: (id: string, status: LeadData['status']) => void; | |
| onDeleteLead: (id: string) => void; | |
| onUpdateUser: (id: string, updates: Partial<UserAccount>) => void; | |
| showToast: (msg: string) => void; | |
| onAdminLoginSuccess: () => void; | |
| } | |
| const CollapsibleSection = ({ title, children, defaultOpen = false }: { title: string, children?: React.ReactNode, defaultOpen?: boolean }) => { | |
| const [isOpen, setIsOpen] = useState(defaultOpen); | |
| return ( | |
| <div className="border border-gray-200 rounded-xl overflow-hidden mb-4 shadow-sm"> | |
| <button | |
| onClick={() => setIsOpen(!isOpen)} | |
| className="w-full flex justify-between items-center p-4 bg-gray-50 hover:bg-gray-100 transition-colors font-bold text-gray-800" | |
| > | |
| {title} | |
| {isOpen ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />} | |
| </button> | |
| {isOpen && <div className="p-4 bg-white animate-fade-in">{children}</div>} | |
| </div> | |
| ); | |
| }; | |
| export const AdminDashboard: React.FC<AdminDashboardProps> = ({ | |
| isVisible, onClose, language, leads, feedback = [], config, onUpdateConfig, onUpdateLeadStatus, onDeleteLead, showToast, onAdminLoginSuccess | |
| }) => { | |
| const [activeTab, setActiveTab] = useState<'leads' | 'settings' | 'feedback'>('leads'); | |
| const [tempConfig, setTempConfig] = useState<AdminConfig>(config); | |
| const [isLoggedIn, setIsLoggedIn] = useState(false); | |
| const [password, setPassword] = useState(''); | |
| const t = TRANSLATIONS[language]; | |
| // Sync temp config when prop changes | |
| useEffect(() => { | |
| setTempConfig(config); | |
| }, [config]); | |
| if (!isVisible) return null; | |
| const handleLogin = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (password === 'admin888') { | |
| setIsLoggedIn(true); | |
| onAdminLoginSuccess(); | |
| } else { alert("密码错误"); } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/80 backdrop-blur-sm"> | |
| <div className="bg-white rounded-2xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden relative"> | |
| {!isLoggedIn && ( | |
| <div className="absolute inset-0 z-50 bg-gray-900 flex flex-col items-center justify-center text-white"> | |
| <h2 className="text-2xl font-bold font-serif mb-6">{t.adminLoginTitle || '员工登录'}</h2> | |
| <form onSubmit={handleLogin} className="flex flex-col gap-4 w-64"> | |
| <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="请输入管理密码" className="p-3 rounded bg-gray-800 border border-gray-700 text-center outline-none focus:border-rose-500 placeholder:text-gray-500" /> | |
| <button type="submit" className="p-3 bg-rose-600 rounded font-bold hover:bg-rose-700 transition-colors">登录后台</button> | |
| </form> | |
| </div> | |
| )} | |
| <div className="bg-gray-900 text-white p-4 flex justify-between items-center shadow-md z-10"> | |
| <div className="flex items-center gap-2"> | |
| <Settings className="w-5 h-5 text-rose-500" /> | |
| <h2 className="text-lg font-bold">{t.adminTitle}</h2> | |
| </div> | |
| <button onClick={onClose} className="p-2 hover:bg-gray-800 rounded-full"><X className="w-5 h-5" /></button> | |
| </div> | |
| <div className="flex flex-1 overflow-hidden"> | |
| <div className="w-48 bg-gray-50 border-r border-gray-200 p-4 space-y-2 shrink-0"> | |
| <button onClick={() => setActiveTab('leads')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'leads' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>客资管理 (Leads)</button> | |
| <button onClick={() => setActiveTab('feedback')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'feedback' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>用户反馈 (Feedback)</button> | |
| <button onClick={() => setActiveTab('settings')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'settings' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>系统设置 (Settings)</button> | |
| </div> | |
| <div className="flex-1 p-6 overflow-y-auto custom-scrollbar bg-gray-50/50"> | |
| {activeTab === 'leads' && ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> | |
| <div className="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50"> | |
| <h3 className="font-bold text-gray-800">客资列表 ({leads.length})</h3> | |
| <button className="text-xs flex items-center gap-1 text-gray-500 hover:text-gray-800"> | |
| <Download className="w-3 h-3" /> 导出表格 | |
| </button> | |
| </div> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="bg-gray-100 text-left text-gray-600"> | |
| <th className="p-3 font-semibold">姓名</th> | |
| <th className="p-3 font-semibold">电话</th> | |
| <th className="p-3 font-semibold">意向服务</th> | |
| <th className="p-3 font-semibold">CRM同步</th> | |
| <th className="p-3 font-semibold">状态</th> | |
| <th className="p-3 font-semibold">操作</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-gray-100"> | |
| {leads.map(lead => ( | |
| <tr key={lead.id} className="hover:bg-gray-50 transition-colors"> | |
| <td className="p-3 font-medium">{lead.name}</td> | |
| <td className="p-3 text-gray-600">{lead.phone}</td> | |
| <td className="p-3 text-gray-500">{lead.service || '-'}</td> | |
| <td className="p-3"> | |
| {lead.syncStatus === 'synced' ? ( | |
| <span className="flex items-center gap-1 text-green-600 text-xs font-bold"><Cloud className="w-3 h-3"/> 已同步</span> | |
| ) : ( | |
| <span className="text-gray-400 text-xs">-</span> | |
| )} | |
| </td> | |
| <td className="p-3"> | |
| <select | |
| value={lead.status} | |
| onChange={e => onUpdateLeadStatus(lead.id, e.target.value as any)} | |
| className={`border rounded-lg p-1.5 text-xs font-bold outline-none cursor-pointer ${ | |
| lead.status === 'new' ? 'bg-blue-50 text-blue-700 border-blue-200' : | |
| lead.status === 'contacted' ? 'bg-amber-50 text-amber-700 border-amber-200' : | |
| 'bg-green-50 text-green-700 border-green-200' | |
| }`} | |
| > | |
| <option value="new">新客</option> | |
| <option value="contacted">已联系</option> | |
| <option value="booked">已成交</option> | |
| </select> | |
| </td> | |
| <td className="p-3"> | |
| <button onClick={() => onDeleteLead(lead.id)} className="text-red-400 hover:text-red-600 p-1" title="删除"><X className="w-4 h-4" /></button> | |
| </td> | |
| </tr> | |
| ))} | |
| {leads.length === 0 && ( | |
| <tr> | |
| <td colSpan={6} className="p-8 text-center text-gray-400">暂无客资数据</td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'feedback' && ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> | |
| <div className="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50"> | |
| <h3 className="font-bold text-gray-800">用户反馈 ({feedback.length})</h3> | |
| </div> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="bg-gray-100 text-left text-gray-600"> | |
| <th className="p-3 font-semibold">类型</th> | |
| <th className="p-3 font-semibold">评分</th> | |
| <th className="p-3 font-semibold">内容</th> | |
| <th className="p-3 font-semibold">联系方式</th> | |
| <th className="p-3 font-semibold">时间</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-gray-100"> | |
| {feedback.map(item => ( | |
| <tr key={item.id} className="hover:bg-gray-50 transition-colors"> | |
| <td className="p-3"> | |
| <span className={`px-2 py-0.5 rounded text-xs font-bold uppercase ${ | |
| item.type === 'bug' ? 'bg-red-100 text-red-600' : | |
| item.type === 'suggestion' ? 'bg-amber-100 text-amber-600' : 'bg-blue-100 text-blue-600' | |
| }`}> | |
| {item.type} | |
| </span> | |
| </td> | |
| <td className="p-3 font-bold text-yellow-500">{item.rating} ★</td> | |
| <td className="p-3 text-gray-700 max-w-xs truncate" title={item.content}>{item.content}</td> | |
| <td className="p-3 text-gray-500 text-xs">{item.contact || '-'}</td> | |
| <td className="p-3 text-gray-400 text-xs">{new Date(item.timestamp).toLocaleString()}</td> | |
| </tr> | |
| ))} | |
| {feedback.length === 0 && ( | |
| <tr> | |
| <td colSpan={5} className="p-8 text-center text-gray-400">暂无反馈数据</td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'settings' && ( | |
| <div className="max-w-3xl mx-auto space-y-2"> | |
| <div className="flex justify-between items-center mb-6"> | |
| <h3 className="text-xl font-bold text-gray-800">小程序/网页配置</h3> | |
| <button | |
| onClick={() => { onUpdateConfig(tempConfig); showToast("配置已保存!"); }} | |
| className="px-6 py-2.5 bg-rose-600 hover:bg-rose-700 text-white rounded-xl font-bold shadow-lg shadow-rose-200 flex items-center gap-2 transition-all active:scale-95" | |
| > | |
| <Save className="w-4 h-4" /> 保存设置 | |
| </button> | |
| </div> | |
| <CollapsibleSection title="📍 基础信息 & 品牌设置" defaultOpen> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="col-span-2"> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">顶部活动通知文案</label> | |
| <input type="text" value={tempConfig.promoText} onChange={e => setTempConfig({...tempConfig, promoText: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">联系电话</label> | |
| <input type="text" value={tempConfig.contactPhone} onChange={e => setTempConfig({...tempConfig, contactPhone: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">门店地址</label> | |
| <input type="text" value={tempConfig.footerAddress} onChange={e => setTempConfig({...tempConfig, footerAddress: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| <div className="col-span-2"> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">Logo URL (品牌标志图片)</label> | |
| <div className="flex gap-2 relative"> | |
| <ImageIcon className="absolute left-3 top-3 w-4 h-4 text-gray-400" /> | |
| <input type="text" value={tempConfig.logoUrl || ''} onChange={e => setTempConfig({...tempConfig, logoUrl: e.target.value})} placeholder="https://example.com/logo.png" className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| {tempConfig.logoUrl && <img src={tempConfig.logoUrl} alt="Logo" className="w-10 h-10 object-contain rounded border bg-white" />} | |
| </div> | |
| </div> | |
| <div className="col-span-2"> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">预约/客服二维码图片链接 (QR Code URL)</label> | |
| <div className="flex gap-2"> | |
| <input type="text" value={tempConfig.qrCodeUrl || ''} onChange={e => setTempConfig({...tempConfig, qrCodeUrl: e.target.value})} placeholder="https://example.com/my-qr.png" className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| {tempConfig.qrCodeUrl && <img src={tempConfig.qrCodeUrl} alt="Preview" className="w-10 h-10 object-cover rounded border" />} | |
| </div> | |
| <p className="text-[10px] text-gray-400 mt-1"> | |
| 提示: 如果您使用家庭宽带部署,请记得在 URL 中加上端口号 (例如 :8080) | |
| </p> | |
| </div> | |
| </div> | |
| </CollapsibleSection> | |
| <CollapsibleSection title="📖 关于我们内容设置"> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">品牌故事 (Brand Story)</label> | |
| <textarea rows={3} value={tempConfig.aboutStory || ''} onChange={e => setTempConfig({...tempConfig, aboutStory: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="请输入品牌故事..." /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">服务理念 (Philosophy)</label> | |
| <textarea rows={2} value={tempConfig.aboutPhilosophy || ''} onChange={e => setTempConfig({...tempConfig, aboutPhilosophy: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="请输入服务理念..." /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">基地环境 (Location)</label> | |
| <textarea rows={2} value={tempConfig.aboutLocation || ''} onChange={e => setTempConfig({...tempConfig, aboutLocation: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="描述拍摄基地环境..." /> | |
| </div> | |
| </div> | |
| </CollapsibleSection> | |
| <CollapsibleSection title="💎 会员与积分策略"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 分享海报 (积分)</label> | |
| <div className="relative"> | |
| <input type="number" value={tempConfig.pointsShare ?? 10} onChange={e => setTempConfig({...tempConfig, pointsShare: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 邀请好友 (积分)</label> | |
| <div className="relative"> | |
| <input type="number" value={tempConfig.pointsInvite ?? 50} onChange={e => setTempConfig({...tempConfig, pointsInvite: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 预约留资 (积分)</label> | |
| <div className="relative"> | |
| <input type="number" value={tempConfig.pointsBook ?? 100} onChange={e => setTempConfig({...tempConfig, pointsBook: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-rose-500 mb-1">消耗: 兑换VIP (积分)</label> | |
| <div className="relative"> | |
| <input type="number" value={tempConfig.pointsVipCost ?? 100} onChange={e => setTempConfig({...tempConfig, pointsVipCost: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-rose-200 bg-rose-50 text-rose-700 font-bold rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| <span className="absolute left-3 top-2.5 text-rose-400 font-bold">Pt</span> | |
| </div> | |
| </div> | |
| </div> | |
| </CollapsibleSection> | |
| <CollapsibleSection title="🚀 裂变营销与红包"> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">分享标题 (Share Title)</label> | |
| <input type="text" value={tempConfig.shareTitle || ''} onChange={e => setTempConfig({...tempConfig, shareTitle: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">分享描述 (Share Description)</label> | |
| <input type="text" value={tempConfig.shareDesc || ''} onChange={e => setTempConfig({...tempConfig, shareDesc: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| <div className="flex gap-4"> | |
| <div className="flex-1"> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">红包最大金额 (¥)</label> | |
| <input type="number" value={tempConfig.redPacketMax || 2000} onChange={e => setTempConfig({...tempConfig, redPacketMax: parseInt(e.target.value)})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" /> | |
| </div> | |
| </div> | |
| </div> | |
| </CollapsibleSection> | |
| <CollapsibleSection title="🔌 系统集成 (CRM/AI)"> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">CRM 接口地址 (API Endpoint)</label> | |
| <div className="relative"> | |
| <Cloud className="absolute left-3 top-3 w-4 h-4 text-gray-400" /> | |
| <input type="text" value={tempConfig.crmApiUrl || ''} onChange={e => setTempConfig({...tempConfig, crmApiUrl: e.target.value})} className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none font-mono text-xs" placeholder="https://api.crm.com/leads" /> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">CRM API 密钥</label> | |
| <input type="password" value={tempConfig.crmApiKey || ''} onChange={e => setTempConfig({...tempConfig, crmApiKey: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none font-mono" /> | |
| </div> | |
| <div className="p-4 bg-blue-50 rounded-lg border border-blue-100"> | |
| <h4 className="font-bold text-blue-700 mb-2 text-xs uppercase tracking-wider flex items-center gap-2"> | |
| <Cloud className="w-4 h-4" /> Gemini AI Configuration | |
| </h4> | |
| <div className="space-y-3"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">API Key</label> | |
| <input type="password" value={tempConfig.geminiApiKey || ''} onChange={e => setTempConfig({...tempConfig, geminiApiKey: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-blue-500 outline-none font-mono" placeholder="AIzSy..." /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-500 mb-1">API Base URL (Proxy/Mirror) - Optional</label> | |
| <div className="relative"> | |
| <LinkIcon className="absolute left-3 top-3 w-4 h-4 text-gray-400" /> | |
| <input type="text" value={tempConfig.geminiApiUrl || ''} onChange={e => setTempConfig({...tempConfig, geminiApiUrl: e.target.value})} className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-blue-500 outline-none font-mono" placeholder="https://my-proxy.com (Leave empty for default)" /> | |
| </div> | |
| <p className="text-[10px] text-gray-400 mt-1"> | |
| 如果您在中国大陆地区,请配置反向代理地址以解决网络问题。 | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </CollapsibleSection> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; |