diff --git a/backend/python/app/api/admin/settings.py b/backend/python/app/api/admin/settings.py index f9436d2..51cb3c0 100644 --- a/backend/python/app/api/admin/settings.py +++ b/backend/python/app/api/admin/settings.py @@ -267,6 +267,77 @@ async def update_sms_config( return {"message": "短信配置已更新"} + +# ============================================================ +# Logo 上传 +# ============================================================ + +from fastapi import UploadFile, File + +@router.post("/logo/upload") +async def upload_logo( + file: UploadFile = File(...), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """上传平台Logo,保存到本地data目录,返回访问URL""" + import os as _os + import shutil as _shutil + + # Validate file type + allowed_types = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'] + if file.content_type not in allowed_types: + raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file.content_type},仅支持 PNG/JPG/SVG/WEBP") + + # Validate size (max 2MB) + contents = await file.read() + if len(contents) > 2 * 1024 * 1024: + raise HTTPException(status_code=400, detail="文件大小不能超过 2MB") + + # Save to /app/data/uploads/logos/ + upload_dir = "/app/data/uploads/logos" + _os.makedirs(upload_dir, exist_ok=True) + + # Generate unique filename + ext = file.filename.split('.')[-1] if '.' in (file.filename or '') else 'png' + filename = f"{uuid.uuid4()}.{ext}" + filepath = _os.path.join(upload_dir, filename) + + with open(filepath, 'wb') as f: + f.write(contents) + + # Build URL + logo_url = f"/data/uploads/logos/{filename}" + + # Update site setting + existing = db.query(SiteSetting).filter(SiteSetting.key == "logo").first() + if existing: + existing.value = logo_url + existing.updated_at = datetime.now() + else: + new_setting = SiteSetting( + id=str(uuid.uuid4()), + key="logo", + value=logo_url, + ) + db.add(new_setting) + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="upload_logo", + target_type="site_settings", + detail=f"上传Logo: {filename}", + ) + + return { + "code": 0, + "data": {"url": logo_url, "filename": filename}, + "message": "Logo 上传成功", + } + + # ============================================================ # 邮件测试 # ============================================================ diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 96cae91..4408c42 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -277,6 +277,7 @@ export const adminAPI = { getStats: () => get('/admin/dashboard/stats'), getRecentLogs: () => get('/admin/dashboard/recent-logs'), getRecentAudit: () => get('/admin/dashboard/recent-audit'), + getDailyStats: (days?: number) => get<{ days: number; data: Array<{ date: string; tokens: number }> }>(`/admin/dashboard/daily-stats${days ? `?days=${days}` : ''}`), // 用户管理 getUsers: (params?: any) => { const sp = new URLSearchParams(); @@ -588,8 +589,16 @@ export const adminAPI = { getAIAppTemplates: () => get('/admin/apps/templates'), createAIAppTemplate: (data: any) => post('/admin/apps/templates', data), createAIAppFromTemplate: (templateId: string, data: any) => post('/admin/apps/apps/from-template/' + templateId, data), - getAIAppCategories: () => get('/admin/apps/categories') - + getAIAppCategories: () => get('/admin/apps/categories'), + // Logo 上传 + uploadLogo: (formData: FormData) => { + const token = localStorage.getItem('access_token'); + return fetch(`/app/api/admin/settings/logo/upload`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData, + }).then(res => res.json()); + }, }; @@ -662,4 +671,5 @@ export const evolutionAPI = { return get(`/evolution/reports${qs}`); }, getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), + }; diff --git a/frontend/src/components/layout/AdminLayout.css b/frontend/src/components/layout/AdminLayout.css index ee28297..3256590 100644 --- a/frontend/src/components/layout/AdminLayout.css +++ b/frontend/src/components/layout/AdminLayout.css @@ -896,6 +896,66 @@ } /* ========== Responsive ========== */ + + +/* Modal sub-components */ +.admin-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.admin-modal-header h3 { + margin: 0 !important; +} + +.admin-modal-close { + background: none; + border: none; + color: #8892B0; + cursor: pointer; + padding: 4px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.admin-modal-close:hover { + color: #E8ECF4; + background: rgba(255, 255, 255, 0.06); +} + +.admin-modal-body { + margin-bottom: 20px; +} + +.admin-modal-footer { + display: flex; + gap: 8px; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +/* Spinner animation for logo upload */ +.spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(108, 99, 255, 0.2); + border-top-color: #6C63FF; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + @media (max-width: 768px) { .admin-sidebar { position: fixed; diff --git a/frontend/src/pages/admin/SiteSettings.tsx b/frontend/src/pages/admin/SiteSettings.tsx index 355bd20..db417fe 100644 --- a/frontend/src/pages/admin/SiteSettings.tsx +++ b/frontend/src/pages/admin/SiteSettings.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { adminAPI } from '@/api'; const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( @@ -7,7 +7,18 @@ const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ child ); +type TabKey = 'basic' | 'logo' | 'email' | 'sms' | 'registration'; + +const TABS: { key: TabKey; label: string; icon: React.ReactNode }[] = [ + { key: 'basic', label: '基础设置', icon: <> }, + { key: 'logo', label: 'Logo 设置', icon: <> }, + { key: 'email', label: '邮件配置', icon: <> }, + { key: 'sms', label: '短信配置', icon: <> }, + { key: 'registration', label: '注册设置', icon: <> }, +]; + const SiteSettings: React.FC = () => { + const [activeTab, setActiveTab] = useState('basic'); const [brand, setBrand] = useState>({ logo: '', siteName: '', announcement: '', icpNumber: '', contactPhone: '', }); @@ -17,6 +28,13 @@ const SiteSettings: React.FC = () => { const [sms, setSms] = useState({ provider: '', api_key: '', api_secret: '', sign_name: '', template_code: '', }); + const [registration, setRegistration] = useState({ + allow_register: true, + require_email_verify: false, + require_phone_verify: false, + default_role: 'user', + welcome_message: '', + }); const [savedMsg, setSavedMsg] = useState(null); const [loading, setLoading] = useState(true); const [testResult, setTestResult] = useState(null); @@ -25,6 +43,9 @@ const SiteSettings: React.FC = () => { const [emailTestSubject, setEmailTestSubject] = useState(''); const [emailTestBody, setEmailTestBody] = useState(''); const [emailGenerating, setEmailGenerating] = useState(false); + const [logoUploading, setLogoUploading] = useState(false); + const [logoPreview, setLogoPreview] = useState(''); + const logoInputRef = useRef(null); // AI生成测试邮件内容 const handleAIGenerateContent = async () => { @@ -35,7 +56,6 @@ const SiteSettings: React.FC = () => { setEmailTestSubject(data.subject || ''); setEmailTestBody(data.body || ''); } catch (e) { - // 降级:使用客户端预设内容 const subjects = [ '【测试】AIFlow邮件服务配置验证', 'AIFlow平台 - 邮件投递测试', @@ -53,23 +73,25 @@ const SiteSettings: React.FC = () => { } }; - // 加载真实配置 + // Load all configs useEffect(() => { async function loadConfig() { try { - const [siteRes, emailRes, smsRes] = await Promise.all([ + const [siteRes, emailRes, smsRes, regRes] = await Promise.all([ adminAPI.getSiteSettings(), adminAPI.getEmailConfig(), adminAPI.getSmsConfig(), + adminAPI.getSiteSettings().then((r: any) => ({ data: r?.data?.registration || {} })).catch(() => ({ data: {} })), ]); if (siteRes?.data) { setBrand({ - logo: siteRes.data.logo || '', + logo: siteRes.data.logo || siteRes.data.site_logo || '', siteName: siteRes.data.site_name || siteRes.data.siteName || '', announcement: siteRes.data.announcement || '', icpNumber: siteRes.data.icp_number || siteRes.data.icpNumber || '', contactPhone: siteRes.data.contact_phone || siteRes.data.contactPhone || '', }); + setLogoPreview(siteRes.data.logo || siteRes.data.site_logo || ''); } if (emailRes?.data) { setMail({ @@ -90,6 +112,19 @@ const SiteSettings: React.FC = () => { template_code: smsRes.data.template_code || '', }); } + // Try to load registration settings from site settings + try { + const regData = (siteRes as any)?.data; + if (regData?.allow_register !== undefined) { + setRegistration({ + allow_register: regData.allow_register === true || regData.allow_register === 'true', + require_email_verify: regData.require_email_verify === true || regData.require_email_verify === 'true', + require_phone_verify: regData.require_phone_verify === true || regData.require_phone_verify === 'true', + default_role: regData.default_role || 'user', + welcome_message: regData.welcome_message || '', + }); + } + } catch {} } catch (e) { console.error('加载配置失败:', e); } finally { @@ -109,23 +144,50 @@ const SiteSettings: React.FC = () => { setTimeout(() => setTestResult(null), 5000); }; - // 保存品牌设置 + // Logo upload handler + const handleLogoUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Preview locally + const reader = new FileReader(); + reader.onload = (ev) => setLogoPreview(ev.target?.result as string); + reader.readAsDataURL(file); + + // Upload to server + setLogoUploading(true); + try { + const formData = new FormData(); + formData.append('file', file); + const res = await adminAPI.uploadLogo(formData); + const data = (res as any)?.data || res; + const url = data?.url || data?.logo_url || ''; + if (url) { + setBrand({ ...brand, logo: url }); + showSaved('Logo'); + } + } catch (e: any) { + showTestResult('Logo 上传失败: ' + (e.detail || e.message || '未知错误')); + } finally { + setLogoUploading(false); + } + }; + const saveSiteSettings = async () => { try { await adminAPI.updateSiteSettings({ - logo: brand.logo, + logo: brand.logo || logoPreview, site_name: brand.siteName, announcement: brand.announcement, icp_number: brand.icpNumber, contact_phone: brand.contactPhone, }); - showSaved('品牌设置'); + showSaved('基础设置'); } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); + showTestResult('保存失败: ' + (e.detail || e.message)); } }; - // 保存邮件配置 const saveEmailConfig = async () => { try { await adminAPI.updateEmailConfig({ @@ -138,11 +200,10 @@ const SiteSettings: React.FC = () => { }); showSaved('邮件配置'); } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); + showTestResult('保存失败: ' + (e.detail || e.message)); } }; - // 保存短信配置 const saveSmsConfig = async () => { try { await adminAPI.updateSmsConfig({ @@ -154,18 +215,31 @@ const SiteSettings: React.FC = () => { }); showSaved('短信配置'); } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); + showTestResult('保存失败: ' + (e.detail || e.message)); + } + }; + + const saveRegistrationSettings = async () => { + try { + await adminAPI.updateSiteSettings({ + allow_register: String(registration.allow_register), + require_email_verify: String(registration.require_email_verify), + require_phone_verify: String(registration.require_phone_verify), + default_role: registration.default_role, + welcome_message: registration.welcome_message, + }); + showSaved('注册设置'); + } catch (e: any) { + showTestResult('保存失败: ' + (e.detail || e.message)); } }; - // 发送测试邮件 - 先保存配置再发送 const sendTestEmail = async () => { if (!emailTestTo.trim()) { showTestResult('请输入收件邮箱地址'); return; } try { - // 测试前先保存邮件配置到数据库,确保使用最新配置 await adminAPI.updateEmailConfig({ smtp_host: mail.smtp_host, smtp_port: mail.smtp_port, @@ -193,7 +267,6 @@ const SiteSettings: React.FC = () => { setShowEmailTest(true); }; - // 发送测试短信 const sendTestSms = async () => { try { const res = await adminAPI.testSms(); @@ -204,13 +277,390 @@ const SiteSettings: React.FC = () => { }; if (loading) { - return
加载中...
; + return
加载中...
; } + const FormGroup = ({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) => ( +
+ + {children} +
+ ); + + const FormRow = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); + + const FormActions = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); + + // ========== Render Tab Content ========== + const renderTabContent = () => { + switch (activeTab) { + // ── 基础设置 ── + case 'basic': + return ( +
+
+

基础信息

+
+ + + setBrand({ ...brand, siteName: e.target.value })} + placeholder="例如: Qihu-AIFlow" /> + + + { setBrand({ ...brand, logo: e.target.value }); setLogoPreview(e.target.value); }} + placeholder="https://... 或上传后自动填入" /> + + + + +