AIWeddingFittingRoom / components /AdminDashboard.tsx
Lianjx's picture
Upload 71 files
459775e verified
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>
);
};