"use client"; import { useState, useEffect, useMemo } 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, Move3D, Upload, Video, Loader2, Check, Paintbrush, } 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"; import { defaultMembershipConfig, defaultMembershipUserOverrides, type MembershipFaqItem, type MembershipPlanConfig, normalizeMembershipConfig, normalizeMembershipUserOverrides, } from "@/lib/membership/config"; import { defaultPointsPackages, defaultPointsSystemConfig, normalizePointsPackages, normalizePointsSystemConfig, type PointsPackage, type PointsSystemConfig, } from "@/lib/points/config"; import { defaultDulupayConfig, normalizeDulupayConfig, type DulupayConfig, } from "@/lib/payment/dulupay-config"; import { DEFAULT_CANVAS_SKILL_TAGS, DEFAULT_CANVAS_SKILL_TAGS_TITLE, normalizeCanvasSkillTags, type CanvasSkillTag, } from "@/lib/canvas-skill-tags"; import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { CANVAS_SKILL_ICON_OPTIONS, getCanvasSkillIconComponent, } from "@/components/canvas/canvas-skill-icons"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import type { AgentWorkflowUiConfig } from "@/types/agent"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b"; const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; function createEmptyCanvasSkillTag(): CanvasSkillTag { return { id: `tag-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: "", prompt: "", placeholder: "", iconName: "message-circle", iconUrl: "", }; } // 类型定义 type User = { id: string; email: string; name: string | null; department: string | null; role: "guest" | "regular" | "admin"; isHighVip: boolean; isActive: boolean; monthlyImageLimit: number | null; pointsBalance: number; pointsGiftedTotal: number; 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" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "volcano-multimodal"; baseUrl: string; apiKey: string; isEnabled: boolean; createdAt: string; }; // 🆕 图片生成模型特性配置 type ImageGenConfig = { maxImages?: number; // 最大输出图片数,如阿里 1-6,Gemini 1-2 supportsSize?: boolean; // 是否支持自定义分辨率 supports1K?: boolean; // 是否支持 1K 分辨率 supports2K?: boolean; // 是否支持 2K 分辨率 supports4K?: boolean; // 是否支持 4K 分辨率 supportsPromptExtend?: boolean; // 是否支持提示词智能优化 supportsNegativePrompt?: boolean; // 是否支持反向提示词 supportsWatermark?: boolean; // 是否支持水印开关 supportsSeed?: boolean; // 是否支持随机种子 creditCostPerImage?: number; // 每张图片积分消耗 creditCostPerVideo?: number; // 每条视频积分消耗 enableLoadingPlaceholder?: boolean; // 是否启用加载占位图 loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" }; type AiModel = { id: string; providerId: string; modelId: string; displayName: string; avatarUrl?: string; description?: string; supportsVision: boolean; supportsImageGen: boolean; isEnabled: boolean; isHidden: boolean; isDefault: boolean; sortOrder: number; fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; isAgentModel?: boolean; agentCodingModelId?: string; isMultimodal?: boolean; imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置 }; type StorageConfig = { id: string; name: string; type: "s3" | "local" | "aliyun-oss"; 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; defaultTheme: "light" | "dark" | "system"; bannerEnabled: boolean; bannerText: string; bannerLink: string | null; toolbox1Name: string; toolbox1Enabled: boolean; toolbox2Name: string; toolbox2Enabled: boolean; tutorialVideoUrl: string | null; tutorialEnabled: boolean; rankingEnabled: boolean; canvasEnabled: boolean; agentEnabled: boolean; agentCategoryTreeConfig: { productCategories: string[]; audienceTags: string[]; usageScenarios: string[]; brands: string[]; }; agentSecondaryTypeConfig: { image: string[]; video: string[]; text: string[]; }; agentWorkflowUiConfig: AgentWorkflowUiConfig; realtimeCanvasEnabled: boolean; aiDetailGenerationEnabled: boolean; canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: string | null; canvasDetailDefaultModelId: string | null; lovartComposerPrefix: string; lovartComposerPreviewTexts: string[]; lovartPromptTagVisibility: LovartPromptTagVisibility; contactConfig: ContactConfig; storageFailoverConfig: StorageFailoverConfig; ossImageMigrationConfig: OssImageMigrationConfig; pointsSystemConfig: PointsSystemConfig; pointsPackages: PointsPackage[]; dulupayConfig: DulupayConfig; lovartPromoText: string; lovartPromoActionText: string; lovartHeroTitle: string; lovartHeroSubtitle: string; homeVariant: "classic" | "lovart"; surpriseCategories: string[]; membershipConfig: ReturnType; membershipUserOverrides: ReturnType; }; type LovartPromptTagVisibility = { nanoBananaPro: boolean; design: boolean; branding: boolean; illustration: boolean; ecommerce: boolean; video: boolean; }; type ContactConfig = { phone: string; wechatQrUrl: string; }; type StorageFailoverConfig = { enabled: boolean; backupStorageConfigId: string; }; type OssImageMigrationConfig = { enabled: boolean; sourceStorageConfigId: string; targetStorageConfigId: string; migrateAfterHours: number; cleanupAfterHours: number; batchSize: number; }; const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = { nanoBananaPro: true, design: true, branding: true, illustration: true, ecommerce: true, video: true, }; const defaultContactConfig: ContactConfig = { phone: "", wechatQrUrl: "", }; const defaultStorageFailoverConfig: StorageFailoverConfig = { enabled: false, backupStorageConfigId: "", }; const defaultOssImageMigrationConfig: OssImageMigrationConfig = { enabled: true, sourceStorageConfigId: "", targetStorageConfigId: "", migrateAfterHours: 48, cleanupAfterHours: 72, batchSize: 20, }; const lovartPromptTagOptions: Array<{ key: keyof LovartPromptTagVisibility; label: string; }> = [ { key: "nanoBananaPro", label: "Nano Banana Pro" }, { key: "design", label: "Design" }, { key: "branding", label: "Branding" }, { key: "illustration", label: "Illustration" }, { key: "ecommerce", label: "E-Commerce" }, { key: "video", label: "Video" }, ]; type NavLink = { id: string; name: string; url: string; order: number; isEnabled: boolean; toolboxGroup: number; // 1 或 2 }; type TutorialVideo = { id: string; title: string; videoUrl: string; coverUrl: string | null; description: string | null; order: number; isEnabled: boolean; }; type DingTalkOrganization = { id: string; name: string; appKey: string; appSecret: string; }; type AuthSettings = { // 保留单一配置用于兼容旧数据(后端会自动同步为第一个组织) dingTalkAppKey?: string | null; dingTalkAppSecret?: string | null; googleClientId?: string | null; googleClientSecret?: string | null; authUrl?: string | null; isDingTalkEnabled: boolean; isGoogleEnabled: boolean; isRegistrationEnabled?: boolean; isEmailVerificationEnabled?: boolean; emailVerificationExpiresMinutes?: number; resendApiKey?: string | null; resendFromEmail?: string | null; isEmailServiceConfigured?: boolean; dingTalkOrganizations: DingTalkOrganization[]; }; type GeneratedImageRecord = { id: string; sourceMessageId?: string; userId: string; userName: string | null; userEmail: string; userDepartment: string | null; thumbnailUrl: string; originalUrl: string; videoUrl?: string | null; recordType?: "image" | "video"; imageSize: string | null; prompt: string | null; modelId: string | null; isFeatured: boolean; featuredCategory: string | null; 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: "alibaba-image", label: "阿里生图模型" }, // 🆕 千问图片生成/编辑 { value: "ruxa-image", label: "RUXA生图" }, { value: "fal-image", label: "FAL 生图模型" }, { value: "openai-image", label: "OpenAI 图片兼容" }, { value: "midjourney", label: "Midjourney" }, { value: "tripo-3d", label: "Tripo3D 模型生成" }, { value: "zhipu", label: "智普官方 4.7" }, { value: "volcano-multimodal", label: "火山引擎多模态" }, { value: "openai", label: "OpenAI 兼容" }, { value: "custom", label: "自定义 OpenAI 兼容" }, { value: "local", label: "本地模型" }, { value: "gemini", label: "Gemini中转" }, { value: "huanke-gemini", label: "环科Gemini生图" }, // 🆕 环科 Gemini 原生格式 API { value: "qianwen-video", label: "千问视频生成" }, { value: "doubao-video", label: "豆包AI视频" }, { value: "ruxa-video", label: "RUXA 视频生成" }, { value: "volcano-vision", label: "火山引擎视觉" }, { value: "volcano-video", label: "火山引擎视频" }, { value: "fal-video", label: "FAL 视频生成" }, ]; const PROVIDER_DEFAULT_BASE_URL: Record = { alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1", "alibaba-image": "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", "ruxa-image": "https://api.ruxa.ai", "fal-image": "https://fal.run", "openai-image": "https://api.openai.com/v1", midjourney: "https://your-midjourney-api.example.com", "fal-video": "https://fal.run", "tripo-3d": "https://api.tripo3d.ai/v2/openapi", zhipu: "https://open.bigmodel.cn/api/paas/v4", "volcano-multimodal": "https://ark.cn-beijing.volces.com/api/v3", openai: "https://api.openai.com/v1", "huanke-gemini": "https://api.huandutech.com/v1", "qianwen-video": "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation", "doubao-video": "https://ark.cn-beijing.volces.com/api/v3", "ruxa-video": "https://api.ruxa.ai", "volcano-vision": "https://ark.cn-beijing.volces.com", "volcano-video": "https://ark.cn-beijing.volces.com/api/v3", }; const LEMON_OPENAI_BASE_URL = "https://new.lemonapi.site/v1"; const LEMON_PROVIDER_PRESET = { name: "LemonAPI Gemini", type: "openai" as const, baseUrl: LEMON_OPENAI_BASE_URL, }; const LEMON_MODEL_PRESETS = [ { modelId: "[L]gemini-3.1-pro-preview", displayName: "Gemini-3.1-pro-preview", supportsVision: true, supportsImageGen: false, isMultimodal: true, description: "LemonAPI OpenAI兼容 Gemini 3.1 Pro 预设模型", }, ]; function getProviderDefaultBaseUrl(type?: string) { if (!type) return ""; return PROVIDER_DEFAULT_BASE_URL[type] || ""; } function isHttpUrlLike(value: string) { return /^https?:\/\//i.test(value.trim()); } function isLemonApiBaseUrl(value?: string | null) { return String(value || "").trim().replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL; } export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); // 用户管理状态 const [userSearch, setUserSearch] = useState(""); const [userPage, setUserPage] = useState(1); const [userLimit] = useState(10); const [giftPointsDraftByUser, setGiftPointsDraftByUser] = useState>({}); const [deductPointsDraftByUser, setDeductPointsDraftByUser] = useState>({}); // 批量设置出图限额状态 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: tutorialVideosData, mutate: mutateTutorialVideos } = useSWR<{ videos: TutorialVideo[] }>("/api/tutorial-videos", 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; totalImages?: number; totalVideos?: 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: "由壹点累了也是需要休息的", defaultTheme: "system", bannerEnabled: true, bannerText: "升级会员, Nano Banana Pro 免费365天!", bannerLink: null, toolbox1Name: "工具箱", toolbox1Enabled: true, toolbox2Name: "工具箱2", toolbox2Enabled: false, tutorialVideoUrl: null, tutorialEnabled: false, rankingEnabled: true, canvasEnabled: true, agentEnabled: true, agentCategoryTreeConfig: { productCategories: [], audienceTags: [], usageScenarios: [], brands: [], }, agentSecondaryTypeConfig: { image: [], video: [], text: [], }, agentWorkflowUiConfig: defaultAgentWorkflowUiConfig, realtimeCanvasEnabled: true, aiDetailGenerationEnabled: false, canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, canvasDetailDefaultModelId: null, lovartComposerPrefix: "让Buyi-ai", lovartComposerPreviewTexts: [ "打造引人注目的社交媒体视觉", "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], lovartPromptTagVisibility: defaultLovartPromptTagVisibility, contactConfig: defaultContactConfig, storageFailoverConfig: defaultStorageFailoverConfig, ossImageMigrationConfig: defaultOssImageMigrationConfig, pointsSystemConfig: defaultPointsSystemConfig, pointsPackages: defaultPointsPackages, dulupayConfig: defaultDulupayConfig, lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: "立即升级", lovartHeroTitle: "由壹点AI 让设计更简单", lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切", homeVariant: "classic", surpriseCategories: ["全部", "精选"], membershipConfig: defaultMembershipConfig, membershipUserOverrides: defaultMembershipUserOverrides, }); const [newOverrideUserId, setNewOverrideUserId] = useState(""); const [newOverrideEmail, setNewOverrideEmail] = useState(""); const [newOverridePlanId, setNewOverridePlanId] = useState(""); useEffect(() => { if (siteSettingsData) { const normalizedMembershipConfig = normalizeMembershipConfig( (siteSettingsData as any).membershipConfig ); const normalizedMembershipOverrides = normalizeMembershipUserOverrides( (siteSettingsData as any).membershipUserOverrides ); const normalizedPointsSystemConfig = normalizePointsSystemConfig( (siteSettingsData as any).pointsSystemConfig ); const normalizedPointsPackages = normalizePointsPackages( (siteSettingsData as any).pointsPackages ); const normalizedDulupayConfig = normalizeDulupayConfig( (siteSettingsData as any).dulupayConfig ); 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 || "由壹点累了也是需要休息的", defaultTheme: siteSettingsData.defaultTheme || "system", bannerEnabled: siteSettingsData.bannerEnabled ?? true, bannerText: siteSettingsData.bannerText || "升级会员, Nano Banana Pro 免费365天!", bannerLink: siteSettingsData.bannerLink || null, toolbox1Name: siteSettingsData.toolbox1Name || "工具箱", toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true, toolbox2Name: siteSettingsData.toolbox2Name || "工具箱2", toolbox2Enabled: siteSettingsData.toolbox2Enabled ?? false, tutorialVideoUrl: siteSettingsData.tutorialVideoUrl || null, tutorialEnabled: siteSettingsData.tutorialEnabled ?? false, rankingEnabled: siteSettingsData.rankingEnabled ?? true, canvasEnabled: siteSettingsData.canvasEnabled ?? true, agentEnabled: (siteSettingsData as any).agentEnabled ?? true, agentCategoryTreeConfig: { productCategories: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.productCategories) ? (siteSettingsData as any).agentCategoryTreeConfig.productCategories : [], audienceTags: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.audienceTags) ? (siteSettingsData as any).agentCategoryTreeConfig.audienceTags : [], usageScenarios: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.usageScenarios) ? (siteSettingsData as any).agentCategoryTreeConfig.usageScenarios : [], brands: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.brands) ? (siteSettingsData as any).agentCategoryTreeConfig.brands : [], }, agentSecondaryTypeConfig: { image: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.image) ? (siteSettingsData as any).agentSecondaryTypeConfig.image : [], video: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.video) ? (siteSettingsData as any).agentSecondaryTypeConfig.video : [], text: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.text) ? (siteSettingsData as any).agentSecondaryTypeConfig.text : [], }, agentWorkflowUiConfig: normalizeAgentWorkflowUiConfig( (siteSettingsData as any).agentWorkflowUiConfig ), realtimeCanvasEnabled: (siteSettingsData as any).realtimeCanvasEnabled ?? true, aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null, canvasDetailDefaultModelId: siteSettingsData.canvasDetailDefaultModelId || null, lovartComposerPrefix: String((siteSettingsData as any).lovartComposerPrefix || "").trim() || "让Buyi-ai", lovartComposerPreviewTexts: Array.isArray(siteSettingsData.lovartComposerPreviewTexts) ? siteSettingsData.lovartComposerPreviewTexts .map((item: any) => String(item).trim()) .filter(Boolean) : [ "打造引人注目的社交媒体视觉", "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], lovartPromptTagVisibility: { nanoBananaPro: (siteSettingsData as any).lovartPromptTagVisibility?.nanoBananaPro !== false, design: (siteSettingsData as any).lovartPromptTagVisibility?.design !== false, branding: (siteSettingsData as any).lovartPromptTagVisibility?.branding !== false, illustration: (siteSettingsData as any).lovartPromptTagVisibility?.illustration !== false, ecommerce: (siteSettingsData as any).lovartPromptTagVisibility?.ecommerce !== false, video: (siteSettingsData as any).lovartPromptTagVisibility?.video !== false, }, contactConfig: { phone: String((siteSettingsData as any).contactConfig?.phone || "").trim(), wechatQrUrl: String((siteSettingsData as any).contactConfig?.wechatQrUrl || "").trim(), }, storageFailoverConfig: { enabled: (siteSettingsData as any).storageFailoverConfig?.enabled === true, backupStorageConfigId: String( (siteSettingsData as any).storageFailoverConfig?.backupStorageConfigId || "" ).trim(), }, ossImageMigrationConfig: { enabled: (siteSettingsData as any).ossImageMigrationConfig?.enabled !== false, sourceStorageConfigId: String( (siteSettingsData as any).ossImageMigrationConfig?.sourceStorageConfigId || "" ).trim(), targetStorageConfigId: String( (siteSettingsData as any).ossImageMigrationConfig?.targetStorageConfigId || "" ).trim(), migrateAfterHours: Math.max( 1, Math.min( 720, Number((siteSettingsData as any).ossImageMigrationConfig?.migrateAfterHours) || defaultOssImageMigrationConfig.migrateAfterHours ) ), cleanupAfterHours: Math.max( 2, Math.min( 1440, Number((siteSettingsData as any).ossImageMigrationConfig?.cleanupAfterHours) || defaultOssImageMigrationConfig.cleanupAfterHours ) ), batchSize: Math.max( 1, Math.min( 100, Number((siteSettingsData as any).ossImageMigrationConfig?.batchSize) || defaultOssImageMigrationConfig.batchSize ) ), }, pointsSystemConfig: normalizedPointsSystemConfig, pointsPackages: normalizedPointsPackages, dulupayConfig: normalizedDulupayConfig, lovartPromoText: siteSettingsData.lovartPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: siteSettingsData.lovartPromoActionText || "立即升级", lovartHeroTitle: siteSettingsData.lovartHeroTitle || "由壹点AI 让设计更简单", lovartHeroSubtitle: siteSettingsData.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切", homeVariant: siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic", surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories) ? siteSettingsData.surpriseCategories.map((item: any) => String(item).trim()).filter(Boolean) : ["全部", "惊喜"], membershipConfig: normalizedMembershipConfig, membershipUserOverrides: normalizedMembershipOverrides, }); } }, [siteSettingsData]); const featuredCategoryOptions = useMemo(() => { const values = (siteSettings.surpriseCategories || []) .map((item) => String(item || "").trim()) .filter((item) => item && item !== "全部"); return Array.from(new Set(values)); }, [siteSettings.surpriseCategories]); // 抠图API配置状态 const [rmbgConfig, setRmbgConfig] = useState({ provider: "rmbg" as "rmbg" | "photoroom" | "aoscdn", // 抠图服务提供商 apiUrl: "", apiKey: "", // PhotoRoom 配置 photoroomApiKey: "", // AOS 配置 aosApiKey: "", aosApiBaseUrl: "https://techsz.aoscdn.com", aosForegroundType: "auto" as "auto" | "person" | "object" | "stamp", aosCrop: false, aosFormat: "png" as "png" | "jpg", }); // 变清晰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 [watermarkRemoveConfig, setWatermarkRemoveConfig] = useState({ apiKey: "", // PicWish API密钥 }); // 角度编辑API配置状态 const [angleEditConfig, setAngleEditConfig] = useState({ enabled: false, falApiKey: "", loraScale: 1.0, // 官方默认值 inferenceSteps: 28, guidanceScale: 4.5, // 官方默认值 maxImageSize: 10, timeout: 120, }); // 实时画布API配置状态 const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ enabled: true, bflApiKey: "", bflBaseUrl: DEFAULT_BFL_BASE_URL, bflModel: DEFAULT_BFL_REALTIME_MODEL, fallbackEnabled: false, fallbackBflApiKey: "", fallbackBflBaseUrl: DEFAULT_BFL_BASE_URL, fallbackBflModel: DEFAULT_BFL_REALTIME_MODEL, renderMode: "balanced" as "strict-structure" | "balanced" | "stylize", throttleInterval: 100, maxBuffering: 2, maxInputResolution: 1024, jpegQuality: 0.55, previewMegapixels: 0.4, detailMegapixels: 1.0, outputJpegQuality: 62, steps: 3, scheduleMu: 2.3, feedback: 0.95, imageSize: "square" as "square" | "square_hd", }); // 批量处理配置状态 const [sceneFusionConfig, setSceneFusionConfig] = useState({ enabled: true, preRmbgEnabled: true, defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: 10, modelId: "", // 首选生图模型ID(多样性模式下优先尝试) }); const [tripo3dConfig, setTripo3dConfig] = useState({ providerId: "", name: "Tripo3D", baseUrl: getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: true, }); // 加载抠图API配置 const { data: rmbgConfigData } = useSWR("/api/admin/settings?key=rmbg_api_config", fetcher); useEffect(() => { if (rmbgConfigData?.setting?.value) { const config = rmbgConfigData.setting.value as { provider?: "rmbg" | "photoroom" | "aoscdn"; apiUrl?: string; apiKey?: string; photoroomApiKey?: string; aosApiKey?: string; aosApiBaseUrl?: string; aosForegroundType?: "" | "person" | "object" | "stamp"; aosCrop?: 0 | 1; aosFormat?: "png" | "jpg"; }; setRmbgConfig({ provider: config.provider || "rmbg", apiUrl: config.apiUrl || "", apiKey: config.apiKey || "", photoroomApiKey: config.photoroomApiKey || "", aosApiKey: config.aosApiKey || "", aosApiBaseUrl: config.aosApiBaseUrl || "https://techsz.aoscdn.com", aosForegroundType: config.aosForegroundType === "person" || config.aosForegroundType === "object" || config.aosForegroundType === "stamp" ? config.aosForegroundType : "auto", aosCrop: config.aosCrop === 1, aosFormat: config.aosFormat === "jpg" ? "jpg" : "png", }); } }, [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]); // 加载去水印API配置 const { data: watermarkRemoveConfigData } = useSWR("/api/admin/settings?key=watermark_remove_config", fetcher); useEffect(() => { if (watermarkRemoveConfigData?.setting?.value) { const config = watermarkRemoveConfigData.setting.value as { apiKey?: string; }; setWatermarkRemoveConfig({ apiKey: config.apiKey || "", }); } }, [watermarkRemoveConfigData]); // 加载角度编辑API配置 const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher); useEffect(() => { if (angleEditConfigData?.setting?.value) { const config = angleEditConfigData.setting.value as { enabled?: boolean; falApiKey?: string; loraScale?: number; inferenceSteps?: number; guidanceScale?: number; maxImageSize?: number; timeout?: number; }; setAngleEditConfig({ enabled: config.enabled || false, falApiKey: config.falApiKey || "", loraScale: config.loraScale ?? 1.0, inferenceSteps: config.inferenceSteps ?? 28, guidanceScale: config.guidanceScale ?? 4.5, maxImageSize: config.maxImageSize ?? 10, timeout: config.timeout ?? 120, }); } }, [angleEditConfigData]); // 加载实时画布API配置 const { data: realtimeCanvasConfigData } = useSWR( "/api/admin/settings?key=realtime_canvas_config", fetcher ); useEffect(() => { if (realtimeCanvasConfigData?.setting?.value) { const config = realtimeCanvasConfigData.setting.value as { enabled?: boolean; bflApiKey?: string; bflBaseUrl?: string; bflModel?: string; fallbackEnabled?: boolean; fallbackBflApiKey?: string; fallbackBflBaseUrl?: string; fallbackBflModel?: string; fallbackRunwareApiKey?: string; fallbackFalApiKey?: string; fallbackRunwareModel?: string; runwareApiKey?: string; falApiKey?: string; runwareModel?: string; renderMode?: "strict-structure" | "balanced" | "stylize"; throttleInterval?: number; maxBuffering?: number; maxInputResolution?: number; jpegQuality?: number; previewMegapixels?: number; detailMegapixels?: number; outputJpegQuality?: number; defaults?: { steps?: number; scheduleMu?: number; feedback?: number; imageSize?: "square" | "square_hd"; }; }; setRealtimeCanvasConfig({ enabled: config.enabled ?? true, bflApiKey: config.bflApiKey || config.runwareApiKey || config.falApiKey || "", bflBaseUrl: config.bflBaseUrl || DEFAULT_BFL_BASE_URL, bflModel: config.bflModel || config.runwareModel || DEFAULT_BFL_REALTIME_MODEL, fallbackEnabled: config.fallbackEnabled === true, fallbackBflApiKey: config.fallbackBflApiKey || config.fallbackRunwareApiKey || config.fallbackFalApiKey || "", fallbackBflBaseUrl: config.fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL, fallbackBflModel: config.fallbackBflModel || config.fallbackRunwareModel || DEFAULT_BFL_REALTIME_MODEL, renderMode: config.renderMode === "strict-structure" || config.renderMode === "stylize" ? config.renderMode : "balanced", throttleInterval: Number(config.throttleInterval || 100), maxBuffering: Number(config.maxBuffering || 2), maxInputResolution: Number(config.maxInputResolution || 1024), jpegQuality: Number(config.jpegQuality || 0.55), previewMegapixels: Number(config.previewMegapixels || 0.4), detailMegapixels: Number(config.detailMegapixels || 1.0), outputJpegQuality: Number(config.outputJpegQuality || 62), steps: Number(config.defaults?.steps || 3), scheduleMu: Number(config.defaults?.scheduleMu || 2.3), feedback: Number(config.defaults?.feedback || 0.95), imageSize: config.defaults?.imageSize === "square_hd" ? "square_hd" : "square", }); } }, [realtimeCanvasConfigData]); // 加载批量处理配置 const { data: sceneFusionConfigData } = useSWR("/api/admin/settings?key=scene_fusion_config", fetcher); useEffect(() => { if (sceneFusionConfigData?.setting?.value) { const config = sceneFusionConfigData.setting.value as { enabled?: boolean; preRmbgEnabled?: boolean; defaultPrompt?: string; maxScenes?: number; modelId?: string; }; setSceneFusionConfig({ enabled: config.enabled ?? true, preRmbgEnabled: config.preRmbgEnabled ?? true, defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: Math.min(config.maxScenes ?? 10, 10), modelId: config.modelId ?? "", }); } }, [sceneFusionConfigData]); useEffect(() => { const tripoProvider = (providersData?.providers || []).find( (provider) => provider.type === "tripo-3d" ); if (tripoProvider) { setTripo3dConfig({ providerId: tripoProvider.id, name: tripoProvider.name || "Tripo3D", baseUrl: tripoProvider.baseUrl || getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: tripoProvider.isEnabled, }); return; } setTripo3dConfig({ providerId: "", name: "Tripo3D", baseUrl: getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: true, }); }, [providersData?.providers]); // 导航链接状态 const [navLinkForm, setNavLinkForm] = useState({ id: "", name: "", url: "", order: 0, isEnabled: true, toolboxGroup: 1, // 默认属于工具箱1 }); const [showNavLinkDialog, setShowNavLinkDialog] = useState(false); const [editingNavLink, setEditingNavLink] = useState(null); // 视频上传状态 const [videoUploading, setVideoUploading] = useState(false); const [videoUploadProgress, setVideoUploadProgress] = useState(""); const [videoUploadPercent, setVideoUploadPercent] = useState(0); const [videoUploadStage, setVideoUploadStage] = useState<"idle" | "uploading" | "compressing" | "saving" | "done">("idle"); const [videoQuality, setVideoQuality] = useState<"low" | "medium" | "high">("medium"); // 视频教程管理状态 const [showTutorialVideoDialog, setShowTutorialVideoDialog] = useState(false); const [editingTutorialVideo, setEditingTutorialVideo] = useState(null); const [tutorialVideoForm, setTutorialVideoForm] = useState({ title: "", videoUrl: "", coverUrl: "", description: "", order: 0, isEnabled: true, }); // 快捷短语状态 const [questions, setQuestions] = useState([]); const { data: questionsData, mutate: mutateQuestions } = useSWR("/api/starter-questions", fetcher); const [canvasSkillTagsTitle, setCanvasSkillTagsTitle] = useState( DEFAULT_CANVAS_SKILL_TAGS_TITLE ); const [canvasSkillTagsEnabled, setCanvasSkillTagsEnabled] = useState(true); const [canvasSkillTags, setCanvasSkillTags] = useState( DEFAULT_CANVAS_SKILL_TAGS ); const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; tags?: CanvasSkillTag[]; }>("/api/canvas-skill-tags", 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]); useEffect(() => { if (canvasSkillTagsData) { setCanvasSkillTagsEnabled(canvasSkillTagsData.enabled !== false); setCanvasSkillTagsTitle( String(canvasSkillTagsData.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE ); setCanvasSkillTags(normalizeCanvasSkillTags(canvasSkillTagsData.tags)); } }, [canvasSkillTagsData]); 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 handleCanvasSkillTagChange = ( index: number, field: "name" | "prompt" | "placeholder" | "iconName" | "iconUrl", value: string ) => { setCanvasSkillTags((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, [field]: value } : item ) ); }; const handleCanvasSkillTagIconUpload = async (index: number, file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } const targetTag = canvasSkillTags[index]; if (!targetTag) return; try { setCanvasSkillTagUploadingId(targetTag.id); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setCanvasSkillTags((prev) => prev.map((tag, tagIndex) => tagIndex === index ? { ...tag, iconUrl: result.url } : tag ) ); toast.success("标签图标上传成功"); } catch (error: any) { toast.error(error?.message || "标签图标上传失败"); } finally { setCanvasSkillTagUploadingId(null); } }; const handleAddCanvasSkillTag = () => { setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]); }; const handleRemoveCanvasSkillTag = (index: number) => { setCanvasSkillTags((prev) => prev.filter((_, itemIndex) => itemIndex !== index)); }; const handleSaveCanvasSkillTags = async () => { try { const payload = normalizeCanvasSkillTags(canvasSkillTags); const res = await fetch("/api/canvas-skill-tags", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: canvasSkillTagsEnabled, title: canvasSkillTagsTitle, tags: payload, }), }); if (!res.ok) throw new Error("Failed"); const data = await res.json().catch(() => ({})); setCanvasSkillTagsEnabled(data?.enabled !== false); setCanvasSkillTagsTitle( String(data?.title || canvasSkillTagsTitle || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE ); setCanvasSkillTags(normalizeCanvasSkillTags(data?.tags ?? payload)); toast.success("快捷标签已更新"); mutateCanvasSkillTags(); } catch (error) { toast.error("快捷标签更新失败"); } }; // 认证设置状态 const [authSettings, setAuthSettings] = useState({ dingTalkAppKey: "", dingTalkAppSecret: "", googleClientId: "", googleClientSecret: "", authUrl: "", isDingTalkEnabled: false, isGoogleEnabled: false, isRegistrationEnabled: true, isEmailVerificationEnabled: false, emailVerificationExpiresMinutes: 10, resendApiKey: "", resendFromEmail: "", isEmailServiceConfigured: false, dingTalkOrganizations: [], }); const [isGoogleConfigured, setIsGoogleConfigured] = useState(false); 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 || "", googleClientId: authSettingsData.googleClientId || "", googleClientSecret: authSettingsData.googleClientSecret || "", authUrl: authSettingsData.authUrl || "", isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, isGoogleEnabled: authSettingsData.isGoogleEnabled || false, isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, isEmailVerificationEnabled: authSettingsData.isEmailVerificationEnabled === true, emailVerificationExpiresMinutes: Number(authSettingsData.emailVerificationExpiresMinutes) || 10, resendApiKey: authSettingsData.resendApiKey || "", resendFromEmail: authSettingsData.resendFromEmail || "", isEmailServiceConfigured: authSettingsData.isEmailServiceConfigured === true, dingTalkOrganizations: normalizedOrgs, }); setIsGoogleConfigured(Boolean(authSettingsData.isGoogleConfigured)); }, [authSettingsData]); // AI 提供商 & 模型状态 const [providerForm, setProviderForm] = useState({ id: "", name: "", type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "volcano-multimodal", 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: "", avatarUrl: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isHidden: false, isDefault: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", // 备用模型 ID 1 fallbackModelId2: "", // 备用模型 ID 2 excludeFromFailover: false, // 🆕 图片生成模型特性配置 imageGenConfig: { maxImages: 2, supportsSize: false, supports1K: true, supports2K: true, supports4K: true, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, supportsSeed: false, creditCostPerImage: 0, creditCostPerVideo: 0, enableLoadingPlaceholder: true, loadingColor: "purple", } as ImageGenConfig, }); const [showModelDialog, setShowModelDialog] = useState(false); const [editingModel, setEditingModel] = useState(null); const [modelAvatarUploading, setModelAvatarUploading] = useState(false); const [contactQrUploading, setContactQrUploading] = useState(false); const [workflowOptionUploadingKey, setWorkflowOptionUploadingKey] = 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 selectedModelProviderType = providersData?.providers?.find((p) => p.id === modelForm.providerId)?.type || ""; const isVideoProviderForModelConfig = [ "volcano-video", "doubao-video", "qianwen-video", "fal-video", "ruxa-video", ].includes(selectedModelProviderType); // 存储配置状态 const [storageForm, setStorageForm] = useState({ id: "", name: "", type: "s3" as "s3" | "local" | "aliyun-oss", endpoint: "", region: "", bucket: "", accessKeyId: "", secretAccessKey: "", publicUrl: "", isDefault: false, isEnabled: true, }); const [showStorageDialog, setShowStorageDialog] = useState(false); const [editingStorage, setEditingStorage] = useState(null); const updateWorkflowStep = ( stepKey: string, updater: (step: AgentWorkflowUiConfig["steps"][number]) => AgentWorkflowUiConfig["steps"][number] ) => { setSiteSettings((prev) => ({ ...prev, agentWorkflowUiConfig: { steps: prev.agentWorkflowUiConfig.steps.map((step) => step.key === stepKey ? updater(step) : step ), }, })); }; const updateWorkflowOption = ( stepKey: string, optionKey: string, updater: ( option: AgentWorkflowUiConfig["steps"][number]["options"][number] ) => AgentWorkflowUiConfig["steps"][number]["options"][number] ) => { updateWorkflowStep(stepKey, (step) => ({ ...step, options: step.options.map((option) => option.key === optionKey ? updater(option) : option ), })); }; const handleWorkflowOptionPreviewUpload = async ( stepKey: string, optionKey: string, file: File ) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } const uploadKey = `${stepKey}:${optionKey}`; try { setWorkflowOptionUploadingKey(uploadKey); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } updateWorkflowOption(stepKey, optionKey, (option) => ({ ...option, previewImageUrl: result.url, })); toast.success("功能卡片预览图上传成功"); } catch (error: any) { toast.error(error?.message || "功能卡片预览图上传失败"); } finally { setWorkflowOptionUploadingKey(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 handleGiftPoints = async (userId: string) => { const rawValue = giftPointsDraftByUser[userId] || ""; const points = Number.parseInt(rawValue, 10); if (!Number.isFinite(points) || points <= 0) { toast.error("请输入有效的赠送积分(大于 0)"); return; } try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, giftPoints: points }), }); const data = await response.json(); if (!response.ok) { throw new Error(data?.message || data?.error || "赠送失败"); } toast.success(`已赠送 ${points} 积分`); setGiftPointsDraftByUser((prev) => ({ ...prev, [userId]: "" })); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "赠送积分失败"); } }; const handleDeductPoints = async (userId: string) => { const rawValue = deductPointsDraftByUser[userId] || ""; const points = Number.parseInt(rawValue, 10); if (!Number.isFinite(points) || points <= 0) { toast.error("请输入有效的扣减积分(大于 0)"); return; } try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, deductPoints: points }), }); const data = await response.json(); if (!response.ok) { throw new Error(data?.message || data?.error || "扣减失败"); } toast.success(`已扣减 ${points} 积分`); setDeductPointsDraftByUser((prev) => ({ ...prev, [userId]: "" })); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "扣减积分失败"); } }; 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 { if ( siteSettings.storageFailoverConfig.enabled && !siteSettings.storageFailoverConfig.backupStorageConfigId ) { toast.error("请先选择备用存储配置"); return; } const selectedBackupConfig = (storageData?.configs || []).find( (config) => config.id === siteSettings.storageFailoverConfig.backupStorageConfigId ); if ( siteSettings.storageFailoverConfig.enabled && selectedBackupConfig?.isDefault ) { toast.error("备用存储配置不能与默认配置相同"); return; } if (siteSettings.ossImageMigrationConfig.enabled) { if ( siteSettings.ossImageMigrationConfig.cleanupAfterHours <= siteSettings.ossImageMigrationConfig.migrateAfterHours ) { toast.error("OSS 清理时间必须晚于转存时间"); return; } if ( siteSettings.ossImageMigrationConfig.sourceStorageConfigId && siteSettings.ossImageMigrationConfig.targetStorageConfigId && siteSettings.ossImageMigrationConfig.sourceStorageConfigId === siteSettings.ossImageMigrationConfig.targetStorageConfigId ) { toast.error("OSS 源桶和目标桶不能相同"); return; } const targetConfig = (storageData?.configs || []).find( (config) => config.id === siteSettings.ossImageMigrationConfig.targetStorageConfigId ); if ( targetConfig && targetConfig.type === "s3" && targetConfig.endpoint?.includes("aliyuncs.com") ) { toast.error("OSS 转存目标桶不能是阿里云 OSS"); return; } } const parsedMembershipConfig = normalizeMembershipConfig(siteSettings.membershipConfig); const parsedMembershipOverrides = normalizeMembershipUserOverrides( siteSettings.membershipUserOverrides ); const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages); const payload = { ...siteSettings, membershipConfig: parsedMembershipConfig, membershipUserOverrides: parsedMembershipOverrides, pointsPackages: parsedPointsPackages, }; const response = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error("保存失败"); setSiteSettings(payload); toast.success("网站设置保存成功"); mutateSiteSettings(); } catch (error) { toast.error("保存网站设置失败"); } }; const updatePointsPackage = (index: number, updater: (item: PointsPackage) => PointsPackage) => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; if (!list[index]) return prev; list[index] = updater(list[index]); return { ...prev, pointsPackages: list }; }); }; const addPointsPackage = () => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; const nextIndex = list.length + 1; list.push({ id: `package-${Date.now()}`, name: `套餐${nextIndex}`, points: 1000, price: 9.9, currency: "CNY", isEnabled: true, }); return { ...prev, pointsPackages: list }; }); }; const removePointsPackage = (index: number) => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; if (list.length <= 1) { toast.error("至少保留一个积分套餐"); return prev; } list.splice(index, 1); return { ...prev, pointsPackages: list }; }); }; const updateMembershipConfigState = ( updater: (config: ReturnType) => ReturnType ) => { setSiteSettings((prev) => ({ ...prev, membershipConfig: updater(prev.membershipConfig), })); }; const updateMembershipOverridesState = ( updater: ( overrides: ReturnType ) => ReturnType ) => { setSiteSettings((prev) => ({ ...prev, membershipUserOverrides: updater(prev.membershipUserOverrides), })); }; const updateMembershipPlan = ( index: number, updater: (plan: MembershipPlanConfig) => MembershipPlanConfig ) => { updateMembershipConfigState((config) => { const plans = [...config.plans]; if (!plans[index]) return config; plans[index] = updater(plans[index]); return { ...config, plans }; }); }; const addMembershipPlan = () => { updateMembershipConfigState((config) => { const nextIndex = config.plans.length + 1; const id = `plan-${Date.now()}`; const plans = [ ...config.plans, { id, name: `套餐${nextIndex}`, monthlyPrice: 0, annualPrice: 0, monthlyCredits: 0, dailyFreeCredits: 0, features: [], }, ]; return { ...config, plans, defaultPlanId: config.defaultPlanId || id, }; }); }; const removeMembershipPlan = (index: number) => { updateMembershipConfigState((config) => { if (config.plans.length <= 1) { toast.error("至少保留一个会员套餐"); return config; } const target = config.plans[index]; if (!target) return config; const plans = [...config.plans]; plans.splice(index, 1); const nextDefaultPlanId = config.defaultPlanId === target.id ? plans[0]?.id || config.defaultPlanId : config.defaultPlanId; return { ...config, plans, defaultPlanId: nextDefaultPlanId, }; }); }; const updateMembershipFaq = ( index: number, updater: (faq: MembershipFaqItem) => MembershipFaqItem ) => { updateMembershipConfigState((config) => { const faqs = [...config.faqs]; if (!faqs[index]) return config; faqs[index] = updater(faqs[index]); return { ...config, faqs }; }); }; const addMembershipFaq = () => { updateMembershipConfigState((config) => ({ ...config, faqs: [...config.faqs, { question: "", answer: "" }], })); }; const removeMembershipFaq = (index: number) => { updateMembershipConfigState((config) => { if (config.faqs.length <= 1) { toast.error("至少保留一条 FAQ"); return config; } const faqs = [...config.faqs]; faqs.splice(index, 1); return { ...config, faqs }; }); }; const addMembershipOverride = (kind: "id" | "email") => { const key = kind === "id" ? newOverrideUserId.trim() : newOverrideEmail.trim().toLowerCase(); const planId = newOverridePlanId.trim(); if (!key || !planId) { toast.error("请先填写标识并选择套餐"); return; } updateMembershipOverridesState((overrides) => { if (kind === "id") { return { ...overrides, userPlansById: { ...overrides.userPlansById, [key]: planId, }, }; } return { ...overrides, userPlansByEmail: { ...overrides.userPlansByEmail, [key]: planId, }, }; }); if (kind === "id") { setNewOverrideUserId(""); } else { setNewOverrideEmail(""); } }; const removeMembershipOverride = (kind: "id" | "email", key: string) => { updateMembershipOverridesState((overrides) => { if (kind === "id") { const next = { ...overrides.userPlansById }; delete next[key]; return { ...overrides, userPlansById: next }; } const next = { ...overrides.userPlansByEmail }; delete next[key]; return { ...overrides, userPlansByEmail: next }; }); }; // --- 抠图API配置逻辑 --- const handleSaveRmbgConfig = async () => { try { // 根据选择的提供商验证配置 if (rmbgConfig.provider === "rmbg" && !rmbgConfig.apiUrl.trim()) { toast.error("请填写RMBG API地址"); return; } if (rmbgConfig.provider === "photoroom" && !rmbgConfig.photoroomApiKey.trim()) { toast.error("请填写PhotoRoom API密钥"); return; } if (rmbgConfig.provider === "aoscdn" && !rmbgConfig.aosApiKey.trim()) { toast.error("请填写AOS API密钥"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "rmbg_api_config", value: { provider: rmbgConfig.provider, apiUrl: rmbgConfig.apiUrl.trim(), apiKey: rmbgConfig.apiKey.trim() || undefined, photoroomApiKey: rmbgConfig.photoroomApiKey.trim() || undefined, aosApiKey: rmbgConfig.aosApiKey.trim() || undefined, aosApiBaseUrl: rmbgConfig.aosApiBaseUrl.trim() || "https://techsz.aoscdn.com", aosForegroundType: rmbgConfig.aosForegroundType === "auto" ? "" : rmbgConfig.aosForegroundType, aosCrop: rmbgConfig.aosCrop ? 1 : 0, aosFormat: rmbgConfig.aosFormat, }, }), }); 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配置失败"); } }; // --- 去水印API配置逻辑 --- const handleSaveWatermarkRemoveConfig = async () => { try { if (!watermarkRemoveConfig.apiKey.trim()) { toast.error("请输入PicWish API密钥"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "watermark_remove_config", value: { apiKey: watermarkRemoveConfig.apiKey.trim(), }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("去水印API配置保存成功"); } catch (error) { toast.error("保存去水印API配置失败"); } }; // --- 角度编辑API配置逻辑 --- const handleSaveAngleEditConfig = async () => { try { if (angleEditConfig.enabled && !angleEditConfig.falApiKey.trim()) { toast.error("启用角度编辑功能需要配置 fal.ai API Key"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "angle_edit_config", value: { enabled: angleEditConfig.enabled, falApiKey: angleEditConfig.falApiKey.trim(), loraScale: angleEditConfig.loraScale, inferenceSteps: angleEditConfig.inferenceSteps, guidanceScale: angleEditConfig.guidanceScale, maxImageSize: angleEditConfig.maxImageSize, timeout: angleEditConfig.timeout, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("角度编辑API配置保存成功"); } catch (error) { toast.error("保存角度编辑API配置失败"); } }; // --- 实时画布API配置逻辑 --- const handleSaveRealtimeCanvasConfig = async () => { try { const bflApiKey = realtimeCanvasConfig.bflApiKey.trim(); const bflBaseUrl = realtimeCanvasConfig.bflBaseUrl.trim() || DEFAULT_BFL_BASE_URL; const bflModel = realtimeCanvasConfig.bflModel.trim() || DEFAULT_BFL_REALTIME_MODEL; const fallbackEnabled = realtimeCanvasConfig.fallbackEnabled; const fallbackBflApiKey = realtimeCanvasConfig.fallbackBflApiKey.trim(); const fallbackBflBaseUrl = realtimeCanvasConfig.fallbackBflBaseUrl.trim() || DEFAULT_BFL_BASE_URL; const fallbackBflModel = realtimeCanvasConfig.fallbackBflModel.trim() || DEFAULT_BFL_REALTIME_MODEL; if (realtimeCanvasConfig.enabled && !bflApiKey) { toast.error("启用实时画布功能需要配置 BFL API Key"); return; } if (realtimeCanvasConfig.enabled && fallbackEnabled && !fallbackBflApiKey) { toast.error("启用备用实时服务需要配置备用 BFL API Key"); return; } if (realtimeCanvasConfig.enabled) { const probeResponse = await fetch("/api/admin/realtime/model-capability", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey: bflApiKey, modelId: bflModel, baseUrl: bflBaseUrl, }), }); const probeData = await probeResponse.json().catch(() => null); if (!probeResponse.ok || probeData?.supported === false) { toast.error(probeData?.message || "模型能力探针失败,已阻止保存"); return; } if (fallbackEnabled) { const fallbackProbeResponse = await fetch("/api/admin/realtime/model-capability", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey: fallbackBflApiKey, modelId: fallbackBflModel, baseUrl: fallbackBflBaseUrl, }), }); const fallbackProbeData = await fallbackProbeResponse.json().catch(() => null); if (!fallbackProbeResponse.ok || fallbackProbeData?.supported === false) { toast.error(fallbackProbeData?.message || "备用实时服务探针失败,已阻止保存"); return; } } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "realtime_canvas_config", value: { enabled: realtimeCanvasConfig.enabled, bflApiKey, bflBaseUrl, bflModel, fallbackEnabled, fallbackBflApiKey, fallbackBflBaseUrl, fallbackBflModel, runwareApiKey: bflApiKey, falApiKey: bflApiKey, runwareModel: bflModel, renderMode: realtimeCanvasConfig.renderMode, throttleInterval: Math.min(260, Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100)), maxBuffering: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2)), maxInputResolution: Math.min( 1536, Math.max(512, Number(realtimeCanvasConfig.maxInputResolution) || 1024) ), jpegQuality: Math.min(0.82, Math.max(0.35, Number(realtimeCanvasConfig.jpegQuality) || 0.55)), previewMegapixels: Math.min( 0.8, Math.max(0.2, Number(realtimeCanvasConfig.previewMegapixels) || 0.4) ), detailMegapixels: Math.min( 1.2, Math.max(0.6, Number(realtimeCanvasConfig.detailMegapixels) || 1.0) ), outputJpegQuality: Math.min( 90, Math.max(35, Number(realtimeCanvasConfig.outputJpegQuality) || 62) ), defaults: { steps: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.steps) || 3)), scheduleMu: Math.min(3.5, Math.max(1.5, Number(realtimeCanvasConfig.scheduleMu) || 2.3)), feedback: Math.min(1, Math.max(0.7, Number(realtimeCanvasConfig.feedback) || 0.95)), imageSize: realtimeCanvasConfig.imageSize, }, }, }), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message || "保存失败"); } toast.success("实时画布配置保存成功"); } catch (error) { toast.error(error instanceof Error ? error.message : "保存实时画布配置失败"); } }; // --- 批量处理配置逻辑 --- const handleSaveSceneFusionConfig = async () => { try { if (!sceneFusionConfig.defaultPrompt.trim()) { toast.error("默认提示词不能为空"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "scene_fusion_config", value: { enabled: sceneFusionConfig.enabled, preRmbgEnabled: sceneFusionConfig.preRmbgEnabled, defaultPrompt: sceneFusionConfig.defaultPrompt.trim(), maxScenes: Math.max(1, Math.min(10, sceneFusionConfig.maxScenes || 10)), modelId: sceneFusionConfig.modelId, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("批量处理配置保存成功"); } catch (error) { toast.error("保存批量处理配置失败"); } }; // --- 导航链接逻辑 --- const resetNavLinkForm = () => { setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true, toolboxGroup: 1 }); 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 resetTutorialVideoForm = () => { setTutorialVideoForm({ title: "", videoUrl: "", coverUrl: "", description: "", order: 0, isEnabled: true, }); setEditingTutorialVideo(null); }; const handleSaveTutorialVideo = async () => { try { const method = editingTutorialVideo ? "PUT" : "POST"; const body = editingTutorialVideo ? { ...tutorialVideoForm, id: editingTutorialVideo } : tutorialVideoForm; const response = await fetch("/api/tutorial-videos", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingTutorialVideo ? "视频更新成功" : "视频创建成功"); mutateTutorialVideos(); setShowTutorialVideoDialog(false); resetTutorialVideoForm(); } catch (error) { toast.error("保存视频失败"); } }; const handleDeleteTutorialVideo = async (id: string) => { if (!confirm("确定要删除此视频吗?")) return; try { const response = await fetch(`/api/tutorial-videos?id=${id}`, { method: "DELETE" }); if (!response.ok) throw new Error("删除失败"); toast.success("视频删除成功"); mutateTutorialVideos(); } 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: getProviderDefaultBaseUrl("openai"), apiKey: "", isEnabled: true, }); setEditingProvider(null); }; const handleSaveProvider = async () => { try { const method = editingProvider ? "PUT" : "POST"; const isNewProvider = !editingProvider; const isVolcanoVideo = providerForm.type === 'volcano-video'; const isLemonProvider = providerForm.type === "openai" && isLemonApiBaseUrl(providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type)); const defaultBaseUrl = getProviderDefaultBaseUrl(providerForm.type); const normalizedBaseUrl = providerForm.baseUrl.trim(); let requestData: any = editingProvider ? { ...providerForm, id: editingProvider } : providerForm; if ( defaultBaseUrl && (!normalizedBaseUrl || !isHttpUrlLike(normalizedBaseUrl) || normalizedBaseUrl.includes("@")) ) { requestData.baseUrl = defaultBaseUrl; } else { requestData.baseUrl = normalizedBaseUrl; } 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('预设模型添加失败,请手动点击"快速添加预设模型"按钮'); } } if (isNewProvider && isLemonProvider && providerId) { try { await handleQuickAddLemonModels(providerId); } catch (modelError) { console.error("[Auto Add Lemon Models] Error:", modelError); toast.warning('LemonAPI 预设模型添加失败,请手动点击"快速添加 Gemini 预设模型"按钮'); } } } 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 handleSaveTripo3dConfig = async () => { try { const trimmedName = tripo3dConfig.name.trim() || "Tripo3D"; const trimmedBaseUrl = tripo3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("tripo-3d"); const hasProvider = Boolean(tripo3dConfig.providerId); if (!hasProvider && !tripo3dConfig.apiKey.trim()) { toast.error("请填写 Tripo3D API Key"); return; } const requestData: Record = hasProvider ? { id: tripo3dConfig.providerId, name: trimmedName, type: "tripo-3d", baseUrl: trimmedBaseUrl, isEnabled: tripo3dConfig.isEnabled, } : { name: trimmedName, type: "tripo-3d", baseUrl: trimmedBaseUrl, apiKey: tripo3dConfig.apiKey.trim(), isEnabled: tripo3dConfig.isEnabled, }; if (hasProvider && tripo3dConfig.apiKey.trim()) { requestData.apiKey = tripo3dConfig.apiKey.trim(); } const response = await fetch("/api/admin/ai-providers", { method: hasProvider ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("Tripo3D 配置保存成功"); setTripo3dConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { toast.error("保存 Tripo3D 配置失败"); } }; // 快速添加火山引擎视频预设模型 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 || "添加预设模型失败"); } }; const handleUseLemonProviderPreset = () => { setProviderForm({ id: "", name: LEMON_PROVIDER_PRESET.name, type: LEMON_PROVIDER_PRESET.type, baseUrl: LEMON_PROVIDER_PRESET.baseUrl, apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleQuickAddLemonModels = async (providerId: string) => { try { toast.info("正在添加 LemonAPI Gemini 预设模型..."); let addedCount = 0; let duplicateCount = 0; for (const preset of LEMON_MODEL_PRESETS) { const response = await fetch("/api/admin/ai-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId, modelId: preset.modelId, displayName: preset.displayName, description: preset.description, supportsVision: preset.supportsVision, supportsImageGen: preset.supportsImageGen, isEnabled: true, isHidden: false, isDefault: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: preset.isMultimodal, sortOrder: 0, }), }); if (response.ok) { addedCount += 1; continue; } duplicateCount += 1; } if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个 LemonAPI 预设模型`); mutateModels(); } if (addedCount === 0 && duplicateCount > 0) { toast.warning("LemonAPI 预设模型可能已存在"); } } catch (error) { console.error("[Quick Add Lemon Models] Error:", error); toast.error("添加 LemonAPI 预设模型失败"); } }; // --- AI 模型逻辑 --- const resetModelForm = () => { setModelForm({ id: "", providerId: "", modelId: "", displayName: "", avatarUrl: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isHidden: false, isDefault: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", fallbackModelId2: "", excludeFromFailover: false, imageGenConfig: { maxImages: 2, supportsSize: false, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, supportsSeed: false, creditCostPerImage: 0, creditCostPerVideo: 0, enableLoadingPlaceholder: true, loadingColor: "purple", }, }); setEditingModel(null); }; const resizeImageToSquare = (file: File, size = 400): Promise => new Promise((resolve, reject) => { const image = new Image(); const objectUrl = URL.createObjectURL(file); image.onload = () => { try { const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const context = canvas.getContext("2d"); if (!context) { URL.revokeObjectURL(objectUrl); reject(new Error("无法处理图片")); return; } const minSide = Math.min(image.width, image.height); const sx = (image.width - minSide) / 2; const sy = (image.height - minSide) / 2; context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.drawImage(image, sx, sy, minSide, minSide, 0, 0, size, size); canvas.toBlob( (blob) => { URL.revokeObjectURL(objectUrl); if (!blob) { reject(new Error("头像压缩失败")); return; } resolve( new File([blob], `model-avatar-${Date.now()}.png`, { type: "image/png", }) ); }, "image/png", 0.95 ); } catch (error) { URL.revokeObjectURL(objectUrl); reject(error); } }; image.onerror = () => { URL.revokeObjectURL(objectUrl); reject(new Error("图片读取失败")); }; image.src = objectUrl; }); const handleModelAvatarUpload = async (file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } try { setModelAvatarUploading(true); const resizedFile = await resizeImageToSquare(file, 400); const formData = new FormData(); formData.append("file", resizedFile); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setModelForm((prev) => ({ ...prev, avatarUrl: result.url })); toast.success("模型头像上传成功"); } catch (error: any) { toast.error(error?.message || "模型头像上传失败"); } finally { setModelAvatarUploading(false); } }; const handleContactQrUpload = async (file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } try { setContactQrUploading(true); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setSiteSettings((prev) => ({ ...prev, contactConfig: { ...prev.contactConfig, wechatQrUrl: result.url, }, })); toast.success("微信二维码上传成功"); } catch (error: any) { toast.error(error?.message || "微信二维码上传失败"); } finally { setContactQrUploading(false); } }; 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: any = editingStorage ? { ...storageForm, id: editingStorage } : { ...storageForm }; const normalizeUrl = (value?: string) => { if (!value) return ""; return value.trim().replace(/\/+$/, ""); }; const endpoint = normalizeUrl(body.endpoint); body.endpoint = /aliyuncs\.com/i.test(endpoint) ? `https://${endpoint.replace(/^https?:\/\//i, "")}` : endpoint; body.region = String(body.region || "").trim(); body.bucket = String(body.bucket || "").trim(); body.accessKeyId = String(body.accessKeyId || "").trim(); body.secretAccessKey = String(body.secretAccessKey || "").trim(); body.publicUrl = normalizeUrl(body.publicUrl); // 兼容处理:如果是阿里云 OSS,转换为 s3 类型提交 if (body.type === "aliyun-oss") { body.type = "s3"; } // 编辑状态下,留空密钥代表不修改 if (editingStorage) { if (!body.accessKeyId) delete body.accessKeyId; if (!body.secretAccessKey) delete body.secretAccessKey; } 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.isHighVip ? "高级VIP" : "普通"}
月度出图限额: {user.monthlyImageLimit ? `${user.monthlyImageLimit} 张/月` : "无限制"}
剩余积分: {Number(user.pointsBalance || 0).toLocaleString()} | 累计赠送:{" "} {Number(user.pointsGiftedTotal || 0).toLocaleString()}
{/* 配额设置 */}
{ 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 }); } } }} /> 张
setGiftPointsDraftByUser((prev) => ({ ...prev, [user.id]: e.target.value, })) } onKeyDown={(e) => { if (e.key === "Enter") { void handleGiftPoints(user.id); } }} />
setDeductPointsDraftByUser((prev) => ({ ...prev, [user.id]: e.target.value, })) } onKeyDown={(e) => { if (e.key === "Enter") { void handleDeductPoints(user.id); } }} />
))}
{/* 分页控件 */} {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?.pagination.totalImages !== undefined || imagesData?.pagination.totalVideos !== undefined ? `(图片 ${imagesData?.pagination.totalImages || 0} / 视频 ${imagesData?.pagination.totalVideos || 0})` : ""}
{/* 图片网格 */}
{imagesData?.images?.map((img) => (
setSelectedImage(img)} >
{img.recordType === "video" ? (
{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?.recordType === "video" ? "视频详情" : "图片详情"} {selectedImage?.recordType !== "video" && selectedImage?.isFeatured && ( 精选 )} {selectedImage && (
{selectedImage.recordType === "video" ? (

{selectedImage.userName || selectedImage.userEmail}

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

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

{selectedImage.imageSize || "未知"}

{analyticsData?.modelDetailStats?.find( (s) => s.modelId === selectedImage.modelId )?.modelName || selectedImage.modelId || "未知"}

{selectedImage.recordType !== "video" ? (

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

) : null} {selectedImage.recordType !== "video" && selectedImage.isFeatured && selectedImage.featuredAt && (

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

)}

{selectedImage.prompt || "无"}

{selectedImage.recordType !== "video" ? (
) : null}
)}
{/* 🆕 数据分析 */} {/* 概览卡片 */}

{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", "doubao-video": "bg-red-400", "fal-video": "bg-indigo-500", "ruxa-video": "bg-violet-500", "alibaba": "bg-blue-500", "alibaba-image": "bg-amber-500", "ruxa-image": "bg-emerald-500", "fal-image": "bg-sky-500", "openai-image": "bg-green-600", "midjourney": "bg-fuchsia-600", "tripo-3d": "bg-teal-500", "openai": "bg-green-500", "zhipu": "bg-lime-500", "volcano-multimodal": "bg-fuchsia-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) && (
暂无数据
)}
{/* 网站设置 */} 网站基本设置 配置网站名称、LOGO、功能开关等
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" />
设置新用户首次访问时的默认主题模式
新版项目首页会启用全新的项目入口、文件列表与精选展示。
{ const parsed = e.target.value .split(",") .map((item) => item.trim()) .filter(Boolean); const unique = Array.from(new Set(parsed)); setSiteSettings({ ...siteSettings, surpriseCategories: unique.length > 0 ? unique : ["全部", "精选"], }); }} placeholder="全部, 精选, 海报, 电商" />
新版首页“精选”模块会按这些分类显示筛选标签。
首页 / 无限画布对话框文案配置
setSiteSettings({ ...siteSettings, lovartComposerPrefix: e.target.value.slice(0, 40), }) } placeholder="让Buyi-ai" />
该前缀会固定显示在打字文案前面,切换文案时不删除。