☰
Zag.js Tour 组件状态机完全解析:从 CHANGELOG 看引导式走查(Product Tour)的演进与实现
2026/10/14 6:16:07 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】zag

Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines

项目地址:https://gitcode.com/gh_mirrors/za/zag
点击查看免费下载

本文以@zag-js/tour包的 CHANGELOG.md 为时间主线,结合 tour.machine.ts、tour.types.ts、tour.connect.ts 等源码,系统梳理 Zag.js 引导式走查组件从 0.37.0 诞生到 1.46.0 的完整演进,并深入讲解其状态机设计、步骤类型、动作系统与各版本修复背后的工程细节。读完本文,你将能够理解 Tour 状态机的内部工作机制,掌握tooltip / dialog / wait / floating四种步骤类型与next / prev / dismiss / skip / goto动作系统的正确用法,并学会规避历史版本中踩过的定位、焦点、无障碍等常见坑。

一、@zag-js/tour 是什么

@zag-js/tour是 Zag.js 中以有限状态机(finite state machine)实现的引导式走查(guided tour / product tour / walkthrough)组件核心逻辑,用于在页面上一步步高亮目标元素、配合遮罩(backdrop)与聚光灯(spotlight)引导用户完成操作流程。包自身定位为"core logic for the tour widget implemented as a state machine"(见 package.json)。

从依赖关系看,Tour 机器直接复用了 Zag.js 生态的多个底层能力(见 package.json 的dependencies):

依赖包在 Tour 中的职责
@zag-js/core状态机运行时(createMachine)
@zag-js/anatomy组件各部分(parts)的 DOM 结构声明
@zag-js/popper步骤浮层定位(placement、offset、arrow)
@zag-js/dismissable可关闭分支管理(dismissable branch)
@zag-js/interact-outside点击/聚焦外部时的关闭行为
@zag-js/focus-trap步骤内容与聚光灯目标的焦点陷阱
@zag-js/dom-query/@zag-js/utils/@zag-js/typesDOM 查询、工具函数与类型定义

这些依赖在整个 CHANGELOG 中反复以 "Updated dependencies" 出现,是理解 Tour 行为的重要线索:Tour 的许多修复(定位抖动、焦点限制、外部交互关闭等)实际受益于底层包的同步升级。

二、版本演进时间线:从诞生到 1.46.0

CHANGELOG 完整记录了 Tour 机器从引入到稳定的历程,可按三个阶段理解:

2.1 诞生期(0.37.0 → 0.71.0):从"新组件"到"不再携带源码"

  • 0.37.0(引入):通过 PR #1253 引入全新的 guided tour 组件,同时依赖@zag-js/dismissable、@zag-js/popper、@zag-js/anatomy、@zag-js/core等包。这是 Tour 的起点。
  • 0.37.1:暴露splitProps函数,便于在 Zag.js 之上创建自定义组件。
  • 0.58.0:所有机器统一导出Service类型(用于基于 Lit 的组件),并确保invalid、required、readOnly等表单相关属性的应用一致。
  • 0.71.0:包不再发布src文件("We no longer shipsrcfiles in the packages"),转向只发布构建产物,这是发布策略的一次收敛。

2.2 重构期(0.72.0 → 0.82.x):面向健壮性的重新设计

  • 0.72.0(Minor):这是 Tour 演进中最关键的一次重构——"Redesign tour to be more robust and support programmatic control in wait steps"(CHANGELOG)。从此 Tour 支持在wait步骤中以编程方式控制流程,后续版本的大量修复都围绕这次重构展开。
  • 0.81.1:移除元素上的zIndex设置,改为交由用户层控制,避免层叠上下文冲突。
  • 0.82.0:修复 dialog 步骤的 z-index 与内容不同步的问题。

2.3 稳定期(1.0.0 → 1.46.0):细节打磨与可访问性修复

1.0.0 之后 Tour 进入语义化版本阶段,修复集中在定位、焦点、无障碍、滚动与类型导出等方向,具体见下文各节。

三、状态机架构:TourSchema 与四个运行子状态

Tour 的状态机定义在 tour.machine.ts,其状态类型在 tour.types.ts 的TourSchema中声明:

export interface TourSchema { tag: "open" | "closed" state: "tourInactive" | "running.resolving" | "running.scrolling" | "running.waiting" | "running.active" props: RequiredBy<TourProps, PropsWithDefault> context: PrivateContext refs: Refs computed: ComputedContext event: EventObject action: string guard: string effect: string }

顶层状态为tourInactive(标签closed)与running。running内又划分为四个子状态,形成 Tour 的核心工作流:

running ├── resolving (等待/解析目标元素,标签 closed) ├── scrolling (滚动到目标,标签 open) ├── waiting (wait 步骤,等待 effect 触发下一步,标签 closed) └── active (步骤展示中,标签 open)

各子状态的职责与切换逻辑(见 tour.machine.ts):

子状态标签进入时的 effects说明
resolvingclosedwaitForTarget、waitForTargetTimeout通过MutationObserver等待目标元素出现;3 秒超时后发送TARGET.NOT_FOUND
scrollingopenscrollToTarget等 7 个 effects滚动到目标,等待SCROLL.END后进入active
waitingclosed无wait 步骤专用,由effect通过next()等编程控制
activeopen6 个 effects展示步骤内容,含焦点陷阱、placement 跟踪、外部交互跟踪等

状态切换通过事件驱动,核心事件包括:

  • START:从tourInactive进入running,执行clearStep→setInitialStep→invokeOnStart
  • STEP.SET/STEP.NEXT/STEP.PREV:在running内部切换步骤
  • STEP.CHANGED/STEP.ROUTE:内部/外部步骤变更的统一路由
  • TARGET.NOT_FOUND/TARGET.RESOLVED:目标解析结果
  • SCROLL.END:滚动完成
  • DISMISS/SKIP:关闭或跳过(DISMISS 在最后一步时额外触发invokeOnComplete)

注意STEP.CHANGED与STEP.ROUTE的区别(源码注释明确说明):STEP.CHANGED用于外部stepId 变化(由watch回调触发),会先cleanupStepEffect;STEP.ROUTE用于内部步骤切换(performStepTransition/show触发),此时 effect 清理已由performStepTransition完成,无需重复执行。这正对应 CHANGELOG 0.43.0 中"从步骤的effect中 dismiss tour 时跳过清理、可能漏掉 completed 状态"的修复——effect 清理时机是 Tour 状态机设计中最精细的部分之一。

running状态还统一定义了DISMISS的两个分支(tour.machine.ts):

DISMISS: [ { guard: "isLastStep", target: "tourInactive", actions: ["cleanupAll", "invokeOnDismiss", "invokeOnComplete"] }, { target: "tourInactive", actions: ["cleanupAll", "invokeOnDismiss"] }, ],

即在最后一步dismiss 时,除了dismissed状态回调外,还会额外触发completed回调。

四、步骤模型:四种类型与参数全解

4.1 步骤类型(StepType)

StepType在 tour.types.ts 中定义为"tooltip" | "dialog" | "wait" | "floating"。normalizeStep(见 utils/step.ts)会根据步骤配置自动补全默认值:

  • tooltip:有target、未指定类型(或显式tooltip)时的默认类型,默认arrow: true、backdrop: true,浮层通过 popper 定位在目标元素周围。
  • dialog:无target或显式dialog时使用,默认placement: "center"、backdrop: true,内容居中展示(模态)。
  • wait:不展示 UI(标签 closed),进入running.waiting状态,由effect挂载事件监听,通过回调(如next())编程推进流程。计算进度时会通过getEffectiveSteps将wait步骤排除。
  • floating:无遮罩、无箭头,默认placement: "bottom-end",用于轻量浮层提示。

对应 CHANGELOG 0.72.0 "support programmatic control in wait steps" 与 1.26.5 "Fix issue where wait step doesn't work correctly" 的修复记录,wait 步骤的编程控制正是重构后的核心能力。

4.2 步骤字段(StepBaseDetails / StepDetails)

StepDetails的完整字段(tour.types.ts):

字段类型默认/说明
idstring步骤唯一标识(必填)
typeStepType步骤类型;无 target 时视为模态步骤
target() => HTMLElement \| null返回要高亮的元素
title/descriptionany步骤标题与描述
placementStepPlacement(Placement \| "center")浮层位置,默认"bottom"
offset{ mainAxis?, crossAxis? }内容与目标之间的偏移
metaRecord<string, any>附加元数据
backdropboolean是否显示遮罩
arrowboolean是否显示箭头
actionsStepAction[]步骤动作列表
effect(args: StepEffectArgs) => StepEffectCleanup步骤展示前执行的副作用,返回清理函数

