@haoxin-web/excalidraw 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{a,b,c,d}from"./chunk-LS7FJGPW.js";import"./chunk-UB3N3ODO.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
1
+ import{a,b,c,d}from"./chunk-LS7FJGPW.js";import"./chunk-H7BFYYZW.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
@@ -1 +1 @@
1
- import{a as r,c as t}from"./chunk-LS7FJGPW.js";import"./chunk-UB3N3ODO.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
1
+ import{a as r,c as t}from"./chunk-LS7FJGPW.js";import"./chunk-H7BFYYZW.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import type { ExcalidrawImperativeAPI } from "../types";
3
+ export type ExternalLeftToolbarProps = {
4
+ excalidrawAPI: ExcalidrawImperativeAPI | null;
5
+ className?: string;
6
+ style?: React.CSSProperties;
7
+ };
8
+ export declare const ExternalLeftToolbar: ({ excalidrawAPI, className, style, }: ExternalLeftToolbarProps) => import("react/jsx-runtime").JSX.Element;
@@ -3,6 +3,7 @@ import Footer from "./components/footer/FooterCenter";
3
3
  import LiveCollaborationTrigger from "./components/live-collaboration/LiveCollaborationTrigger";
4
4
  import MainMenu from "./components/main-menu/MainMenu";
5
5
  import WelcomeScreen from "./components/welcome-screen/WelcomeScreen";
6
+ import { UserList } from "./components/UserList";
6
7
  import "./css/app.scss";
7
8
  import "./css/styles.scss";
8
9
  import "./fonts/fonts.css";
@@ -32,7 +33,10 @@ export { MainMenu };
32
33
  export { Ellipsify } from "./components/Ellipsify";
33
34
  export { useDevice } from "./components/App";
34
35
  export { WelcomeScreen };
36
+ export { UserList };
35
37
  export { LiveCollaborationTrigger };
38
+ export { ExternalLeftToolbar } from "./components/ExternalLeftToolbar";
39
+ export type { ExternalLeftToolbarProps } from "./components/ExternalLeftToolbar";
36
40
  export { Stats } from "./components/Stats";
37
41
  export { DefaultSidebar } from "./components/DefaultSidebar";
38
42
  export { TTDDialog } from "./components/TTDDialog/TTDDialog";
@@ -45,3 +49,4 @@ export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToC
45
49
  export { getDataURL } from "./data/blob";
46
50
  export { isElementLink } from "@excalidraw/element";
47
51
  export { setCustomTextMetricsProvider } from "@excalidraw/element";
52
+ export type { LeftToolbarCommand, LeftToolbarFreedrawVariant, LeftToolbarLineVariant, LeftToolbarShapeTool, LeftToolbarStylePatch, } from "./types";
@@ -628,6 +628,52 @@ export type PointerDownState = Readonly<{
628
628
  };
629
629
  }>;
630
630
  export type UnsubscribeCallback = () => void;
