mcpField
B
basic-custom-app-build-w-video

basic-custom-app-build-w-video

github.com/stahura开发24166 次安装26 周前更新
代码仓库

关于 basic-custom-app-build-w-video

用于构建或接手 Domo 自定义应用并制作 Remotion 演示视频的开发手册。它加入确定性合成样例数据和 Remotion 样式约束,使应用组件无需实时 Domo 数据即可用于视频渲染。

主要功能

  1. 01Domo 应用构建阶段指引
  2. 02Remotion 兼容样式规范
  3. 03确定性合成样例数据
  4. 04将应用组件渲染为演示视频
  5. 05由属性驱动的视觉状态

适用场景

  1. 01构建带利益相关方演示的新 Domo 应用
  2. 02为现有应用制作可视化演示
  3. 03无需实时数据集或凭据即可渲染应用视图

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/stahura/domo-ai-vibe-rules --skill basic-custom-app-build-w-video

安装后在 Claude Code 中输入/basic-custom-app-build-w-video确认已加载。

详细介绍

Domo 应用 + 演示视频构建指南

适用场景

  • 构建新的 Domo 自定义应用,并为利益相关方、文档或营销制作演示视频。
  • 接手现有应用,在规范化的同时制作可视化演示。

与 basic-custom-app-build 的区别

本指南在标准 Domo 应用构建流程的基础上增加了三项内容:

  1. 强制执行兼容 Remotion 的样式约束,从阶段 0 开始,确保应用 UI 可在 Remotion 中直接截图并制作动画,无需返工。
  2. 生成示例数据(阶段 3A),提供逼真的模拟数据,让演示呈现填充了内容的 UI,无需实时 Domo 数据集或凭据。
  3. 制作演示视频(阶段 10),使用 Remotion 为完成的应用制作一段精美的 10-15 秒视频。

进度检查清单

复制此检查清单,并在工作过程中更新。

Demo-Ready Build Progress:
- [ ] Phase 0 : Rules loaded (platform + Remotion styling constraints)
- [ ] Phase 1 : Manifest & contracts
- [ ] Phase 2 : App shell (domo.js)
- [ ] Phase 3 : Data access
- [ ] Phase 3A: Sample data generation
- [ ] Phase 4 : App storage (if needed)
- [ ] Phase 5 : Toolkit clients (if needed)
- [ ] Phase 6 : Feature skills (AI / Code Engine / Workflow — as needed)
- [ ] Phase 7 : UI build (Remotion-safe styling)
- [ ] Phase 8 : Performance review
- [ ] Phase 9 : Build & publish
- [ ] Phase 10: Demo video (Remotion)
- [ ] Phase 11: Verification

阶段 0 — 加载规则(始终启用)

编写任何代码之前,先应用以下规则:

  • rules/core-custom-apps-rule.md
  • rules/custom-app-gotchas.md
Remotion 样式约束(所有阶段均须遵守)

这些规则可确保你构建的每个 UI 组件都能通过截图捕获,或直接在 Remotion 合成中渲染,无需返工。

规则原因
不要使用 CSS transition-* 或 animation-* 属性Remotion 无法捕获由 CSS 驱动的动画。演示中的所有动画都将由 useCurrentFrame() 驱动。静态样式(颜色、边框、阴影、静止状态下的变换)没有问题。
不要使用 Tailwind transition-* 或 animate-* 工具类原因相同。其他 Tailwind 工具类均可使用,且建议使用。
使用内联 style 对象或 Tailwind 进行布局Remotion 会将 React 组件渲染为帧;CSS 模块和作用域样式可以正常使用,但 CSS-in-JS 运行时库(styled-components、Emotion)可能会在 Remotion 渲染流程中导致水合问题。优先使用内联样式或 Tailwind。
在 Domo 应用中正常使用 <img>在 应用 代码中,使用标准 <img> 标签。Remotion 演示在合成场景时会使用自己的 <Img> 组件。此约束旨在避免依赖延迟加载或交叉观察器模式,因为这些模式在 Remotion 帧捕获时不会触发。
保持视觉状态确定性演示中展示的每种 UI 状态都必须仅根据 props/数据即可复现,而不能依赖用户交互历史或计时器。构建组件时,应确保传入示例数据即可生成演示所需的精确视觉效果。
按 1920x1080 进行设计确保应用布局在 1920x1080 分辨率下显示良好,因为这是演示视频的原生分辨率。仍可采用响应式设计,但请务必验证 1080p 断点。

