docs & cleanup: update stack info, remove dead code, fix backend bugs
- Fix AGENTS.md and 技术架构设计.md to reflect actual stack (Prisma+MySQL, static export, PM2) - Remove 4 unused frontend components (page-transition, page-layout, image-upload, section-card) - Fix card.tsx hardcoded colors → CSS variables - Remove provider name from model selector, remove sensenova-u1-fast from models - Fix analytics.controller.ts raw SQL table names (runtime bug) - Add JWT auth guard to tools POST endpoint - Fix AI gateway test: update env vars and model names - Fix admin test: mock role structure for Prisma relation
This commit is contained in:
@@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('rounded-xl border border-gray-200 bg-white text-gray-900 shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100', className)} {...props} />
|
||||
<div ref={ref} className={cn('rounded-xl border border-border bg-card text-foreground shadow-sm', className)} {...props} />
|
||||
),
|
||||
)
|
||||
Card.displayName = 'Card'
|
||||
@@ -24,7 +24,7 @@ CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn('text-sm text-gray-500 dark:text-gray-400', className)} {...props} />
|
||||
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
interface ImageUploadProps {
|
||||
onUploaded: (url: string) => void;
|
||||
defaultImage?: string;
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
export function ImageUpload({ onUploaded, defaultImage, accept = 'image/*' }: ImageUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [preview, setPreview] = useState(defaultImage || '');
|
||||
const [error, setError] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
setError('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
setError('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.message || '上传失败');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setPreview(`${API_BASE}${data.url}`);
|
||||
onUploaded(data.url);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className="relative w-32 h-32 border-2 border-dashed border-gray-200 rounded-lg overflow-hidden cursor-pointer hover:border-brand-400 transition-colors bg-gray-50"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{preview ? (
|
||||
<img src={preview} alt="preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center w-full h-full text-gray-400">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{uploading && (
|
||||
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
|
||||
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input ref={inputRef} type="file" accept={accept} onChange={handleFile} className="hidden" />
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
<p className="text-xs text-gray-400">点击上传图片(最大 5MB,支持 jpg/png/gif/webp)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function ModelSelector({ value, onChange, className = '' }: ModelSelector
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">{m.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{m.provider} · {m.desc}</div>
|
||||
<div className="text-xs text-muted-foreground">{m.desc}</div>
|
||||
</div>
|
||||
{m.id === value && (
|
||||
<svg className="w-4 h-4 text-brand-600 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface PageLayoutProps {
|
||||
title: string
|
||||
description?: string
|
||||
backHref?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function PageLayout({ title, description, backHref, children }: PageLayoutProps) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
{backHref && (
|
||||
<a href={backHref} className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
||||
← 返回
|
||||
</a>
|
||||
)}
|
||||
<h1 className="text-3xl font-bold text-foreground">{title}</h1>
|
||||
{description && <p className="mt-2 text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
export function PageTransition({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
padding?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
export function Card({ children, className = '', padding = 'md' }: CardProps) {
|
||||
const pads = { sm: 'p-4', md: 'p-6', lg: 'p-8' }
|
||||
return (
|
||||
<div className={`bg-card rounded-2xl border border-border shadow-sm ${pads[padding]} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export function StatCard({ label, value }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">{label}</div>
|
||||
<div className="text-2xl font-bold text-foreground">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user