OpenMAIC 只读幻灯片渲染包 @openmaic/renderer:从设计稿到 React 接入的完整实战指南
2026/9/10 13:50:50 网站建设 项目流程

OpenMAIC 只读幻灯片渲染包 @openmaic/renderer:从设计稿到 React 接入的完整实战指南

【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC

导读

本指南以 OpenMAIC 仓库中 packages/@openmaic/renderer/DESIGN.md 设计稿为主线,完整讲解@openmaic/renderer(规划名maic-renderer)这一只读画布渲染包的诞生动机、八大设计决策、对外 API 与源码组织,并结合包内真实实现(SlideCanvasSlideRendererProvider、九个Base*Element、四类播放特效)给出可直接复制运行的接入示例。读完本文,你将掌握:如何在任意 React + Tailwind 4 项目中"装包 → 传 Slide JSON → 渲染出 OpenMAIC 同款幻灯片画布",以及如何通过renderImage/renderVideo插槽把业务媒体管线注入只读画布。

1. 设计目标:把只读画布抽成独立 workspace 包

设计稿开篇即明确了 v1 的核心目标:把 OpenMAIC 主仓components/maic-renderer/中的只读画布部分抽成独立 workspace 包,让任意 React + Tailwind 4 项目都能"装包 → 传 Slide → 直接渲染"

v1 的范围被严格限定为只读画布(设计稿中对应主仓Editor/ScreenCanvas.tsx子树),而编辑能力(Editor/Canvas/*Operate/*、ProseMirror 实时编辑器)整体留给 v2。这一"只读先行"的取舍基于三条理由:

  1. 闭环最小可用:与pptxtojson-pro形成"产 Slide → 渲 Slide"的对子,打通端到端演示链路;
  2. 编辑器接口面难定型:选中态、命令、撤销、剪贴板、协作等接口一旦定下再改代价极高,第一版强行定型大概率返工;
  3. 只读包本身有独立价值:可以独立用于课件嵌入、报告展示、课堂回放、PDF 导出预览等场景。

从当前仓库的目录结构看,这个设计已经落地为独立的 workspace 包packages/@openmaic/renderer,包名@openmaic/renderer,版本0.1.4(见 package.json)。包内包含完整的src/源码、test/测试、rollup.config.js构建配置与README.md/DESIGN.md/FONTS.md文档。其配套的@openmaic/importer(对应设计稿中的pptxtojson-pro)则负责将.pptx转为同样的Slide[]结构,实现.pptx → @openmaic/renderer的端到端渲染。

2. 八大设计决策(D1–D8)

设计稿用一张决策表锁定了包的架构方向,当前实现几乎逐条兑现:

#决策点选择落地情况(源码证据)
D1范围分两步:v1 只读 + v2 编辑包内只有SlideCanvas等只读组件;README.md 明确"editing lives in the separate@openmaic/editorpackage"
D2数据 API纯 Props 主;同时导出可选 ProviderSlideCanvas读取全部数据于 props;context.tsx 提供SlideRendererProvider+useSlideContext
D3样式要求消费者使用 Tailwind 4package.jsontailwindcss >= 4列为 peerDependency;README 强调"emits Tailwind 4 arbitrary-value classes"
D4特效层包含,4 个全做成可选 props,默认关effects.ts 定义 laser/spotlight/highlight/zoom,SlideCanvas默认不启用
D5业务耦合元素暴露renderImage/renderVideo插槽,包内默认渲原生标签BaseImageElement.tsx 默认渲染纯<img>,插槽返回值具有权威性
D6i18n包不带文案包内无任何 UI 文案,alt=""等均由消费者自行处理
D7ProseMirrorv1 不依赖package.json依赖表中没有 ProseMirror 系列
D8包发布workspace 包,未来可发 npm已作为@openmaic/renderer发布,publishConfig.access: public

其中 D2 的实现非常典型:SlideCanvas通过useOptionalSlideContext()先尝试读取上下文,props 优先、context 兜底(props.slide ?? ctx?.slide),既保证了纯 props 模式的上手速度,也兼顾了需要共享 slide 数据的进阶场景。

3. 对外 API:三层使用方式

设计稿将对外 API 分为三层,源码中的 src/index.ts 是这三层的总出口。

3.1 主入口:<SlideCanvas />

设计稿规划的 props 在 SlideCanvas.tsx 中已完整实现,且在实际落地时又扩充了若干面向生产场景的选项。完整签名如下:

import { SlideCanvas, type SlideCanvasProps } from '@openmaic/renderer'; interface SlideCanvasProps { /** 单页幻灯片数据(PPTist 风格),置于 Provider 内时可省略 */ slide?: Slide; /** 画布缩放;省略时按容器自动适配(见 useViewportSize) */ scale?: number; /** 自动适配时画布占父容器的百分比,默认 100 */ canvasPercentage?: number; /** 自动适配计算出的 scale 通过该回调上报 */ onScaleChange?: (scale: number) => void; /** 覆盖 slide.background */ background?: SlideBackground; /** 4 个播放特效,默认全关 */ effects?: SlideEffects; /** 图片渲染插槽:可注入 placeholder / retry 逻辑 */ renderImage?: (el, resolvedSrc, defaultContent) => ReactNode; /** 视频渲染插槽 */ renderVideo?: (el) => ReactNode; /** 文本/形状标签/表格内容插槽(源码中额外补充) */ renderText?: (el, defaultContent) => ReactNode; renderShapeLabel?: (el, defaultContent) => ReactNode; renderTable?: (el, defaultContent) => ReactNode; /** 允许视频控件或自定义视频 UI 接收指针事件,默认 true */ videoInteractive?: boolean; /** 元素点击回调 */ onElementClick?: (el, event: React.MouseEvent) => void; /** 元素根 DOM id 前缀,默认 'slide-element-' */ elementIdPrefix?: string; /** 拖拽偏移(编辑面移动手势使用) */ dragOffsets?: ReadonlyMap<string, { x: number; y: number }>; /** 隐藏指定元素 id(不渲染且不参与特效定位) */ hiddenElementIds?: readonly string[]; /** 卡片式外壳(阴影+圆角),默认 true;快照管线应传 false 以精确还原 PPT 边缘 */ chrome?: boolean; className?: string; style?: React.CSSProperties; }

