mcpField
W
wix-cli-site-widget

wix-cli-site-widget

github.com/wix开发36288 次安装今天更新
代码仓库

关于 wix-cli-site-widget

wix-cli-site-widget 是用于 Wix CLI 应用的开发技能,指导创建可在 Wix Editor 中添加和配置的站点小组件。它将 React 组件转换为 Web 组件,并包含侧边设置面板、属性映射和扩展注册说明。

主要功能

  1. 01React 到 Web 组件转换
  2. 02Wix Editor 设置面板
  3. 03组件属性配置
  4. 04颜色与字体选择器
  5. 05Wix Data 编辑器环境处理

适用场景

  1. 01创建倒计时小组件
  2. 02从集合展示商品
  3. 03构建可配置颜色的计算器

安装

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

$npx skills add https://github.com/wix/skills --skill wix-cli-site-widget

安装后在 Claude Code 中输入/wix-cli-site-widget确认已加载。

附带文件references/SETTINGS_PANEL.md

详细介绍

Wix 网站微件构建器

为 Wix CLI 应用创建自定义元素微件扩展。网站微件是转换为 Web 组件的 React 组件,会显示在 Wix Editor 中,让网站所有者可以将交互式、可配置的微件添加到页面,并使用内置设置面板进行配置。

快速入门清单

创建网站微件时,请按顺序执行以下步骤:

  1. 创建微件文件夹:src/extensions/site/widgets/custom-elements/<widget-name>/
  2. 创建 widget.tsx,其中包含 React 组件和 reactToWebComponent 转换
  3. 创建 panel.tsx,其中包含 WDS 组件和 widget.getProp/setProp
  4. 创建 extensions.ts,其中包含 extensions.customElement() 和唯一 UUID
  5. 更新 src/extensions.ts,导入并使用新扩展

架构

网站微件包含两个必需文件:

1. 微件组件(widget.tsx)

使用 react-to-webcomponent 将 React 组件转换为 Web 组件:

  • 定义带有可配置属性的 Props 接口(camelCase)
  • 创建用于渲染微件 UI 的 React 函数组件
  • 使用属性映射将其转换为 Web 组件
  • 使用内联样式(不导入 CSS)
  • 使用 Wix Data API 时,处理 Wix Editor 环境
2. 设置面板(panel.tsx)

显示在 Wix Editor 侧边栏中的设置面板:

  • 使用 Wix Design System 组件(参见 references/SETTINGS_PANEL.md)
  • 通过 @wix/editor 微件 API 管理微件属性
  • 使用 widget.getProp('kebab-case-name') 加载初始值
  • 使用 widget.setProp('kebab-case-name', value) 更新属性
  • 将内容包装在 WixDesignSystemProvider > SidePanel > SidePanel.Content 中
  • 对于颜色选择器,使用 @wix/editor 中的 inputs.selectColor() 和 FillPreview,不要使用 <Input type="color">
  • 对于字体选择器,使用 @wix/editor 中的 inputs.selectFont() 和 Button,不要使用文本 Input

微件组件模式

import React, { type FC, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import reactToWebComponent from 'react-to-webcomponent';

interface WidgetProps {
  title?: string;
  targetDate?: string;
  targetTime?: string;
  bgColor?: string;
  textColor?: string;
  font?: string;
}

const CustomElement: FC<WidgetProps> = ({
  title = 'Countdown',
  targetDate = '',
  targetTime = '00:00',
  bgColor = '#ffffff',
  textColor = '#333333',
  font = "{}",
}) => {
  const { font: textFont, textDecoration } = JSON.parse(font);
  const [time, setTime] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: false });

  useEffect(() => {
    if (!targetDate) return;

    const update = () => {
      const target = new Date(`${targetDate}T${targetTime}`);
      const diff = target.getTime() - Date.now();
      if (diff <= 0) {
        setTime({ days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true });
        return;
      }
      setTime({
        days: Math.floor(diff / (1000 * 60 * 60 * 24)),
        hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
        minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
        seconds: Math.floor((diff % (1000 * 60)) / 1000),
        isExpired: false,
      });
    };

    update();
    const interval = setInterval(update, 1000);
    return () => clearInterval(interval);
  }, [targetDate, targetTime]);

  const styles = {
    wrapper: {
      backgroundColor: bgColor,
      padding: '24px 32px',
      textAlign: 'center' as const,
      display: 'inline-block',
    },
    title: {
      font: textFont || '600 24px sans-serif',
      color: textColor,
      textDecoration,
      marginBottom: '16px',
    },
  };

  return (
    <div style={styles.wrapper}>
      {title && <div style={styles.title}>{title}</div>}
      {/* Widget content */}
    </div>
  );
};