阶段 1 — 清单与契约

使用 manifest。

首先定义所有外部资源映射:数据集、集合、工作流、Code Engine 包。其他所有内容都依赖这些映射。

仅使用安全别名名称(^[A-Za-z][A-Za-z0-9]*$),不得包含空格或特殊字符。

检查根目录中是否已有 thumbnail.png,并将其复制到应用的 public/ 文件夹。

接手现有应用? 修改前,先审查当前 manifest.json,确认其与实际代码用法一致。


阶段 2 — 应用框架

使用 domo-js。

设置基础内容:导入 ryuu.js、通过 domo.navigate() 导航、添加事件监听器、获取环境信息。

DA CLI 用户: 如果用户明确提出要求,也使用 da-cli 搭建脚手架。


阶段 3 — 数据访问

使用 dataset-query(主要)和 data-api(路由概览)。

使用 @domoinc/query 构建查询。在编写 UI/数据映射逻辑之前,先获取 manifest.json datasetsMapping 中每个数据集的真实架构,并创建明确的字段映射。

需要原始 SQL? 使用 sql-query,但请注意,SQL 会忽略页面筛选器。


阶段 3A — 生成示例数据

这是确保应用适合演示的关键补充。生成逼真的示例数据,要求:

  1. 填充演示中展示的所有视图(仪表板、详情面板、图表、表格、聊天消息等)
  2. 不包含任何真实的客户或用户数据——所有姓名、电子邮件、ID 和指标均为合成数据
  3. 具有确定性——相同的种子会生成相同的数据,因此截图和 Remotion 渲染结果可复现
实现方法

在应用中创建一个 src/sample-data/ 目录,其中包含:

src/sample-data/
  index.ts          # Re-exports all sample datasets; single import point
  datasets/
    <alias>.ts      # One file per manifest dataset alias, exporting typed arrays
  README.md         # Documents the shape and purpose of each sample dataset
对于 manifest.json 中的每个数据集别名:
  1. 创建一个 TypeScript 文件,导出与阶段 3 中该数据集字段映射相匹配的类型化数组。
  2. 生成 20-100 行逼真的数据(足以令人信服地填充图表和表格)。
  3. 使用数据集架构中的实际字段名称,而非猜测的名称。
  4. 包含一定范围的值,以呈现有趣的视觉模式(趋势、异常值、类别分布)。
// src/sample-data/datasets/sales.ts
export type SalesRow = {
  Date: string;
  Region: string;
  Product: string;
  Revenue: number;
  Units: number;
};

export const salesData: SalesRow[] = [
  { Date: "2026-01-15", Region: "West", Product: "Widget A", Revenue: 12400, Units: 62 },
  { Date: "2026-01-16", Region: "East", Product: "Widget B", Revenue: 8750, Units: 35 },
  // ... 20-100 rows with realistic distribution
];
将示例数据接入应用

创建一个数据访问层,在实时 Domo 查询和示例数据之间切换:

// src/data/use-dataset.ts
import { salesData } from "../sample-data/datasets/sales";

const USE_SAMPLE_DATA = !window.__DOMO_ENV__; // true outside Domo runtime

export async function fetchSalesData(): Promise<SalesRow[]> {
  if (USE_SAMPLE_DATA) {
    return salesData;
  }
  // Real query via @domoinc/query
  return new Query().select([...]).fetch("sales");
}

这样可以确保:

  • **在 Domo 中:**应用会照常查询真实数据集。
  • **在本地 / Remotion 中:**应用使用示例数据进行渲染,无需凭据。
  • **安全性:**演示视频中不会包含任何真实数据。
