feat(frontend): 时区下拉、表格时区列与刷新反馈
- 编辑器 IANA 时区改为 Select,当前只保留 Asia/Shanghai;编辑旧任务时保留其已有值 - 任务表新增「时区」列,类型标签并入名称列,重新分配列宽 - 手动刷新改为独立状态 + 顶部进度条 + 固定按钮宽度,避免刷新时工具栏左右跳动与旧内容残留的观感
This commit is contained in:
+53
-1
@@ -62,9 +62,44 @@
|
||||
/* ---------- 任务列表 ---------- */
|
||||
|
||||
.task-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 手动刷新时的顶部进度条:不改变表格布局,也避免旧数据看起来像「残影」 */
|
||||
.refresh-bar {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
background-color: var(--su-color-primary-soft);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.refresh-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 40%;
|
||||
background-color: var(--su-color-primary);
|
||||
animation: refresh-slide 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes refresh-slide {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(250%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格嵌在卡片里:去掉自身描边与圆角,避免「卡片套卡片」 */
|
||||
.task-table {
|
||||
border: 0;
|
||||
@@ -99,6 +134,12 @@
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
/* 「刷新」和「同步中...」字数不同,固定宽度避免刷新时工具栏左右跳动 */
|
||||
.refresh-button {
|
||||
min-width: 68px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.task-name-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -106,9 +147,16 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.task-name-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
display: block;
|
||||
max-width: 240px;
|
||||
max-width: 170px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -119,6 +167,10 @@
|
||||
font-family: var(--su-font-mono);
|
||||
}
|
||||
|
||||
.timezone-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -60,13 +60,7 @@ const HTTP_METHOD_OPTIONS = [
|
||||
{ value: 'POST', label: 'POST' },
|
||||
]
|
||||
|
||||
const TIMEZONE_SUGGESTIONS = [
|
||||
'Asia/Shanghai',
|
||||
'Asia/Hong_Kong',
|
||||
'UTC',
|
||||
'Europe/London',
|
||||
'America/New_York',
|
||||
]
|
||||
const TIMEZONE_OPTIONS = ['Asia/Shanghai']
|
||||
|
||||
const CRON_PREVIEW_COUNTS = [3, 5, 8, 10]
|
||||
|
||||
@@ -112,6 +106,11 @@ export function TaskEditorModal({
|
||||
const cronValue = watch('cron')
|
||||
const timezoneValue = watch('timezone')
|
||||
const scriptPreview = watch('script')
|
||||
// 当前只开放 Asia/Shanghai;编辑旧任务时把它已有的时区也保留成一个选项,
|
||||
// 避免下拉显示成空值。
|
||||
const timezoneOptions = Array.from(
|
||||
new Set(timezoneValue ? [...TIMEZONE_OPTIONS, timezoneValue] : TIMEZONE_OPTIONS),
|
||||
).map((zone) => ({ value: zone, label: zone }))
|
||||
const debouncedCron = useDebouncedValue(cronValue, 300)
|
||||
const debouncedTimezone = useDebouncedValue(timezoneValue, 300)
|
||||
const debouncedWorkingDir = useDebouncedValue(
|
||||
@@ -425,23 +424,19 @@ export function TaskEditorModal({
|
||||
</section>
|
||||
|
||||
<div className="form-grid">
|
||||
<FormField
|
||||
label="IANA 时区"
|
||||
htmlFor="task-timezone"
|
||||
error={errors.timezone?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-timezone"
|
||||
{...register('timezone')}
|
||||
list="timezone-options"
|
||||
placeholder="Asia/Shanghai"
|
||||
aria-invalid={Boolean(errors.timezone)}
|
||||
<FormField label="IANA 时区" error={errors.timezone?.message}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="timezone"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="form-select"
|
||||
options={timezoneOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<datalist id="timezone-options">
|
||||
{TIMEZONE_SUGGESTIONS.map((zone) => (
|
||||
<option value={zone} key={zone} />
|
||||
))}
|
||||
</datalist>
|
||||
</FormField>
|
||||
|
||||
<FormField label="任务类型 (Action Type)">
|
||||
|
||||
@@ -68,12 +68,12 @@ const ACTION_TYPE_OPTIONS = [
|
||||
]
|
||||
|
||||
const TABLE_COLUMNS: TableColumn[] = [
|
||||
{ key: 'name', title: '名称 / 任务标识', width: 260 },
|
||||
{ key: 'schedule', title: '调度周期', width: 240 },
|
||||
{ key: 'type', title: '类型', width: 96 },
|
||||
{ key: 'status', title: '状态', width: 120 },
|
||||
{ key: 'timeout', title: '超时配置', width: 100 },
|
||||
{ key: 'actions', title: '操作', align: 'right', width: 300 },
|
||||
{ key: 'name', title: '名称 / 任务标识', width: 250 },
|
||||
{ key: 'schedule', title: '调度周期', width: 210 },
|
||||
{ key: 'timezone', title: '时区', width: 120 },
|
||||
{ key: 'status', title: '状态', width: 110 },
|
||||
{ key: 'timeout', title: '超时', width: 90 },
|
||||
{ key: 'actions', title: '操作', align: 'right', width: 260 },
|
||||
]
|
||||
|
||||
export function TaskDashboard() {
|
||||
@@ -89,6 +89,7 @@ export function TaskDashboard() {
|
||||
const [logTask, setLogTask] = useState<Task | null>(null)
|
||||
const [initialRunId, setInitialRunId] = useState<number | null>(null)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [queryDraft, setQueryDraft] = useState(q)
|
||||
const debouncedQuery = useDebouncedValue(queryDraft, 300)
|
||||
|
||||
@@ -229,9 +230,19 @@ export function TaskDashboard() {
|
||||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
const refreshAll = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['health'] })
|
||||
const refreshAll = async () => {
|
||||
if (refreshing) {
|
||||
return
|
||||
}
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['health'] }),
|
||||
])
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const rows = tasks.map((task) => {
|
||||
@@ -244,9 +255,12 @@ export function TaskDashboard() {
|
||||
id: task.id,
|
||||
name: (
|
||||
<div className="task-name-cell">
|
||||
<div className="task-name-row">
|
||||
<Text className="task-name" strong>
|
||||
{task.name}
|
||||
</Text>
|
||||
<Tag shape="rounded">{actionTypeLabel(task.action.type)}</Tag>
|
||||
</div>
|
||||
<Text className="task-id" size="xs" tone="muted">
|
||||
{task.id}
|
||||
</Text>
|
||||
@@ -255,9 +269,6 @@ export function TaskDashboard() {
|
||||
schedule: (
|
||||
<div className="schedule-cell">
|
||||
<code className="code-chip">{task.cron}</code>
|
||||
<Text size="xs" tone="muted">
|
||||
{task.timezone}
|
||||
</Text>
|
||||
<Text size="xs" tone="muted">
|
||||
{task.enabled
|
||||
? nextRuns.get(task.id)
|
||||
@@ -267,8 +278,10 @@ export function TaskDashboard() {
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
type: (
|
||||
<Tag shape="rounded">{actionTypeLabel(task.action.type)}</Tag>
|
||||
timezone: (
|
||||
<Text className="timezone-cell" size="sm" tone="sub">
|
||||
{task.timezone}
|
||||
</Text>
|
||||
),
|
||||
status: <EnabledBadge enabled={task.enabled} />,
|
||||
timeout: (
|
||||
@@ -365,6 +378,9 @@ export function TaskDashboard() {
|
||||
</section>
|
||||
|
||||
<Card padding="none" className="task-card">
|
||||
{refreshing ? (
|
||||
<div className="refresh-bar" role="progressbar" aria-label="正在刷新" />
|
||||
) : null}
|
||||
<div className="toolbar">
|
||||
<SearchField
|
||||
className="toolbar-search"
|
||||
@@ -403,8 +419,12 @@ export function TaskDashboard() {
|
||||
清除筛选
|
||||
</TextButton>
|
||||
) : null}
|
||||
<TextButton onClick={refreshAll} disabled={tasksQuery.isFetching}>
|
||||
{tasksQuery.isFetching ? '同步中...' : '刷新'}
|
||||
<TextButton
|
||||
className="refresh-button"
|
||||
onClick={() => void refreshAll()}
|
||||
disabled={refreshing}
|
||||
>
|
||||
{refreshing ? '同步中...' : '刷新'}
|
||||
</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user