代码提交

This commit is contained in:
fengxiangdebaozi committed 2026-06-18 21:09:37 +08:00
1 parent 179a3f2b2f
commit ce78cbf936
4 files changed
+188 -26

No files matched your search

@@ -9,10 +9,19 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { ArrowRight, ExternalLink, Maximize2 } from "lucide-react";
import {
ArrowRight,
ExternalLink,
FileSearch,
GitBranch,
Maximize2,
Network,
Workflow,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { readTaskContext } from "@/core/auth/task-purpose";
import {
DEFAULT_TASK_DEEPLINK_CONFIG,
@@ -23,6 +32,7 @@ import {
buttonsForBusiness,
resolveTaskButtons,
runTaskButton,
type TaskButton,
type TaskBusiness,
} from "@/core/auth/task-buttons";
import { useTaskButtons } from "@/strategy-components/api/task-buttons";
@@ -81,31 +91,171 @@ export function TaskJumpBar({
});
};
const nextButtons = jumpButtons.filter(isNextTaskButton);
const menuButtons = jumpButtons.filter((btn) => !isNextTaskButton(btn));
return (
<div className="border-border/70 bg-background/90 flex shrink-0 flex-wrap items-center justify-end gap-3 border-t px-5 py-3 backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/85">
{jumpButtons.map((btn) => (
<Button
key={btn.id}
className="h-11 gap-2 border-transparent bg-[var(--color-main)] px-6 text-base text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={() => handleRun(btn)}
title={desensitize(btn.label)}
>
<span className="truncate">{desensitize(btn.label)}</span>
{btn.linkType === "url" ? (
<ExternalLink className="h-5 w-5 shrink-0" />
) : btn.linkType === "purpose" ? (
<Maximize2 className="h-5 w-5 shrink-0" />
) : (
<ArrowRight className="h-5 w-5 shrink-0" />
)}
</Button>
))}
<>
{menuButtons.length > 0 && <TaskSideJumpMenu buttons={menuButtons} onRun={handleRun} />}
{nextButtons.length > 0 && (
<div className="border-border/70 bg-background/90 flex shrink-0 flex-wrap items-center justify-end gap-3 border-t px-5 py-3 backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/85">
{nextButtons.map((btn) => (
<TaskJumpButton key={btn.id} button={btn} onRun={handleRun} />
))}
</div>
)}
<TaskPurposeDialog
taskId={taskId}
open={purposeOpen}
onOpenChange={setPurposeOpen}
resolveFromActionId={purposeActionId}
/>
</>
);
}
function isNextTaskButton(btn: TaskButton) {
return btn.id.toLowerCase().includes("next") || btn.label.trim() === "下一步";
}
function TaskJumpButton({
button,
onRun,
}: {
button: TaskButton;
onRun: (button: TaskButton) => void;
}) {
return (
<Button
className="h-11 gap-2 border-transparent bg-[var(--color-main)] px-6 text-base text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={() => onRun(button)}
title={desensitize(button.label)}
>
<span className="truncate">{desensitize(button.label)}</span>
{renderTaskJumpIcon(button, "h-5 w-5 shrink-0")}
</Button>
);
}
function TaskSideJumpMenu({
buttons,
onRun,
}: {
buttons: TaskButton[];
onRun: (button: TaskButton) => void;
}) {
const [open, setOpen] = useState(false);
const menuWidth = 168;
const collapsedWidth = 64;
return (
<div
className="fixed right-0 top-1/2 z-50 -translate-y-1/2 overflow-visible"
style={{ width: collapsedWidth }}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
}}
>
<div
className="absolute right-0 top-1/2 overflow-hidden rounded-l-2xl rounded-r-none bg-white/95 py-1.5 shadow-[0_18px_45px_rgba(15,23,42,0.16)] backdrop-blur-2xl dark:bg-[#061b35]/90 dark:shadow-[0_18px_45px_rgba(0,0,0,0.35)]"
style={{
width: menuWidth,
transform: `translateY(-50%) translateX(${open ? 0 : menuWidth - collapsedWidth}px)`,
transition:
"transform 950ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 950ms cubic-bezier(0.22, 1, 0.36, 1), background-color 950ms ease",
}}
>
<div className="flex flex-col gap-1 px-2">
{buttons.map((btn) => {
const tone = getRoundtableMenuTone(btn);
return (
<button
key={btn.id}
type="button"
className={cn(
"grid h-11 w-full grid-cols-[2rem_1fr] items-center gap-2.5 overflow-hidden rounded-l-xl rounded-r-none pl-2 pr-3 text-left text-sm font-semibold transition-[background-color,color,transform] duration-300 ease-out",
"text-slate-700 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300/70 dark:text-slate-100 dark:hover:bg-white/10 dark:focus-visible:ring-sky-300/60",
tone.item,
)}
onClick={() => onRun(btn)}
title={desensitize(btn.label)}
aria-label={desensitize(btn.label)}
>
<span
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-lg transition-transform duration-300",
tone.icon,
)}
style={{ transform: open ? "scale(1.05)" : "scale(1)" }}
>
{renderRoundtableMenuIcon(btn)}
</span>
<span
className="block min-w-0 truncate tracking-normal"
style={{
opacity: open ? 1 : 0,
transform: `translateX(${open ? 0 : 14}px)`,
transition:
"opacity 650ms ease 140ms, transform 650ms cubic-bezier(0.22, 1, 0.36, 1) 140ms",
}}
>
{desensitize(btn.label)}
</span>
</button>
);
})}
</div>
</div>
</div>
);
}
function renderTaskJumpIcon(btn: TaskButton, className: string) {
if (btn.linkType === "url") return <ExternalLink className={className} />;
if (btn.linkType === "purpose") return <Maximize2 className={className} />;
return <ArrowRight className={className} />;
}
function renderRoundtableMenuIcon(btn: TaskButton) {
const className = "h-4 w-4";
if (btn.linkType === "purpose") return <Maximize2 className={className} />;
if (btn.linkType === "url") return <FileSearch className={className} />;
if (btn.target === "3qfx") return <Network className={className} />;
if (btn.target === "xdfx") return <Workflow className={className} />;
if (btn.target === "rwfx") return <GitBranch className={className} />;
return <ArrowRight className={className} />;
}
function getRoundtableMenuTone(btn: TaskButton) {
if (btn.linkType === "url") {
return {
item: "hover:text-rose-700 dark:hover:text-rose-100",
icon: "bg-rose-50 text-rose-600 dark:bg-rose-400/10 dark:text-rose-200",
};
}
if (btn.target === "3qfx") {
return {
item: "hover:text-sky-700 dark:hover:text-sky-100",
icon: "bg-sky-50 text-sky-600 dark:bg-sky-400/10 dark:text-sky-200",
};
}
if (btn.target === "xdfx") {
return {
item: "hover:text-emerald-700 dark:hover:text-emerald-100",
icon: "bg-emerald-50 text-emerald-600 dark:bg-emerald-400/10 dark:text-emerald-200",
};
}
if (btn.target === "rwfx") {
return {
item: "hover:text-amber-700 dark:hover:text-amber-100",
icon: "bg-amber-50 text-amber-600 dark:bg-amber-400/10 dark:text-amber-200",
};
}
return {
item: "hover:text-slate-900 dark:hover:text-white",
icon: "bg-slate-50 text-slate-600 dark:bg-white/10 dark:text-slate-100",
};
}
@@ -19,6 +19,8 @@ import {
purposeTreeToFlowGraph,
type TaskPurposeResult,
} from "@/core/auth/task-purpose";
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
import { desensitize } from "@/lib/desensitize";
import { FlowGraphPanel } from "@/roundtable-planning/components/FlowGraphPanel";
export function TaskPurposeDialog({
@@ -52,10 +54,11 @@ export function TaskPurposeDialog({
};
}, [open, taskId]);
const flowData = useMemo(
() => purposeTreeToFlowGraph(result?.tree ?? [], taskId),
[result, taskId],
);
// 任务为流程图的单一根节点:根节点文案用登录深链暂存的任务名称(已脱敏),取不到回退「任务」。
const flowData = useMemo(() => {
const taskName = desensitize((readCopTaskDetail()?.taskName ?? "").trim()) || "任务";
return purposeTreeToFlowGraph(result?.tree ?? [], taskId, taskName);
}, [result, taskId]);
const hasNodes = flowData.nodes.length > 0;