feat(canvas): 绘图工具支持自定义 tldraw License Key (#100)
* feat(canvas): 绘图工具 - 支持自定义 tldraw License Key * docs(canvas): 绘图工具 - 添加 License Key 页面截图
This commit is contained in:
1 parent
9b3ec218e2
commit
d83ec033bb
11 files changed
+186
-47
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -590,6 +590,17 @@ func (r *Repository) SaveSystemSetting(setting *model.SystemSetting) error {
|
|||||||
return r.db.Save(setting).Error
|
return r.db.Save(setting).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) SaveSystemSettings(settings ...*model.SystemSetting) error {
|
||||||
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, setting := range settings {
|
||||||
|
if err := tx.Save(setting).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) DeleteSystemSetting(key string) error {
|
func (r *Repository) DeleteSystemSetting(key string) error {
|
||||||
return r.db.Delete(&model.SystemSetting{}, "key = ?", key).Error
|
return r.db.Delete(&model.SystemSetting{}, "key = ?", key).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const drawingEngineSettingKey = "drawing_engine"
|
const (
|
||||||
|
drawingEngineSettingKey = "drawing_engine"
|
||||||
|
tldrawLicenseKeySettingKey = "tldraw_license_key"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DrawingEngineTldraw = "tldraw"
|
DrawingEngineTldraw = "tldraw"
|
||||||
@@ -19,7 +22,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DrawingEngineSetting struct {
|
type DrawingEngineSetting struct {
|
||||||
DefaultEngine string `json:"defaultEngine"`
|
DefaultEngine string `json:"defaultEngine"`
|
||||||
|
TldrawLicenseKey string `json:"tldrawLicenseKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicDrawingEngineSetting struct {
|
type PublicDrawingEngineSetting struct {
|
||||||
@@ -57,11 +61,22 @@ func (s *Service) UpdateDrawingEngineSetting(actor *model.User, value DrawingEng
|
|||||||
if err := validateDrawingEngineSetting(value); err != nil {
|
if err := validateDrawingEngineSetting(value); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
value.TldrawLicenseKey = strings.TrimSpace(value.TldrawLicenseKey)
|
||||||
current, before, err := s.readDrawingEngineSetting()
|
current, before, err := s.readDrawingEngineSetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
encoded, err := json.Marshal(value)
|
currentLicenseSetting, _, err := s.readTldrawLicenseKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(struct {
|
||||||
|
DefaultEngine string `json:"defaultEngine"`
|
||||||
|
}{DefaultEngine: value.DefaultEngine})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encodedLicenseKey, err := json.Marshal(value.TldrawLicenseKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,10 +84,14 @@ func (s *Service) UpdateDrawingEngineSetting(actor *model.User, value DrawingEng
|
|||||||
if current != nil {
|
if current != nil {
|
||||||
setting.CreatedAt = current.CreatedAt
|
setting.CreatedAt = current.CreatedAt
|
||||||
}
|
}
|
||||||
if err := s.repo.SaveSystemSetting(&setting); err != nil {
|
licenseSetting := model.SystemSetting{Key: tldrawLicenseKeySettingKey, ValueJSON: string(encodedLicenseKey), UpdatedBy: actor.ID}
|
||||||
|
if currentLicenseSetting != nil {
|
||||||
|
licenseSetting.CreatedAt = currentLicenseSetting.CreatedAt
|
||||||
|
}
|
||||||
|
if err := s.repo.SaveSystemSettings(&setting, &licenseSetting); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.appendAdminAudit(actor, "drawing_engine.update", "system_setting", drawingEngineSettingKey, "更新默认绘图工具", map[string]any{"before": before, "after": value}); err != nil {
|
if err := s.appendAdminAudit(actor, "drawing_engine.update", "system_setting", drawingEngineSettingKey, "更新绘图工具配置", map[string]any{"before": drawingEngineAuditValue(before), "after": drawingEngineAuditValue(value)}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return publicDrawingEngineSetting(&setting, value), nil
|
return publicDrawingEngineSetting(&setting, value), nil
|
||||||
@@ -80,22 +99,42 @@ func (s *Service) UpdateDrawingEngineSetting(actor *model.User, value DrawingEng
|
|||||||
|
|
||||||
func (s *Service) readDrawingEngineSetting() (*model.SystemSetting, DrawingEngineSetting, error) {
|
func (s *Service) readDrawingEngineSetting() (*model.SystemSetting, DrawingEngineSetting, error) {
|
||||||
setting, err := s.repo.SystemSetting(drawingEngineSettingKey)
|
setting, err := s.repo.SystemSetting(drawingEngineSettingKey)
|
||||||
|
value := defaultDrawingEngineSetting()
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, defaultDrawingEngineSetting(), nil
|
setting = nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, DrawingEngineSetting{}, err
|
||||||
|
} else {
|
||||||
|
if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil {
|
||||||
|
return nil, DrawingEngineSetting{}, errors.New("绘图工具配置格式无效")
|
||||||
|
}
|
||||||
|
if err := validateDrawingEngineSetting(value); err != nil {
|
||||||
|
return nil, DrawingEngineSetting{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_, licenseKey, err := s.readTldrawLicenseKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, DrawingEngineSetting{}, err
|
return nil, DrawingEngineSetting{}, err
|
||||||
}
|
}
|
||||||
value := DrawingEngineSetting{}
|
value.TldrawLicenseKey = licenseKey
|
||||||
if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil {
|
|
||||||
return nil, DrawingEngineSetting{}, errors.New("绘图工具配置格式无效")
|
|
||||||
}
|
|
||||||
if err := validateDrawingEngineSetting(value); err != nil {
|
|
||||||
return nil, DrawingEngineSetting{}, err
|
|
||||||
}
|
|
||||||
return setting, value, nil
|
return setting, value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) readTldrawLicenseKey() (*model.SystemSetting, string, error) {
|
||||||
|
setting, err := s.repo.SystemSetting(tldrawLicenseKeySettingKey)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
var value string
|
||||||
|
if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil {
|
||||||
|
return nil, "", errors.New("tldraw License Key 配置格式无效")
|
||||||
|
}
|
||||||
|
return setting, strings.TrimSpace(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateDrawingEngineSetting(value DrawingEngineSetting) error {
|
func validateDrawingEngineSetting(value DrawingEngineSetting) error {
|
||||||
if value.DefaultEngine != DrawingEngineTldraw && value.DefaultEngine != DrawingEngineExcalidraw {
|
if value.DefaultEngine != DrawingEngineTldraw && value.DefaultEngine != DrawingEngineExcalidraw {
|
||||||
return BadAuthRequest("默认绘图工具必须是 tldraw 或 Excalidraw")
|
return BadAuthRequest("默认绘图工具必须是 tldraw 或 Excalidraw")
|
||||||
@@ -103,6 +142,10 @@ func validateDrawingEngineSetting(value DrawingEngineSetting) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawingEngineAuditValue(value DrawingEngineSetting) map[string]any {
|
||||||
|
return map[string]any{"defaultEngine": value.DefaultEngine, "hasTldrawLicenseKey": strings.TrimSpace(value.TldrawLicenseKey) != ""}
|
||||||
|
}
|
||||||
|
|
||||||
func publicDrawingEngineSetting(setting *model.SystemSetting, value DrawingEngineSetting) *PublicDrawingEngineSetting {
|
func publicDrawingEngineSetting(setting *model.SystemSetting, value DrawingEngineSetting) *PublicDrawingEngineSetting {
|
||||||
result := &PublicDrawingEngineSetting{DrawingEngineSetting: value, Configured: setting != nil}
|
result := &PublicDrawingEngineSetting{DrawingEngineSetting: value, Configured: setting != nil}
|
||||||
if setting != nil {
|
if setting != nil {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"infinite-canvas/backend/internal/model"
|
||||||
|
"infinite-canvas/backend/internal/repository"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
func TestDefaultDrawingEngineUsesExcalidraw(t *testing.T) {
|
func TestDefaultDrawingEngineUsesExcalidraw(t *testing.T) {
|
||||||
setting := defaultDrawingEngineSetting()
|
setting := defaultDrawingEngineSetting()
|
||||||
@@ -19,3 +29,53 @@ func TestValidateDrawingEngineSetting(t *testing.T) {
|
|||||||
t.Fatal("validate unknown engine = nil")
|
t.Fatal("validate unknown engine = nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateDrawingEngineSettingKeepsDefaultEngineAndStoresTldrawLicenseSeparately(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.SystemSetting{}, &model.AdminAuditEvent{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
repo := repository.New(db)
|
||||||
|
svc := New(repo, t.TempDir())
|
||||||
|
actor := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||||
|
|
||||||
|
setting, err := svc.UpdateDrawingEngineSetting(actor, DrawingEngineSetting{
|
||||||
|
DefaultEngine: DrawingEngineExcalidraw,
|
||||||
|
TldrawLicenseKey: "license-key-for-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if setting.DefaultEngine != DrawingEngineExcalidraw {
|
||||||
|
t.Fatalf("default engine = %q, want %q", setting.DefaultEngine, DrawingEngineExcalidraw)
|
||||||
|
}
|
||||||
|
if setting.TldrawLicenseKey != "license-key-for-test" {
|
||||||
|
t.Fatalf("license key = %q", setting.TldrawLicenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseSetting, err := repo.SystemSetting(tldrawLicenseKeySettingKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var savedLicenseKey string
|
||||||
|
if err := json.Unmarshal([]byte(licenseSetting.ValueJSON), &savedLicenseKey); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if savedLicenseKey != "license-key-for-test" {
|
||||||
|
t.Fatalf("stored license key = %q", savedLicenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
engineSetting, err := repo.SystemSetting(drawingEngineSettingKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(engineSetting.ValueJSON, "license-key-for-test") {
|
||||||
|
t.Fatalf("drawing engine setting must not contain the license key: %s", engineSetting.ValueJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type { CanvasDrawingEditorHandle } from "@/components/canvas/canvas-drawi
|
|||||||
import { drawingEngineForNode, drawingEngineLabel, isDrawingEngineAvailable } from "@/lib/canvas/canvas-drawing-engine";
|
import { drawingEngineForNode, drawingEngineLabel, isDrawingEngineAvailable } from "@/lib/canvas/canvas-drawing-engine";
|
||||||
import { loadCanvasDrawing, saveCanvasDrawing, type CanvasDrawingSnapshot } from "@/lib/canvas/canvas-drawing-storage";
|
import { loadCanvasDrawing, saveCanvasDrawing, type CanvasDrawingSnapshot } from "@/lib/canvas/canvas-drawing-storage";
|
||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
import { useThemeStore } from "@/stores/use-theme-store";
|
||||||
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
import type { CanvasNodeData } from "@/types/canvas";
|
import type { CanvasNodeData } from "@/types/canvas";
|
||||||
|
|
||||||
const CanvasDrawingTldrawEditor = lazy(() => import("@/components/canvas/canvas-drawing-tldraw-editor").then((module) => ({ default: module.CanvasDrawingTldrawEditor })));
|
const CanvasDrawingTldrawEditor = lazy(() => import("@/components/canvas/canvas-drawing-tldraw-editor").then((module) => ({ default: module.CanvasDrawingTldrawEditor })));
|
||||||
@@ -22,6 +23,7 @@ type CanvasDrawingEditorModalProps = {
|
|||||||
export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) {
|
export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const colorScheme = useThemeStore((state) => state.theme);
|
const colorScheme = useThemeStore((state) => state.theme);
|
||||||
|
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
|
||||||
const engine = drawingEngineForNode(node);
|
const engine = drawingEngineForNode(node);
|
||||||
const currentRef = useRef<CanvasDrawingSnapshot | null>(null);
|
const currentRef = useRef<CanvasDrawingSnapshot | null>(null);
|
||||||
const editorRef = useRef<CanvasDrawingEditorHandle | null>(null);
|
const editorRef = useRef<CanvasDrawingEditorHandle | null>(null);
|
||||||
@@ -76,7 +78,7 @@ export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSav
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const unavailable = !isDrawingEngineAvailable(engine);
|
const unavailable = !isDrawingEngineAvailable(engine, tldrawLicenseKey);
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onCancel={() => void handleClose()} footer={null} closable={false} destroyOnHidden width="100vw" centered styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }} className="canvas-drawing-editor-modal">
|
<Modal open={open} onCancel={() => void handleClose()} footer={null} closable={false} destroyOnHidden width="100vw" centered styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }} className="canvas-drawing-editor-modal">
|
||||||
<div className="flex h-[min(92dvh,980px)] flex-col">
|
<div className="flex h-[min(92dvh,980px)] flex-col">
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import type { Editor } from "tldraw";
|
|||||||
import "tldraw/tldraw.css";
|
import "tldraw/tldraw.css";
|
||||||
|
|
||||||
import type { CanvasDrawingEditorHandle, CanvasDrawingEditorProps } from "@/components/canvas/canvas-drawing-editor-types";
|
import type { CanvasDrawingEditorHandle, CanvasDrawingEditorProps } from "@/components/canvas/canvas-drawing-editor-types";
|
||||||
|
import { resolveTldrawLicenseKey } from "@/lib/canvas/canvas-drawing-engine";
|
||||||
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
|
|
||||||
const SINGLE_PAGE_OPTIONS = { maxPages: 1 } as const;
|
const SINGLE_PAGE_OPTIONS = { maxPages: 1 } as const;
|
||||||
const DRAWING_RENDER_MAX_DIMENSION = 2048;
|
const DRAWING_RENDER_MAX_DIMENSION = 2048;
|
||||||
const DRAWING_RENDER_PADDING = 24;
|
const DRAWING_RENDER_PADDING = 24;
|
||||||
|
|
||||||
export const CanvasDrawingTldrawEditor = forwardRef<CanvasDrawingEditorHandle, CanvasDrawingEditorProps>(function CanvasDrawingTldrawEditor({ snapshot, colorScheme, onReady }, ref) {
|
export const CanvasDrawingTldrawEditor = forwardRef<CanvasDrawingEditorHandle, CanvasDrawingEditorProps>(function CanvasDrawingTldrawEditor({ snapshot, colorScheme, onReady }, ref) {
|
||||||
|
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
|
||||||
const store = useMemo(() => {
|
const store = useMemo(() => {
|
||||||
const next = createTLStore();
|
const next = createTLStore();
|
||||||
if (snapshot) loadSnapshot(next, snapshot as never);
|
if (snapshot) loadSnapshot(next, snapshot as never);
|
||||||
@@ -46,7 +49,7 @@ export const CanvasDrawingTldrawEditor = forwardRef<CanvasDrawingEditorHandle, C
|
|||||||
locale="zh-cn"
|
locale="zh-cn"
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
options={SINGLE_PAGE_OPTIONS}
|
options={SINGLE_PAGE_OPTIONS}
|
||||||
licenseKey={import.meta.env.VITE_TLDRAW_LICENSE_KEY || undefined}
|
licenseKey={resolveTldrawLicenseKey(tldrawLicenseKey)}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
// 绘图节点只保留单页,避免预览和生成引用出现页选择歧义。
|
// 绘图节点只保留单页,避免预览和生成引用出现页选择歧义。
|
||||||
@@ -61,4 +64,4 @@ export const CanvasDrawingTldrawEditor = forwardRef<CanvasDrawingEditorHandle, C
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,7 @@ export type CanvasDrawingEngine = "tldraw" | "excalidraw";
|
|||||||
|
|
||||||
export type CanvasDrawingEngineSetting = {
|
export type CanvasDrawingEngineSetting = {
|
||||||
defaultEngine: CanvasDrawingEngine;
|
defaultEngine: CanvasDrawingEngine;
|
||||||
|
tldrawLicenseKey?: string;
|
||||||
configured?: boolean;
|
configured?: boolean;
|
||||||
updatedBy?: string;
|
updatedBy?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
@@ -17,10 +18,14 @@ export function drawingEngineForNode(node?: Pick<CanvasNodeData, "metadata"> | n
|
|||||||
return node?.metadata?.drawingEngine === "excalidraw" ? "excalidraw" : "tldraw";
|
return node?.metadata?.drawingEngine === "excalidraw" ? "excalidraw" : "tldraw";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDrawingEngineAvailable(engine: CanvasDrawingEngine) {
|
export function resolveTldrawLicenseKey(configuredLicenseKey?: string) {
|
||||||
return engine === "excalidraw" || import.meta.env.DEV || Boolean(import.meta.env.VITE_TLDRAW_LICENSE_KEY);
|
return configuredLicenseKey?.trim() || import.meta.env.VITE_TLDRAW_LICENSE_KEY?.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDrawingEngineAvailable(engine: CanvasDrawingEngine, configuredLicenseKey?: string) {
|
||||||
|
return engine === "excalidraw" || Boolean(resolveTldrawLicenseKey(configuredLicenseKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drawingEngineLabel(engine: CanvasDrawingEngine) {
|
export function drawingEngineLabel(engine: CanvasDrawingEngine) {
|
||||||
return engine === "excalidraw" ? "Excalidraw" : "tldraw";
|
return engine === "excalidraw" ? "Excalidraw" : "tldraw";
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { App, Button, Segmented, Tag } from "antd";
|
import { App, Button, Input, Segmented, Tag } from "antd";
|
||||||
import { Brush, Save } from "lucide-react";
|
import { Brush, Save } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export default function DrawingEngineSettingsPage() {
|
|||||||
const current = useUserStore((state) => state.drawingEngine);
|
const current = useUserStore((state) => state.drawingEngine);
|
||||||
const setDrawingEngine = useUserStore((state) => state.setDrawingEngine);
|
const setDrawingEngine = useUserStore((state) => state.setDrawingEngine);
|
||||||
const [engine, setEngine] = useState<CanvasDrawingEngine>(current.defaultEngine);
|
const [engine, setEngine] = useState<CanvasDrawingEngine>(current.defaultEngine);
|
||||||
|
const [tldrawLicenseKey, setTldrawLicenseKey] = useState(current.tldrawLicenseKey || "");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function DrawingEngineSettingsPage() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setDrawingEngine(setting);
|
setDrawingEngine(setting);
|
||||||
setEngine(setting.defaultEngine);
|
setEngine(setting.defaultEngine);
|
||||||
|
setTldrawLicenseKey(setting.tldrawLicenseKey || "");
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (!cancelled) message.error(error instanceof Error ? error.message : "读取绘图工具配置失败");
|
if (!cancelled) message.error(error instanceof Error ? error.message : "读取绘图工具配置失败");
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -30,17 +32,19 @@ export default function DrawingEngineSettingsPage() {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [message, setDrawingEngine]);
|
}, [message, setDrawingEngine]);
|
||||||
|
|
||||||
|
const tldrawAvailable = isDrawingEngineAvailable("tldraw", tldrawLicenseKey);
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!isDrawingEngineAvailable(engine)) {
|
if (!isDrawingEngineAvailable(engine, tldrawLicenseKey)) {
|
||||||
message.error("当前生产构建未配置 tldraw License Key");
|
message.error("请先配置有效的 tldraw License Key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const { setting } = await updateAdminDrawingEngineSetting(engine);
|
const { setting } = await updateAdminDrawingEngineSetting({ defaultEngine: engine, tldrawLicenseKey });
|
||||||
setDrawingEngine(setting);
|
setDrawingEngine(setting);
|
||||||
setEngine(setting.defaultEngine);
|
setEngine(setting.defaultEngine);
|
||||||
message.success("默认绘图工具已更新");
|
setTldrawLicenseKey(setting.tldrawLicenseKey || "");
|
||||||
|
message.success("绘图工具配置已更新");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : "保存绘图工具配置失败");
|
message.error(error instanceof Error ? error.message : "保存绘图工具配置失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,28 +52,35 @@ export default function DrawingEngineSettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dirty = engine !== current.defaultEngine;
|
const dirty = engine !== current.defaultEngine || tldrawLicenseKey !== (current.tldrawLicenseKey || "");
|
||||||
return (
|
return (
|
||||||
<AdminPageFrame title="绘图工具" description="设置新建绘图节点使用的默认编辑器">
|
<AdminPageFrame title="绘图工具" description="设置新建绘图节点使用的默认编辑器">
|
||||||
<div className="mx-auto max-w-4xl space-y-5">
|
<div className="mx-auto max-w-4xl space-y-5">
|
||||||
<SettingsSectionCard icon={<Brush className="size-4" />} title="默认绘图引擎" description="已有绘图保持原引擎,仅新建绘图使用这里的选择。" status={<Tag variant="filled" color="blue">{drawingEngineLabel(current.defaultEngine)}</Tag>}>
|
<SettingsSectionCard icon={<Brush className="size-4" />} title="默认绘图引擎" description="已有绘图保持原引擎,仅新建绘图使用这里的选择。" status={<Tag variant="filled" color="blue">{drawingEngineLabel(current.defaultEngine)}</Tag>}>
|
||||||
<div className="flex flex-col gap-4 px-5 py-5 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-5 px-5 py-5">
|
||||||
<Segmented<CanvasDrawingEngine>
|
<div className="w-full sm:max-w-md">
|
||||||
block
|
<label className="mb-2 block text-sm font-medium">tldraw License Key</label>
|
||||||
className="w-full sm:max-w-md"
|
<Input.Password value={tldrawLicenseKey} onChange={(event) => setTldrawLicenseKey(event.target.value)} placeholder="输入 tldraw License Key" disabled={loading || saving} autoComplete="off" />
|
||||||
disabled={loading || saving}
|
<p className="mt-2 text-xs text-foreground/55">填入有效的 License Key 后保存,即可解锁 tldraw 选项。</p>
|
||||||
value={engine}
|
</div>
|
||||||
onChange={setEngine}
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
options={[
|
<Segmented<CanvasDrawingEngine>
|
||||||
{ label: "Excalidraw", value: "excalidraw" },
|
block
|
||||||
{ label: "tldraw", value: "tldraw", disabled: !isDrawingEngineAvailable("tldraw") },
|
className="w-full sm:max-w-md"
|
||||||
]}
|
disabled={loading || saving}
|
||||||
/>
|
value={engine}
|
||||||
<Button type="primary" icon={<Save className="size-4" />} loading={saving} disabled={loading || !dirty} onClick={() => void save()}>保存配置</Button>
|
onChange={setEngine}
|
||||||
|
options={[
|
||||||
|
{ label: "Excalidraw", value: "excalidraw" },
|
||||||
|
{ label: "tldraw", value: "tldraw", disabled: !tldrawAvailable },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button type="primary" icon={<Save className="size-4" />} loading={saving} disabled={loading || !dirty} onClick={() => void save()}>保存配置</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isDrawingEngineAvailable("tldraw") ? <div className="border-t border-border px-5 py-3 text-xs text-amber-600 dark:text-amber-400">当前生产构建没有 tldraw License Key,不能将其设为新建默认值。</div> : null}
|
{!tldrawAvailable ? <div className="border-t border-border px-5 py-3 text-xs text-amber-600 dark:text-amber-400">当前未配置 tldraw License Key,不能将其设为新建默认值。</div> : null}
|
||||||
</SettingsSectionCard>
|
</SettingsSectionCard>
|
||||||
</div>
|
</div>
|
||||||
</AdminPageFrame>
|
</AdminPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor,
|
|||||||
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
|
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
|
||||||
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
||||||
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
|
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
|
||||||
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
|
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
|
||||||
|
|
||||||
type UseCanvasConnectionControllerOptions = {
|
type UseCanvasConnectionControllerOptions = {
|
||||||
@@ -55,6 +56,7 @@ export function useCanvasConnectionController({
|
|||||||
setDrawingNodeId,
|
setDrawingNodeId,
|
||||||
}: UseCanvasConnectionControllerOptions) {
|
}: UseCanvasConnectionControllerOptions) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
|
||||||
const [connectingParams, setConnectingParams] = useState<ConnectionHandle | null>(null);
|
const [connectingParams, setConnectingParams] = useState<ConnectionHandle | null>(null);
|
||||||
const [connectionTargetNodeId, setConnectionTargetNodeId] = useState<string | null>(null);
|
const [connectionTargetNodeId, setConnectionTargetNodeId] = useState<string | null>(null);
|
||||||
const [connectionTargetAnchorRatio, setConnectionTargetAnchorRatio] = useState<number | undefined>();
|
const [connectionTargetAnchorRatio, setConnectionTargetAnchorRatio] = useState<number | undefined>();
|
||||||
@@ -114,7 +116,7 @@ export function useCanvasConnectionController({
|
|||||||
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
|
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
|
||||||
|
|
||||||
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
|
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
|
||||||
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine)) {
|
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
|
||||||
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
|
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -190,7 +192,7 @@ export function useCanvasConnectionController({
|
|||||||
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
|
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
|
||||||
closeConnectionCreateMenu();
|
closeConnectionCreateMenu();
|
||||||
setConnecting(null);
|
setConnecting(null);
|
||||||
}, [closeConnectionCreateMenu, defaultDrawingEngine, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
}, [closeConnectionCreateMenu, defaultDrawingEngine, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, tldrawLicenseKey]);
|
||||||
|
|
||||||
const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
|
const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
|
||||||
const world = screenToCanvas(clientX, clientY);
|
const world = screenToCanvas(clientX, clientY);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { isolateCopiedNodeMetadata } from "@/lib/canvas/canvas-node-copy";
|
|||||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas";
|
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas";
|
||||||
import { cloneCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
import { cloneCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
||||||
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
||||||
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
|
|
||||||
type CanvasClipboard = {
|
type CanvasClipboard = {
|
||||||
nodes: CanvasNodeData[];
|
nodes: CanvasNodeData[];
|
||||||
@@ -52,6 +53,7 @@ export function useCanvasNodeOperations({
|
|||||||
onNodesDeleted,
|
onNodesDeleted,
|
||||||
}: UseCanvasNodeOperationsOptions) {
|
}: UseCanvasNodeOperationsOptions) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
|
||||||
const clipboardRef = useRef<CanvasClipboard | null>(null);
|
const clipboardRef = useRef<CanvasClipboard | null>(null);
|
||||||
const preferCopiedNodesRef = useRef(false);
|
const preferCopiedNodesRef = useRef(false);
|
||||||
const markerWritePendingRef = useRef(false);
|
const markerWritePendingRef = useRef(false);
|
||||||
@@ -120,7 +122,7 @@ export function useCanvasNodeOperations({
|
|||||||
}, [selectedNodeIdsRef, setSelectedConnectionId, setSelectedNodeIds]);
|
}, [selectedNodeIdsRef, setSelectedConnectionId, setSelectedNodeIds]);
|
||||||
|
|
||||||
const createNode = useCallback((type: CanvasNodeType, position?: Position) => {
|
const createNode = useCallback((type: CanvasNodeType, position?: Position) => {
|
||||||
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine)) {
|
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
|
||||||
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
|
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,7 +130,7 @@ export function useCanvasNodeOperations({
|
|||||||
commitNodes([...nodesRef.current, node]);
|
commitNodes([...nodesRef.current, node]);
|
||||||
selectNodes(new Set([node.id]));
|
selectNodes(new Set([node.id]));
|
||||||
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio && type !== CanvasNodeType.Frame && type !== CanvasNodeType.Drawing) setDialogNodeId(node.id);
|
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio && type !== CanvasNodeType.Frame && type !== CanvasNodeType.Drawing) setDialogNodeId(node.id);
|
||||||
}, [commitNodes, defaultDrawingEngine, getCanvasCenter, message, nodesRef, selectNodes, setDialogNodeId]);
|
}, [commitNodes, defaultDrawingEngine, getCanvasCenter, message, nodesRef, selectNodes, setDialogNodeId, tldrawLicenseKey]);
|
||||||
|
|
||||||
const arrangeSelectedNodes = useCallback((mode: "row" | "column" | "grid" | "flow") => {
|
const arrangeSelectedNodes = useCallback((mode: "row" | "column" | "grid" | "flow") => {
|
||||||
const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id) && !node.metadata?.locked && !isFrameNode(node));
|
const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id) && !node.metadata?.locked && !isFrameNode(node));
|
||||||
|
|||||||
@@ -437,8 +437,8 @@ export function getAdminDrawingEngineSetting() {
|
|||||||
return request<{ setting: CanvasDrawingEngineSetting }>(api.get("/admin/settings/drawing-engine"));
|
return request<{ setting: CanvasDrawingEngineSetting }>(api.get("/admin/settings/drawing-engine"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminDrawingEngineSetting(defaultEngine: CanvasDrawingEngineSetting["defaultEngine"]) {
|
export function updateAdminDrawingEngineSetting(input: Pick<CanvasDrawingEngineSetting, "defaultEngine" | "tldrawLicenseKey">) {
|
||||||
return request<{ setting: CanvasDrawingEngineSetting }>(api.patch("/admin/settings/drawing-engine", { defaultEngine }));
|
return request<{ setting: CanvasDrawingEngineSetting }>(api.patch("/admin/settings/drawing-engine", input));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listAdminApiLogs(params: AdminListParams = {}) {
|
export function listAdminApiLogs(params: AdminListParams = {}) {
|
||||||
|
|||||||
Reference in new issue
Block a user