631
+ export type LeftToolbarShapeTool = Extract<ToolType, "rectangle" | "diamond" | "ellipse" | "triangle" | "pentagon" | "callout" | "hollow_arrow" | "hollow_double_arrow">;
632
+ export type LeftToolbarLineVariant = "line" | "arrow" | "doubleArrow" | "outlineArrow" | "outlineDoubleArrow";
633
+ export type LeftToolbarFreedrawVariant = "pen" | "highlighter";
634
+ export type LeftToolbarStylePatch = Partial<Pick<AppState, "currentItemStrokeColor" | "currentItemBackgroundColor" | "currentItemFillStyle" | "currentItemStrokeWidth" | "currentItemOpacity" | "currentItemStartArrowhead" | "currentItemEndArrowhead" | "currentItemFontSize" | "currentItemTextAlign" | "currentTextBold" | "currentTextItalic" | "currentTextUnderline" | "currentTextStrikethrough">>;
635
+ export type LeftToolbarCommand = {
636
+ type: "selectSelection";
637
+ } | {
638
+ type: "selectFreedraw";
639
+ payload?: {
640
+ variant?: LeftToolbarFreedrawVariant;
641
+ strokeWidth?: number;
642
+ strokeColor?: string;
643
+ };
644
+ } | {
645
+ type: "selectLine";
646
+ payload?: {
647
+ variant?: LeftToolbarLineVariant;
648
+ strokeWidth?: number;
649
+ strokeColor?: string;
650
+ };
651
+ } | {
652
+ type: "selectShape";
653
+ payload?: {
654
+ shape?: LeftToolbarShapeTool;
655
+ strokeWidth?: number;
656
+ strokeColor?: string;
657
+ backgroundColor?: string;
658
+ fillStyle?: AppState["currentItemFillStyle"];
659
+ };
660
+ } | {
661
+ type: "selectText";
662
+ payload?: {
663
+ fontSize?: number;
664
+ textAlign?: AppState["currentItemTextAlign"];
665
+ strokeColor?: string;
666
+ bold?: boolean;
667
+ italic?: boolean;
668
+ underline?: boolean;
669
+ strikethrough?: boolean;
670
+ };
671
+ } | {
672
+ type: "selectEraser";
673
+ } | {
674
+ type: "updateStyle";
675
+ payload: LeftToolbarStylePatch;
676
+ };
631
677
  export interface ExcalidrawImperativeAPI {
632
678
  updateScene: InstanceType<typeof App>["updateScene"];
633
679
  /**
@@ -658,6 +704,61 @@ export interface ExcalidrawImperativeAPI {
658
704
  addFiles: (data: BinaryFileData[]) => void;
659
705
  id: string;
660
706
  setActiveTool: InstanceType<typeof App>["setActiveTool"];
707
+ leftToolbar: {
708
+ /**
709
+ * Left toolbar: selection tool.
710
+ */
711
+ selectSelection: () => void;
712
+ /**
713
+ * Left toolbar: freedraw tool.
714
+ */
715
+ selectFreedraw: (opts?: {
716
+ variant?: LeftToolbarFreedrawVariant;
717
+ strokeWidth?: number;
718
+ strokeColor?: string;
719
+ }) => void;
720
+ /**
721
+ * Left toolbar: line/arrow group.
722
+ */
723
+ selectLine: (opts?: {
724
+ variant?: LeftToolbarLineVariant;
725
+ strokeWidth?: number;
726
+ strokeColor?: string;
727
+ }) => void;
728
+ /**
729
+ * Left toolbar: shapes group.
730
+ */
731
+ selectShape: (shape?: LeftToolbarShapeTool, opts?: {
732
+ strokeWidth?: number;
733
+ strokeColor?: string;
734
+ backgroundColor?: string;
735
+ fillStyle?: AppState["currentItemFillStyle"];
736
+ }) => void;
737
+ /**
738
+ * Left toolbar: text tool.
739
+ */
740
+ selectText: (opts?: {
741
+ fontSize?: number;
742
+ textAlign?: AppState["currentItemTextAlign"];
743
+ strokeColor?: string;
744
+ bold?: boolean;
745
+ italic?: boolean;
746
+ underline?: boolean;
747
+ strikethrough?: boolean;
748
+ }) => void;
749
+ /**
750
+ * Left toolbar: eraser tool.
751
+ */
752
+ selectEraser: () => void;
753
+ /**
754
+ * Generic style update used by external custom toolbar.
755
+ */
756
+ updateStyle: (patch: LeftToolbarStylePatch) => void;
757
+ /**
758
+ * Executes a serializable command. Designed for cross-end communication.
759
+ */
760
+ dispatch: (command: LeftToolbarCommand) => void;
761
+ };
661
762
  setCursor: InstanceType<typeof App>["setCursor"];
662
763
  resetCursor: InstanceType<typeof App>["resetCursor"];