非数据集来源的示例数据
来源示例数据策略
AppDB 集合在 src/sample-data/collections/<name>.ts 中导出示例文档数组
AI 响应在 src/sample-data/ai-responses.ts 中导出预设响应字符串
用户身份导出模拟用户对象:{ displayName: "Alex Demo", email: "[email protected]", role: "Admin" }
工作流结果在 src/sample-data/workflows/<name>.ts 中导出示例状态对象

阶段 4 — 应用存储(如有需要)

需要创建存储时,使用 appdb 和 appdb-collection-create。

如果应用只读取数据集,则跳过。


阶段 5 — Toolkit 客户端(如有需要)

使用 toolkit。

在能发挥作用的地方,使用类型化的 @domoinc/toolkit 客户端。


阶段 6 — 功能技能(按需)

只加载应用实际需要的技能:

所需功能技能
AI 文本生成或文本转 SQLai-service-layer
服务端函数(密钥、外部 API)code-engine + code-engine-create + code-engine-update
触发自动化工作流workflow

如果不需要这些功能,则跳过此阶段。


阶段 7 — 构建 UI(兼容 Remotion 的样式)

构建应用的 UI 组件。这是标准的 React 开发,但必须遵守阶段 0 中的 Remotion 约束:

  • 所有样式均使用 Tailwind 工具类或内联 style 对象。
  • 不使用 transition-*、animate-* 或 CSS 关键帧动画。
  • 所有视觉状态都必须能通过属性 + 示例数据(阶段 3A)复现。
  • 在 1920x1080 分辨率下检查布局。
  • 演示中展示的每个“场景”(加载状态、已填充内容的仪表板、详情视图等),都应能通过传入不同的示例数据来呈现,而不是通过模拟用户点击来实现。
便于演示的组件设计

对组件进行结构化设计,使每种视觉状态都由数据决定:

// Good: demo can render any state by passing props
<ChatPanel messages={sampleMessages} isTyping={false} />
<ChatPanel messages={sampleMessages} isTyping={true} />

// Bad: state is internal and requires user interaction to reach
<ChatPanel /> // internally manages messages via useState

无需按这种方式重构整个应用,只需处理演示中需要展示的视图即可。将 3-5 种关键视觉状态作为由属性驱动的变体公开。


阶段 8 — 性能审查

使用 performance。

检查所有查询。查看是否存在全量数据集提取、缺少聚合或不必要的列。


阶段 9 — 构建并发布

使用 publish。

npm run build -> cd dist -> domo publish。首次发布时,将生成的 id 复制回源清单。


阶段 10 — 演示视频(Remotion)

演示视频会直接在 Remotion 中渲染实际的应用组件——不是截图、标题卡片或营销幻灯片。观众应看到填充了示例数据的真实 UI,并通过滚动动画或视图切换来展示应用的所有部分。

10.1 — 搭建 Remotion 项目

在应用源代码旁创建一个 demo/ 目录:

my-app/
  src/            # Domo app source
  demo/           # Remotion demo project
    src/
      Root.tsx
      DemoVideo.tsx
      index.ts
    remotion.config.ts  # Webpack alias to import app components
    package.json
package.json

演示项目必须包含应用使用的相同 UI 依赖(例如 recharts),这样 Remotion 才能渲染实际组件:

{
  "dependencies": {
    "@remotion/cli": "^4.0.0",
    "@remotion/transitions": "^4.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "recharts": "^2.12.0",
    "remotion": "^4.0.0"
  }
}

添加应用使用的其他 UI 库(图表库、UI 组件套件等)。

remotion.config.ts — 应用导入的 Webpack 别名

这一点至关重要。配置 @app 别名,以便演示代码可以直接从应用的 src/ 导入:

import { Config } from '@remotion/cli/config';
import path from 'path';

