refactor: 新建工单页面全高布局优化 — 参考查询页面风格

- 全高布局 h-[calc(100vh-3rem)],去掉 max-w-6xl 限制
- 顶栏紧凑排列: 返回+标题+实例+数据库+定时+浏览+格式化+操作按钮
- 左侧 Schema 浏览面板(280px 可折叠)
- SQL 编辑器占满剩余空间
- 底部: 审核结果+变更原因(单行 input 替代 textarea)
- 按钮精简: 'SQL 检测' + '提交审批',去掉冗长提示文案
This commit is contained in:
logan committed 2026-05-25 00:45:53 +08:00
1 parent 0995abce08
commit 7109031f71
1 file changed
+131 -118
+131 -118
View File
@@ -657,180 +657,193 @@ export default function TicketCreatePage() {
}; };
/* ================================================================== */ /* ================================================================== */
/* MAIN RENDER */ /* MAIN RENDER — 全高布局,参考 QueriesPage */
/* ================================================================== */ /* ================================================================== */
return ( const [showBrowser, setShowBrowser] = useState(true);
<div className="space-y-5 max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center gap-3">
<button onClick={() => navigate('/tickets')}
className="w-9 h-9 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-all"
aria-label="返回工单列表">
<ChevronRight className="w-4 h-4 rotate-180" />
</button>
<h1 className="text-2xl font-semibold">新建数据变更工单</h1>
</div>
{/* Error */} return (
{error && ( <div className="flex h-[calc(100vh-3rem)]">
<div className="flex items-center gap-2 text-sm text-red-400 bg-red-500/10 px-4 py-3 rounded-lg border border-red-500/20"> {/* ====== 左侧 Schema 浏览面板 ====== */}
<AlertTriangle className="w-4 h-4 shrink-0" /> <span>{error}</span> {showBrowser && instanceId && database && (
<div className="w-[280px] shrink-0 border-r border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-3 py-2.5 shrink-0">
<span className="text-xs font-semibold text-[var(--dg-text-secondary)]">库表浏览</span>
<button onClick={() => setShowBrowser(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
<ChevronRight className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
{renderBrowserPanel()}
</div>
</div> </div>
)} )}
{/* === Form — single page === */} {/* ====== 主区域 ====== */}
<div className="space-y-5 bg-[var(--dg-bg-secondary)] rounded-lg p-5"> <div className="flex-1 flex flex-col min-w-0 overflow-y-auto">
{/* Row 1: Title + Instance + Database */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> {/* -- 顶栏 -- */}
<div> <div className="flex items-center gap-3 px-4 py-2.5 border-b border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] shrink-0 flex-wrap">
<label className="block text-sm font-medium mb-2">工单标题 <span className="text-red-400">*</span></label> {/* 返回 */}
<button onClick={() => navigate('/tickets')}
className="w-7 h-7 rounded-md flex items-center justify-center text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-all"
title="返回工单列表">
<ChevronRight className="w-4 h-4 rotate-180" />
</button>
{/* 工单标题 */}
<input value={title} onChange={(e) => setTitle(e.target.value)} <input value={title} onChange={(e) => setTitle(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" className="px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none min-w-[180px] max-w-[280px]"
placeholder="如:用户表添加索引" /> placeholder="工单标题" />
</div>
<div> {/* 实例 */}
<label className="block text-sm font-medium mb-2">目标实例 <span className="text-red-400">*</span></label>
<select value={instanceId} onChange={(e) => setInstanceId(e.target.value)} disabled={loadingInstances} <select value={instanceId} onChange={(e) => setInstanceId(e.target.value)} disabled={loadingInstances}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors disabled:opacity-50"> className="px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none min-w-[160px] disabled:opacity-50">
<option value="">{loadingInstances ? '加载中...' : instances.length === 0 ? '暂无变更授权实例' : '请选择实例'}</option> <option value="">{loadingInstances ? '加载中...' : '选择实例'}</option>
{instances.map((inst) => ( {instances.map((inst) => (
<option key={inst.id} value={String(inst.id)}> <option key={inst.id} value={String(inst.id)}>
{inst.name} ({inst.host}:{inst.port}) — {DB_TYPE_LABELS[inst.db_type] || inst.db_type} {inst.name} ({DB_TYPE_LABELS[inst.db_type] || inst.db_type})
</option> </option>
))} ))}
</select> </select>
</div>
<div> {/* 数据库 */}
<label className="block text-sm font-medium mb-2">数据库 <span className="text-red-400">*</span></label>
{isRedis ? ( {isRedis ? (
<select value={database} onChange={(e) => setDatabase(e.target.value)} <select value={database} onChange={(e) => setDatabase(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors"> className="px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none w-24">
{databases.length > 0 ? databases.map((db) => <option key={db} value={db}>{db}</option>) : <option value="db0">db0</option>} {databases.length > 0 ? databases.map((db) => <option key={db} value={db}>{db}</option>) : <option value="db0">db0</option>}
</select> </select>
) : databases.length > 0 ? ( ) : databases.length > 0 ? (
<select value={database} onChange={(e) => setDatabase(e.target.value)} disabled={loadingDatabases} <select value={database} onChange={(e) => setDatabase(e.target.value)} disabled={loadingDatabases}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors disabled:opacity-50"> className="px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none min-w-[140px] disabled:opacity-50">
{loadingDatabases ? <option>加载中...</option> : databases.map((db) => <option key={db} value={db}>{db}</option>)} <option value="">{loadingDatabases ? '加载中...' : '选择数据库'}</option>
{databases.map((db) => <option key={db} value={db}>{db}</option>)}
</select> </select>
) : ( ) : (
<input value={database} onChange={(e) => setDatabase(e.target.value)} disabled={!instanceId} <input value={database} onChange={(e) => setDatabase(e.target.value)} disabled={!instanceId}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors disabled:opacity-50" className="px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none min-w-[140px] disabled:opacity-50"
placeholder={instanceId ? '请输入数据库名' : '请先选择实例'} /> placeholder={instanceId ? '数据库名' : '选择实例'} />
)} )}
</div>
</div> <div className="flex-1" />
{/* 定时执行 */} {/* 定时执行 */}
<div className="flex items-center gap-4"> <label className="flex items-center gap-1.5 cursor-pointer select-none text-xs text-[var(--dg-text-muted)]">
<label className="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" checked={enableSchedule} onChange={(e) => setEnableSchedule(e.target.checked)} <input type="checkbox" checked={enableSchedule} onChange={(e) => setEnableSchedule(e.target.checked)}
className="w-4 h-4 rounded border-[var(--dg-border)] bg-[var(--dg-bg-primary)] text-blue-500 focus:ring-blue-500 focus:ring-offset-0" /> className="w-3.5 h-3.5 rounded border-[var(--dg-border)] bg-[var(--dg-bg-primary)] text-blue-500 focus:ring-blue-500 focus:ring-offset-0" />
<Clock className="w-4 h-4 text-[var(--dg-text-muted)]" /> <Clock className="w-3.5 h-3.5" />
<span className="text-sm">定时执行</span> 定时
</label> </label>
{enableSchedule && ( {enableSchedule && (
<input type="datetime-local" value={scheduledAt} onChange={(e) => setScheduledAt(e.target.value)} <input type="datetime-local" value={scheduledAt} onChange={(e) => setScheduledAt(e.target.value)}
step="1" step="1" min={new Date().toISOString().slice(0, 16)}
className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" className="px-2 py-1 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-xs focus:border-blue-400 focus:outline-none" />
min={new Date().toISOString().slice(0, 16)} />
)} )}
{enableSchedule && scheduledAt && (
<span className="text-sm font-semibold text-red-500">
将于 {new Date(scheduledAt).toLocaleString('zh-CN')} 执行
</span>
)}
</div>
{/* SQL Editor + Browser Panel */} {/* 浏览面板开关 */}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4"> {instanceId && database && (
<div className="lg:col-span-3 space-y-3"> <button onClick={() => setShowBrowser(!showBrowser)}
<div> className={`flex items-center gap-1 px-2 py-1.5 text-xs font-medium rounded-md border transition-all ${
<div className="flex items-center justify-between mb-2"> showBrowser ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
<label className="block text-sm font-medium">SQL 语句 <span className="text-red-400">*</span></label> }`}>
<div className="flex items-center gap-2"> <Database className="w-3.5 h-3.5" />
{showBrowser ? '隐藏' : '浏览'}
</button>
)}
{/* 格式化 */}
<button onClick={handleFormatSQL} disabled={!sqlContent.trim()} <button onClick={handleFormatSQL} disabled={!sqlContent.trim()}
className="flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-blue-400 bg-blue-500/10 rounded-md hover:bg-blue-500/20 transition-colors disabled:opacity-40"> className="flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-blue-400 bg-blue-500/10 rounded-md hover:bg-blue-500/20 transition-colors disabled:opacity-30">
<Sparkles className="w-3 h-3" /> 格式化 <Sparkles className="w-3 h-3" />
</button>
{/* SQL 检测 */}
<button onClick={handleValidate}
disabled={validating || !sqlContent.trim() || !instanceId || !database}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border rounded-md transition-all disabled:opacity-30 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]">
{validating ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Shield className="w-3.5 h-3.5" />}
SQL 检测
</button>
{/* 提交审批 */}
<button onClick={handleSubmit}
disabled={submitting || !validated}
className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium bg-gradient-to-r from-blue-500 to-purple-600 rounded-md text-white hover:opacity-90 disabled:opacity-40 transition-all">
{submitting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Check className="w-3.5 h-3.5" />}
提交审批
</button> </button>
<span className="text-xs text-[var(--dg-text-muted)]">
{sqlContent.trim() ? sqlContent.trim().split(/;\s*/).filter(Boolean).length + ' 条语句' : ''}
</span>
</div> </div>
{/* -- 定时执行提示 -- */}
{enableSchedule && scheduledAt && (
<div className="px-4 py-1.5 text-xs font-semibold text-red-500 bg-red-500/5 shrink-0">
定时执行:{new Date(scheduledAt).toLocaleString('zh-CN')}
</div> </div>
<SqlEditor value={sqlContent} onChange={handleSqlChange} language="sql" height="280px" )}
{/* -- 错误 -- */}
{error && (
<div className="mx-4 mt-3 flex items-center gap-2 text-sm text-red-400 bg-red-500/10 px-3 py-2 rounded-lg border border-red-500/20">
<AlertTriangle className="w-4 h-4 shrink-0" />{error}
</div>
)}
{/* -- SQL 编辑器 -- */}
<div className="flex-1 min-h-0 p-4">
<div className="dg-card overflow-hidden h-full flex flex-col">
<SqlEditor value={sqlContent} onChange={handleSqlChange} language="sql"
height="100%"
placeholder="请输入 SQL 变更语句,支持多条 SQL(以分号分隔)..." /> placeholder="请输入 SQL 变更语句,支持多条 SQL(以分号分隔)..." />
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] text-[var(--dg-text-muted)] shrink-0">
<span>
{sqlContent.trim() ? sqlContent.trim().split(/;\s*/).filter(Boolean).length + ' 条语句 · ' : ''}
{sqlContent.length} 字符
</span>
<div className="flex items-center gap-2">
{validated && <span className="flex items-center gap-1 text-green-400"><Check className="w-3 h-3" />检测通过</span>}
<span className="px-1.5 py-0.5 rounded bg-[var(--dg-bg-primary)] border border-[var(--dg-border)]">Ctrl+Enter 格式化</span>
</div> </div>
</div> </div>
<div className="lg:col-span-2">
{renderBrowserPanel()}
</div> </div>
</div> </div>
{/* Reason */} {/* -- 变更原因 + 审核结果(底部面板) -- */}
<div> <div className="border-t border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] shrink-0">
<label className="block text-sm font-medium mb-2">变更原因 <span className="text-red-400">*</span></label> {/* 审核结果 */}
<textarea value={reason} onChange={(e) => setReason(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm min-h-[80px] resize-y focus:outline-none focus:border-blue-500 transition-colors"
placeholder="请描述变更原因、影响范围、回滚方案等" />
</div>
</div>
{/* === Inline Audit Result === */}
{auditResult && ( {auditResult && (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5 space-y-4"> <div className="px-4 py-2.5 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center gap-3 text-xs">
<h3 className="text-base font-semibold flex items-center gap-2"> <span className="font-semibold flex items-center gap-1.5">
<Shield className="w-4 h-4 text-blue-400" /> SQL 审核结果 <Shield className="w-3.5 h-3.5 text-blue-400" /> 审核结果
</h3> </span>
<div className="flex items-center gap-3 text-sm">
{errorCount > 0 && <span className="text-red-400 font-medium">{errorCount} 错误</span>} {errorCount > 0 && <span className="text-red-400 font-medium">{errorCount} 错误</span>}
{warnCount > 0 && <span className="text-amber-400 font-medium">{warnCount} 警告</span>} {warnCount > 0 && <span className="text-amber-400 font-medium">{warnCount} 警告</span>}
{infoCount > 0 && <span className="text-blue-400 font-medium">{infoCount} 提示</span>} {infoCount > 0 && <span className="text-blue-400 font-medium">{infoCount} 提示</span>}
{errorCount === 0 && warnCount === 0 && infoCount === 0 && ( {errorCount === 0 && warnCount === 0 && infoCount === 0 && (
<span className="text-green-400 font-medium flex items-center gap-1"><Check className="w-4 h-4" /> 审核通过</span> <span className="text-green-400 font-medium flex items-center gap-1"><Check className="w-3.5 h-3.5" /> 通过</span>
)} )}
</div> </div>
</div>
{auditResult.findings.length > 0 && ( {auditResult.findings.length > 0 && (
<div className="space-y-2"> <div className="flex flex-wrap gap-1.5">
{auditResult.findings.map((f, idx) => ( {auditResult.findings.map((f, idx) => (
<div key={idx} className={'flex items-start gap-3 p-3 rounded-lg ' + (LEVEL_COLOR[f.level] || '')}> <span key={idx} className={'inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] ' + (LEVEL_COLOR[f.level] || '')}>
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" /> <AlertTriangle className="w-3 h-3" />
<div className="flex-1"> {LEVEL_LABEL[f.level] || f.level}: {f.message}
<div className="flex items-center gap-2 text-sm font-medium"> </span>
<span>{LEVEL_LABEL[f.level] || f.level}</span>
<span className="opacity-60">·</span>
<span className="font-mono text-xs opacity-80">{f.rule}</span>
</div>
<p className="text-sm opacity-80 mt-0.5">{f.message}</p>
</div>
</div>
))} ))}
</div> </div>
)} )}
</div> </div>
)} )}
{/* === Bottom Actions === */} {/* 变更原因 */}
<div className="flex justify-end items-center gap-3"> <div className="px-4 py-2 flex items-center gap-3">
<button onClick={handleValidate} <span className="text-xs text-[var(--dg-text-muted)] shrink-0">变更原因 <span className="text-red-400">*</span></span>
disabled={validating || !sqlContent.trim() || !instanceId || !database} <input value={reason} onChange={(e) => setReason(e.target.value)}
className="flex items-center gap-2 px-5 py-2.5 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] rounded-lg text-sm font-medium transition-all hover:bg-[var(--dg-bg-hover)] disabled:opacity-40 disabled:cursor-not-allowed"> className="flex-1 px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-sm focus:border-blue-400 focus:outline-none"
{validating placeholder="请描述变更原因、影响范围、回滚方案等" />
? <><Loader2 className="w-4 h-4 animate-spin" /> 检测中...</> </div>
: <><Shield className="w-4 h-4" /> SQL 检测</>} </div>
</button>
<button onClick={handleSubmit}
disabled={submitting || !validated}
className="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-all disabled:opacity-40 disabled:cursor-not-allowed">
{submitting
? <><Loader2 className="w-4 h-4 animate-spin" /> 提交中...</>
: validated
? <><Check className="w-4 h-4" /> 提交工单</>
: <><Check className="w-4 h-4" /> 请先通过 SQL 检测</>}
</button>
</div> </div>
</div> </div>
); );