chore: 前端代码优化 - ESLint 配置、Prettier 格式化、加载骨架屏、React 组件规范

- 更新 ESLint 配置添加 react-hooks 规则
- 运行 Prettier 格式化所有前端文件
- 为 users、apps、audit、quotas 页面添加 Skeleton 加载骨架屏
- 修复动态组件渲染问题(CategoryIcon/TypeIcon 使用 React.createElement)
- 修复 useRef 渲染期间访问问题(改用 useMemo)
- 修复 effect 中 setState 同步调用问题
- 新增 global-not-found.tsx 404 页面
- 修复 knowledge 页面引号转义问题
This commit is contained in:
selfrelease
2026-06-23 14:43:04 +08:00
parent 6ca1c27162
commit 96b944ac8d
68 changed files with 2182 additions and 1969 deletions
+117 -101
View File
@@ -1,5 +1,6 @@
"use client";
import React from "react";
import { useState, useMemo, useCallback, useRef } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
@@ -9,15 +10,7 @@ import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
Sparkles,
RotateCcw,
Copy,
Check,
Loader2,
FileText,
ListChecks,
} from "lucide-react";
import { Sparkles, RotateCcw, Copy, Check, Loader2, FileText, ListChecks } from "lucide-react";
import { getCategoryIcon, getCategoryColor } from "@/lib/category-config";
import GovMarkdown from "@/components/ui/gov-markdown";
import { toast } from "sonner";
@@ -50,7 +43,7 @@ export default function CompletionUI({ app }: CompletionUIProps) {
setConversationId(convId);
try {
const data = await api.get<{ data: Message[] }>(
`/api/v1/apps/${app.id}/conversations/${convId}/messages`
`/api/v1/apps/${app.id}/conversations/${convId}/messages`,
);
const msgs = data.data || [];
const userMsg = msgs.find((m) => m.role === "user");
@@ -62,14 +55,16 @@ export default function CompletionUI({ app }: CompletionUIProps) {
setOutput("");
}
},
[app.id]
[app.id],
);
const appConfig = useMemo(() => {
try {
if (typeof app.app_config === "string") return JSON.parse(app.app_config);
return app.app_config || {};
} catch { return {}; }
} catch {
return {};
}
}, [app.app_config]);
const inputLabel = appConfig.input_label || "输入内容";
@@ -78,7 +73,7 @@ export default function CompletionUI({ app }: CompletionUIProps) {
const formatTemplates: Record<string, FormatTemplate> = appConfig.format_templates || {};
const hasFormats = Object.keys(formatTemplates).length > 0;
const CategoryIcon = getCategoryIcon(app.category_slug);
const CategoryIconComponent = getCategoryIcon(app.category_slug);
const categoryColor = getCategoryColor(app.category_slug);
const handleGenerate = useCallback(async () => {
@@ -121,7 +116,9 @@ export default function CompletionUI({ app }: CompletionUIProps) {
accumulated += event.answer;
setOutput(accumulated);
}
} catch { /* skip */ }
} catch {
/* skip */
}
}
}
queryClient.invalidateQueries({ queryKey: ["conversations", app.id] });
@@ -163,8 +160,12 @@ export default function CompletionUI({ app }: CompletionUIProps) {
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<div className="border-b px-3 md:px-5 py-3 flex items-center gap-2 md:gap-3 shrink-0">
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${categoryColor} shrink-0`}>
<CategoryIcon className="h-4 w-4" />
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg ${categoryColor} shrink-0`}
>
{CategoryIconComponent
? React.createElement(CategoryIconComponent, { className: "h-4 w-4" })
: null}
</div>
<div className="min-w-0">
<h1 className="font-semibold text-sm truncate">{app.name}</h1>
@@ -177,99 +178,114 @@ export default function CompletionUI({ app }: CompletionUIProps) {
</div>
<div className="flex-1 overflow-auto">
<div className="mx-auto w-full max-w-7xl px-3 md:px-6 lg:px-8 py-4 md:py-6 space-y-4 md:space-y-6">
{/* 格式选择区域 */}
{hasFormats && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<ListChecks className="h-4 w-4 text-muted-foreground" />
<label className="text-sm font-medium"></label>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
{Object.entries(formatTemplates).map(([key, fmt]) => (
<button
key={key}
onClick={() => setSelectedFormat(selectedFormat === key ? "" : key)}
className={`text-left p-3 rounded-lg border transition-all ${
selectedFormat === key
? "border-emerald-500 bg-emerald-50 ring-1 ring-emerald-500"
: "border-border hover:border-emerald-300 hover:bg-emerald-50/50"
}`}
>
<div className="text-sm font-medium truncate">{fmt.name}</div>
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{fmt.description}</div>
</button>
))}
</div>
{selectedFormat && formatTemplates[selectedFormat] && (
<div className="mt-3 p-2.5 rounded-md bg-emerald-50 border border-emerald-200/60">
<p className="text-xs text-emerald-700">
<span className="font-medium"></span>
{formatTemplates[selectedFormat].sections.join(" → ")}
</p>
<div className="mx-auto w-full max-w-7xl px-3 md:px-6 lg:px-8 py-4 md:py-6 space-y-4 md:space-y-6">
{/* 格式选择区域 */}
{hasFormats && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<ListChecks className="h-4 w-4 text-muted-foreground" />
<label className="text-sm font-medium"></label>
</div>
)}
</CardContent>
</Card>
)}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
{Object.entries(formatTemplates).map(([key, fmt]) => (
<button
key={key}
onClick={() => setSelectedFormat(selectedFormat === key ? "" : key)}
className={`text-left p-3 rounded-lg border transition-all ${
selectedFormat === key
? "border-emerald-500 bg-emerald-50 ring-1 ring-emerald-500"
: "border-border hover:border-emerald-300 hover:bg-emerald-50/50"
}`}
>
<div className="text-sm font-medium truncate">{fmt.name}</div>
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
{fmt.description}
</div>
</button>
))}
</div>
{selectedFormat && formatTemplates[selectedFormat] && (
<div className="mt-3 p-2.5 rounded-md bg-emerald-50 border border-emerald-200/60">
<p className="text-xs text-emerald-700">
<span className="font-medium"></span>
{formatTemplates[selectedFormat].sections.join(" → ")}
</p>
</div>
)}
</CardContent>
</Card>
)}
<Card>
<CardContent className="p-5 space-y-3">
<label className="text-sm font-medium">{inputLabel}</label>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={inputPlaceholder}
className="min-h-[160px] resize-none"
disabled={isLoading}
/>
<div className="flex items-center gap-2">
<Button onClick={handleGenerate} disabled={!input.trim() || isLoading} className="gap-2">
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
{isLoading ? "生成中..." : "生成"}
</Button>
{selectedFormat && formatTemplates[selectedFormat] && (
<span className="text-xs text-emerald-600 bg-emerald-50 px-2 py-1 rounded">
{formatTemplates[selectedFormat].name}
</span>
)}
{(output || input) && (
<Button variant="outline" onClick={handleReset} className="gap-2">
<RotateCcw className="h-4 w-4" />
</Button>
)}
</div>
</CardContent>
</Card>
{isLoading && !output && (
<Card>
<CardContent className="p-5 space-y-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
)}
{output && (
<Card className="border-emerald-200/60 bg-emerald-50/30">
<CardContent className="p-5">
<div className="flex items-center justify-between mb-3">
<label className="text-sm font-medium text-emerald-800">{outputLabel}</label>
<Button variant="ghost" size="sm" onClick={handleCopy} className="gap-1.5 text-xs h-7">
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? "已复制" : "复制"}
<label className="text-sm font-medium">{inputLabel}</label>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={inputPlaceholder}
className="min-h-[160px] resize-none"
disabled={isLoading}
/>
<div className="flex items-center gap-2">
<Button
onClick={handleGenerate}
disabled={!input.trim() || isLoading}
className="gap-2"
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
{isLoading ? "生成中..." : "生成"}
</Button>
{selectedFormat && formatTemplates[selectedFormat] && (
<span className="text-xs text-emerald-600 bg-emerald-50 px-2 py-1 rounded">
{formatTemplates[selectedFormat].name}
</span>
)}
{(output || input) && (
<Button variant="outline" onClick={handleReset} className="gap-2">
<RotateCcw className="h-4 w-4" />
</Button>
)}
</div>
<GovMarkdown content={output} />
</CardContent>
</Card>
)}
</div>
{isLoading && !output && (
<Card>
<CardContent className="p-5 space-y-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
)}
{output && (
<Card className="border-emerald-200/60 bg-emerald-50/30">
<CardContent className="p-5">
<div className="flex items-center justify-between mb-3">
<label className="text-sm font-medium text-emerald-800">{outputLabel}</label>
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="gap-1.5 text-xs h-7"
>
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? "已复制" : "复制"}
</Button>
</div>
<GovMarkdown content={output} />
</CardContent>
</Card>
)}
</div>
</div>
</div>
</div>