Merge branch 'dhk-1' into 'master'

AI 写作四阶段内置智能体 Phase 1-4 落地

See merge request knowledge-base-bj/deerflow-server!116
This commit is contained in:
董宏凯 committed 2026-06-12 10:20:27 +00:00
commit c2f6d4fd6f
94 files changed
+6935 -1132

No files matched your search

@@ -0,0 +1,41 @@
/**
* 轻应用 iframe 宿主页 —— 渲染某个已注册 iframe 类型应用。
*
* 路由:/page/strategy/light-app/view/:appId。侧边栏动态注入的 iframe 应用子项指向此页。
* 用 buildLightAppUrl 构造带路由参数(登录态 / 主题)的 src。
*/
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLightApp } from "@/strategy-components/api/light-apps";
import { buildLightAppUrl } from "@/strategy-components/lib/light-app";
export default function LightAppIframeView() {
const { appId } = useParams<{ appId: string }>();
const { theme } = useTheme();
const isDark = theme === "dark";
const { data: app, isLoading, error } = useLightApp(appId);
const src = useMemo(() => (app ? buildLightAppUrl(app) : ""), [app]);
const centerClass = `flex h-full w-full items-center justify-center text-sm ${
isDark ? "text-slate-400" : "text-slate-500"
}`;
if (isLoading) {
return <div className={centerClass}>加载中…</div>;
}
if (error || !app) {
return <div className={centerClass}>应用不存在或已被删除</div>;
}
if (app.appType !== "iframe") {
return <div className={centerClass}>该应用不是 iframe 嵌套类型</div>;
}
return (
<div className="flex h-full w-full flex-col">
<iframe src={src} title={app.name} className="w-full flex-1 border-0" />
</div>
);
}
+114
View File
@@ -0,0 +1,114 @@
/**
* 应用列表页 —— 以卡片网格展示「新页跳转(window.open)」类型的轻应用。
*
* 支持搜索 + 分页。点击卡片用 buildLightAppUrl 构造最终地址(追加解析后的路由参数)
* 并 window.open 新开页跳转。仅展示已启用的应用;iframe 类型不在此处(它们注入侧边栏)。
*/
import { useEffect, useMemo, useState } from "react";
import { ExternalLink, Search } from "lucide-react";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Pagination } from "@/components/ui/pagination";
import { useTheme } from "@/contexts/ThemeContext";
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
import { buildLightAppUrl } from "@/strategy-components/lib/light-app";
const PAGE_SIZE = 12;
export default function LightAppListPage() {
const { theme } = useTheme();
const isDark = theme === "dark";
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const { data: apps = [], isLoading } = useLightApps({
appType: "window_open",
enabledOnly: true,
search,
});
// 搜索变化时回到第一页。
useEffect(() => {
setPage(1);
}, [search]);
const pageApps = useMemo(
() => apps.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
[apps, page],
);
const openApp = (app: LightApp) => {
const url = buildLightAppUrl(app);
if (!url) {
toast.error("该应用地址为空,无法打开");
return;
}
window.open(url, "_blank", "noopener,noreferrer");
};
const shellClass = isDark
? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100"
: "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]";
const cardClass = isDark
? "group cursor-pointer rounded-2xl border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] p-4 transition-all hover:-translate-y-0.5 hover:border-[rgba(77,141,255,0.6)] hover:shadow-[0_18px_38px_rgba(3,10,22,0.35)]"
: "group cursor-pointer rounded-2xl border border-[#E8E8E8] bg-white p-4 transition-all hover:-translate-y-0.5 hover:border-[#FFA39E] hover:shadow-[0_16px_32px_rgba(245,34,45,0.1)]";
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
const iconWrap = isDark
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
: "bg-[#FFF1F0] text-[#CF1322]";
return (
<div className={shellClass}>
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col gap-5 px-6 py-6 lg:px-8">
<div className="space-y-1">
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? "text-slate-50" : "text-[#222222]"}`}>
应用列表
</h1>
<p className={`text-sm ${isDark ? "text-slate-300/80" : "text-slate-500"}`}>
点击卡片在新标签页打开应用。地址会自动附带配置的路由参数(登录态 / 主题)。
</p>
</div>
<div className="relative max-w-sm">
<Search className={`pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 ${mutedText}`} />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索应用名称 / 描述"
className="pl-9"
/>
</div>
{apps.length === 0 ? (
<div className={`rounded-3xl border py-20 text-center ${isDark ? "border-[rgba(67,89,124,0.4)] text-slate-500" : "border-[#F0F0F0] text-[#A6A6A6]"}`}>
{isLoading ? "加载中…" : "暂无可用应用"}
</div>
) : (
<>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{pageApps.map((app) => (
<button key={app.appId} type="button" onClick={() => openApp(app)} className={`${cardClass} text-left`}>
<div className="flex items-start gap-3">
<span className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl ${iconWrap}`}>
<ExternalLink className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<div className={`truncate text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
{app.name}
</div>
<div className={`mt-1 line-clamp-2 text-xs leading-relaxed ${isDark ? "text-slate-400" : "text-slate-500"}`}>
{app.description || app.url}
</div>
</div>
</div>
</button>
))}
</div>
<Pagination page={page} pageSize={PAGE_SIZE} total={apps.length} onPageChange={setPage} className="pt-2" />
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,676 @@
import { useMemo, useState } from "react";
import {
ExternalLink,
HelpCircle,
Info,
Layers,
Pencil,
Plus,
Search,
Trash2,
X,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
useLightAppActions,
useLightApps,
type LightApp,
type LightAppRouteParam,
type LightAppType,
} from "@/strategy-components/api/light-apps";
import {
LOGIN_SOURCE_OPTIONS,
MOUNT_OPTIONS,
mountLabel,
} from "@/strategy-components/lib/light-app";
interface FormState {
name: string;
description: string;
appType: LightAppType;
url: string;
mountLocation: string;
enabled: boolean;
routeParams: LightAppRouteParam[];
}
const emptyForm = (): FormState => ({
name: "",
description: "",
appType: "window_open",
url: "",
mountLocation: "",
enabled: true,
routeParams: [],
});
const newParam = (): LightAppRouteParam => ({
key: "",
paramType: "login",
source: "accessToken",
customValue: "",
themeValue: "dark-blue",
});
function formFromApp(app: LightApp): FormState {
return {
name: app.name,
description: app.description ?? "",
appType: app.appType,
url: app.url,
mountLocation: app.mountLocation ?? "",
enabled: app.enabled,
routeParams: app.routeParams.map((p) => ({
key: p.key,
paramType: p.paramType,
source: p.source ?? "accessToken",
customValue: p.customValue ?? "",
themeValue: p.themeValue ?? "dark-blue",
})),
};
}
function getParamSourceLabel(param: LightAppRouteParam): string {
if (param.paramType === "theme") {
return `主题参数: ${param.themeValue || ""}`;
}
const source = param.source || "accessToken";
const option = LOGIN_SOURCE_OPTIONS.find((o) => o.value === source);
if (source === "other") {
return `登录参数: ${param.customValue || ""}`;
}
return `登录参数: ${option?.label || source}`;
}
export default function LightAppManagementPage() {
const [search, setSearch] = useState("");
const { data: apps = [], isLoading } = useLightApps({ search });
const actions = useLightAppActions();
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<LightApp | null>(null);
const [form, setForm] = useState<FormState>(emptyForm);
const [activeParamPopover, setActiveParamPopover] = useState<string | null>(null);
const saving = actions.create.isPending || actions.update.isPending;
const openCreate = () => {
setEditing(null);
setForm(emptyForm());
setDialogOpen(true);
};
const openEdit = (app: LightApp) => {
setEditing(app);
setForm(formFromApp(app));
setDialogOpen(true);
};
const patchForm = (patch: Partial<FormState>) =>
setForm((f) => ({ ...f, ...patch }));
const patchParam = (idx: number, patch: Partial<LightAppRouteParam>) =>
setForm((f) => ({
...f,
routeParams: f.routeParams.map((p, i) => (i === idx ? { ...p, ...patch } : p)),
}));
const addParam = () => patchForm({ routeParams: [...form.routeParams, newParam()] });
const removeParam = (idx: number) =>
patchForm({ routeParams: form.routeParams.filter((_, i) => i !== idx) });
const handleSave = async () => {
const name = form.name.trim();
const url = form.url.trim();
if (!name) return toast.error("请输入应用名称");
if (!url) return toast.error("请输入应用地址");
if (form.appType === "iframe" && !form.mountLocation) {
return toast.error("iframe 类型请选择挂载菜单");
}
for (const p of form.routeParams) {
if (!p.key.trim()) return toast.error("路由参数键名不可为空");
if (p.paramType === "login" && p.source === "other" && !p.customValue?.trim()) {
return toast.error("登录参数自定义值不可为空");
}
if (p.paramType === "theme" && !p.themeValue?.trim()) {
return toast.error("主题参数值不可为空");
}
}
const payload = {
name,
description: form.description.trim() || null,
appType: form.appType,
url,
routeParams: cleanParams(form.routeParams),
mountLocation: form.appType === "iframe" ? form.mountLocation : null,
enabled: form.enabled,
};
try {
if (editing) {
await actions.update.mutateAsync({ appId: editing.appId, patch: payload });
toast.success(`已更新「${name}」`);
} else {
await actions.create.mutateAsync(payload);
toast.success(`已注册「${name}」`);
}
setDialogOpen(false);
} catch (err) {
toast.error((err as Error).message || "保存失败");
}
};
const handleDelete = async (app: LightApp) => {
if (!window.confirm(`是否确定要删除「${app.name}」?该操作不可逆。`)) return;
try {
await actions.remove.mutateAsync(app.appId);
toast.success(`已删除「${app.name}」`);
} catch (err) {
toast.error((err as Error).message || "删除失败");
}
};
const tableApps = useMemo(() => apps, [apps]);
return (
<div className="h-full overflow-y-auto bg-[#FAFAFA]">
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col space-y-6 px-6 py-6">
<div className="flex items-start justify-between">
<div className="space-y-1">
<h1 className="text-[20px] font-bold tracking-tight text-[#222222]">应用管理</h1>
<p className="max-w-4xl text-[13px] leading-relaxed text-[#575757]">
注册轻应用。window.open 类型在「应用列表」以卡片展示;iframe 类型挂载到所选菜单下并注入侧栏。
</p>
</div>
<button
onClick={openCreate}
className="flex h-9 cursor-pointer items-center space-x-1.5 rounded-[2px] bg-[#A8071A] px-4 text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-all hover:bg-[#BC0D1E] active:bg-[#820014]"
>
<span>+ 注册应用</span>
</button>
</div>
<div className="relative max-w-sm">
<Search className="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[#C1C1C1]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索应用名称 / 描述"
className="h-9 w-full rounded-[2px] border border-[#E8E8E8] bg-white pl-10 pr-4 text-[14px] text-[#222222] placeholder-[#C1C1C1] shadow-sm transition-all focus:border-[#F5222D] focus:outline-none focus:ring-1 focus:ring-[#F5222D]"
/>
{search && (
<button
onClick={() => setSearch("")}
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-[12px] text-[#A6A6A6] hover:text-[#F5222D]"
>
清除
</button>
)}
</div>
<div className="overflow-visible rounded-[4px] border border-[#E8E8E8] bg-white shadow-[0_2px_8px_rgba(34,34,34,0.08)]">
<table className="w-full table-fixed border-collapse text-left">
<thead>
<tr className="border-b border-[#E8E8E8] bg-[#FAFAFA] text-[12px] font-bold uppercase tracking-wider text-[#A6A6A6]">
<th className="w-[18%] px-6 py-3.5">名称</th>
<th className="w-[12%] px-4 py-3.5">类型</th>
<th className="w-[34%] px-4 py-3.5">地址 / 挂载</th>
<th className="w-[12%] px-4 py-3.5">参数</th>
<th className="w-[12%] px-4 py-3.5">启用</th>
<th className="w-[12%] px-4 py-3.5 pr-6 text-right">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-[#E8E8E8]">
{tableApps.map((app) => (
<tr key={app.appId} className="group align-top transition-colors hover:bg-[#FEFAFA]">
<td className="px-6 py-[18px]">
<div className="flex flex-col space-y-1">
<span className="text-[14px] font-bold text-[#222222] transition-colors group-hover:text-[#F5222D]">
{app.name}
</span>
{app.description ? (
<span className="line-clamp-2 text-[12px] text-[#575757]">{app.description}</span>
) : (
<span className="text-[11px] italic text-[#C1C1C1]">无描述说明</span>
)}
</div>
</td>
<td className="px-4 py-[18px]">
{app.appType === "window_open" ? (
<span className="inline-flex select-none items-center space-x-1 rounded-[2px] border border-[#FDDADC] bg-[#FEEDED] px-2 py-0.5 text-[11px] font-medium text-[#F5222D] shadow-sm">
<ExternalLink className="h-3 w-3" />
<span>新页跳转</span>
</span>
) : (
<span className="inline-flex select-none items-center space-x-1 rounded-[2px] border border-[#D4E9FF] bg-[#EAF4FF] px-2 py-0.5 text-[11px] font-medium text-[#0079FE] shadow-sm">
<Layers className="h-3 w-3" />
<span>iframe 嵌套</span>
</span>
)}
</td>
<td className="overflow-hidden px-4 py-[18px]">
<div className="flex max-w-full flex-col space-y-1">
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-1.5 truncate font-mono text-[13px] text-[#222222] hover:text-[#0079FE] hover:underline"
>
<span className="truncate">{app.url}</span>
</a>
{app.appType === "iframe" && app.mountLocation && (
<div className="flex items-center space-x-1 text-[11px] text-[#A6A6A6]">
<span className="h-1.5 w-1.5 rounded-full bg-[#0079FE]" />
<span>挂载: {mountLabel(app.mountLocation)}</span>
</div>
)}
</div>
</td>
<td className="relative px-4 py-[18px]">
<div className="flex items-center select-none">
{app.routeParams.length > 0 ? (
<button
onClick={() =>
setActiveParamPopover(
activeParamPopover === app.appId ? null : app.appId
)
}
className="group/param inline-flex cursor-pointer items-center space-x-1 text-[14px] text-[#222222] transition-colors hover:text-[#0079FE]"
>
<span className="font-medium">{app.routeParams.length}个</span>
<span className="text-[10px] text-[#A6A6A6] transition-colors group-hover/param:text-[#0079FE]">
▼
</span>
</button>
) : (
<span className="text-[14px] text-[#A6A6A6]">0个</span>
)}
{activeParamPopover === app.appId && app.routeParams.length > 0 && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setActiveParamPopover(null)}
/>
<div className="absolute left-4 top-11 z-50 mt-1 w-[280px] animate-in fade-in rounded-[4px] border border-[#E8E8E8] bg-white p-3 text-[12px] shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="mb-2 flex items-center justify-between border-b border-[#E8E8E8] pb-2 font-semibold text-[#3D3D3D]">
<span>路由参数明细</span>
<span className="rounded-sm bg-[#EAF4FF] px-1.5 py-0.5 text-[10px] text-[#0079FE]">
URL 拼接
</span>
</div>
<div className="max-h-[160px] space-y-1.5 overflow-y-auto">
{app.routeParams.map((p, idx) => (
<div
key={`${app.appId}-${idx}`}
className="flex items-center justify-between rounded-sm bg-[#FAFAFA] p-1.5 font-mono hover:bg-[#FEF6F6]"
>
<span className="font-bold text-[#F5222D]">{p.key}</span>
<span className="text-[11px] text-[#A6A6A6]">
({getParamSourceLabel(p)})
</span>
</div>
))}
</div>
</div>
</>
)}
</div>
</td>
<td className="px-4 py-[18px]">
<div className="flex select-none items-center space-x-2">
{app.enabled ? (
<>
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#2FD69A] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[#2FD69A]" />
</span>
<span className="text-[13px] font-semibold text-[#189D72]">已启用</span>
</>
) : (
<>
<span className="h-2 w-2 rounded-full bg-[#A6A6A6]" />
<span className="text-[13px] text-[#A6A6A6]">已禁用</span>
</>
)}
</div>
</td>
<td className="px-4 py-[18px] pr-6 text-right">
<div className="inline-flex items-center justify-end space-x-2.5">
<button
onClick={() => openEdit(app)}
className="group/btn flex h-[34px] w-[34px] cursor-pointer items-center justify-center rounded-[4px] border border-[#D4E9FF] bg-[#EAF4FF] text-[#0079FE] transition-all duration-200 hover:scale-105 hover:border-[#0079FE] hover:bg-[#0079FE] hover:text-white active:scale-95"
title="编辑配置"
>
<Pencil className="h-3.5 w-3.5 transition-transform duration-200 group-hover/btn:rotate-12" />
</button>
<button
onClick={() => handleDelete(app)}
className="group/btn flex h-[34px] w-[34px] cursor-pointer items-center justify-center rounded-[4px] border border-[#FDDADC] bg-[#FEEDED] text-[#F5222D] transition-all duration-200 hover:scale-105 hover:border-[#F5222D] hover:bg-[#F5222D] hover:text-white active:scale-95"
title="删除配置"
>
<Trash2 className="h-3.5 w-3.5 transition-transform duration-200 group-hover/btn:scale-110" />
</button>
</div>
</td>
</tr>
))}
{tableApps.length === 0 && (
<tr>
<td colSpan={6} className="py-12 text-center">
<div className="flex flex-col items-center justify-center space-y-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#F5F5F5] text-[#C1C1C1]">
<Info className="h-6 w-6" />
</div>
<div className="space-y-1">
<p className="text-[14px] font-semibold text-[#A6A6A6]">
{isLoading ? "加载中..." : "暂无匹配的组件应用"}
</p>
{!isLoading && (
<p className="text-[12px] text-[#C1C1C1]">
{search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"}
</p>
)}
</div>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="flex max-w-full select-none items-start space-x-3.5 rounded-[4px] border border-[#FFBB55] bg-[#FFF6EA] p-4 text-[13px] text-[#575757]">
<Info className="mt-0.5 h-5 w-5 flex-shrink-0 text-[#FF9900]" />
<div className="space-y-1">
<p className="font-bold text-[#222222]">📌 智能多维挂载规则:</p>
<ul className="list-inside list-disc space-y-1 pl-1">
<li>
当您注册 <strong>iframe 嵌套</strong>{" "}
应用时,您需要在菜单一栏指定分类(如“资源管理”),组件将直接作为新导航菜单项加入该分类。
</li>
<li>
当路由挂载成功,点击左侧注入的子栏名称可在主区域调起{" "}
<strong>真实 Iframe 预览器</strong>
,并自动将参数以加密/标准的QueryString传递过去。
</li>
</ul>
</div>
</div>
</div>
{dialogOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center overflow-y-auto select-none">
<div
className="fixed inset-0 bg-[#3D3D3D]/40 backdrop-blur-[1px]"
onClick={() => setDialogOpen(false)}
/>
<div className="relative z-[110] m-4 flex max-h-[90vh] w-full max-w-[580px] flex-col rounded-[4px] border border-[#E8E8E8] bg-white p-6 shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="flex flex-shrink-0 items-center justify-between border-b border-[#E8E8E8] pb-4">
<h2 className="text-[17px] font-bold text-[#222222]">
{editing ? "修改轻应用" : "注册应用"}
</h2>
<button
onClick={() => setDialogOpen(false)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="flex-1 space-y-4 overflow-y-auto py-4 pr-1 text-[#222222]">
<div className="space-y-1.5">
<label className="flex items-center text-[14px] font-medium">
<span>应用类型</span>
<span className="ml-1 text-[#F5222D]">*</span>
</label>
<select
value={form.appType}
onChange={(e) => patchForm({ appType: e.target.value as LightAppType })}
className="h-9 w-full cursor-pointer rounded-[2px] border border-[#DBDBDB] bg-white px-3 text-[14px] focus:border-[#F5222D] focus:outline-none focus:ring-1 focus:ring-[#F5222D]"
>
<option value="iframe">iframe 嵌套 (挂载到菜单)</option>
<option value="window_open">window.open (新页跳转)</option>
</select>
</div>
<div className="space-y-1.5">
<label className="flex items-center text-[14px] font-medium">
<span>应用名称</span>
<span className="ml-1 text-[#F5222D]">*</span>
</label>
<Input
value={form.name}
onChange={(e) => patchForm({ name: e.target.value })}
placeholder="如: 舆情看板"
className="h-9 rounded-[2px] border-[#DBDBDB] text-[14px]"
/>
</div>
<div className="space-y-1.5">
<label className="flex items-center text-[14px] font-medium">
<span>应用地址</span>
<span className="ml-1 text-[#F5222D]">*</span>
</label>
<Input
value={form.url}
onChange={(e) => patchForm({ url: e.target.value })}
placeholder="https://example.com/page 或 http://47.88.25.99:3000/"
className="h-9 rounded-[2px] border-[#DBDBDB] font-mono text-[14px]"
/>
</div>
{form.appType === "iframe" && (
<div className="animate-in fade-in space-y-1.5">
<label className="flex items-center text-[14px] font-medium">
<span>挂载菜单</span>
<span className="ml-1 text-[#F5222D]">*</span>
</label>
<select
value={form.mountLocation}
onChange={(e) => patchForm({ mountLocation: e.target.value })}
className="h-9 w-full cursor-pointer rounded-[2px] border border-[#DBDBDB] bg-white px-3 text-[14px] focus:border-[#F5222D] focus:outline-none focus:ring-1 focus:ring-[#F5222D]"
>
<option value="">请选择挂载菜单</option>
{MOUNT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
)}
<div className="space-y-1.5">
<label className="text-[14px] font-medium">应用描述 (可选)</label>
<Textarea
value={form.description}
onChange={(e) => patchForm({ description: e.target.value })}
placeholder="简要说明该应用的用途"
rows={2}
className="resize-none rounded-[2px] border-[#DBDBDB] text-[14px]"
/>
</div>
<div className="space-y-2.5 pt-1.5">
<div className="flex items-center justify-between">
<span className="flex items-center text-[14px] font-medium">
<span>路由参数</span>
<HelpCircle
className="ml-1 h-3.5 w-3.5 text-[#A6A6A6]"
title="参数会在加载此应用时作为 query 参数拼接在 URL 的尾部"
/>
</span>
<button
type="button"
onClick={addParam}
className="flex h-7 cursor-pointer items-center space-x-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2.5 text-[12px] font-bold text-[#575757] transition-colors hover:border-[#F5222D] hover:text-[#F5222D]"
>
<Plus className="h-3.5 w-3.5" />
<span>+ 添加路由参数</span>
</button>
</div>
{form.routeParams.length > 0 ? (
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-sm border border-[#E8E8E8] bg-[#FAFAFA] p-1">
{form.routeParams.map((p, idx) => (
<div
key={`${p.key}-${idx}`}
className="relative flex items-center space-x-2 rounded-sm border border-[#E8E8E8] bg-white p-2"
>
<div className="flex-1 space-y-1">
<input
type="text"
value={p.key}
onChange={(e) => patchParam(idx, { key: e.target.value })}
placeholder="参数名 key"
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
</div>
<div className="w-[110px]">
<select
value={p.paramType}
onChange={(e) =>
patchParam(idx, {
paramType: e.target.value as "login" | "theme",
source:
e.target.value === "login" ? (p.source || "accessToken") : undefined,
})
}
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-[#FAFAFA] px-1 text-[12px] focus:border-[#F5222D] focus:outline-none"
>
<option value="login">登录参数</option>
<option value="theme">主题参数</option>
</select>
</div>
<div className="flex-[1.2] text-[11px]">
{p.paramType === "login" ? (
<div className="flex items-center gap-2">
<select
value={p.source || "accessToken"}
onChange={(e) => patchParam(idx, { source: e.target.value })}
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-1 text-[12px] focus:border-[#F5222D] focus:outline-none"
>
{LOGIN_SOURCE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
{p.source === "other" && (
<input
type="text"
value={p.customValue || ""}
onChange={(e) =>
patchParam(idx, { customValue: e.target.value })
}
placeholder="自定义值"
className="h-8 w-[120px] rounded-[2px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
/>
)}
</div>
) : (
<input
type="text"
value={p.themeValue || ""}
onChange={(e) => patchParam(idx, { themeValue: e.target.value })}
placeholder="主题值,如 dark-blue"
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
)}
</div>
<button
type="button"
onClick={() => removeParam(idx)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
) : (
<div className="rounded-sm border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
暂无注入的路由参数,加载地址时不传递多余拼接。
</div>
)}
</div>
<div className="flex items-center justify-between border-t border-[#E8E8E8] py-2.5">
<span className="text-[14px] font-medium text-[#222222]">立即启用该应用</span>
<button
type="button"
onClick={() => patchForm({ enabled: !form.enabled })}
className={`relative h-[22px] w-10 cursor-pointer rounded-full p-0.5 transition-colors duration-200 ${
form.enabled ? "bg-[#F5222D]" : "bg-[#DBDBDB]"
}`}
>
<div
className={`h-[18px] w-[18px] rounded-full bg-white shadow-sm transition-transform duration-200 ${
form.enabled ? "translate-x-[18px]" : "translate-x-0"
}`}
/>
</button>
</div>
</div>
<div className="flex flex-shrink-0 items-center justify-end space-x-3 border-t border-[#E8E8E8] pt-4">
<Button
variant="outline"
disabled={saving}
onClick={() => setDialogOpen(false)}
className="h-9 rounded-[2px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
>
取消
</Button>
<button
type="button"
disabled={saving}
onClick={handleSave}
className="h-9 cursor-pointer rounded-[2px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70"
>
{saving ? "保存中..." : editing ? "保存修改" : "立即注册"}
</button>
</div>
</div>
</div>
)}
</div>
);
}
function cleanParams(params: LightAppRouteParam[]): LightAppRouteParam[] {
return params
.filter((p) => p.key.trim())
.map((p) => {
const base: LightAppRouteParam = { key: p.key.trim(), paramType: p.paramType };
if (p.paramType === "login") {
base.source = p.source || "accessToken";
if (base.source === "other") {
base.customValue = (p.customValue ?? "").trim();
}
} else {
base.themeValue = (p.themeValue ?? "").trim();
}
return base;
});
}
+10 -6
View File
@@ -5,14 +5,12 @@ import { SidebarProvider } from "@/components/ui/sidebar";
import { getStoredAuth } from "@/core/auth";
import { RouteBaseProvider } from "@/core/route-base";
import GeneralQA from "@/strategy-components/components/chat-qa/GeneralQA";
import FanganTyPage from "@/strategy-components/components/task-components/FanganTy";
import SentimentAnalyzes from "@/strategy-components/components/task-components/SentimentAnalyzes";
import TaskManagement from "@/strategy-components/components/task-components/TaskManagement";
import TaskPgFrom from "@/strategy-components/components/task-components/TaskPgFrom";
import TaskTyFrom from "@/strategy-components/components/task-components/TaskTyFrom";
import TaskXkFrom from "@/strategy-components/components/task-components/TaskXkFrom";
import TaskAnalyzes from "@/strategy-components/components/task-components/TaskAnalyzes";
import XingdongTyPage from "@/strategy-components/components/task-components/XingdongTy";
import XXTyPage from "@/strategy-components/components/task-components/XXTy";
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
@@ -27,6 +25,9 @@ import ChatPage from "./ChatPage";
import ChatsPage from "./ChatsPage";
import AdminUserLeaderboardPage from "./AdminUserLeaderboardPage";
import AdminUserLeaderboardSettingsPage from "./AdminUserLeaderboardSettingsPage";
import LightAppIframeView from "./LightAppIframeView";
import LightAppListPage from "./LightAppListPage";
import LightAppManagementPage from "./LightAppManagementPage";
import LightAppNewNotePage from "./LightAppNewNotePage";
import LightAppNoteListPage from "./LightAppNoteListPage";
import LightAppNoteQAPage from "./LightAppNoteQAPage";
@@ -125,9 +126,9 @@ export default function StrategyRoutes() {
/>
}
/>
<Route path="mnty-task-mechanism" element={<XingdongTyPage />} />
<Route path="mnty-task-plan" element={<FanganTyPage />} />
<Route path="mnty-task-zsty" element={<XXTyPage />} />
<Route path="mnty-task-mechanism" element={<TaskTyFrom defaultPage="iframeXdty" title="XDTY任务" />} />
<Route path="mnty-task-plan" element={<TaskTyFrom defaultPage="iframeFaty" title="方案TY任务" />} />
<Route path="mnty-task-zsty" element={<TaskTyFrom defaultPage="iframeSysystem" title="XXTY任务" />} />
</Route>
<Route
@@ -143,6 +144,9 @@ export default function StrategyRoutes() {
element={<TaskXkFrom gopath="#/pageIndie/pageZyjc/pageZyzl" />}
/>
<Route path="light-app/management" element={<LightAppManagementPage />} />
<Route path="light-app/apps" element={<LightAppListPage />} />
<Route path="light-app/view/:appId" element={<LightAppIframeView />} />
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
<Route path="light-app/note-qa" element={<LightAppNoteQAPage />} />