"use client"; import { useState, useEffect } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { formatBeijingTime } from "@/lib/utils"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Separator } from "@/components/ui/separator"; import { Trash2, Save, User, Settings, Wrench, Bot, HardDrive, Plus, Edit, Eye, EyeOff, Menu, Key, MessageSquare, Bell, Image as ImageIcon, BarChart3, Search, Calendar, TrendingUp, Users, Clock, Sparkles, RefreshCw, Scissors, Expand, } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); // 类型定义 type User = { id: string; email: string; name: string | null; department: string | null; role: "guest" | "regular" | "admin"; isActive: boolean; monthlyImageLimit: number | null; createdAt: string; }; const ROLE_LABELS: Record = { guest: "访客用户", regular: "普通用户", admin: "管理员", }; type ToolConfig = { toolName: string; isEnabled: boolean; description?: string; config?: any; }; type AiProvider = { id: string; name: string; type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video"; baseUrl: string; apiKey: string; isEnabled: boolean; createdAt: string; }; type AiModel = { id: string; providerId: string; modelId: string; displayName: string; description?: string; supportsVision: boolean; supportsImageGen: boolean; isEnabled: boolean; isDefault: boolean; sortOrder: number; }; type StorageConfig = { id: string; name: string; type: "s3" | "local"; endpoint?: string; region?: string; bucket?: string; accessKeyId?: string; secretAccessKey?: string; publicUrl?: string; isDefault: boolean; isEnabled: boolean; }; type SiteSettings = { siteName: string; siteLogo: string | null; siteLogoDark: string | null; favicon: string | null; showAutoModel: boolean; usageTimeEnabled: boolean; usageTimeStart: string; usageTimeEnd: string; usageTimeMessage: string; }; type NavLink = { id: string; name: string; url: string; order: number; isEnabled: boolean; }; type DingTalkOrganization = { id: string; name: string; appKey: string; appSecret: string; }; type AuthSettings = { // 保留单一配置用于兼容旧数据(后端会自动同步为第一个组织) dingTalkAppKey?: string | null; dingTalkAppSecret?: string | null; isDingTalkEnabled: boolean; isRegistrationEnabled?: boolean; dingTalkOrganizations: DingTalkOrganization[]; }; type GeneratedImageRecord = { id: string; userId: string; userName: string | null; userEmail: string; userDepartment: string | null; thumbnailUrl: string; originalUrl: string; imageSize: string | null; prompt: string | null; modelId: string | null; isFeatured: boolean; featuredAt: string | null; createdAt: string; chatId: string | null; }; type ImageAnalytics = { overview: { totalImages: number; totalUsers: number; todayImages: number; todayUsers: number; weekImages: number; weekUsers: number; monthImages: number; monthUsers: number; }; userStats: Array<{ userId: string; userName: string | null; userEmail: string; imageCount: number; }>; dailyStats: Array<{ date: string; count: number }>; sizeStats: Array<{ imageSize: string | null; count: number }>; modelStats: Array<{ modelId: string | null; count: number }>; modelDetailStats: Array<{ modelId: string | null; count: number; userCount: number; modelName: string | null; providerName: string | null; providerType: string | null; }>; providerTypeStats: Array<{ providerType: string | null; providerName: string | null; count: number; }>; modelTrendStats: Array<{ date: string; modelId: string | null; count: number; }>; monthlyStats: Array<{ month: string; count: number }>; hourlyStats: Array<{ hour: number; count: number }>; }; const AVAILABLE_TOOLS = [ { name: "getWeather", description: "获取天气信息" }, { name: "createDocument", description: "创建文档" }, { name: "updateDocument", description: "更新文档" }, { name: "requestSuggestions", description: "请求文档建议" }, ]; const PROVIDER_TYPES = [ { value: "alibaba", label: "阿里云通义千问" }, { value: "openai", label: "OpenAI 兼容" }, { value: "custom", label: "自定义 OpenAI 兼容" }, { value: "local", label: "本地模型" }, { value: "gemini", label: "Gemini中转" }, { value: "qianwen-video", label: "千问视频生成" }, { value: "volcano-vision", label: "火山引擎视觉" }, { value: "volcano-video", label: "火山引擎视频" }, ]; export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); // 用户管理状态 const [userSearch, setUserSearch] = useState(""); const [userPage, setUserPage] = useState(1); const [userLimit] = useState(10); // 批量设置出图限额状态 const [showBatchLimitDialog, setShowBatchLimitDialog] = useState(false); const [batchLimitType, setBatchLimitType] = useState<"all" | "department" | "no_department">("all"); const [batchLimitValue, setBatchLimitValue] = useState(""); const [batchLimitDepartment, setBatchLimitDepartment] = useState(""); // 🆕 生图记录状态 const [imageSearch, setImageSearch] = useState(""); const [imagePage, setImagePage] = useState(1); const [imageStartDate, setImageStartDate] = useState(""); const [imageEndDate, setImageEndDate] = useState(""); const [selectedImage, setSelectedImage] = useState(null); // 🆕 数据分析状态 const [selectedMonth, setSelectedMonth] = useState(() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const [selectedDate, setSelectedDate] = useState(null); // 选中的日期,用于联动高峰时段 const [hoveredBar, setHoveredBar] = useState<{ type: string; index: number; value: number; label: string } | null>(null); // API 数据获取 const { data: usersData, mutate: mutateUsers } = useSWR<{ users: User[]; total: number; page: number; totalPages: number; }>( `/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`, fetcher ); // 获取部门列表(用于批量设置) const { data: departmentsData } = useSWR<{ departments: string[] }>( "/api/admin/users/batch-limit", fetcher ); const { data: providersData, mutate: mutateProviders } = useSWR<{ providers: AiProvider[] }>("/api/admin/ai-providers", fetcher); const { data: modelsData, mutate: mutateModels } = useSWR<{ models: AiModel[] }>("/api/admin/ai-models", fetcher); const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[] }>("/api/admin/storage", fetcher); const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR("/api/site-settings", fetcher); const { data: navLinksData, mutate: mutateNavLinks } = useSWR<{ links: NavLink[] }>("/api/nav-links", fetcher); const { data: authSettingsData, mutate: mutateAuthSettings } = useSWR( "/api/auth-settings", fetcher, ); // 🆕 生图记录数据 const imageQueryParams = new URLSearchParams({ page: imagePage.toString(), pageSize: "200", ...(imageSearch && { search: imageSearch }), ...(imageStartDate && { startDate: imageStartDate }), ...(imageEndDate && { endDate: imageEndDate }), }); const { data: imagesData, mutate: mutateImages } = useSWR<{ images: GeneratedImageRecord[]; pagination: { page: number; pageSize: number; total: number; totalPages: number }; }>(`/api/admin/images?${imageQueryParams}`, fetcher); // 🆕 生图数据分析 const analyticsUrl = activeTab === "analytics" ? `/api/admin/images/analytics?month=${selectedMonth}${selectedDate ? `&date=${selectedDate}` : ""}` : null; const { data: analyticsData } = useSWR(analyticsUrl, fetcher); // 网站设置状态 const [siteSettings, setSiteSettings] = useState({ siteName: "AI Chatbot", siteLogo: "", siteLogoDark: "", favicon: "", showAutoModel: true, usageTimeEnabled: false, usageTimeStart: "08:00", usageTimeEnd: "20:00", usageTimeMessage: "由壹点累了也是需要休息的", }); useEffect(() => { if (siteSettingsData) { setSiteSettings({ siteName: siteSettingsData.siteName || "AI Chatbot", siteLogo: siteSettingsData.siteLogo || "", siteLogoDark: siteSettingsData.siteLogoDark || "", favicon: siteSettingsData.favicon || "", showAutoModel: siteSettingsData.showAutoModel ?? true, usageTimeEnabled: siteSettingsData.usageTimeEnabled ?? false, usageTimeStart: siteSettingsData.usageTimeStart || "08:00", usageTimeEnd: siteSettingsData.usageTimeEnd || "20:00", usageTimeMessage: siteSettingsData.usageTimeMessage || "由壹点累了也是需要休息的", }); } }, [siteSettingsData]); // 抠图API配置状态 const [rmbgConfig, setRmbgConfig] = useState({ apiUrl: "", apiKey: "", }); // 变清晰API配置状态 const [enhanceConfig, setEnhanceConfig] = useState({ apiUrl: "", apiKey: "", type: "clean", // "clean" | "face" scaleFactor: "auto", // "auto" | "1" | "2" | "4" // 备用API配置 (Real-ESRGAN) useBackupApi: false, // 是否启用备用API backupApiUrl: "http://156.225.27.52:5233", // 备用API地址 backupApiKey: "", // 备用API密钥 backupScale: "4", // 备用API放大倍数 "2" | "4" | "8" backupModel: "RealESRGAN_x4plus", // 备用API模型 }); // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ apiKey: "", xScale: "1.5", // X轴放大倍数 yScale: "1.5", // Y轴放大倍数 }); // 加载抠图API配置 const { data: rmbgConfigData } = useSWR("/api/admin/settings?key=rmbg_api_config", fetcher); useEffect(() => { if (rmbgConfigData?.setting?.value) { const config = rmbgConfigData.setting.value as { apiUrl?: string; apiKey?: string }; setRmbgConfig({ apiUrl: config.apiUrl || "", apiKey: config.apiKey || "", }); } }, [rmbgConfigData]); // 加载变清晰API配置 const { data: enhanceConfigData } = useSWR("/api/admin/settings?key=enhance_api_config", fetcher); useEffect(() => { if (enhanceConfigData?.setting?.value) { const config = enhanceConfigData.setting.value as { apiUrl?: string; apiKey?: string; type?: string; scaleFactor?: number; useBackupApi?: boolean; backupApiUrl?: string; backupApiKey?: string; backupScale?: string; backupModel?: string; }; setEnhanceConfig({ apiUrl: config.apiUrl || "", apiKey: config.apiKey || "", type: config.type || "clean", scaleFactor: config.scaleFactor ? String(config.scaleFactor) : "auto", useBackupApi: config.useBackupApi || false, backupApiUrl: config.backupApiUrl || "http://156.225.27.52:5233", backupApiKey: config.backupApiKey || "", backupScale: config.backupScale || "4", backupModel: config.backupModel || "RealESRGAN_x4plus", }); } }, [enhanceConfigData]); // 加载图像拓展API配置 const { data: extendConfigData } = useSWR("/api/admin/settings?key=extend_api_config", fetcher); useEffect(() => { if (extendConfigData?.setting?.value) { const config = extendConfigData.setting.value as { apiKey?: string; xScale?: number; yScale?: number; }; setExtendConfig({ apiKey: config.apiKey || "", xScale: config.xScale ? String(config.xScale) : "1.5", yScale: config.yScale ? String(config.yScale) : "1.5", }); } }, [extendConfigData]); // 导航链接状态 const [navLinkForm, setNavLinkForm] = useState({ id: "", name: "", url: "", order: 0, isEnabled: true, }); const [showNavLinkDialog, setShowNavLinkDialog] = useState(false); const [editingNavLink, setEditingNavLink] = useState(null); // 快捷短语状态 const [questions, setQuestions] = useState([]); const { data: questionsData, mutate: mutateQuestions } = useSWR("/api/starter-questions", fetcher); // 系统通知状态 const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string }>({ enabled: false, title: "", content: "", }); useEffect(() => { fetch("/api/admin/settings?key=login_notification") .then((res) => res.json()) .then((data) => { if (data.setting?.value) { setNotification(data.setting.value); } }); }, []); const handleSaveNotification = async () => { try { await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "login_notification", value: notification, }), }); toast.success("系统通知已保存"); } catch (error) { toast.error("保存失败"); } }; useEffect(() => { if (questionsData) { setQuestions(questionsData); } }, [questionsData]); const handleQuestionChange = (index: number, value: string) => { const newQuestions = [...questions]; newQuestions[index] = value; setQuestions(newQuestions); }; const handleAddQuestion = () => { setQuestions([...questions, ""]); }; const handleRemoveQuestion = (index: number) => { const newQuestions = questions.filter((_, i) => i !== index); setQuestions(newQuestions); }; const handleSaveQuestions = async () => { try { const res = await fetch("/api/starter-questions", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(questions.filter(q => q.trim() !== "")), }); if (!res.ok) throw new Error("Failed"); toast.success("快捷短语已更新"); mutateQuestions(); } catch (error) { toast.error("更新失败"); } }; // 认证设置状态 const [authSettings, setAuthSettings] = useState({ dingTalkAppKey: "", dingTalkAppSecret: "", isDingTalkEnabled: false, isRegistrationEnabled: true, dingTalkOrganizations: [], }); useEffect(() => { if (!authSettingsData) return; const organizations: DingTalkOrganization[] = (authSettingsData.dingTalkOrganizations as DingTalkOrganization[] | null) ?? []; // 如果没有多组织配置,但有单一 AppKey/Secret,则自动构造一个默认组织 const normalizedOrgs = organizations.length > 0 || !authSettingsData.dingTalkAppKey ? organizations : [ { id: "default", name: "默认组织", appKey: authSettingsData.dingTalkAppKey ?? "", appSecret: authSettingsData.dingTalkAppSecret ?? "", }, ]; setAuthSettings({ dingTalkAppKey: authSettingsData.dingTalkAppKey || "", dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "", isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, dingTalkOrganizations: normalizedOrgs, }); }, [authSettingsData]); // AI 提供商 & 模型状态 const [providerForm, setProviderForm] = useState({ id: "", name: "", type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video", baseUrl: "", apiKey: "", isEnabled: true, }); const [showProviderDialog, setShowProviderDialog] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [showApiKey, setShowApiKey] = useState(false); const [modelForm, setModelForm] = useState({ id: "", providerId: "", modelId: "", displayName: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isDefault: false, sortOrder: 0, fallbackModelId1: "", // 🆕 备用模型 ID 1 fallbackModelId2: "", // 🆕 备用模型 ID 2 }); const [showModelDialog, setShowModelDialog] = useState(false); const [editingModel, setEditingModel] = useState(null); // 🆕 火山引擎视频多模型表单(动态从API加载) const [volcanoVideoModels, setVolcanoVideoModels] = useState>([]); // 🆕 当选择火山引擎视频Provider时,从API加载预设模型配置 useEffect(() => { const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); if (selectedProvider?.type === 'volcano-video' && volcanoVideoModels.length === 0) { // 从API获取预设模型配置(✅ 显示所有模型供管理员自定义) fetch('/api/admin/volcano-video-models') .then(res => res.json()) .then(data => { if (data.presets) { // ✅ 显示主模型和所有子模型,允许管理员自定义显示名称和描述 const allModels = data.presets.map((preset: any) => ({ modelId: preset.modelId, displayName: preset.displayName, description: preset.description, isSubModel: preset.isSubModel || false, // 标记是否为子模型 })); setVolcanoVideoModels(allModels); } }) .catch(err => { console.error('[Load Volcano Models] Error:', err); toast.error('加载预设模型配置失败'); }); } }, [modelForm.providerId, providersData?.providers, volcanoVideoModels.length]); // 存储配置状态 const [storageForm, setStorageForm] = useState({ id: "", name: "", type: "s3" as "s3" | "local", endpoint: "", region: "", bucket: "", accessKeyId: "", secretAccessKey: "", publicUrl: "", isDefault: false, isEnabled: true, }); const [showStorageDialog, setShowStorageDialog] = useState(false); const [editingStorage, setEditingStorage] = useState(null); // --- 用户管理逻辑 --- const handleUserSearch = (search: string) => { setUserSearch(search); setUserPage(1); // 搜索时重置到第一页 }; const handleUpdateUser = async (userId: string, updates: any) => { try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, ...updates }), }); if (!response.ok) throw new Error("更新失败"); toast.success("用户更新成功"); mutateUsers(); } catch (error) { toast.error("更新用户失败"); } }; const handleDeleteUser = async (userId: string) => { if (!confirm("确定要删除此用户吗?")) return; try { const response = await fetch(`/api/admin/users?id=${userId}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("用户删除成功"); mutateUsers(); } catch (error) { toast.error("删除用户失败"); } }; // 批量设置出图限额 const handleBatchSetLimit = async () => { try { const response = await fetch("/api/admin/users/batch-limit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: batchLimitType, limit: batchLimitValue === "" ? null : parseInt(batchLimitValue, 10), department: batchLimitDepartment, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "批量设置失败"); } toast.success(data.message); setShowBatchLimitDialog(false); setBatchLimitValue(""); setBatchLimitDepartment(""); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "批量设置失败"); } }; // --- 网站设置逻辑 --- const handleSaveSiteSettings = async () => { try { const response = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(siteSettings), }); if (!response.ok) throw new Error("保存失败"); toast.success("网站设置保存成功"); mutateSiteSettings(); } catch (error) { toast.error("保存网站设置失败"); } }; // --- 抠图API配置逻辑 --- const handleSaveRmbgConfig = async () => { try { if (!rmbgConfig.apiUrl.trim()) { toast.error("请填写API地址"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "rmbg_api_config", value: { apiUrl: rmbgConfig.apiUrl.trim(), apiKey: rmbgConfig.apiKey.trim() || undefined, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("抠图API配置保存成功"); } catch (error) { toast.error("保存抠图API配置失败"); } }; // --- 变清晰API配置逻辑 --- const handleSaveEnhanceConfig = async () => { try { // 如果启用了备用API,验证备用API配置 if (enhanceConfig.useBackupApi) { if (!enhanceConfig.backupApiUrl.trim()) { toast.error("请输入备用API地址"); return; } if (!enhanceConfig.backupApiKey.trim()) { toast.error("请输入备用API密钥"); return; } } else { // 如果使用主API,验证主API配置 if (!enhanceConfig.apiUrl.trim()) { toast.error("请输入API地址"); return; } if (!enhanceConfig.apiKey.trim()) { toast.error("请输入API密钥"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "enhance_api_config", value: { apiUrl: enhanceConfig.apiUrl.trim(), apiKey: enhanceConfig.apiKey.trim(), type: enhanceConfig.type, scaleFactor: enhanceConfig.scaleFactor && enhanceConfig.scaleFactor !== "auto" ? Number.parseInt(enhanceConfig.scaleFactor) : undefined, // 备用API配置 useBackupApi: enhanceConfig.useBackupApi, backupApiUrl: enhanceConfig.backupApiUrl.trim(), backupApiKey: enhanceConfig.backupApiKey.trim(), backupScale: enhanceConfig.backupScale, backupModel: enhanceConfig.backupModel, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("变清晰API配置保存成功"); } catch (error) { toast.error("保存变清晰API配置失败"); } }; // --- 图像拓展API配置逻辑 --- const handleSaveExtendConfig = async () => { try { if (!extendConfig.apiKey.trim()) { toast.error("请输入API密钥"); return; } const xScale = Number.parseFloat(extendConfig.xScale); const yScale = Number.parseFloat(extendConfig.yScale); if (isNaN(xScale) || xScale < 1.0 || xScale > 2.0) { toast.error("X轴放大倍数必须在1.0-2.0之间"); return; } if (isNaN(yScale) || yScale < 1.0 || yScale > 2.0) { toast.error("Y轴放大倍数必须在1.0-2.0之间"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "extend_api_config", value: { apiKey: extendConfig.apiKey.trim(), xScale: xScale, yScale: yScale, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("图像拓展API配置保存成功"); } catch (error) { toast.error("保存图像拓展API配置失败"); } }; // --- 导航链接逻辑 --- const resetNavLinkForm = () => { setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true }); setEditingNavLink(null); }; const handleSaveNavLink = async () => { try { const method = editingNavLink ? "PUT" : "POST"; const body = editingNavLink ? { ...navLinkForm, id: editingNavLink } : navLinkForm; const response = await fetch("/api/nav-links", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingNavLink ? "链接更新成功" : "链接创建成功"); mutateNavLinks(); setShowNavLinkDialog(false); resetNavLinkForm(); } catch (error) { toast.error("保存导航链接失败"); } }; const handleDeleteNavLink = async (id: string) => { if (!confirm("确定要删除此链接吗?")) return; try { const response = await fetch(`/api/nav-links?id=${id}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("链接删除成功"); mutateNavLinks(); } catch (error) { toast.error("删除链接失败"); } }; // --- 认证设置逻辑 --- const handleSaveAuthSettings = async () => { try { const response = await fetch("/api/auth-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authSettings), }); if (!response.ok) throw new Error("保存失败"); toast.success("认证设置保存成功"); mutateAuthSettings(); } catch (error) { toast.error("保存认证设置失败"); } }; // --- AI 提供商逻辑 --- const resetProviderForm = () => { setProviderForm({ id: "", name: "", type: "openai", baseUrl: "", apiKey: "", isEnabled: true, }); setEditingProvider(null); }; const handleSaveProvider = async () => { try { const method = editingProvider ? "PUT" : "POST"; const isNewProvider = !editingProvider; const isVolcanoVideo = providerForm.type === 'volcano-video'; let requestData: any = editingProvider ? { ...providerForm, id: editingProvider } : providerForm; if (editingProvider && !providerForm.apiKey) { const { apiKey, ...restData } = requestData; requestData = restData; } const response = await fetch("/api/admin/ai-providers", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) throw new Error("保存失败"); const data = await response.json(); const providerId = data.provider?.id || editingProvider; toast.success(editingProvider ? "提供商更新成功" : "提供商创建成功"); mutateProviders(); setShowProviderDialog(false); resetProviderForm(); // 🆕 如果是新创建的火山引擎视频Provider,自动添加预设模型 if (isNewProvider && isVolcanoVideo && providerId) { toast.info("正在自动添加预设模型..."); try { const modelsResponse = await fetch("/api/admin/volcano-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const modelsData = await modelsResponse.json(); if (modelsResponse.ok && modelsData.addedCount > 0) { toast.success(`✅ 已自动添加 ${modelsData.addedCount} 个预设模型!`); mutateModels(); // 刷新模型列表 } } catch (modelError) { console.error('[Auto Add Models] Error:', modelError); toast.warning('预设模型添加失败,请手动点击"快速添加预设模型"按钮'); } } } catch (error) { toast.error("保存提供商失败"); } }; const handleDeleteProvider = async (id: string) => { if (!confirm("确定删除?关联模型也会被删除。")) return; try { const response = await fetch(`/api/admin/ai-providers?id=${id}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("提供商删除成功"); mutateProviders(); mutateModels(); } catch (error) { toast.error("删除提供商失败"); } }; // 快速添加火山引擎视频预设模型 const handleQuickAddVolcanoModels = async (providerId: string) => { if (!confirm("将为此服务商添加4个预设模型:\n- Seedance Pro (全功能)\n- Seedance Pro Fast (快速)\n- Seedance Lite I2V (图生视频)\n- Seedance Lite T2V (文生视频)\n\n继续吗?")) { return; } try { toast.info("正在添加预设模型..."); const response = await fetch("/api/admin/volcano-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个预设模型!`); mutateModels(); // 刷新模型列表 } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error('[Quick Add Models] Error:', error); toast.error(error.message || "添加预设模型失败"); } }; // --- AI 模型逻辑 --- const resetModelForm = () => { setModelForm({ id: "", providerId: "", modelId: "", displayName: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isDefault: false, sortOrder: 0, fallbackModelId1: "", fallbackModelId2: "", }); setEditingModel(null); }; const handleSaveModel = async () => { try { // 🆕 检查是否是火山引擎视频Provider const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); const isVolcanoVideo = selectedProvider?.type === 'volcano-video'; // 如果是火山引擎视频且不是编辑模式,批量创建模型 if (isVolcanoVideo && !editingModel) { toast.info("正在添加火山引擎视频模型(1个主模型 + 4个子模型)..."); const response = await fetch("/api/admin/volcano-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId: modelForm.providerId, // ✅ 传递所有模型配置(管理员可以自定义显示名称和描述) models: volcanoVideoModels, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "批量添加失败"); toast.success(`✅ 成功添加 ${data.addedCount} 个模型(1个主模型 + ${data.addedCount - 1} 个子模型)!`); mutateModels(); setShowModelDialog(false); resetModelForm(); return; } // 普通模型的单个创建/编辑逻辑 const method = editingModel ? "PUT" : "POST"; const body = editingModel ? { ...modelForm, id: editingModel } : modelForm; const response = await fetch("/api/admin/ai-models", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingModel ? "模型更新成功" : "模型创建成功"); mutateModels(); setShowModelDialog(false); resetModelForm(); } catch (error: any) { console.error('[Save Model] Error:', error); toast.error(error.message || "保存模型失败"); } }; const handleDeleteModel = async (id: string) => { if (!confirm("确定删除此模型?")) return; try { const response = await fetch(`/api/admin/ai-models?id=${id}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("模型删除成功"); mutateModels(); } catch (error) { toast.error("删除模型失败"); } }; // --- 存储配置逻辑 --- const resetStorageForm = () => { setStorageForm({ id: "", name: "", type: "s3", endpoint: "", region: "", bucket: "", accessKeyId: "", secretAccessKey: "", publicUrl: "", isDefault: false, isEnabled: true, }); setEditingStorage(null); }; const handleSaveStorage = async () => { try { const method = editingStorage ? "PUT" : "POST"; const body = editingStorage ? { ...storageForm, id: editingStorage } : storageForm; const response = await fetch("/api/admin/storage", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingStorage ? "存储配置更新成功" : "存储配置创建成功"); mutateStorage(); setShowStorageDialog(false); resetStorageForm(); } catch (error) { toast.error("保存存储配置失败"); } }; const handleDeleteStorage = async (id: string) => { if (!confirm("确定删除此存储配置?")) return; try { const response = await fetch(`/api/admin/storage?id=${id}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("存储配置删除成功"); mutateStorage(); } catch (error) { toast.error("删除存储配置失败"); } }; return (
用户管理 生图记录 数据分析 网站设置 导航设置 快捷短语 认证设置 系统通知 AI 模型 存储配置 本地AI设置 {/* 用户管理 */} {/* 搜索框和同步按钮 */}
handleUserSearch(e.target.value)} className="max-w-md" />
{/* 用户列表 */}
{usersData?.users?.map((user) => (
{user.name || user.email}
邮箱: {user.email}
部门: {user.department || "未设置"}
角色: {ROLE_LABELS[user.role] ?? user.role} | 状态: {user.isActive ? "已激活" : "未激活"}
月度出图限额: {user.monthlyImageLimit ? `${user.monthlyImageLimit} 张/月` : "无限制"}
{/* 配额设置 */}
{ const value = e.target.value ? parseInt(e.target.value, 10) : null; if (value !== user.monthlyImageLimit) { handleUpdateUser(user.id, { monthlyImageLimit: value }); } }} onKeyDown={(e) => { if (e.key === "Enter") { const value = (e.target as HTMLInputElement).value ? parseInt((e.target as HTMLInputElement).value, 10) : null; if (value !== user.monthlyImageLimit) { handleUpdateUser(user.id, { monthlyImageLimit: value }); } } }} /> 张
))}
{/* 分页控件 */} {usersData && usersData.totalPages > 1 && (
显示 {((userPage - 1) * userLimit) + 1} - {Math.min(userPage * userLimit, usersData.total)} 条,共 {usersData.total} 条
{/* 首页按钮 */} {/* 上一页按钮 */} {/* 页码显示和跳转 */}
第 { const page = Number.parseInt(e.target.value, 10); if (page >= 1 && page <= usersData.totalPages) { setUserPage(page); } }} onKeyDown={(e) => { if (e.key === 'Enter') { const page = Number.parseInt(e.currentTarget.value, 10); if (page >= 1 && page <= usersData.totalPages) { setUserPage(page); } else { e.currentTarget.value = userPage.toString(); } } }} className="w-16 h-8 text-center" /> / {usersData.totalPages} 页
{/* 下一页按钮 */} {/* 末页按钮 */}
)}
{/* 🆕 生图记录 */} 所有生图记录 查看所有用户的 AI 生图历史记录 {/* 筛选条件 */}
{ setImageSearch(e.target.value); setImagePage(1); }} className="pl-9" />
{ setImageStartDate(e.target.value); setImagePage(1); }} />
{ setImageEndDate(e.target.value); setImagePage(1); }} />
{/* 统计信息 */}
共 {imagesData?.pagination.total || 0} 条记录
{/* 图片网格 */}
{imagesData?.images?.map((img) => (
setSelectedImage(img)} >
{img.prompt {/* 精选图标 */} {img.isFeatured && (
)}
{img.userName || img.userEmail?.split("@")[0]}
{formatBeijingTime(img.createdAt, { month: "numeric", day: "numeric" })}
))}
{/* 分页 */} {imagesData && imagesData.pagination.totalPages > 1 && (
第 {imagesData.pagination.page} / {imagesData.pagination.totalPages} 页
)}
{/* 图片详情弹窗 */} !open && setSelectedImage(null)}> 图片详情 {selectedImage?.isFeatured && ( 精选 )} {selectedImage && (
{selectedImage.prompt

{selectedImage.userName || selectedImage.userEmail}

{selectedImage.userDepartment || "未设置"}

{new Date(selectedImage.createdAt).toLocaleString("zh-CN")}

{selectedImage.imageSize || "未知"}

{selectedImage.modelId || "未知"}

{selectedImage.isFeatured && selectedImage.featuredAt && (

{new Date(selectedImage.featuredAt).toLocaleString("zh-CN")}

)}

{selectedImage.prompt || "无"}

{/* 精选操作按钮 */}
)}
{/* 🆕 数据分析 */} {/* 概览卡片 */}

{analyticsData?.overview.totalImages || 0}

总生图数

{analyticsData?.overview.totalUsers || 0}

总用户数

{analyticsData?.overview.todayImages || 0}

今日生图

{analyticsData?.overview.monthImages || 0}

{selectedMonth === `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}` ? "本月生图" : `${selectedMonth} 生图`}

setSelectedMonth(e.target.value)} className="w-[140px] h-8 text-xs" />
{/* 用户排行 */} 用户生图排行 (Top 20)
{analyticsData?.userStats?.map((user, index) => (
{index + 1} {user.userName || user.userEmail?.split("@")[0]}
{user.imageCount}
))}
{/* 图片尺寸分布 */} 图片尺寸分布
{analyticsData?.sizeStats?.map((stat) => { const total = analyticsData.overview.totalImages || 1; const percentage = ((stat.count / total) * 100).toFixed(1); return (
{stat.imageSize || "未知"} {stat.count} ({percentage}%)
); })}
{/* 每日趋势 */} 最近30天趋势 {selectedDate && ( )}
{analyticsData?.dailyStats?.map((stat, index) => { const maxCount = Math.max(...(analyticsData.dailyStats?.map(s => s.count) || [1])); const height = (stat.count / maxCount) * 100; const isHovered = hoveredBar?.type === "daily" && hoveredBar?.index === index; const isSelected = selectedDate === stat.date; return (
setHoveredBar({ type: "daily", index, value: stat.count, label: stat.date })} onMouseLeave={() => setHoveredBar(null)} onClick={() => setSelectedDate(selectedDate === stat.date ? null : stat.date)} > {isHovered && (
{stat.date}: {stat.count} 张
点击查看当天高峰时段
)}
); })}
{analyticsData?.dailyStats?.[0]?.date} {analyticsData?.dailyStats?.at(-1)?.date}
{/* 高峰时段 */} 生图高峰时段 {selectedDate && ( {selectedDate} )} {!selectedDate && ( 点击左侧日期柱形查看当天数据 )}
{Array.from({ length: 24 }, (_, hour) => { const stat = analyticsData?.hourlyStats?.find(s => s.hour === hour); const count = stat?.count || 0; const maxCount = Math.max(...(analyticsData?.hourlyStats?.map(s => s.count) || [1]), 1); const height = (count / maxCount) * 100; const isHovered = hoveredBar?.type === "hourly" && hoveredBar?.index === hour; return (
setHoveredBar({ type: "hourly", index: hour, value: count, label: `${hour}:00` })} onMouseLeave={() => setHoveredBar(null)} > {isHovered && (
{hour}:00-{hour + 1}:00: {count} 张
)}
); })}
0:00 12:00 23:00
{/* 月度统计 */} 月度统计
{analyticsData?.monthlyStats?.slice(-6).map((stat) => (

{stat.count}

{stat.month}

))}
{/* 🆕 模型使用分析 */}

