关于 wix-cli-site-widget
wix-cli-site-widget 是用于 Wix CLI 应用的开发技能,指导创建可在 Wix Editor 中添加和配置的站点小组件。它将 React 组件转换为 Web 组件,并包含侧边设置面板、属性映射和扩展注册说明。
主要功能
- 01React 到 Web 组件转换
- 02Wix Editor 设置面板
- 03组件属性配置
- 04颜色与字体选择器
- 05Wix Data 编辑器环境处理
适用场景
- 01创建倒计时小组件
- 02从集合展示商品
- 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 中,让网站所有者可以将交互式、可配置的微件添加到页面,并使用内置设置面板进行配置。
快速入门清单
创建网站微件时,请按顺序执行以下步骤:
- 创建微件文件夹:
src/extensions/site/widgets/custom-elements/<widget-name>/ - 创建
widget.tsx,其中包含 React 组件和reactToWebComponent转换 - 创建
panel.tsx,其中包含 WDS 组件和widget.getProp/setProp - 创建
extensions.ts,其中包含extensions.customElement()和唯一 UUID - 更新
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) - 通过
fontCSS 简写属性和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 接口) | camelCase | targetDate、bgColor、textColor |
panel.tsx(微件 API) | kebab-case | "target-date"、"bg-color"、"text-color" |
reactToWebComponent 配置 | camelCase | targetDate: '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"。
| 属性 | 类型 | 说明 |
|---|---|---|
id | string | 唯一且固定的 UUID v4(生成新的 UUID) |
name | string | 编辑器中显示的名称 |
tagName | string | HTML 自定义元素标签(短横线命名法) |
element | string | 组件 React 组件的路径 |
settings | string | 设置面板组件的路径 |
installation | object | 自动添加行为 |
width | object | 默认宽度和拉伸设置 |
height | object | 默认高度设置 |
步骤 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)