const customElement = reactToWebComponent(CustomElement, React, ReactDOM, {
  props: {
    title: 'string',
    targetDate: 'string',
    targetTime: 'string',
    bgColor: 'string',
    textColor: 'string',
    font: 'string',
  },
});

export default customElement;

要点:

  • Props 接口使用 camelCase(例如 targetDate、bgColor)
  • reactToWebComponent 配置使用 camelCase 键和 'string' 类型
  • 所有属性都会以字符串形式从 Web 组件传入
  • 使用内联样式,不要导入 CSS
  • 将复杂属性(如 font)从 JSON 字符串解析:const { font: textFont, textDecoration } = JSON.parse(font)
  • 通过 font CSS 简写属性和 textDecoration 属性应用字体
  • 将辅助组件、实用函数和样式提取到单独文件中,以保持代码结构清晰

设置面板模式

import React, { type FC, useState, useEffect, useCallback } from "react";
import { widget } from "@wix/editor";
import {
  SidePanel,
  WixDesignSystemProvider,
  Input,
  FormField,
  TimeInput,
  Box,
} from "@wix/design-system";
import "@wix/design-system/styles.global.css";
import { ColorPickerField } from "./components/ColorPickerField";
import { FontPickerField } from "./components/FontPickerField";
import { parseTimeValue } from "./utils";

const DEFAULT_BG_COLOR = "#0a0e27";
const DEFAULT_TEXT_COLOR = "#00ff88";
const DEFAULT_TEXT_FONT = "";
const DEFAULT_TEXT_DECORATION = "";

