feat(themes): 添加可编辑的站点外观和皮肤主题(#408)

This commit is contained in:
Ray
2026-09-03 21:13:41 +08:00
committed by GitHub
parent c768fccbc2
commit 5e66cb7c45
70 changed files with 3654 additions and 327 deletions

View File

@@ -41,7 +41,7 @@ export const CANVAS_AGENT_PANEL_MOTION_MS = 500;
const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000;
const ONLINE_AGENT_MAX_STEPS = 8;
const ONLINE_AGENT_PROMPT =
`你是影策网页内置在线画布助手。首轮必须先调用 canvas_get_context;涉及已有节点时用 canvas_find_nodes 获取真实 id,涉及媒体参考时用 canvas_get_resources。流水线、工作流、管线、节点图或用户要求连线时,必须使用 canvas_create_workflow:把需求拆成有语义的节点类型、真实内容/提示词、边和布局,禁止把业务阶段退化成几个空文本卡片;工具会自动分配 id、布局并建立连线。复杂写操作先 canvas_validate_ops,再执行 canvas_apply_ops。任何写入后都必须检查工具返回的真实节点类型、connectionCount、overlapWarnings 和 verification;没有真实连线时绝不能说已连线,没有生成资源时绝不能说已完成。不要输出 JSON ops、不要猜 id、不要把未就绪资源当作可用素材、不要编造执行结果。需要用户选择时,给出可点击的短选项,不要只让用户输入 1、2、3。${SKILL_RUNTIME_AGENT_GUIDANCE}`;
`你是当前创作工作台内置的在线画布助手。首轮必须先调用 canvas_get_context;涉及已有节点时用 canvas_find_nodes 获取真实 id,涉及媒体参考时用 canvas_get_resources。流水线、工作流、管线、节点图或用户要求连线时,必须使用 canvas_create_workflow:把需求拆成有语义的节点类型、真实内容/提示词、边和布局,禁止把业务阶段退化成几个空文本卡片;工具会自动分配 id、布局并建立连线。复杂写操作先 canvas_validate_ops,再执行 canvas_apply_ops。任何写入后都必须检查工具返回的真实节点类型、connectionCount、overlapWarnings 和 verification;没有真实连线时绝不能说已连线,没有生成资源时绝不能说已完成。不要输出 JSON ops、不要猜 id、不要把未就绪资源当作可用素材、不要编造执行结果。需要用户选择时,给出可点击的短选项,不要只让用户输入 1、2、3。${SKILL_RUNTIME_AGENT_GUIDANCE}`;
const JSON_RECORD_SCHEMA = { type: "object", additionalProperties: true };
const POSITION_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"], additionalProperties: false };
const VIEWPORT_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, k: { type: "number" } }, required: ["x", "y", "k"], additionalProperties: false };
@@ -1443,7 +1443,7 @@ function stringifyLog(value: unknown) {
function formatOnlineLogText(logs: OnlineAgentLog[], context: OnlineAgentLogContext) {
const head = [
"影策网站 Agent 诊断日志",
"站点在线 Agent 诊断日志",
`model: ${context.model || "none"}`,
`running: ${context.running}`,
`confirmTools: ${context.confirmTools}`,

View File

@@ -112,7 +112,7 @@ export function CanvasFolderCard({ project, projectName, onClick, onPrefetch, op
menu={{
onClick: ({ domEvent }) => domEvent.stopPropagation(),
items: [
{ key: "export", icon: <Download className="size-3.5" />, label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "影策画布") },
{ key: "export", icon: <Download className="size-3.5" />, label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "画布") },
{ type: "divider" },
{ key: "delete", danger: true, icon: <Trash2 className="size-3.5" />, label: "删除", onClick: () => setDeleteIds([project.id]) },
],

View File

@@ -1201,7 +1201,7 @@ function parseEventJson<T>(data: string) {
function formatLogText(logs: AgentEventLog[], context: AgentLogContext) {
const head = [
"影策 Canvas Agent 诊断日志",
"站点 Canvas Agent 诊断日志",
`连接: ${context.connected ? "在线" : context.enabled ? "连接中" : "未启用"}`,
`状态: ${context.activity}`,
`waiting: ${context.waiting}`,

View File

@@ -114,7 +114,7 @@ export function CanvasProjectCard({ project, projectName, variant = "library", r
menu={{
onClick: ({ domEvent }) => domEvent.stopPropagation(),
items: [
{ key: "export", icon: <Download className="size-3.5" />, label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "影策画布") },
{ key: "export", icon: <Download className="size-3.5" />, label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "画布") },
{ type: "divider" },
{ key: "delete", danger: true, icon: <Trash2 className="size-3.5" />, label: "删除", onClick: () => setDeleteIds([project.id]) },
],

View File

@@ -7,6 +7,7 @@ import zhCN from "antd/locale/zh_CN";
import { AuthSessionHydrator } from "@/components/auth/auth-session-hydrator";
import { ClientRootInit } from "@/components/layout/client-root-init";
import { getAntThemeConfig } from "@/lib/app-theme";
import { applySkinTheme } from "@/lib/skin-themes";
import { appQueryClient } from "@/lib/query-client";
import { useThemeStore } from "@/stores/use-theme-store";
import { applyAppearanceMetadata, useAppearanceStore } from "@/stores/use-appearance-store";
@@ -72,6 +73,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = theme;
applySkinTheme(appearance.activeSkin, theme);
applyAppearanceMetadata(appearance);
}, [appearance, dark, theme]);
@@ -81,7 +83,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
const isolateDevRepro = import.meta.env.DEV && typeof window !== "undefined" && window.location.pathname === "/dev/director-repro";
return (
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark)}>
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark, appearance.activeSkin)}>
<App message={{ duration: 3, maxCount: 3 }} notification={{ duration: 4.5, maxCount: 3, placement: "topRight" }}>
<QueryClientProvider client={appQueryClient}>
{isolateDevRepro ? (

View File

@@ -0,0 +1,22 @@
import { ExternalLink } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAppearanceStore } from "@/stores/use-appearance-store";
const MIIT_FILING_URL = "https://beian.miit.gov.cn/";
export function SiteComplianceFooter({ className, variant = "default" }: { className?: string; variant?: "default" | "auth" }) {
const appearance = useAppearanceStore((state) => state.appearance);
return (
<footer className={cn("site-compliance-footer", variant === "auth" && "is-auth", className)} aria-label="站点版权与备案信息">
<span>{appearance.footerCopyright}</span>
{appearance.icpFilingEnabled && appearance.icpFilingNumber ? (
<a href={MIIT_FILING_URL} target="_blank" rel="noopener noreferrer" aria-label={`${appearance.icpFilingNumber},前往工业和信息化部备案管理系统查询`}>
{appearance.icpFilingNumber}
<ExternalLink aria-hidden="true" />
</a>
) : null}
</footer>
);
}