模型使用分析

{/* 模型使用排行 */} 模型使用排行 按生图数量排序
{analyticsData?.modelDetailStats?.slice(0, 8).map((stat, index) => { const maxCount = analyticsData?.modelDetailStats?.[0]?.count || 1; const percentage = (stat.count / maxCount) * 100; return (
{index + 1} {stat.modelName || stat.modelId || "未知模型"}
{stat.count} 张
{stat.providerName || "未知提供商"} {stat.userCount} 用户
); })} {(!analyticsData?.modelDetailStats || analyticsData.modelDetailStats.length === 0) && (
暂无数据
)}
{/* 提供商类型分布 */} 提供商分布 按 AI 提供商统计
{(() => { const providerColors: Record = { "gemini": "bg-purple-500", "volcano-vision": "bg-orange-500", "volcano-video": "bg-red-500", "alibaba": "bg-blue-500", "openai": "bg-green-500", "qianwen-video": "bg-cyan-500", "custom": "bg-gray-500", "local": "bg-yellow-500", }; const total = analyticsData?.providerTypeStats?.reduce((acc, s) => acc + s.count, 0) || 0; return analyticsData?.providerTypeStats?.map((stat, index) => { const percentage = total > 0 ? ((stat.count / total) * 100).toFixed(1) : "0"; const color = providerColors[stat.providerType || ""] || "bg-gray-400"; return (
{stat.providerName || stat.providerType || "未知"} {stat.count} ({percentage}%)
); }); })()} {(!analyticsData?.providerTypeStats || analyticsData.providerTypeStats.length === 0) && (
暂无数据
)}
{/* 图片尺寸分布 */} 图片尺寸分布 按生成图片尺寸统计
{analyticsData?.sizeStats?.map((stat) => { const total = analyticsData?.sizeStats?.reduce((acc, s) => acc + s.count, 0) || 0; const percentage = total > 0 ? ((stat.count / total) * 100).toFixed(1) : "0"; return (

{stat.imageSize || "未知"}

{stat.count} 张

{percentage}%

); })}
{/* 模型使用趋势(最近7天) */} 模型使用趋势 最近7天各模型使用量
{(() => { // 按日期分组 const groupedByDate = analyticsData?.modelTrendStats?.reduce((acc, stat) => { if (!acc[stat.date]) acc[stat.date] = []; acc[stat.date].push(stat); return acc; }, {} as Record) || {}; const dates = Object.keys(groupedByDate).sort().slice(-7); const maxDailyTotal = Math.max( ...dates.map(date => groupedByDate[date]?.reduce((sum, s) => sum + s.count, 0) || 0 ), 1 ); return dates.map(date => { const dayStats = groupedByDate[date] || []; const dayTotal = dayStats.reduce((sum, s) => sum + s.count, 0); const heightPercent = (dayTotal / maxDailyTotal) * 100; return (
{date.slice(5)}
{dayStats.map((stat, idx) => { const statPercent = dayTotal > 0 ? (stat.count / dayTotal) * 100 : 0; const colors = ["bg-blue-500", "bg-green-500", "bg-purple-500", "bg-orange-500", "bg-pink-500"]; return (
); })}
{dayTotal}
); }); })()} {(!analyticsData?.modelTrendStats || analyticsData.modelTrendStats.length === 0) && (
暂无数据
)}
{/* 网站设置 */}
setSiteSettings({ ...siteSettings, siteName: e.target.value })} placeholder="输入网站名称" />
setSiteSettings({ ...siteSettings, siteLogo: e.target.value })} placeholder="https://example.com/logo.png" />
setSiteSettings({ ...siteSettings, siteLogoDark: e.target.value })} placeholder="https://example.com/logo-dark.png" />
setSiteSettings({ ...siteSettings, favicon: e.target.value })} placeholder="https://example.com/favicon.ico" />
开启后,用户可以选择"智能自动"模型,系统将根据场景智能选择最佳模型
setSiteSettings({ ...siteSettings, showAutoModel: checked })} />
开启后,仅在指定时间段内允许使用系统,超出时间段输入框将被禁用
setSiteSettings({ ...siteSettings, usageTimeEnabled: checked })} />
{siteSettings.usageTimeEnabled && ( <>