Config.overrideWebpackConfig((currentConfiguration) => ({
  ...currentConfiguration,
  resolve: {
    ...currentConfiguration.resolve,
    alias: {
      ...currentConfiguration.resolve?.alias,
      '@app': path.resolve(process.cwd(), '..', 'src'),
    },
  },
}));
index.ts — Remotion 入口点
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);
tsconfig.json — 路径映射
{
  "compilerOptions": {
    "paths": { "@app/*": ["../src/*"] },
    "baseUrl": "."
  },
  "include": ["src", "../src"]
}
10.2 — AnimatedCursor 组件

创建一个可复用的光标,使其在不同位置间移动并显示点击脉冲,以营造有人正在使用应用的效果。

放置在 demo/src/components/AnimatedCursor.tsx:

import React from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';

export type CursorWaypoint = {
  x: number;       // Pixel position from left
  y: number;       // Pixel position from top
  atFrame: number; // Frame at which cursor should arrive here
  click?: boolean; // Show a click pulse when arriving
};

type Props = {
  waypoints: CursorWaypoint[];
  size?: number;  // Default 20
};

export const AnimatedCursor: React.FC<Props> = ({ waypoints, size = 20 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  if (waypoints.length === 0) return null;

  // Find which two waypoints we're interpolating between
  let fromWp = waypoints[0];
  let toWp = waypoints[0];
  for (let i = 0; i < waypoints.length - 1; i++) {
    if (frame >= waypoints[i].atFrame) {
      fromWp = waypoints[i];
      toWp = waypoints[i + 1];
    }
  }
  if (frame >= waypoints[waypoints.length - 1].atFrame) {
    fromWp = waypoints[waypoints.length - 1];
    toWp = fromWp;
  }

  const easing = Easing.inOut(Easing.quad);
  const x = fromWp === toWp ? fromWp.x
    : interpolate(frame, [fromWp.atFrame, toWp.atFrame], [fromWp.x, toWp.x],
        { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing });
  const y = fromWp === toWp ? fromWp.y
    : interpolate(frame, [fromWp.atFrame, toWp.atFrame], [fromWp.y, toWp.y],
        { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing });

  // Click pulse at waypoints marked click: true
  let pulseOpacity = 0;
  let pulseScale = 0;
  for (const wp of waypoints) {
    if (wp.click && frame >= wp.atFrame && frame < wp.atFrame + 20) {
      const p = spring({ frame: frame - wp.atFrame, fps, config: { damping: 20, stiffness: 200 } });
      pulseScale = interpolate(p, [0, 1], [0.5, 1.5]);
      pulseOpacity = interpolate(frame - wp.atFrame, [0, 20], [0.5, 0], { extrapolateRight: 'clamp' });
    }
  }

  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 9999 }}>
      {pulseOpacity > 0 && (
        <div style={{
          position: 'absolute', left: x - 15, top: y - 15,
          width: 30, height: 30, borderRadius: '50%',
          border: '2px solid rgba(255,255,255,0.8)',
          transform: `scale(${pulseScale})`, opacity: pulseOpacity,
        }} />
      )}
      <svg style={{ position: 'absolute', left: x, top: y, width: size, height: size,
        filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.5))' }} viewBox="0 0 24 24" fill="none">
        <path d="M5 3L19 12L12 12L8 21L5 3Z" fill="white" stroke="#1e293b"
          strokeWidth="1.5" strokeLinejoin="round" />
      </svg>
    </div>
  );
};
10.3 — Root.tsx
import { Composition } from 'remotion';
import { DemoVideo } from './DemoVideo';

export const RemotionRoot = () => (
  <Composition
    id="AppDemo"
    component={DemoVideo}
    durationInFrames={300}  // 10s at 30fps
    fps={30}
    width={1920}
    height={1080}
  />
);
10.4 — 按应用类型选择演示模式

选择与你的应用匹配的模式,并根据需要组合使用。所有模式都通过 @app/ 导入实际应用组件。

模式 A:单视图滚动(仪表板、表格、长表单)

渲染组件,并通过动画调整 translateY 来滚动浏览内容。

import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
import { Dashboard } from '@app/App';
import { sampleData } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';

