fix: source list scroll, citation targeting, and page UI polish
- Replace ScrollArea with native overflow-y-auto for reliable mouse wheel scrolling - Scope source DOM IDs by message ID to fix cross-round citation jumps - Improve profile, settings, forum, knowledge page layouts - Update mobile navigation and chat interface Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@ export default function LoginPage() {
|
||||
href="/"
|
||||
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
||||
>
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow">
|
||||
<div className="w-12 h-12 bg-primary rounded-xl flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow">
|
||||
<BookOpen className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function RegisterPage() {
|
||||
href="/"
|
||||
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
||||
>
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow">
|
||||
<div className="w-12 h-12 bg-primary rounded-xl flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow">
|
||||
<BookOpen className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
|
||||
@@ -193,7 +193,7 @@ export default function ForumAdminPage() {
|
||||
</button>
|
||||
{confirmDelete?.type === "cat" && confirmDelete.id === cat.id ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => handleDeleteCategory(cat.id)} className="text-xs px-1.5 py-0.5 bg-red-600 text-white rounded">确认</button>
|
||||
<button onClick={() => handleDeleteCategory(cat.id)} className="text-xs px-1.5 py-0.5 bg-destructive text-destructive-foreground rounded">确认</button>
|
||||
<button onClick={() => setConfirmDelete(null)} className="text-xs px-1.5 py-0.5 border rounded">取消</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -91,8 +91,8 @@ export default function KnowledgeAdminPage() {
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
kb.is_system
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-slate-100 text-slate-600"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{kb.is_system ? "系统" : "用户"}
|
||||
@@ -108,7 +108,7 @@ export default function KnowledgeAdminPage() {
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => handleDelete(kb.id)}
|
||||
className="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
className="text-xs px-2 py-1 bg-destructive text-destructive-foreground rounded hover:bg-destructive/80"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
@@ -122,7 +122,7 @@ export default function KnowledgeAdminPage() {
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setConfirmDelete(kb.id)}
|
||||
className="text-red-500 hover:text-red-700 transition-colors"
|
||||
className="text-destructive hover:text-destructive/80 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -16,12 +16,13 @@ interface DashboardStats {
|
||||
total_generated_images: number;
|
||||
}
|
||||
|
||||
function TrendChart({ title, icon: Icon, data, color }: {
|
||||
function TrendChart({ title, icon: Icon, data, variant }: {
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
data: { date: string; count: number }[];
|
||||
color: string;
|
||||
variant?: "default" | "secondary";
|
||||
}) {
|
||||
const barClass = variant === "secondary" ? "bg-emerald-500/35" : "bg-primary/35";
|
||||
return (
|
||||
<div className="rounded-xl border p-5 space-y-3">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
@@ -36,7 +37,7 @@ function TrendChart({ title, icon: Icon, data, color }: {
|
||||
return (
|
||||
<div key={d.date} className="flex-1 flex flex-col items-center gap-1">
|
||||
<span className="text-[9px] text-muted-foreground tabular-nums">{d.count || ""}</span>
|
||||
<div className="w-full rounded-t" style={{ height: `${h}%`, backgroundColor: color, opacity: 0.35 }} title={`${d.date}: ${d.count}`} />
|
||||
<div className={`w-full rounded-t ${barClass}`} style={{ height: `${h}%` }} title={`${d.date}: ${d.count}`} />
|
||||
<span className="text-[9px] text-muted-foreground">{d.date.slice(5)}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -101,8 +102,8 @@ export default function AdminPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} color="#2563eb" />
|
||||
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} color="#16a34a" />
|
||||
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} variant="default" />
|
||||
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} variant="secondary" />
|
||||
</div>
|
||||
|
||||
{systemStatus && (
|
||||
@@ -118,7 +119,7 @@ export default function AdminPage() {
|
||||
{ label: "嵌入模型", ok: true, sub: systemStatus.embedding_model },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="flex items-center gap-2 text-sm">
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: s.ok ? "#22c55e" : "#ef4444" }} />
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${s.ok ? "bg-emerald-500" : "bg-destructive"}`} />
|
||||
<span>{s.label}</span>
|
||||
{s.sub && <span className="text-muted-foreground text-xs truncate">{s.sub}</span>}
|
||||
</div>
|
||||
|
||||
@@ -77,10 +77,7 @@ export default function SystemSettingsPage() {
|
||||
{statusItems.map((s) => (
|
||||
<div key={s.label} className="flex items-center justify-between px-5 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: s.ok ? "#22c55e" : "#ef4444" }}
|
||||
/>
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${s.ok ? "bg-emerald-500" : "bg-destructive"}`} />
|
||||
<span className="text-sm">{s.label}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">{s.detail}</span>
|
||||
@@ -115,9 +112,9 @@ export default function SystemSettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div className="rounded-xl border border-red-200">
|
||||
<div className="px-5 py-3 border-b border-red-200">
|
||||
<h3 className="text-sm font-medium text-red-600">危险操作</h3>
|
||||
<div className="rounded-xl border border-destructive/30">
|
||||
<div className="px-5 py-3 border-b border-destructive/30">
|
||||
<h3 className="text-sm font-medium text-destructive">危险操作</h3>
|
||||
</div>
|
||||
<div className="px-5 py-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -126,7 +123,7 @@ export default function SystemSettingsPage() {
|
||||
<div className="text-xs text-muted-foreground">重新处理所有知识库文档并建立向量索引</div>
|
||||
</div>
|
||||
<button
|
||||
className="px-3 py-1.5 text-sm border border-red-200 text-red-600 rounded-lg hover:bg-red-50 transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-destructive/30 text-destructive rounded-lg hover:bg-destructive/10 transition-colors"
|
||||
onClick={() => alert("此功能需要通过后端命令行执行")}
|
||||
>
|
||||
重建索引
|
||||
@@ -138,7 +135,7 @@ export default function SystemSettingsPage() {
|
||||
<div className="text-xs text-muted-foreground">清除系统缓存和临时文件</div>
|
||||
</div>
|
||||
<button
|
||||
className="px-3 py-1.5 text-sm border border-red-200 text-red-600 rounded-lg hover:bg-red-50 transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-destructive/30 text-destructive rounded-lg hover:bg-destructive/10 transition-colors"
|
||||
onClick={() => alert("此功能需要通过后端命令行执行")}
|
||||
>
|
||||
清理
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function UsersPage() {
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => handleDelete(u.id)}
|
||||
className="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
className="text-xs px-2 py-1 bg-destructive text-destructive-foreground rounded hover:bg-destructive/80"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
|
||||
@@ -188,7 +188,7 @@ export default function AnalyticsPage() {
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<MessageSquare className="w-8 h-8 text-blue-600" />
|
||||
<MessageSquare className="w-8 h-8 text-primary" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">对话会话</p>
|
||||
<p className="text-2xl font-bold text-foreground">{statistics.total_sessions}</p>
|
||||
@@ -200,7 +200,7 @@ export default function AnalyticsPage() {
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<BookOpen className="w-8 h-8 text-green-600" />
|
||||
<BookOpen className="w-8 h-8 text-emerald-500" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">消息总数</p>
|
||||
<p className="text-2xl font-bold text-foreground">{statistics.total_messages}</p>
|
||||
@@ -212,7 +212,7 @@ export default function AnalyticsPage() {
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<FileText className="w-8 h-8 text-purple-600" />
|
||||
<FileText className="w-8 h-8 text-primary" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">文档数量</p>
|
||||
<p className="text-2xl font-bold text-foreground">{statistics.total_documents}</p>
|
||||
@@ -224,7 +224,7 @@ export default function AnalyticsPage() {
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-8 h-8 text-orange-600" />
|
||||
<Calendar className="w-8 h-8 text-muted-foreground" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">活跃天数</p>
|
||||
<p className="text-2xl font-bold text-foreground">{statistics.active_days}</p>
|
||||
@@ -250,7 +250,7 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${item.coverage}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ export default function AnalyticsPage() {
|
||||
<p className="text-sm font-medium text-foreground">{item.question}</p>
|
||||
<p className="text-xs text-muted-foreground">{item.category}</p>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-blue-600">{item.count} 次</div>
|
||||
<div className="text-sm font-medium text-primary">{item.count} 次</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -300,11 +300,11 @@ export default function AnalyticsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-medium text-foreground">总体进度</span>
|
||||
<span className="text-2xl font-bold text-blue-600">{learningReport.learning_progress}%</span>
|
||||
<span className="text-2xl font-bold text-primary">{learningReport.learning_progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-4">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-600 h-4 rounded-full transition-all duration-500"
|
||||
className="bg-primary h-4 rounded-full transition-all duration-500"
|
||||
style={{ width: `${learningReport.learning_progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -334,8 +334,8 @@ export default function AnalyticsPage() {
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{learningReport.recommendations.map((recommendation, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 p-3 bg-blue-500/10 dark:bg-blue-500/20 rounded-lg">
|
||||
<div className="w-2 h-2 bg-blue-600 rounded-full mt-2 flex-shrink-0" />
|
||||
<div key={index} className="flex items-start space-x-3 p-3 bg-primary/10 dark:bg-blue-500/20 rounded-lg">
|
||||
<div className="w-2 h-2 bg-primary rounded-full mt-2 flex-shrink-0" />
|
||||
<p className="text-sm text-foreground">{recommendation}</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -355,8 +355,8 @@ export default function AnalyticsPage() {
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{learningReport.knowledge_gaps.map((gap, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 p-3 bg-orange-500/10 dark:bg-orange-500/20 rounded-lg">
|
||||
<div className="w-2 h-2 bg-orange-600 rounded-full mt-2 flex-shrink-0" />
|
||||
<div key={index} className="flex items-start space-x-3 p-3 bg-muted dark:bg-orange-500/20 rounded-lg">
|
||||
<div className="w-2 h-2 bg-muted-foreground rounded-full mt-2 flex-shrink-0" />
|
||||
<p className="text-sm text-foreground">{gap}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
export default function ChatPage() {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, user, isLoading: authLoading } = useAuthStore();
|
||||
const { loadSessions, sessions } = useChatStore();
|
||||
const { loadSessions, sessions, selectSession, currentSession } = useChatStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
@@ -26,7 +26,14 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
if (isAuthenticated && !isInitialized) {
|
||||
loadSessions().then(() => setInitialLoading(false));
|
||||
loadSessions().then(async () => {
|
||||
setInitialLoading(false);
|
||||
// Auto-select the most recent session, or do nothing (welcome page)
|
||||
const store = useChatStore.getState();
|
||||
if (!store.currentSession && store.sessions.length > 0) {
|
||||
await store.selectSession(store.sessions[0].id);
|
||||
}
|
||||
});
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [isAuthenticated, authLoading, isInitialized, router, loadSessions]);
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function CourseContentPage() {
|
||||
<>
|
||||
{/* 知识图谱视图 */}
|
||||
{viewMode === 'graph' && (
|
||||
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-border/40 overflow-hidden bg-slate-50">
|
||||
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-border overflow-hidden bg-muted">
|
||||
<KnowledgeGraph bookStructure={bookStructure} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,43 +12,26 @@ import {
|
||||
ChevronRight,
|
||||
Clock,
|
||||
User,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
import { forumAPI } from "@/lib/api";
|
||||
import type { ForumCategory, ForumPostSummary } from "@/types";
|
||||
|
||||
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
||||
default: <MessageSquare className="w-5 h-5" />,
|
||||
};
|
||||
import { format } from "date-fns";
|
||||
|
||||
const CATEGORY_ORDER = ["公告", "通知", "学习", "讨论", "课程", "反馈", "使用"];
|
||||
|
||||
function getCategoryIcon(name: string) {
|
||||
if (name.includes("公告") || name.includes("通知"))
|
||||
return <Megaphone className="w-5 h-5" />;
|
||||
return <Megaphone className="w-4 h-4" />;
|
||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程"))
|
||||
return <BookOpen className="w-5 h-5" />;
|
||||
return <BookOpen className="w-4 h-4" />;
|
||||
if (name.includes("反馈") || name.includes("使用"))
|
||||
return <Lightbulb className="w-5 h-5" />;
|
||||
return CATEGORY_ICONS.default;
|
||||
return <Lightbulb className="w-4 h-4" />;
|
||||
return <MessageSquare className="w-4 h-4" />;
|
||||
}
|
||||
|
||||
function getCategoryAccent(name: string) {
|
||||
if (name.includes("公告") || name.includes("通知")) return "text-rose-600 dark:text-rose-400";
|
||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "text-blue-600 dark:text-blue-400";
|
||||
if (name.includes("反馈") || name.includes("使用")) return "text-amber-600 dark:text-amber-400";
|
||||
return "text-slate-600 dark:text-slate-400";
|
||||
}
|
||||
|
||||
function getCategoryBorder(name: string) {
|
||||
if (name.includes("公告") || name.includes("通知")) return "border-l-rose-500";
|
||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "border-l-blue-500";
|
||||
if (name.includes("反馈") || name.includes("使用")) return "border-l-amber-500";
|
||||
return "border-l-slate-500";
|
||||
}
|
||||
|
||||
import { format } from "date-fns";
|
||||
|
||||
export default function ForumHomePage() {
|
||||
const [categories, setCategories] = useState<ForumCategory[]>([]);
|
||||
const [categoryPosts, setCategoryPosts] = useState<
|
||||
@@ -56,17 +39,30 @@ export default function ForumHomePage() {
|
||||
>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await forumAPI.getCategories();
|
||||
setCategories(data);
|
||||
const sorted = [...data].sort((a, b) => {
|
||||
const getOrder = (name: string) => {
|
||||
for (let i = 0; i < CATEGORY_ORDER.length; i++) {
|
||||
if (name.includes(CATEGORY_ORDER[i])) return i;
|
||||
}
|
||||
return CATEGORY_ORDER.length;
|
||||
};
|
||||
return getOrder(a.name) - getOrder(b.name);
|
||||
});
|
||||
setCategories(sorted);
|
||||
if (sorted.length > 0) {
|
||||
setActiveTab(String(sorted[0].id));
|
||||
}
|
||||
const postsEntries = await Promise.all(
|
||||
data.map(async (category) => {
|
||||
sorted.map(async (category) => {
|
||||
try {
|
||||
const posts = await forumAPI.getPosts(category.id, 3);
|
||||
const posts = await forumAPI.getPosts(category.id, 10);
|
||||
return [category.id, posts] as const;
|
||||
} catch {
|
||||
return [category.id, []] as const;
|
||||
@@ -87,69 +83,83 @@ export default function ForumHomePage() {
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 text-muted-foreground py-20">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
正在加载社区...
|
||||
</div>
|
||||
) : error ? (
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{[...categories]
|
||||
.sort((a, b) => {
|
||||
const getOrder = (name: string) => {
|
||||
for (let i = 0; i < CATEGORY_ORDER.length; i++) {
|
||||
if (name.includes(CATEGORY_ORDER[i])) return i;
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return CATEGORY_ORDER.length;
|
||||
};
|
||||
return getOrder(a.name) - getOrder(b.name);
|
||||
})
|
||||
.map((category) => {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
{categories.map((category) => (
|
||||
<TabsTrigger
|
||||
key={category.id}
|
||||
value={String(category.id)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{getCategoryIcon(category.name)}
|
||||
{category.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{categories.map((category) => {
|
||||
const posts = categoryPosts[category.id] || [];
|
||||
return (
|
||||
<div
|
||||
<TabsContent
|
||||
key={category.id}
|
||||
className={`rounded-xl border border-border overflow-hidden border-l-4 ${getCategoryBorder(category.name)}`}
|
||||
value={String(category.id)}
|
||||
className="mt-6"
|
||||
>
|
||||
{/* Category header */}
|
||||
<div className="px-5 py-3.5 flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={getCategoryAccent(category.name)}>
|
||||
{getCategoryIcon(category.name)}
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
{category.name}
|
||||
</h2>
|
||||
{/* Category description */}
|
||||
{category.description && (
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
{/* New post button */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-muted-foreground tabular-nums">
|
||||
{category.post_count} 条讨论
|
||||
</span>
|
||||
<Link
|
||||
href={`/forum/${category.id}`}
|
||||
className="text-sm font-medium text-primary hover:underline flex items-center gap-0.5"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
查看全部
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
发表讨论
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Posts list */}
|
||||
<div className="rounded-xl border border-border overflow-hidden">
|
||||
<div className="divide-y divide-border">
|
||||
{posts.length > 0 ? (
|
||||
posts.map((post) => (
|
||||
@@ -169,7 +179,10 @@ export default function ForumHomePage() {
|
||||
<span>{post.author_name}</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
{format(new Date(post.created_at), "yyyy/M/d HH:mm")}
|
||||
{format(
|
||||
new Date(post.created_at),
|
||||
"yyyy/M/d HH:mm"
|
||||
)}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
@@ -181,16 +194,32 @@ export default function ForumHomePage() {
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div className="px-5 py-8 text-center text-sm text-muted-foreground">
|
||||
<div className="px-5 py-12 text-center">
|
||||
<MessageSquare className="w-8 h-8 text-muted-foreground/40 mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
暂无讨论,成为第一个发帖的人
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View all link */}
|
||||
{posts.length > 0 && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Link
|
||||
href={`/forum/${category.id}`}
|
||||
className="text-sm text-muted-foreground hover:text-primary transition-colors inline-flex items-center gap-1"
|
||||
>
|
||||
查看全部 {category.post_count} 条讨论
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -209,7 +209,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
返回
|
||||
</Button>
|
||||
<div className="flex items-center space-x-4">
|
||||
<BookOpen className="w-8 h-8 text-blue-600" />
|
||||
<BookOpen className="w-8 h-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">{knowledgeBase.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -262,7 +262,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
|
||||
<Button variant="default">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
上传文档
|
||||
</Button>
|
||||
@@ -302,17 +302,17 @@ export default function KnowledgeBaseDetailPage() {
|
||||
<div key={fileId} className="flex items-center justify-between p-2 border rounded-md">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FileText className="w-4 h-4 text-blue-600 flex-shrink-0" />
|
||||
<FileText className="w-4 h-4 text-primary flex-shrink-0" />
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-xs text-red-600 mt-1">{error}</div>
|
||||
<div className="text-xs text-destructive mt-1">{error}</div>
|
||||
)}
|
||||
{progress > 0 && progress < 100 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div className="w-full bg-muted rounded-full h-1 mt-1">
|
||||
<div
|
||||
className="bg-blue-600 h-1 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
@@ -320,7 +320,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
{progress === 100 && !error && (
|
||||
<div className="text-xs text-green-600 mt-1">✓ 上传完成</div>
|
||||
<div className="text-xs text-emerald-500 mt-1">✓ 上传完成</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
@@ -330,7 +330,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
setUploadFiles(prev => prev.filter((_, i) => i !== index));
|
||||
}}
|
||||
disabled={isUploading}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -398,7 +398,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索文档..."
|
||||
value={searchQuery}
|
||||
@@ -450,14 +450,14 @@ export default function KnowledgeBaseDetailPage() {
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell>
|
||||
{doc.is_processed ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-yellow-600" />
|
||||
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FileText className="w-4 h-4 text-blue-600" />
|
||||
<FileText className="w-4 h-4 text-primary" />
|
||||
<span>{doc.title}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -480,7 +480,7 @@ export default function KnowledgeBaseDetailPage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteDocument(doc.id)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function KnowledgePage() {
|
||||
{/* 创建知识库按钮 */}
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
|
||||
<Button variant="default">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建知识库
|
||||
</Button>
|
||||
@@ -206,7 +206,7 @@ export default function KnowledgePage() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索知识库..."
|
||||
value={searchQuery}
|
||||
@@ -224,17 +224,17 @@ export default function KnowledgePage() {
|
||||
{systemKBs.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<FolderOpen className="w-5 h-5 text-amber-600" />
|
||||
<FolderOpen className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">系统知识库</h2>
|
||||
<span className="text-sm text-muted-foreground">({systemKBs.length})</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{systemKBs.map((kb) => (
|
||||
<Card key={kb.id} className="hover:shadow-lg transition-shadow backdrop-blur-sm bg-card/80 border-amber-500/30">
|
||||
<Card key={kb.id} className="hover:shadow-lg transition-shadow backdrop-blur-sm bg-card/80 border-primary/30">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FolderOpen className="w-5 h-5 text-amber-600" />
|
||||
<FolderOpen className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
@@ -244,9 +244,9 @@ export default function KnowledgePage() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{kb.document_count > 0 ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<Clock className="w-4 h-4 text-gray-400" />
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,7 +263,7 @@ export default function KnowledgePage() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>状态</span>
|
||||
<span className={kb.document_count > 0 ? "text-green-600" : "text-gray-500"}>
|
||||
<span className={kb.document_count > 0 ? "text-emerald-500" : "text-muted-foreground"}>
|
||||
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -311,7 +311,7 @@ export default function KnowledgePage() {
|
||||
<div>
|
||||
{systemKBs.length > 0 && (
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">我的知识库</h2>
|
||||
<span className="text-sm text-muted-foreground">({userKBs.length})</span>
|
||||
</div>
|
||||
@@ -322,7 +322,7 @@ export default function KnowledgePage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
@@ -332,9 +332,9 @@ export default function KnowledgePage() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{kb.document_count > 0 ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<Clock className="w-4 h-4 text-gray-400" />
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -351,7 +351,7 @@ export default function KnowledgePage() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>状态</span>
|
||||
<span className={kb.document_count > 0 ? "text-green-600" : "text-gray-500"}>
|
||||
<span className={kb.document_count > 0 ? "text-emerald-500" : "text-muted-foreground"}>
|
||||
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -371,7 +371,7 @@ export default function KnowledgePage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteKnowledgeBase(kb.id)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import MobileNav from "@/components/layout/mobile-nav";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
@@ -15,11 +16,9 @@ import {
|
||||
Edit2,
|
||||
Save,
|
||||
X,
|
||||
MessageSquare,
|
||||
Database,
|
||||
FileText
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { authAPI, analyticsAPI } from "@/lib/api";
|
||||
import { authAPI } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
|
||||
const profileSchema = z.object({
|
||||
@@ -37,12 +36,6 @@ export default function ProfilePage() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [statistics, setStatistics] = useState<{
|
||||
total_sessions: number;
|
||||
total_messages: number;
|
||||
total_documents: number;
|
||||
active_days: number;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -79,17 +72,6 @@ export default function ProfilePage() {
|
||||
setError(null);
|
||||
const userData = await authAPI.getCurrentUser();
|
||||
setUser(userData);
|
||||
try {
|
||||
const stats = await analyticsAPI.getStatistics();
|
||||
setStatistics({
|
||||
total_sessions: stats.total_sessions || 0,
|
||||
total_messages: stats.total_messages || 0,
|
||||
total_documents: stats.total_documents || 0,
|
||||
active_days: stats.active_days || 0,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("加载统计数据失败:", err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("加载用户信息失败:", err);
|
||||
setError("加载用户信息失败");
|
||||
@@ -142,24 +124,28 @@ export default function ProfilePage() {
|
||||
const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-app pb-16 lg:pb-0">
|
||||
<div className="min-h-screen bg-background pb-16 lg:pb-0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
{/* 状态提示 */}
|
||||
{error && (
|
||||
<div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">{error}</div>
|
||||
<div className="mb-6 p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive text-sm">{error}</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
|
||||
<div className="mb-6 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-400 text-sm flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-2xl space-y-8">
|
||||
{/* 头像与基本信息 */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4 pb-3 border-b">
|
||||
<h2 className="text-base font-semibold">个人信息</h2>
|
||||
<div className="space-y-6">
|
||||
{/* 个人信息卡片 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
<CardTitle>个人信息</CardTitle>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
@@ -169,32 +155,34 @@ export default function ProfilePage() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 头像 */}
|
||||
<div className="flex items-center gap-5 mb-6">
|
||||
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center text-primary-foreground text-xl font-bold">
|
||||
<CardDescription>管理您的账户基本信息</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 头像 + 表单 */}
|
||||
<div className="flex items-center gap-5 mb-6 pb-6 border-b">
|
||||
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center text-primary-foreground text-xl font-bold flex-shrink-0">
|
||||
{initials}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{user.full_name || user.username}</p>
|
||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||
<div className="flex-1">
|
||||
<p className="text-base font-medium">{user.full_name || user.username}</p>
|
||||
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">用户名</Label>
|
||||
<Input value={user.username} disabled className="mt-1 bg-muted" />
|
||||
<p className="text-[11px] text-muted-foreground mt-1">用户名创建后无法修改</p>
|
||||
<Input value={user.username} disabled className="mt-1.5 bg-muted" />
|
||||
<p className="text-xs text-muted-foreground mt-1">用户名创建后无法修改</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">注册时间</Label>
|
||||
<Input value={formatDate(user.created_at)} disabled className="mt-1 bg-muted" />
|
||||
<Input value={formatDate(user.created_at)} disabled className="mt-1.5 bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="email" className="text-sm text-muted-foreground">邮箱</Label>
|
||||
<Input
|
||||
@@ -202,11 +190,10 @@ export default function ProfilePage() {
|
||||
type="email"
|
||||
{...register("email")}
|
||||
disabled={!isEditing}
|
||||
className={`mt-1 ${errors.email ? "border-red-500" : ""}`}
|
||||
className={`mt-1.5 ${errors.email ? "border-destructive" : ""}`}
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1">{errors.email.message}</p>}
|
||||
{errors.email && <p className="text-xs text-destructive mt-1">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="full_name" className="text-sm text-muted-foreground">真实姓名</Label>
|
||||
<Input
|
||||
@@ -214,12 +201,13 @@ export default function ProfilePage() {
|
||||
{...register("full_name")}
|
||||
disabled={!isEditing}
|
||||
placeholder="请输入您的真实姓名(可选)"
|
||||
className="mt-1"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div className="flex items-center gap-3 pt-4 border-t">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
@@ -238,43 +226,9 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 学习统计 */}
|
||||
<section>
|
||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
||||
<h2 className="text-base font-semibold">学习统计</h2>
|
||||
</div>
|
||||
{statistics ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="p-3 rounded-lg border text-center">
|
||||
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-primary" />
|
||||
<p className="text-lg font-semibold">{statistics.total_sessions}</p>
|
||||
<p className="text-[11px] text-muted-foreground">对话数</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border text-center">
|
||||
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-green-600" />
|
||||
<p className="text-lg font-semibold">{statistics.total_messages}</p>
|
||||
<p className="text-[11px] text-muted-foreground">消息数</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border text-center">
|
||||
<Database className="w-4 h-4 mx-auto mb-1.5 text-purple-600" />
|
||||
<p className="text-lg font-semibold">{statistics.total_documents}</p>
|
||||
<p className="text-[11px] text-muted-foreground">文档数</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border text-center">
|
||||
<FileText className="w-4 h-4 mx-auto mb-1.5 text-orange-600" />
|
||||
<p className="text-lg font-semibold">{statistics.active_days}</p>
|
||||
<p className="text-[11px] text-muted-foreground">活跃天数</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<Loader2 className="w-5 h-5 animate-spin mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">加载中...</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<MobileNav />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import MobileNav from "@/components/layout/mobile-nav";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
@@ -44,9 +45,9 @@ function getPasswordStrength(password: string) {
|
||||
if (/[0-9]/.test(password)) score++;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
||||
|
||||
if (score <= 2) return { label: "弱", color: "text-red-600", score };
|
||||
if (score <= 4) return { label: "中", color: "text-yellow-600", score };
|
||||
return { label: "强", color: "text-green-600", score };
|
||||
if (score <= 2) return { label: "弱", color: "text-destructive", score };
|
||||
if (score <= 4) return { label: "中", color: "text-amber-500", score };
|
||||
return { label: "强", color: "text-emerald-500", score };
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -106,38 +107,43 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-app pb-16 lg:pb-0">
|
||||
<div className="min-h-screen bg-background pb-16 lg:pb-0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
{/* 状态提示 */}
|
||||
{error && (
|
||||
<div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
||||
<div className="mb-6 p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
|
||||
<div className="mb-6 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-400 text-sm flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-2xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
{/* 账户安全 */}
|
||||
<section>
|
||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
||||
<Shield className="w-4 h-4 text-primary" />
|
||||
<h2 className="text-base font-semibold">账户安全</h2>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-primary" />
|
||||
<CardTitle>账户安全</CardTitle>
|
||||
</div>
|
||||
<CardDescription>修改您的登录密码</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="old_password" className="text-sm text-muted-foreground">当前密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<div className="relative mt-1.5">
|
||||
<Input
|
||||
id="old_password"
|
||||
type={showOldPassword ? "text" : "password"}
|
||||
{...register("old_password")}
|
||||
className={errors.old_password ? "border-red-500" : ""}
|
||||
className={errors.old_password ? "border-destructive" : ""}
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
<button
|
||||
@@ -148,17 +154,17 @@ export default function SettingsPage() {
|
||||
{showOldPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.old_password && <p className="text-xs text-red-500 mt-1">{errors.old_password.message}</p>}
|
||||
{errors.old_password && <p className="text-xs text-destructive mt-1">{errors.old_password.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="new_password" className="text-sm text-muted-foreground">新密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<div className="relative mt-1.5">
|
||||
<Input
|
||||
id="new_password"
|
||||
type={showNewPassword ? "text" : "password"}
|
||||
{...register("new_password")}
|
||||
className={errors.new_password ? "border-red-500" : ""}
|
||||
className={errors.new_password ? "border-destructive" : ""}
|
||||
placeholder="请输入新密码(至少6个字符)"
|
||||
/>
|
||||
<button
|
||||
@@ -178,25 +184,26 @@ export default function SettingsPage() {
|
||||
<div className="w-full bg-muted rounded-full h-1.5">
|
||||
<div
|
||||
className={`h-1.5 rounded-full transition-all ${
|
||||
passwordStrength.score <= 2 ? "bg-red-500 w-1/3"
|
||||
: passwordStrength.score <= 4 ? "bg-yellow-500 w-2/3"
|
||||
: "bg-green-500 w-full"
|
||||
passwordStrength.score <= 2 ? "bg-destructive w-1/3"
|
||||
: passwordStrength.score <= 4 ? "bg-amber-500 w-2/3"
|
||||
: "bg-emerald-500 w-full"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errors.new_password && <p className="text-xs text-red-500 mt-1">{errors.new_password.message}</p>}
|
||||
{errors.new_password && <p className="text-xs text-destructive mt-1">{errors.new_password.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="max-w-md">
|
||||
<Label htmlFor="confirm_password" className="text-sm text-muted-foreground">确认新密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<div className="relative mt-1.5">
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
{...register("confirm_password")}
|
||||
className={errors.confirm_password ? "border-red-500" : ""}
|
||||
className={errors.confirm_password ? "border-destructive" : ""}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
<button
|
||||
@@ -207,10 +214,10 @@ export default function SettingsPage() {
|
||||
{showConfirmPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.confirm_password && <p className="text-xs text-red-500 mt-1">{errors.confirm_password.message}</p>}
|
||||
{errors.confirm_password && <p className="text-xs text-destructive mt-1">{errors.confirm_password.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<div className="pt-4 border-t">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
@@ -224,14 +231,19 @@ export default function SettingsPage() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 偏好设置 */}
|
||||
<section>
|
||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
||||
<Palette className="w-4 h-4 text-primary" />
|
||||
<h2 className="text-base font-semibold">偏好设置</h2>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-5 h-5 text-primary" />
|
||||
<CardTitle>偏好设置</CardTitle>
|
||||
</div>
|
||||
<CardDescription>自定义您的使用体验</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between py-3 border-b last:border-0">
|
||||
<div>
|
||||
<p className="text-sm font-medium">外观主题</p>
|
||||
@@ -239,7 +251,8 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<MobileNav />
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function TextToImagePage() {
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-4">
|
||||
文生图 - Text to Image
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function ForumCategoryPage() {
|
||||
{isAuthenticated && (
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<PenLine className="w-3.5 h-3.5" />
|
||||
{showForm ? "收起" : "发帖"}
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function ForumPostPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !replyContent.trim()}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
||||
@@ -188,7 +188,7 @@ export default function ChatInterface() {
|
||||
"transition-all duration-150",
|
||||
isStreaming
|
||||
? "bg-destructive hover:bg-destructive/90 text-white"
|
||||
: "bg-[#2563eb] hover:bg-[#1d4ed8] text-white disabled:bg-[#2563eb]/40 disabled:cursor-not-allowed"
|
||||
: "bg-primary hover:bg-primary/90 text-primary-foreground disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||
)}
|
||||
>
|
||||
{isStreaming ? (
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Database, Globe, ChevronDown, ChevronRight, Star } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SourceReference {
|
||||
@@ -249,9 +248,9 @@ export default function SourceReferences({
|
||||
return (
|
||||
<div className="mt-3 space-y-2">
|
||||
{expanded ? (
|
||||
<ScrollArea className="max-h-96">
|
||||
<div className="pr-3">{sourceSection}</div>
|
||||
</ScrollArea>
|
||||
<div className="max-h-96 overflow-y-auto pr-1">
|
||||
{sourceSection}
|
||||
</div>
|
||||
) : (
|
||||
sourceSection
|
||||
)}
|
||||
|
||||
@@ -46,8 +46,8 @@ export default function MobileNav() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 移动端导航栏 */}
|
||||
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 mobile-safe-area z-50">
|
||||
{/* Bottom navigation bar */}
|
||||
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-background border-t border-border mobile-safe-area z-50">
|
||||
<div className="flex items-center justify-around py-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
@@ -58,7 +58,7 @@ export default function MobileNav() {
|
||||
size="sm"
|
||||
onClick={() => handleNavClick(item.href)}
|
||||
className={`flex flex-col items-center space-y-1 px-3 py-2 ${
|
||||
isActive ? "text-white" : "text-gray-600"
|
||||
isActive ? "" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
@@ -69,48 +69,43 @@ export default function MobileNav() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端菜单按钮 */}
|
||||
{/* Menu toggle button */}
|
||||
<div className="lg:hidden fixed top-4 right-4 z-50">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="bg-white shadow-lg"
|
||||
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-background border border-border shadow-sm hover:bg-muted transition-colors"
|
||||
>
|
||||
{isOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 移动端侧边菜单 */}
|
||||
{/* Side menu overlay */}
|
||||
{isOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-40">
|
||||
{/* 遮罩 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black bg-opacity-50"
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* 菜单内容 */}
|
||||
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-white shadow-xl mobile-safe-area">
|
||||
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-background shadow-xl mobile-safe-area">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 用户信息 */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
{/* User info */}
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-gray-600" />
|
||||
<div className="w-12 h-12 bg-muted rounded-full flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-lg font-medium text-gray-900 truncate">
|
||||
<p className="text-lg font-medium text-foreground truncate">
|
||||
{user?.full_name || user?.username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{user?.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
{/* Navigation */}
|
||||
<div className="flex-1 p-6">
|
||||
<nav className="space-y-2">
|
||||
{navItems.map((item) => {
|
||||
@@ -130,8 +125,8 @@ export default function MobileNav() {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<div className="p-6 border-t border-gray-200 space-y-2">
|
||||
{/* Bottom actions */}
|
||||
<div className="p-6 border-t border-border space-y-2">
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -161,7 +156,7 @@ export default function MobileNav() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleLogout}
|
||||
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
className="w-full justify-start text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<LogOut className="w-5 h-5 mr-3" />
|
||||
退出登录
|
||||
@@ -174,9 +169,3 @@ export default function MobileNav() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
|
||||
// 选择会话
|
||||
selectSession: async (sessionId: number) => {
|
||||
set({ isLoading: true, error: null });
|
||||
set({ error: null });
|
||||
|
||||
try {
|
||||
const session = get().sessions.find(s => s.id === sessionId);
|
||||
|
||||
Reference in New Issue
Block a user