const Panel: FC = () => {
  const [title, setTitle] = useState<string>("Countdown");
  const [targetDate, setTargetDate] = useState<string>("");
  const [targetTime, setTargetTime] = useState<string>("00:00");
  const [bgColor, setBgColor] = useState<string>(DEFAULT_BG_COLOR);
  const [textColor, setTextColor] = useState<string>(DEFAULT_TEXT_COLOR);
  const [font, setFont] = useState({ font: DEFAULT_TEXT_FONT, textDecoration: DEFAULT_TEXT_DECORATION });

  useEffect(() => {
    Promise.all([
      widget.getProp("title"),
      widget.getProp("target-date"),
      widget.getProp("target-time"),
      widget.getProp("bg-color"),
      widget.getProp("text-color"),
      widget.getProp("font"),
    ])
      .then(([titleVal, dateVal, timeVal, bgColorVal, textColorVal, fontString]) => {
        setTitle(titleVal || "Countdown");
        setTargetDate(dateVal || "");
        setTargetTime(timeVal || "00:00");
        setBgColor(bgColorVal || DEFAULT_BG_COLOR);
        setTextColor(textColorVal || DEFAULT_TEXT_COLOR);
        setFont(JSON.parse(fontString || "{}"));
      })
      .catch((error) => console.error("Failed to fetch widget properties:", error));
  }, []);

  const handleTitleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    const newTitle = event.target.value;
    setTitle(newTitle);
    widget.setProp("title", newTitle);
  }, []);

  const handleDateChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    const newDate = event.target.value;
    setTargetDate(newDate);
    widget.setProp("target-date", newDate);
  }, []);

  const handleTimeChange = useCallback(({ date }: { date: Date }) => {
    if (date) {
      const hours = String(date.getHours()).padStart(2, '0');
      const minutes = String(date.getMinutes()).padStart(2, '0');
      const newTime = `${hours}:${minutes}`;
      setTargetTime(newTime);
      widget.setProp("target-time", newTime);
    }
  }, []);

  const handleBgColorChange = (value: string) => {
    setBgColor(value);
    widget.setProp("bg-color", value);
  };

  const handleTextColorChange = (value: string) => {
    setTextColor(value);
    widget.setProp("text-color", value);
  };

  const handleFontChange = (value: { font: string; textDecoration: string }) => {
    setFont(value);
    widget.setProp("font", JSON.stringify(value));
  };

  return (
    <WixDesignSystemProvider>
      <SidePanel width="300" height="100vh">
        <SidePanel.Header title="Countdown Settings" />
        <SidePanel.Content noPadding stretchVertically>
          <Box direction="vertical" gap="24px">
            <SidePanel.Field>
              <FormField label="Title" required>
                <Input
                  type="text"
                  value={title}
                  onChange={handleTitleChange}
                  placeholder="Enter countdown title"
                />
              </FormField>
            </SidePanel.Field>

            <SidePanel.Field>
              <FormField label="Target Date" required>
                <Input
                  type="date"
                  value={targetDate}
                  onChange={handleDateChange}
                />
              </FormField>
            </SidePanel.Field>

            <SidePanel.Field>
              <FormField label="Target Time" required>
                <TimeInput
                  value={parseTimeValue(targetTime)}
                  onChange={handleTimeChange}
                />
              </FormField>
            </SidePanel.Field>

            <ColorPickerField
              label="Background Color"
              value={bgColor}
              onChange={handleBgColorChange}
            />

            <ColorPickerField
              label="Text Color"
              value={textColor}
              onChange={handleTextColorChange}
            />

            <FontPickerField
              label="Text Font"
              value={font}
              onChange={handleFontChange}
            />
          </Box>
        </SidePanel.Content>
      </SidePanel>
    </WixDesignSystemProvider>
  );
};

export default Panel;

要点:

  • widget.getProp() 和 widget.setProp() 中的属性名称使用 kebab-case(例如 "target-date"、"bg-color")
  • 在 onChange 处理程序中,始终同时更新本地状态和微件属性
  • 将内容包装在 WixDesignSystemProvider > SidePanel > SidePanel.Content 中
  • 使用 @wix/design-system 中的 WDS 组件(参见 references/SETTINGS_PANEL.md)
  • 导入 @wix/design-system/styles.global.css 以应用样式
  • 对于颜色,使用 ColorPickerField 和 @wix/editor 中的 inputs.selectColor(),不要使用 <Input type="color">
  • 对于字体,使用 FontPickerField 和 @wix/editor 中的 inputs.selectFont(),不要使用文本 Input
  • 字体值通过 JSON.stringify() / JSON.parse() 以 JSON 字符串形式存储

属性命名约定

**重要:**各文件中的属性使用不同的命名约定:

文件约定示例
widget.tsx(Props 接口)camelCasetargetDate、bgColor、textColor
panel.tsx(微件 API)kebab-case"target-date"、"bg-color"、"text-color"
reactToWebComponent 配置camelCasetargetDate: 'string'

Web 组件会自动在 camelCase(React 属性)和 kebab-case(HTML 属性)之间进行转换。

Wix Data API 集成

在组件中使用 Wix Data API 时,必须妥善处理 Wix Editor 环境:

import { items } from "@wix/data";
import { window as wixWindow } from "@wix/site-window";

