Implement automatic task management with CRUD operations and UI integration

- Added `automatic_tasks.go` and `automatic_tasks_test.go` for backend logic and testing of automatic tasks.
- Created `automatic_tasks_test.go` to validate task claiming and deletion behavior.
- Developed `AutomaticTasksPage.tsx` for frontend management of automatic tasks, including task creation, editing, and execution.
- Integrated device and eSIM profile selection for task configuration.
- Implemented automatic task scheduling and retry logic in the backend.
This commit is contained in:
MengMengCode
2026-08-10 22:15:54 +08:00
parent 70191ce1e3
commit f4aaa3cfdd
66 changed files with 15338 additions and 462 deletions
+57 -74
View File
@@ -1,6 +1,6 @@
import { AddRegular, DeleteRegular, DesktopRegular, EditRegular, GlobeRegular } from "@fluentui/react-icons";
import { DeleteRegular, DesktopRegular, EditRegular, GlobeRegular } from "@fluentui/react-icons";
import type { UpstreamProxy } from "../../types";
import { Button, EmptyState, ErrorState, ListSkeleton, Tag } from "../ui";
import { Button, Tag } from "../ui";
import type { LoadError, UpstreamRow } from "./shared";
import { useI18n } from "../../lib/i18n";
@@ -9,90 +9,73 @@ export interface UpstreamSectionProps {
loading: boolean;
error: LoadError | null;
onRetry: () => void;
onNew: () => void;
onEdit: (proxy: UpstreamProxy) => void;
onDelete: (proxy: UpstreamProxy) => void;
onOpenBindings: (proxy: UpstreamProxy) => void;
}
function UpstreamRowCard({
row,
onEdit,
onDelete,
onOpenBindings,
}: {
row: UpstreamRow;
onEdit: (proxy: UpstreamProxy) => void;
onDelete: (proxy: UpstreamProxy) => void;
onOpenBindings: (proxy: UpstreamProxy) => void;
}) {
export function UpstreamSection({ rows, loading, error, onRetry, onEdit, onDelete, onOpenBindings }: UpstreamSectionProps) {
const { t } = useI18n();
return (
<div className="ui-panel-muted flex flex-col gap-3 p-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex min-w-0 items-center gap-3">
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${row.enabled ? "bg-green-500" : "bg-gray-300"}`} />
<div className="min-w-0">
<div className="truncate font-bold text-gray-900 dark:text-white">{row.name || row.id}</div>
<div className="mt-0.5 truncate text-xs text-gray-500">
SOCKS5 · <span className="font-mono">{row.addr}</span>
{row.username ? <span> · {t("鉴权")}: {row.username}</span> : null}
</div>
</div>
</div>
<div className="flex shrink-0 flex-wrap items-center gap-2">
<Tag type={row.enabled ? "success" : "info"}>{row.enabled ? t("已启用") : t("已禁用")}</Tag>
<div className="inline-flex items-center gap-1 rounded border border-indigo-200/60 bg-indigo-50 px-2 py-0.5 text-[11px] font-medium text-indigo-600 dark:border-indigo-800/40 dark:bg-indigo-900/20 dark:text-indigo-400">
<DesktopRegular className="text-[14px]" />
<span>{row.bindingCount} {t("台设备")}</span>
</div>
<div className="mx-0.5 hidden h-3.5 w-px bg-gray-200 dark:bg-gray-700 sm:block" />
<Button size="small" icon={<DesktopRegular />} onClick={() => onOpenBindings(row)}>
<span className="hidden sm:inline">{t("设备绑定")}</span>
</Button>
<Button size="small" icon={<EditRegular />} onClick={() => onEdit(row)} />
<Button size="small" variant="danger" icon={<DeleteRegular />} onClick={() => onDelete(row)} />
</div>
</div>
);
}
export function UpstreamSection({ rows, loading, error, onRetry, onNew, onEdit, onDelete, onOpenBindings }: UpstreamSectionProps) {
const { t } = useI18n();
return (
<div>
<div className="ui-card overflow-hidden">
{error ? (
<ErrorState className="mb-6" title={t("加载上游代理失败")} message={error.message} statusCode={error.status} retryText={t("重试")} onRetry={onRetry} />
) : null}
<div className="ui-card p-6">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-[#0ea5e9] to-[#0284c7] text-white shadow-lg shadow-indigo-500/25">
<GlobeRegular className="text-[20px]" />
</div>
<div>
<div className="text-lg font-bold text-gray-900 dark:text-white">{t("VoWiFi 上游代理")}</div>
<div className="text-xs text-gray-500">{t("将设备的 VoWiFi 建链、IMS 和短信通信通过支持 UDP Associate 的 SOCKS5 代理传输。")}</div>
</div>
</div>
<Button variant="primary" className="!border-0" icon={<AddRegular />} onClick={onNew}>
{t("新增代理")}
</Button>
<div className="flex items-center justify-between gap-3 border-b border-red-200 bg-red-50 p-3 text-sm text-red-600 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-300">
<span className="min-w-0 truncate">
{t("加载上游代理失败")}{error.message}
{error.status ? `${error.status}` : ""}
</span>
<button type="button" className="shrink-0 font-medium underline underline-offset-2" onClick={onRetry}>
{t("重试")}
</button>
</div>
{loading && rows.length === 0 ? (
<ListSkeleton rows={2} />
) : rows.length === 0 ? (
<EmptyState
title={t("暂无上游代理")}
subtitle={t("点击“新增代理”创建 SOCKS5 上游代理,然后将需要使用它的设备直接绑定;未绑定设备默认直连。")}
/>
) : (
<div className="space-y-3">
) : null}
<div className="overflow-x-auto">
<table className="w-full min-w-[900px] text-left text-sm">
<thead className="border-b border-gray-100 bg-gray-50/70 text-xs uppercase tracking-wide text-gray-500 dark:border-white/10 dark:bg-white/[0.025]">
<tr>
<th className="px-4 py-3">{t("名称")}</th>
<th className="px-4 py-3">{t("协议")}</th>
<th className="px-4 py-3">{t("地址")}</th>
<th className="px-4 py-3">{t("鉴权")}</th>
<th className="px-4 py-3">{t("状态")}</th>
<th className="px-4 py-3">{t("设备绑定")}</th>
<th className="px-4 py-3 text-right">{t("操作")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-white/10">
{rows.map((row) => (
<UpstreamRowCard key={row.id} row={row} onEdit={onEdit} onDelete={onDelete} onOpenBindings={onOpenBindings} />
<tr key={row.id} className="hover:bg-sky-50/40 dark:hover:bg-sky-500/[0.04]">
<td className="px-4 py-3 font-semibold">{row.name || row.id}</td>
<td className="px-4 py-3"><Tag type="primary">SOCKS5</Tag></td>
<td className="px-4 py-3 font-mono text-xs">{row.addr}</td>
<td className="px-4 py-3">{row.username || t("无")}</td>
<td className="px-4 py-3"><Tag type={row.enabled ? "success" : "info"}>{row.enabled ? t("已启用") : t("已禁用")}</Tag></td>
<td className="px-4 py-3">
<div className="inline-flex items-center gap-1 rounded border border-indigo-200/60 bg-indigo-50 px-2 py-0.5 text-[11px] font-medium text-indigo-600 dark:border-indigo-800/40 dark:bg-indigo-900/20 dark:text-indigo-400">
<DesktopRegular className="text-[14px]" />
<span>{row.bindingCount} {t("台设备")}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex justify-end gap-2">
<Button size="small" icon={<DesktopRegular />} onClick={() => onOpenBindings(row)}>{t("设备绑定")}</Button>
<Button size="small" icon={<EditRegular />} onClick={() => onEdit(row)}>{t("编辑")}</Button>
<Button size="small" variant="danger" plain icon={<DeleteRegular />} onClick={() => onDelete(row)}>{t("删除")}</Button>
</div>
</td>
</tr>
))}
</div>
)}
</tbody>
</table>
</div>
{!loading && !rows.length ? (
<div className="flex flex-col items-center justify-center px-6 py-16 text-center text-gray-400">
<GlobeRegular className="mb-3 text-4xl" />
<div className="text-sm">{t("暂无上游代理")}</div>
<div className="mt-1 text-xs">{t("点击“新增代理”创建 SOCKS5 上游代理,然后将需要使用它的设备直接绑定;未绑定设备默认直连。")}</div>
</div>
) : null}
{loading ? <div className="px-6 py-16 text-center text-sm text-gray-400">{t("加载中...")}</div> : null}
</div>
);
}