4.3 步骤动作(StepAction)与动作映射

StepAction结构(tour.types.ts):

export interface StepAction { label: string action?: StepActionType | StepActionFn | undefined // "next" | "prev" | "dismiss" | "skip" 或自定义函数 attrs?: Record<string, any> | undefined // 传递给触发按钮的属性 }

StepActionMap为自定义动作函数提供完整控制能力(tour.types.ts):

export interface StepActionMap { next: VoidFunction prev: VoidFunction dismiss: VoidFunction skip: VoidFunction goto: (id: string) => void }

getActionTriggerProps(tour.connect.ts)按动作类型生成按钮属性:

  • next:data-type: "next",无下一步时disabled+data-disabled
  • prev:data-type: "prev",无上一步时disabled+data-disabled
  • dismiss:data-type: "close"
  • skip:data-type: "skip"
  • 自定义函数:data-type: "custom",点击时以actionMap为参数调用函数

CHANGELOG 中与动作系统相关的关键修复:

  • 1.43.0:修复action: "skip"的步骤动作点击后无效果的问题(此前 skip 动作被忽略)。
  • 1.18.1:修复StepActionMap中goto函数传入步骤 ID(字符串)时不生效的问题。
  • 1.46.0:修复动作触发器被翻译文本(如 "Got it" 被读作 "close tour")而非其可见标签宣告的问题;对仅图标的触发器,可通过attrs: { "aria-label": ... }命名,这正是StepAction.attrs字段的用途。

4.4 完整步骤示例

结合 examples/next-ts/pages/tour/step-types.tsx 与 examples/next-ts/pages/tour/wait-step.tsx,一个覆盖四种类型的步骤数组示例:

const steps: tour.StepDetails[] = [ { id: "dialog-intro", type: "dialog", title: "Dialog Step", description: "This is a centered dialog step with no target element.", actions: [{ label: "Next", action: "next" }], }, { id: "tooltip-feature", type: "tooltip", target: () => document.querySelector<HTMLElement>("#feature-btn"), title: "Tooltip Step", description: "This tooltip points to a specific element on the page.", placement: "bottom", actions: [ { label: "Prev", action: "prev" }, { label: "Next", action: "next" }, ], }, { id: "wait-action", type: "wait", effect({ next }) { const btn = document.querySelector<HTMLElement>("#continue-btn") if (!btn) return const handler = () => next() btn.addEventListener("click", handler, { once: true }) return () => btn.removeEventListener("click", handler) }, }, { id: "floating-end", type: "floating", title: "Floating Step", description: "A floating step with no backdrop or arrow.", actions: [{ label: "Done", action: "dismiss" }], }, ]

4.5 步骤的 effect 与清理

StepEffectArgs(tour.types.ts)提供 effect 内可用的控制句柄:

export interface StepEffectArgs { next: VoidFunction goto: (id: string) => void dismiss: VoidFunction show: VoidFunction update: (data: Partial<StepBaseDetails>) => void target?: (() => HTMLElement | null) | undefined }

executeStepEffect(tour.machine.ts)的要点:

  • effect的返回值会存入refs.effectCleanup,在步骤切换(cleanupStepEffect)或整体关闭(cleanupAll)时执行;
  • effect抛错会被捕获并console.error,不会导致状态机崩溃;
  • 若步骤类型为wait,effect 执行后自动调用show()完成展示。

这正是 CHANGELOG 1.26.5 "Fix issue where effects were not cleanup correct"、0.43.0 "dismissing a tour from a step'seffectskipped cleanup and could miss firing the completed status" 等修复的落点:effect 清理的可靠性直接决定 Tour 关闭时状态回调的完整性。

五、机器 Props 与组件 API

5.1 TourProps 参数

TourProps完整定义于 tour.types.ts,createProps声明于 tour.props.ts。核心参数:

参数类型默认值说明
stepsStepDetails[][]步骤列表
stepIdstring \| null—当前高亮步骤 id(受控)
onStepChange(details: StepChangeDetails) => void—高亮步骤变化回调(含stepId、stepIndex、totalSteps、complete、progress)
onStepsChange(details: StepsChangeDetails) => void—步骤列表变化回调
onStatusChange(details: StatusChangeDetails) => void—状态变化回调(idle / started / skipped / completed / dismissed / not-found)
closeOnInteractOutsidebooleantrue点击外部是否关闭
closeOnEscapebooleantrue按 Esc 是否关闭
keyboardNavigationbooleantrue是否允许左右方向键切换步骤
preventInteractionbooleanfalse是否阻止与页面其余部分交互(对目标元素设置inert)
spotlightOffsetPoint{ x: 10, y: 10 }聚光灯外扩偏移
spotlightRadiusnumber4聚光灯圆角半径(clip-path 圆角)
translationsIntlTranslations见下方文案翻译
idsElementIds—各部分 DOM id(用于组合)
dir"ltr" \| "rtl"—文本方向,影响键盘导航方向

默认翻译(tour.connect.ts):

const defaultTranslations = { nextStep: "next step", prevStep: "previous step", close: "close tour", progressText: ({ current, total }) => `${current + 1} of ${total}`, skip: "skip tour", }

5.2 TourApi 组件 API

TourApi(tour.types.ts)提供状态查询与操作:

  • 状态查询:open、totalSteps、stepIndex、step、hasNextStep、hasPrevStep、firstStep、lastStep
  • 步骤管理:addStep、removeStep、updateStep、setSteps、setStep、isValidStep、isCurrentStep
  • 流程控制:start(id?)(可从指定步骤启动)、next()、prev()
  • 进度:getProgressText()、getProgressPercent()
  • DOM Props:getBackdropProps、getSpotlightProps、getProgressTextProps、getPositionerProps、getArrowProps、getArrowTipProps、getContentProps、getTitleProps、getDescriptionProps、getCloseTriggerProps、getActionTriggerProps

典型用法(examples/next-ts/pages/tour/basic.tsx):

const service = useMachine(tour.machine, { id: useId(), steps: tourData }) const api = tour.connect(service, normalizeProps) return ( <> <button onClick={() => api.start()}>Start Tour</button> {api.step && api.open && ( <Portal> {api.step.backdrop && <div {...api.getBackdropProps()} />} <div {...api.getSpotlightProps()} /> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {api.step.arrow && ( <div {...api.getArrowProps()}><div {...api.getArrowTipProps()} /></div> )} <p {...api.getTitleProps()}>{api.step.title}</p> <div {...api.getDescriptionProps()}>{api.step.description}</div> <div {...api.getProgressTextProps()}>{api.getProgressText()}</div> {api.step.actions?.map((action) => ( <button key={action.label} {...api.getActionTriggerProps({ action })}>{action.label}</button> ))} <button {...api.getCloseTriggerProps()}><X /></button> </div> </div> </Portal> )} </> )

与start(id)相关的修复记录:1.12.3 修复api.start(<id>)传步骤 id 不生效的问题;0.38.1 修复next、prev、setStep在 Tour 未激活时仍触发、绕过start流程的问题——这正对应状态机只在running状态响应STEP.NEXT/STEP.PREV/STEP.SET的设计。

六、关键实现细节:遮罩、聚光灯与定位

6.1 组件各部分(anatomy)

anatomy.ts 声明了 11 个 parts:

export const anatomy = createAnatomy("tour").parts( "content", "actionTrigger", "closeTrigger", "progressText", "title", "description", "positioner", "arrow", "arrowTip", "backdrop", "spotlight", )

6.2 遮罩与聚光灯(backdrop / spotlight)

