From 4a5c189dbd31dac11d64fcbb8fc1963e1fd39186 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 24 Apr 2026 17:20:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=EF=BC=8C=E9=87=8D=E6=9E=84=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 4 +- src/api/ai.ts | 2 + src/components/LoginCharacters.tsx | 253 +++++++++++++++++++++++++++++ src/components/PageUI.tsx | 4 +- src/index.css | 171 +++++++++++-------- src/layouts/AdminLayout.tsx | 84 +++++----- src/pages/Dashboard.tsx | 20 +-- src/pages/LoginPage.tsx | 234 ++++++++++++++------------ src/pages/system/AiConfig.tsx | 34 +++- 9 files changed, 582 insertions(+), 224 deletions(-) create mode 100644 src/components/LoginCharacters.tsx diff --git a/index.html b/index.html index 8ab0c6e..c892c5b 100644 --- a/index.html +++ b/index.html @@ -3,9 +3,9 @@ - + - 植趣ZeeQ - 后台管理系统 + Sundynix Console - 光衍矩阵 diff --git a/src/api/ai.ts b/src/api/ai.ts index d8972ba..ebaddd6 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -20,6 +20,8 @@ export interface SysAiConfig { embeddingApiUrl: string embeddingApiKey: string embeddingModelName: string + // 用量限制 + dailyQueryLimit: number // 基础字段 createdAt?: string createdAtStr?: string diff --git a/src/components/LoginCharacters.tsx b/src/components/LoginCharacters.tsx new file mode 100644 index 0000000..1d07990 --- /dev/null +++ b/src/components/LoginCharacters.tsx @@ -0,0 +1,253 @@ +import { useState, useEffect, useRef } from 'react'; + +interface PupilProps { + size?: number; + maxDistance?: number; + pupilColor?: string; + forceLookX?: number; + forceLookY?: number; +} + +const Pupil = ({ + size = 12, maxDistance = 5, pupilColor = 'black', + forceLookX, forceLookY, +}: PupilProps) => { + const [mouseX, setMouseX] = useState(0); + const [mouseY, setMouseY] = useState(0); + const ref = useRef(null); + + useEffect(() => { + const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); }; + window.addEventListener('mousemove', h); + return () => window.removeEventListener('mousemove', h); + }, []); + + const calc = () => { + if (!ref.current) return { x: 0, y: 0 }; + if (forceLookX !== undefined && forceLookY !== undefined) return { x: forceLookX, y: forceLookY }; + const r = ref.current.getBoundingClientRect(); + const cx = r.left + r.width / 2, cy = r.top + r.height / 2; + const dx = mouseX - cx, dy = mouseY - cy; + const dist = Math.min(Math.sqrt(dx * dx + dy * dy), maxDistance); + const a = Math.atan2(dy, dx); + return { x: Math.cos(a) * dist, y: Math.sin(a) * dist }; + }; + const p = calc(); + return ( +
+ ); +}; + +interface EyeBallProps { + size?: number; pupilSize?: number; maxDistance?: number; + eyeColor?: string; pupilColor?: string; isBlinking?: boolean; + forceLookX?: number; forceLookY?: number; +} + +const EyeBall = ({ + size = 48, pupilSize = 16, maxDistance = 10, + eyeColor = 'white', pupilColor = 'black', isBlinking = false, + forceLookX, forceLookY, +}: EyeBallProps) => { + const [mouseX, setMouseX] = useState(0); + const [mouseY, setMouseY] = useState(0); + const ref = useRef(null); + + useEffect(() => { + const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); }; + window.addEventListener('mousemove', h); + return () => window.removeEventListener('mousemove', h); + }, []); + + const calc = () => { + if (!ref.current) return { x: 0, y: 0 }; + if (forceLookX !== undefined && forceLookY !== undefined) return { x: forceLookX, y: forceLookY }; + const r = ref.current.getBoundingClientRect(); + const cx = r.left + r.width / 2, cy = r.top + r.height / 2; + const dx = mouseX - cx, dy = mouseY - cy; + const dist = Math.min(Math.sqrt(dx * dx + dy * dy), maxDistance); + const a = Math.atan2(dy, dx); + return { x: Math.cos(a) * dist, y: Math.sin(a) * dist }; + }; + const p = calc(); + return ( +
+ {!isBlinking && ( +
+ )} +
+ ); +}; + +interface Props { isTyping?: boolean; showPassword?: boolean; passwordLength?: number; } + +export default function LoginCharacters({ isTyping = false, showPassword = false, passwordLength = 0 }: Props) { + const [mouseX, setMouseX] = useState(0); + const [mouseY, setMouseY] = useState(0); + const [isPrimaryBlink, setIsPrimaryBlink] = useState(false); + const [isDarkBlink, setIsDarkBlink] = useState(false); + const [isLooking, setIsLooking] = useState(false); + const [isPrimaryPeek, setIsPrimaryPeek] = useState(false); + const primaryRef = useRef(null); + const darkRef = useRef(null); + const yellowRef = useRef(null); + const orangeRef = useRef(null); + + useEffect(() => { + const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); }; + window.addEventListener('mousemove', h); + return () => window.removeEventListener('mousemove', h); + }, []); + + // Primary blink + useEffect(() => { + const schedule = (): ReturnType => setTimeout(() => { + setIsPrimaryBlink(true); + setTimeout(() => { setIsPrimaryBlink(false); schedule(); }, 150); + }, Math.random() * 4000 + 3000); + const t = schedule(); return () => clearTimeout(t); + }, []); + + // Dark blink + useEffect(() => { + const schedule = (): ReturnType => setTimeout(() => { + setIsDarkBlink(true); + setTimeout(() => { setIsDarkBlink(false); schedule(); }, 150); + }, Math.random() * 4000 + 3000); + const t = schedule(); return () => clearTimeout(t); + }, []); + + // Look at each other when typing + useEffect(() => { + if (isTyping) { setIsLooking(true); const t = setTimeout(() => setIsLooking(false), 800); return () => clearTimeout(t); } + else setIsLooking(false); + }, [isTyping]); + + // Primary peek when password visible + useEffect(() => { + if (passwordLength > 0 && showPassword) { + const t = setTimeout(() => { + setIsPrimaryPeek(true); + setTimeout(() => setIsPrimaryPeek(false), 800); + }, Math.random() * 3000 + 2000); + return () => clearTimeout(t); + } else setIsPrimaryPeek(false); + }, [passwordLength, showPassword, isPrimaryPeek]); + + const calcPos = (ref: React.RefObject) => { + if (!ref.current) return { faceX: 0, faceY: 0, bodySkew: 0 }; + const r = ref.current.getBoundingClientRect(); + const cx = r.left + r.width / 2, cy = r.top + r.height / 3; + const dx = mouseX - cx, dy = mouseY - cy; + return { + faceX: Math.max(-15, Math.min(15, dx / 20)), + faceY: Math.max(-10, Math.min(10, dy / 30)), + bodySkew: Math.max(-6, Math.min(6, -dx / 120)), + }; + }; + + const pp = calcPos(primaryRef), bp = calcPos(darkRef); + const yp = calcPos(yellowRef), op = calcPos(orangeRef); + const hiding = passwordLength > 0 && !showPassword; + const showing = passwordLength > 0 && showPassword; + + return ( +
+ {/* Primary tall - back */} +
+
+ + +
+
+ + {/* Dark - middle */} +
+
+ + +
+
+ + {/* Orange semi-circle - front left */} +
+
+ + +
+
+ + {/* Yellow rounded - front right */} +
+
+ + +
+ {/* mouth */} +
+
+
+ ); +} diff --git a/src/components/PageUI.tsx b/src/components/PageUI.tsx index f2f4e6f..e3ffb6b 100644 --- a/src/components/PageUI.tsx +++ b/src/components/PageUI.tsx @@ -73,7 +73,7 @@ interface SearchBarProps { export function SearchBar({ value, onChange, onSearch, placeholder = '搜索...', extra }: SearchBarProps) { return ( -
+
+
{loading ? (
diff --git a/src/index.css b/src/index.css index 55f7559..2b7d3db 100644 --- a/src/index.css +++ b/src/index.css @@ -1,72 +1,78 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap'); @import "tailwindcss"; @theme { - /* Modern green-based color palette */ - --color-background: oklch(0.985 0.002 120); - --color-foreground: oklch(0.18 0.01 120); - --color-card: oklch(1 0 0); - --color-card-foreground: oklch(0.18 0.01 120); + /* Nature-inspired Light Theme - Refined Premium */ + --color-background: oklch(0.98 0.01 145); /* Very soft cool mint/gray */ + --color-foreground: oklch(0.15 0.02 145); + --color-card: oklch(1 0 0); /* Pure white cards */ + --color-card-foreground: oklch(0.15 0.02 145); --color-popover: oklch(1 0 0); - --color-popover-foreground: oklch(0.18 0.01 120); - --color-primary: oklch(0.52 0.14 150); - --color-primary-foreground: oklch(0.99 0 0); - --color-secondary: oklch(0.965 0.015 150); - --color-secondary-foreground: oklch(0.30 0.06 150); - --color-muted: oklch(0.965 0.005 120); - --color-muted-foreground: oklch(0.48 0.01 120); - --color-accent: oklch(0.96 0.02 150); - --color-accent-foreground: oklch(0.30 0.06 150); + --color-popover-foreground: oklch(0.15 0.02 145); + --color-primary: oklch(0.55 0.12 145); /* Emerald */ + --color-primary-foreground: oklch(1 0 0); + --color-secondary: oklch(0.96 0.01 145); + --color-secondary-foreground: oklch(0.35 0.08 145); + --color-muted: oklch(0.96 0.01 145); + --color-muted-foreground: oklch(0.55 0.02 145); + --color-accent: oklch(0.96 0.01 145); + --color-accent-foreground: oklch(0.35 0.08 145); --color-destructive: oklch(0.58 0.18 25); - --color-destructive-foreground: oklch(0.99 0 0); - --color-border: oklch(0.92 0.005 120); - --color-input: oklch(0.92 0.005 120); - --color-ring: oklch(0.52 0.14 150); + --color-destructive-foreground: oklch(1 0 0); + --color-border: oklch(0.92 0.01 145); /* Softer borders */ + --color-input: oklch(0.92 0.01 145); + --color-ring: oklch(0.55 0.12 145); /* Sidebar colors */ - --color-sidebar-background: oklch(0.99 0.002 120); - --color-sidebar-foreground: oklch(0.40 0.01 120); - --color-sidebar-primary: oklch(0.52 0.14 150); + --color-sidebar-background: oklch(0.985 0.005 100 / 0.8); + --color-sidebar-foreground: oklch(0.30 0.02 140); + --color-sidebar-primary: oklch(0.55 0.12 145); --color-sidebar-primary-foreground: oklch(0.99 0 0); - --color-sidebar-accent: oklch(0.965 0.02 150); - --color-sidebar-accent-foreground: oklch(0.25 0.06 150); - --color-sidebar-border: oklch(0.94 0.005 120); - --color-sidebar-ring: oklch(0.52 0.14 150); + --color-sidebar-accent: oklch(0.95 0.02 135); + --color-sidebar-accent-foreground: oklch(0.35 0.08 145); + --color-sidebar-border: oklch(0.90 0.01 110 / 0.3); + --color-sidebar-ring: oklch(0.55 0.12 145); - /* Refined radius for smoother look */ - --radius-lg: 0.875rem; - --radius-md: 0.625rem; - --radius-sm: 0.375rem; + /* Rounder, softer radii for premium feel */ + --radius-lg: 1.5rem; + --radius-md: 1rem; + --radius-sm: 0.6rem; + + /* Custom shadows - more diffused and elegant */ + --shadow-soft: 0 4px 20px -2px oklch(0 0 0 / 0.05), 0 0 3px oklch(0 0 0 / 0.02); + --shadow-soft-lg: 0 10px 40px -4px oklch(0 0 0 / 0.08), 0 0 4px oklch(0 0 0 / 0.03); } .dark { - --color-background: oklch(0.11 0.01 120); - --color-foreground: oklch(0.96 0.005 120); - --color-card: oklch(0.14 0.01 120); - --color-card-foreground: oklch(0.96 0.005 120); - --color-popover: oklch(0.14 0.01 120); - --color-popover-foreground: oklch(0.96 0.005 120); - --color-primary: oklch(0.62 0.14 150); - --color-primary-foreground: oklch(0.10 0 0); - --color-secondary: oklch(0.20 0.02 150); - --color-secondary-foreground: oklch(0.88 0.02 150); - --color-muted: oklch(0.20 0.01 120); - --color-muted-foreground: oklch(0.62 0.01 120); - --color-accent: oklch(0.20 0.02 150); - --color-accent-foreground: oklch(0.88 0.02 150); + /* Midnight Forest Dark Theme */ + --color-background: oklch(0.18 0.02 145); + --color-foreground: oklch(0.96 0.01 130); + --color-card: oklch(0.22 0.02 145); + --color-card-foreground: oklch(0.96 0.01 130); + --color-popover: oklch(0.22 0.02 145); + --color-popover-foreground: oklch(0.96 0.01 130); + --color-primary: oklch(0.65 0.15 145); + --color-primary-foreground: oklch(0.12 0.02 145); + --color-secondary: oklch(0.28 0.03 145); + --color-secondary-foreground: oklch(0.92 0.02 145); + --color-muted: oklch(0.25 0.02 145); + --color-muted-foreground: oklch(0.70 0.02 145); + --color-accent: oklch(0.28 0.03 145); + --color-accent-foreground: oklch(0.92 0.02 145); --color-destructive: oklch(0.58 0.18 25); --color-destructive-foreground: oklch(0.99 0 0); - --color-border: oklch(0.26 0.01 120); - --color-input: oklch(0.26 0.01 120); - --color-ring: oklch(0.62 0.14 150); - --color-sidebar-background: oklch(0.10 0.01 120); - --color-sidebar-foreground: oklch(0.68 0.01 120); - --color-sidebar-primary: oklch(0.62 0.14 150); - --color-sidebar-primary-foreground: oklch(0.10 0 0); - --color-sidebar-accent: oklch(0.20 0.02 150); - --color-sidebar-accent-foreground: oklch(0.88 0.02 150); - --color-sidebar-border: oklch(0.26 0.01 120); - --color-sidebar-ring: oklch(0.62 0.14 150); + --color-border: oklch(0.32 0.03 145); + --color-input: oklch(0.32 0.03 145); + --color-ring: oklch(0.65 0.15 145); + + --color-sidebar-background: oklch(0.16 0.02 145 / 0.8); + --color-sidebar-foreground: oklch(0.85 0.02 145); + --color-sidebar-primary: oklch(0.65 0.15 145); + --color-sidebar-primary-foreground: oklch(0.12 0.02 145); + --color-sidebar-accent: oklch(0.28 0.03 145); + --color-sidebar-accent-foreground: oklch(0.92 0.02 145); + --color-sidebar-border: oklch(0.32 0.03 145 / 0.3); + --color-sidebar-ring: oklch(0.65 0.15 145); } * { @@ -81,10 +87,21 @@ html { body { @apply bg-background text-foreground; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; - letter-spacing: -0.01em; + letter-spacing: -0.015em; + /* More premium, subtle background */ + background-image: + radial-gradient(circle at 15% 50%, oklch(0.55 0.12 145 / 0.05), transparent 25%), + radial-gradient(circle at 85% 30%, oklch(0.65 0.15 170 / 0.06), transparent 25%); + background-attachment: fixed; +} + +.dark body { + background-image: + radial-gradient(circle at 15% 50%, oklch(0.55 0.12 145 / 0.15), transparent 25%), + radial-gradient(circle at 85% 30%, oklch(0.65 0.15 170 / 0.15), transparent 25%); } /* Smooth scrollbar */ @@ -164,6 +181,10 @@ button { @apply transition-all duration-150; } +button:active:not(:disabled) { + transform: scale(0.97); +} + /* Input focus enhancements */ input:focus, textarea:focus, @@ -171,14 +192,7 @@ select:focus { @apply transition-shadow duration-150; } -/* Modern shadows */ -.shadow-soft { - box-shadow: 0 2px 8px -2px oklch(0 0 0 / 0.08), 0 4px 16px -4px oklch(0 0 0 / 0.06); -} -.shadow-soft-lg { - box-shadow: 0 4px 12px -2px oklch(0 0 0 / 0.1), 0 8px 24px -4px oklch(0 0 0 / 0.08); -} /* Status colors */ .status-success { @@ -224,4 +238,33 @@ select:focus { /* Skeleton loading */ .skeleton { @apply bg-muted animate-pulse rounded; +} + +/* Glassmorphism Utilities */ +.glass-panel { + @apply bg-background/60 backdrop-blur-2xl border-white/40 dark:border-white/10 shadow-soft; +} + +.glass-card { + @apply bg-card/80 backdrop-blur-xl border border-white/60 dark:border-white/10 shadow-soft transition-all duration-300; +} + +.glass-card:hover { + @apply shadow-soft-lg -translate-y-1 bg-card/90 border-white/80 dark:border-white/20; +} + +/* Page Transitions */ +.animate-fade-in-up { + animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 25a70f0..c7e43d8 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -145,9 +145,9 @@ function NavItemComponent({ item, collapsed, level = 0 }: { item: NavItem; colla @@ -379,16 +385,16 @@ export default function AdminLayout() { {/* Main content wrapper */}
{/* Header */} -
+
-
+
-
+
- +
-
{/* Page content */} -
-
- -
-
+ +
+
+ +
+
+
) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 96df168..80bcdcb 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -37,8 +37,8 @@ export default function DashboardPage() { {/* Stats */}
{stats.map((s, i) => ( -
-
+
+
@@ -60,8 +60,8 @@ export default function DashboardPage() { {/* Content */}
{/* Recent Topics */} -
-
+
+
@@ -89,8 +89,8 @@ export default function DashboardPage() {
{/* Activities */} -
-
+
+
@@ -118,8 +118,8 @@ export default function DashboardPage() {
{/* Plant categories */} -
-
+
+
@@ -128,8 +128,8 @@ export default function DashboardPage() {
{mockCategories.map(cat => ( -
-
+
+
{cat.icon || '🌱'}
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 9e02d01..1c96e0e 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,12 +1,12 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { Leaf, Eye, EyeOff, RefreshCw, Loader2 } from 'lucide-react' +import { Leaf, Eye, EyeOff, Loader2, Disc3 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { useAuth } from '@/contexts/AuthContext' import { getCaptcha, login as apiLogin } from '@/api/system' +import LoginCharacters from '@/components/LoginCharacters' export default function LoginPage() { const navigate = useNavigate() @@ -18,6 +18,7 @@ export default function LoginPage() { const [captchaId, setCaptchaId] = useState('') const [captchaImg, setCaptchaImg] = useState('') const [showPassword, setShowPassword] = useState(false) + const [passwordFocused, setPasswordFocused] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -70,127 +71,146 @@ export default function LoginPage() { } return ( -
- {/* Background decoration */} -
-
-
-
-
+
+ {/* Background pattern */} +
- - -
- +
+ + {/* Left - Characters Banner */} +
+ {/* Logo */} +
+
+ +
+ Sundynix Console
-
- - 植趣ZeeQ - - - 请登录您的管理员账号 - + + {/* Characters Area */} +
+
- - -
- {error && ( -
-
- {error} + + {/* Bottom */} +
+ © {new Date().getFullYear() > 2026 ? `2026-${new Date().getFullYear()}` : '2026'} 光衍矩阵 · SUNDYNIX TECH +
+ + {/* Decorative Background for the left panel */} +
+
+
+ + {/* Right - Login Form */} +
+
+ {/* Mobile logo */} +
+
+
- )} - -
- - setAccount(e.target.value)} - disabled={loading} - className="h-11 rounded-xl border-border/50 bg-white/60 placeholder:text-muted-foreground/60 focus:bg-white transition-colors" - /> + Sundynix Console
-
- -
- setPassword(e.target.value)} - disabled={loading} - className="h-11 rounded-xl border-border/50 bg-white/60 placeholder:text-muted-foreground/60 focus:bg-white transition-colors pr-11" - /> - -
+ {/* Header */} +
+

+ 欢迎回来! +

+

请输入管理员账号与密码

-
- -
+ {/* Form */} + + {error && ( +
+
+ {error} +
+ )} + +
+ setCaptcha(e.target.value)} + placeholder="请输入管理员账号" + className="h-12 bg-slate-50/50 border-slate-200 focus:bg-white focus:border-emerald-500 focus:ring-emerald-500/20 text-slate-900 placeholder:text-slate-400 rounded-xl transition-all" + value={account} + onChange={(e) => setAccount(e.target.value)} disabled={loading} - className="flex-1 h-11 rounded-xl border-border/50 bg-white/60 placeholder:text-muted-foreground/60 focus:bg-white transition-colors" /> -
- {captchaImg ? ( - 验证码 - ) : ( -
- -
- )} -
- +
+ +
+ +
+ setPassword(e.target.value)} + onFocus={() => setPasswordFocused(true)} + onBlur={() => setPasswordFocused(false)} + disabled={loading} + /> + +
+
+ +
+ +
+ setCaptcha(e.target.value)} + disabled={loading} + /> +
+ {captchaImg ? ( + captcha + ) : ( + + )} +
-
- - - -
-

- © {new Date().getFullYear() > 2026 ? `2026-${new Date().getFullYear()}` : '2026'} sundynix · 安全登录 -

+ +
- - +
+
) } diff --git a/src/pages/system/AiConfig.tsx b/src/pages/system/AiConfig.tsx index e7043af..ddba398 100644 --- a/src/pages/system/AiConfig.tsx +++ b/src/pages/system/AiConfig.tsx @@ -3,7 +3,7 @@ import { Bot, Plus, Pencil, Trash2, CheckCircle2, RefreshCw, Database, Cpu, ChevronDown, ChevronUp, AlertTriangle, Loader2, Zap, Sparkles, Activity, - Server, Key, ExternalLink + Server, Key, ExternalLink, Shield } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -31,6 +31,7 @@ const defaultForm: Omit = { embeddingApiUrl: 'http://localhost:11434/v1', embeddingApiKey: '', embeddingModelName: 'bge-m3', + dailyQueryLimit: 20, } // ────────────────────────────────────────────── @@ -136,6 +137,14 @@ function ConfigCard({
+ {/* Daily limit tag */} +
+ + + 每日限额: {cfg.dailyQueryLimit > 0 ? `${cfg.dailyQueryLimit} 次/人` : '不限'} + +
+ {/* Qdrant URL */}
@@ -238,6 +247,7 @@ export default function AiConfigPage() { embeddingApiUrl: cfg.embeddingApiUrl, embeddingApiKey: cfg.embeddingApiKey, embeddingModelName: cfg.embeddingModelName, + dailyQueryLimit: cfg.dailyQueryLimit ?? 20, }) setSelectedConfig(cfg) setIsEdit(true) @@ -569,6 +579,28 @@ export default function AiConfigPage() {
+ {/* ── Usage Limit Section ── */} +
+
+ } + label="用量限制" + color="text-amber-600" + /> +
+ + f('dailyQueryLimit', Number(e.target.value))} + placeholder="20" + className="h-8 text-sm bg-white" + /> + +
+
+
+