几个容易忽略但很实用的细节:

  • 自动适配:省略scale时,useViewportSize.ts 依据slide.viewportSize(设计稿宽,默认 1000)与slide.viewportRatio(宽高比,默认 0.5625 即 16:9),用ResizeObserver监听容器尺寸变化并居中适配;容器高宽比与画布不一致时自动取"宽限"或"高限"中较紧的一边。
  • chrome开关:默认为画布外层叠加 1px 淡描边 + 圆角(0 0 0 1px rgba(0,0,0,0.01), 0 0 12px 0 rgba(0,0,0,0.1))。快照/导出场景应传false,否则html2canvas会把这层阴影和 0.5rem 圆角烘焙进 PNG,导致与原 PPT 边缘比对时出现假性差异。
  • hiddenElementIds:用于在渲染层直接过滤元素(源码用useMemo+Set过滤),可服务于"渐进披露"等播放编排需求。

3.2 Provider 高阶模式

当需要多个兄弟层共享同一份 slide 数据时,使用SlideRendererProvider包裹,子组件通过useSlideContext()取值:

import { SlideRendererProvider, SlideCanvas, useSlideContext } from '@openmaic/renderer'; function MyAnnotationLayer() { const { slide, scale } = useSlideContext(); return <div>Annotations for {slide.id} at scale {scale}</div>; } export default function Demo() { return ( <SlideRendererProvider slide={slide} scale={0.9}> <SlideCanvas /> {/* slide/scale 从 context 读取 */} <MyAnnotationLayer /> </SlideRendererProvider> ); }

实现上,context.tsx 定义SlideContextValue(slide、scale、background、effects、各渲染插槽、videoInteractiveonElementClick),SlideRendererProvider将其整体作为 context value 下发。useSlideContext()在 Provider 外调用会抛出明确错误;useOptionalSlideContext()则返回null,供SlideCanvas这类"可选消费"场景使用。值得注意:Provider 的 props 与SlideCanvas的 props 一一对应,意味着插槽函数也可以经由 context 向下传递,适合大型应用在顶层统一注入媒体行为。

3.3 元素子组件:细粒度复用

