定时任务订阅模式
This commit is contained in:
1 parent
839386f0b4
commit
aa034f8669
14 files changed
+1143
-98
No files matched your search
@@ -2,10 +2,12 @@ import { apiFetch } from "@/core/api";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
import type {
|
||||
PublishedScheduledTask,
|
||||
ScheduledTask,
|
||||
ScheduledTaskDeliveryProfile,
|
||||
ScheduledTaskResultFileContent,
|
||||
ScheduledTaskRun,
|
||||
ScheduledTaskSubscription,
|
||||
SchedulerThread,
|
||||
} from "./types";
|
||||
|
||||
@@ -214,3 +216,57 @@ export async function triggerScheduledTask(taskId: string) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function listPublishedScheduledTasks(params: { limit?: number; offset?: number } = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.limit != null) query.set("limit", String(params.limit));
|
||||
if (params.offset != null) query.set("offset", String(params.offset));
|
||||
const suffix = query.toString();
|
||||
return parseResponse<PublishedScheduledTask[]>(
|
||||
await apiFetch(`${baseUrl()}/published${suffix ? `?${suffix}` : ""}`),
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishScheduledTask(taskId: string) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/publish`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function unpublishScheduledTask(taskId: string) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/unpublish`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function subscribeScheduledTask(taskId: string, notifyElement: boolean) {
|
||||
return parseResponse<ScheduledTaskSubscription>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ notify_element: notifyElement }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateScheduledTaskSubscription(taskId: string, notifyElement: boolean) {
|
||||
return parseResponse<ScheduledTaskSubscription>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ notify_element: notifyElement }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function unsubscribeScheduledTask(taskId: string) {
|
||||
return parseResponse<{ success: boolean }>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -7,13 +7,19 @@ import {
|
||||
getSchedulerThread,
|
||||
getScheduledTaskDeliveryProfile,
|
||||
getScheduledTaskRun,
|
||||
listPublishedScheduledTasks,
|
||||
listScheduledTaskRuns,
|
||||
listScheduledTasks,
|
||||
pauseScheduledTask,
|
||||
publishScheduledTask,
|
||||
resumeScheduledTask,
|
||||
subscribeScheduledTask,
|
||||
triggerScheduledTask,
|
||||
unpublishScheduledTask,
|
||||
unsubscribeScheduledTask,
|
||||
updateScheduledTaskDeliveryProfile,
|
||||
updateScheduledTask,
|
||||
updateScheduledTaskSubscription,
|
||||
} from "./api";
|
||||
|
||||
export function useSchedulerThread() {
|
||||
@@ -58,6 +64,15 @@ export function useScheduledTaskDeliveryProfile() {
|
||||
});
|
||||
}
|
||||
|
||||
export function usePublishedScheduledTasks(enabled: boolean = true) {
|
||||
return useQuery({
|
||||
queryKey: ["scheduled-tasks", "published"],
|
||||
queryFn: () => listPublishedScheduledTasks({ limit: 100 }),
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useScheduledTaskActions() {
|
||||
const queryClient = useQueryClient();
|
||||
const invalidate = () =>
|
||||
@@ -107,5 +122,27 @@ export function useScheduledTaskActions() {
|
||||
queryKey: ["scheduled-tasks", "delivery-profile"],
|
||||
}),
|
||||
}),
|
||||
publish: useMutation({
|
||||
mutationFn: publishScheduledTask,
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
unpublish: useMutation({
|
||||
mutationFn: unpublishScheduledTask,
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
subscribe: useMutation({
|
||||
mutationFn: ({ taskId, notifyElement }: { taskId: string; notifyElement: boolean }) =>
|
||||
subscribeScheduledTask(taskId, notifyElement),
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
updateSubscription: useMutation({
|
||||
mutationFn: ({ taskId, notifyElement }: { taskId: string; notifyElement: boolean }) =>
|
||||
updateScheduledTaskSubscription(taskId, notifyElement),
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
unsubscribe: useMutation({
|
||||
mutationFn: unsubscribeScheduledTask,
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
export type ScheduledTaskRole = "owner" | "subscriber";
|
||||
|
||||
export type ScheduledTaskSubscription = {
|
||||
id: string;
|
||||
task_id: string;
|
||||
user_id: string;
|
||||
notify_element: boolean;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
export type ScheduledTask = {
|
||||
task_id: string;
|
||||
name: string;
|
||||
@@ -12,6 +23,20 @@ export type ScheduledTask = {
|
||||
source_agent_name?: string | null;
|
||||
execution_agent_name?: string | null;
|
||||
scheduler_thread_id: string;
|
||||
published?: boolean;
|
||||
published_at?: string | null;
|
||||
role?: ScheduledTaskRole;
|
||||
_subscription?: ScheduledTaskSubscription | null;
|
||||
};
|
||||
|
||||
export type PublishedScheduledTask = {
|
||||
task_id: string;
|
||||
name: string;
|
||||
prompt_summary: string;
|
||||
schedule_text: string;
|
||||
timezone: string;
|
||||
published_at?: string | null;
|
||||
owner_user_id: string;
|
||||
};
|
||||
|
||||
export type SchedulerThread = {
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
BellOff,
|
||||
BellRing,
|
||||
Download,
|
||||
Edit3,
|
||||
Eye,
|
||||
Globe,
|
||||
Heart,
|
||||
HeartOff,
|
||||
Lock,
|
||||
Pause,
|
||||
Play,
|
||||
Plus,
|
||||
@@ -21,6 +27,8 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
WorkspaceBody,
|
||||
@@ -30,11 +38,13 @@ import {
|
||||
import { useAgents } from "@/core/agents";
|
||||
import {
|
||||
downloadScheduledTaskRun,
|
||||
type PublishedScheduledTask,
|
||||
type ScheduledTask,
|
||||
useScheduledTaskActions,
|
||||
useScheduledTaskDeliveryProfile,
|
||||
useScheduledTaskRuns,
|
||||
useScheduledTasks,
|
||||
usePublishedScheduledTasks,
|
||||
} from "@/core/scheduled-tasks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -217,6 +227,9 @@ export default function ScheduledTasksPage() {
|
||||
const { data: deliveryProfile } = useScheduledTaskDeliveryProfile();
|
||||
const { agents } = useAgents();
|
||||
const actions = useScheduledTaskActions();
|
||||
const [activeTab, setActiveTab] = useState<"mine" | "plaza">("mine");
|
||||
const { data: published = [], isLoading: publishedLoading } =
|
||||
usePublishedScheduledTasks(activeTab === "plaza");
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
const [elementUserId, setElementUserId] = useState("");
|
||||
const [editing, setEditing] = useState<ScheduledTask | null>(null);
|
||||
@@ -224,6 +237,8 @@ export default function ScheduledTasksPage() {
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [scheduleConfig, setScheduleConfig] = useState<ScheduleConfig>(DEFAULT_SCHEDULE_CONFIG);
|
||||
const [runsTimeFilter, setRunsTimeFilter] = useState<"all" | "today" | "7d" | "30d">("all");
|
||||
const [subscribeTarget, setSubscribeTarget] = useState<PublishedScheduledTask | null>(null);
|
||||
const [subscribeNotify, setSubscribeNotify] = useState(false);
|
||||
|
||||
const sortedTasks = useMemo(
|
||||
() =>
|
||||
@@ -377,95 +392,232 @@ export default function ScheduledTasksPage() {
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as typeof activeTab)}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="mine" className="flex-1">我的任务</TabsTrigger>
|
||||
<TabsTrigger value="plaza" className="flex-1">广场</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 flex-1 p-0">
|
||||
<ScrollArea className="size-full">
|
||||
<div className="flex flex-col gap-3 p-4 pt-0">
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground text-sm">正在加载任务...</div>
|
||||
)}
|
||||
{!isLoading && sortedTasks.length === 0 && (
|
||||
<div className="text-muted-foreground rounded-md border p-4 text-sm">
|
||||
还没有定时任务。点击新建创建第一个任务。
|
||||
</div>
|
||||
)}
|
||||
{sortedTasks.map((task) => (
|
||||
<button
|
||||
key={task.task_id}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground rounded-md border p-4 text-left transition-colors hover:bg-accent/60",
|
||||
selectedTaskId === task.task_id && "border-primary bg-accent",
|
||||
)}
|
||||
onClick={() => setSelectedTaskId(task.task_id)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{task.name}</div>
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">
|
||||
{task.schedule_text} · {task.timezone}
|
||||
{activeTab === "mine" ? (
|
||||
<div className="flex flex-col gap-3 p-4 pt-0">
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground text-sm">正在加载任务...</div>
|
||||
)}
|
||||
{!isLoading && sortedTasks.length === 0 && (
|
||||
<div className="text-muted-foreground rounded-md border p-4 text-sm">
|
||||
还没有定时任务。点击新建创建第一个任务,或去“广场”收藏他人发布的任务。
|
||||
</div>
|
||||
)}
|
||||
{sortedTasks.map((task) => {
|
||||
const isOwner = task.role !== "subscriber";
|
||||
const isSubscriber = task.role === "subscriber";
|
||||
const notifyOn = !!task._subscription?.notify_element;
|
||||
return (
|
||||
<button
|
||||
key={task.task_id}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground rounded-md border p-4 text-left transition-colors hover:bg-accent/60",
|
||||
selectedTaskId === task.task_id && "border-primary bg-accent",
|
||||
)}
|
||||
onClick={() => setSelectedTaskId(task.task_id)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="truncate font-medium">{task.name}</div>
|
||||
{isSubscriber && (
|
||||
<Badge variant="outline" className="shrink-0 text-xs">
|
||||
<Heart className="size-3" />收藏
|
||||
</Badge>
|
||||
)}
|
||||
{isOwner && task.published && (
|
||||
<Badge variant="outline" className="shrink-0 text-xs">
|
||||
<Globe className="size-3" />已发布
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">
|
||||
{task.schedule_text} · {task.timezone}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={task.enabled ? "default" : "secondary"}>
|
||||
{task.enabled ? "启用" : "暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-3 grid gap-1 text-xs">
|
||||
<div>下次执行:{formatDate(task.next_run_at)}</div>
|
||||
<div>上次执行:{formatDate(task.last_run_at)}</div>
|
||||
<div>最近状态:{statusLabel(task.last_status)}</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title="编辑"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openEdit(task);
|
||||
}}
|
||||
>
|
||||
<Edit3 className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title={task.enabled ? "暂停" : "恢复"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
task.enabled
|
||||
? actions.pause.mutate(task.task_id)
|
||||
: actions.resume.mutate(task.task_id);
|
||||
}}
|
||||
>
|
||||
{task.enabled ? <Pause className="size-4" /> : <Play className="size-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title="立即执行"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
actions.trigger.mutate(task.task_id);
|
||||
toast.success("已触发执行");
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title={task.published ? "取消发布" : "发布到广场"}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
if (task.published) {
|
||||
await actions.unpublish.mutateAsync(task.task_id);
|
||||
toast.success("已取消发布(已订阅者保留)");
|
||||
} else {
|
||||
await actions.publish.mutateAsync(task.task_id);
|
||||
toast.success("已发布到广场");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{task.published ? <Lock className="size-4" /> : <Globe className="size-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
title="删除"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
actions.remove.mutate(task.task_id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
title={notifyOn ? "关闭通知" : "开启通知"}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await actions.updateSubscription.mutateAsync({
|
||||
taskId: task.task_id,
|
||||
notifyElement: !notifyOn,
|
||||
});
|
||||
toast.success(notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notifyOn ? <BellRing className="size-4" /> : <BellOff className="size-4" />}
|
||||
{notifyOn ? "通知中" : "未通知"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
title="取消收藏"
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await actions.unsubscribe.mutateAsync(task.task_id);
|
||||
toast.success("已取消收藏");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HeartOff className="size-4" />取消收藏
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 p-4 pt-0">
|
||||
{publishedLoading && (
|
||||
<div className="text-muted-foreground text-sm">正在加载广场任务...</div>
|
||||
)}
|
||||
{!publishedLoading && published.length === 0 && (
|
||||
<div className="text-muted-foreground rounded-md border p-4 text-sm">
|
||||
广场上还没有发布的任务。
|
||||
</div>
|
||||
)}
|
||||
{published.map((item) => {
|
||||
const subscribed = tasks.some((t) => t.task_id === item.task_id);
|
||||
return (
|
||||
<div key={item.task_id} className="bg-card rounded-md border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{item.name}</div>
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">
|
||||
{item.schedule_text} · {item.timezone}
|
||||
</div>
|
||||
</div>
|
||||
{subscribed && (
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
<Heart className="size-3" />已收藏
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-3 line-clamp-3 text-xs whitespace-pre-wrap">
|
||||
{item.prompt_summary || "(无摘要)"}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={subscribed ? "outline" : "default"}
|
||||
disabled={subscribed}
|
||||
onClick={() => {
|
||||
setSubscribeTarget(item);
|
||||
setSubscribeNotify(false);
|
||||
}}
|
||||
>
|
||||
<Heart className="size-4" />
|
||||
{subscribed ? "已收藏" : "收藏到我的任务"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={task.enabled ? "default" : "secondary"}>
|
||||
{task.enabled ? "启用" : "暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-3 grid gap-1 text-xs">
|
||||
<div>下次执行:{formatDate(task.next_run_at)}</div>
|
||||
<div>上次执行:{formatDate(task.last_run_at)}</div>
|
||||
<div>最近状态:{statusLabel(task.last_status)}</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title="编辑"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openEdit(task);
|
||||
}}
|
||||
>
|
||||
<Edit3 className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title={task.enabled ? "暂停" : "恢复"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
task.enabled
|
||||
? actions.pause.mutate(task.task_id)
|
||||
: actions.resume.mutate(task.task_id);
|
||||
}}
|
||||
>
|
||||
{task.enabled ? <Pause className="size-4" /> : <Play className="size-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
title="立即执行"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
actions.trigger.mutate(task.task_id);
|
||||
toast.success("已触发执行");
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
title="删除"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
actions.remove.mutate(task.task_id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -566,6 +718,75 @@ export default function ScheduledTasksPage() {
|
||||
</div>
|
||||
</WorkspaceBody>
|
||||
|
||||
<Dialog
|
||||
open={!!subscribeTarget}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setSubscribeTarget(null);
|
||||
setSubscribeNotify(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>收藏到我的任务</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="font-medium">{subscribeTarget?.name}</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
{subscribeTarget?.schedule_text} · {subscribeTarget?.timezone}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground line-clamp-4 text-sm whitespace-pre-wrap">
|
||||
{subscribeTarget?.prompt_summary || "(无摘要)"}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">同步接收 Element 通知</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
开启后,每次执行结果将复制一份发送到你设置的 Element 用户。
|
||||
{!deliveryProfile?.element_user_id && (
|
||||
<span className="text-destructive"> 你尚未配置 Element 用户 ID。</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={subscribeNotify} onCheckedChange={setSubscribeNotify} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSubscribeTarget(null);
|
||||
setSubscribeNotify(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!subscribeTarget) return;
|
||||
try {
|
||||
await actions.subscribe.mutateAsync({
|
||||
taskId: subscribeTarget.task_id,
|
||||
notifyElement: subscribeNotify,
|
||||
});
|
||||
toast.success("已收藏到我的任务");
|
||||
setSubscribeTarget(null);
|
||||
setSubscribeNotify(false);
|
||||
setActiveTab("mine");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "收藏失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认收藏
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={creating || !!editing} onOpenChange={(open) => !open && closeDialog()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
|
||||
Reference in new issue
Block a user