fix(*): 请求明细与画布主题 - 区分非计费下载日志并更新默认网格配色
This commit is contained in:
@@ -235,7 +235,7 @@ func (s *Service) decorateAPICallLogs(logs []model.ApiCallLog) error {
|
||||
taskIDs := make([]string, 0, len(logs))
|
||||
seenTaskIDs := make(map[string]struct{}, len(logs))
|
||||
for _, log := range logs {
|
||||
if log.BillingOrderID != "" {
|
||||
if log.Billable && log.BillingOrderID != "" {
|
||||
if _, exists := seenBillingOrderIDs[log.BillingOrderID]; !exists {
|
||||
seenBillingOrderIDs[log.BillingOrderID] = struct{}{}
|
||||
billingOrderIDs = append(billingOrderIDs, log.BillingOrderID)
|
||||
@@ -277,13 +277,15 @@ func (s *Service) decorateAPICallLogs(logs []model.ApiCallLog) error {
|
||||
logs[index].UserDisplayName = user.DisplayName
|
||||
logs[index].UserAccount = user.Username
|
||||
}
|
||||
if order, exists := billingOrderByID[logs[index].BillingOrderID]; exists && order.UserID == logs[index].UserID {
|
||||
logs[index].BillingAvailable = true
|
||||
logs[index].BillingStatus = order.Status
|
||||
if order.Status == model.BillingStatusSettled {
|
||||
logs[index].BillingAmount = order.ActualAmountMicrocredits
|
||||
} else if order.Status != model.BillingStatusRefunded {
|
||||
logs[index].BillingAmount = order.ReservedAmountMicrocredits
|
||||
if logs[index].Billable {
|
||||
if order, exists := billingOrderByID[logs[index].BillingOrderID]; exists && order.UserID == logs[index].UserID {
|
||||
logs[index].BillingAvailable = true
|
||||
logs[index].BillingStatus = order.Status
|
||||
if order.Status == model.BillingStatusSettled {
|
||||
logs[index].BillingAmount = order.ActualAmountMicrocredits
|
||||
} else if order.Status != model.BillingStatusRefunded {
|
||||
logs[index].BillingAmount = order.ReservedAmountMicrocredits
|
||||
}
|
||||
}
|
||||
}
|
||||
if task, exists := taskByID[logs[index].TaskID]; exists && task.UserID == logs[index].UserID {
|
||||
|
||||
@@ -37,12 +37,13 @@ func TestDecorateAPICallLogsUsesBillingOrderSnapshot(t *testing.T) {
|
||||
}
|
||||
|
||||
logs := []model.ApiCallLog{
|
||||
{ID: "log-settled", UserID: "user-1", BillingOrderID: "settled", Capability: "text"},
|
||||
{ID: "log-reserved", UserID: "user-1", BillingOrderID: "reserved", Capability: "text"},
|
||||
{ID: "log-uncertain", UserID: "user-1", BillingOrderID: "uncertain", Capability: "text"},
|
||||
{ID: "log-refunded", UserID: "user-1", BillingOrderID: "refunded", Capability: "text"},
|
||||
{ID: "log-missing", UserID: "user-1", BillingOrderID: "missing", Capability: "text"},
|
||||
{ID: "log-other-user", UserID: "user-1", BillingOrderID: "other-user", Capability: "text"},
|
||||
{ID: "log-settled", UserID: "user-1", BillingOrderID: "settled", Capability: "text", Billable: true},
|
||||
{ID: "log-reserved", UserID: "user-1", BillingOrderID: "reserved", Capability: "text", Billable: true},
|
||||
{ID: "log-uncertain", UserID: "user-1", BillingOrderID: "uncertain", Capability: "text", Billable: true},
|
||||
{ID: "log-refunded", UserID: "user-1", BillingOrderID: "refunded", Capability: "text", Billable: true},
|
||||
{ID: "log-missing", UserID: "user-1", BillingOrderID: "missing", Capability: "text", Billable: true},
|
||||
{ID: "log-other-user", UserID: "user-1", BillingOrderID: "other-user", Capability: "text", Billable: true},
|
||||
{ID: "log-download", UserID: "user-1", BillingOrderID: "settled", Capability: "image", RequestKind: "download", Billable: false},
|
||||
}
|
||||
|
||||
svc := &Service{repo: repository.New(db)}
|
||||
@@ -56,6 +57,7 @@ func TestDecorateAPICallLogsUsesBillingOrderSnapshot(t *testing.T) {
|
||||
assertBillingSnapshot(t, logs[3], true, model.BillingStatusRefunded, 0)
|
||||
assertBillingSnapshot(t, logs[4], false, "", 0)
|
||||
assertBillingSnapshot(t, logs[5], false, "", 0)
|
||||
assertBillingSnapshot(t, logs[6], false, "", 0)
|
||||
}
|
||||
|
||||
func assertBillingSnapshot(t *testing.T, log model.ApiCallLog, available bool, status model.BillingStatus, amount int64) {
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from "@/lib/canvas/canvas-appearance";
|
||||
import type { CanvasBackgroundMode, CanvasColorTheme, CanvasTheme } from "@/lib/canvas-theme";
|
||||
|
||||
const LIGHT_PRESETS = ["#EDEEEE", "#F3DCE5", "#EEE5D8", "#DFE9E6"];
|
||||
const DARK_PRESETS = ["#262626", "#30272B", "#302D26", "#25302E"];
|
||||
const LIGHT_PRESETS = ["#F0F0F0", "#F3DCE5", "#EEE5D8", "#DFE9E6"];
|
||||
const DARK_PRESETS = ["#000000", "#30272B", "#302D26", "#25302E"];
|
||||
|
||||
export function CanvasAppearanceControls({
|
||||
appearance,
|
||||
|
||||
@@ -4,9 +4,9 @@ export type CanvasBackgroundMode = "dots" | "lines" | "blank";
|
||||
export const canvasThemes = {
|
||||
light: {
|
||||
canvas: {
|
||||
background: "#edeeee",
|
||||
dot: "rgba(15,23,42,.20)",
|
||||
line: "rgba(15,23,42,.15)",
|
||||
background: "#f0f0f0",
|
||||
dot: "rgba(0,0,0,.80)",
|
||||
line: "rgba(0,0,0,.80)",
|
||||
selectionFill: "rgba(17,17,17,.10)",
|
||||
},
|
||||
node: {
|
||||
@@ -70,9 +70,9 @@ export const canvasThemes = {
|
||||
},
|
||||
dark: {
|
||||
canvas: {
|
||||
background: "#262626",
|
||||
dot: "rgba(205,205,205,.28)",
|
||||
line: "rgba(205,205,205,.28)",
|
||||
background: "#000000",
|
||||
dot: "rgba(175,175,175,.80)",
|
||||
line: "rgba(175,175,175,.80)",
|
||||
selectionFill: "rgba(255,255,255,.12)",
|
||||
},
|
||||
node: {
|
||||
|
||||
@@ -27,10 +27,12 @@ export type ResolvedCanvasAppearance = {
|
||||
grid: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_CANVAS_BACKGROUND_MODE: CanvasBackgroundMode = "dots";
|
||||
|
||||
const CANVAS_APPEARANCE_DEFAULT_KEY = "infinite-canvas:canvas-appearance-default";
|
||||
const HEX_COLOR_PATTERN = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
const CUSTOM_GRID_COLOR = "#B6B3B3";
|
||||
const CUSTOM_GRID_OPACITY = 70;
|
||||
const CUSTOM_GRID_COLOR: Record<CanvasColorTheme, string> = { light: "#000000", dark: "#AFAFAF" };
|
||||
const CUSTOM_GRID_OPACITY = 80;
|
||||
|
||||
export function canvasAppearanceForTheme(theme: CanvasColorTheme, previous?: CanvasAppearance): CanvasAppearance {
|
||||
return previous?.custom?.baseTheme === theme ? { mode: theme, custom: previous.custom } : { mode: theme };
|
||||
@@ -43,7 +45,7 @@ export function customCanvasAppearanceFromTheme(theme: CanvasColorTheme): Canvas
|
||||
baseTheme: theme,
|
||||
backgroundColor: canvasThemes[theme].canvas.background.toUpperCase(),
|
||||
backgroundBrightness: 0,
|
||||
gridColor: CUSTOM_GRID_COLOR,
|
||||
gridColor: CUSTOM_GRID_COLOR[theme],
|
||||
gridOpacity: CUSTOM_GRID_OPACITY,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,6 +70,8 @@ function LogDetail({ log, querying, onQueryProviderTask }: { log: ApiCallLog; qu
|
||||
],
|
||||
["渠道 / 模型", `${log.channelName || "未记录渠道"} / ${log.model || "未识别模型"}`],
|
||||
["能力", capabilityText(log.capability)],
|
||||
["请求阶段", requestKindText(log.requestKind)],
|
||||
["计费属性", log.billable ? "计费调用" : "不计费"],
|
||||
["总耗时", formatDuration(log.durationMs)],
|
||||
["视频轮询", log.capability === "video" ? `${log.pollCount || 0} 次` : "--"],
|
||||
["Token", log.usageAvailable ? `${log.inputTokens} 输入 / ${log.outputTokens} 输出 / ${log.cachedTokens} 缓存` : "未返回"],
|
||||
@@ -110,12 +112,18 @@ function LogDetail({ log, querying, onQueryProviderTask }: { log: ApiCallLog; qu
|
||||
}
|
||||
|
||||
function billingText(log: ApiCallLog) {
|
||||
if (!log.billable) return "不计费";
|
||||
if (!log.billingAvailable) return "未扣积分";
|
||||
const status = log.billingStatus || "reserved";
|
||||
const statusLabel = ({ settled: "已结算", refunded: "已退回", uncertain: "待核对", running: "运行中", reserved: "已预授权" } as const)[status];
|
||||
return `${formatCredits(log.billingAmountMicrocredits)} 积分 · ${statusLabel}`;
|
||||
}
|
||||
|
||||
function requestKindText(value: ApiCallLog["requestKind"]) {
|
||||
const labels: Partial<Record<ApiCallLog["requestKind"], string>> = { create: "模型生成", poll: "状态查询", download: "结果下载", repair: "结果修复" };
|
||||
return labels[value] || "上游请求";
|
||||
}
|
||||
|
||||
function PayloadPanel({ value, empty }: { value?: string; empty: string }) {
|
||||
if (!value) return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={empty} />;
|
||||
return (
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function LogsPage() {
|
||||
},
|
||||
{ title: "能力", dataIndex: "capability", width: 88, render: capabilityText },
|
||||
{ title: "结果", width: 118, render: (_, log) => <MediaResult log={log} onPreview={(url, kind) => setMediaPreview({ url, kind, title: `${capabilityText(log.capability)}结果` })} /> },
|
||||
{ title: "调用状态", width: 160, render: (_, log) => <CallStatus log={log} /> },
|
||||
{ title: "请求阶段 / 状态", width: 160, render: (_, log) => <CallStatus log={log} /> },
|
||||
{
|
||||
title: "错误信息",
|
||||
width: 260,
|
||||
@@ -125,7 +125,7 @@ export default function LogsPage() {
|
||||
return (
|
||||
<AdminPageFrame
|
||||
title="请求明细"
|
||||
description="上游调用与积分计费"
|
||||
description="模型生成、状态查询与结果下载;仅计费调用扣除积分"
|
||||
actions={
|
||||
<AdminExportButton
|
||||
exportFile={() => exportAdminApiLogs({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status })}
|
||||
@@ -258,6 +258,7 @@ function formatDuration(value: number) {
|
||||
}
|
||||
|
||||
function BillingSummary({ log }: { log: ApiCallLog }) {
|
||||
if (!log.billable) return <span className="text-foreground/45">不计费</span>;
|
||||
if (!log.billingAvailable) return <span className="text-foreground/35">未扣积分</span>;
|
||||
const status = log.billingStatus || "reserved";
|
||||
const statusLabel = ({ settled: "已结算", refunded: "已退回", uncertain: "待核对", running: "运行中", reserved: "已预授权" } as const)[status];
|
||||
@@ -308,8 +309,14 @@ function CallStatus({ log }: { log: ApiCallLog }) {
|
||||
const failed = log.status === "failed" || ["failed", "cancelled", "expired"].includes(providerStatus || "");
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-medium text-foreground/70">{requestKindText(log.requestKind)}</div>
|
||||
<AdminStatusBadge label={failed ? "失败" : processing ? "处理中" : "成功"} tone={failed ? "error" : processing ? "warning" : "success"} />
|
||||
{log.capability === "video" ? <div className="mt-1 text-xs tabular-nums text-foreground/45">已轮询 {log.pollCount || 0} 次</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function requestKindText(value: ApiCallLog["requestKind"]) {
|
||||
const labels: Partial<Record<ApiCallLog["requestKind"], string>> = { create: "模型生成", poll: "状态查询", download: "结果下载", repair: "结果修复" };
|
||||
return labels[value] || "上游请求";
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { uploadImage } from "@/services/image-storage";
|
||||
import { imageMetadata } from "@/lib/canvas/canvas-generation-task-sync";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { nanoid } from "nanoid";
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, normalizeCanvasAppearance, resolveCanvasAppearance, writeCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, resolveCanvasAppearance, writeCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
|
||||
import { persistCanvasMediaPerformanceMode, readCanvasMediaPerformanceMode } from "@/lib/canvas/canvas-performance-mode";
|
||||
import { summarizeCanvasContext } from "@/lib/canvas/canvas-context-summary";
|
||||
@@ -256,7 +256,7 @@ function InfiniteCanvasPage() {
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
|
||||
const [canvasAppearance, setCanvasAppearance] = useState<CanvasAppearance>(() => canvasAppearanceForTheme(colorTheme));
|
||||
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>("lines");
|
||||
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>(DEFAULT_CANVAS_BACKGROUND_MODE);
|
||||
const [showImageInfo, setShowImageInfo] = useState(false);
|
||||
const [canvasTool, setCanvasTool] = useState<CanvasToolMode>("move");
|
||||
const [mediaPerformanceMode, setMediaPerformanceMode] = useState<CanvasMediaPerformanceMode>(readCanvasMediaPerformanceMode);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { InfiniteCanvas } from "@/components/canvas/infinite-canvas";
|
||||
import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader";
|
||||
import { WorkspaceState } from "@/components/layout/workspace-state";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, normalizeCanvasAppearance, resolveCanvasAppearance, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, resolveCanvasAppearance, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { FOLDER_COLLAPSED_HEIGHT, FOLDER_COLLAPSED_WIDTH, isCanvasFolderNode, isFrameNode, isNodeHiddenByCollapsedFrame, resolveFrameConnection } from "@/lib/canvas/canvas-frame";
|
||||
import { ensureMediaNodeMinimumSize } from "@/lib/canvas/canvas-node-size";
|
||||
@@ -93,7 +93,7 @@ export default function SharedCanvasPage() {
|
||||
setAppearance(nextAppearance);
|
||||
appliedShareTheme = canvasAppearanceBaseTheme(nextAppearance, themeBeforeShare);
|
||||
useThemeStore.getState().setTheme(appliedShareTheme);
|
||||
setBackgroundMode(project.backgroundMode || "lines");
|
||||
setBackgroundMode(project.backgroundMode || DEFAULT_CANVAS_BACKGROUND_MODE);
|
||||
const initial = project.viewport || { x: 0, y: 0, k: 1 };
|
||||
viewportRef.current = initial;
|
||||
setViewport(initial);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { startTransition, useCallback, useEffect, useRef, useState, type Dispatc
|
||||
import { App } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, normalizeCanvasAppearance, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import type { CanvasBackgroundMode } from "@/lib/canvas-theme";
|
||||
import { removeCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
||||
import { normalizeCanvasNodeTimestamps } from "@/lib/canvas/canvas-node-timestamps";
|
||||
@@ -103,7 +103,7 @@ export function useCanvasProjectLifecycle({
|
||||
chatSessions: restoredSessions,
|
||||
activeChatId: project.activeChatId || null,
|
||||
canvasAppearance: restoredAppearance,
|
||||
backgroundMode: project.backgroundMode || "dots",
|
||||
backgroundMode: project.backgroundMode || DEFAULT_CANVAS_BACKGROUND_MODE,
|
||||
showImageInfo: project.showImageInfo || false,
|
||||
};
|
||||
nodesRef.current = snapshot.nodes;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { create } from "zustand";
|
||||
import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
import { normalizeCanvasAppearance, readCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, readCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { parseCanvasStorageDocument, rebaseCanvasProjects, serializeCanvasStorageDocument, type CanvasStorageDocument } from "@/lib/canvas/canvas-storage-revision";
|
||||
import { localForageStorageForScope } from "@/lib/localforage-storage";
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
@@ -448,7 +448,7 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
chatSessions: [],
|
||||
activeChatId: null,
|
||||
appearance: appearanceDefault?.appearance,
|
||||
backgroundMode: appearanceDefault?.backgroundMode || "lines",
|
||||
backgroundMode: appearanceDefault?.backgroundMode || DEFAULT_CANVAS_BACKGROUND_MODE,
|
||||
showImageInfo: false,
|
||||
viewport: initialViewport,
|
||||
directorScenes: [],
|
||||
@@ -470,7 +470,7 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
activeChatId: source.activeChatId || null,
|
||||
starterMode: source.starterMode,
|
||||
appearance: source.appearance ? normalizeCanvasAppearance(source.appearance, "dark") : undefined,
|
||||
backgroundMode: source.backgroundMode || "lines",
|
||||
backgroundMode: source.backgroundMode || DEFAULT_CANVAS_BACKGROUND_MODE,
|
||||
showImageInfo: source.showImageInfo || false,
|
||||
viewport: source.viewport || initialViewport,
|
||||
directorScenes: source.directorScenes || [],
|
||||
|
||||
@@ -192,9 +192,15 @@ test("request logs display user credit billing independently from upstream cost"
|
||||
|
||||
const billingSummary = sourceSection(listSource, "function BillingSummary", "function MediaResult");
|
||||
expect(listSource).toContain('title: "积分计费"');
|
||||
expect(listSource).toContain('title: "请求阶段 / 状态"');
|
||||
expect(listSource).toContain('description="模型生成、状态查询与结果下载;仅计费调用扣除积分"');
|
||||
expect(billingSummary).toContain("billingAmountMicrocredits");
|
||||
expect(billingSummary).toContain("billingAvailable");
|
||||
expect(billingSummary).toContain("!log.billable");
|
||||
expect(billingSummary).toContain("不计费");
|
||||
expect(billingSummary).not.toContain("costAvailable");
|
||||
expect(detailSource).toContain('["请求阶段", requestKindText(log.requestKind)]');
|
||||
expect(detailSource).toContain('["计费属性", log.billable ? "计费调用" : "不计费"]');
|
||||
expect(detailSource).toContain('["积分计费", billingText(log)]');
|
||||
expect(detailSource).toContain('["上游成本", log.costAvailable');
|
||||
expect(apiSource).toContain("billingAmountMicrocredits: number");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
DEFAULT_CANVAS_BACKGROUND_MODE,
|
||||
canvasAppearanceForTheme,
|
||||
customCanvasAppearanceFromTheme,
|
||||
enterCustomCanvasAppearance,
|
||||
@@ -29,21 +30,25 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("canvas custom appearance", () => {
|
||||
test("uses point grid as the default for new canvases", () => {
|
||||
expect(DEFAULT_CANVAS_BACKGROUND_MODE).toBe("dots");
|
||||
});
|
||||
|
||||
test("inherits the active fixed theme the first time custom mode is selected", () => {
|
||||
const light = enterCustomCanvasAppearance(canvasAppearanceForTheme("light"), "light");
|
||||
expect(light).toEqual({
|
||||
mode: "custom",
|
||||
custom: {
|
||||
baseTheme: "light",
|
||||
backgroundColor: "#EDEEEE",
|
||||
backgroundColor: "#F0F0F0",
|
||||
backgroundBrightness: 0,
|
||||
gridColor: "#B6B3B3",
|
||||
gridOpacity: 70,
|
||||
gridColor: "#000000",
|
||||
gridOpacity: 80,
|
||||
},
|
||||
});
|
||||
|
||||
const dark = enterCustomCanvasAppearance(canvasAppearanceForTheme("dark"), "dark");
|
||||
expect(dark.custom).toMatchObject({ baseTheme: "dark", backgroundColor: "#262626", backgroundBrightness: 0, gridColor: "#B6B3B3", gridOpacity: 70 });
|
||||
expect(dark.custom).toMatchObject({ baseTheme: "dark", backgroundColor: "#000000", backgroundBrightness: 0, gridColor: "#AFAFAF", gridOpacity: 80 });
|
||||
});
|
||||
|
||||
test("restores a previous custom profile only under the same base theme", () => {
|
||||
@@ -56,7 +61,7 @@ describe("canvas custom appearance", () => {
|
||||
expect(fixedDark.custom).toBeUndefined();
|
||||
expect(enterCustomCanvasAppearance(fixedDark, "dark").custom).toMatchObject({
|
||||
baseTheme: "dark",
|
||||
backgroundColor: "#262626",
|
||||
backgroundColor: "#000000",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +141,8 @@ describe("canvas custom appearance", () => {
|
||||
|
||||
test("does not expose background opacity after legacy values are ignored", async () => {
|
||||
const controlsSource = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
||||
expect(controlsSource).toContain('const LIGHT_PRESETS = ["#F0F0F0"');
|
||||
expect(controlsSource).toContain('const DARK_PRESETS = ["#000000"');
|
||||
expect(controlsSource).not.toContain('label="背景透明度"');
|
||||
expect(controlsSource).toContain('aria-label="界面样式"');
|
||||
expect(controlsSource).toContain('label="网格强度"');
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("canvas visual contrast", () => {
|
||||
});
|
||||
|
||||
test("changes only the canvas substrate while retaining original surfaces", () => {
|
||||
expect(canvasThemes.light.canvas.background).toBe("#edeeee");
|
||||
expect(canvasThemes.light.canvas.background).toBe("#f0f0f0");
|
||||
expect(canvasThemes.light.node.fill).toBe("#ffffff");
|
||||
expect(canvasThemes.light.node.edge).toBe("rgba(15,23,42,.16)");
|
||||
expect(canvasThemes.light.node.shadow).toBe("0 6px 18px rgba(15,23,42,.08)");
|
||||
@@ -19,7 +19,7 @@ describe("canvas visual contrast", () => {
|
||||
expect(canvasThemes.light.toolbar.panel).toBe("rgba(255,255,255,.94)");
|
||||
expect(canvasThemes.light.spatial.elevated).toBe("rgba(255,255,255,.94)");
|
||||
|
||||
expect(canvasThemes.dark.canvas.background).toBe("#262626");
|
||||
expect(canvasThemes.dark.canvas.background).toBe("#000000");
|
||||
expect(canvasThemes.dark.node.fill).toBe("#181818");
|
||||
expect(canvasThemes.dark.node.edge).toBe("rgba(255,255,255,.18)");
|
||||
expect(canvasThemes.dark.node.shadow).toBe("0 8px 24px rgba(0,0,0,.34)");
|
||||
@@ -30,10 +30,10 @@ describe("canvas visual contrast", () => {
|
||||
});
|
||||
|
||||
test("pins intentional grid tokens while retaining canvas grid opacity", async () => {
|
||||
expect(canvasThemes.light.canvas.dot).toBe("rgba(15,23,42,.20)");
|
||||
expect(canvasThemes.light.canvas.line).toBe("rgba(15,23,42,.15)");
|
||||
expect(canvasThemes.dark.canvas.dot).toBe("rgba(205,205,205,.28)");
|
||||
expect(canvasThemes.dark.canvas.line).toBe("rgba(205,205,205,.28)");
|
||||
expect(canvasThemes.light.canvas.dot).toBe("rgba(0,0,0,.80)");
|
||||
expect(canvasThemes.light.canvas.line).toBe("rgba(0,0,0,.80)");
|
||||
expect(canvasThemes.dark.canvas.dot).toBe("rgba(175,175,175,.80)");
|
||||
expect(canvasThemes.dark.canvas.line).toBe("rgba(175,175,175,.80)");
|
||||
|
||||
const source = await Bun.file(new URL("../src/components/canvas/infinite-canvas.tsx", import.meta.url)).text();
|
||||
expect(source).toContain('opacity: mode === "dots" ? 0.34 : 0.46');
|
||||
|
||||
Reference in New Issue
Block a user