663
764
  toggleSidebar: InstanceType<typeof App>["toggleSidebar"];
@@ -0,0 +1,380 @@
1
+ # Left Toolbar Remote Commands
2
+
3
+ 本文档说明如何在“工具条端与白板端隔离”的场景下,通过命令控制白板左侧工具能力。
4
+
5
+ ## 适用场景
6
+
7
+ - 工具条和白板不在同一个组件中
8
+ - 通过 `WebSocket` / `postMessage` / 其他消息通道传 JSON
9
+ - 白板端收到命令后执行,不直接暴露 UI
10
+
11
+ ## 白板端执行入口
12
+
13
+ 白板端拿到 `excalidrawAPI` 后,统一调用:
14
+
15
+ ```ts
16
+ excalidrawAPI.leftToolbar.dispatch(command);
17
+ ```
18
+
19
+ 其中 `command` 的类型是 `LeftToolbarCommand`。
20
+
21
+ ## Command 总览
22
+
23
+ | `type` | 作用 | `payload` |
24
+ | --- | --- | --- |
25
+ | `selectSelection` | 切换到“选择”工具 | 无 |
26
+ | `selectFreedraw` | 切换到“自由书写”工具 | 可选 |
27
+ | `selectLine` | 切换到“线条/箭头”工具组 | 可选 |
28
+ | `selectShape` | 切换到“形状”工具组 | 可选 |
29
+ | `selectText` | 切换到“文字”工具 | 可选 |
30
+ | `selectEraser` | 切换到“橡皮”工具 | 无 |
31
+ | `updateStyle` | 仅更新样式,不切换工具 | 必填 |
32
+
33
+ ---
34
+
35
+ ## 1) `selectSelection`
36
+
37
+ 切换到选择工具。
38
+
39
+ ```json
40
+ { "type": "selectSelection" }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 2) `selectFreedraw`
46
+
47
+ 切换到自由书写工具(普通画笔/荧光笔)。
48
+
49
+ ### payload 字段
50
+
51
+ | 字段 | 类型 | 必填 | 含义 |
52
+ | --- | --- | --- | --- |
53
+ | `variant` | `"pen" \| "highlighter"` | 否 | 画笔类型。`pen` 普通笔,`highlighter` 荧光笔(半透明) |
54
+ | `strokeWidth` | `number` | 否 | 线宽 |
55
+ | `strokeColor` | `string` | 否 | 笔颜色(如 `#1677ff`) |
56
+
57
+ ### `variant` 取值对照(freedraw)
58
+
59
+ | 值 | 含义 |
60
+ | --- | --- |
61
+ | `pen` | 普通画笔(不透明) |
62
+ | `highlighter` | 荧光笔(半透明,适合标注) |
63
+
64
+ ### 示例
65
+
66
+ ```json
67
+ {
68
+ "type": "selectFreedraw",
69
+ "payload": {
70
+ "variant": "highlighter",
71
+ "strokeWidth": 6,
72
+ "strokeColor": "#ffcc00"
73
+ }
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 3) `selectLine`
80
+
81
+ 切换到线条/箭头工具组,并可指定箭头类型。
82
+
83
+ ### payload 字段
84
+
85
+ | 字段 | 类型 | 必填 | 含义 |
86
+ | --- | --- | --- | --- |
87
+ | `variant` | `"line" \| "arrow" \| "doubleArrow" \| "outlineArrow" \| "outlineDoubleArrow"` | 否 | 线条变体:直线、单箭头、双箭头、空心单箭头、空心双箭头 |
88
+ | `strokeWidth` | `number` | 否 | 线宽 |
89
+ | `strokeColor` | `string` | 否 | 线条颜色 |
90
+
91
+ ### `variant` 取值对照(line)
92
+
93
+ | 值 | 含义 |
94
+ | --- | --- |
95
+ | `line` | 直线 |
96
+ | `arrow` | 实心单箭头 |
97
+ | `doubleArrow` | 实心双箭头 |
98
+ | `outlineArrow` | 空心单箭头 |
99
+ | `outlineDoubleArrow` | 空心双箭头 |
100
+
101
+ ### 示例
102
+
103
+ ```json
104
+ {
105
+ "type": "selectLine",
106
+ "payload": {
107
+ "variant": "outlineDoubleArrow",
108
+ "strokeWidth": 2,
109
+ "strokeColor": "#1677ff"
110
+ }
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 4) `selectShape`
117
+
118
+ 切换到形状工具组,并可指定具体形状。
119
+
120
+ ### payload 字段
121
+
122
+ | 字段 | 类型 | 必填 | 含义 |
123
+ | --- | --- | --- | --- |
124
+ | `shape` | `"rectangle" \| "diamond" \| "ellipse" \| "triangle" \| "pentagon" \| "callout" \| "hollow_arrow" \| "hollow_double_arrow"` | 否 | 具体形状。不传时默认矩形 |
125
+ | `strokeWidth` | `number` | 否 | 描边宽度 |
126
+ | `strokeColor` | `string` | 否 | 描边颜色 |
127
+ | `backgroundColor` | `string` | 否 | 填充背景色 |
128
+ | `fillStyle` | `AppState["currentItemFillStyle"]` | 否 | 填充样式(如 `solid` / `hachure` / `cross-hatch`) |
129
+
130
+ ### `shape` 取值对照
131
+
132
+ | 值 | 对应图形 |
133
+ | --- | --- |
134
+ | `rectangle` | 矩形 |
135
+ | `diamond` | 菱形 |
136
+ | `ellipse` | 椭圆 |
137
+ | `triangle` | 三角形 |
138
+ | `pentagon` | 五边形 |
139
+ | `callout` | 标注框(气泡) |
140
+ | `hollow_arrow` | 空心单箭头形状 |
141
+ | `hollow_double_arrow` | 空心双箭头形状 |
142
+
143
+ ### `fillStyle` 常见值对照
144
+
145
+ | 值 | 含义 |
146
+ | --- | --- |
147
+ | `hachure` | 斜线填充 |
148
+ | `cross-hatch` | 交叉线填充 |
149
+ | `solid` | 实心填充 |
150
+
151
+ ### 示例
152
+
153
+ ```json
154
+ {
155
+ "type": "selectShape",
156
+ "payload": {
157
+ "shape": "ellipse",
158
+ "strokeWidth": 3,
159
+ "strokeColor": "#1f1f1f",
160
+ "backgroundColor": "#E6F4FF",
161
+ "fillStyle": "solid"
162
+ }
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 5) `selectText`
169
+
170
+ 切换到文字工具,并可设置文字默认样式。
171
+
172
+ ### payload 字段
173
+
174
+ | 字段 | 类型 | 必填 | 含义 |
175
+ | --- | --- | --- | --- |
176
+ | `fontSize` | `number` | 否 | 字号 |
177
+ | `textAlign` | `"left" \| "center" \| "right"` | 否 | 对齐方式 |
178
+ | `strokeColor` | `string` | 否 | 文字颜色 |
179
+ | `bold` | `boolean` | 否 | 是否粗体 |
180
+ | `italic` | `boolean` | 否 | 是否斜体 |
181
+ | `underline` | `boolean` | 否 | 是否下划线 |
182
+ | `strikethrough` | `boolean` | 否 | 是否删除线 |
183
+
184
+ ### `textAlign` 取值对照
185
+
186
+ | 值 | 含义 |
187
+ | --- | --- |
188
+ | `left` | 左对齐 |
189
+ | `center` | 居中对齐 |
190
+ | `right` | 右对齐 |
191
+
192
+ ### 示例
193
+
194
+ ```json
195
+ {
196
+ "type": "selectText",
197
+ "payload": {
198
+ "fontSize": 24,
199
+ "textAlign": "center",
200
+ "strokeColor": "#222222",
201
+ "bold": true
202
+ }
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 6) `selectEraser`
209
+
210
+ 切换到橡皮工具。
211
+
212
+ ```json
213
+ { "type": "selectEraser" }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 7) `updateStyle`
219
+
220
+ 只更新样式,不切换当前工具。
221
+
222
+ ### payload 字段(可选子集)
223
+
224
+ 可以传递以下字段中的任意组合:
225
+
226
+ - `currentItemStrokeColor`
227
+ - `currentItemBackgroundColor`
228
+ - `currentItemFillStyle`
229
+ - `currentItemStrokeWidth`
230
+ - `currentItemOpacity`
231
+ - `currentItemStartArrowhead`
232
+ - `currentItemEndArrowhead`
233
+ - `currentItemFontSize`
234
+ - `currentItemTextAlign`
235
+ - `currentTextBold`
236
+ - `currentTextItalic`
237
+ - `currentTextUnderline`
238
+ - `currentTextStrikethrough`
239
+
240
+ ### `updateStyle` 字段含义对照
241
+
242
+ | 字段 | 含义 |
243
+ | --- | --- |
244
+ | `currentItemStrokeColor` | 当前工具的描边颜色 |
245
+ | `currentItemBackgroundColor` | 当前工具的填充背景色 |
246
+ | `currentItemFillStyle` | 当前工具的填充样式 |
247
+ | `currentItemStrokeWidth` | 当前工具线宽 |
248
+ | `currentItemOpacity` | 当前工具透明度(0-100) |
249
+ | `currentItemStartArrowhead` | 箭头起点箭头样式 |
250
+ | `currentItemEndArrowhead` | 箭头终点箭头样式 |
251
+ | `currentItemFontSize` | 文字默认字号 |
252
+ | `currentItemTextAlign` | 文字默认对齐方式 |
253
+ | `currentTextBold` | 文字是否粗体 |
254
+ | `currentTextItalic` | 文字是否斜体 |
255
+ | `currentTextUnderline` | 文字是否下划线 |
256
+ | `currentTextStrikethrough` | 文字是否删除线 |
257
+
258
+ ### 示例
259
+
260
+ ```json
261
+ {
262
+ "type": "updateStyle",
263
+ "payload": {
264
+ "currentItemStrokeWidth": 4,
265
+ "currentItemStrokeColor": "#ff4d4f"
266
+ }
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 接入示例(白板端)
273
+
274
+ ```ts
275
+ socket.on("toolbar-command", (command) => {
276
+ excalidrawAPI?.leftToolbar.dispatch(command);
277
+ });
278
+ ```
279
+
280
+ ## 完整命令 JSON 示例集合
281
+
282
+ 以下示例可直接用于联调(通过 socket 发送):
283
+
284
+ ### 1) 选择工具
285
+
286
+ ```json
287
+ { "type": "selectSelection" }
288
+ ```
289
+
290
+ ### 2) 自由书写(普通画笔)
291
+
292
+ ```json
293
+ {
294
+ "type": "selectFreedraw",
295
+ "payload": {
296
+ "variant": "pen",
297
+ "strokeWidth": 2,
298
+ "strokeColor": "#1677ff"
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### 3) 自由书写(荧光笔)
304
+
305
+ ```json
306
+ {
307
+ "type": "selectFreedraw",
308
+ "payload": {
309
+ "variant": "highlighter",
310
+ "strokeWidth": 6,
311
+ "strokeColor": "#ffcc00"
312
+ }
313
+ }
314
+ ```
315
+
316
+ ### 4) 线条(空心双箭头)
317
+
318
+ ```json
319
+ {
320
+ "type": "selectLine",
321
+ "payload": {
322
+ "variant": "outlineDoubleArrow",
323
+ "strokeWidth": 2,
324
+ "strokeColor": "#1f1f1f"
325
+ }
326
+ }
327
+ ```
328
+
329
+ ### 5) 形状(椭圆 + 实心填充)
330
+
331
+ ```json
332
+ {
333
+ "type": "selectShape",
334
+ "payload": {
335
+ "shape": "ellipse",
336
+ "strokeWidth": 3,
337
+ "strokeColor": "#1f1f1f",
338
+ "backgroundColor": "#E6F4FF",
339
+ "fillStyle": "solid"
340
+ }
341
+ }
342
+ ```
343
+
344
+ ### 6) 文字(24号,居中,加粗)
345
+
346
+ ```json
347
+ {
348
+ "type": "selectText",
349
+ "payload": {
350
+ "fontSize": 24,
351
+ "textAlign": "center",
352
+ "strokeColor": "#222222",
353
+ "bold": true
354
+ }
355
+ }
356
+ ```
357
+
358
+ ### 7) 橡皮
359
+
360
+ ```json
361
+ { "type": "selectEraser" }
362
+ ```
363
+
364
+ ### 8) 仅更新样式(不切工具)
365
+
366
+ ```json
367
+ {
368
+ "type": "updateStyle",
369
+ "payload": {
370
+ "currentItemStrokeWidth": 4,
371
+ "currentItemStrokeColor": "#ff4d4f"
372
+ }
373
+ }
374
+ ```
375
+
376
+ ## 注意事项
377
+
378
+ - 命令建议使用 JSON 格式,便于跨端传输和记录日志
379
+ - 白板端建议做一次类型校验后再 `dispatch`
380
+ - 建议在“进入注释模式”时再接收命令,退出时解绑监听
@@ -0,0 +1,170 @@
1
+ # Remote Annotation Integration Guide
2
+
3
+ 本文档用于说明“白板端”和“控制端”完全隔离时,如何通过命令实现远程注释控制。
4
+
5
+ ## 目标
6
+
7
+ - 白板端只负责渲染 `Excalidraw` 并执行命令
8
+ - 控制端只负责发送工具命令(不直接操作白板实例)
9
+ - 两端通过 `socket`(或其他消息通道)通信
10
+
11
+ ---
12
+
13
+ ## 一、白板端如何接入
14
+
15
+ 白板端核心职责:
16
+
17
+ 1. 挂载 `Excalidraw`,拿到 `excalidrawAPI`
18
+ 2. 监听远端命令消息
19
+ 3. 收到命令后执行 `excalidrawAPI.leftToolbar.dispatch(command)`
20
+
21
+ ### 示例(React + socket)
22
+
23
+ ```tsx
24
+ import { useEffect, useState } from "react";
25
+ import Excalidraw from "@excalidraw/excalidraw";
26
+ import type {
27
+ ExcalidrawImperativeAPI,
28
+ LeftToolbarCommand,
29
+ } from "@excalidraw/excalidraw";
30
+
31
+ type SocketLike = {
32
+ on: (event: string, cb: (data: unknown) => void) => void;
33
+ off: (event: string, cb: (data: unknown) => void) => void;
34
+ };
35
+
36
+ const isLeftToolbarCommand = (v: unknown): v is LeftToolbarCommand => {
37
+ if (!v || typeof v !== "object") return false;
38
+ const type = (v as { type?: unknown }).type;
39
+ return (
40
+ typeof type === "string" &&
41
+ [
42
+ "selectSelection",
43
+ "selectFreedraw",
44
+ "selectLine",
45
+ "selectShape",
46
+ "selectText",
47
+ "selectEraser",
48
+ "updateStyle",
49
+ ].includes(type)
50
+ );
51
+ };
52
+
53
+ export default function WhiteboardPage({ socket }: { socket: SocketLike }) {
54
+ const [api, setApi] = useState<ExcalidrawImperativeAPI | null>(null);
55
+
56
+ useEffect(() => {
57
+ const handler = (data: unknown) => {
58
+ if (!isLeftToolbarCommand(data)) return;
59
+ api?.leftToolbar.dispatch(data);
60
+ };
61
+
62
+ socket.on("toolbar-command", handler);
63
+ return () => socket.off("toolbar-command", handler);
64
+ }, [socket, api]);
65
+
66
+ return (
67
+ <Excalidraw
68
+ excalidrawAPI={setApi}
69
+ showToolBar={false}
70
+ // 透明叠加在 PPT 场景可按需开启:
71
+ // viewModeEnabled={false}
72
+ />
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### 白板端注意事项
78
+
79
+ - 建议对命令先做类型校验再执行
80
+ - 退出注释模式时及时解绑 socket 监听
81
+ - 若需“只读观看模式”,可以结合 `viewModeEnabled`
82
+
83
+ ---
84
+
85
+ ## 二、控制端如何调用命令
86
+
87
+ 控制端核心职责:
88
+
89
+ 1. 监听本地工具条交互(按钮、颜色、线宽、形状等)
90
+ 2. 将操作转换成 `LeftToolbarCommand`
91
+ 3. 通过 socket 发给白板端
92
+
93
+ ### 示例(发送命令)
94
+
95
+ ```ts
96
+ import type { LeftToolbarCommand } from "@excalidraw/excalidraw";
97
+
98
+ type SocketLike = {
99
+ emit: (event: string, data: unknown) => void;
100
+ };
101
+
102
+ const sendToolbarCommand = (
103
+ socket: SocketLike,
104
+ command: LeftToolbarCommand,
105
+ ) => {
106
+ socket.emit("toolbar-command", command);
107
+ };
108
+
109
+ // 切换到荧光笔
110
+ sendToolbarCommand(socket, {
111
+ type: "selectFreedraw",
112
+ payload: { variant: "highlighter", strokeWidth: 6, strokeColor: "#ffcc00" },
113
+ });
114
+
115
+ // 切换到线条:空心双箭头
116
+ sendToolbarCommand(socket, {
117
+ type: "selectLine",
118
+ payload: { variant: "outlineDoubleArrow", strokeWidth: 2, strokeColor: "#1677ff" },
119
+ });
120
+
121
+ // 切换到形状:椭圆
122
+ sendToolbarCommand(socket, {
123
+ type: "selectShape",
124
+ payload: { shape: "ellipse", strokeWidth: 3, strokeColor: "#1f1f1f" },
125
+ });
126
+
127
+ // 仅更新样式(不切工具)
128
+ sendToolbarCommand(socket, {
129
+ type: "updateStyle",
130
+ payload: { currentItemStrokeWidth: 4, currentItemStrokeColor: "#ff4d4f" },
131
+ });
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 三、推荐事件约定
137
+
138
+ 建议统一使用一个事件名:
139
+
140
+ - `toolbar-command`
141
+
142
+ 消息体直接使用 `LeftToolbarCommand` JSON。
143
+
144
+ 这样控制端和白板端都容易调试,也便于后续扩展(录制/回放命令流)。
145
+
146
+ ---
147
+
148
+ ## 四、常见问题
149
+
150
+ ### 1) 为什么控制端不能直接调用白板方法?
151
+
152
+ 因为是跨端隔离部署,控制端拿不到白板实例。必须通过消息通道转发命令。
153
+
154
+ ### 2) 一定要用 socket 吗?
155
+
156
+ 不是。只要能传 JSON 的通道都可以:`WebSocket`、`postMessage`、Electron IPC 等。
157
+
158
+ ### 3) 如何保证命令安全?
159
+
160
+ - 白板端做类型校验
161
+ - 只允许白名单命令类型
162
+ - 必要时增加鉴权和房间隔离
163
+
164
+ ---
165
+
166
+ ## 五、下一步建议
167
+
168
+ - 接入日志:记录每次命令时间戳,方便排查问题
169
+ - 接入回放:按命令时间线回放注释过程
170
+ - 接入权限:区分“可控白板”的角色与“只读观众”角色