设计稿规划的 9 个基础元素组件在 elements/index.ts 中全部导出,并在实现时额外补充了BaseAudioElement

import { BaseTextElement, BaseShapeElement, BaseImageElement, BaseLineElement, BaseChartElement, BaseLatexElement, BaseTableElement, BaseVideoElement, BaseCodeElement, BaseAudioElement, // 实现时补充 ElementOutline, // 共享描边 useElementFill, useElementOutline, useElementShadow, useElementFlip, } from '@openmaic/renderer/elements';

每个Base*Element统一接收{ elementInfo: PPTXxxElement };图片/视频元素额外接收渲染插槽。这些组件可以脱离SlideCanvas单独使用,用于自建布局。此外,SlideElement(SlideElement.tsx)作为画布内部的"元素分派器",依据elementInfo.type(IMAGE/TEXT/SHAPE/LINE/CHART/LATEX/TABLE/VIDEO/AUDIO/CODE)分发到对应组件,整个组件树用memo包裹以隔离元素级重渲染,未传入onElementClick时元素默认pointerEvents: none,保持纯展示性能。

3.4 类型导出

设计稿列出的类型在@openmaic/renderer/types子路径完整提供:

import type { Slide, PPTElement, SlideBackground, SlideTheme, PPTTextElement, PPTShapeElement, PPTImageElement, PPTLineElement, PPTChartElement, PPTLatexElement, PPTTableElement, PPTVideoElement, PPTCodeElement, ImageElementClip, ImageElementFilters, Gradient, GradientType, PPTElementOutline, PPTElementShadow, SlideEffects, LaserEffectOptions, SpotlightEffectOptions, HighlightEffectOptions, ZoomEffectOptions, } from '@openmaic/renderer/types';

设计稿中的幻灯片数据结构类型在实现中从@/lib/types/slides拷贝并收敛到@openmaic/dsl包(见package.json的 dependencies),由@openmaic/dsl统一维护Slide/PPTElement族,避免多包重复定义导致的类型漂移——这正是设计稿"风险"一节中预测的演进方向。

4. 包结构与源码组织

设计稿规划的目录树与当前实现基本一致,关键差异是包名从maic-renderer定稿为@openmaic/renderer,并新增了audio元素、snapshot快照子路径与styles.ts。当前 packages/@openmaic/renderer 的源码结构如下:

packages/@openmaic/renderer/ ├── package.json # name: @openmaic/renderer, v0.1.4 ├── README.md / DESIGN.md / FONTS.md ├── fonts.config.mjs # 字体白名单与 CDN 地址配置 ├── rollup.config.js # 构建:rollup -c && tsc --emitDeclarationOnly ├── src/ │ ├── index.ts # 主入口(导出 SlideCanvas / Provider / effects / hooks / utils / types) │ ├── SlideCanvas.tsx # 只读画布主组件 │ ├── SlideElement.tsx # 元素分派器(memo 化) │ ├── context.tsx # SlideRendererProvider + useSlideContext │ ├── styles.ts # 注入画布的全局样式(SLIDE_RENDERER_STYLES) │ ├── effects/ # HighlightOverlay / SpotlightOverlay / LaserOverlay / ZoomWrapper │ ├── elements/ # 9+1 个 Base*Element 与共享 hooks │ ├── hooks/ # useSlideBackgroundStyle / useViewportSize │ ├── snapshot/ # 快照:katex-fonts-embed / measure / index │ ├── types/ # effects.ts 与 slides 类型 │ └── utils/ # cn / geometry / element / richText └── test/ # SlideCanvas、SlideElement、各元素与特效的 vitest 用例

与设计稿一一对应的是effects/elements/hooks/utils/四个核心目录:

  • elements/下每个元素一个子目录:text/shape/(含GradientDefs.tsxPatternDefs.tsx)、image/(含ImageOutline.tsxuseClipImage.tsuseFilter.tsclipPaths.ts)、line/chart/latex/table/video/code/audio/,以及共享的ElementOutline.tsxuseElementFill.tsuseElementOutline.tsuseElementShadow.tsuseElementFlip.ts
  • effects/四个文件对应四类播放特效;
  • utils/geometry.ts正是设计稿 7.1 中"从findElementGeometry(...)拷贝进来"的工具,index.ts对外导出findElementGeometryfindNearestCornergetElementPercentageGeometry

5. 依赖与构建

5.1 依赖声明

package.json中的依赖与设计稿高度吻合,并标注了可选 peer:

  • peerDependencies(消费者需自行安装):react >= 18react-dom >= 18motion >= 11tailwindcss >= 4echarts >= 5shiki >= 1.0.0标记为optional(仅在幻灯片含 chart / code 元素时才需要,见 README.md);
  • dependencies(运行时必备):@openmaic/dsl(Slide 类型与常量)、katex(LaTeX 渲染)、tinycolor2(颜色工具)、lucide-react(图标)、clsx+tailwind-mergecnhelper),另含html-to-imagehtml2canvas-pro(快照/导出辅助);
  • ProseMirror 系列:如设计稿 D7 所承诺,v1 完全不依赖。

