Merge remote-tracking branch 'origin/master' into ji
# Conflicts: # frontend-web/src/components/workspace/input-box.tsx # offline-backend-20260512/backend/CLAUDE.md
This commit is contained in:
commit
fa70b31873
159 files changed
+15521
-2415
No files matched your search
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -28,6 +28,12 @@ import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
||||
import { Welcome } from "@/components/workspace/welcome";
|
||||
import { CompactWelcome } from "@/components/workspace/compact-welcome";
|
||||
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
|
||||
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PanelLeftOpen } from "lucide-react";
|
||||
import { useAgentSelector } from "@/core/agents";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
@@ -39,6 +45,7 @@ import {
|
||||
useThreadSettings,
|
||||
} from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { env } from "@/env";
|
||||
@@ -85,7 +92,12 @@ export default function ChatPage() {
|
||||
const routeBase = useRouteBase();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const { open: historySidebarOpen, toggleSidebar: toggleHistorySidebar } =
|
||||
useSidebar();
|
||||
const { t } = useI18n();
|
||||
// 有效界面模式:用户自选 > 管理员系统默认 > (下方再回退会话级 sidebarEnabled)
|
||||
const [effectiveLayoutMode] = useLayoutMode();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
||||
@@ -402,11 +414,32 @@ export default function ChatPage() {
|
||||
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
|
||||
: undefined;
|
||||
const isLandingState = isNewThread && !hasStartedConversation;
|
||||
// 紧凑模式(正常 App,有外层页面侧栏)落地态:用 CM 参考图样式重画;
|
||||
// 判定与 PageRoutes 的 compactWorkspaceSidebar(pageSidebarAllowed) 保持一致 ——
|
||||
// 优先看外观设置的「全量/简洁」,未设置过才回退到会话级 sidebarEnabled。否则用户显式
|
||||
// 选「全量」时 PageRoutes 已挂出顶栏+侧栏,这里却还按旧 sidebarEnabled 判定,导致深蓝
|
||||
// 主题底图(workspace-chat-bg)不生效。非紧凑(嵌入/standalone)保持原样。
|
||||
const layoutMode = effectiveLayoutMode;
|
||||
const chromeByPreference =
|
||||
layoutMode === "full"
|
||||
? true
|
||||
: layoutMode === "compact"
|
||||
? false
|
||||
: readSidebarEnabled();
|
||||
const compactChrome =
|
||||
chromeByPreference && !isChatsIframeWorkspacePath(pathname);
|
||||
const compactLanding = isLandingState && compactChrome;
|
||||
|
||||
return (
|
||||
<ThreadContext.Provider value={{ thread, isMock }}>
|
||||
<ChatBox threadId={threadId}>
|
||||
<div className="relative flex size-full min-h-0 justify-between">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex size-full min-h-0 justify-between",
|
||||
// 紧凑模式(有顶栏+左侧页面侧栏)下,深色背景换成设计稿深蓝主题底图
|
||||
compactChrome && "workspace-chat-bg",
|
||||
)}
|
||||
>
|
||||
<header
|
||||
className={cn(
|
||||
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center border-b px-4",
|
||||
@@ -419,6 +452,20 @@ export default function ChatPage() {
|
||||
<ThreadTitle threadId={threadId} thread={thread} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 仅在收起态显示「展开历史」钮;展开后由侧栏内(新对话右侧)的收起钮负责收起。 */}
|
||||
{compactChrome && !historySidebarOpen && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={toggleHistorySidebar}
|
||||
title="打开历史记录"
|
||||
aria-label="打开历史记录"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<PanelLeftOpen className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<TokenUsageIndicator
|
||||
enabled={tokenUsageEnabled}
|
||||
messages={thread.messages}
|
||||
@@ -453,11 +500,25 @@ export default function ChatPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
||||
<div
|
||||
className={cn(
|
||||
// 外层居中容器不吃指针事件:空白区域(尤其落地态铺满 inset-0 时盖住的
|
||||
// 右上角「展开历史」按钮)让点击穿透,仅真正的内容块(下方 pointer-events-auto)
|
||||
// 接收交互。否则该透明层会吞掉顶栏按钮的 hover/光标/点击。
|
||||
"pointer-events-none absolute z-30 flex justify-center px-4",
|
||||
// 紧凑落地态:整块垂直+水平居中于对话区
|
||||
compactLanding
|
||||
? "inset-0 items-center"
|
||||
: "right-0 bottom-0 left-0",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full",
|
||||
isLandingState && "-translate-y-[calc(50vh-96px)]",
|
||||
"pointer-events-auto relative w-full",
|
||||
// 非紧凑落地态沿用上移居中;紧凑态由外层 items-center 居中,无需位移
|
||||
isLandingState &&
|
||||
!compactLanding &&
|
||||
"-translate-y-[calc(50vh-96px)]",
|
||||
isLandingState
|
||||
? "max-w-(--container-width-md)"
|
||||
: "max-w-(--container-width-lg)",
|
||||
@@ -474,17 +535,21 @@ export default function ChatPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLandingState && mounted && (
|
||||
<Welcome
|
||||
mode={settings.context.mode}
|
||||
className="mb-6 -translate-y-4"
|
||||
/>
|
||||
)}
|
||||
{isLandingState && mounted &&
|
||||
(compactLanding ? (
|
||||
<CompactWelcome className="mb-6 -translate-y-4" />
|
||||
) : (
|
||||
<Welcome
|
||||
mode={settings.context.mode}
|
||||
className="mb-6 -translate-y-4"
|
||||
/>
|
||||
))}
|
||||
{mounted ? (
|
||||
<div className="w-full -translate-y-4">
|
||||
<InputBox
|
||||
className="w-full"
|
||||
isNewThread={isLandingState}
|
||||
compact={compactLanding}
|
||||
threadId={threadId}
|
||||
autoFocus={isLandingState}
|
||||
status={
|
||||
|
||||
@@ -9,7 +9,7 @@ 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";
|
||||
import { buildLightAppUrl, type AppThemeColor } from "@/strategy-components/lib/light-app";
|
||||
|
||||
export default function LightAppIframeView() {
|
||||
const { appId } = useParams<{ appId: string }>();
|
||||
@@ -17,7 +17,15 @@ export default function LightAppIframeView() {
|
||||
const isDark = theme === "dark";
|
||||
const { data: app, isLoading, error } = useLightApp(appId);
|
||||
|
||||
const src = useMemo(() => (app ? buildLightAppUrl(app) : ""), [app]);
|
||||
// 由 useTheme 的 theme 直接换算主题色并显式传入,切主题时 src 立即重建、
|
||||
// 把新主题色传给被嵌套应用(不依赖 <html> 的 dark class,避免时序读旧值)。
|
||||
// 深色=dark-blue,浅蓝=blue,其余(红白)=red。
|
||||
const themeColor: AppThemeColor =
|
||||
theme === "dark" ? "dark-blue" : theme === "lightblue" ? "blue" : "red";
|
||||
const src = useMemo(
|
||||
() => (app ? buildLightAppUrl(app, undefined, themeColor) : ""),
|
||||
[app, themeColor],
|
||||
);
|
||||
|
||||
const centerClass = `flex h-full w-full items-center justify-center text-sm ${
|
||||
isDark ? "text-slate-400" : "text-slate-500"
|
||||
|
||||
@@ -1,27 +1,76 @@
|
||||
/**
|
||||
* 应用列表页 —— 以卡片网格展示「新页跳转(window.open)」类型的轻应用。
|
||||
*
|
||||
* 支持搜索 + 分页。点击卡片用 buildLightAppUrl 构造最终地址(追加解析后的路由参数)
|
||||
* 并 window.open 新开页跳转。仅展示已启用的应用;iframe 类型不在此处(它们注入侧边栏)。
|
||||
* 只读启动器:搜索 + 分页;每张卡片预览「拼接后最终路由」(buildLightAppUrl 追加解析后的
|
||||
* 登录态 / 主题路由参数),并提供「一键复制URL」「进入应用」(window.open 新开页) 两个动作。
|
||||
* 增删改不在本页,走独立的「应用管理」页(admin);iframe 类型也不在此(它们注入侧边栏)。
|
||||
*
|
||||
* 配色(与「业务链条配置」页统一):
|
||||
* - 浅色:品牌红 `#CF1322`(hover `#A8071A`)。
|
||||
* - 深蓝:按《深蓝色 UI 规范》——页面 `#0C1836`、卡片 `#122347`、边框 `#1A2F5A`、
|
||||
* 正文 `#C5CFE0`、次级 `#6E88B0`;主操作改用规范主色蓝 `#1677FF`(hover `#0958D9`)。
|
||||
*
|
||||
* 后端 LightApp 无「分类」字段,故不做分类筛选;卡片图标 / 主题色按 appId 哈希派生
|
||||
* (`icon` 字段当前未用),仅作视觉区分,不承载语义。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ExternalLink, Search } from "lucide-react";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bot,
|
||||
Check,
|
||||
Code,
|
||||
Copy,
|
||||
FileText,
|
||||
Globe,
|
||||
Layers,
|
||||
Notebook,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Terminal,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Pagination } from "@/components/ui/pagination";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
|
||||
import { buildLightAppUrl } from "@/strategy-components/lib/light-app";
|
||||
|
||||
import "./light-app-pages.css";
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
|
||||
export default function LightAppListPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
/** 卡片图标池(按 appId 哈希取一个,仅作视觉区分)。 */
|
||||
const ICON_POOL: LucideIcon[] = [Bot, Notebook, Terminal, SlidersHorizontal, Globe, Code, Layers, FileText];
|
||||
|
||||
/** 卡片主题色池:浅色用对应色阶,深蓝统一落到 `#1A2F5A` 容器底 + 规范强调色。 */
|
||||
const THEME_POOL = [
|
||||
{ iconBg: "bg-rose-100 text-rose-600 dark:bg-[#1A2F5A] dark:text-[#FF7875]" },
|
||||
{ iconBg: "bg-indigo-100 text-indigo-600 dark:bg-[#1A2F5A] dark:text-[#6388FF]" },
|
||||
{ iconBg: "bg-emerald-100 text-emerald-600 dark:bg-[#1A2F5A] dark:text-[#52C41A]" },
|
||||
{ iconBg: "bg-amber-100 text-amber-600 dark:bg-[#1A2F5A] dark:text-[#FAAD14]" },
|
||||
{ iconBg: "bg-blue-100 text-blue-600 dark:bg-[#1A2F5A] dark:text-[#4096FF]" },
|
||||
{ iconBg: "bg-purple-100 text-purple-600 dark:bg-[#1A2F5A] dark:text-[#9C7BFF]" },
|
||||
] as const;
|
||||
|
||||
/** 稳定字符串哈希 → [0, mod) 下标。 */
|
||||
function hashIndex(str: string, mod: number): number {
|
||||
let h = 0;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
h = (h * 31 + str.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return mod > 0 ? h % mod : 0;
|
||||
}
|
||||
|
||||
export default function LightAppListPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const { theme } = useTheme();
|
||||
const isLightBlue = theme === 'lightblue';
|
||||
|
||||
const { data: apps = [], isLoading } = useLightApps({
|
||||
appType: "window_open",
|
||||
enabledOnly: true,
|
||||
@@ -47,67 +96,228 @@ export default function LightAppListPage() {
|
||||
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]";
|
||||
const copyUrl = async (app: LightApp) => {
|
||||
const url = buildLightAppUrl(app);
|
||||
if (!url) {
|
||||
toast.error("该应用地址为空,无法复制");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
setCopiedId(app.appId);
|
||||
setTimeout(() => setCopiedId((id) => (id === app.appId ? null : id)), 2000);
|
||||
} catch {
|
||||
toast.error("复制失败,请手动选择地址");
|
||||
}
|
||||
};
|
||||
|
||||
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="light-app-list-page h-full overflow-y-auto bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-6 px-4 py-7 sm:px-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"}`}>
|
||||
点击卡片在新标签页打开应用。地址会自动附带配置的路由参数(登录态 / 主题)。
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="relative flex size-3">
|
||||
<span className={cn("absolute inline-flex size-full animate-ping rounded-full opacity-75", isLightBlue ? "bg-[#0062D9]" : "bg-[#CF1322]", "dark:bg-[#1677FF]")} />
|
||||
<span className={cn("relative inline-flex size-3 rounded-full", isLightBlue ? "bg-[#0062D9]" : "bg-[#CF1322]", "dark:bg-[#1677FF]")} />
|
||||
</span>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-[#E8EDF5]">
|
||||
应用列表
|
||||
</h1>
|
||||
<span className={cn("rounded-[5px] border px-2 py-0.5 font-mono text-xs font-medium dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]", isLightBlue ? "border-[#0062D9]/15 bg-[#EAF4FF] text-[#0062D9]" : "border-[#CF1322]/15 bg-[#FFF1F0] text-[#CF1322]")}>
|
||||
导航工作台
|
||||
</span>
|
||||
</div>
|
||||
<p className="max-w-2xl text-sm leading-relaxed text-zinc-500 dark:text-[#6E88B0]">
|
||||
点击卡片将在新标签页中一键直达应用。地址会自动、智能地拼接路由参数(跨域登录态、个性化主题等)。
|
||||
</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 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
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="键入应用名称、目标地址、或描述,实时过滤检索…"
|
||||
className={cn("w-full rounded-[5px] border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:bg-white focus:outline-none focus:ring-2 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", isLightBlue ? "focus:border-[#0062D9]/60 focus:ring-[#0062D9]/20" : "focus:border-[#CF1322]/60 focus:ring-[#CF1322]/20")}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
title="清除搜索"
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-400 transition hover:text-zinc-600 dark:hover:text-[#C5CFE0]"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 结果计数 */}
|
||||
<div className="-mt-2 flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-[#6E88B0]">
|
||||
检索到最终结果:{" "}
|
||||
<span className={cn("font-bold dark:text-[#4096FF]", isLightBlue ? "text-[#0062D9]" : "text-[#CF1322]")}>{apps.length}</span> 个关联应用
|
||||
</div>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
className={cn("text-xs font-medium underline dark:text-[#4096FF] dark:hover:text-[#1677FF]", isLightBlue ? "text-[#0062D9] hover:text-[#0056C6]" : "text-[#CF1322] hover:text-[#A8071A]")}
|
||||
>
|
||||
重置过滤条件
|
||||
</button>
|
||||
)}
|
||||
</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 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>
|
||||
<h4 className="mb-1 text-base font-medium text-zinc-800 dark:text-[#C5CFE0]">
|
||||
{isLoading ? "正在加载应用…" : search ? "未找到匹配的应用" : "暂无可用应用"}
|
||||
</h4>
|
||||
<p className="mx-auto max-w-md text-sm text-zinc-400 dark:text-[#6E88B0]">
|
||||
{isLoading
|
||||
? "请稍候。"
|
||||
: search
|
||||
? "尝试更换关键词,或清除搜索查看全部应用。"
|
||||
: "应用由管理员在「应用管理」中登记后会出现在这里。"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{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>
|
||||
<AppCard
|
||||
key={app.appId}
|
||||
app={app}
|
||||
copied={copiedId === app.appId}
|
||||
onCopy={() => void copyUrl(app)}
|
||||
onOpen={() => openApp(app)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Pagination page={page} pageSize={PAGE_SIZE} total={apps.length} onPageChange={setPage} className="pt-2" />
|
||||
{apps.length > PAGE_SIZE && (
|
||||
<Pagination
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={apps.length}
|
||||
onPageChange={setPage}
|
||||
className="pt-2 [&_button]:rounded-[5px]"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer className="mt-auto pt-8 text-center text-xs text-zinc-400 dark:text-[#4A6491]">
|
||||
应用导航工作台 • 优雅可靠的任务中转入口
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 单张应用卡 ───────────────────────────────────────────────────────────────
|
||||
|
||||
function AppCard({
|
||||
app,
|
||||
copied,
|
||||
onCopy,
|
||||
onOpen,
|
||||
}: {
|
||||
app: LightApp;
|
||||
copied: boolean;
|
||||
onCopy: () => void;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const isLightBlue = theme === 'lightblue';
|
||||
const Icon = ICON_POOL[hashIndex(app.appId, ICON_POOL.length)];
|
||||
const cardTheme = THEME_POOL[hashIndex(app.appId, THEME_POOL.length)];
|
||||
|
||||
const parsedUrl = buildLightAppUrl(app);
|
||||
const hasParams = (app.routeParams ?? []).some((p) => p.key) && parsedUrl !== app.url;
|
||||
|
||||
return (
|
||||
<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-[5px] p-3 shadow-sm", cardTheme.iconBg)}>
|
||||
<Icon className="size-5.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className={cn("mb-2 line-clamp-1 text-base font-bold leading-snug text-zinc-900 transition-colors dark:text-[#E8EDF5] dark:group-hover:text-[#4096FF]", isLightBlue ? "group-hover:text-[#0062D9]" : "group-hover:text-[#CF1322]")}>
|
||||
{app.name}
|
||||
</h4>
|
||||
|
||||
<p className="mb-4 line-clamp-2 min-h-[2.5rem] text-xs leading-relaxed text-zinc-500 dark:text-[#6E88B0]">
|
||||
{app.description || "暂无应用详细描述说明,点击进入下方链接查看详情。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 下:URL 预览 + 动作 */}
|
||||
<div className="mt-auto border-t border-zinc-100 pt-4 dark:border-[#1A2F5A]">
|
||||
<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={cn("text-[9px] font-bold uppercase tracking-wider dark:text-[#4096FF]", isLightBlue ? "text-[#0062D9]" : "text-[#CF1322]")}>
|
||||
自动拼接已激活参
|
||||
</span>
|
||||
</div>
|
||||
<div className="line-clamp-1 select-all break-all font-mono text-[11px] font-medium text-zinc-600 dark:text-[#C5CFE0]">
|
||||
{parsedUrl || "(地址为空)"}
|
||||
</div>
|
||||
{hasParams && (
|
||||
<div className="mt-1 flex items-center gap-1 text-[9px] text-zinc-400 dark:text-[#6E88B0]">
|
||||
<span className="inline-block size-1 rounded-full bg-amber-400 dark:bg-[#FAAD14]" />
|
||||
<span>包含动态拼接参数</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
className={cn(
|
||||
"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]",
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="size-3.5" />
|
||||
<span>已复制!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="size-3.5" />
|
||||
<span>一键复制URL</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className={cn("flex items-center justify-center gap-1.5 rounded-[5px] px-3 py-2 text-xs font-semibold text-white transition active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]", isLightBlue ? "bg-[#0062D9] shadow-[0_1px_3px_rgba(0,98,217,0.2)] hover:bg-[#0079FE]" : "bg-[#CF1322] shadow-[0_1px_3px_rgba(207,19,34,0.2)] hover:bg-[#A8071A]")}
|
||||
>
|
||||
<span>进入应用</span>
|
||||
<ArrowUpRight className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
|
||||
export default function LightAppNoteQAPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const isLightBlue = theme === 'lightblue';
|
||||
const [question, setQuestion] = useState("");
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -111,7 +112,9 @@ export default function LightAppNoteQAPage() {
|
||||
"flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition-colors",
|
||||
isDark
|
||||
? "bg-sky-500 text-white hover:bg-sky-400"
|
||||
: "bg-[#F5222D] text-white hover:bg-[#CF1322]",
|
||||
: isLightBlue
|
||||
? "bg-[#0062D9] text-white hover:bg-[#0079FE]"
|
||||
: "bg-[#F5222D] text-white hover:bg-[#CF1322]",
|
||||
)}
|
||||
aria-label="发送"
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
@@ -16,6 +16,7 @@ import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
|
||||
import {
|
||||
GENERAL_QA_ROUTE,
|
||||
loginByYUserId,
|
||||
persistAccessToken,
|
||||
persistToolsToken,
|
||||
} from '@/core/auth/yuser-login';
|
||||
import {
|
||||
@@ -53,6 +54,8 @@ export default function LoginPage() {
|
||||
// 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage
|
||||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||||
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
||||
const accessToken = searchParams.get('access_token')?.trim() ?? '';
|
||||
// 用户名直登口令门:开启后端开关时,口令从 ?password= 透传
|
||||
const gatePassword = searchParams.get('password') ?? '';
|
||||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||||
@@ -114,6 +117,7 @@ export default function LoginPage() {
|
||||
setErrorMessage('');
|
||||
try {
|
||||
let auth;
|
||||
let loginUserName = '';
|
||||
if (authToken) {
|
||||
// token 换登录:后端用 authToken 调上游接口换出用户名再登录
|
||||
auth = await loginByToken(authToken);
|
||||
@@ -128,11 +132,17 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
auth = await loginByUsername(deerflowUsername, gatePassword);
|
||||
loginUserName = deerflowUsername;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setStoredAuth(auth);
|
||||
// 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖)
|
||||
setStoredUserName(loginUserName);
|
||||
// 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后),
|
||||
// XK/PG 中任务 iframe 据此取 accessToken
|
||||
persistAccessToken(accessToken);
|
||||
persistSidebarEnabled(sidebarEnabled);
|
||||
setStatus('success');
|
||||
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
|
||||
@@ -157,6 +167,7 @@ export default function LoginPage() {
|
||||
sidebarEnabled,
|
||||
yUserId,
|
||||
toolToken,
|
||||
accessToken,
|
||||
onlineTicket,
|
||||
authToken,
|
||||
gatePassword,
|
||||
|
||||
@@ -5,8 +5,11 @@ import { PageSidebar } from "@/components/page-sidebar";
|
||||
import { PageSidebarDark } from "@/components/page-sidebar-dark";
|
||||
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
|
||||
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
|
||||
import { isWujieEnv } from "@/core/embed/wujie";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
|
||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||
|
||||
import WorkspaceRoutes from "./WorkspaceRoutes";
|
||||
import StrategyRoutes from "./StrategyRoutes";
|
||||
@@ -21,8 +24,20 @@ function PageLayout() {
|
||||
const location = useLocation();
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
// 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。
|
||||
const inWujie = useMemo(() => isWujieEnv(), []);
|
||||
const lightNarrowSidebar = !isDark && !inWujie;
|
||||
// 浅色窄栏在右侧展开「三级列」时整体变宽(96→236px),主内容左内边距随之过渡。
|
||||
const thirdLevel = useActiveThirdLevel();
|
||||
const lightExpanded = lightNarrowSidebar && thirdLevel.length > 0;
|
||||
// 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认
|
||||
// (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId
|
||||
// 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。
|
||||
const [layoutMode] = useLayoutMode();
|
||||
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 等价;无界等嵌套
|
||||
@@ -53,8 +68,11 @@ function PageLayout() {
|
||||
// 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。
|
||||
// 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐
|
||||
? (collapsed ? "pl-0" : "pl-[118px]")
|
||||
// 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展)
|
||||
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
|
||||
: lightNarrowSidebar
|
||||
// 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡
|
||||
? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}`
|
||||
// 浅色旧版手风琴(wujie 嵌入):pl-16 ↔ pl-64 平滑收展
|
||||
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
Loader2,
|
||||
ScrollText,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useParams } from "react-router-dom";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -21,6 +23,12 @@ import {
|
||||
} from "@/core/roundtable-draft-shares/api";
|
||||
import { usePublicBackground } from "@/core/embed/background";
|
||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
|
||||
import { parseReportJson } from "@/roundtable-planning/lib/report-template";
|
||||
|
||||
/** 该分享页支持的三种视图:落地汇总 / 结果绘制(经典 HTML) / 大屏多页(模式二)。 */
|
||||
export type RoundtableShareView = "landing" | "report" | "dashboard";
|
||||
|
||||
/** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */
|
||||
function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) {
|
||||
@@ -81,20 +89,22 @@ function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) {
|
||||
* Public, read-only roundtable-result viewer.
|
||||
*
|
||||
* Reachable at `#/roundtable-share/<code>` with NO authentication — anyone with
|
||||
* the link can open it. Two views, driven by the `report` prop / route:
|
||||
* the link can open it. Three views, driven by the `view` prop / route:
|
||||
*
|
||||
* - landing (`#/roundtable-share/<code>`): a read-only result page mirroring the
|
||||
* in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」
|
||||
* button that opens the full report in a NEW TAB (when one was generated).
|
||||
* - report (`#/roundtable-share/<code>/report`): the Step 3 report (a
|
||||
* in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终
|
||||
* 结论 + 各席位最终交付.
|
||||
* - report (`#/roundtable-share/<code>/report`): the Step 3 经典 HTML 看板 (a
|
||||
* self-contained HTML document) rendered full-screen inside a sandboxed
|
||||
* `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute
|
||||
* code in the app origin.
|
||||
* `iframe` (no `allow-scripts`).
|
||||
* - dashboard (`#/roundtable-share/<code>/dashboard`): the Step 3 大屏多页 (模式二)
|
||||
* — rendered on the fly from the stored structured data via `renderDashboardHtml`
|
||||
* into the same sandboxed `iframe`.
|
||||
*/
|
||||
export default function PublicRoundtableSharePage({
|
||||
report = false,
|
||||
view = "landing",
|
||||
}: {
|
||||
report?: boolean;
|
||||
view?: RoundtableShareView;
|
||||
}) {
|
||||
const { shareCode } = useParams<{ shareCode: string }>();
|
||||
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||
@@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
// 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。
|
||||
// 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与
|
||||
// Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。
|
||||
const dashboardData = useMemo(() => {
|
||||
const json = data?.dashboard_json;
|
||||
if (!json) return undefined;
|
||||
const parsed = parseReportJson(json);
|
||||
if (!parsed) return undefined;
|
||||
return adaptReportData(parsed, data?.generated_at ?? "");
|
||||
}, [data?.dashboard_json, data?.generated_at]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm" style={pageBg}>
|
||||
@@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({
|
||||
);
|
||||
}
|
||||
|
||||
// ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
|
||||
if (report) {
|
||||
// ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ──
|
||||
if (view === "dashboard") {
|
||||
return (
|
||||
<div className="bg-background h-screen w-screen overflow-auto" style={pageBg}>
|
||||
{dashboardData ? (
|
||||
<DashboardApp data={dashboardData} />
|
||||
) : (
|
||||
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm">
|
||||
<p className="text-base font-medium text-foreground">暂无大屏多页</p>
|
||||
<p>这次会商还没有生成第三步「大屏多页」的方案报告。</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ─────────────────
|
||||
if (view === "report") {
|
||||
return (
|
||||
<div className="bg-background flex h-screen flex-col" style={pageBg}>
|
||||
<header className="flex shrink-0 items-center gap-2 border-b px-6 py-3">
|
||||
@@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({
|
||||
);
|
||||
}
|
||||
|
||||
// ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)─────
|
||||
const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
|
||||
// ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ──
|
||||
const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`;
|
||||
const reportUrl = `${shareBase}/report`;
|
||||
const dashboardUrl = `${shareBase}/dashboard`;
|
||||
|
||||
return (
|
||||
<div className="bg-[#fef6f6] flex h-screen flex-col text-slate-800" style={pageBg}>
|
||||
@@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-6xl space-y-6 p-6">
|
||||
{/* 1. 结果绘制 —— 新标签打开报告页 */}
|
||||
{/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-primary" />
|
||||
结果绘制
|
||||
</h4>
|
||||
{data.has_report ? (
|
||||
<a
|
||||
href={reportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-primary text-white hover:bg-[var(--brand-900)] inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm shadow-primary/10"
|
||||
>
|
||||
<BarChart3 className="size-4" />
|
||||
查看结果绘制
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
{data.has_report || data.has_dashboard ? (
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{data.has_report && (
|
||||
<a
|
||||
href={reportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-primary text-white hover:bg-[var(--brand-900)] inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm shadow-primary/10"
|
||||
>
|
||||
<BarChart3 className="size-4" />
|
||||
查看结果绘制
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
)}
|
||||
{data.has_dashboard && (
|
||||
<a
|
||||
href={dashboardUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-white text-primary border border-primary/30 hover:bg-primary/5 inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm"
|
||||
>
|
||||
<LayoutDashboard className="size-4" />
|
||||
查看大屏多页
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-slate-400 italic">
|
||||
这次会商还没有生成第三步「结果绘制」的方案报告。
|
||||
@@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 2. 总控最终结论 */}
|
||||
{/* 2. 方案总结报告(Markdown) */}
|
||||
{data.has_summary_report && (
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<ScrollText className="w-4 h-4 text-primary" />
|
||||
方案总结报告
|
||||
</h4>
|
||||
<div className="text-xs text-slate-700 leading-relaxed">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{data.summary_report_md}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 3. 总控最终结论 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||
@@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 3. 各席位最终交付 */}
|
||||
{/* 4. 各席位最终交付 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-primary" />
|
||||
|
||||
@@ -80,11 +80,11 @@ export default function StrategyRoutes() {
|
||||
<Route index element={<Navigate to="xk-task-jc" replace />} />
|
||||
<Route
|
||||
path="xk-task-jc"
|
||||
element={<TaskXkFrom gopath="#/pageIndie/pageHome/pageSwrw" />}
|
||||
element={<TaskXkFrom gopath="/#/pageIndie/pageHome/pageSwrw" />}
|
||||
/>
|
||||
<Route
|
||||
path="xk-task-dispatch"
|
||||
element={<TaskXkFrom gopath="#/pageIndie/pageJczl" />}
|
||||
element={<TaskXkFrom gopath="/#/pageIndie/pageJczl" />}
|
||||
/>
|
||||
<Route
|
||||
path="effect-task-pg"
|
||||
|
||||
@@ -110,8 +110,10 @@ export function WorkspaceLayout({
|
||||
'--header-height': headerHidden ? '0rem' : '4rem',
|
||||
} as CSSProperties
|
||||
}
|
||||
key={String(compactWorkspaceSidebar)}
|
||||
data-page-sidebar-collapsed={pageSidebarCollapsed}
|
||||
defaultOpen
|
||||
// 紧凑模式下历史侧栏默认收起(聊天区更宽),由聊天页右上角图标钮展开
|
||||
defaultOpen={!compactWorkspaceSidebar}
|
||||
>
|
||||
{!workspaceSidebarHidden && (
|
||||
<WorkspaceSidebar compactChrome={compactWorkspaceSidebar} />
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
@import "../../docs/ui规范-深蓝/tokens.css";
|
||||
@import "./light-app-pages.css";
|
||||
|
||||
/**
|
||||
* 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》)
|
||||
* 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。
|
||||
*/
|
||||
.dark .light-app-management-page {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-desc {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-desc code {
|
||||
background: rgba(22, 119, 255, 0.12);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn {
|
||||
background: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn:active {
|
||||
background: var(--color-primary-active);
|
||||
}
|
||||
|
||||
/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-toolbar {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input:hover {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input:focus {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn.is-active {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-link);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-count {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-count strong {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* ── 表格(§28)──────────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-table-wrap {
|
||||
background: var(--color-bg-page);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table thead tr {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-row-name {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tag-window {
|
||||
border-color: rgba(245, 34, 45, 0.35);
|
||||
background: var(--func-error-bg);
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tag-iframe {
|
||||
border-color: rgba(22, 119, 255, 0.35);
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover .lam-url {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url a:hover {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-mount-tag {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-mount-dot {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-btn {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-btn:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover-head {
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover-close {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-item {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-key {
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-val {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on {
|
||||
color: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on-dot {
|
||||
background: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on-ping {
|
||||
background: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-off {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-off-dot {
|
||||
background: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn-danger:hover {
|
||||
background: var(--func-error-bg);
|
||||
color: var(--func-error);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-icon {
|
||||
background: var(--color-bg-page);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-title {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-sub {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card:hover {
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card:hover .lam-card-title {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-desc {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box {
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-param {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-mount {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-footer {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(12, 24, 54, 0.5);
|
||||
}
|
||||
|
||||
/* ── 底部提示(§33 警告)────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-tip-box {
|
||||
background: var(--func-warning-bg);
|
||||
border: 1px solid rgba(250, 173, 20, 0.25);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-head {
|
||||
border-color: rgba(250, 173, 20, 0.2);
|
||||
color: var(--func-warning);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-list {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-list strong {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-bullet {
|
||||
color: var(--func-warning);
|
||||
}
|
||||
|
||||
/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-dialog-overlay {
|
||||
background: rgba(6, 14, 36, 0.6);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog {
|
||||
border-color: var(--color-border);
|
||||
background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page));
|
||||
box-shadow: var(--shadow-3);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-header {
|
||||
border-color: var(--color-border);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-header::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
background: var(--gradient-title-line);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-icon {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-close {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-close:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label .lam-required {
|
||||
color: var(--func-error);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label svg {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-hint {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"] {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input::placeholder,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input:focus,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"]:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-panel {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-row {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-theme-box {
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-empty {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-add-param-btn {
|
||||
border-color: var(--color-border-strong);
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-add-param-btn:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-footer {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-cancel {
|
||||
border-color: var(--color-border-strong);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-cancel:hover {
|
||||
border-color: var(--color-link);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save {
|
||||
background: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save:active {
|
||||
background: var(--color-primary-active);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-switch-on {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-switch-off {
|
||||
background: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
/* ── TDesign Select(§15)──────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .t-select .t-input,
|
||||
.dark .light-app-management-page .t-select .t-input__wrap {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
border-radius: var(--radius-input);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-input__inner {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-input__inner::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select:hover .t-input,
|
||||
.dark .light-app-management-page .t-select:hover .t-input__wrap {
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select.t-is-focused .t-input,
|
||||
.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-fake-arrow {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select__dropdown {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option.t-is-selected {
|
||||
background: rgba(22, 119, 255, 0.16);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* shadcn Input / Textarea in dialog */
|
||||
.dark .light-app-management-page .lam-dialog [data-slot="input"],
|
||||
.dark .light-app-management-page .lam-dialog textarea {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible,
|
||||
.dark .light-app-management-page .lam-dialog textarea:focus-visible {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-theme-box span {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* TDesign 下拉挂载到 body 时的深蓝样式 */
|
||||
.dark .lam-select-popup.t-popup {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option.t-is-selected {
|
||||
background: rgba(22, 119, 255, 0.16);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input + button,
|
||||
.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table .text-neutral-300 {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box button {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box button:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
浅蓝主题覆盖([data-theme="lightblue"])
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── 注册应用按钮 ─────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-primary-btn {
|
||||
background-color: #0062D9;
|
||||
box-shadow: 0 2px 6px rgba(0, 98, 217, 0.2);
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-primary-btn:hover {
|
||||
background-color: #0079FE;
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-primary-btn:active {
|
||||
background-color: #0056C6;
|
||||
}
|
||||
|
||||
/* ── 搜索框 focus ────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-search-input:focus {
|
||||
border-color: rgba(0, 98, 217, 0.4);
|
||||
box-shadow: 0 0 0 4px rgba(0, 98, 217, 0.1);
|
||||
}
|
||||
|
||||
/* ── 视图切换激活态 ──────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-view-toggle-btn.is-active {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 结果计数 ───────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-count strong {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 表格行名 hover ──────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-table tr:hover .lam-row-name {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 参数弹窗 ───────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-param-popover-close {
|
||||
color: #0062D9;
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-param-key {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 卡片标题 hover ──────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-card:hover .lam-card-title {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 弹窗图标 / 关闭按钮 ─────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-icon {
|
||||
background-color: #EAF4FF;
|
||||
color: #0062D9;
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-close:hover {
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 添加路由参数按钮 hover ───────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-add-param-btn:hover {
|
||||
border-color: #0062D9;
|
||||
color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 表单输入框 focus ────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-input:focus,
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog input[type="text"]:focus {
|
||||
border-color: #0062D9;
|
||||
box-shadow: 0 0 0 1px #0062D9;
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible,
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog textarea:focus-visible {
|
||||
border-color: #0062D9;
|
||||
box-shadow: 0 0 0 2px rgba(0, 98, 217, 0.16);
|
||||
}
|
||||
|
||||
/* ── TDesign Select focus / hover ────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .t-select:hover .t-input,
|
||||
[data-theme="lightblue"] .light-app-management-page .t-select:hover .t-input__wrap {
|
||||
border-color: rgba(0, 98, 217, 0.5);
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .t-select.t-is-focused .t-input,
|
||||
[data-theme="lightblue"] .light-app-management-page .t-select.t-is-focused .t-input__wrap {
|
||||
border-color: #0062D9;
|
||||
box-shadow: 0 0 0 2px rgba(0, 98, 217, 0.16);
|
||||
}
|
||||
|
||||
/* ── 启用开关 ───────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-switch-on {
|
||||
background-color: #0062D9;
|
||||
}
|
||||
|
||||
/* ── 保存按钮 ───────────────────────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-save {
|
||||
background-color: #0062D9;
|
||||
box-shadow: 0 2px 8px rgba(0, 98, 217, 0.2);
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-save:hover {
|
||||
background-color: #0079FE;
|
||||
}
|
||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-save:active {
|
||||
background-color: #0056C6;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@import "../../docs/ui规范-深蓝/tokens.css";
|
||||
|
||||
/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */
|
||||
/* ── 浅蓝主题:页面底色轻微蓝染 ──────────────────────────────────────── */
|
||||
[data-theme="lightblue"] .light-app-management-page,
|
||||
[data-theme="lightblue"] .light-app-list-page {
|
||||
background-color: #F9FCFF;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page,
|
||||
.dark .light-app-list-page {
|
||||
background-color: var(--color-bg-page);
|
||||
background-image: url("../assets/img/layout-bg-theme-dark-blue.svg");
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
Reference in new issue
Block a user