feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user