5.2 构建与产物

构建命令与设计稿一致(rollup -c && tsc --emitDeclarationOnly),实际package.json中的build脚本还增加了两步字体 CSS 生成:

pnpm --filter @openmaic/renderer build # = node scripts/generate-fonts-css.mjs # && node scripts/generate-katex-fonts.mjs # && rm -rf dist && rollup -c && tsc --emitDeclarationOnly --declarationDir dist

产物通过exports字段对外暴露五个入口:.(主入口)、./elements./types./snapshot./fonts.css,每个 JS 入口同时产出index.js(ESM)与.d.ts类型声明。测试用 vitest 运行(pnpm test),prepublishOnly会在发布前自动执行构建。

6. 关键解耦点:从主仓画布到独立包

设计稿第 7 节用表格明确了"原实现 → 包内实现"的映射关系,这也是理解包架构的钥匙。

6.1 ScreenCanvas → SlideCanvas:全局 store 换成 props

设计稿列出了五组替换关系,核心是把 zustand 全局状态全部改为 props 驱动:

原(主仓 store)新(包内 props)
useCanvasStore.use.canvasScale()props.scale ?? 1
useSceneSelector(c => c.canvas.elements)props.slide.elements
useSceneSelector(c => c.canvas.background)props.background ?? props.slide.background
useCanvasStore.use.laserElementId/Options()props.effects?.laser
useCanvasStore.use.zoomTarget()props.effects?.zoom
findElementGeometry(...)拷贝进utils/geometry.ts

在 SlideCanvas.tsx 中可以看到"零全局状态"的兑现:画布只做纯函数式派生(visibleElementselementIndexByIdlaserGeometryzoomGeometry均为useMemo派生),并注释说明了在 React Compiler 构建下这些派生会自动 memo 化。

6.2 ScreenElement → SlideElement:主题从 props 读取

theme.fontColor/theme.fontName从 slide 数据读取(props.slide.theme),移除useSceneSelectorSlideElement实现中还提供了DEFAULT_THEME#333333/Microsoft YaHei)作为无主题时的兜底。

6.3 BaseImageElement:退化成纯<img>+ 插槽注入

这是最核心的解耦。设计稿要求"包内退化成纯<img>版本(含 clip/filter/outline/shadow),所有 placeholder / retry / i18n 逻辑剥离"。当前 BaseImageElement.tsx 的实现细节:

  • 默认内容为原生<img>,带loading="lazy"decoding="async"(服务缩略图/侧栏等大量挂载场景),拖拽被preventDefault禁止;
  • 通过useClipImageuseFilterImageOutlineuseElementShadowuseElementFlip组装裁剪路径、滤镜、描边、阴影、翻转;
  • 额外支持softEdge羽化(两条线性渐变 mask 交叉,四个边缘渐隐)与colorMask色彩蒙层——注释说明html2canvas-pro忽略 CSS mask,因此slideToPng快照路径通过data-soft-edge钩子把羽化烘焙进像素;
  • renderImage插槽收到(element, resolvedSrc, defaultContent),其中defaultContent是已经过裁剪/滤镜/羽化/蒙层处理的完整默认渲染,插槽返回值具有权威性,返回null可刻意隐藏图片

6.4 BaseVideoElement 与其余元素