export const DemoVideo: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const scrollY = interpolate(frame, [2 * fps, 6 * fps], [0, -500], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.inOut(Easing.quad),
  });

  return (
    <AbsoluteFill style={{ backgroundColor: '#0f172a', overflow: 'hidden' }}>
      <div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
        <Dashboard data={sampleData} />
      </div>
      <AnimatedCursor waypoints={[
        { x: 700, y: 400, atFrame: 0 },
        { x: 700, y: 500, atFrame: Math.round(3 * fps) },
        { x: 500, y: 450, atFrame: Math.round(6 * fps) },
      ]} />
    </AbsoluteFill>
  );
};
模式 B:带标签页/多视图的应用(最常见)

使用 <Sequence> 切换视图。光标移至标签页/按钮,触发点击脉冲,然后下一个 Sequence 渲染新视图。

import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
import { Dashboard } from '@app/App';
import { sampleData } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';

const OverviewScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const scrollY = interpolate(frame, [2 * fps, 4 * fps], [0, -350], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.inOut(Easing.quad),
  });

  return (
    <AbsoluteFill style={{ overflow: 'hidden' }}>
      <div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
        <Dashboard data={sampleData} activeTab="overview" />
      </div>
      <AnimatedCursor waypoints={[
        { x: 700, y: 400, atFrame: 0 },
        { x: 900, y: 450, atFrame: Math.round(3 * fps) },
        // End near the target tab button
        { x: 1195, y: 48, atFrame: Math.round(4.5 * fps) },
      ]} />
    </AbsoluteFill>
  );
};

const DetailScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const scrollY = interpolate(frame, [2 * fps, 4 * fps], [0, -200], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.inOut(Easing.quad),
  });

  return (
    <AbsoluteFill style={{ overflow: 'hidden' }}>
      <div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
        <Dashboard data={sampleData} activeTab="detail" selectedItem={sampleData.items[0]} />
      </div>
      <AnimatedCursor waypoints={[
        { x: 1195, y: 48, atFrame: 0, click: true },  // Click the tab
        { x: 600, y: 300, atFrame: Math.round(0.8 * fps) },
        { x: 500, y: 400, atFrame: Math.round(3 * fps) },
      ]} />
    </AbsoluteFill>
  );
};

export const DemoVideo: React.FC = () => {
  const { fps } = useVideoConfig();
  return (
    <AbsoluteFill>
      <Sequence from={0} durationInFrames={5 * fps} premountFor={fps}>
        <OverviewScene />
      </Sequence>
      <Sequence from={5 * fps} durationInFrames={5 * fps} premountFor={fps}>
        <DetailScene />
      </Sequence>
    </AbsoluteFill>
  );
};

要点: 场景 1 中光标的最后一个路径点和场景 2 中的第一个路径点应位于同一位置(标签页按钮),以便在切换时营造无缝的“点击”效果。

模式 C:AI 聊天/对话类应用

使用基于帧的数组切片,逐步增加消息数量来渲染聊天组件。这样无需实际交互,就能展示消息“逐条到达”的效果。

import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion';
import { ChatPanel } from '@app/components/ChatPanel';
import { sampleMessages } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';

// Pre-define which messages are visible at which frame
const messageTimeline = [
  { visibleCount: 2, atFrame: 0 },    // Start with 2 messages already visible
  { visibleCount: 3, atFrame: 60 },   // User message appears at 2s
  { visibleCount: 3, typingIndicator: true, atFrame: 90 },  // AI typing at 3s
  { visibleCount: 4, atFrame: 120 },  // AI response appears at 4s
  { visibleCount: 5, atFrame: 180 },  // Another user message at 6s
  { visibleCount: 5, typingIndicator: true, atFrame: 210 },
  { visibleCount: 6, atFrame: 240 },  // Final AI response at 8s
];