const CustomElement: FC<WidgetProps> = ({ collectionId }) => {
  const [data, setData] = useState(null);
  const [isEditor, setIsEditor] = useState(false);

  useEffect(() => {
    const loadData = async () => {
      const currentViewMode = await wixWindow.viewMode();

      if (currentViewMode === "Editor") {
        // Don't fetch data in editor - show placeholder
        setIsEditor(true);
        return;
      }

      // Fetch real data only on live site
      try {
        const results = await items.query(collectionId).limit(10).find();
        setData(results.items);
      } catch (error) {
        console.error("Failed to load data:", error);
      }
    };

    loadData();
  }, [collectionId]);

  if (isEditor) {
    return (
      <div style={{ padding: "20px", border: "2px dashed #ccc" }}>
        <p>Widget will display data on the live site</p>
        <p>Collection: {collectionId}</p>
      </div>
    );
  }

  // Render widget with real data
  return (
    <div>
      {data?.map((item) => (
        <div key={item._id}>{item.title}</div>
      ))}
    </div>
  );
};

要求:

  • 从 "@wix/site-window" 导入 { window as wixWindow }
  • 获取数据前检查 await wixWindow.viewMode()
  • 如果 viewMode === 'Editor',则显示占位 UI
  • 仅在非编辑器模式下获取并渲染真实数据

颜色选择

在设置面板中选择颜色时,使用 ColorPickerField 组件和来自 @wix/editor 的 inputs.selectColor()。不要使用 <Input type="color">。

// components/ColorPickerField.tsx
import React, { type FC } from 'react';
import { inputs } from '@wix/editor';
import { FormField, Box, FillPreview, SidePanel } from '@wix/design-system';

interface ColorPickerFieldProps {
  label: string;
  value: string;
  onChange: (value: string) => void;
}

export const ColorPickerField: FC<ColorPickerFieldProps> = ({
  label,
  value,
  onChange,
}) => (
  <SidePanel.Field>
    <FormField label={label}>
      <Box width="30px" height="30px">
        <FillPreview
          fill={value}
          onClick={() => inputs.selectColor(value, { onChange: (val) => { if (val) onChange(val); } })}
        />
      </Box>
    </FormField>
  </SidePanel.Field>
);

面板中的用法:

const handleBgColorChange = (value: string) => {
  setBgColor(value);
  widget.setProp("bg-color", value);
};

<ColorPickerField label="Background Color" value={bgColor} onChange={handleBgColorChange} />

**重要:**将来自 @wix/editor 的 inputs.selectColor(value, { onChange }) 与 WDS 中的 FillPreview 配合使用。这会打开 Wix 原生颜色选择器,其中包含主题颜色、渐变等选项。切勿使用 <Input type="color">。

字体选择

在设置面板中选择字体时,使用 FontPickerField 组件和来自 @wix/editor 的 inputs.selectFont()。不要使用文本 Input。

// components/FontPickerField.tsx
import React, { type FC } from 'react';
import { inputs } from '@wix/editor';
import { FormField, Button, Text, SidePanel } from '@wix/design-system';

interface FontValue {
  font: string;
  textDecoration: string;
}

interface FontPickerFieldProps {
  label: string;
  value: FontValue;
  onChange: (value: FontValue) => void;
}

export const FontPickerField: FC<FontPickerFieldProps> = ({
  label,
  value,
  onChange,
}) => (
  <SidePanel.Field>
    <FormField label={label}>
      <Button
        size="small"
        priority="secondary"
        onClick={() => inputs.selectFont(value, { onChange: (val) => onChange({ font: val.font, textDecoration: val.textDecoration || "" }) })}
        fullWidth
      >
        <Text size="small" ellipsis>Change Font</Text>
      </Button>
    </FormField>
  </SidePanel.Field>
);

面板中的用法:

const [font, setFont] = useState<FontValue>({ font: "", textDecoration: "" });

const handleFontChange = (value: FontValue) => {
  setFont(value);
  widget.setProp("font", JSON.stringify(value));
};

<FontPickerField label="Text Font" value={font} onChange={handleFontChange} />

