refactor: style css -> tailwindcss

This commit is contained in:
tao.chen
2026-08-24 18:34:43 +08:00
parent 9ce0b106fa
commit 6b7701852f
9 changed files with 475 additions and 2142 deletions
+69 -90
View File
@@ -1,4 +1,36 @@
@import "tailwindcss";
@import "tailwindcss/theme.css";
@import "tailwindcss/utilities.css";
@theme {
--font-sans: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
--color-brand: #1978d4;
--color-brand-strong: #0e5fb9;
--color-brand-soft: #edf6ff;
--color-success: #18b979;
--color-success-strong: #0f7d55;
--color-success-soft: #e8f7f1;
--color-danger: #d75b5b;
--color-danger-strong: #a94f4f;
--color-danger-soft: #fff6f6;
--color-warning: #e6a33c;
--color-warning-soft: #fff0ee;
--color-ink: #27394d;
--color-ink-muted: #5d7186;
--color-ink-subtle: #8b99a8;
--color-line: #dce4ec;
--color-line-soft: #edf1f5;
--color-bg: #f3f6f9;
--color-bg-panel: #ffffff;
--color-bg-canvas: #f9fbfd;
--color-bg-log: #17212b;
}
@font-face {
font-family: "Inter";
@@ -16,97 +48,44 @@
font-display: swap;
}
:root {
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
color: #1f354b;
background: #f3f6f9;
font-synthesis: none;
text-rendering: optimizeLegibility;
@layer base {
:root {
font-family: var(--font-sans);
color: #1f354b;
background: #f3f6f9;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
html,
body {
margin: 0;
min-width: 1024px;
min-height: 100%;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; min-width: 1024px; min-height: 100%; }
button, input, select, textarea { font: inherit; }
@layer utilities {
.scrollbar-thin::-webkit-scrollbar {
width: 5px;
}
.auth-loading,
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.auth-loading {
gap: 12px;
color: #61758a;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #d9e1e9;
}
.auth-loading > span {
width: 20px;
height: 20px;
border: 2px solid #bfd5e9;
border-top-color: #1677ff;
border-radius: 50%;
animation: auth-spin 0.8s linear infinite;
}
@keyframes auth-spin { to { transform: rotate(360deg); } }
.login-page {
padding: 48px;
background:
radial-gradient(circle at 20% 15%, rgba(22, 119, 255, 0.12), transparent 34%),
linear-gradient(145deg, #eef5fb, #f8fafc 55%, #edf3f8);
}
.login-card {
width: 420px;
padding: 42px;
border: 1px solid #dbe6ef;
border-radius: 18px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 22px 60px rgba(37, 63, 88, 0.14);
}
.login-brand {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 13px;
color: white;
background: linear-gradient(145deg, #1177e8, #25a1f2);
}
.login-kicker {
margin: 26px 0 8px;
color: #1677ff;
font-size: 11px;
font-weight: 750;
letter-spacing: 0.14em;
}
.login-card h1 { margin: 0; font-size: 27px; }
.login-description { margin: 10px 0 28px; color: #728398; }
.login-card form { display: grid; gap: 18px; }
.login-card label { display: grid; gap: 8px; color: #465b70; font-size: 13px; }
.login-card input {
width: 100%;
padding: 12px 14px;
border: 1px solid #cad8e5;
border-radius: 9px;
outline: none;
color: #18334f;
background: white;
}
.login-card input:focus { border-color: #1677ff; box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12); }
.login-card button {
min-height: 44px;
border: 0;
border-radius: 9px;
color: white;
background: #1677ff;
cursor: pointer;
}
.login-card button:disabled { opacity: 0.65; cursor: wait; }
.login-error { margin: -4px 0 0; color: #d4380d; font-size: 13px; }
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #c4d0dc;
}
}
@@ -58,24 +58,27 @@ function ArtifactListImpl({
}
return (
<section className="schedule-panel schedule-artifacts-panel">
<div className="schedule-panel__title">
<div>
<strong></strong>
<span className="schedule-panel__count">{artifacts.length}</span>
<section className="flex min-h-0 flex-col overflow-hidden">
<div className="flex min-h-[48px] items-center justify-between px-3">
<div className="flex items-center gap-[7px]">
<strong className="text-[14px] text-[#223247]"></strong>
<span className="inline-grid h-[20px] min-w-[20px] place-items-center rounded-[10px] bg-[#eef3f8] px-[5px] text-[10px] text-[#718195]">
{artifacts.length}
</span>
</div>
</div>
<label className="schedule-search">
<label className="mx-[10px] mb-[7px] flex min-h-[35px] items-center gap-[7px] rounded-[6px] border border-[#dce5ed] bg-white px-[9px] text-[#95a3b2] focus-within:border-[#8db9e8] focus-within:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]">
<Icon name="search" size={15} />
<input
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
value={keyword}
placeholder="搜索稳定版本"
onChange={(event) => onKeywordChange(event.target.value)}
/>
</label>
<div className="artifact-list">
<div className="scrollbar-thin min-h-0 overflow-auto px-2 pb-[9px]">
{groups.length === 0 ? (
<p className="schedule-empty">
<p className="my-[18px_10px] text-center text-[10px] leading-[1.6] text-[#9aa6b3]">
"构建脚本"
</p>
) : (
@@ -84,33 +87,50 @@ function ArtifactListImpl({
const scriptType = items[0].script_type;
const iconName = scriptType === "notebook" ? "notebook" : "python";
return (
<div key={scriptName} className="artifact-group">
<div
key={scriptName}
className="mb-1.5 overflow-hidden rounded-[8px] border border-line-soft bg-white transition-[border-color,box-shadow] duration-[160ms] hover:border-[#e1e8ef]"
>
<button
type="button"
className="artifact-group__header"
className="flex min-h-[42px] w-full cursor-pointer select-none items-center justify-between gap-2 border-0 bg-transparent px-[7px] py-[7px] text-left text-inherit hover:bg-[#f8fafc] min-[1281px]:px-2"
aria-expanded={isExpanded}
onClick={() => toggle(scriptName)}
>
<span className="artifact-group__main">
<span className={`artifact-group__chevron${isExpanded ? " is-open" : ""}`}>
<span className="flex min-w-0 items-center gap-[7px]">
<span
data-state={isExpanded ? "open" : "closed"}
className="h-[14px] w-[14px] flex-none text-[#9aa7b5] transition-transform duration-[180ms] data-[state=open]:rotate-90"
>
<Icon name="chevron" size={14} />
</span>
<span className={`artifact-group__icon artifact-group__icon--${scriptType}`}>
<span
className={`grid h-[27px] w-[27px] flex-none place-items-center rounded-[6px] ${
scriptType === "notebook"
? "bg-[#fff0ee] text-[#dc5c52]"
: "bg-[#eaf3ff] text-[#326fc1]"
}`}
>
<Icon name={iconName} size={14} />
</span>
<span className="artifact-group__name" title={scriptName}>
<span
className="max-w-[125px] min-w-0 truncate text-[11px] font-semibold text-[#33465a] min-[1281px]:max-w-none"
title={scriptName}
>
{scriptName}
</span>
</span>
{items.length > 1 && (
<span className="artifact-group__count">{items.length}</span>
<span className="inline-flex flex-none items-center rounded-full border border-[#e5eaf0] bg-[#f5f7fa] px-1.5 py-[2px] text-[9px] leading-[1.3] whitespace-nowrap text-[#8794a3]">
{items.length}
</span>
)}
</button>
{isExpanded && (
<div className="artifact-versions">
<div className="flex flex-col gap-[5px] border-t border-line-soft bg-[#f8fafc] p-[7px_7px_7px_27px] min-[1281px]:pl-[29px]">
{items.map((artifact) => (
<div
className="artifact-version"
className="group flex min-h-[34px] cursor-grab select-none items-center justify-between gap-[7px] rounded-[6px] border border-[#e4e9ee] bg-white p-1.5 transition-[border-color,background-color] duration-[140ms] hover:border-[#a8c8e9] hover:bg-[#fbfdff] active:cursor-grabbing min-[1281px]:px-[7px]"
draggable
key={artifact.versions_id}
onDragStart={(event) => {
@@ -124,18 +144,18 @@ function ArtifactListImpl({
onContextMenu={(event) => onContextMenu(event, artifact)}
onDoubleClick={() => onDoubleClick(artifact)}
>
<span className="artifact-version__main">
<span className="artifact-version__handle">
<span className="flex min-w-0 items-center gap-[5px]">
<span className="h-[13px] w-[13px] flex-none text-[#c4cdd6] group-hover:text-[#7e8d9d]">
<Icon name="grip-vertical" size={13} />
</span>
<span className="artifact-version__name">
<span className="flex-none text-[10px] font-semibold text-[#526477]">
{artifact.version_label}
</span>
<span className="artifact-version__commit">
<span className="min-w-0 truncate font-mono text-[8px] text-[#9aa5b1]">
{shortHash(artifact.content_hash)}
</span>
</span>
<span className="artifact-version__action">
<span className="grid h-[18px] w-[18px] flex-none place-items-center rounded-[4px] text-[#c1cad3] group-hover:bg-[#eef6ff] group-hover:text-[#287ac5]">
<Icon name="external" size={12} />
</span>
</div>
@@ -147,9 +167,9 @@ function ArtifactListImpl({
})
)}
</div>
<p className="artifact-tip artifact-tip--footer">
<p className="mx-[10px] mb-[9px] mt-[3px] flex items-center justify-center gap-1 rounded-[6px] border border-line-soft bg-[#f8fafc] p-[7px_8px] text-center text-[9px] leading-[1.45] text-[#8795a4]">
<span></span>
<span className="artifact-tip__grip">
<span className="inline-flex items-center justify-center text-[#9eabb8]">
<Icon name="grip-vertical" size={12} />
</span>
<span></span>
@@ -158,4 +178,4 @@ function ArtifactListImpl({
);
}
export const ArtifactList = memo(ArtifactListImpl);
export const ArtifactList = memo(ArtifactListImpl);
@@ -19,33 +19,48 @@ export function NodeInspector({
onDelete: () => void;
}) {
return (
<div className="schedule-inspector">
<section className="node-inspector-title">
<span className={`artifact-card__icon artifact-card__icon--${node.version.script_type}`}>
<div className="flex min-h-full flex-col">
<section className="flex flex-row items-center gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<span
className={`grid h-[31px] w-[31px] flex-none place-items-center rounded-[6px] ${
node.version.script_type === "notebook"
? "bg-[#fff0ee] text-[#e05e53]"
: "bg-[#eaf3ff] text-[#326fc1]"
}`}
>
<Icon
name={node.version.script_type === "notebook" ? "notebook" : "python"}
size={17}
/>
</span>
<div>
<small></small>
<strong>{node.node_key}</strong>
<div className="flex min-w-0 flex-col gap-[3px]">
<small className="text-[9px] text-[#96a2af]"></small>
<strong className="truncate font-mono text-[11px] text-[#30465d]">
{node.node_key}
</strong>
</div>
</section>
<section>
<h3></h3>
<div className="version-readonly">
<strong>{node.version.script_name}</strong>
<span>{node.version.version_label}</span>
<small>versions_id: {node.versions_id}</small>
<small>SHA-256: {shortHash(node.version.content_hash)}</small>
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<div className="flex flex-col gap-[5px] rounded-[6px] border border-[#dae6f2] bg-[#f6faff] p-[9px]">
<strong className="text-[11px] text-[#35516e]">{node.version.script_name}</strong>
<span className="w-fit rounded-[8px] bg-[#e3f0fd] px-1.5 py-[2px] text-[9px] font-bold text-[#176ec5]">
{node.version.version_label}
</span>
<small className="truncate font-mono text-[8px] text-[#8190a0]">
versions_id: {node.versions_id}
</small>
<small className="truncate font-mono text-[8px] text-[#8190a0]">
SHA-256: {shortHash(node.version.content_hash)}
</small>
</div>
</section>
<section>
<h3></h3>
<label>
<span></span>
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.nodeName}
onChange={(event) => onChange({
...form,
@@ -53,12 +68,13 @@ export function NodeInspector({
})}
/>
</label>
<div className="inspector-grid">
<label>
<span></span>
<div className="grid grid-cols-2 gap-2">
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
type="number"
min="1"
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.timeoutSeconds}
onChange={(event) => onChange({
...form,
@@ -66,12 +82,13 @@ export function NodeInspector({
})}
/>
</label>
<label>
<span></span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
type="number"
min="0"
max="10"
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.retryCount}
onChange={(event) => onChange({
...form,
@@ -80,12 +97,13 @@ export function NodeInspector({
/>
</label>
</div>
<div className="inspector-grid">
<label>
<span></span>
<div className="grid grid-cols-2 gap-2">
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
type="number"
min="0"
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.retryIntervalSec}
onChange={(event) => onChange({
...form,
@@ -93,9 +111,10 @@ export function NodeInspector({
})}
/>
</label>
<label>
<span>Python </span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]">Python </span>
<select
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.pythonVersion}
onChange={(event) => onChange({
...form,
@@ -111,12 +130,12 @@ export function NodeInspector({
</label>
</div>
</section>
<section>
<h3></h3>
<label>
<span>arguments_json</span>
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]">arguments_json</span>
<textarea
className="json-editor"
className="w-full resize-y rounded-[5px] border border-line p-[7px_8px] font-mono text-[10px] leading-[1.5] !bg-[#f8fafc] !text-[#37516a] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
rows={5}
spellCheck={false}
value={form.argumentsJson}
@@ -126,10 +145,10 @@ export function NodeInspector({
})}
/>
</label>
<label>
<span>env_refs_json</span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]">env_refs_json</span>
<textarea
className="json-editor"
className="w-full resize-y rounded-[5px] border border-line p-[7px_8px] font-mono text-[10px] leading-[1.5] !bg-[#f8fafc] !text-[#37516a] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
rows={4}
spellCheck={false}
value={form.envRefsJson}
@@ -140,9 +159,9 @@ export function NodeInspector({
/>
</label>
</section>
<div className="node-inspector-actions">
<div className="mt-auto grid grid-cols-[1fr_90px] gap-[7px] p-[14px]">
<button
className="inspector-primary-button"
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-brand bg-brand text-[11px] text-white [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onSave}
@@ -150,7 +169,7 @@ export function NodeInspector({
</button>
<button
className="inspector-danger-button"
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-[#efc6c6] bg-[#fff7f7] text-[11px] text-[#bd4f4f] [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onDelete}
+122 -52
View File
@@ -216,13 +216,16 @@ export function RunHistory({
};
return (
<section className="schedule-run-history">
<header>
<div>
<strong></strong>
<span>{runs.length}</span>
<section className="flex min-h-0 flex-col border-t border-[#e0e7ee] bg-[#fbfcfe]">
<header className="flex min-h-[44px] items-center justify-between border-b border-[#e8edf2] bg-white px-3">
<div className="flex items-center gap-[7px]">
<strong className="text-[12px] text-[#2b4056]"></strong>
<span className="inline-grid h-[18px] min-w-[18px] place-items-center rounded-[9px] bg-[#edf2f7] text-[9px] text-[#71849a]">
{runs.length}
</span>
</div>
<button
className="grid h-[27px] w-[27px] cursor-pointer place-items-center rounded-[5px] border border-[#dce5ee] bg-white text-[#66809a] disabled:cursor-not-allowed disabled:opacity-[0.45]"
type="button"
aria-label="刷新运行记录"
disabled={disabled || loading}
@@ -231,34 +234,50 @@ export function RunHistory({
<Icon name="refresh" size={13} />
</button>
</header>
<div className="schedule-run-list">
<div className="flex min-h-0 flex-1 flex-col gap-[7px] overflow-auto p-[9px]">
{loading && runs.length === 0 ? (
<p></p>
<p className="my-auto p-3 text-center text-[10px] leading-[1.6] text-[#929fac]">
</p>
) : runs.length === 0 ? (
<p></p>
<p className="my-auto p-3 text-center text-[10px] leading-[1.6] text-[#929fac]">
</p>
) : (
runs.map((run) => {
const expanded = expandedRunId === run.run_id;
const detail = runDetails[run.run_id];
return (
<article
className={`schedule-run-card${expanded ? " is-expanded" : ""}`}
data-state={expanded ? "expanded" : "collapsed"}
className="flex-none overflow-hidden rounded-[6px] border border-[#e1e8ef] bg-white data-[state=expanded]:border-[#b9d6f1] data-[state=expanded]:shadow-[0_3px_10px_rgb(38_117_185_/_8%)]"
key={run.run_id}
>
<div
className="schedule-run-summary"
className="grid w-full items-center gap-[7px] border-0 bg-white p-2 text-left text-inherit [grid-template-columns:8px_minmax(0,1fr)_auto_auto] hover:bg-[#f7fbff]"
>
<span className={`run-status-dot is-${run.run_status}`} />
<span className="schedule-run-summary-copy">
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong>
<small>
<span
data-state={run.run_status}
className="mt-[3px] size-[7px] rounded-full bg-slate-400 data-[state=queued]:bg-warning data-[state=running]:bg-blue-500 data-[state=succeeded]:bg-emerald-500 data-[state=failed]:bg-red-500 data-[state=cancelled]:bg-red-500 data-[state=timed_out]:bg-red-500 data-[state=skipped]:bg-slate-400"
/>
<span className="flex min-w-0 flex-col gap-[2px]">
<strong className="text-[10px] text-[#425970]">
{RUN_STATUS_LABELS[run.run_status]}
</strong>
<small className="text-[8px] text-ink-subtle">
{formatTime(run.queued_at)} · {formatDuration(run.duration_ms)}
</small>
{run.error_message && <span>{run.error_message}</span>}
{run.error_message && (
<span className="truncate text-[8px] leading-[1.4] text-[#b65353]">
{run.error_message}
</span>
)}
</span>
<code title={run.run_id}>{run.run_id.slice(-8)}</code>
<code className="font-mono text-[8px] text-[#8b9bad]" title={run.run_id}>
{run.run_id.slice(-8)}
</code>
<button
className="schedule-run-result-button"
className="min-h-[27px] cursor-pointer whitespace-nowrap rounded-[5px] border border-[#bcd4e9] bg-[#f5faff] px-[9px] text-[9px] text-[#2374ba] [font-weight:650] hover:border-[#7eafe0] hover:bg-[#eaf5ff]"
type="button"
aria-haspopup="dialog"
onClick={() => toggleRun(run.run_id)}
@@ -270,25 +289,29 @@ export function RunHistory({
{expanded && (
<>
<button
className="schedule-run-result-backdrop"
className="fixed inset-0 z-[40] cursor-default border-0 bg-[rgb(10_27_45_/_46%)]"
type="button"
aria-label="关闭运行结果"
onClick={() => toggleRun(run.run_id)}
/>
<section
className="schedule-run-result-modal"
className="fixed left-1/2 top-1/2 z-[41] grid max-h-[min(760px,calc(100vh-64px))] w-[min(780px,calc(100vw-48px))] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-[12px] border border-[#d8e2eb] bg-white shadow-[0_24px_80px_rgb(8_27_48_/_30%)] [grid-template-rows:auto_minmax(0,1fr)]"
role="dialog"
aria-modal="true"
aria-label="调度运行结果"
>
<header className="schedule-run-result-modal__header">
<div>
<span></span>
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong>
<code>{run.run_id}</code>
<header className="flex items-center justify-between gap-[18px] border-b border-[#e3eaf0] bg-white p-[17px_20px]">
<div className="grid min-w-0 items-center gap-[5px_10px] [grid-template-columns:auto_auto]">
<span className="text-[17px] font-bold text-[#1f344a]"></span>
<strong className="justify-self-start rounded-[11px] bg-success-soft px-2 py-[3px] text-[10px] text-[#17805a]">
{RUN_STATUS_LABELS[run.run_status]}
</strong>
<code className="col-span-2 truncate font-mono text-[10px] text-[#8291a1]">
{run.run_id}
</code>
</div>
<button
className="icon-button"
className="grid h-[34px] w-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]"
type="button"
aria-label="关闭运行结果"
onClick={() => toggleRun(run.run_id)}
@@ -296,49 +319,90 @@ export function RunHistory({
<Icon name="close" size={16} />
</button>
</header>
<div className="schedule-run-detail">
<div className="min-h-0 overflow-auto bg-[#f8fbfe] p-[18px]">
{detailLoadingId === run.run_id && !detail ? (
<p className="run-detail-message"></p>
<p className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6]">
</p>
) : detailErrors[run.run_id] ? (
<p className="run-detail-message is-error">
<p
data-state="error"
className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6] data-[state=error]:text-[#bf5656]"
>
{detailErrors[run.run_id]}
</p>
) : detail ? (
<>
<dl className="schedule-run-meta">
<div><dt></dt><dd>{TRIGGER_TYPE_LABELS[detail.trigger_type]}</dd></div>
<div><dt></dt><dd>v{detail.workflow_version}</dd></div>
<div><dt></dt><dd>{formatTime(detail.started_at)}</dd></div>
<div><dt></dt><dd>{formatTime(detail.finished_at)}</dd></div>
<dl className="mb-[14px] grid gap-[10px_18px] [grid-template-columns:repeat(4,minmax(0,1fr))]">
<div className="min-w-0">
<dt className="mb-1 text-[10px] text-[#95a2af]"></dt>
<dd className="m-0 truncate text-[12px] text-[#41566c]">
{TRIGGER_TYPE_LABELS[detail.trigger_type]}
</dd>
</div>
<div className="min-w-0">
<dt className="mb-1 text-[10px] text-[#95a2af]"></dt>
<dd className="m-0 truncate text-[12px] text-[#41566c]">
v{detail.workflow_version}
</dd>
</div>
<div className="min-w-0">
<dt className="mb-1 text-[10px] text-[#95a2af]"></dt>
<dd className="m-0 truncate text-[12px] text-[#41566c]">
{formatTime(detail.started_at)}
</dd>
</div>
<div className="min-w-0">
<dt className="mb-1 text-[10px] text-[#95a2af]"></dt>
<dd className="m-0 truncate text-[12px] text-[#41566c]">
{formatTime(detail.finished_at)}
</dd>
</div>
</dl>
{(detail.error_code || detail.error_message) && (
<div className="schedule-run-error">
{detail.error_code && <code>{detail.error_code}</code>}
{detail.error_message && <p>{detail.error_message}</p>}
<div className="mb-2 rounded-[5px] border border-[#f0cece] bg-danger-soft p-[7px] text-danger-strong">
{detail.error_code && <code className="text-[8px] font-bold">{detail.error_code}</code>}
{detail.error_message && (
<p className="mt-[3px] text-[8px] leading-[1.45]">{detail.error_message}</p>
)}
</div>
)}
<div className="schedule-node-run-list">
<div className="flex flex-col gap-2.5">
{detail.node_runs.length === 0 ? (
<p className="run-detail-message"></p>
<p className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6]">
</p>
) : detail.node_runs.map((nodeRun) => {
const node = nodes.find((item) => item.node_id === nodeRun.node_id);
const logState = logStates[nodeRun.node_run_id];
const logOpen = openLogNodeRunIds.has(nodeRun.node_run_id);
return (
<article className="schedule-node-run" key={nodeRun.node_run_id}>
<header>
<span className={`run-status-dot is-${nodeRun.node_status}`} />
<strong>{node?.node_name ?? nodeRun.node_id.slice(-8)}</strong>
<span>{NODE_STATUS_LABELS[nodeRun.node_status]}</span>
<article className="rounded-[7px] border border-[#dde7f0] bg-white p-3" key={nodeRun.node_run_id}>
<header className="grid items-start gap-1.5 [grid-template-columns:8px_minmax(0,1fr)_auto]">
<span
data-state={nodeRun.node_status}
className="mt-[3px] size-[7px] rounded-full bg-slate-400 data-[state=queued]:bg-warning data-[state=running]:bg-blue-500 data-[state=succeeded]:bg-emerald-500 data-[state=failed]:bg-red-500 data-[state=cancelled]:bg-red-500 data-[state=timed_out]:bg-red-500 data-[state=skipped]:bg-slate-400"
/>
<strong className="truncate text-[13px] text-[#40566c]">
{node?.node_name ?? nodeRun.node_id.slice(-8)}
</strong>
<span className="text-[10px] text-[#71869a]">
{NODE_STATUS_LABELS[nodeRun.node_status]}
</span>
</header>
<div className="schedule-node-run-meta">
<div className="ml-[14px] mt-[5px] flex flex-wrap gap-[3px_8px] text-[10px] text-[#8a98a7]">
<span> {nodeRun.attempt_no} </span>
<span>{formatDuration(nodeRun.duration_ms)}</span>
<span>退 {nodeRun.exit_code ?? "—"}</span>
</div>
{nodeRun.message && <p>{nodeRun.message}</p>}
<div className="schedule-node-run-actions">
{nodeRun.message && (
<p className="ml-[14px] mt-[5px] text-[10px] leading-[1.45] text-[#6c7e90]">
{nodeRun.message}
</p>
)}
<div className="ml-[14px] mt-[7px] flex gap-1.5">
<button
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
type="button"
disabled={!nodeRun.logs_object_id || logState?.loading}
onClick={() => void toggleLog(run.run_id, nodeRun.node_run_id)}
@@ -346,6 +410,7 @@ export function RunHistory({
{logState?.loading ? "读取中…" : logOpen ? "收起日志" : "查看日志"}
</button>
<button
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
type="button"
disabled={
!nodeRun.logs_object_id
@@ -358,6 +423,7 @@ export function RunHistory({
: "下载日志"}
</button>
<button
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
type="button"
disabled={
!nodeRun.result_object_id
@@ -371,24 +437,28 @@ export function RunHistory({
</button>
</div>
{artifactErrors[nodeRun.node_run_id] && (
<p className="schedule-artifact-error">
<p className="!text-[#b65353]">
{artifactErrors[nodeRun.node_run_id]}
</p>
)}
{logOpen && (
<div className="schedule-node-log">
<div className="ml-[14px] mt-[7px] overflow-hidden rounded-[4px] border border-slate-700 bg-bg-log">
{logState?.fileName && (
<header>
<header className="flex items-center justify-between border-b border-slate-700 bg-bg-log p-[5px_7px] text-[10px] text-slate-300">
<span>{logState.fileName}</span>
<small>
<small className="text-[#8292a1]">
{formatSize(new TextEncoder().encode(logState.content ?? "").length)}
</small>
</header>
)}
{logState?.error ? (
<p>{logState.error}</p>
<p className="m-0 max-h-[260px] overflow-auto p-[11px] font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-slate-200">
{logState.error}
</p>
) : (
<pre>{logState?.content ?? "正在读取日志…"}</pre>
<pre className="m-0 max-h-[260px] overflow-auto p-[11px] font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-slate-200">
{logState?.content ?? "正在读取日志…"}
</pre>
)}
</div>
)}
@@ -8,15 +8,19 @@ export function ScheduleCanvasHeader({
onCancelLink: () => void;
}) {
return (
<div className="schedule-canvas-title">
<div>
<strong></strong>
<span>
<div className="flex min-h-[53px] items-center justify-between border-b border-[#e4eaf0] px-3">
<div className="flex min-w-0 flex-col gap-[3px]">
<strong className="text-[14px] text-ink"></strong>
<span className="hidden truncate text-[10px] text-[#93a0ae] 2xl:block">
线
</span>
</div>
{linkSourceId && (
<button type="button" onClick={onCancelLink}>
<button
className="inline-flex min-h-[28px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-3 text-[12px] font-semibold text-[#b25a4f] hover:border-[#aac6e5] hover:bg-[#f6faff] hover:text-[#126aca]"
type="button"
onClick={onCancelLink}
>
线
</button>
)}
@@ -32,20 +32,23 @@ export function ScheduleInspector({
}) {
if (!schedule) {
return (
<div className="schedule-inspector-empty">
<div className="flex min-h-full flex-col items-center justify-center p-[30px] text-center text-[#9aa8b6] [&>svg]:mb-[10px] [&>svg]:text-[#93b4d6]">
<Icon name="settings" size={28} />
<strong></strong>
<p>Cron </p>
<strong className="text-[14px] text-[#516a82]"></strong>
<p className="text-[11px] leading-[1.6]">
Cron
</p>
</div>
);
}
return (
<div className="schedule-inspector">
<section>
<h3></h3>
<label>
<span></span>
<div className="flex min-h-full flex-col">
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.scheduleName}
onChange={(event) => onChange({
...form,
@@ -53,9 +56,10 @@ export function ScheduleInspector({
})}
/>
</label>
<label>
<span></span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<textarea
className="w-full resize-y rounded-[5px] border border-line bg-white p-[7px_8px] text-[11px] leading-[1.5] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
rows={2}
value={form.description}
onChange={(event) => onChange({
@@ -64,13 +68,14 @@ export function ScheduleInspector({
})}
/>
</label>
<label className="schedule-switch-row">
<span>
<b></b>
<small>DAG </small>
<label className="flex flex-row items-center justify-between rounded-[6px] bg-[#f5f8fb] p-[8px_9px]">
<span className="flex flex-col gap-[2px]">
<b className="text-[11px] text-[#45596f]"></b>
<small className="text-[9px] font-normal text-[#94a1ae]">DAG </small>
</span>
<input
type="checkbox"
className="h-[18px] w-[34px] accent-brand"
checked={form.enabled}
onChange={(event) => onChange({
...form,
@@ -80,11 +85,12 @@ export function ScheduleInspector({
</label>
</section>
<section>
<h3></h3>
<label>
<span></span>
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<select
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.triggerType}
onChange={(event) => onChange({
...form,
@@ -99,9 +105,10 @@ export function ScheduleInspector({
</label>
{form.triggerType === "cron" && (
<>
<label>
<span>Cron </span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]">Cron </span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.cronExpression}
onChange={(event) => onChange({
...form,
@@ -109,9 +116,10 @@ export function ScheduleInspector({
})}
/>
</label>
<label>
<span></span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.timezone}
onChange={(event) => onChange({
...form,
@@ -120,7 +128,7 @@ export function ScheduleInspector({
/>
</label>
<button
className="inspector-secondary-button"
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-[#bcd2e9] bg-[#f7fbff] text-[11px] text-[#2871bb] [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onPreview}
@@ -128,7 +136,7 @@ export function ScheduleInspector({
5
</button>
{cronResult && (
<ol className="cron-preview-list">
<ol className="m-0 list-decimal rounded-[5px] bg-[#f5f8fb] p-[8px_8px_8px_28px] font-mono text-[9px] leading-[1.7] text-ink-muted">
{cronResult.occurrences.map((item) => (
<li key={item.utc_time}>
{new Date(item.local_time).toLocaleString("zh-CN", {
@@ -142,15 +150,16 @@ export function ScheduleInspector({
)}
</section>
<section>
<h3></h3>
<div className="inspector-grid">
<label>
<span></span>
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<div className="grid grid-cols-2 gap-2">
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<input
type="number"
min="1"
max="64"
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.maxConcurrency}
onChange={(event) => onChange({
...form,
@@ -158,9 +167,10 @@ export function ScheduleInspector({
})}
/>
</label>
<label>
<span></span>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-[#758497]"></span>
<select
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
value={form.failurePolicy}
onChange={(event) => onChange({
...form,
@@ -172,14 +182,14 @@ export function ScheduleInspector({
</select>
</label>
</div>
<div className="schedule-next-run">
<div className="flex items-center justify-between rounded-[5px] bg-[#f5f8fb] p-[8px_9px] text-[10px] text-ink-subtle">
<span></span>
<strong>{formatTime(schedule.next_run_at)}</strong>
<strong className="text-[10px] text-[#425970]">{formatTime(schedule.next_run_at)}</strong>
</div>
</section>
<button
className="inspector-primary-button"
className="m-[14px] flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-brand bg-brand text-[11px] text-white [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onSave}
@@ -23,49 +23,63 @@ export function ScheduleList({
: schedules;
return (
<section className="schedule-panel schedule-list-panel">
<section className="flex min-h-0 flex-col overflow-hidden border-b border-[#e3e9ef]">
<div
className="schedule-panel__title"
className="flex min-h-[48px] items-center justify-between px-3"
onContextMenu={(event) => onContextMenu(event, null)}
>
<div><strong></strong><span>{schedules.length}</span></div>
<div className="flex items-center gap-[7px]">
<strong className="text-[14px] text-[#223247]"></strong>
<span className="inline-grid h-[20px] min-w-[20px] place-items-center rounded-[10px] bg-[#eef3f8] px-[5px] text-[10px] text-[#718195]">
{schedules.length}
</span>
</div>
</div>
<label className="schedule-search">
<label className="mx-[10px] mb-[7px] flex min-h-[35px] items-center gap-[7px] rounded-[6px] border border-[#dce5ed] bg-white px-[9px] text-[#95a3b2] focus-within:border-[#8db9e8] focus-within:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]">
<Icon name="search" size={15} />
<input
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
value={keyword}
placeholder="搜索调度名称"
onChange={(event) => onKeywordChange(event.target.value)}
/>
</label>
<div
className="schedule-list"
className="min-h-0 overflow-auto px-2 pb-[9px]"
onContextMenu={(event) => onContextMenu(event, null)}
>
{loading ? (
<p className="schedule-empty"></p>
<p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
</p>
) : filtered.length ? (
filtered.map((item) => (
<button
className={`schedule-card${
selectedScheduleId === item.schedule_id ? " schedule-card--active" : ""
}`}
className={`relative mb-1.5 flex min-h-[62px] w-full cursor-pointer items-center gap-[9px] rounded-[6px] border border-[#e1e8ef] bg-white px-[10px] py-2 text-left text-[#526276] hover:border-[#b9cfe7] hover:bg-[#f8fbff] data-[state=active]:border-[#89b8ec] data-[state=active]:bg-brand-soft data-[state=active]:text-[#1c64b1] data-[state=active]:shadow-[inset_3px_0_#247fdc]`}
data-state={selectedScheduleId === item.schedule_id ? "active" : "idle"}
key={item.schedule_id}
type="button"
onClick={() => onSelect(item.schedule_id)}
onContextMenu={(event) => onContextMenu(event, item)}
>
<span className={`schedule-status${item.enabled ? " is-enabled" : ""}`} />
<span>
<strong>{item.schedule_name}</strong>
<small>
<span
data-state={item.enabled ? "enabled" : "disabled"}
className="h-2 w-2 flex-none rounded-full bg-[#aeb9c5] shadow-[0_0_0_3px_rgb(142_156_171_/_11%)] data-[state=enabled]:bg-success data-[state=enabled]:shadow-[0_0_0_3px_rgb(24_185_121_/_12%)]"
/>
<span className="flex min-w-0 flex-col gap-1">
<strong className="max-w-[205px] truncate text-[12px] text-[#283b50]">
{item.schedule_name}
</strong>
<small className="text-[10px] text-[#8d9aab]">
{item.node_count} · {item.enabled ? "已启用" : "未启用"}
</small>
</span>
</button>
))
) : (
<p className="schedule-empty"></p>
<p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
</p>
)}
</div>
</section>
+101 -54
View File
@@ -33,7 +33,6 @@ import {
} from "./state/schedulesStore";
import { useSchedulesStore } from "./state/schedulesStore";
import { edgePath, nodeToForm, scheduleToForm } from "./utils";
import "../../styles/schedule.css";
type Notice = {
tone: "success" | "error" | "info";
@@ -78,7 +77,7 @@ function RunNowButton({
const positionDraftCount = useSchedulesStore((s) => s.positionDraftCount);
return (
<button
className="schedule-action--run"
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-emerald-600 bg-emerald-600 px-2 text-[12px] font-semibold text-white enabled:hover:border-emerald-700 enabled:hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
type="button"
disabled={
!schedule
@@ -139,7 +138,7 @@ const ScheduleEdge = memo(function ScheduleEdge({
if (!path) return null;
return (
<g
className={selectedEdgeId === edge.edge_id ? "is-selected" : ""}
data-state={selectedEdgeId === edge.edge_id ? "selected" : "idle"}
onClick={(event) => {
event.stopPropagation();
useSchedulesStore.getState().setSelectedEdgeId(edge.edge_id);
@@ -150,9 +149,16 @@ const ScheduleEdge = memo(function ScheduleEdge({
edge,
})}
>
<path className="schedule-edge-hit" d={path} />
<path
className="schedule-edge-line"
className="schedule-edge-hit fill-none stroke-transparent [stroke-width:14] cursor-pointer [pointer-events:stroke]"
d={path}
/>
<path
className={`schedule-edge-line fill-none ${
selectedEdgeId === edge.edge_id
? "stroke-[#df8142] [stroke-width:3.4]"
: "stroke-[#4e92db] [stroke-width:2.2]"
}`}
d={path}
markerEnd="url(#schedule-arrow)"
/>
@@ -338,23 +344,28 @@ export default function SchedulePage({
);
return (
<section className="schedule-page">
<header className="schedule-toolbar">
<div>
<strong></strong>
<span>
<section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
<header className="flex min-h-[56px] items-center justify-between rounded-[7px] border border-line bg-white px-[14px] shadow-[0_2px_7px_rgb(25_46_73_/_3%)]">
<div className="flex items-baseline gap-[10px]">
<strong className="text-[16px] text-[#1c2d42]"></strong>
<span className="hidden text-[11px] text-[#8b99a9] 2xl:inline">
{schedule
? `版本 ${schedule.workflow_version} · ${schedule.dag_validation.valid ? "DAG 有效" : "待校验"}`
: "创建调度后开始编排"}
<PositionDraftBadge />
</span>
</div>
<div className="schedule-toolbar__actions">
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}>
<Icon name="plus" size={15} />
<div className="flex items-center gap-2">
<button
className="inline-flex h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[12px] font-semibold text-[#526477] enabled:hover:border-[#aac6e5] enabled:hover:bg-[#f6faff] enabled:hover:text-[#126aca] disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
type="button"
onClick={() => useSchedulesStore.getState().openCreateDialog()}
>
<Icon name="plus" size={14} />
<span></span>
</button>
<button
className="schedule-action--primary"
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-brand bg-brand px-2 text-[12px] font-semibold text-white enabled:hover:border-brand-strong enabled:hover:bg-brand-strong disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
type="button"
disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().saveSchedule()}
@@ -362,6 +373,7 @@ export default function SchedulePage({
<Icon name="check" size={15} />
</button>
<button
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[12px] font-semibold text-[#526477] enabled:hover:border-[#aac6e5] enabled:hover:bg-[#f6faff] enabled:hover:text-[#126aca] disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
type="button"
disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().checkDag()}
@@ -370,7 +382,7 @@ export default function SchedulePage({
</button>
<RunNowButton schedule={schedule} busy={busy} />
<button
className="schedule-action--danger"
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-red-200 bg-white px-2 text-[12px] font-semibold text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
type="button"
disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)}
@@ -380,8 +392,8 @@ export default function SchedulePage({
</div>
</header>
<div className="schedule-workbench">
<aside className="schedule-left">
<div className="grid min-h-0 flex-1 gap-[10px] [grid-template-columns:225px_minmax(360px,1fr)_270px] min-[1281px]:[grid-template-columns:250px_minmax(380px,1fr)_292px] 2xl:[grid-template-columns:278px_minmax(420px,1fr)_316px]">
<aside className="grid min-h-0 overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)] [grid-template-rows:minmax(210px,42%)_minmax(260px,58%)]">
<ScheduleList
schedules={filteredSchedules}
selectedScheduleId={schedule?.schedule_id ?? null}
@@ -401,14 +413,21 @@ export default function SchedulePage({
/>
</aside>
<main className="schedule-center">
<main className="flex min-h-0 flex-col overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)]">
<ScheduleCanvasHeader
linkSourceId={linkSourceId}
onCancelLink={() => setLinkSourceId(null)}
/>
<div
className={`schedule-canvas${linkSourceId ? " is-linking" : ""}`}
className="relative min-h-0 flex-1 overflow-auto bg-bg-canvas data-[state=linking]:cursor-crosshair"
data-state={linkSourceId ? "linking" : "idle"}
ref={canvasRef}
style={{
backgroundImage:
"linear-gradient(#e7edf3 1px, transparent 1px), linear-gradient(90deg, #e7edf3 1px, transparent 1px), linear-gradient(#f1f4f7 1px, transparent 1px), linear-gradient(90deg, #f1f4f7 1px, transparent 1px)",
backgroundSize: "80px 80px, 80px 80px, 16px 16px, 16px 16px",
backgroundPosition: "-1px -1px",
}}
onDragOver={(event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
@@ -427,12 +446,12 @@ export default function SchedulePage({
}}
>
<div
className="schedule-canvas__surface"
className="relative"
style={{ width: CANVAS_WIDTH, height: CANVAS_HEIGHT }}
>
{schedule && (
<svg
className="schedule-edges"
className="absolute inset-0 z-[1] h-full w-full overflow-visible"
viewBox={`0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`}
aria-label="调度连线"
>
@@ -461,9 +480,8 @@ export default function SchedulePage({
{schedule?.nodes.map((node) => (
<div
className={`schedule-node${
selectedNodeId === node.node_id ? " is-selected" : ""
}`}
className={`schedule-node absolute z-[2] flex cursor-grab touch-none select-none flex-col rounded-[8px] border border-[#91b7de] bg-white p-[11px_12px_9px] text-[#34516f] shadow-[0_7px_18px_rgb(36_75_117_/_11%)] active:cursor-grabbing data-[state=selected]:border-brand data-[state=selected]:shadow-[0_0_0_3px_rgb(25_120_212_/_11%),0_8px_22px_rgb(36_75_117_/_13%)]`}
data-state={selectedNodeId === node.node_id ? "selected" : "idle"}
key={node.node_id}
style={{
left: node.position_x,
@@ -481,7 +499,7 @@ export default function SchedulePage({
})}
>
<button
className="schedule-node__port schedule-node__port--in"
className="absolute left-[-8px] top-[calc(50%-7px)] z-[4] h-[14px] w-[14px] cursor-crosshair rounded-full border-2 border-[#4e92db] bg-white p-0 hover:border-[#176ec5] hover:bg-[#4e92db] hover:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)]"
type="button"
aria-label={`连接到${node.node_name}`}
title={linkSourceId ? "点击完成连线" : "输入端口"}
@@ -491,15 +509,24 @@ export default function SchedulePage({
void useSchedulesStore.getState().connectTo(node.node_id);
}}
/>
<div className="schedule-node__heading">
<span className={`schedule-node__type schedule-node__type--${node.version.script_type}`}>
<div className="flex items-center gap-[7px]">
<span
className={`grid h-[25px] w-[25px] flex-none place-items-center rounded-[5px] ${
node.version.script_type === "notebook"
? "bg-[#fff0ef] text-[#da5c52]"
: "bg-[#eaf3ff] text-[#316fbf]"
}`}
>
<Icon
name={node.version.script_type === "notebook" ? "notebook" : "python"}
size={14}
/>
</span>
<strong>{node.node_name}</strong>
<strong className="min-w-0 flex-1 truncate text-[12px] text-[#243b53]">
{node.node_name}
</strong>
<button
className="grid h-[23px] w-[23px] cursor-pointer place-items-center rounded-[4px] border-0 bg-transparent p-0 text-[#8b99a7] hover:bg-[#edf5fd] hover:text-brand"
type="button"
aria-label="删除节点"
onPointerDown={(event) => event.stopPropagation()}
@@ -512,15 +539,18 @@ export default function SchedulePage({
<Icon name="settings" size={14} />
</button>
</div>
<p>{node.version.script_name}</p>
<footer>
<span>{node.node_key}</span>
<b>{node.version.version_label}</b>
<p className="mt-2 mb-1.5 truncate text-[10px] text-[#7e8d9d]">
{node.version.script_name}
</p>
<footer className="flex items-center justify-between border-t border-line-soft pt-1.5">
<span className="max-w-[145px] truncate font-mono text-[9px] text-[#8d9baa]">
{node.node_key}
</span>
<b className="text-[10px] text-brand">{node.version.version_label}</b>
</footer>
<button
className={`schedule-node__port schedule-node__port--out${
linkSourceId === node.node_id ? " is-active" : ""
}`}
className="absolute right-[-8px] top-[calc(50%-7px)] z-[4] h-[14px] w-[14px] cursor-crosshair rounded-full border-2 border-[#4e92db] bg-white p-0 hover:border-[#176ec5] hover:bg-[#4e92db] hover:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)] data-[state=active]:border-[#176ec5] data-[state=active]:bg-[#4e92db] data-[state=active]:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)]"
data-state={linkSourceId === node.node_id ? "active" : "idle"}
type="button"
aria-label={`${node.node_name}开始连线`}
title="输出端口"
@@ -540,39 +570,50 @@ export default function SchedulePage({
))}
{!schedule ? (
<div className="schedule-canvas-empty">
<div className="sticky left-1/2 top-[46%] z-0 mx-auto mt-[190px] flex w-[350px] flex-col items-center text-center text-[#9aa8b6] [&>svg]:mb-3 [&>svg]:text-[#8cb4de]">
<Icon name="schedule" size={34} />
<strong></strong>
<p>"新建"</p>
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}>
<strong className="text-[14px] text-[#567089]"></strong>
<p className="my-[7px_13px] text-[11px]">"新建"</p>
<button
className="inline-flex min-h-[32px] cursor-pointer items-center gap-1 rounded-[6px] border border-[#2580da] bg-[#2580da] px-3 text-[11px] text-white"
type="button"
onClick={() => useSchedulesStore.getState().openCreateDialog()}
>
<Icon name="plus" size={15} />
</button>
</div>
) : schedule.nodes.length === 0 ? (
<div className="schedule-canvas-empty">
<div className="sticky left-1/2 top-[46%] z-0 mx-auto mt-[190px] flex w-[350px] flex-col items-center text-center text-[#9aa8b6] [&>svg]:mb-3 [&>svg]:text-[#8cb4de]">
<Icon name="release" size={34} />
<strong></strong>
<p></p>
<strong className="text-[14px] text-[#567089]"></strong>
<p className="my-[7px_13px] text-[11px]"></p>
</div>
) : null}
</div>
</div>
<footer className="schedule-canvas-status">
<span className={schedule?.dag_validation.valid ? "is-valid" : ""}>
<footer className="flex min-h-[35px] items-center gap-[14px] border-t border-[#e4eaf0] bg-white px-3 text-[10px] text-[#8b99a9]">
<span
data-state={schedule?.dag_validation.valid ? "valid" : "invalid"}
className="text-[#b17a48] data-[state=valid]:text-[#16875b]"
>
{schedule?.dag_validation.valid ? "✓ DAG 有效" : "○ DAG 待校验"}
</span>
<span>{schedule?.nodes.length ?? 0} </span>
<span>{schedule?.edges.length ?? 0} 线</span>
{selectedEdge && (
<button type="button" onClick={() => useSchedulesStore.getState().removeEdge(selectedEdge)}>
<button
className="ml-auto inline-flex min-h-[24px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[10px] font-semibold text-[#bd504b] hover:border-[#aac6e5] hover:bg-[#f6faff] hover:text-[#126aca]"
type="button"
onClick={() => useSchedulesStore.getState().removeEdge(selectedEdge)}
>
线
</button>
)}
</footer>
</main>
<aside className="schedule-right">
<div className="schedule-inspector-scroll">
<aside className="grid min-h-0 overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)] [grid-template-rows:repeat(2,minmax(0,1fr))]">
<div className="min-h-0 overflow-auto">
{selectedNode ? (
<NodeInspector
node={selectedNode}
@@ -606,14 +647,14 @@ export default function SchedulePage({
{contextMenu && (
<div
className="schedule-context-menu"
className="fixed z-[60] min-w-[168px] rounded-[8px] border border-[#d9e3ec] bg-white p-[5px] shadow-[0_14px_36px_rgb(14_35_57_/_22%)]"
role="menu"
style={{ left: contextMenu.x, top: contextMenu.y }}
onPointerDown={(event) => event.stopPropagation()}
>
{contextMenu.kind === "schedule-list" && (
<button
className="schedule-context-menu__action"
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().openCreateDialog()}
@@ -625,7 +666,7 @@ export default function SchedulePage({
{contextMenu.kind === "schedule" && (
<>
<button
className="schedule-context-menu__action"
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().openCreateDialog()}
@@ -634,7 +675,7 @@ export default function SchedulePage({
</button>
<button
className="schedule-context-menu__action"
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)}
@@ -643,6 +684,7 @@ export default function SchedulePage({
</button>
<button
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)}
@@ -655,6 +697,7 @@ export default function SchedulePage({
{contextMenu.kind === "artifact" && null}
{contextMenu.kind === "node" && (
<button
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)}
@@ -665,6 +708,7 @@ export default function SchedulePage({
)}
{contextMenu.kind === "edge" && (
<button
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
type="button"
role="menuitem"
onClick={() => useSchedulesStore.getState().removeEdge(contextMenu.edge)}
@@ -690,7 +734,7 @@ export default function SchedulePage({
<h2 id="create-schedule-title"></h2>
</div>
<button
className="icon-button"
className="grid h-[34px] w-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]"
type="button"
aria-label="关闭"
disabled={Boolean(busy)}
@@ -737,8 +781,11 @@ export default function SchedulePage({
)}
{busy && (
<div className="schedule-busy" aria-live="polite">
<span />
<div
className="absolute bottom-[22px] right-[22px] z-[20] flex min-h-[38px] items-center gap-2 rounded-[7px] border border-[#c9dcee] bg-white px-[13px] text-[11px] text-[#4e657d] shadow-[0_8px_25px_rgb(28_56_86_/_15%)]"
aria-live="polite"
>
<span className="h-[15px] w-[15px] rounded-full border-2 border-[#b9d4ef] border-t-brand" />
{busy === "run-now" ? "正在创建运行…" : "正在同步调度配置…"}
</div>
)}
File diff suppressed because it is too large Load Diff