export const DemoVideo: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Find current state based on frame
  let currentState = messageTimeline[0];
  for (const state of messageTimeline) {
    if (frame >= state.atFrame) currentState = state;
  }

  const visibleMessages = sampleMessages.slice(0, currentState.visibleCount);

  return (
    <AbsoluteFill style={{ overflow: 'hidden' }}>
      <ChatPanel
        messages={visibleMessages}
        isTyping={currentState.typingIndicator ?? false}
      />
      <AnimatedCursor waypoints={[
        { x: 960, y: 800, atFrame: 0 },
        // Cursor moves to input area before "sending" a message
        { x: 700, y: 950, atFrame: 50, click: true },
        { x: 960, y: 600, atFrame: 90 },   // Watches AI type
        { x: 700, y: 950, atFrame: 170, click: true },  // Sends another
        { x: 960, y: 500, atFrame: 240 },  // Reads response
      ]} />
    </AbsoluteFill>
  );
};

聊天类应用要点: ChatPanel 组件必须将 messages 和 isTyping 作为 props 接收(阶段 7 会强制执行这一点)。阶段 3A 中的示例数据应包含一段逼真的对话。

模式 D:按钮/交互密集型应用(调度面板、表单)

按顺序渲染不同的 prop 状态,以模拟按钮点击。使用光标指向按钮,触发点击脉冲,然后切换到“点击后”状态。

// Scene 1: Empty form state, cursor fills in a field
<Sequence from={0} durationInFrames={4 * fps}>
  <FormView formData={emptyForm} />
  <AnimatedCursor waypoints={[
    { x: 500, y: 300, atFrame: 0 },
    { x: 500, y: 300, atFrame: 30, click: true },
  ]} />
</Sequence>

// Scene 2: Filled form, cursor clicks submit
<Sequence from={4 * fps} durationInFrames={3 * fps}>
  <FormView formData={filledForm} />
  <AnimatedCursor waypoints={[
    { x: 500, y: 600, atFrame: 0 },
    { x: 500, y: 600, atFrame: 30, click: true },  // Click "Submit"
  ]} />
</Sequence>

// Scene 3: Success state
<Sequence from={7 * fps} durationInFrames={3 * fps}>
  <FormView formData={filledForm} submitSuccess={true} />
</Sequence>
10.5 — 光标路径点定位提示
  • 查找坐标: 运行 npx remotion studio,将鼠标悬停在预览上,并记下按钮/标签页/元素渲染时的像素坐标。
  • 在 Sequence 之间匹配: 当光标在场景 1 结束时“点击”某个标签页,其在场景 2 中的第一个位置应使用相同坐标,以实现无缝过渡。
  • 移动速度: 每次移动控制在 30–60 帧(1–2 秒)。速度太快会显得慌乱,太慢则会让人觉得无聊。
  • 不要过度添加动画: 每个场景移动光标 2–3 次就足够了。光标应该引导注意力,而不是分散注意力。
10.6 — 预览和渲染
cd demo
npx remotion studio          # Preview in browser
npx remotion render AppDemo  # Render to out/AppDemo.mp4

第 11 阶段 — 验证

发布后,确认:

  • 应用在 Domo 中加载时没有控制台错误
  • 所有数据集别名均可解析(数据请求没有 404 错误)
  • AppDB 集合已接入卡片 UI(如果使用)
  • 页面筛选器能正确传递(如果应用嵌入在仪表板中)
  • 导航使用 domo.navigate(),而不是 <a href>
  • 缩略图已复制到 public/ 文件夹
  • 在 Domo 外运行时,示例数据能正确渲染(没有 undefined/null 错误)
  • 演示视频渲染正常,没有空白帧或缺失资源(npx remotion render AppDemo)
  • 演示视频显示已填充的 UI,而不是空状态或加载指示器

构建时防护规则

  • 仅限客户端:不使用 SSR/服务器路由/服务器组件。
  • 使用 Vite base: './'。
  • 除非已专门处理重写规则,否则优先使用 HashRouter。
  • 仅将 domo.env.* 视为便利功能;进行信任决策时使用经过验证的身份。
  • 演示录制的任何组件都不得使用 CSS 过渡或动画。
  • 示例数据不得包含任何真实客户数据。
  • 所有演示视觉状态都必须由 props 驱动,而非由交互驱动。

评论

支持 Markdown