**重要:**按照回调模式,使用来自 @wix/editor 的 inputs.selectFont(value, { onChange })。这会提供功能丰富的字体选择对话框,包含粗体、斜体、字号和排版功能。字体值以 JSON 字符串形式存储。

输出结构

src/extensions/site/widgets/custom-elements/
└── {widget-name}/
    ├── widget.tsx           # Main widget component
    ├── panel.tsx            # Settings panel component
    ├── extensions.ts         # Extension registration
    ├── components/          # Optional sub-components
    │   ├── ColorPickerField.tsx
    │   └── FontPickerField.tsx
    └── utils/               # Optional helper functions
        └── formatters.ts

示例

倒计时器组件

请求:“创建一个倒计时器组件”

输出:

  • 带有可配置标题、目标日期/时间、颜色和字体的组件
  • 包含日期选择器、时间输入、颜色选择器和字体选择器的设置面板
  • 实时倒计时显示天、小时、分钟和秒
产品展示组件

请求:“创建一个显示集合中产品的组件”

输出:

  • 查询 Wix Data 集合的组件
  • 处理编辑器环境(在编辑器中显示占位内容)
  • 用于选择集合、显示选项和样式的设置面板
  • 带有产品卡片的响应式网格布局
交互式计算器组件

请求:“创建一个可自定义颜色的计算器组件”

输出:

  • 功能完整的计算器组件
  • 用于自定义颜色(背景、按钮、文本)的设置面板
  • 所有样式均使用内联样式
  • 无外部依赖项

前端美学

避免使用千篇一律的美学设计。打造独具特色的设计,采用独特字体(避免使用 Inter、Roboto、Arial)、协调的配色方案、用于微交互的 CSS 动画,以及符合具体情境的设计选择。不要使用陈词滥调的配色方案或可预见的布局。

扩展注册

扩展注册是必需的,且必须完成两个步骤。

步骤 1:创建组件专属的扩展文件

每个站点组件都需要在其文件夹中包含一个 extensions.ts 文件:

import { extensions } from "@wix/astro/builders";

export const sitewidgetMyWidget = extensions.customElement({
  id: "{{GENERATE_UUID}}",
  name: "My Widget",
  tagName: "my-widget",
  element: "./extensions/site/widgets/custom-elements/my-widget/widget.tsx",
  settings: "./extensions/site/widgets/custom-elements/my-widget/panel.tsx",
  installation: {
    autoAdd: true,
  },
  width: {
    defaultWidth: 500,
    allowStretch: true,
  },
  height: {
    defaultHeight: 500,
  },
});

关键:UUID 生成

id 必须是唯一且固定的 UUID v4 字符串。为每个扩展生成新的 UUID——不要使用 randomUUID(),也不要复制示例中的 UUID。将 {{GENERATE_UUID}} 替换为新生成的 UUID,例如 "a1b2c3d4-e5f6-7890-abcd-ef1234567890"。

属性类型说明
idstring唯一且固定的 UUID v4(生成新的 UUID)
namestring编辑器中显示的名称
tagNamestringHTML 自定义元素标签(短横线命名法)
elementstring组件 React 组件的路径
settingsstring设置面板组件的路径
installationobject自动添加行为
widthobject默认宽度和拉伸设置
heightobject默认高度设置
步骤 2:在主扩展文件中注册

重要: 创建特定于小组件的扩展文件后,你必须阅读 wix-cli-extension-registration,并遵循其中的“应用注册”部分,更新 src/extensions.ts。

未完成步骤 2,网站小组件将无法在 Wix Editor 中使用。

代码质量要求

  • 严格使用 TypeScript(不使用 any,显式声明返回类型)
  • 使用带有 hooks 的函数式 React 组件
  • 妥善处理错误和加载状态
  • 不添加 @ts-ignore 注释
  • 仅使用内联样式(不导入 CSS)
  • 使用 Wix Data API 时,处理 Wix Editor 环境
  • 统一的属性命名(小组件中使用 camelCase,面板中使用 kebab-case)

评论

支持 Markdown