Files
UHD_Media_Tool/apps/web/app/components/schedules/labels.ts
T
rootandClaude Opus 5.5 082c556a67 fix(web): visual QA front-end items U4–U6, U13–U16 and copy list
- U4: form save failures toast the first problem and scroll/focus the first
  invalid field (policy draft, settings cards, resource drawers, subscription
  form) via lib/form-errors.
- U5: FormField keeps 8px between a hint and the next label.
- U6: file manager status bar keeps free space left-aligned, clear of the
  Agent button.
- U13: move/copy dialog stops "target is the source folder / inside the
  source" before requesting a preview (transferTargetProblem + tests).
- U14: account popover uses the solid float material.
- U15/U16: inbox tile hint shortened; TMDB assign dialog uses the shared
  Select and labelled fields (type, TMDB ID, note).
- Copy: "TMDB 已保存" spacing, API Key block no longer repeats the section
  title, whole-day intervals read "每 7 天", "最高级别:提示",
  "延后:已有计划在途", failed records show "未生成目标路径", upstream model
  column "默认模型".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 16:58:38 +08:00

97 lines
3.6 KiB
TypeScript

import type {
AgentCheckTrigger,
ScheduleGroup,
ScheduleRunStatus,
ScheduleSpec,
ScheduleTrigger,
} from "@umt/contracts";
import type { StatusTone } from "~/components/console/status-badge.tsx";
// 定时任务看板的标签映射(只在本页用,不放 lib/labels.ts)
export const GROUP_ORDER: ScheduleGroup[] = ["collect", "download", "organize", "maintain", "agent"];
export const GROUP_LABEL: Record<ScheduleGroup, string> = {
collect: "采集",
download: "下载",
organize: "整理",
maintain: "维护",
agent: "Agent 巡检",
};
export const RUN_STATUS: Record<ScheduleRunStatus, { label: string; tone: StatusTone }> = {
ok: { label: "成功", tone: "success" },
failed: { label: "失败", tone: "destructive" },
skipped: { label: "已跳过", tone: "warning" },
running: { label: "运行中", tone: "neutral" },
};
export const TRIGGER_LABEL: Record<ScheduleTrigger, string> = {
schedule: "定时",
manual: "手动",
skip: "跳过",
};
export const CHECK_TRIGGER_LABEL: Record<AgentCheckTrigger, string> = {
always: "每次运行",
open_decisions: "有待处理时",
stalled_downloads: "有下载无进展时",
subscriptions_active: "有追踪订阅时",
};
export type IntervalUnit = "s" | "m" | "h";
export const UNIT_MS: Record<IntervalUnit, number> = { s: 1000, m: 60_000, h: 3600_000 };
export const UNIT_LABEL: Record<IntervalUnit, string> = { s: "秒", m: "分钟", h: "小时" };
/** 毫秒 → 最大整除单位 */
export function splitInterval(ms: number): { value: number; unit: IntervalUnit } {
if (ms % UNIT_MS.h === 0) return { value: ms / UNIT_MS.h, unit: "h" };
if (ms % UNIT_MS.m === 0) return { value: ms / UNIT_MS.m, unit: "m" };
return { value: Math.max(1, Math.round(ms / 1000)), unit: "s" };
}
/** 调度的简短描述:"每 30 秒" / "cron 0 3 * * *" */
export function describeSchedule(s: ScheduleSpec): string {
if (s.type === "cron") return `cron ${s.cron}`;
if (s.everyMs < 1000) return `每 ${s.everyMs} 毫秒`;
const { value, unit } = splitInterval(s.everyMs);
// 整天的间隔按天说(每 168 小时 → 每 7 天)
if (unit === "h" && value >= 24 && value % 24 === 0) return `每 ${value / 24} 天`;
return `每 ${value} ${UNIT_LABEL[unit]}`;
}
/** 毫秒耗时:"320 毫秒" / "2.4 秒" / "3 分 5 秒" */
export function formatMs(ms: number | null | undefined): string {
if (ms === null || ms === undefined) return "—";
if (ms < 1000) return `${ms} 毫秒`;
if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} 秒`;
const sec = Math.round(ms / 1000);
if (sec < 3600) return `${Math.floor(sec / 60)} 分 ${sec % 60} 秒`;
return `${Math.floor(sec / 3600)} 小时 ${Math.floor((sec % 3600) / 60)} 分`;
}
/** 倒计时:"12 秒后" / "3 分 5 秒后" / "已到期" */
export function countdown(iso: string, now: number): string {
const diff = Math.round((new Date(iso).getTime() - now) / 1000);
if (Number.isNaN(diff)) return "—";
if (diff <= 0) return "即将运行";
if (diff < 60) return `${diff} 秒后`;
if (diff < 3600) return `${Math.floor(diff / 60)} 分 ${diff % 60} 秒后`;
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时 ${Math.floor((diff % 3600) / 60)} 分后`;
return `${Math.floor(diff / 86400)} 天 ${Math.floor((diff % 86400) / 3600)} 小时后`;
}
export function formatClock(iso: string | null | undefined): string {
if (!iso) return "—";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleString("zh-CN", {
hour12: false,
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}