feat(ui): theme & layout improvements

- Dark-blue theme background assets and styling integration
- Appearance settings with layoutMode (full/compact) preference
- Settings dialog and auth context updates
- AI writing form and panel refinements
- Open Canvas context updates
- Roundtable planning styling and dashboard enhancements
- Strategy components and header updates
- Light app management and routing improvements
- Global style enhancements for dark theme
- Various page layout and workspace routing optimizations
This commit is contained in:
董宏凯 committed 2026-06-14 19:30:54 +08:00
1 parent da2864f422
commit 56449ed129
33 files changed
+1133 -491

No files matched your search

+1 -1
View File
@@ -180,7 +180,7 @@ export default function AgentChatPage() {
separate flex sibling OUTSIDE ChatBox, so the artifact preview
always opens between the chat and the detail panel — the detail
panel stays rightmost. */}
<div className="flex size-full min-h-0 overflow-hidden">
<div className="flex size-full min-h-0 overflow-hidden workspace-chat-bg">
<div className="min-h-0 min-w-0 flex-1">
<ChatBox threadId={threadId}>
{/* Main conversation column. `relative` hosts the floating
@@ -1,14 +1,23 @@
/**
* 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
* 配置管理页 —— 把固定业务码绑定到「智能体」+「业务链」。
*
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
* - 智能体来自 `/api/agents`
* - 业务链来自 `/api/roundtable-chains?scope=public`(只列公共链,保证全局共享时各账号都能解析)
* 智能体来自 `/api/agents`,业务链来自 `/api/roundtable-chains?scope=public`。
* 保存调 `PUT /api/business-mapping/{code}`。映射全局共享、无用户隔离。
*/
import { useEffect, useMemo, useState } from "react";
import { Loading, Select } from "tdesign-react";
import { Link2, Pencil, Save, Users, X } from "lucide-react";
import { motion } from "motion/react";
import { Select } from "tdesign-react";
import {
AlertCircle,
HelpCircle,
Info,
Link2,
Pencil,
Save,
Search,
Users,
X,
} from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "@/contexts/ThemeContext";
@@ -21,11 +30,7 @@ import {
updateBusinessMapping,
type BusinessMapping,
} from "@/strategy-components/api/business-mapping";
interface DraftSelection {
agentId: string | null;
chainId: string | null;
}
import { cn } from "@/lib/utils";
export default function BusinessChainMappingPage() {
const { theme } = useTheme();
@@ -36,8 +41,13 @@ export default function BusinessChainMappingPage() {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [loading, setLoading] = useState(true);
const [editingCode, setEditingCode] = useState<string | null>(null);
const [draft, setDraft] = useState<DraftSelection>({ agentId: null, chainId: null });
const [draft, setDraft] = useState<{ agentId: string | null; chainId: string | null }>({
agentId: null,
chainId: null,
});
const [saving, setSaving] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "configured" | "pending">("all");
useEffect(() => {
let cancelled = false;
@@ -58,9 +68,7 @@ export default function BusinessChainMappingPage() {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
return () => { cancelled = true; };
}, []);
const agentOptions = useMemo(
@@ -72,6 +80,24 @@ export default function BusinessChainMappingPage() {
[chains],
);
const stats = useMemo(() => {
const total = rows.length;
const boundAgents = rows.filter((r) => r.agentId !== null).length;
return { total, boundAgents };
}, [rows]);
const filteredRows = useMemo(() => {
return rows.filter((r) => {
const q = searchQuery.toLowerCase();
const matchesSearch =
r.label.toLowerCase().includes(q) || r.businessCode.toLowerCase().includes(q);
const isConfigured = r.agentId !== null && r.chainId !== null;
if (statusFilter === "configured") return matchesSearch && isConfigured;
if (statusFilter === "pending") return matchesSearch && !isConfigured;
return matchesSearch;
});
}, [rows, searchQuery, statusFilter]);
const beginEdit = (row: BusinessMapping) => {
setEditingCode(row.businessCode);
setDraft({ agentId: row.agentId, chainId: row.chainId });
@@ -86,10 +112,10 @@ export default function BusinessChainMappingPage() {
setSaving(true);
try {
const agentName = draft.agentId
? agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null
? (agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null)
: null;
const chainTitle = draft.chainId
? chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null
? (chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null)
: null;
const updated = await updateBusinessMapping(row.businessCode, {
agentId: draft.agentId,
@@ -111,162 +137,416 @@ export default function BusinessChainMappingPage() {
}
};
/* ── 样式 ── */
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]";
? "h-full overflow-y-auto text-slate-100 workspace-chat-bg"
: "h-full overflow-y-auto bg-white text-[#222]";
// 卡片:无边框,纯阴影
const cardClass = isDark
? "border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] shadow-[0_16px_36px_rgba(3,10,22,0.2)]"
: "border border-[#E8E8E8] bg-white shadow-[0_14px_30px_rgba(245,34,45,0.06)]";
const headRowClass = isDark ? "text-slate-400" : "text-[#8C8C8C]";
const rowBorder = isDark ? "border-[rgba(67,89,124,0.32)]" : "border-[#F0F0F0]";
? "bg-[rgba(12,29,54,0.88)] shadow-[0_8px_32px_rgba(3,10,22,0.28)] backdrop-blur-sm"
: "bg-white shadow-[0_8px_32px_rgba(0,0,0,0.07)]";
// 行分隔:极淡,不带色
const rowDivider = isDark ? "border-white/[0.04]" : "border-black/[0.04]";
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
const codeBadge = isDark
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
: "bg-[#FFF1F0] text-[#CF1322]";
const subText = isDark ? "text-slate-400" : "text-slate-500";
const filterTabs: { id: "all" | "configured" | "pending"; label: string }[] = [
{ id: "all", label: "全部业务" },
{ id: "configured", label: "已绑定" },
{ id: "pending", label: "待配置" },
];
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={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
<div className="mx-auto flex min-h-full max-w-7xl flex-col gap-6 px-6 py-7 lg:px-8">
<Loading loading={loading} showOverlay={false}>
<div className={`overflow-hidden rounded-3xl ${cardClass}`}>
<table className="w-full border-collapse text-sm">
<thead>
<tr className={`text-left text-xs uppercase tracking-widest ${headRowClass}`}>
<th className="px-6 py-4 font-semibold">业务</th>
<th className="px-6 py-4 font-semibold">智能体</th>
<th className="px-6 py-4 font-semibold">业务链</th>
<th className="px-6 py-4 text-right font-semibold">操作</th>
</tr>
</thead>
<tbody>
{rows.length === 0 && !loading ? (
<tr>
<td colSpan={4} className={`px-6 py-12 text-center ${mutedText}`}>
暂无业务数据
</td>
</tr>
) : (
rows.map((row) => {
const editing = editingCode === row.businessCode;
return (
<tr key={row.businessCode} className={`border-t ${rowBorder}`}>
{/* 业务(只读) */}
<td className="px-6 py-4 align-middle">
<div className="flex items-center gap-2">
<span className={`text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
{row.label}
</span>
<span className={`rounded-full px-2 py-0.5 text-[11px] font-mono ${codeBadge}`}>
{row.businessCode}
</span>
</div>
</td>
{/* ── 页头 banner ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className={cn("rounded-[5px] p-6 lg:p-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 relative overflow-hidden", cardClass)}
>
<div
className="absolute right-0 top-0 w-72 h-32 pointer-events-none"
style={{
background: isDark
? "radial-gradient(ellipse at top right, rgba(77,141,255,0.07), transparent)"
: "radial-gradient(ellipse at top right, rgba(245,34,45,0.04), transparent)",
}}
/>
{/* 智能体 */}
<td className="px-6 py-4 align-middle">
{editing ? (
<Select
className="min-w-[200px]"
value={draft.agentId ?? undefined}
options={agentOptions}
filterable
clearable
placeholder="搜索并选择智能体"
onChange={(v) => setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))}
/>
) : (
<span className="inline-flex items-center gap-1.5">
<Users className={`h-3.5 w-3.5 ${mutedText}`} />
{row.agentName || row.agentId ? (
<span>{row.agentName || row.agentId}</span>
) : (
<span className={mutedText}>未配置</span>
)}
</span>
)}
</td>
{/* 业务链 */}
<td className="px-6 py-4 align-middle">
{editing ? (
<Select
className="min-w-[220px]"
value={draft.chainId ?? undefined}
options={chainOptions}
filterable
clearable
placeholder="搜索并选择业务链(公共)"
onChange={(v) => setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))}
/>
) : (
<span className="inline-flex items-center gap-1.5">
<Link2 className={`h-3.5 w-3.5 ${mutedText}`} />
{row.chainTitle || row.chainId ? (
<span>{row.chainTitle || row.chainId}</span>
) : (
<span className={mutedText}>未配置</span>
)}
</span>
)}
</td>
{/* 操作 */}
<td className="px-6 py-4 text-right align-middle">
{editing ? (
<div className="inline-flex items-center gap-2">
<button
type="button"
disabled={saving}
onClick={() => handleSave(row)}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
isDark ? "bg-[rgba(34,197,94,0.18)] text-[#86EFAC]" : "bg-[#EEFCF7] text-[#0F9D6C]"
}`}
>
<Save className="h-3.5 w-3.5" />
保存
</button>
<button
type="button"
disabled={saving}
onClick={cancelEdit}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
isDark ? "bg-[rgba(110,136,176,0.18)] text-slate-300" : "bg-[#F5F5F5] text-[#575757]"
}`}
>
<X className="h-3.5 w-3.5" />
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => beginEdit(row)}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium ${
isDark ? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]" : "bg-[#FFF1F0] text-[#CF1322]"
}`}
>
<Pencil className="h-3.5 w-3.5" />
编辑
</button>
)}
</td>
</tr>
);
})
<div className="space-y-3 relative z-10">
<div className="flex flex-wrap items-center gap-2.5">
<h2 className={cn("text-xl font-bold tracking-tight", isDark ? "text-slate-50" : "text-slate-900")}>
配置管理
</h2>
<span
className={cn(
"text-[10px] uppercase font-mono tracking-wider font-bold px-2.5 py-1 rounded-[5px] flex items-center gap-1.5",
isDark
? "bg-[rgba(77,141,255,0.12)] text-[#9CCBFF]"
: "bg-red-50/80 text-red-500",
)}
</tbody>
</table>
>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse", isDark ? "bg-[#55A6FE]" : "bg-red-500")} />
智能会商神经中枢
</span>
<span
className={cn(
"text-[10px] font-mono tracking-wider px-2 py-0.5 rounded-[5px]",
isDark ? "text-slate-500 bg-white/5" : "text-slate-400 bg-slate-50",
)}
>
v1.2.5
</span>
</div>
<p className={cn("text-xs max-w-2xl leading-relaxed", subText)}>
业务流转与交叉验证的高级控制中心。在此注册高频业务模块并绑定专职执行智能体(Agent)及公共流转决策链(Chain),决策信息将基于该拓扑自动触发多智能体自诊断及会商组织。
</p>
<div className={cn("flex flex-wrap items-center gap-x-4 gap-y-1.5 pt-1 text-[11px] font-medium", mutedText)}>
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
会商引擎: 正常运行中
</span>
<span className="opacity-30">•</span>
<span>
全局配置项:{" "}
<strong className={isDark ? "text-slate-200 font-semibold" : "text-slate-700 font-semibold"}>
{stats.total} 项注册
</strong>
</span>
<span className="opacity-30">•</span>
<span>
指定专家节点:{" "}
<strong className={isDark ? "text-[#9CCBFF] font-semibold" : "text-red-500 font-semibold"}>
{stats.boundAgents} / {stats.total}
</strong>
</span>
</div>
</div>
</Loading>
</motion.div>
{/* ── 表格区 ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
className={cn("rounded-[5px] overflow-hidden", cardClass)}
>
{/* 搜索 + 筛选 */}
<div className={cn("px-6 py-5 border-b flex flex-col sm:flex-row sm:items-center justify-between gap-4", rowDivider)}>
<div className="relative w-full max-w-sm">
<Search className={cn("absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4", mutedText)} />
<input
type="text"
placeholder="搜索业务名称、ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={cn(
"w-full pl-10 pr-4 py-2.5 rounded-[5px] text-xs transition-all placeholder:text-slate-400 focus:outline-none",
isDark
? "text-slate-200 bg-white/5 focus:bg-white/8 focus:ring-1 focus:ring-[#4D8DFF]/30"
: "text-slate-700 bg-slate-50/60 hover:bg-slate-50 focus:bg-white focus:ring-1 focus:ring-red-400/30",
)}
/>
</div>
<div className={cn("flex items-center gap-1 p-1 rounded-[5px] self-end sm:self-auto shrink-0", isDark ? "bg-white/5" : "bg-slate-100/70")}>
{filterTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setStatusFilter(tab.id)}
className={cn(
"px-3.5 py-1.5 rounded-[5px] text-xs font-semibold transition-all cursor-pointer",
statusFilter === tab.id
? isDark
? "bg-[rgba(77,141,255,0.18)] text-[#9CCBFF] shadow-sm"
: "bg-white text-gray-900 shadow-sm"
: isDark
? "text-slate-500 hover:text-slate-300"
: "text-gray-400 hover:text-gray-700",
)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 表格 */}
<div className="overflow-x-auto">
{loading ? (
<div className={cn("py-16 text-center text-sm", mutedText)}>加载中…</div>
) : (
<table className="w-full table-fixed text-left border-collapse">
<thead>
<tr className={cn("border-b", rowDivider, isDark ? "bg-white/[0.02]" : "bg-black/[0.02]")}>
<th className={cn("pl-8 pr-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[20%]", mutedText)}>业务</th>
<th className={cn("px-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[26%]", mutedText)}>智能体</th>
<th className={cn("px-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[26%]", mutedText)}>业务链</th>
<th className={cn("pl-4 pr-8 py-3.5 text-xs font-semibold uppercase tracking-wider w-[28%] text-right", mutedText)}>操作</th>
</tr>
</thead>
<tbody>
{filteredRows.length > 0 ? (
filteredRows.map((row) => {
const editing = editingCode === row.businessCode;
return (
<tr
key={row.businessCode}
className={cn(
"border-b last:border-0 transition-colors duration-150",
rowDivider,
!editing && (isDark ? "hover:bg-white/[0.025]" : "hover:bg-black/[0.015]"),
editing && (isDark ? "bg-[rgba(77,141,255,0.04)]" : "bg-red-50/30"),
)}
>
{/* 业务 */}
<td className="pl-8 pr-4 py-5 align-middle">
<div className="flex items-center gap-2.5">
<span className={cn("font-semibold tracking-tight text-sm", isDark ? "text-slate-100" : "text-gray-800")}>
{row.label}
</span>
<span
className={cn(
"text-[10px] font-mono font-bold px-2 py-0.5 rounded-[5px] leading-none",
isDark
? "bg-[rgba(77,141,255,0.12)] text-[#9CCBFF]"
: "bg-red-50 text-red-400",
)}
>
{row.businessCode}
</span>
</div>
</td>
{/* 智能体 */}
<td className="px-4 py-4 align-middle">
{editing ? (
<Select
className="min-w-[200px]"
value={draft.agentId ?? undefined}
options={agentOptions}
filterable
clearable
placeholder="搜索并选择智能体"
onChange={(v) =>
setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))
}
/>
) : (
<div className="flex items-center gap-2.5">
<div
className={cn(
"p-1.5 rounded-[5px] transition-colors",
row.agentId
? isDark
? "bg-[rgba(77,141,255,0.1)] text-[#9CCBFF]"
: "bg-red-50 text-red-400"
: isDark
? "bg-white/5 text-slate-600"
: "bg-slate-50 text-gray-300",
)}
>
<Users className="w-3.5 h-3.5 shrink-0" />
</div>
{row.agentName || row.agentId ? (
<div className="space-y-0.5">
<span className={cn("text-xs font-semibold", isDark ? "text-slate-200" : "text-gray-800")}>
{row.agentName || row.agentId}
</span>
{row.agentId && agents.find((a) => a.id === row.agentId)?.description && (
<span className={cn("block text-[10px]", mutedText)}>
{(agents.find((a) => a.id === row.agentId)?.description ?? "").slice(0, 40)}
{(agents.find((a) => a.id === row.agentId)?.description?.length ?? 0) > 40 ? "…" : ""}
</span>
)}
</div>
) : (
<span className={cn("text-xs", mutedText)}>未配置</span>
)}
</div>
)}
</td>
{/* 业务链 */}
<td className="px-4 py-4 align-middle">
{editing ? (
<Select
className="min-w-[220px]"
value={draft.chainId ?? undefined}
options={chainOptions}
filterable
clearable
placeholder="搜索并选择业务链(公共)"
onChange={(v) =>
setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))
}
/>
) : (
<div className="flex items-center gap-2.5">
<div
className={cn(
"p-1.5 rounded-[5px] transition-colors",
row.chainId
? isDark
? "bg-[rgba(34,197,94,0.08)] text-green-400"
: "bg-blue-50 text-blue-400"
: isDark
? "bg-white/5 text-slate-600"
: "bg-slate-50 text-gray-300",
)}
>
<Link2 className="w-3.5 h-3.5 shrink-0" />
</div>
{row.chainTitle || row.chainId ? (
<div className="space-y-0.5">
<span className={cn("text-xs font-semibold", isDark ? "text-slate-200" : "text-gray-800")}>
{row.chainTitle || row.chainId}
</span>
{row.chainId && (
<span className={cn("flex items-center gap-1 text-[10px] leading-none", mutedText)}>
<span
className={cn(
"px-1 rounded-[5px] text-[9px] font-medium",
isDark
? "bg-[rgba(34,197,94,0.08)] text-green-400"
: "bg-green-50 text-green-600",
)}
>
公共
</span>
节点数: {chains.find((c) => c.id === row.chainId)?.seats?.length ?? "—"}
</span>
)}
</div>
) : (
<span className={cn("text-xs", mutedText)}>未配置</span>
)}
</div>
)}
</td>
{/* 操作 */}
<td className="pl-4 pr-8 py-4 text-right align-middle whitespace-nowrap">
{editing ? (
<div className="inline-flex items-center gap-2 justify-end">
<button
type="button"
disabled={saving}
onClick={() => handleSave(row)}
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all active:scale-95 disabled:opacity-50",
isDark
? "bg-[rgba(34,197,94,0.12)] text-green-400 hover:bg-[rgba(34,197,94,0.2)]"
: "bg-green-50 text-green-600 hover:bg-green-100",
)}
>
<Save className="h-3.5 w-3.5" />
保存
</button>
<button
type="button"
disabled={saving}
onClick={cancelEdit}
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all active:scale-95 disabled:opacity-50",
isDark
? "bg-white/5 text-slate-400 hover:bg-white/10 hover:text-slate-200"
: "bg-slate-50 text-slate-500 hover:bg-slate-100",
)}
>
<X className="h-3.5 w-3.5" />
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => beginEdit(row)}
title="编辑配置"
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all duration-200 active:scale-95",
isDark
? "text-[#9CCBFF] bg-[rgba(77,141,255,0.12)] hover:bg-[rgba(77,141,255,0.18)]"
: "text-red-500 bg-red-50 hover:bg-red-100",
)}
>
<Pencil className="h-3.5 w-3.5" />
<span>编辑</span>
</button>
)}
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={4} className="py-14 text-center">
<div className="flex flex-col items-center justify-center gap-2">
<AlertCircle className={cn("w-7 h-7", isDark ? "text-slate-700" : "text-gray-200")} />
<p className={cn("text-xs font-medium", mutedText)}>未找到符合筛选条件的业务配置</p>
<button
onClick={() => { setSearchQuery(""); setStatusFilter("all"); }}
className={cn("text-[11px] font-semibold mt-1 hover:underline", isDark ? "text-[#9CCBFF]" : "text-red-500")}
>
重置筛选条件
</button>
</div>
</td>
</tr>
)}
</tbody>
</table>
)}
</div>
{/* 表格尾栏 */}
<div className={cn("px-6 py-4 border-t flex items-center justify-between text-xs font-medium", rowDivider, isDark ? "bg-white/[0.02]" : "bg-black/[0.015]", mutedText)}>
<span className="flex items-center gap-1.5">
<Info className="w-3.5 h-3.5" />
会商程序会在对应业务的业务链生效时开启多体交叉核验证。
</span>
<span className="font-mono text-[10px]">
显示 {filteredRows.length} / {rows.length} 条
</span>
</div>
</motion.div>
{/* ── 使用说明 ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }}
className={cn(
"rounded-[5px] p-5 flex gap-4 text-xs leading-relaxed",
isDark
? "bg-[rgba(245,158,11,0.05)] text-amber-300/70"
: "bg-amber-50/50 text-amber-800",
)}
>
<div className={cn("w-8 h-8 rounded-[5px] shrink-0 flex items-center justify-center", isDark ? "bg-[rgba(245,158,11,0.08)] text-amber-400" : "bg-amber-100/60 text-amber-500")}>
<HelpCircle className="w-4 h-4" />
</div>
<div>
<h4 className={cn("font-bold mb-0.5", isDark ? "text-amber-300" : "text-amber-900")}>如何配置业务流转?</h4>
<p className={isDark ? "text-amber-400/60" : "text-amber-700/80"}>
1. 将鼠标悬停在任一业务行右侧,点击出现的{" "}
<span className={cn("font-semibold", isDark ? "text-[#9CCBFF]" : "text-red-500")}>「编辑」</span>。
<br />
2. 在该行内直接搜索并选择 <strong>执行智能体</strong> 与 <strong>业务链</strong>,点击「保存」即时生效。
<br />
3. 若无需绑定,可搜索框清空后保存,置为「未配置」状态。
</p>
</div>
</motion.div>
</div>
</div>
);
+10 -10
View File
@@ -119,7 +119,7 @@ export default function LightAppListPage() {
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-[#E8EDF5]">
应用列表
</h1>
<span className="rounded-full border border-[#CF1322]/15 bg-[#FFF1F0] px-2 py-0.5 font-mono text-xs font-medium text-[#CF1322] dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]">
<span className="rounded-[5px] border border-[#CF1322]/15 bg-[#FFF1F0] px-2 py-0.5 font-mono text-xs font-medium text-[#CF1322] dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]">
导航工作台
</span>
</div>
@@ -129,7 +129,7 @@ export default function LightAppListPage() {
</div>
{/* 搜索 */}
<div className="rounded-2xl border border-zinc-200/80 bg-white p-5 shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="rounded-[5px] bg-white p-5 shadow-sm dark:bg-[#122347]">
<div className="relative w-full">
<Search className="pointer-events-none absolute left-3.5 top-1/2 size-4.5 -translate-y-1/2 text-zinc-400 dark:text-[#6E88B0]" />
<input
@@ -137,7 +137,7 @@ export default function LightAppListPage() {
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="键入应用名称、目标地址、或描述,实时过滤检索…"
className="w-full rounded-xl border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25"
className="w-full rounded-[5px] border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25"
/>
{search && (
<button
@@ -171,7 +171,7 @@ export default function LightAppListPage() {
{/* 卡片网格 / 空态 */}
{apps.length === 0 ? (
<div className="rounded-2xl border border-dashed border-zinc-200 bg-white p-16 text-center shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="rounded-[5px] bg-white p-16 text-center shadow-sm dark:bg-[#122347]">
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full border border-zinc-100 bg-zinc-50 text-zinc-300 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#6E88B0]">
<Search className="size-6" />
</div>
@@ -205,7 +205,7 @@ export default function LightAppListPage() {
pageSize={PAGE_SIZE}
total={apps.length}
onPageChange={setPage}
className="pt-2"
className="pt-2 [&_button]:rounded-[5px]"
/>
)}
</>
@@ -240,11 +240,11 @@ function AppCard({
const hasParams = (app.routeParams ?? []).some((p) => p.key) && parsedUrl !== app.url;
return (
<div className="group relative flex flex-col justify-between overflow-hidden rounded-2xl border border-zinc-100 bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] transition-all duration-300 hover:-translate-y-1 hover:border-zinc-200/90 hover:shadow-[0_12px_24px_rgba(0,0,0,0.04)] dark:border-[#1A2F5A] dark:bg-[#122347] dark:shadow-none dark:hover:border-[#2D4473]">
<div className="group relative flex flex-col justify-between overflow-hidden rounded-[5px] bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_12px_24px_rgba(0,0,0,0.04)] dark:bg-[#122347] dark:shadow-none">
{/* 上:图标 + 标题 + 描述 */}
<div>
<div className="mb-3.5 flex items-start gap-4">
<div className={cn("rounded-xl border border-white/60 p-3 shadow-sm dark:border-transparent", theme.iconBg)}>
<div className={cn("rounded-[5px] p-3 shadow-sm", theme.iconBg)}>
<Icon className="size-5.5" />
</div>
</div>
@@ -260,7 +260,7 @@ function AppCard({
{/* 下:URL 预览 + 动作 */}
<div className="mt-auto border-t border-zinc-100 pt-4 dark:border-[#1A2F5A]">
<div className="mb-4 rounded-xl border border-zinc-100 bg-zinc-50/70 p-3 dark:border-[#1A2F5A] dark:bg-[#0C1836]">
<div className="mb-4 rounded-[5px] bg-zinc-50/70 p-3 dark:bg-[#0C1836]">
<div className="mb-1 flex items-center justify-between text-[10px] font-semibold text-zinc-400 dark:text-[#6E88B0]">
<span>拼接后目标路由终端:</span>
<span className="text-[9px] font-bold uppercase tracking-wider text-[#CF1322] dark:text-[#4096FF]">
@@ -283,7 +283,7 @@ function AppCard({
type="button"
onClick={onCopy}
className={cn(
"flex items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-semibold transition",
"flex items-center justify-center gap-1.5 rounded-[5px] border px-3 py-2 text-xs font-semibold transition",
copied
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-[#52C41A]/30 dark:bg-[#52C41A]/15 dark:text-[#52C41A]"
: "border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 hover:text-zinc-800 dark:border-[#1A2F5A] dark:bg-[#122347] dark:text-[#C5CFE0] dark:hover:bg-[#1A2F5A]",
@@ -305,7 +305,7 @@ function AppCard({
<button
type="button"
onClick={onOpen}
className="flex items-center justify-center gap-1.5 rounded-lg bg-[#CF1322] px-3 py-2 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(207,19,34,0.2)] transition hover:bg-[#A8071A] active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]"
className="flex items-center justify-center gap-1.5 rounded-[5px] bg-[#CF1322] px-3 py-2 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(207,19,34,0.2)] transition hover:bg-[#A8071A] active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]"
>
<span>进入应用</span>
<ArrowUpRight className="size-3.5" />
@@ -22,6 +22,7 @@ import {
X,
} from "lucide-react";
import { toast } from "sonner";
import { Select } from "tdesign-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -52,6 +53,18 @@ interface FormState {
routeParams: LightAppRouteParam[];
}
const APP_TYPE_OPTIONS: { label: string; value: LightAppType }[] = [
{ label: "iframe 嵌套 (挂载到菜单)", value: "iframe" },
{ label: "window.open (新页跳转)", value: "window_open" },
];
const PARAM_TYPE_OPTIONS: { label: string; value: "login" | "theme" }[] = [
{ label: "登录参数", value: "login" },
{ label: "主题参数", value: "theme" },
];
const DIALOG_SELECT_POPUP = { attach: () => document.body };
const emptyForm = (): FormState => ({
name: "",
description: "",
@@ -209,11 +222,11 @@ export default function LightAppManagementPage() {
</h1>
<p className="max-w-2xl text-xs leading-relaxed text-neutral-500">
注册轻应用。
<code className="mx-1 rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
<code className="mx-1 rounded-[5px] bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
window.open
</code>
类型在「应用列表」以卡片展示;
<code className="mx-1 rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
<code className="mx-1 rounded-[5px] bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
iframe
</code>
类型挂载到所选菜单下并注入侧栏。
@@ -222,14 +235,14 @@ export default function LightAppManagementPage() {
<button
onClick={openCreate}
className="flex cursor-pointer items-center justify-center space-x-1.5 rounded-lg bg-[#A8071A] px-5 py-2.5 text-xs font-semibold text-white shadow-md shadow-rose-950/10 transition-all hover:bg-[#BC0D1E] hover:shadow-lg hover:shadow-rose-900/15 active:bg-[#820014]"
className="flex cursor-pointer items-center justify-center space-x-1.5 rounded-[5px] bg-[#A8071A] px-5 py-2.5 text-xs font-semibold text-white shadow-md shadow-rose-950/10 transition-all hover:bg-[#BC0D1E] hover:shadow-lg hover:shadow-rose-900/15 active:bg-[#820014]"
>
<Plus className="h-4 w-4" />
<span>注册应用</span>
</button>
</div>
<div className="flex flex-col items-center gap-3.5 rounded-xl border border-neutral-100 bg-white p-3 shadow-[0_1px_3px_rgba(0,0,0,0.01)] md:flex-row">
<div className="flex flex-col items-center gap-3.5 rounded-[5px] bg-white p-3 shadow-[0_1px_3px_rgba(0,0,0,0.01)] md:flex-row">
<div className="relative w-full flex-1">
<Search className="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
<input
@@ -237,7 +250,7 @@ export default function LightAppManagementPage() {
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索应用名称 / 描述..."
className="w-full rounded-lg border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10"
className="w-full rounded-[5px] border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10"
/>
{search && (
<button
@@ -249,10 +262,10 @@ export default function LightAppManagementPage() {
)}
</div>
<div className="flex shrink-0 items-center space-x-0.5 self-end rounded-lg border border-neutral-200/50 bg-neutral-100/60 p-0.5 md:self-auto">
<div className="flex shrink-0 items-center space-x-0.5 self-end rounded-[5px] border border-neutral-200/50 bg-neutral-100/60 p-0.5 md:self-auto">
<button
onClick={() => setViewMode("table")}
className={`cursor-pointer rounded-md px-4 py-1.5 text-xs font-bold transition-all ${
className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
viewMode === "table"
? "bg-white text-[#F5222D] shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
@@ -262,7 +275,7 @@ export default function LightAppManagementPage() {
</button>
<button
onClick={() => setViewMode("cards")}
className={`cursor-pointer rounded-md px-4 py-1.5 text-xs font-bold transition-all ${
className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
viewMode === "cards"
? "bg-white text-[#F5222D] shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
@@ -280,7 +293,7 @@ export default function LightAppManagementPage() {
</div>
{viewMode === "table" ? (
<div className="overflow-visible rounded-xl border border-neutral-100 bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)]">
<div className="overflow-visible rounded-[5px] bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)]">
<table className="w-full table-fixed border-collapse text-left">
<thead>
<tr className="select-none border-b border-neutral-100/80 bg-neutral-50/40 text-[12.5px] font-semibold text-neutral-500">
@@ -304,12 +317,12 @@ export default function LightAppManagementPage() {
<td className="px-4 py-[18px]">
{app.appType === "window_open" ? (
<span className="inline-flex select-none items-center space-x-1.5 rounded-lg border border-rose-100 bg-rose-50 px-3 py-1 text-[12px] font-semibold text-rose-800">
<span className="inline-flex select-none items-center space-x-1.5 rounded-[5px] border border-rose-100 bg-rose-50 px-3 py-1 text-[12px] font-semibold text-rose-800">
<ExternalLink className="h-3.5 w-3.5" />
<span>新页跳转</span>
</span>
) : (
<span className="inline-flex select-none items-center space-x-1.5 rounded-lg border border-sky-100 bg-sky-50 px-3 py-1 text-[12px] font-semibold text-sky-800">
<span className="inline-flex select-none items-center space-x-1.5 rounded-[5px] border border-sky-100 bg-sky-50 px-3 py-1 text-[12px] font-semibold text-sky-800">
<Monitor className="h-3.5 w-3.5" />
<span>iframe 嵌套</span>
</span>
@@ -329,7 +342,7 @@ export default function LightAppManagementPage() {
</a>
<button
onClick={() => handleCopyUrl(app.url)}
className="shrink-0 rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
title="复制网址"
>
<Copy className="h-3 w-3" />
@@ -339,7 +352,7 @@ export default function LightAppManagementPage() {
<td className="px-4 py-[18px]">
{app.appType === "iframe" && app.mountLocation ? (
<span className="inline-flex max-w-full items-center space-x-1.5 rounded bg-sky-50 px-2 py-0.5 text-[11px] font-medium text-sky-700">
<span className="inline-flex max-w-full items-center space-x-1.5 rounded-[5px] bg-sky-50 px-2 py-0.5 text-[11px] font-medium text-sky-700">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
<span className="truncate">{mountLabel(app.mountLocation)}</span>
</span>
@@ -357,7 +370,7 @@ export default function LightAppManagementPage() {
activeParamPopover === app.appId ? null : app.appId
)
}
className="inline-flex cursor-pointer items-center space-x-1.5 rounded border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100"
className="inline-flex cursor-pointer items-center space-x-1.5 rounded-[5px] border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100"
>
<span>{app.routeParams.length}个</span>
{activeParamPopover === app.appId ? (
@@ -376,7 +389,7 @@ export default function LightAppManagementPage() {
className="fixed inset-0 z-40"
onClick={() => setActiveParamPopover(null)}
/>
<div className="absolute left-4 top-12 z-50 w-[280px] animate-in fade-in rounded-xl border border-neutral-100 bg-white p-3.5 text-[12px] shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="absolute left-4 top-12 z-50 w-[280px] animate-in fade-in rounded-[5px] border border-neutral-100 bg-white p-3.5 text-[12px] shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="mb-2.5 flex items-center justify-between border-b border-neutral-100 pb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-neutral-400">
注入的路由参数
@@ -392,7 +405,7 @@ export default function LightAppManagementPage() {
{app.routeParams.map((p, idx) => (
<div
key={`${app.appId}-${idx}`}
className="rounded-lg border border-neutral-100 bg-neutral-50/60 p-2 text-[11px] leading-relaxed"
className="rounded-[5px] border border-neutral-100 bg-neutral-50/60 p-2 text-[11px] leading-relaxed"
>
<div className="flex items-center justify-between gap-1 font-mono">
<span className="truncate font-bold text-rose-700">{p.key}</span>
@@ -432,14 +445,14 @@ export default function LightAppManagementPage() {
<div className="flex items-center justify-center space-x-1">
<button
onClick={() => openEdit(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
title="编辑配置"
>
<Pencil className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
title="删除配置"
>
<Trash2 className="h-4 w-4" />
@@ -474,7 +487,7 @@ export default function LightAppManagementPage() {
</table>
</div>
) : tableApps.length === 0 ? (
<div className="rounded-xl border border-dashed border-neutral-200 bg-white py-20 text-center">
<div className="rounded-[5px] bg-white py-20 text-center">
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-neutral-100 text-neutral-300">
<Info className="h-6 w-6" />
</div>
@@ -492,7 +505,7 @@ export default function LightAppManagementPage() {
{tableApps.map((app) => (
<div
key={app.appId}
className="group flex flex-col justify-between overflow-hidden rounded-xl border border-neutral-100 bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)] transition-all hover:border-[#F5222D]/20 hover:shadow-md"
className="group flex flex-col justify-between overflow-hidden rounded-[5px] bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)] transition-all hover:shadow-md"
>
<div
className={`h-1.5 w-full bg-gradient-to-r ${
@@ -505,12 +518,12 @@ export default function LightAppManagementPage() {
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1.5">
{app.appType === "window_open" ? (
<span className="inline-flex items-center space-x-1 rounded border border-rose-100 bg-rose-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-rose-700">
<span className="inline-flex items-center space-x-1 rounded-[5px] border border-rose-100 bg-rose-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-rose-700">
<ExternalLink className="h-3 w-3" />
<span>新页跳转</span>
</span>
) : (
<span className="inline-flex items-center space-x-1 rounded border border-sky-100 bg-sky-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-sky-700">
<span className="inline-flex items-center space-x-1 rounded-[5px] border border-sky-100 bg-sky-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-sky-700">
<Monitor className="h-3 w-3" />
<span>iframe 嵌套</span>
</span>
@@ -536,14 +549,14 @@ export default function LightAppManagementPage() {
{app.description || "暂无应用详细描述说明。"}
</p>
<div className="flex items-center space-x-1.5 rounded-lg border border-neutral-200/40 bg-neutral-50/60 p-2 font-mono text-[11px] text-neutral-600">
<div className="flex items-center space-x-1.5 rounded-[5px] bg-neutral-50/60 p-2 font-mono text-[11px] text-neutral-600">
<Globe className="h-3.5 w-3.5 shrink-0 text-neutral-400" />
<span className="flex-1 truncate" title={app.url}>
{app.url}
</span>
<button
onClick={() => handleCopyUrl(app.url)}
className="shrink-0 rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
title="复制网址"
>
<Copy className="h-3.5 w-3.5" />
@@ -555,7 +568,7 @@ export default function LightAppManagementPage() {
{app.routeParams.map((p, idx) => (
<span
key={`${app.appId}-card-${idx}`}
className="rounded border border-neutral-200/40 bg-neutral-100 px-1.5 py-0.5 font-mono text-[10px] text-neutral-500"
className="rounded-[5px] border border-neutral-200/40 bg-neutral-100 px-1.5 py-0.5 font-mono text-[10px] text-neutral-500"
>
{p.key}
</span>
@@ -564,7 +577,7 @@ export default function LightAppManagementPage() {
)}
{app.appType === "iframe" && app.mountLocation && (
<div className="inline-flex items-center gap-1.5 rounded-lg border border-sky-100/60 bg-sky-50/60 p-2 text-[11px] font-medium leading-none text-sky-700">
<div className="inline-flex items-center gap-1.5 rounded-[5px] bg-sky-50/60 p-2 text-[11px] font-medium leading-none text-sky-700">
<span>挂载分类:</span>
<code className="font-semibold">{mountLabel(app.mountLocation)}</code>
</div>
@@ -574,14 +587,14 @@ export default function LightAppManagementPage() {
<div className="flex items-center justify-end space-x-1 border-t border-neutral-100/60 bg-neutral-50/30 px-5 py-3">
<button
onClick={() => openEdit(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
title="编辑配置"
>
<Pencil className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
title="删除配置"
>
<Trash2 className="h-4 w-4" />
@@ -594,7 +607,7 @@ export default function LightAppManagementPage() {
</div>
{viewMode === "table" && (
<div className="select-none rounded-xl border border-amber-200/40 bg-amber-50/40 p-5 shadow-[0_1px_2px_rgba(0,0,0,0.005)]">
<div className="select-none rounded-[5px] bg-amber-50/40 p-5 shadow-[0_1px_2px_rgba(0,0,0,0.005)]">
<div className="flex items-center space-x-2 border-b border-amber-100/50 pb-3 text-amber-800">
<AlertCircle className="h-4 w-4 shrink-0 text-amber-600" />
<h4 className="text-[13px] font-bold tracking-tight">智能多维挂载规则:</h4>
@@ -627,10 +640,10 @@ export default function LightAppManagementPage() {
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="relative z-[110] m-4 flex max-h-[90vh] w-full max-w-[580px] flex-col rounded-[5px] 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">
<div className="flex items-center space-x-2.5">
<span className="rounded-lg bg-rose-50 p-1.5 text-[#A8071A]">
<span className="rounded-[5px] bg-rose-50 p-1.5 text-[#A8071A]">
<Layers className="h-5 w-5" />
</span>
<h2 className="text-[17px] font-bold text-[#222222]">
@@ -639,7 +652,7 @@ export default function LightAppManagementPage() {
</div>
<button
onClick={() => setDialogOpen(false)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
>
<X className="h-5 w-5" />
</button>
@@ -652,14 +665,13 @@ export default function LightAppManagementPage() {
<Layers className="h-4 w-4 text-[#A6A6A6]" />
<span>应用类型</span>
</label>
<select
<Select
className="w-full"
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>
options={APP_TYPE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchForm({ appType: v as LightAppType })}
/>
<p className="text-[11.5px] leading-relaxed text-[#A6A6A6]">
{form.appType === "window_open"
? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。"
@@ -677,7 +689,7 @@ export default function LightAppManagementPage() {
value={form.name}
onChange={(e) => patchForm({ name: e.target.value })}
placeholder="如: 舆情看板"
className="h-9 rounded-[2px] border-[#DBDBDB] text-[14px]"
className="h-9 rounded-[5px] border-[#DBDBDB] text-[14px]"
/>
</div>
@@ -691,7 +703,7 @@ export default function LightAppManagementPage() {
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]"
className="h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]"
/>
</div>
@@ -702,18 +714,14 @@ export default function LightAppManagementPage() {
<FolderTree className="h-4 w-4 text-[#A6A6A6]" />
<span>挂载菜单</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>
<Select
className="w-full"
value={form.mountLocation || undefined}
placeholder="请选择挂载菜单"
options={MOUNT_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchForm({ mountLocation: (v as string) ?? "" })}
/>
</div>
)}
@@ -728,7 +736,7 @@ export default function LightAppManagementPage() {
onChange={(e) => patchForm({ description: e.target.value })}
placeholder="简要说明该应用的用途"
rows={2}
className="resize-none rounded-[2px] border-[#DBDBDB] text-[14px]"
className="resize-none rounded-[5px] border-[#DBDBDB] text-[14px]"
/>
</div>
@@ -747,7 +755,7 @@ export default function LightAppManagementPage() {
<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]"
className="flex h-7 cursor-pointer items-center space-x-1 rounded-[5px] 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>
@@ -755,14 +763,14 @@ export default function LightAppManagementPage() {
</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">
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-[5px] border border-[#E8E8E8] bg-[#FAFAFA] p-1">
{form.routeParams.map((p, idx) => {
const themeMap =
p.paramType === "theme" ? parseThemeMap(p.themeValue) : {};
return (
<div
key={idx}
className="relative rounded-sm border border-[#E8E8E8] bg-white p-2"
className="relative rounded-[5px] border border-[#E8E8E8] bg-white p-2"
>
<div className="flex items-center space-x-2">
<div className="flex-1">
@@ -771,40 +779,36 @@ export default function LightAppManagementPage() {
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]"
className="h-8 w-full rounded-[5px] 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
<Select
className="w-full"
size="small"
value={p.paramType}
onChange={(e) =>
options={PARAM_TYPE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) =>
patchParam(idx, {
paramType: e.target.value as "login" | "theme",
source:
e.target.value === "login" ? (p.source || "accessToken") : undefined,
paramType: v as "login" | "theme",
source: v === "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>
{p.paramType === "login" && (
<div className="flex flex-[1.2] items-center gap-2 text-[11px]">
<select
<Select
className="w-full"
size="small"
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>
options={LOGIN_SOURCE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchParam(idx, { source: v as string })}
/>
{p.source === "other" && (
<input
type="text"
@@ -813,7 +817,7 @@ export default function LightAppManagementPage() {
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"
className="h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
/>
)}
</div>
@@ -822,7 +826,7 @@ export default function LightAppManagementPage() {
<button
type="button"
onClick={() => removeParam(idx)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
>
<X className="h-4 w-4" />
</button>
@@ -830,7 +834,7 @@ export default function LightAppManagementPage() {
{/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */}
{p.paramType === "theme" && (
<div className="mt-2 space-y-1.5 rounded-sm bg-[#FAFAFA] p-2">
<div className="mt-2 space-y-1.5 rounded-[5px] bg-[#FAFAFA] p-2">
<p className="text-[11px] leading-relaxed text-[#A6A6A6]">
各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)
</p>
@@ -858,7 +862,7 @@ export default function LightAppManagementPage() {
})
}
placeholder={c.key}
className="h-7 flex-1 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]"
className="h-7 flex-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
</div>
))}
@@ -869,7 +873,7 @@ export default function LightAppManagementPage() {
})}
</div>
) : (
<div className="rounded-sm border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
<div className="rounded-[5px] border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
暂无注入的路由参数,加载地址时不传递多余拼接。
</div>
)}
@@ -898,7 +902,7 @@ export default function LightAppManagementPage() {
variant="outline"
disabled={saving}
onClick={() => setDialogOpen(false)}
className="h-9 rounded-[2px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
className="h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
>
取消
</Button>
@@ -906,7 +910,7 @@ export default function LightAppManagementPage() {
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"
className="h-9 cursor-pointer rounded-[5px] 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>
+5 -1
View File
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useTheme } from '@/contexts/ThemeContext';
import { loginByToken, loginByUsername } from '@/core/auth/api';
import { setStoredAuth } from '@/core/auth';
import { setStoredAuth, setStoredUserName } from '@/core/auth';
import {
persistExternalLoginParams,
readExternalLoginParams,
@@ -114,6 +114,7 @@ export default function LoginPage() {
setErrorMessage('');
try {
let auth;
let loginUserName = '';
if (authToken) {
// token 换登录:后端用 authToken 调上游接口换出用户名再登录
auth = await loginByToken(authToken);
@@ -128,11 +129,14 @@ export default function LoginPage() {
}
auth = await loginByUsername(deerflowUsername, gatePassword);
loginUserName = deerflowUsername;
}
if (cancelled) {
return;
}
setStoredAuth(auth);
// 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖)
setStoredUserName(loginUserName);
persistSidebarEnabled(sidebarEnabled);
setStatus('success');
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
+9 -1
View File
@@ -7,6 +7,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { useTheme } from "@/contexts/ThemeContext";
import { useLocation } from "react-router-dom";
import { useLocalSettings } from "@/core/settings";
import WorkspaceRoutes from "./WorkspaceRoutes";
import StrategyRoutes from "./StrategyRoutes";
@@ -21,8 +22,15 @@ function PageLayout() {
const location = useLocation();
const { theme } = useTheme();
const isDark = theme === "dark";
const [settings] = useLocalSettings();
// 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到
// 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户
// 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。
const layoutMode = settings.display.layoutMode;
const chromeByPreference =
layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled;
const pageSidebarAllowed =
sidebarEnabled && !isChatsIframeWorkspacePath(location.pathname);
chromeByPreference && !isChatsIframeWorkspacePath(location.pathname);
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
// 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套
@@ -108,6 +108,7 @@ export function WorkspaceLayout({
'--header-height': headerHidden ? '0rem' : '4rem',
} as CSSProperties
}
key={String(compactWorkspaceSidebar)}
data-page-sidebar-collapsed={pageSidebarCollapsed}
// 紧凑模式下历史侧栏默认收起(聊天区更宽),由聊天页右上角图标钮展开
defaultOpen={!compactWorkspaceSidebar}