BaseVideoElement同样退化为纯<video>,业务行为由renderVideo注入;videoInteractive控制视频控件是否接收指针事件。其余Base*Element只是把类型导入路径从@/lib/types/slides改为@openmaic/dsl,无业务耦合。

7. 四类播放特效:OpenMAIC 同款交互还原

设计稿 D4 将特效层纳入包内但默认零负担。类型定义见 effects.ts:

export interface LaserEffectOptions { elementId: string; color?: string; duration?: number; } export interface SpotlightEffectOptions { elementId: string; dimness?: number; } export interface HighlightEffectOptions { elementId: string; color?: string; opacity?: number; borderWidth?: number; animated?: boolean; } export interface ZoomEffectOptions { elementId: string; scale: number; } export interface SlideEffects { laser?: LaserEffectOptions; spotlight?: SpotlightEffectOptions; highlight?: HighlightEffectOptions; highlights?: HighlightEffectOptions[]; // 支持同时高亮多个元素 zoom?: ZoomEffectOptions; }

接入方式:

<SlideCanvas slide={slide} effects={{ laser: { elementId: 't1', color: '#ff3b30' }, spotlight: { elementId: 't1' }, highlight: { elementId: 't1', color: '#ff6b6b', animated: true }, zoom: { elementId: 't1', scale: 1.5 }, }} />

实现要点(均有源码佐证):