  • backdrop:getBackdropProps通过clip-path实现"挖洞"效果——tooltip 步骤时用getClipPath(utils/clip-path.ts)以目标矩形 +spotlightRadius圆角生成裁剪路径;dialog 步骤时position: fixed铺满视口。
  • spotlight:getSpotlightProps使用 CSS 变量--spotlight-x/y/width/height定位,borderRadius: var(--spotlight-radius),pointerEvents: none。suppressSpotlightTransition为true时transitionDuration: 0ms,即聚光灯只在步骤切换之间动画,resize/滚动重定位时直接跳转(对应 CHANGELOG 1.45.0 "The spotlight now only animates between steps")。

6.3 placement 与>getPlacement(anchorEl, positionerEl, { defer: true, placement: step.placement ?? "bottom", strategy: "absolute", gutter: 10, offset: step.offset, restoreStyles: false, applyStyles: false, getAnchorRect(el) { if (!isHTMLElement(el)) return null const rect = el.getBoundingClientRect() return offset(rect, prop("spotlightOffset")) }, onComplete(data) { /* 更新 targetRect、currentPlacement、floatingOffset */ }, })

其中restoreStyles是 CHANGELOG 1.38.1 的重要变更:popper 包将cleanupStyles选项重命名为restoreStyles,同时修复了"从 tooltip 步骤切换到 dialog/非 tooltip 步骤时 popper 样式未清理"的问题。在 Tour 中restoreStyles: false,样式清理由 Tour 自己管理(floatingOffset在关闭后保留,以便 Presence 退出动画保持锚定——见 tour.types.ts 中floatingOffset的注释)。

另外getAnchorRect对目标矩形应用spotlightOffset外扩,因此遮罩挖洞区域与聚光灯视觉区域保持一致。visualViewport的容错处理(1.45.0 修复 jsdom 等无visualViewport环境下的崩溃)可在trackBoundarySize中看到:win.visualViewport?.width ?? win.innerWidth。

七、焦点与无障碍:从 1.32.0 到 1.46.0 的持续修复

Tour 的焦点管理由trapFocuseffect 实现(tour.machine.ts):

return trapFocus([contentEl, targetEl], { escapeDeactivates: false, allowOutsideClick: true, preventScroll: true, returnFocusOnDeactivate: false, getShadowRoot: true, })

CHANGELOG 中与焦点/无障碍直接相关的修复:

  • 1.32.0:修复聚光灯目标内的输入元素不可交互的问题——focus trap 现在将 tour 内容和聚光灯目标都作为容器,允许用户与高亮区域内的元素交互。这正是上面trapFocus([contentEl, targetEl], ...)传两个容器的原因。
  • 1.27.0:所有 focus trap 组件(dialog、popover、tour 等)改进对shadow DOM的支持(getShadowRoot: true)。
  • 1.43.1:给getCloseTriggerProps()增加type="button"。此前按钮默认type="submit",点击会提交祖先表单——这是表单场景下的经典陷阱。
  • 1.46.0:修复动作触发器被翻译文本(而非可见标签)宣告的问题,可通过attrs: { "aria-label": ... }为图标按钮命名。

内容容器本身也具备完整的无障碍属性(tour.connect.ts):role: "alertdialog"、aria-modal: "true"、aria-live: "polite"、aria-atomic: "true"、aria-labelledby指向标题、aria-describedby指向描述、tabIndex: -1。

键盘导航方面,getContentProps的onKeyDown支持左右方向键切换步骤(keyboardNavigation为true时),并在 RTL 模式下自动反转方向:

case "ArrowRight": send({ type: isRtl ? "STEP.PREV" : "STEP.NEXT", src: "keydown" })

八、视口、滚动与动态目标:1.45.0 的大修

1.45.0 集中修复了三类视觉问题,对应三个独立 PR:

