feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
1 parent 1acb0c49af
commit e363ded445
213 files changed
+7148 -6818

No files matched your search

+137 -129
View File
@@ -1,12 +1,11 @@
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
import React, { useState, useEffect } from 'react';
import { Plus, Search, RotateCcw, Trash2, Loader2 } from 'lucide-react';
import { Plus, Search, RotateCcw, Trash2, FileCode } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
@@ -20,6 +19,9 @@ import ScriptDetailDialog from './ScriptDetailDialog';
import ScriptLogsDialog from './ScriptLogsDialog';
import ScriptCard from './ScriptCard';
import ScriptExecutionsTable from './ScriptExecutionsTable';
import { PageShell } from '@/components/layout/PageShell';
import { ContentPanel } from '@/components/layout/ContentPanel';
import { PageEmpty } from '@/components/layout/PageEmpty';
import type { JsonData } from '@/types/common';
export function ScriptManager() {
@@ -34,9 +36,9 @@ export function ScriptManager() {
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// 弹窗状态
const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑
const [detailOpen, setDetailOpen] = useState(false); // 查看
const [logsOpen, setLogsOpen] = useState(false); // 执行日志
const [editorOpen, setEditorOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
const [selectedExec, setSelectedExec] = useState<ExecutionRecord | null>(null);
@@ -74,8 +76,6 @@ export function ScriptManager() {
fetchExecutions();
}, []);
// ---- 新建/编辑 ----
const openCreate = () => {
setEditingId(null);
setFormData({
@@ -100,7 +100,6 @@ export function ScriptManager() {
};
const handleTypeChange = (t: string) => {
// 新建状态下切换类型时若内容仍为默认模板则同步替换,避免用户手动清空
setFormData((prev) => {
const isDefaultSample = scriptTypes.some(s => s.sample === prev.content) || prev.content === '';
const match = scriptTypes.find(s => s.value === t);
@@ -140,8 +139,6 @@ export function ScriptManager() {
}
};
// ---- 复制 ----
const handleClone = async (sc: Script) => {
try {
await scriptApi.createScript({
@@ -159,8 +156,6 @@ export function ScriptManager() {
}
};
// ---- 删除 ----
const handleDelete = async (sc: Script) => {
const ok = await _confirm({
title: '删除脚本',
@@ -178,8 +173,6 @@ export function ScriptManager() {
}
};
// ---- 执行 ----
const handleExecute = async (sc: Script) => {
const ok = await _confirm({
title: '执行脚本',
@@ -207,8 +200,6 @@ export function ScriptManager() {
}
};
// ---- 辅助 ----
const filteredScripts = scripts.filter(sc => {
const q = searchQuery.toLowerCase();
const matchSearch = sc.name.toLowerCase().includes(q) ||
@@ -218,8 +209,6 @@ export function ScriptManager() {
return matchSearch && matchType;
});
// ---- 批量选择 / 批量删除 ----
const toggleSelect = (id: number) => {
setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
};
@@ -249,123 +238,144 @@ export function ScriptManager() {
}
};
// 页面级 Tab 配置 // Logan.Li
const pageTabs = [
{ id: 'scripts', label: '脚本库' },
{ id: 'jobs', label: '执行记录' },
];
const scriptsToolbar = (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 dg-text-muted" />
<Input
placeholder="搜索名称、编码、描述..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 dg-input"
/>
</div>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[140px] dg-input">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部类型</SelectItem>
{scriptTypes.map(t => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
return (
<div className="space-y-6">
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
<TabsList className="grid w-full grid-cols-2 lg:w-[360px]">
<TabsTrigger value="scripts">脚本库</TabsTrigger>
<TabsTrigger value="jobs">执行记录</TabsTrigger>
</TabsList>
{/* ============ 脚本库 ============ */}
<TabsContent value="scripts" className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索名称、编码、描述..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
<>
<PageShell
variant="list"
title="脚本库"
subtitle="脚本管理与执行记录"
icon={<FileCode className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={(id) => {
setActiveTab(id);
if (id === 'jobs') fetchExecutions();
}}
toolbar={activeTab === 'scripts' ? scriptsToolbar : undefined}
loading={activeTab === 'scripts' && loading}
actions={activeTab === 'scripts' ? (
<>
<Button variant="outline" className="dg-btn-secondary" onClick={fetchScripts}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
</>
) : (
<Button variant="outline" className="dg-btn-secondary" onClick={fetchExecutions}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
)}
>
{activeTab === 'scripts' ? (
<>
{selectedIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] mb-4">
<div className="flex items-center gap-3">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
<span className="text-sm dg-text-secondary">
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button
variant="outline"
size="sm"
className="dg-btn-danger dg-btn-sm"
onClick={handleBatchDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
</div>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部类型</SelectItem>
{scriptTypes.map(t => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={fetchScripts}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
</div>
</div>
{/* 批量操作栏 */}
{selectedIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
<div className="flex items-center gap-3">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
<span className="text-sm text-[var(--dg-text-secondary)]">
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
onClick={handleBatchDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
</div>
)}
{/* 脚本卡片列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
<div className="col-span-full text-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin inline mr-2" />
加载中...
</div>
) : filteredScripts.length === 0 ? (
<div className="col-span-full text-center py-8 text-muted-foreground">
{scripts.length === 0 ? '暂无脚本,点击「新建脚本」创建第一个' : '未匹配到符合条件的脚本'}
</div>
) : (
filteredScripts.map((sc) => (
<ScriptCard
key={sc.id}
script={sc}
selected={selectedIds.includes(sc.id)}
executing={executing}
onToggleSelect={() => toggleSelect(sc.id)}
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
onExecute={() => handleExecute(sc)}
onEdit={() => openEdit(sc)}
onClone={() => handleClone(sc)}
onDelete={() => handleDelete(sc)}
/>
))
)}
</div>
</TabsContent>
{/* ============ 执行记录 ============ */}
<TabsContent value="jobs">
{filteredScripts.length === 0 ? (
<PageEmpty
title={scripts.length === 0 ? '暂无脚本' : '未匹配到符合条件的脚本'}
description={scripts.length === 0 ? '点击「新建脚本」创建第一个' : undefined}
action={scripts.length === 0 ? (
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
) : undefined}
/>
) : (
<ContentPanel>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredScripts.map((sc) => (
<ScriptCard
key={sc.id}
script={sc}
selected={selectedIds.includes(sc.id)}
executing={executing}
onToggleSelect={() => toggleSelect(sc.id)}
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
onExecute={() => handleExecute(sc)}
onEdit={() => openEdit(sc)}
onClone={() => handleClone(sc)}
onDelete={() => handleDelete(sc)}
/>
))}
</div>
</ContentPanel>
)}
</>
) : (
<ScriptExecutionsTable
executions={executions}
onRefresh={fetchExecutions}
onViewLogs={(job) => { setSelectedExec(job); setLogsOpen(true); }}
/>
</TabsContent>
</Tabs>
)}
</PageShell>
{/* ===== 新建/编辑脚本对话框 ===== */}
<ScriptFormDialog
open={editorOpen}
onOpenChange={setEditorOpen}
@@ -377,7 +387,6 @@ export function ScriptManager() {
onSave={handleSave}
/>
{/* ===== 查看脚本内容 ===== */}
<ScriptDetailDialog
open={detailOpen}
onOpenChange={setDetailOpen}
@@ -385,12 +394,11 @@ export function ScriptManager() {
onEdit={openEdit}
/>
{/* ===== 执行日志 ===== */}
<ScriptLogsDialog
open={logsOpen}
onOpenChange={setLogsOpen}
exec={selectedExec}
/>
</div>
</>
);
}
}