  • 激光笔(LaserOverlay.tsx):默认颜色#ff3b30、默认时长 3000ms。用motion实现"从画布对角飞入 → 到达目标元素中心 → 呼吸脉冲 → 淡出"的动画,初始位置根据元素几何中心(百分比坐标)自动选择从左上或右下进入;
  • 聚焦光斑(SpotlightOverlay):目标元素外压暗(dimness可调),用ResizeObserver感知尺寸变化;
  • 高亮(HighlightOverlay):支持单元素(highlight)与多元素(highlights数组),可设颜色、透明度、边框宽与是否动画;
  • 缩放(ZoomWrapper / 画布级 transform):SlideCanvas根据zoomGeometry的百分比中心设置transformOrigin,再对画布整体做scale()变换并配 700ms 的 transform 过渡。

特效定位依赖findElementGeometry把元素的像素坐标换算为百分比几何(PercentageGeometry),因此缩放/旋转等变换后的元素也能被准确命中。

8. 接入实战:三步跑通只读画布

8.1 安装与 Tailwind 4 配置

pnpm add @openmaic/renderer # 或 npm install @openmaic/renderer

由于包输出的是 Tailwind 4 arbitrary-value 类,必须在tailwind.config.{ts,js}content中包含包产物目录:

export default { content: [ './src/**/*.{ts,tsx}', './node_modules/@openmaic/renderer/dist/**/*.{js,cjs}', ], };

8.2 最小可运行示例

以 README.md 的 Quickstart 为基础(并补全类型字段使其更贴近@openmaic/dsl的真实结构):

import { SlideCanvas, type Slide } from '@openmaic/renderer'; const slide: Slide = { id: 'demo-1', viewportSize: 1000, // 画布设计宽度 viewportRatio: 0.5625, // 16:9 theme: { backgroundColor: '#ffffff', themeColors: ['#5b8def'], fontColor: '#222222', fontName: 'sans-serif', }, elements: [ { type: 'text', id: 't1', left: 100, top: 80, width: 800, height: 60, rotate: 0, content: '<p>Hello, Slide</p>', defaultFontName: 'sans-serif', defaultColor: '#222', }, ], background: { type: 'solid', color: '#ffffff' }, }; export default function Demo() { return ( <div style={{ width: 800, height: 450 }}> <SlideCanvas slide={slide} /> </div> ); }

关键约定:父容器必须具有确定的宽高SlideCanvas外层容器width/height: 100%,内层画布依据viewportSize × viewportRatio自动适配并居中;省略scale即进入自动适配模式。

8.3 注入业务媒体行为

真实业务中图片可能处于"占位 → 生成中 → 就绪"状态,用renderImage插槽接管即可:

<SlideCanvas slide={slide} renderImage={(el, src, defaultContent) => src.startsWith('placeholder:') ? <MyPlaceholder taskId={src} /> : defaultContent } />

defaultContent已包含裁剪、滤镜、羽化、colorMask与阴影等全部画布级处理,业务侧只需在需要时覆盖。

8.4 中文字体(可选但建议了解)

PowerPoint 导入的幻灯片常引用本机未安装的中文字体。包提供fonts.css,声明 6 个可再分发字体的@font-face(思源黑体、思源宋体、霞鹜文楷、朱雀仿宋、站酷快乐体、文鼎 PL 简中楷),在应用入口导入一次即可:

import '@openmaic/renderer/fonts.css';

需要注意(README 与 FONTS.md 均有明确警告):@font-facesrcURL 指向外部字体托管https://file.maic.chat/fonts/<name>.woff2woff2 文件并不打包在包内,因此该导入是一份硬运行时依赖——托管地址必须可达且允许 CORS,否则浏览器会静默回退系统字体(不报错,但字形/度量不同)。如需私有 CDN 或离线内网,可修改fonts.config.mjs中的FONT_CDN_BASE_URL并运行pnpm run genfonts重新生成。不导入该 CSS 也能正常渲染(回退系统字体)。各字体的许可与再分发条件记录在 FONTS.md 中,含 OFL 1.1 文本与文鼎 Public License 全文。

8.5 快照与导出

package.jsonexports与 src/snapshot/ 目录可以看出,包还提供@openmaic/renderer/snapshot子路径(含 katex 字体嵌入与 DOM 测量工具),配合html2canvas-pro/html-to-image依赖支撑幻灯片截图为 PNG 的场景。快照管线传入chrome={false}可避免阴影与圆角被烘焙进输出(见SlideCanvasProps.chrome的源码注释)。

9. 验收标准、v2 预告与风险清单

9.1 验收标准

设计稿第 8 节定义了三条验收标准,均可在当前仓库核验:

  1. pnpm --filter @openmaic/renderer build通过并产出干净的dist/(含index.jselements/index.jstypes/index.js及对应.d.ts);
  2. 用该包渲染手写 Slide 时,视觉效果与主仓ScreenCanvas一致;
  3. v1 不替换主仓既有调用——设计稿明确本次任务不动主仓页面,主仓继续使用自己的BaseImageElement,因此"砍掉业务逻辑"不会影响既有功能。

9.2 v2 预告

设计稿预告的 v2 编辑能力包括:Editor/Canvas/*抽入包内editing/、ProseMirror 实时编辑器、选中/拖拽/resize/旋转/裁剪/对齐线/标尺、撤销/重做/剪贴板,以及新的 API 形态<SlideEditor slide editable onChange={...} />。从 README.md 看,编辑能力已独立到@openmaic/editor包(0.1.0起移除实验性的@openmaic/renderer/editing子路径),只读的<SlideCanvas>API 保持稳定。

9.3 风险与缓解(设计稿原文)

风险等级缓解
Chart 库依赖不明,包体积可能膨胀实现阶段已确认:echartsshiki标记为optional peer,仅在用到对应元素时才安装
Tailwind 4 强依赖筛掉很多消费者README 显著说明;未来版本再考虑编译 CSS
主仓Slide类型未来演进,包内拷贝版会漂移实现阶段已通过@openmaic/dsl三方共享,从源头消除漂移
BaseImageElement 砍业务后主仓现有页面会丢功能无影响v1 不替换主仓调用,主仓继续使用自己的实现

可以看到,"低风险"的两项(Chart 依赖、类型漂移)在实现阶段已按缓解方案落地:echarts/shiki改为 optional peer,Slide类型收敛到@openmaic/dsl

10. 结语

@openmaic/renderer是 OpenMAIC"产 Slide → 渲 Slide"闭环中的渲染半边:它以设计稿 DESIGN.md 的八大决策为骨架,落地为一个零全局状态、纯 Props 驱动、特效默认零负担、媒体行为由插槽注入的只读画布包。对开发者而言,从 README.md 的三步 Quickstart 即可接入;想要深入原理,可按SlideCanvas(SlideCanvas.tsx)→ 元素分派(SlideElement.tsx)→ 各Base*Element(elements/index.ts)→ 特效层(effects.ts)的路径阅读源码与 test/ 目录下的测试用例,即可获得"课件嵌入、报告展示、课堂回放、PDF 导出预览"等场景的完整技术储备。

【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询