  1. 步骤卡片、聚光灯与遮罩在目标元素被替换(replaced)时坍缩到左上角(PR #3341)——trackPlacement中anchorEl每次更新都会重新解析目标:若step.target()返回的新元素与resolvedTarget不同,则更新 context 并重新同步目标属性(见 tour.machine.ts 的anchorEl注释 "Re-resolved per update: the target can be replaced mid-tour, and a detached node measures zero")。
  2. 窗口 resize 或内容加载时,遮罩漏掉部分页面、聚光灯滑动到新位置(PR #3340)——通过suppressSpotlightTransition机制:在onComplete中对比prevSpotlightRect与当前rects.reference,若 rect 变化且prevSpotlightStepId为空或等于当前 stepId(即非步骤切换的重定位),则抑制过渡(suppressSpotlightTransition = true),聚光灯直接跳到新位置而非动画滑动。
  3. iframe 或自定义 root 中的 Tour 错误测量外部页面,且在没有visualViewport的环境(如 jsdom)挂载时崩溃——trackBoundarySize使用scope.getWin()/scope.getDoc()(作用域窗口,iframe 场景下即 iframe 自身窗口),宽度取visualViewport?.width ?? innerWidth,高度取documentElement.scrollHeight,并通过 rAF 轮询(注释说明:height: 100%这类场景下溢出变化不会触发 root 边框尺寸的 resize 事件)。

与此相关的还有 1.33.0 的滚动优化:将scrollIntoView从block: "center"改为block: "nearest",页面只在目标未可见时才滚动,消除步骤间切换的抖动(源码中scrollToTarget使用{ behavior: "instant", block: "nearest", inline: "nearest" })。

九、工程实践:从 CHANGELOG 提炼的避坑清单

综合整个 CHANGELOG 与源码,以下工程要点值得在集成@zag-js/tour时牢记:

  1. 按钮类型陷阱:所有getActionTriggerProps/getCloseTriggerProps输出的按钮均已带type="button"(1.43.1),集成时不要覆盖此属性,避免嵌套在表单中触发提交。
  2. effect 必须返回清理函数:从 0.43.0、1.26.5 的修复看,effect 清理是状态回调(completed/dismissed)可靠触发的关键。effect返回的 cleanup 会在步骤切换与 Tour 关闭时执行。
  3. 目标元素可能被替换:1.45.0 起anchorEl会在每次定位时重新解析目标,若目标被 React/Vue 等框架替换,Tour 会自适应;但target回调应始终返回最新的元素引用。
  4. wait 步骤不计入进度:getProgressText/getProgressPercent基于getEffectiveSteps(排除wait类型)计算,wait 步骤只是流程中的"等待哨兵"。
  5. 图标按钮需要显式无障碍名:1.46.0 起可用attrs: { "aria-label": "..." }为纯图标动作按钮命名,否则屏幕阅读器会读翻译文案。
  6. z-index 由用户层控制:0.81.1 起 Tour 不再设置zIndex,通过 CSS 变量--tour-layer(backdrop=0、spotlight=1、positioner=2)与--tour-z-index组合实现层级,用户需自行保证容器层叠上下文正确。
  7. 避免步骤 id 重复:validateSteps会在挂载时校验(tour.machine.ts),重复 id 会直接抛错:[zag-js/tour] Duplicate step id: ...;同时每个步骤必须提供target或type之一。

十、依赖协同:Tour 如何受益于生态升级

CHANGELOG 中几乎每个版本都带 "Updated dependencies",这些不是例行公事,而是 Tour 行为改进的直接来源:

  • @zag-js/focus-trap的升级带来 shadow DOM 支持(1.27.0)、目标容器纳入陷阱(1.32.0);
  • @zag-js/popper的升级带来restoreStyles重命名与样式清理修复(1.38.1)、data-side支持(1.41.0);
  • @zag-js/dismissable/@zag-js/interact-outside的升级支撑了点击外部关闭、dismissable branch 的稳定性;
  • @zag-js/types的升级(1.43.3)将可选属性改为显式?: T | undefined,修复了Partial导出遮蔽内置类型、破坏@vue/compiler-sfc的interface X extends Partial<Y>的问题——这是类型层面对 Vue 生态的兼容性修复。

结语

从 0.37.0 引入到 1.46.0,@zag-js/tour的 CHANGELOG 浓缩了一个真实生产组件的打磨历程:状态机从resolving → scrolling → waiting → active的清晰分层,到 wait 步骤的编程化重构(0.72.0),再到聚光灯过渡抑制(1.45.0)、目标替换自适应(1.45.0)、焦点陷阱扩展(1.32.0)与无障碍细节(1.43.1、1.46.0)的持续收敛。结合 tour.machine.ts 与 tour.connect.ts 的源码阅读,你可以将这套状态机设计思路直接迁移到自己的引导式走查实现中,并在集成 Zag.js Tour 时避开历史上沉淀下来的全部已知坑点。

  • 前端
  • UI组件

【免费下载链接】zag

Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines

项目地址:https://gitcode.com/gh_mirrors/za/zag
点击查看免费下载
上一篇:Strata 的 AI 代理部署指南:从 PC 体检到 API 服务,AI_SETUP 全流程非交互安装实践
下一篇:AndroidRecyclerViewDemo核心功能解析:7种布局类型与实战案例演示

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

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

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

立即咨询