feat: sync full workspace including web modules, docs, and configurations to Gitea

Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-09 18:12:25 +08:00
parent 351688006e
commit a27e3b8e43
1510 changed files with 162044 additions and 1517 deletions

View File

@@ -0,0 +1,36 @@
/**
* ThemeShell 图标组件
*/
import React from 'react';
export const Icons = {
Menu: () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
),
Collapse: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7" />
<polyline points="18 17 13 12 18 7" />
</svg>
),
Expand: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7" />
<polyline points="6 17 11 12 6 7" />
</svg>
),
Close: () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
),
};

View File

@@ -0,0 +1,130 @@
/**
* MarkdownViewer - 轻量级 Markdown 渲染组件
*
* 零依赖,支持基础 Markdown 语法:
* - 标题 (h1-h3)
* - 列表 (有序/无序)
* - 引用
* - 行内代码
* - 加粗
*/
import React from 'react';
export interface MarkdownViewerProps {
content: string;
className?: string;
}
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, className }) => {
// 解析行内样式
const parseInlineStyles = (text: string): React.ReactNode => {
// 处理行内代码 `code`
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code
key={i}
style={{
backgroundColor: 'var(--theme-bg-tertiary, rgba(0, 0, 0, 0.04))',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '0.9em',
fontFamily: 'Monaco, Consolas, monospace',
color: 'var(--theme-text-primary, #1a1a1a)',
}}
>
{part.slice(1, -1)}
</code>
);
}
// 处理加粗 **text**
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((bp, j) => {
if (bp.startsWith('**') && bp.endsWith('**')) {
return <strong key={`${i}-${j}`}>{bp.slice(2, -2)}</strong>;
}
return bp;
});
});
};
const parseLine = (line: string, index: number): React.ReactNode => {
// H3
if (line.startsWith('### ')) {
return (
<h3 key={index} style={{ fontSize: '18px', fontWeight: 600, marginTop: '24px', marginBottom: '12px', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(4))}
</h3>
);
}
// H2
if (line.startsWith('## ')) {
return (
<h2 key={index} style={{ fontSize: '22px', fontWeight: 600, marginTop: '32px', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid var(--theme-border-light, rgba(0,0,0,0.06))', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(3))}
</h2>
);
}
// H1
if (line.startsWith('# ')) {
return (
<h1 key={index} style={{ fontSize: '28px', fontWeight: 600, marginBottom: '20px', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(2))}
</h1>
);
}
// 无序列表
if (line.startsWith('- ')) {
return (
<li key={index} style={{ marginLeft: '20px', marginBottom: '8px', listStyle: 'disc', color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))' }}>
{parseInlineStyles(line.slice(2))}
</li>
);
}
// 有序列表
if (/^\d+\.\s/.test(line)) {
const match = line.match(/^\d+\.\s(.*)$/);
if (match) {
return (
<li key={index} style={{ marginLeft: '20px', marginBottom: '8px', listStyle: 'decimal', color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))' }}>
{parseInlineStyles(match[1])}
</li>
);
}
}
// 引用
if (line.startsWith('> ')) {
return (
<blockquote key={index} style={{ marginLeft: 0, paddingLeft: '16px', borderLeft: '3px solid var(--theme-border, rgba(0,0,0,0.1))', color: 'var(--theme-text-tertiary, rgba(0,0,0,0.55))', fontStyle: 'italic', margin: '16px 0' }}>
{parseInlineStyles(line.slice(2))}
</blockquote>
);
}
// 分隔线
if (line.trim() === '---') {
return <hr key={index} style={{ border: 'none', borderTop: '1px solid var(--theme-border-light, rgba(0,0,0,0.06))', margin: '24px 0' }} />;
}
// 空行
if (line.trim() === '') {
return <div key={index} style={{ height: '12px' }} />;
}
// 普通段落
return (
<p key={index} style={{ color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))', lineHeight: 1.7, marginBottom: '12px' }}>
{parseInlineStyles(line)}
</p>
);
};
const lines = content.split('\n');
return (
<div className={className} style={{ fontFamily: 'inherit' }}>
{lines.map((line, i) => parseLine(line, i))}
</div>
);
};
export default MarkdownViewer;

View File

@@ -0,0 +1,330 @@
/**
* ThemeShell - 去主题化的设计系统展示组件
*
* 核心特性:
* - 零视觉干扰:使用中性色,不抢占内容焦点
* - 完全可配置:标题、分组、导航项均通过 props 配置
* - 响应式支持:桌面端侧边栏 + 移动端抽屉式导航
* - 可折叠侧边栏:支持展开/收起
* - 主题支持:支持深色/浅色主题配置
*/
import React, { useState, useCallback, useMemo, CSSProperties } from 'react';
import { ThemeShellProps, NavGroup, NavItem, ThemeColors } from './types';
import { createShellStyles, getThemeColors } from './styles';
import { Icons } from './Icons';
// 单个导航项组件 - 使用独立的 hover 状态
const NavItemButton: React.FC<{
item: NavItem;
isActive: boolean;
onNavigate: (id: string) => void;
styles: Record<string, CSSProperties>;
colors: ThemeColors;
}> = ({ item, isActive, onNavigate, styles, colors }) => {
const [isHovered, setIsHovered] = useState(false);
// 点击后清除 hover 状态
const handleClick = () => {
setIsHovered(false);
onNavigate(item.id);
};
// 只有当前激活项显示激活样式,其他项只在 hover 时显示 hover 样式
const style: CSSProperties = {
...styles.navItem,
...(isActive ? styles.navItemActive : {}),
...(!isActive && isHovered ? styles.navItemHover : {}),
};
return (
<li>
<button
onClick={handleClick}
style={style}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{item.icon && <span style={{ marginRight: 8 }}>{item.icon}</span>}
{item.label}
</button>
</li>
);
};
export const ThemeShell: React.FC<ThemeShellProps> = ({
brand,
groups,
items,
activeId,
onNavigate,
sidebar,
theme,
children,
header,
className,
style,
}) => {
const [sidebarOpen, setSidebarOpen] = useState(sidebar?.defaultOpen ?? true);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const sidebarWidth = sidebar?.width ?? 256;
const collapsible = sidebar?.collapsible ?? true;
// 计算主题颜色和样式
const themeColors = useMemo(() =>
getThemeColors(theme?.mode ?? 'light', theme?.colors),
[theme?.mode, theme?.colors]
);
const STYLES = useMemo(() =>
createShellStyles(themeColors),
[themeColors]
);
// 按分组组织导航项
const sortedGroups = [...groups].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
const getItemsByGroup = useCallback(
(groupId: string): NavItem[] => items.filter(item => item.groupId === groupId),
[items]
);
// 处理导航点击
const handleNavigate = useCallback(
(id: string) => {
onNavigate(id);
setMobileMenuOpen(false);
},
[onNavigate]
);
// 渲染单个导航项
const renderNavItem = (item: NavItem) => {
const isActive = item.id === activeId;
return (
<NavItemButton
key={item.id}
item={item}
isActive={isActive}
onNavigate={handleNavigate}
styles={STYLES}
colors={themeColors}
/>
);
};
// 渲染导航分组
const renderNavGroup = (group: NavGroup) => {
const groupItems = getItemsByGroup(group.id);
if (groupItems.length === 0) return null;
return (
<div key={group.id} style={STYLES.navGroup}>
<div style={STYLES.navGroupTitle}>{group.title}</div>
<ul style={STYLES.navList}>
{groupItems.map(renderNavItem)}
</ul>
</div>
);
};
// 渲染品牌区域
const renderBrand = () => {
if (!brand) return null;
return (
<div style={STYLES.brandArea}>
<div style={STYLES.brandContent as CSSProperties}>
<div style={STYLES.brandText as CSSProperties}>
<span style={STYLES.brandName}>{brand.name}</span>
{brand.subtitle && (
<span style={STYLES.brandSubtitle}>{brand.subtitle}</span>
)}
</div>
</div>
{collapsible && (
<button
onClick={() => setSidebarOpen(false)}
style={STYLES.collapseBtn}
title="收起侧边栏"
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgHover;
e.currentTarget.style.color = themeColors.textPrimary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = themeColors.textTertiary;
}}
>
<Icons.Collapse />
</button>
)}
</div>
);
};
// 渲染侧边栏内容
const renderSidebarContent = () => (
<>
{renderBrand()}
<nav className="theme-shell-scroll" style={STYLES.navArea as CSSProperties}>
{sortedGroups.map(renderNavGroup)}
</nav>
</>
);
// 桌面端侧边栏样式
const desktopSidebarStyle: CSSProperties = {
...STYLES.sidebar as CSSProperties,
...(sidebarOpen
? { ...STYLES.sidebarOpen, width: sidebarWidth }
: STYLES.sidebarClosed),
};
// 移动端侧边栏样式
const mobileSidebarStyle: CSSProperties = {
...STYLES.mobileSidebar as CSSProperties,
...(mobileMenuOpen ? STYLES.mobileSidebarOpen : {}),
};
return (
<div
className={className}
style={{
...STYLES.root as CSSProperties,
'--theme-text-primary': themeColors.textPrimary,
'--theme-text-secondary': themeColors.textSecondary,
'--theme-text-tertiary': themeColors.textTertiary,
'--theme-text-muted': themeColors.textMuted,
'--theme-border': themeColors.border,
'--theme-border-light': themeColors.borderLight,
'--theme-bg-secondary': themeColors.bgSecondary,
'--theme-bg-tertiary': themeColors.bgTertiary,
...style,
} as CSSProperties}
>
<style>{`
.theme-shell-scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
.theme-shell-scroll::-webkit-scrollbar {
width: 0;
height: 0;
}
.theme-shell-scroll::-webkit-scrollbar-track {
background: transparent;
}
.theme-shell-scroll::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 0;
}
.theme-shell-scroll:hover::-webkit-scrollbar-thumb {
background-color: transparent;
}
`}</style>
{/* 移动端顶栏 */}
<div
className="theme-shell-mobile-topbar"
style={{
...STYLES.mobileTopbar as CSSProperties,
display: 'none', // 默认隐藏CSS 媒体查询控制
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{brand && <span style={{ fontWeight: 600, fontSize: 15 }}>{brand.name}</span>}
</div>
<button
onClick={() => setMobileMenuOpen(true)}
style={{
background: 'none',
border: 'none',
padding: 8,
cursor: 'pointer',
color: themeColors.textSecondary,
}}
>
<Icons.Menu />
</button>
</div>
{/* 移动端遮罩 */}
{mobileMenuOpen && (
<div
style={STYLES.mobileOverlay as CSSProperties}
onClick={() => setMobileMenuOpen(false)}
/>
)}
{/* 移动端侧边栏 */}
<aside className="theme-shell-mobile-sidebar" style={mobileSidebarStyle}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ ...STYLES.brandArea as CSSProperties, justifyContent: 'space-between' }}>
{brand && (
<div style={STYLES.brandContent as CSSProperties}>
<div style={STYLES.brandText as CSSProperties}>
<span style={STYLES.brandName}>{brand.name}</span>
{brand.subtitle && (
<span style={STYLES.brandSubtitle}>{brand.subtitle}</span>
)}
</div>
</div>
)}
<button
onClick={() => setMobileMenuOpen(false)}
style={{
background: 'none',
border: 'none',
padding: 4,
cursor: 'pointer',
color: themeColors.textTertiary,
}}
>
<Icons.Close />
</button>
</div>
<nav className="theme-shell-scroll" style={STYLES.navArea as CSSProperties}>
{sortedGroups.map(renderNavGroup)}
</nav>
</div>
</aside>
{/* 桌面端侧边栏 */}
<aside className="theme-shell-sidebar" style={desktopSidebarStyle}>
{renderSidebarContent()}
</aside>
{/* 展开按钮(侧边栏关闭时) */}
{!sidebarOpen && collapsible && (
<button
className="theme-shell-expand-btn"
onClick={() => setSidebarOpen(true)}
style={STYLES.expandBtn as CSSProperties}
title="展开侧边栏"
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgHover;
e.currentTarget.style.color = themeColors.textPrimary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgPrimary;
e.currentTarget.style.color = themeColors.textSecondary;
}}
>
<Icons.Expand />
</button>
)}
{/* 主内容区 */}
<main style={STYLES.main as CSSProperties}>
{/* 可选头部区域 */}
{header && <div style={STYLES.header as CSSProperties}>{header}</div>}
{/* 内容区 */}
<div style={STYLES.content as CSSProperties}>{children}</div>
</main>
</div>
);
};
export default ThemeShell;

View File

@@ -0,0 +1,10 @@
/**
* ThemeShell - 去主题化的设计系统展示组件
*
* 导出模块
*/
export { ThemeShell, default } from './ThemeShell';
export * from './types';
export { MarkdownViewer } from './MarkdownViewer';
export type { MarkdownViewerProps } from './MarkdownViewer';

View File

@@ -0,0 +1,314 @@
/**
* ThemeShell 样式定义
*
* 支持深色/浅色主题 - 使用中性色,减少视觉干扰
*/
import { CSSProperties } from 'react';
import { ThemeColors, ThemeMode } from './types';
// 浅色主题色板
export const LIGHT_COLORS: ThemeColors = {
// 文字
textPrimary: '#1a1a1a',
textSecondary: '#666666',
textTertiary: '#999999',
textMuted: '#b3b3b3',
// 背景
bgPrimary: '#ffffff',
bgSecondary: '#fafafa',
bgTertiary: '#f5f5f5',
bgHover: '#f0f0f0',
bgActive: '#e8e8e8',
// 边框
border: '#e5e5e5',
borderLight: '#f0f0f0',
// 状态指示
activeIndicator: '#1a1a1a',
};
// 深色主题色板
export const DARK_COLORS: ThemeColors = {
// 文字
textPrimary: '#f5f5f5',
textSecondary: '#a0a0a0',
textTertiary: '#707070',
textMuted: '#505050',
// 背景
bgPrimary: '#1a1a1a',
bgSecondary: '#141414',
bgTertiary: '#242424',
bgHover: '#2a2a2a',
bgActive: '#333333',
// 边框
border: '#333333',
borderLight: '#2a2a2a',
// 状态指示
activeIndicator: '#f5f5f5',
};
// 获取主题颜色
export function getThemeColors(mode: ThemeMode = 'light', customColors?: Partial<ThemeColors>): ThemeColors {
const baseColors = mode === 'dark' ? DARK_COLORS : LIGHT_COLORS;
return customColors ? { ...baseColors, ...customColors } : baseColors;
}
// 生成样式
export function createShellStyles(colors: ThemeColors): Record<string, CSSProperties> {
return {
// 根容器
root: {
display: 'flex',
minHeight: '100vh',
height: '100vh',
backgroundColor: colors.bgSecondary,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
color: colors.textPrimary,
overflow: 'hidden',
},
// 侧边栏
sidebar: {
display: 'flex',
flexDirection: 'column',
backgroundColor: colors.bgPrimary,
borderRight: `1px solid ${colors.border}`,
transition: 'width 0.2s ease, opacity 0.2s ease',
overflow: 'hidden',
},
sidebarOpen: {
width: 256,
opacity: 1,
},
sidebarClosed: {
width: 0,
opacity: 0,
borderRight: 'none',
},
// 品牌区域
brandArea: {
padding: '20px 16px',
borderBottom: `1px solid ${colors.borderLight}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minWidth: 224,
},
brandContent: {
display: 'flex',
alignItems: 'center',
gap: 12,
},
brandLogo: {
width: 36,
height: 36,
borderRadius: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
flexShrink: 0,
},
brandText: {
display: 'flex',
flexDirection: 'column',
gap: 2,
},
brandName: {
fontSize: 15,
fontWeight: 600,
color: colors.textPrimary,
lineHeight: 1.2,
},
brandSubtitle: {
fontSize: 11,
color: colors.textTertiary,
letterSpacing: '0.02em',
},
// 折叠按钮
collapseBtn: {
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
background: 'transparent',
color: colors.textTertiary,
cursor: 'pointer',
borderRadius: 6,
transition: 'all 0.15s ease',
flexShrink: 0,
},
collapseBtnHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
// 导航区域
navArea: {
flex: 1,
overflowY: 'auto',
padding: '16px 0',
minWidth: 224,
},
// 分组
navGroup: {
marginBottom: 24,
},
navGroupTitle: {
padding: '0 16px',
marginBottom: 8,
fontSize: 11,
fontWeight: 600,
color: colors.textMuted,
letterSpacing: '0.08em',
textTransform: 'uppercase',
},
navList: {
listStyle: 'none',
margin: 0,
padding: 0,
},
// 导航项
navItem: {
display: 'block',
width: '100%',
textAlign: 'left',
padding: '8px 16px',
border: 'none',
background: 'transparent',
fontSize: 14,
color: colors.textSecondary,
cursor: 'pointer',
transition: 'all 0.15s ease',
},
navItemHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
navItemActive: {
backgroundColor: colors.bgTertiary,
color: colors.textPrimary,
fontWeight: 500,
},
// 主内容区
main: {
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minHeight: 0,
},
// 头部区域(可选)
header: {
flexShrink: 0,
padding: '16px 24px',
backgroundColor: colors.bgPrimary,
borderBottom: `1px solid ${colors.borderLight}`,
},
// 内容区域
content: {
flex: 1,
overflowY: 'auto',
minHeight: 0,
padding: '24px 32px',
},
// 展开按钮(侧边栏关闭时)
expandBtn: {
position: 'fixed',
top: 20,
left: 20,
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.bgPrimary,
border: `1px solid ${colors.border}`,
borderRadius: 8,
cursor: 'pointer',
color: colors.textSecondary,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
transition: 'all 0.15s ease',
},
expandBtnHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
// 移动端顶栏
mobileTopbar: {
display: 'none', // 默认隐藏,通过媒体查询显示
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 50,
height: 56,
padding: '0 16px',
backgroundColor: colors.bgPrimary,
borderBottom: `1px solid ${colors.border}`,
alignItems: 'center',
justifyContent: 'space-between',
},
// 移动端遮罩
mobileOverlay: {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 99,
},
// 移动端侧边栏
mobileSidebar: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
width: 280,
zIndex: 100,
backgroundColor: colors.bgPrimary,
boxShadow: '4px 0 24px rgba(0,0,0,0.12)',
transform: 'translateX(-100%)',
transition: 'transform 0.3s ease',
},
mobileSidebarOpen: {
transform: 'translateX(0)',
},
};
}
// 保持向后兼容 - 默认使用浅色主题
export const SHELL_STYLES = createShellStyles(LIGHT_COLORS);

View File

@@ -0,0 +1,118 @@
/**
* ThemeShell 类型定义
*
* 去主题化的设计系统展示组件类型
*/
/** 主题模式 */
export type ThemeMode = 'light' | 'dark';
/** 主题颜色配置 */
export interface ThemeColors {
/** 主要文字色 */
textPrimary: string;
/** 次要文字色 */
textSecondary: string;
/** 三级文字色 */
textTertiary: string;
/** 静音文字色 */
textMuted: string;
/** 主背景色 */
bgPrimary: string;
/** 次要背景色 */
bgSecondary: string;
/** 三级背景色 */
bgTertiary: string;
/** 悬浮背景色 */
bgHover: string;
/** 激活背景色 */
bgActive: string;
/** 边框色 */
border: string;
/** 浅边框色 */
borderLight: string;
/** 激活指示器色 */
activeIndicator: string;
}
/** 主题配置 */
export interface ThemeConfig {
/** 主题模式 */
mode?: ThemeMode;
/** 自定义颜色(覆盖默认值) */
colors?: Partial<ThemeColors>;
}
/** 导航项 */
export interface NavItem {
/** 唯一标识 */
id: string;
/** 显示标签 */
label: string;
/** 所属分组 ID */
groupId: string;
/** 可选描述 */
description?: string;
/** 可选图标React 节点) */
icon?: React.ReactNode;
}
/** 导航分组 */
export interface NavGroup {
/** 分组唯一标识 */
id: string;
/** 分组标题 */
title: string;
/** 分组排序权重(越小越靠前) */
order?: number;
}
/** 品牌配置 */
export interface BrandConfig {
/** 品牌名称 */
name: string;
/** 副标题 */
subtitle?: string;
/** Logo 图标React 节点) */
logo?: React.ReactNode;
/** Logo 背景色 */
logoBgColor?: string;
/** Logo 文字色 */
logoTextColor?: string;
}
/** 侧边栏配置 */
export interface SidebarConfig {
/** 默认宽度 */
width?: number;
/** 是否默认展开 */
defaultOpen?: boolean;
/** 是否允许折叠 */
collapsible?: boolean;
}
/** ThemeShell 组件属性 */
export interface ThemeShellProps {
/** 品牌配置 */
brand?: BrandConfig;
/** 导航分组列表 */
groups: NavGroup[];
/** 导航项列表 */
items: NavItem[];
/** 当前激活项 ID */
activeId: string;
/** 导航切换回调 */
onNavigate: (id: string) => void;
/** 侧边栏配置 */
sidebar?: SidebarConfig;
/** 主题配置 */
theme?: ThemeConfig;
/** 内容区域 */
children: React.ReactNode;
/** 顶部额外内容(如比选面板) */
header?: React.ReactNode;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
}

View File

@@ -0,0 +1,863 @@
/**
*本项目Variant Switcher 组件 (重构版)
*
* 核心特性:
* - 零依赖:不依赖任何外部 CSS 框架或图标库
* - 轻量化 UI使用图标入口替代重型控制条
* - 丰富信息:支持标题和描述
* - 全局面板:支持页面级统一管理和跳转
* - 隐形控制:支持快捷键显隐入口
* - 自动全局入口:当有比选组件时自动显示全局入口按钮
*/
import React, { useState, useEffect, useCallback, CSSProperties, useRef } from 'react';
import { createPortal } from 'react-dom';
// --- 类型定义 ---
export interface VariantItem {
/** 唯一标识,若不提供则使用索引 */
key?: string;
/** 渲染内容 */
content: React.ReactNode;
/** 方案标题 */
title: string;
/** 方案一句话描述 */
description: string;
/** 方案详细说明文档Markdown 格式) */
markdown?: string;
}
export interface VariantAPI {
id: string;
/** 比选方案的中文名称,用于在全局面板中显示 */
name: string;
currentIndex: number;
totalVariants: number;
isDecided: boolean;
variants: VariantItem[]; // 暴露方案详情供全局面板使用
select: (index: number) => void;
confirm: () => void;
reset: () => void;
focus: () => void; // 聚焦到该组件(滚动)
}
declare global {
interface Window {
AXHUB_VARIANT_MANAGER?: VariantManager;
}
}
type Listener = () => void;
export interface VariantManager {
register: (id: string, api: VariantAPI) => void;
unregister: (id: string) => void;
instances: Record<string, VariantAPI>;
subscribe: (listener: Listener) => () => void;
notify: () => void;
setVisibility: (visible: boolean) => void;
isVisible: boolean;
}
export interface VariantSwitcherProps {
id?: string;
/** 比选方案的中文名称,显示在全局面板中(如"头部设计"、"登录页布局" */
name?: string;
/** 方案列表 */
variants: VariantItem[];
defaultIndex?: number;
onConfirm?: (index: number, item: VariantItem) => void;
onReset?: () => void;
style?: CSSProperties;
className?: string;
}
// --- 图标定义 ---
const Icons = {
Switcher: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
),
Check: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
Close: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
),
// 比选图标:两个重叠的卡片,表示多个方案比选
VariantCompare: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{/* 底层卡片 */}
<rect x="4" y="6" width="12" height="10" rx="2" opacity="0.4" />
{/* 顶层卡片(偏移) */}
<rect x="8" y="4" width="12" height="10" rx="2" />
</svg>
),
Target: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
</svg>
),
Exit: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
),
Doc: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
),
Back: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
)
};
// --- 主题配置 ---
const THEME_COLOR = '#008F5D';
const THEME_COLOR_BG = 'rgba(0, 143, 93, 0.1)';
// --- 内置 Markdown 渲染器(零依赖) ---
const MarkdownViewer: React.FC<{ content: string }> = ({ content }) => {
const parseInlineStyles = (text: string): React.ReactNode => {
// 处理行内代码 `code`
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '13px',
fontFamily: 'Monaco, Consolas, monospace',
color: '#e83e8c'
}}>
{part.slice(1, -1)}
</code>
);
}
// 处理加粗 **text**
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((bp, j) => {
if (bp.startsWith('**') && bp.endsWith('**')) {
return <strong key={`${i}-${j}`}>{bp.slice(2, -2)}</strong>;
}
return bp;
});
});
};
const parseLine = (line: string, index: number): React.ReactNode => {
// 标题
if (line.startsWith('### ')) {
return <h3 key={index} style={{ fontSize: '16px', fontWeight: 600, color: '#333', marginTop: '20px', marginBottom: '8px' }}>{parseInlineStyles(line.slice(4))}</h3>;
}
if (line.startsWith('## ')) {
return <h2 key={index} style={{ fontSize: '18px', fontWeight: 600, color: '#333', marginTop: '24px', marginBottom: '12px', paddingBottom: '8px', borderBottom: '1px solid #eee' }}>{parseInlineStyles(line.slice(3))}</h2>;
}
if (line.startsWith('# ')) {
return <h1 key={index} style={{ fontSize: '22px', fontWeight: 600, color: '#333', marginBottom: '16px' }}>{parseInlineStyles(line.slice(2))}</h1>;
}
// 列表
if (line.startsWith('- ')) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'disc' }}>
{parseInlineStyles(line.slice(2))}
</li>
);
}
if (/^\d+\.\s/.test(line)) {
const match = line.match(/^(\d+)\.\s(.*)$/);
if (match) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'decimal' }}>
{parseInlineStyles(match[2])}
</li>
);
}
}
// 引用
if (line.startsWith('> ')) {
return (
<blockquote key={index} style={{ marginLeft: 0, paddingLeft: '12px', borderLeft: `3px solid ${THEME_COLOR}`, color: '#666', fontStyle: 'italic', margin: '12px 0' }}>
{parseInlineStyles(line.slice(2))}
</blockquote>
);
}
// 空行
if (line.trim() === '') {
return <div key={index} style={{ height: '12px' }} />;
}
// 普通段落
return <p key={index} style={{ color: '#555', lineHeight: 1.7, marginBottom: '12px' }}>{parseInlineStyles(line)}</p>;
};
const lines = content.split('\n');
return (
<div style={{ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: '14px' }}>
{lines.map((line, i) => parseLine(line, i))}
</div>
);
};
// --- 全局管理器实现 ---
const listeners: Listener[] = [];
let globalVisible = true;
function initGlobalManager(): VariantManager {
if (!window.AXHUB_VARIANT_MANAGER) {
window.AXHUB_VARIANT_MANAGER = {
instances: {},
isVisible: true,
register(id, api) {
this.instances[id] = api;
this.notify();
},
unregister(id) {
delete this.instances[id];
this.notify();
},
subscribe(listener) {
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
};
},
notify() {
this.isVisible = globalVisible;
listeners.forEach(fn => fn());
},
setVisibility(visible) {
globalVisible = visible;
this.notify();
}
};
}
return window.AXHUB_VARIANT_MANAGER;
}
// --- Hooks ---
/** 获取所有注册的实例及全局可见性 */
function useVariantManager() {
const [state, setState] = useState<{
instances: Record<string, VariantAPI>;
isVisible: boolean;
}>({ instances: {}, isVisible: true });
useEffect(() => {
const manager = initGlobalManager();
const update = () => setState({
instances: { ...manager.instances },
isVisible: manager.isVisible
});
update();
return manager.subscribe(update);
}, []);
return state;
}
// --- 样式定义 ---
const STYLES = {
container: {
position: 'relative' as const,
width: '100%',
height: '100%',
},
triggerBtn: {
position: 'absolute' as const,
top: '4px',
right: '4px',
zIndex: 9001,
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(0, 0, 0, 0.08)',
borderRadius: '2px',
color: '#666',
cursor: 'pointer',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
transition: 'all 0.2s',
},
popover: {
position: 'absolute' as const,
top: '32px',
right: '0px',
width: '260px',
backgroundColor: '#fff',
borderRadius: '2px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)',
padding: '4px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column' as const,
gap: '2px',
opacity: 0,
transform: 'translateY(-4px)',
pointerEvents: 'none' as const,
transition: 'all 0.15s ease-out',
},
popoverVisible: {
opacity: 1,
transform: 'translateY(0)',
pointerEvents: 'auto' as const,
},
variantCard: {
display: 'flex',
flexDirection: 'column' as const,
padding: '8px 10px',
borderRadius: '0',
cursor: 'pointer',
border: 'none',
transition: 'background 0.2s',
textAlign: 'left' as const,
background: 'transparent',
},
variantCardActive: {
background: THEME_COLOR_BG,
border: 'none',
},
variantTitle: {
fontSize: '13px',
fontWeight: 500,
color: '#333',
marginBottom: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
variantDesc: {
fontSize: '12px',
color: '#888',
lineHeight: '1.4',
},
globalTrigger: {
position: 'fixed' as const,
bottom: '24px',
right: '24px',
zIndex: 99999,
width: '32px',
height: '32px',
borderRadius: '16px',
backgroundColor: '#fff',
color: '#555',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
cursor: 'pointer',
border: '1px solid rgba(0,0,0,0.05)',
transition: 'transform 0.2s, opacity 0.2s',
},
globalPanel: {
position: 'fixed' as const,
top: 0,
right: 0,
bottom: 0,
width: '300px',
backgroundColor: '#fff',
boxShadow: '-4px 0 24px rgba(0,0,0,0.08)',
zIndex: 100000,
padding: '0',
display: 'flex',
flexDirection: 'column' as const,
transform: 'translateX(100%)',
transition: 'transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)',
},
globalPanelVisible: {
transform: 'translateX(0)',
},
globalPanelHeader: {
padding: '16px',
borderBottom: '1px solid #f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: '15px',
fontWeight: 600,
color: '#333',
},
globalPanelContent: {
flex: 1,
overflowY: 'auto' as const,
padding: '16px',
},
globalPanelFooter: {
padding: '12px 16px',
borderTop: '1px solid #f5f5f5',
display: 'flex',
justifyContent: 'center',
},
nodeGroup: {
marginBottom: '16px',
border: '1px solid #eee',
borderRadius: '0',
overflow: 'hidden',
},
nodeHeader: {
padding: '6px 10px',
backgroundColor: '#fafafa',
borderBottom: '1px solid #eee',
fontSize: '12px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
color: '#666',
},
exitBtn: {
display: 'flex',
alignItems: 'center',
gap: '6px',
background: 'none',
border: 'none',
color: '#999',
fontSize: '12px',
cursor: 'pointer',
padding: '8px',
borderRadius: '0',
transition: 'all 0.2s',
}
};
// --- 全局入口组件(单例) ---
let globalControlMountRef = { current: false };
/** 全局入口控制组件 */
const GlobalVariantControl: React.FC = () => {
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [docView, setDocView] = useState<{ title: string; content: string } | null>(null);
const { instances: allInstances, isVisible } = useVariantManager();
// 键盘快捷键监听
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === '.') {
e.preventDefault();
initGlobalManager().setVisibility(!isVisible);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isVisible]);
const instancesList = Object.values(allInstances);
// 按 id 字母顺序排序,保持稳定的显示顺序
const sortedInstances = [...instancesList].sort((a, b) => a.id.localeCompare(b.id));
// 如果没有实例或不可见,则不渲染
if (!isVisible || sortedInstances.length === 0) {
return null;
}
return (
<>
{/* 全局悬浮球 */}
<button
style={STYLES.globalTrigger}
onClick={() => setIsPanelOpen(true)}
title="方案比选 (Ctrl + .)"
>
<Icons.VariantCompare />
</button>
{/* 全局侧边栏面板 */}
<div style={{
...STYLES.globalPanel,
...(isPanelOpen ? STYLES.globalPanelVisible : {})
}}>
{/* Header */}
<div style={STYLES.globalPanelHeader}>
{docView ? (
<>
<button
onClick={() => setDocView(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: '#666', display: 'flex', alignItems: 'center', gap: '4px' }}
>
<Icons.Back />
<span style={{ fontSize: '14px' }}></span>
</button>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
) : (
<>
<span></span>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
)}
</div>
{/* Content */}
<div style={STYLES.globalPanelContent}>
{docView ? (
/* 文档视图 - 简洁无风格 */
<div style={{ padding: '0 4px' }}>
<div style={{ fontSize: '13px', color: '#999', marginBottom: '16px' }}>{docView.title}</div>
<MarkdownViewer content={docView.content} />
</div>
) : (
/* 方案列表视图 */
sortedInstances.map(inst => (
<div key={inst.id} style={STYLES.nodeGroup}>
{/* Node Header */}
<div style={STYLES.nodeHeader}>
<span>{inst.name}</span>
<button
onClick={() => {
inst.focus();
setIsPanelOpen(false);
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: THEME_COLOR, display: 'flex', alignItems: 'center', fontSize: '12px'
}}
>
<Icons.Target />
<span style={{ marginLeft: 4 }}></span>
</button>
</div>
{/* Variants List */}
<div>
{inst.variants.map((v, idx) => {
const isActive = inst.currentIndex === idx;
return (
<div
key={v.key || idx}
style={{
...STYLES.variantCard,
...(isActive ? { background: THEME_COLOR_BG } : {}),
borderBottom: '1px solid #f9f9f9',
borderRadius: 0,
}}
>
<div
onClick={() => inst.select(idx)}
style={{ cursor: 'pointer' }}
>
<div style={STYLES.variantTitle}>
<span>{v.title}</span>
{isActive && <span style={{color: THEME_COLOR, fontSize: '12px'}}></span>}
</div>
<div style={STYLES.variantDesc}>{v.description}</div>
</div>
{/* 文档按钮 */}
{v.markdown && (
<button
onClick={(e) => {
e.stopPropagation();
setDocView({ title: v.title, content: v.markdown! });
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: '#999', display: 'flex', alignItems: 'center',
fontSize: '12px', marginTop: '6px', padding: 0
}}
onMouseEnter={e => e.currentTarget.style.color = THEME_COLOR}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Doc />
<span style={{ marginLeft: 4 }}></span>
</button>
)}
</div>
);
})}
</div>
</div>
))
)}
</div>
{/* Footer: Exit Button - 仅在列表视图显示 */}
{!docView && (
<div style={STYLES.globalPanelFooter}>
<button
style={STYLES.exitBtn}
onClick={() => {
initGlobalManager().setVisibility(false);
setIsPanelOpen(false);
}}
title="隐藏比选入口 (Ctrl + . 重新开启)"
onMouseEnter={e => e.currentTarget.style.color = '#333'}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Exit />
<span>退</span>
</button>
</div>
)}
</div>
{/* 遮罩层 */}
{isPanelOpen && (
<div
onClick={() => setIsPanelOpen(false)}
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.1)', zIndex: 99999
}}
/>
)}
</>
);
};
// --- 主组件 ---
export const VariantSwitcher: React.FC<VariantSwitcherProps> = ({
id: propId,
name: propName,
variants = [],
defaultIndex = 0,
onConfirm,
onReset,
style,
className,
}) => {
const [instanceId] = useState(() =>
propId || `axhub_vs_${Math.random().toString(36).substr(2, 9)}`
);
// 如果没有提供 name使用 id 作为显示名称
const displayName = propName || instanceId;
const containerRef = useRef<HTMLDivElement>(null);
const [currentIndex, setCurrentIndex] = useState(defaultIndex);
const [isDecided, setIsDecided] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { isVisible: globalVisible } = useVariantManager();
// 标记当前组件负责渲染全局入口(单例,只渲染一次)
const [isGlobalControlOwner, setIsGlobalControlOwner] = useState(false);
useEffect(() => {
// 如果还没有组件负责渲染全局入口,则当前组件负责
if (!globalControlMountRef.current) {
globalControlMountRef.current = true;
setIsGlobalControlOwner(true);
}
// 组件卸载时,如果当前组件是全局入口的拥有者,则释放
return () => {
if (isGlobalControlOwner) {
globalControlMountRef.current = false;
}
};
}, [isGlobalControlOwner]);
// --- API Methods ---
const select = useCallback((index: number) => {
if (index >= 0 && index < variants.length) {
setCurrentIndex(index);
}
}, [variants.length]);
const confirm = useCallback(() => {
setIsDecided(true);
setIsPopoverOpen(false);
if (variants[currentIndex]) {
onConfirm?.(currentIndex, variants[currentIndex]);
}
}, [currentIndex, variants, onConfirm]);
const reset = useCallback(() => {
setIsDecided(false);
onReset?.();
}, [onReset]);
const focus = useCallback(() => {
if (containerRef.current) {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
setIsHovered(true);
setTimeout(() => setIsHovered(false), 2000);
}
}, []);
// --- 注册到全局 ---
useEffect(() => {
if (variants.length > 0) {
const manager = initGlobalManager();
const api: VariantAPI = {
id: instanceId,
name: displayName,
currentIndex,
totalVariants: variants.length,
isDecided,
variants,
select,
confirm,
reset,
focus,
};
manager.register(instanceId, api);
return () => manager.unregister(instanceId);
}
}, [instanceId, displayName, currentIndex, variants, isDecided, select, confirm, reset, focus]);
// --- 点击外部关闭弹窗 ---
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsPopoverOpen(false);
}
};
if (isPopoverOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isPopoverOpen]);
if (variants.length === 0) return null;
return (
<>
{/* 全局入口(单例,通过 Portal 渲染到 body只由第一个组件渲染 */}
{isGlobalControlOwner && typeof document !== 'undefined' && createPortal(
<GlobalVariantControl />,
document.body
)}
<div
ref={containerRef}
className={className}
style={{ ...STYLES.container, ...style }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
data-axhub-variant-id={instanceId}
>
{/* 渲染当前方案内容 */}
{variants[currentIndex]?.content}
{/* 触发器图标 (轻量化,悬停显示) */}
{globalVisible && (
<button
style={{
...STYLES.triggerBtn,
opacity: (isHovered || isPopoverOpen) ? 1 : 0,
pointerEvents: (isHovered || isPopoverOpen) ? 'auto' : 'none',
backgroundColor: isPopoverOpen ? THEME_COLOR : '#fff',
color: isPopoverOpen ? '#fff' : '#666',
borderColor: isPopoverOpen ? THEME_COLOR : 'rgba(0,0,0,0.06)'
}}
onClick={(e) => {
e.stopPropagation();
setIsPopoverOpen(!isPopoverOpen);
}}
title="切换方案"
>
<Icons.Switcher />
</button>
)}
{/* 下拉选择面板 */}
{globalVisible && (
<div style={{
...STYLES.popover,
...(isPopoverOpen ? STYLES.popoverVisible : {})
}}>
<div style={{ padding: '4px 8px', fontSize: '12px', color: '#999', fontWeight: 600 }}>
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{variants.map((variant, index) => {
const isActive = index === currentIndex;
return (
<div
key={variant.key || index}
onClick={(e) => {
e.stopPropagation();
select(index);
}}
style={{
...STYLES.variantCard,
...(isActive ? STYLES.variantCardActive : {}),
}}
>
<div style={STYLES.variantTitle}>
{variant.title}
{isActive && <Icons.Check />}
</div>
<div style={STYLES.variantDesc}>
{variant.description}
</div>
</div>
);
})}
</div>
<div style={{ borderTop: '1px solid #f5f5f5', marginTop: '4px', paddingTop: '8px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
{isDecided ? (
<button
onClick={(e) => { e.stopPropagation(); reset(); }}
style={{
background: 'transparent', border: '1px solid #eee', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#666'
}}
>
</button>
) : (
<button
onClick={(e) => { e.stopPropagation(); confirm(); }}
style={{
background: THEME_COLOR, border: 'none', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#fff'
}}
>
</button>
)}
</div>
</div>
)}
</div>
</>
);
};
export default VariantSwitcher;

View File

@@ -0,0 +1,237 @@
/**
*Axue渲染引擎共类型定义
* 包含所有组件和页面共用的类型、接口和工具函数
*/
// ============ 基础类型定义 ============
// 小写字母 a-z
type Lower =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g'
| 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n'
| 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u'
| 'v' | 'w' | 'x' | 'y' | 'z';
// 数字 0-9
type Digit =
| '0' | '1' | '2' | '3' | '4' | '5'
| '6' | '7' | '8' | '9';
// 允许的字符:小写字母 + 数字 + 下划线
type AllowedChar = Lower | Digit | '_';
// snake_case 字符串类型仅用于文档说明TypeScript 无法完全约束)
// 正确示例: 'user_name', 'item_count', 'is_active'
// 错误示例: 'userName', 'ItemCount', 'Is-Active'
type SnakeCaseString = string;
// name 字段规则:
// - 必须由小写字母、数字、下划线组成a-z、0-9、_
// - 长度 >= 1不能为空
// - 建议使用 snake_case 命名风格
//
// ⚠️ 注意TypeScript 类型系统无法在编译时完全约束字符串内容
// 请手动遵守命名规范,或使用下面的运行时验证函数
export type KeyDesc = {
name: SnakeCaseString;
desc: string;
};
/**
* 验证 name 是否符合规范(运行时检查)
* @param name 要验证的名称
* @returns 是否合法
*/
export function isValidKeyName(name: string): boolean {
return /^[a-z0-9_]+$/.test(name);
}
/**
* 断言 name 符合规范,不符合则抛出错误
* @param name 要验证的名称
* @param context 上下文信息(用于错误提示)
*/
export function assertValidKeyName(name: string, context = 'KeyDesc'): asserts name is SnakeCaseString {
if (!isValidKeyName(name)) {
throw new Error(
`[${context}] Invalid name "${name}". Name must only contain lowercase letters, digits, and underscores (a-z, 0-9, _)`
);
}
}
export type DataKey = { name: string; desc: string };
export type DataDesc = { name: string; desc: string; keys: DataKey[] };
/**
* 配置项定义
* Configuration item definition
* 参考 AttributeComponentProps 结构
*/
export type ConfigItem = {
/** 组件类型 Component type */
type?:
| 'group' // 分组
| 'input' // 文本输入框
| 'inputNumber' // 数字输入框
| 'checkbox' // 复选框
| 'slider' // 滑块
| 'select' // 下拉选择框
| 'autoComplete' // 自动完成
| 'colorPicker' // 颜色选择器
| 'arrayData' // 数组数据编辑器
| 'table' // 表格数据编辑器
| 'map' // 键值对数据编辑器
| 'fontSetting' // 字体设置
| 'lineSetting' // 线条设置
| 'pointSetting' // 端点设置
| 'collapse'; // 折叠面板
/** 属性唯一标识符 Unique attribute identifier (supports dot notation like 'style.fontSize') */
attributeId?: string;
/** 显示名称 Display name shown in UI */
displayName?: string;
/** 描述信息(提示文本) Description or tooltip text */
info?: string;
/** 默认值 Initial/default value */
initialValue?: any;
/** 子配置项 Child configuration items (for nested structures) */
children?: ConfigItem[];
/** 是否显示 Whether to show this item (default: true) */
show?: boolean;
/** 组件特定配置 Component-specific configuration properties */
[k: string]: any;
};
export type Action = { name: string; desc: string; params?: string };
export type EventItem = { name: string; desc: string; payload?: string };
export type CSSProperties = Record<string, string | number>;
export type AnyFunction = (...args: any[]) => any;
export type Ref<T> = { current: T | null } | ((instance: T | null) => void) | null;
export type ForwardRefRenderFunction<T, P> = (props: P, ref: Ref<T>) => any;
// ============Axure组件接口 ============
/**
* Axure 组件的 Props 接口
* 所有 Axure 组件都应该接受这些属性
*/
export interface AxureProps {
/** 数据源,用于传递组件需要的数据 */
data?: Record<string, any>;
/** 配置项,用于配置组件的行为和样式 */
config?: Record<string, any>;
/** 事件处理函数,组件触发事件时调用
* ⚠️ 强制规则payload 必须是字符串类型
*/
onEvent?: (name: string, payload?: string) => void;
/** 容器元素,用于挂载组件 */
container?: HTMLElement | null;
}
/**
* Axure 组件的 Handle 接口
* 通过 ref 暴露给外部的方法和属性
*/
export interface AxureHandle {
/** 获取组件内部变量 */
getVar: (name: string) => any;
/** 触发组件动作
* ⚠️ 强制规则params 必须是字符串类型
*/
fireAction: (name: string, params?: string) => void;
/** 组件支持的事件列表 */
eventList: EventItem[];
/** 组件支持的动作列表 */
actionList: Action[];
/** 组件暴露的变量列表 */
varList: KeyDesc[];
/** 组件的配置项列表 */
configList: ConfigItem[];
/** 组件的数据项列表 */
dataList: DataDesc[];
}
// ============ 工具函数 ============
/**
* 安全地触发事件
* @param handler 事件处理函数
* @param eventName 事件名称
* @param payload 事件数据(⚠️ 强制规则:必须是字符串类型)
*/
export function safeEmitEvent(
handler: ((name: string, payload?: string) => void) | undefined,
eventName: string,
payload?: string
): void {
if (typeof handler === 'function') {
try {
handler(eventName, payload);
} catch (error) {
console.warn(`事件 ${eventName} 调用失败:`, error);
}
}
}
/**
* 创建事件发射器
* @param onEventHandler 事件处理函数
* @returns 事件发射函数(⚠️ 强制规则payload 必须是字符串类型)
*/
export function createEventEmitter(onEventHandler?: (name: string, payload?: string) => void) {
return function emitEvent(eventName: string, payload?: string) {
safeEmitEvent(onEventHandler, eventName, payload);
};
}
/**
* 合并样式对象
* @param styles 样式对象数组
* @returns 合并后的样式对象
*/
export function mergeStyles(...styles: (CSSProperties | undefined)[]): CSSProperties {
return Object.assign({}, ...styles.filter(Boolean));
}
/**
* 获取配置值,如果不存在则返回默认值
* @param config 配置对象
* @param key 配置键
* @param defaultValue 默认值
* @returns 配置值或默认值
*/
export function getConfigValue<T>(
config: Record<string, any> | undefined,
key: string,
defaultValue: T
): T {
if (!config || config[key] === undefined) {
return defaultValue;
}
return config[key] as T;
}
/**
* 获取数据值,如果不存在则返回默认值
* @param data 数据对象
* @param key 数据键
* @param defaultValue 默认值
* @returns 数据值或默认值
*/
export function getDataValue<T>(
data: Record<string, any> | undefined,
key: string,
defaultValue: T
): T {
if (!data || data[key] === undefined) {
return defaultValue;
}
return data[key] as T;
}

View File

@@ -0,0 +1,502 @@
/**
* ConfigPanel API Type Definitions
* 配置面板 API 类型定义
*
* @version 2.0
* @author Lintendo
* @description 可扩展的属性配置系统,支持第三方集成使用
*
* 核心特性:
* - 声明式配置
* - 树形结构组织
* - 动态显示/隐藏
* - 丰富的组件类型
*/
// ============================================================================
// 核心类型定义 Core Type Definitions
// ============================================================================
/**
* 配置项基础属性
* Base properties for all configuration items
*/
export interface AttributeComponentProps {
/** 组件类型 Component type (e.g., 'input', 'select', 'colorPicker') */
type?: string;
/** 属性唯一标识符 Unique attribute identifier (supports dot notation like 'style.fontSize') */
attributeId?: string;
/** 显示名称 Display name shown in UI */
displayName?: string;
/** 描述信息(提示文本) Description or tooltip text */
info?: string;
/** 默认值 Initial/default value */
initialValue?: any;
/** 子配置项 Child configuration items (for nested structures) */
children?: AttributeComponentProps[];
/** 是否显示 Whether to show this item (default: true) */
show?: boolean;
/** 组件特定配置 Component-specific configuration properties */
[k: string]: any;
}
/**
* 完整配置对象
* Complete configuration object
*/
export interface AttributesConfig {
/** 配置树 Configuration tree */
config: AttributeComponentProps;
}
// ============================================================================
// 布局组件 Layout Components
// ============================================================================
/**
* 分组配置
* Group configuration
*
* @description 用于在面板内部进行逻辑分组
* Used for logical grouping within panels
*/
export interface GroupConfig extends AttributeComponentProps {
type: 'group';
/** 分组显示名称 Group display name */
displayName: string;
/** 显示类型 Display type ('inline' for inline display) */
displayType?: 'inline' | 'default';
/** 分组内的配置项 Configuration items within the group */
children: AttributeComponentProps[];
}
// ============================================================================
// 基础输入组件 Basic Input Components
// ============================================================================
/**
* 文本输入框配置
* Text input configuration
*/
export interface InputConfig extends AttributeComponentProps {
type: 'input';
attributeId: string;
displayName: string;
/** 占位符文本 Placeholder text */
placeholder?: string;
/** 输入框宽度 Input width (default: '40%') */
width?: string;
/** 是否禁用 Whether disabled */
disabled?: boolean;
/** 初始值 Initial value */
initialValue?: string;
}
/**
* 数字输入框配置
* Number input configuration
*/
export interface InputNumberConfig extends AttributeComponentProps {
type: 'inputNumber';
attributeId: string;
displayName: string;
/** 最小值 Minimum value */
min?: number;
/** 最大值 Maximum value */
max?: number;
/** 步长 Step increment (default: 1) */
step?: number;
/** 占位符文本 Placeholder text */
placeholder?: string;
/** 初始值 Initial value */
initialValue?: number;
}
/**
* 复选框配置
* Checkbox configuration
*/
export interface CheckboxConfig extends AttributeComponentProps {
type: 'checkbox';
attributeId: string;
displayName: string;
/** 是否禁用 Whether disabled */
disabled?: boolean;
/** 初始值 Initial value */
initialValue?: boolean;
}
/**
* 滑块配置
* Slider configuration
*/
export interface SliderConfig extends AttributeComponentProps {
type: 'slider';
attributeId: string;
displayName: string;
/** 最小值 Minimum value */
min: number;
/** 最大值 Maximum value */
max: number;
/** 步长 Step increment */
step?: number;
/** 是否显示数字输入框 Whether to show number input alongside slider */
showInputNumber?: boolean;
/** 初始值 Initial value */
initialValue?: number;
}
// ============================================================================
// 选择组件 Selection Components
// ============================================================================
/**
* 选项定义
* Option definition for select components
*/
export interface SelectOption {
/** 显示标签 Display label */
label: string;
/** 选项值 Option value */
value: string | number;
}
/**
* 下拉选择框配置
* Select dropdown configuration
*/
export interface SelectConfig extends AttributeComponentProps {
type: 'select';
attributeId: string;
displayName: string;
/** 选项数组 Array of options */
options: SelectOption[];
/** 选择模式 Selection mode ('multiple' for multi-select) */
mode?: 'multiple';
/** 下拉框宽度 Dropdown width (default: 120) */
dropdownMatchSelectWidth?: number;
/** 初始值 Initial value */
initialValue?: string | number | string[] | number[];
}
/**
* 自动完成配置
* AutoComplete configuration
*/
export interface AutoCompleteConfig extends AttributeComponentProps {
type: 'autoComplete';
attributeId: string;
displayName: string;
/** 建议选项数组 Array of suggestion options */
options: SelectOption[];
/** 弹出框宽度 Popup width (default: 200) */
popupMatchSelectWidth?: number;
/** 初始值 Initial value */
initialValue?: string;
}
// ============================================================================
// 颜色组件 Color Components
// ============================================================================
/**
* 颜色选择器配置
* Color picker configuration
*/
export interface ColorPickerConfig extends AttributeComponentProps {
type: 'colorPicker';
attributeId: string;
displayName: string;
/** 选择器类型 Picker type */
picker?: 'common' | 'lite';
/** 初始值 Initial color value (hex format) */
initialValue?: string;
}
// ============================================================================
// 复杂数据组件 Complex Data Components
// ============================================================================
/**
* 数组数据编辑器配置
* Array data editor configuration
*
* @description 弹窗编辑,每行一个数据项
* Modal editor, one item per line
*/
export interface ArrayDataConfig extends AttributeComponentProps {
type: 'arrayData';
attributeId: string;
displayName: string;
/** 初始值 Initial array value */
initialValue?: string[];
}
/**
* 表格列定义
* Table column definition
*/
export interface TableColumn {
/** 字段名 Field name */
name: string;
/** 列显示名 Column display name */
colName: string;
/** 输入类型 Input type */
type: 'text' | 'select' | 'color' | 'number' | 'icon';
/** select 类型的选项 Options for select type */
options?: SelectOption[];
}
/**
* 表格数据编辑器配置
* Table data editor configuration
*
* @description 支持添加、删除、复制、排序行
* Supports add, delete, copy, and reorder rows
*/
export interface TableConfig extends AttributeComponentProps {
type: 'table';
attributeId: string;
displayName: string;
/** 是否为 Map 结构(以第一列值为 key Whether to use Map structure (first column as key) */
isMap?: boolean;
/** 列定义数组 Array of column definitions */
columns: TableColumn[];
/** 初始值 Initial table data */
initialValue?: any[];
}
/**
* Map 列定义
* Map column definition
*/
export interface MapColumn {
/** 键名 Key name */
key: string;
/** 列显示名 Column display name */
colName: string;
/** 输入类型 Input type */
type: 'text' | 'select' | 'color' | 'number' | 'icon';
/** 属性 ID支持 {key} 占位符) Attribute ID (supports {key} placeholder) */
attributeId: string;
/** select 类型的选项 Options for select type */
options?: SelectOption[];
}
/**
* 键值对数据编辑器配置
* Key-value pair data editor configuration
*
* @description 固定键名,编辑值,支持嵌套属性更新
* Fixed keys, editable values, supports nested property updates
*/
export interface MapConfig extends AttributeComponentProps {
type: 'map';
attributeId: string;
displayName: string;
/** 列定义数组 Array of column definitions */
columns: MapColumn[];
/** 初始值 Initial map value */
initialValue?: Record<string, any>;
}
// ============================================================================
// 组合组件 Composite Components
// ============================================================================
/**
* 字体设置配置
* Font setting configuration
*
* @description 组合了颜色、字号、字重、透明度等设置
* Combines color, size, weight, opacity settings
*/
export interface FontSettingConfig extends AttributeComponentProps {
type: 'fontSetting';
displayName: string;
/** 属性 ID 映射 Attribute ID mapping */
attributeIdMap: {
fontColor?: string;
fontSize?: string;
fontWeight?: string;
fontFamily?: string;
textAlign?: string;
opacity?: string;
};
/** 初始值 Initial values */
initialValue?: {
fontColor?: string;
fontSize?: number;
fontWeight?: string;
fontFamily?: string;
textAlign?: string;
opacity?: number;
};
}
/**
* 线条设置配置
* Line setting configuration
*
* @description 组合了线条颜色、线宽、虚线样式、长度等设置
* Combines line color, width, dash style, length settings
*/
export interface LineSettingConfig extends AttributeComponentProps {
type: 'lineSetting';
displayName: string;
/** 属性 ID 映射 Attribute ID mapping */
attributeIdMap: {
lineWidth?: string;
lineColor?: string;
lineDash?: string;
length?: string;
};
/** 初始值 Initial values */
initialValue?: {
lineWidth?: number;
lineColor?: string;
lineDash?: number[];
length?: number;
};
}
/**
* 端点设置配置
* Point setting configuration
*
* @description 用于设置线条端点样式
* Used for setting line endpoint styles
*/
export interface PointSettingConfig extends AttributeComponentProps {
type: 'pointSetting';
displayName: string;
attributeIdMap?: Record<string, string>;
initialValue?: any;
}
// ============================================================================
// 使用示例 Usage Examples
// ============================================================================
/**
* 完整配置示例
* Complete configuration example
*
* @example
* const config: AttributesConfig = {
* config: {
* type: 'collapse',
* children: [
* {
* displayName: '基础设置',
* show: true,
* children: [
* {
* type: 'input',
* attributeId: 'title',
* displayName: '标题',
* initialValue: '默认标题',
* placeholder: '请输入标题'
* },
* {
* type: 'checkbox',
* attributeId: 'showBorder',
* displayName: '显示边框',
* initialValue: true
* },
* {
* type: 'colorPicker',
* attributeId: 'borderColor',
* displayName: '边框颜色',
* initialValue: '#000000'
* }
* ]
* }
* ]
* }
* };
*/
// ============================================================================
// 最佳实践 Best Practices
// ============================================================================
/**
* 最佳实践指南
* Best Practices Guide
*
* 1. attributeId 命名规范 Naming Convention:
* - 使用点分隔的路径 Use dot-separated paths: 'style.fontSize'
* - 保持一致性和可读性 Maintain consistency and readability
* - 避免使用特殊字符 Avoid special characters
*
* 2. 初始值设置 Initial Values:
* - 始终提供合理的 initialValue Always provide reasonable initialValue
* - 确保初始值类型与组件匹配 Ensure type matches component
*
* 3. 分组组织 Grouping:
* - 使用 Collapse 组织大量配置项 Use Collapse for many items
* - 使用 Group 进行逻辑分组 Use Group for logical grouping
* - 相关配置项放在一起 Keep related items together
*
* 4. 性能优化 Performance:
* - 避免过深的嵌套层级 Avoid deep nesting
* - 合理使用 show 属性预先隐藏不需要的项 Use show to hide unnecessary items
* - 大量数据使用 Table 或 Map 组件 Use Table/Map for large datasets
*/

24
axhub-make/src/common/react-dom-shim.js vendored Normal file
View File

@@ -0,0 +1,24 @@
// react-dom-shim.js
const RD = window.ReactDOM;
export default RD;
// ReactDOM 18+ (createRoot / hydrateRoot)
export const {
createRoot,
hydrateRoot,
// ReactDOM 17 兼容 API一些环境仍然可能需要
render,
hydrate,
unmountComponentAtNode,
findDOMNode,
// Server side features (如果 CDN 提供)
createPortal,
// React 18 Transition API可能存在
flushSync,
unstable_batchedUpdates,
unstable_renderSubtreeIntoContainer,
} = RD;

47
axhub-make/src/common/react-shim.js vendored Normal file
View File

@@ -0,0 +1,47 @@
const R = window.React;
const RJSXRuntime = window.ReactJSXRuntime || {};
export default R;
export const {
useState,
useEffect,
useRef,
useMemo,
useCallback,
useContext,
useReducer,
useLayoutEffect,
useImperativeHandle,
useDebugValue,
useDeferredValue,
useTransition,
useId,
useSyncExternalStore,
useInsertionEffect,
forwardRef,
memo,
createElement,
Fragment,
Component,
PureComponent,
createContext,
createRef,
lazy,
Suspense,
StrictMode,
Profiler,
Children,
cloneElement,
isValidElement,
createFactory,
startTransition,
act,
version,
} = R;
// JSX Runtime exports for modern React
export const jsx = RJSXRuntime.jsx || createElement;
export const jsxs = RJSXRuntime.jsxs || createElement;
export const jsxDEV = RJSXRuntime.jsxDEV || createElement;

View File

@@ -0,0 +1,259 @@
/**
* @name 按钮元素
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
*
*/
import './style.css';
import React, { useState, useCallback, useImperativeHandle, forwardRef } from 'react';
import type {
KeyDesc,
DataDesc,
ConfigItem,
Action,
EventItem,
AxureProps,
AxureHandle
} from '../../common/axure-types';
// 图标组件(内部使用)
const IconPlus = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
);
const IconRefresh = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
);
const EVENT_LIST: EventItem[] = [
{ name: 'onClick', desc: '点击按钮时触发' },
{ name: 'onCountChange', desc: '计数器改变时触发' }
];
const ACTION_LIST: Action[] = [
{ name: 'increment', desc: '计数器加1' },
{ name: 'reset', desc: '重置计数器' },
{ name: 'setMessage', desc: '设置消息内容' }
];
const VAR_LIST: KeyDesc[] = [
{ name: 'count', desc: '当前计数值' },
{ name: 'message', desc: '消息内容' }
];
// type 可以是input, inputNumber, colorPicker, checkbox, select 等
const CONFIG_LIST: ConfigItem[] = [
{
type: 'input',
attributeId: 'title',
displayName: '元素标题',
info: '元素顶部显示的标题文本',
initialValue: 'React 示例'
},
{
type: 'input',
attributeId: 'buttonText',
displayName: '按钮文本',
info: '主按钮显示的文本内容',
initialValue: '点击我'
},
{
type: 'colorPicker',
attributeId: 'primaryColor',
displayName: '主色调',
info: '元素的主题颜色',
initialValue: '#1890ff'
},
{
type: 'inputNumber',
attributeId: 'initialCount',
displayName: '初始计数值',
info: '计数器的初始值',
initialValue: 0,
min: 0
},
{
type: 'input',
attributeId: 'message',
displayName: '消息内容',
info: '元素显示的消息文本',
initialValue: '这是一个 React 元素示例'
}
];
const DATA_LIST: DataDesc[] = [
{
name: 'data1',
desc: '基础数据列表',
keys: [
{ name: 'name', desc: '名称' },
{ name: 'value', desc: '值' }
]
}
];
const Component = forwardRef<AxureHandle, AxureProps>(function AxhubButton(innerProps, ref) {
// 安全解构 props 并提供默认值,避免访问 undefined 属性
const dataSource = innerProps && innerProps.data ? innerProps.data : {};
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function' ? innerProps.onEvent : function () { return undefined; };
// 使用类型检查避免使用 || 运算符(会误判 0、false 等值)
const initialCount = typeof configSource.initialCount === 'number' ? configSource.initialCount : 0;
const defaultMessage = typeof configSource.message === 'string' && configSource.message ? configSource.message : '这是一个 React 元素示例';
const titleText = typeof configSource.title === 'string' && configSource.title ? configSource.title : 'React 元素示例';
const buttonText = typeof configSource.buttonText === 'string' && configSource.buttonText ? configSource.buttonText : '点击我';
// 避免使用 ES6 解构,使用数组索引访问 state 和 setter
const countState = useState<number>(initialCount);
const count = countState[0];
const setCount = countState[1];
const messageState = useState<string>(defaultMessage);
const message = messageState[0];
const setMessage = messageState[1];
// 使用 useCallback 优化性能,包含错误处理
const emitEvent = useCallback(function (eventName: string, payload?: any) {
try {
onEventHandler(eventName, payload);
} catch (error) {
console.warn('onEvent 调用失败:', error);
}
}, [onEventHandler]);
// 使用 useCallback 包装所有回调函数,避免在 JSX 中直接定义函数
const incrementCount = useCallback(function () {
setCount(function (prev) {
const newValue = prev + 1;
emitEvent('onCountChange', { count: newValue, action: 'increment' });
return newValue;
});
}, [emitEvent]);
const resetCount = useCallback(function () {
setCount(initialCount);
emitEvent('onCountChange', { count: initialCount, action: 'reset' });
}, [initialCount, emitEvent]);
const updateMessage = useCallback(function (params?: any) {
if (params && typeof params.message === 'string' && params.message.length > 0) {
setMessage(params.message);
}
}, []);
const handlePrimaryClick = useCallback(function () {
emitEvent('onClick', { count });
incrementCount();
}, [count, emitEvent, incrementCount]);
const handleResetClick = useCallback(function () {
resetCount();
}, [resetCount]);
// 使用 switch 语句处理不同的动作类型
const fireActionHandler = useCallback(function (name: string, params?: any) {
switch (name) {
case 'increment':
incrementCount();
break;
case 'reset':
resetCount();
break;
case 'setMessage':
updateMessage(params);
break;
default:
console.warn('未知的动作类型:', name);
}
}, [incrementCount, resetCount, updateMessage]);
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) {
const vars: Record<string, any> = { count, message };
return vars[name];
},
fireAction: fireActionHandler,
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [count, message, fireActionHandler]);
// 从配置和数据源中提取需要的值,提供默认值
const primaryColor = typeof configSource.primaryColor === 'string' && configSource.primaryColor ? configSource.primaryColor : '#1890ff';
const dataListSource = Array.isArray(dataSource.data1) ? dataSource.data1 : [];
// 使用语义化的类名,添加元素前缀避免冲突
// 避免在 JSX 中直接定义函数,使用预定义的 useCallback 函数
return (
<div className="axhub-button-container">
<div className="axhub-button-header">
<h2 className="axhub-button-title">{titleText}</h2>
</div>
<div className="axhub-button-controls">
<button
type="button"
className="axhub-button-primary"
style={{ backgroundColor: primaryColor }}
onClick={handlePrimaryClick}
>
<IconPlus />
<span>{buttonText}</span>
<span className="axhub-button-count-badge">{count}</span>
</button>
<button
type="button"
className="axhub-button-secondary"
onClick={handleResetClick}
>
<IconRefresh />
<span></span>
</button>
</div>
<div className="axhub-button-content">
<div className="axhub-button-message">
<div className="axhub-button-label"></div>
<div className="axhub-button-value">{message}</div>
</div>
{dataListSource.length > 0 && (
<div className="axhub-button-data-container">
<div className="axhub-button-label"></div>
<div className="axhub-button-data-list">
{dataListSource.map(function (item: any, index: number) {
return (
<div key={index} className="axhub-button-data-item">
<span className="axhub-button-data-name">{item.name}</span>
<span className="axhub-button-data-val">{item.value}</span>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
);
});
// 这是本项目平台集成的必要条件
export default Component;

View File

@@ -0,0 +1,130 @@
# 按钮组件
## 📋 业务与功能
### 1.1 核心目标
该组件提供可配置的按钮交互能力,适用于轻量操作场景。
该元素强调简单清晰的交互反馈与可配置性。
### 1.2 功能清单
- **标题区域**:展示组件标题
- **控制区域**:主按钮(计数 + 图标)与重置按钮
- **消息区域**:展示可配置消息文本
- **数据展示区域**:展示 data1 数据列表
### 1.3 交互要点
- 点击主按钮:计数 +1触发 `onClick``onCountChange` 事件
- 点击重置按钮:计数回到初始值,触发 `onCountChange` 事件
- 动作 `increment`/`reset` 可外部调用
- 动作 `setMessage` 可动态更新消息内容
---
## 📊 内容规划
### 2.1 信息架构
```
按钮组件
├── 标题
├── 控制区域
│ ├── 主按钮
│ └── 重置按钮
├── 消息区域
└── 数据展示区域
```
### 2.2 数据来源
- **数据类型**:基础数据列表(`data1`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `name`: 名称
- `value`: 值
### 2.3 内容示例
**数据示例**
- name: `示例项` / value: `123`
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:单栏卡片式
- **容器宽度**:流式
- **关键尺寸**
- 容器内边距:`16px`
- 按钮高度:`40px`
- 圆角:`6-8px`
### 3.2 响应式适配
- **桌面端≥1200px**:自适应容器宽度
- **平板端768-1199px**:自适应容器宽度
- **移动端(<768px**:自适应容器宽度,内容溢出可滚动
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
### 4.2 自定义设计要点
**自定义色彩**(如有):
- `primaryColor`:主按钮背景色
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
- 按钮与卡片阴影强调轻量层次
### 4.3 组件状态
- **默认态default**:按钮与卡片正常显示
- **悬停态hover**:主按钮加深阴影与轻微上浮
- **按压态active**:主按钮压下、亮度降低
- **禁用态disabled**:未定义(可由业务扩展)
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
### 5.2 动作列表actionList
### 5.3 变量列表varList
### 5.4 配置项列表configList
### 5.5 数据项列表dataList
**数据结构**
```typescript
{
data1: Array<{
name: string; // 名称
value: any; // 值
}>;
}
```
&nbsp;

View File

@@ -0,0 +1,212 @@
/**
* @name 按钮元素样式
* 设计风格:简约、现代化、产品级
*/
/* 容器:卡片式设计,适配容器宽高 */
.axhub-button-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 16px; /* 减小内边距以适应小尺寸 */
border: 1px solid #e5e7eb;
border-radius: 8px; /* 稍微减小圆角 */
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
box-sizing: border-box;
overflow: hidden; /* 防止溢出 */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.axhub-button-container:hover {
border-color: #d1d5db; /* Cool gray 300 */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
}
/* 头部区域 */
.axhub-button-header {
flex-shrink: 0; /* 防止压缩 */
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.axhub-button-title {
margin: 0;
font-size: 16px; /* 稍微减小字号 */
font-weight: 600;
color: #111827;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 控制区域:按钮组 */
.axhub-button-controls {
flex-shrink: 0; /* 防止压缩 */
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
/* 基础按钮样式 */
.axhub-button-primary,
.axhub-button-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 40px;
padding: 0 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
white-space: nowrap;
}
/* 主按钮 */
.axhub-button-primary {
/* 背景色由内联样式控制 */
color: #ffffff;
border: 1px solid transparent;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.axhub-button-primary:hover {
filter: brightness(110%);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.axhub-button-primary:active {
transform: translateY(0);
filter: brightness(95%);
}
.axhub-button-primary svg {
width: 16px;
height: 16px;
opacity: 0.9;
}
/* 计数徽标 */
.axhub-button-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
margin-left: 4px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 10px;
font-size: 12px;
font-weight: 600;
line-height: 1;
}
/* 次要按钮 */
.axhub-button-secondary {
background-color: #ffffff;
color: #374151; /* Gray 700 */
border: 1px solid #d1d5db; /* Gray 300 */
}
.axhub-button-secondary:hover {
background-color: #f9fafb; /* Gray 50 */
border-color: #9ca3af; /* Gray 400 */
color: #111827; /* Gray 900 */
}
.axhub-button-secondary:active {
background-color: #f3f4f6; /* Gray 100 */
}
/* 内容区域 */
.axhub-button-content {
flex: 1; /* 占据剩余空间 */
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto; /* 内容过多时滚动 */
min-height: 0; /* 允许 flex 子项收缩 */
padding-right: 4px; /* 为滚动条预留空间 */
}
/* 滚动条样式优化 */
.axhub-button-content::-webkit-scrollbar {
width: 4px;
}
.axhub-button-content::-webkit-scrollbar-track {
background: transparent;
}
.axhub-button-content::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 2px;
}
.axhub-button-content::-webkit-scrollbar-thumb:hover {
background-color: #9ca3af;
}
/* 消息框 & 数据列表容器 - 使用 Box Logic */
.axhub-button-message,
.axhub-button-data-container {
flex-shrink: 0; /* 防止内部内容被压缩 */
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px;
background-color: #f9fafb;
border: 1px solid #f3f4f6;
border-radius: 6px;
}
/* 标签样式 */
.axhub-button-label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
color: #6b7280; /* Gray 500 */
}
/* 值样式 */
.axhub-button-value {
font-size: 14px;
color: #1f2937; /* Gray 800 */
line-height: 1.5;
}
/* 数据列表 */
.axhub-button-data-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.axhub-button-data-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 13px;
}
.axhub-button-data-name {
color: #6b7280;
}
.axhub-button-data-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: #111827;
font-weight: 500;
}

View File

@@ -0,0 +1,308 @@
/**
* @name ECharts 折线图
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
* - /skills/default-resource-recommendations/SKILL.md (ECharts)
*
*/
import './style.css';
import React, { useState, useCallback, useImperativeHandle, forwardRef, useEffect, useRef } from 'react';
import * as echarts from 'echarts/core';
import { LineChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
// 注册需要的组件
echarts.use([
LineChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
CanvasRenderer
]);
import type {
KeyDesc,
DataDesc,
ConfigItem,
Action,
EventItem,
CSSProperties,
AxureProps,
AxureHandle
} from '../../common/axure-types';
const EVENT_LIST: EventItem[] = [
{ name: 'onClick', desc: '点击图表时触发' },
{ name: 'onDataZoom', desc: '数据缩放时触发' },
{ name: 'onLegendSelect', desc: '图例选择时触发' }
];
const ACTION_LIST: Action[] = [
{ name: 'updateData', desc: '更新图表数据' },
{ name: 'resize', desc: '调整图表大小' },
{ name: 'showLoading', desc: '显示加载动画' },
{ name: 'hideLoading', desc: '隐藏加载动画' }
];
const VAR_LIST: KeyDesc[] = [
{ name: 'chart_instance', desc: 'ECharts 实例对象' },
{ name: 'current_data', desc: '当前图表数据' }
];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'input', attributeId: 'title', displayName: '图表标题', info: '显示在图表顶部的标题文本', initialValue: '折线图' },
{ type: 'input', attributeId: 'xAxisName', displayName: 'X轴名称', info: 'X轴的标签名称', initialValue: 'X轴' },
{ type: 'input', attributeId: 'yAxisName', displayName: 'Y轴名称', info: 'Y轴的标签名称', initialValue: 'Y轴' },
{ type: 'colorPicker', attributeId: 'primaryColor', displayName: '主题色', info: '图表的主色调', initialValue: '#1890ff' },
{ type: 'checkbox', attributeId: 'showLegend', displayName: '显示图例', info: '是否显示图例', initialValue: true },
{ type: 'checkbox', attributeId: 'showTooltip', displayName: '显示提示框', info: '是否显示鼠标悬停提示', initialValue: true }
];
const DATA_LIST: DataDesc[] = [
{
name: 'series',
desc: '折线图系列数据',
keys: [
{ name: 'name', desc: '系列名称' },
{ name: 'data', desc: '数据数组(数值)' }
]
},
{
name: 'xAxis',
desc: 'X轴数据',
keys: [
{ name: 'data', desc: 'X轴标签数组' }
]
}
];
const Component = forwardRef<AxureHandle, AxureProps>(function LineChart(innerProps, ref) {
// 安全解构 props 并提供默认值,包括 container
const dataSource = innerProps && innerProps.data ? innerProps.data : {};
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function' ? innerProps.onEvent : function () { return undefined; };
// 用于直接操作 DOM适合 ECharts、D3.js 等需要直接挂载的库
const container = innerProps && innerProps.container ? innerProps.container : null;
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<any>(null);
// 使用类型检查避免使用 || 运算符(会误判 0、false 等值)
const title = typeof configSource.title === 'string' && configSource.title ? configSource.title : '折线图';
const xAxisName = typeof configSource.xAxisName === 'string' && configSource.xAxisName ? configSource.xAxisName : 'X轴';
const yAxisName = typeof configSource.yAxisName === 'string' && configSource.yAxisName ? configSource.yAxisName : 'Y轴';
const primaryColor = typeof configSource.primaryColor === 'string' && configSource.primaryColor ? configSource.primaryColor : '#1890ff';
const showLegend = configSource.showLegend !== false;
const showTooltip = configSource.showTooltip !== false;
const seriesData = Array.isArray(dataSource.series) ? dataSource.series : [
{ name: '系列1', data: [120, 132, 101, 134, 90, 230, 210] }
];
const xAxisData = Array.isArray(dataSource.xAxis?.data) ? dataSource.xAxis.data : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// 避免使用 ES6 解构,使用数组索引访问 state 和 setter
const currentDataState = useState<any>({ series: seriesData, xAxis: { data: xAxisData } });
const currentData = currentDataState[0];
const setCurrentData = currentDataState[1];
const emitEvent = useCallback(function (eventName: string, payload?: any) {
try {
onEventHandler(eventName, payload);
} catch (error) {
console.warn('onEvent 调用失败:', error);
}
}, [onEventHandler]);
// 生成 ECharts 配置项
const getChartOption = useCallback(function () {
const colors = [primaryColor, '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
return {
animation: true,
animationDuration: 30,
animationEasing: 'cubicOut' as const,
title: {
text: title,
left: 'center',
textStyle: {
color: primaryColor
}
},
tooltip: {
show: showTooltip,
trigger: 'axis'
},
legend: {
show: showLegend,
top: showLegend ? 40 : 'auto'
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
name: xAxisName,
nameLocation: 'middle',
nameGap: 30
},
yAxis: {
type: 'value',
name: yAxisName,
nameLocation: 'middle',
nameGap: 50
},
series: seriesData.map(function (item: any, index: number) {
return {
name: item.name || '系列' + (index + 1),
type: 'line',
data: Array.isArray(item.data) ? item.data : [],
smooth: true,
animation: true,
animationDuration: 30,
itemStyle: {
color: colors[index % colors.length]
},
lineStyle: {
color: colors[index % colors.length]
}
};
})
};
}, [title, xAxisName, yAxisName, primaryColor, showLegend, showTooltip, xAxisData, seriesData]);
// 【重要】直接使用 container 初始化图表,不需要返回 JSX 元素
// 只在 container 变化时重新初始化,避免重复创建实例
useEffect(function () {
const targetElement = container
if (!targetElement) {
return;
}
// 等待容器有尺寸后再初始化
function initChart() {
if (!targetElement) {
return;
}
if (!chartInstanceRef.current && targetElement) {
const chartInstance = echarts.init(targetElement);
chartInstanceRef.current = chartInstance;
// 绑定事件
chartInstance.on('click', function (params: any) {
emitEvent('onClick', params);
});
chartInstance.on('datazoom', function (params: any) {
emitEvent('onDataZoom', params);
});
chartInstance.on('legendselectchanged', function (params: any) {
emitEvent('onLegendSelect', params);
});
// 初始化时设置配置
const option = getChartOption();
chartInstance.setOption(option);
setCurrentData({ series: seriesData, xAxis: { data: xAxisData } });
}
}
initChart();
return function () {
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose();
chartInstanceRef.current = null;
}
};
}, [container]); // 只在 container 变化时重新初始化
// 更新图表数据(当数据或配置变化时)
useEffect(function () {
if (!chartInstanceRef.current) {
return;
}
const option = getChartOption();
chartInstanceRef.current.setOption(option, false);
setCurrentData({ series: seriesData, xAxis: { data: xAxisData } });
}, [getChartOption, seriesData, xAxisData]);
const handleUpdateData = useCallback(function (params?: any) {
if (params && params.series) {
setCurrentData({ series: params.series, xAxis: params.xAxis || { data: xAxisData } });
}
}, [xAxisData]);
const handleResize = useCallback(function () {
if (chartInstanceRef.current) {
chartInstanceRef.current.resize();
}
}, []);
const handleShowLoading = useCallback(function () {
if (chartInstanceRef.current) {
chartInstanceRef.current.showLoading();
}
}, []);
const handleHideLoading = useCallback(function () {
if (chartInstanceRef.current) {
chartInstanceRef.current.hideLoading();
}
}, []);
const fireActionHandler = useCallback(function (name: string, params?: any) {
switch (name) {
case 'updateData':
handleUpdateData(params);
break;
case 'resize':
handleResize();
break;
case 'showLoading':
handleShowLoading();
break;
case 'hideLoading':
handleHideLoading();
break;
default:
console.warn('未知的动作类型:', name);
}
}, [handleUpdateData, handleResize, handleShowLoading, handleHideLoading]);
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) {
const vars: Record<string, any> = {
chart_instance: chartInstanceRef.current,
current_data: currentData
};
return vars[name];
},
fireAction: fireActionHandler,
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [currentData, fireActionHandler]);
// 因为图表已经通过 container 直接挂载到 DOM 上了
return null;
});
// 这是本项目平台集成的必要条件
export default Component;

View File

@@ -0,0 +1,159 @@
# ECharts 折线图
## 📋 业务与功能
### 1.1 核心目标
该元素用于展示时间序列与趋势数据,支持多系列可视化与基础交互。
该元素面向通用数据可视化场景,强调清晰的趋势表达与可配置性。
### 1.2 功能清单
- **标题区域**:展示图表标题
- **图例区域**:显示系列名称并支持开关
- **绘图区**:折线与数据点绘制
- **坐标轴**X/Y 轴名称与刻度
- **提示框**:鼠标悬停展示数据详情
- **加载动画**:支持显示/隐藏加载状态
### 1.3 交互要点
- 悬停数据点:显示 tooltip反馈当前数据值
- 点击图表:触发 `onClick` 事件
- 图例切换:触发 `onLegendSelect` 事件
- 数据缩放:触发 `onDataZoom` 事件
- 动作 `updateData` 更新数据并重绘图表
---
## 📊 内容规划
### 2.1 信息架构
```
ECharts 折线图
├── 标题
├── 图例
└── 绘图区
├── 坐标轴
├── 折线与数据点
└── 提示框
```
### 2.2 数据来源
- **数据类型**:系列数据(`series`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `name`: 系列名称
- `data`: 数值数组
- **数据类型**X 轴数据(`xAxis.data`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `data`: 类目/时间标签数组
### 2.3 内容示例
**示例**
- 系列:`系列1` / [120, 132, 101, 134, 90, 230, 210]
- X 轴:`Mon` ~ `Sun`
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:单容器自适应
- **容器宽度**:流式
- **关键尺寸**
- 最小尺寸:`300px` × `200px`
- 图表区域占满容器
### 3.2 响应式适配
- **桌面端≥1200px**:图表自适应容器宽高
- **平板端768-1199px**:图表自适应容器宽高
- **移动端(<768px**:图表自适应容器宽高
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
### 4.2 自定义设计要点
**自定义色彩**(如有):
- `primaryColor`:折线主题色(默认 `#1890ff`
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
-
### 4.3 组件状态
- **默认态default**:折线与坐标轴正常显示
- **悬停态hover**:显示提示框并高亮数据点
- **加载态loading**:显示/隐藏加载动画遮罩
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
| 事件名称 | Payload 类型 | 触发时机 | 说明 |
|---------|-------------|---------|------|
| `onClick` | `object` | 点击图表时 | ECharts 点击事件参数 |
| `onDataZoom` | `object` | 数据缩放时 | ECharts 缩放事件参数 |
| `onLegendSelect` | `object` | 切换图例时 | ECharts 图例事件参数 |
### 5.2 动作列表actionList
| 动作名称 | Params 类型 | 参数说明 | 功能描述 |
|---------|------------|---------|---------|
| `updateData` | `object` | `{ series, xAxis }` | 更新图表数据 |
| `resize` | `void` | 无 | 触发图表自适应 |
| `showLoading` | `void` | 无 | 显示加载动画 |
| `hideLoading` | `void` | 无 | 隐藏加载动画 |
### 5.3 变量列表varList
| 变量名称 | 类型 | 默认值 | 说明 |
|---------|-----|-------|------|
| `chart_instance` | `EChartsInstance` | `null` | ECharts 实例对象 |
| `current_data` | `object` | 默认数据 | 当前图表数据 |
### 5.4 配置项列表configList
| 配置项名称 | 类型 | 默认值 | 说明 |
|----------|-----|-------|------|
| `title` | `string` | `折线图` | 图表标题 |
| `xAxisName` | `string` | `X轴` | X 轴名称 |
| `yAxisName` | `string` | `Y轴` | Y 轴名称 |
| `primaryColor` | `string` | `#1890ff` | 主题色 |
| `showLegend` | `boolean` | `true` | 是否显示图例 |
| `showTooltip` | `boolean` | `true` | 是否显示提示框 |
### 5.5 数据项列表dataList
**数据结构**
```typescript
{
series: Array<{
name: string; // 系列名称
data: number[]; // 数值数组
}>;
xAxis: {
data: string[]; // X 轴标签
};
}
```

View File

@@ -0,0 +1,12 @@
/**
* @name 折线图样式
* 图表容器基础样式
*/
.axhub-line-chart-container {
width: 100%;
height: 100%;
min-width: 300px;
min-height: 200px;
box-sizing: border-box;
}

View File

@@ -0,0 +1,196 @@
/**
* @name 侧边菜单
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
* - /src/themes/antd-new/designToken.json (Ant Design 主题)
* - /skills/default-resource-recommendations/SKILL.md (Ant Design 组件库)
*/
import './style.css';
import React, { useState, useCallback, useMemo } from 'react';
import { Layout, Menu, Button, theme } from 'antd';
import {
DashboardOutlined,
ShoppingOutlined,
UserOutlined,
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
type MenuItemInput = {
key?: string;
label?: string;
icon?: string;
disabled?: boolean;
children?: MenuItemInput[];
};
type SideMenuProps = {
title?: string;
width?: number;
collapsible?: boolean;
defaultCollapsed?: boolean;
defaultSelectedKey?: string;
items?: MenuItemInput[];
onMenuSelect?: (key: string) => void;
onCollapseChange?: (collapsed: boolean) => void;
};
function resolveIcon(name?: string) {
switch (name) {
case 'dashboard':
return <DashboardOutlined />;
case 'shop':
return <ShoppingOutlined />;
case 'user':
return <UserOutlined />;
case 'setting':
return <SettingOutlined />;
default:
return undefined;
}
}
function normalizeItems(items: any): MenuItemInput[] {
if (!Array.isArray(items)) {
return [];
}
return items.map(function (it: any) {
return {
key: typeof it?.key === 'string' && it.key ? it.key : String(it?.key ?? it?.label ?? ''),
label: typeof it?.label === 'string' ? it.label : String(it?.label ?? ''),
icon: typeof it?.icon === 'string' ? it.icon : undefined,
disabled: it?.disabled === true,
children: Array.isArray(it?.children) ? normalizeItems(it.children) : undefined
};
}).filter(function (it: MenuItemInput) { return !!it.key; });
}
const DEFAULT_ITEMS: MenuItemInput[] = [
{ key: 'dashboard', label: '仪表盘', icon: 'dashboard' },
{
key: 'orders',
label: '订单管理',
icon: 'shop',
children: [
{ key: 'orders_list', label: '订单列表' },
{ key: 'orders_refund', label: '退款管理' }
]
},
{ key: 'users', label: '用户管理', icon: 'user' },
{ key: 'settings', label: '系统设置', icon: 'setting' }
];
const Component = function SideMenu(props: SideMenuProps) {
const title = typeof props.title === 'string' && props.title ? props.title : 'Axhub';
const width = typeof props.width === 'number' && props.width > 0 ? props.width : 240;
const collapsible = props.collapsible !== false;
const defaultCollapsed = props.defaultCollapsed === true;
const defaultSelectedKey = typeof props.defaultSelectedKey === 'string' && props.defaultSelectedKey
? props.defaultSelectedKey
: 'dashboard';
const normalizedItems = useMemo(function () {
const fromProps = normalizeItems(props.items);
return fromProps.length > 0 ? fromProps : DEFAULT_ITEMS;
}, [props.items]);
const { token } = theme.useToken();
const collapsedState = useState<boolean>(defaultCollapsed);
const collapsed = collapsedState[0];
const setCollapsed = collapsedState[1];
const selectedKeyState = useState<string>(defaultSelectedKey);
const selectedKey = selectedKeyState[0];
const setSelectedKey = selectedKeyState[1];
const openKeysState = useState<string[]>([]);
const openKeys = openKeysState[0];
const setOpenKeys = openKeysState[1];
const menuItems = useMemo(function () {
function toAntdItems(list: MenuItemInput[]): any[] {
return list.map(function (item) {
return {
key: item.key,
label: item.label,
icon: resolveIcon(item.icon),
disabled: item.disabled,
children: Array.isArray(item.children) && item.children.length > 0 ? toAntdItems(item.children) : undefined
};
});
}
return toAntdItems(normalizedItems);
}, [normalizedItems]);
const handleToggleCollapsed = useCallback(function () {
setCollapsed(function (prev) {
const next = !prev;
if (typeof props.onCollapseChange === 'function') {
props.onCollapseChange(next);
}
return next;
});
}, [props]);
const handleMenuClick = useCallback(function (info: any) {
const key = typeof info?.key === 'string' ? info.key : String(info?.key ?? '');
if (!key) {
return;
}
setSelectedKey(key);
if (typeof props.onMenuSelect === 'function') {
props.onMenuSelect(key);
}
}, [props]);
const handleOpenChange = useCallback(function (keys: any) {
const next = Array.isArray(keys) ? keys.map(String) : [];
setOpenKeys(next);
}, []);
return (
<Layout.Sider
className="axhub-side-menu"
width={width}
collapsedWidth={64}
collapsed={collapsed}
trigger={null}
style={{
background: token.colorBgContainer,
borderRight: '1px solid ' + token.colorBorderSecondary
}}
>
<div className={'axhub-side-menu__header' + (collapsed ? ' axhub-side-menu__header--collapsed' : '')}>
{!collapsed && <div className="axhub-side-menu__title">{title}</div>}
{collapsible && (
<Button
className="axhub-side-menu__collapse"
type="text"
size="small"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={handleToggleCollapsed}
/>
)}
</div>
<Menu
mode="inline"
inlineCollapsed={collapsed}
items={menuItems}
selectedKeys={selectedKey ? [selectedKey] : []}
openKeys={collapsed ? [] : openKeys}
onClick={handleMenuClick}
onOpenChange={handleOpenChange}
style={{ borderInlineEnd: 0, background: 'transparent' }}
/>
</Layout.Sider>
);
};
export default Component;

View File

@@ -0,0 +1,138 @@
# 侧边菜单
## 📋 业务与功能
### 1.1 核心目标
该组件提供后台场景常见的侧边导航能力,支持分级菜单与折叠交互。
该元素强调结构清晰、信息密度适中,适配后台场景。
### 1.2 功能清单
- **顶部标题区域**:显示应用或系统名称
- **菜单项区域**:支持一级/二级菜单项、图标与禁用状态
- **折叠按钮**:控制侧边栏折叠/展开
### 1.3 交互要点
- 点击菜单项:选中菜单项并触发 `onMenuSelect`
- 点击折叠按钮:切换折叠状态并触发 `onCollapseChange`
- 子菜单展开/收起:点击父级菜单项控制
- 悬停菜单项:高亮反馈
---
## 📊 内容规划
### 2.1 信息架构
```
侧边菜单
├── 顶部标题
│ └── 折叠按钮
└── 菜单列表
├── 一级菜单
└── 二级子菜单(可选)
```
### 2.2 数据来源
- **数据类型**:菜单项列表(`items`
- **数据源**用户提供props/ 内置默认菜单
- **关键字段**
- `key`: 唯一标识
- `label`: 显示文本
- `icon`: 图标名称dashboard / shop / user / setting
- `disabled`: 是否禁用
- `children`: 子菜单数组
### 2.3 内容示例
**默认菜单示例**
- 仪表盘 / 订单管理(含订单列表、退款管理)/ 用户管理 / 系统设置
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:垂直侧边栏
- **容器宽度**:固定
- **关键尺寸**
- 展开宽度:`240px`
- 折叠宽度:`64px`
- 顶部标题区高度:`48px`
### 3.2 响应式适配
- **桌面端≥1200px**:默认展开显示
- **平板端768-1199px**:建议默认折叠或在主区保留更大空间
- **移动端(<768px**:建议折叠或使用抽屉式替代
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
- [x] 主题设计系统:`/src/themes/antd-new/`DESIGN.md + designToken.json + globals.css
### 4.2 自定义设计要点
**自定义色彩**(如有):
-
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
-
### 4.3 组件状态
- **默认态default**:菜单项正常显示
- **悬停态hover**:菜单项高亮背景
- **选中态selected**:当前菜单项高亮
- **禁用态disabled**:菜单项灰化不可点击
- **折叠态collapsed**:仅展示图标,标题隐藏
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
### 5.2 动作列表actionList
### 5.3 变量列表varList
### 5.4 配置项列表configList
### 5.5 数据项列表dataList
**数据结构**
```typescript
{
items: Array<{
key: string; // 菜单项唯一标识
label: string; // 显示文本
icon?: string; // 图标名称
disabled?: boolean; // 是否禁用
children?: Array<{ // 子菜单项
key: string;
label: string;
icon?: string;
disabled?: boolean;
}>;
}>;
}
```

View File

@@ -0,0 +1,35 @@
.axhub-side-menu {
box-sizing: border-box;
}
.axhub-side-menu__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
height: 48px;
box-sizing: border-box;
}
.axhub-side-menu__header--collapsed {
justify-content: center;
padding: 0;
}
.axhub-side-menu__title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.axhub-side-menu__collapse {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
}

View File

@@ -0,0 +1,2 @@
# This file ensures the database directory is tracked by git
# Actual data files (*.json) are ignored via .gitignore

View File

@@ -0,0 +1,63 @@
# src/database
本目录存放页面可直接消费的数据表 JSON。
## 文件格式(强约束)
每个文件必须是 JSON 对象(不要输出为纯数组),包含 `tableName` + `records`
```json
{
"tableName": "表名(中文)",
"records": [
{ "id": 1, "字段1": "值1", "字段2": "值2" }
]
}
```
## 示例
列表类id 为 number
```json
{
"tableName": "百科表",
"records": [
{
"id": 1,
"标题": "宝宝什么时候开始刷牙?",
"描述": "牙齿护理要从娃娃抓起",
"浏览量": 8844,
"喜欢数": 1234,
"封面图": "images/百科列表/u5.png"
}
]
}
```
订单/单据类id 为 string
```json
{
"tableName": "订单表",
"records": [
{
"id": "ORD20251230001",
"下单时间": "2025-12-30 09:15:22",
"收货人姓名": "张伟",
"手机号码": "138****1234",
"商品名称": "小米14 Pro 手机",
"单价(元)": 4999,
"数量": 1,
"订单总额(元)": 5000,
"订单状态": "已完成"
}
]
}
```
## 约束(最小集)
- 文件名:英文(如 `users.json``order-items.json`
- `records`:数组;每条记录必须有唯一 `id`(允许 number 或 string同表保持类型一致
- 字段:优先中文并与页面一致;同字段同类型;图片字段用相对路径(如 `images/xxx.png`

View File

@@ -0,0 +1,395 @@
{
"tableName": "订单表",
"records": [
{
"id": "ORD20251230001",
"下单时间": "2025-12-30 09:15:22",
"收货人姓名": "张伟",
"手机号码": "138****1234",
"商品名称": "小米14 Pro 手机",
"单价(元)": 4999,
"数量": 1,
"订单总额(元)": 5000,
"收货地址": "北京市朝阳区建国路88号SOHO现代城",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230002",
"下单时间": "2025-12-30 09:18:05",
"收货人姓名": "李秀英",
"手机号码": "159****5678",
"商品名称": "维达抽纸 3层120抽*24包",
"单价(元)": 59.9,
"数量": 2,
"订单总额(元)": 119.8,
"收货地址": "上海市浦东新区陆家嘴环路1000号",
"支付方式": "支付宝",
"订单状态": "已发货"
},
{
"id": "ORD20251230003",
"下单时间": "2025-12-30 09:25:30",
"收货人姓名": "王芳",
"手机号码": "186****9012",
"商品名称": "雅诗兰黛小棕瓶精华",
"单价(元)": 680,
"数量": 1,
"订单总额(元)": 680,
"收货地址": "广东省广州市天河区珠江新城IFC",
"支付方式": "微信支付",
"订单状态": "待发货"
},
{
"id": "ORD20251230004",
"下单时间": "2025-12-30 09:40:11",
"收货人姓名": "刘强",
"手机号码": "135****3344",
"商品名称": "罗技MX Master 3S鼠标",
"单价(元)": 699,
"数量": 1,
"订单总额(元)": 699,
"收货地址": "浙江省杭州市余杭区文一西路969号",
"支付方式": "支付宝",
"订单状态": "已完成"
},
{
"id": "ORD20251230005",
"下单时间": "2025-12-30 10:05:55",
"收货人姓名": "陈杰",
"手机号码": "137****7788",
"商品名称": "三只松鼠坚果礼盒",
"单价(元)": 128,
"数量": 5,
"订单总额(元)": 640,
"收货地址": "江苏省南京市鼓楼区汉中路2号",
"支付方式": "银联云闪付",
"订单状态": "已发货"
},
{
"id": "ORD20251230006",
"下单时间": "2025-12-30 10:12:40",
"收货人姓名": "杨洋",
"手机号码": "150****2233",
"商品名称": "优衣库U系列纯棉T恤",
"单价(元)": 99,
"数量": 3,
"订单总额(元)": 297,
"收货地址": "四川省成都市锦江区春熙路IFS",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230007",
"下单时间": "2025-12-30 10:30:18",
"收货人姓名": "赵静",
"手机号码": "189****6655",
"商品名称": "飞利浦电动牙刷HX6730",
"单价(元)": 329,
"数量": 1,
"订单总额(元)": 329,
"收货地址": "湖北省武汉市江汉区解放大道688号",
"支付方式": "支付宝",
"订单状态": "已退款"
},
{
"id": "ORD20251230008",
"下单时间": "2025-12-30 10:45:00",
"收货人姓名": "黄磊",
"手机号码": "136****9900",
"商品名称": "农夫山泉饮用天然水550ml*24",
"单价(元)": 36,
"数量": 10,
"订单总额(元)": 360,
"收货地址": "湖南省长沙市岳麓区大学城",
"支付方式": "微信支付",
"订单状态": "配送中"
},
{
"id": "ORD20251230009",
"下单时间": "2025-12-30 11:02:12",
"收货人姓名": "周涛",
"手机号码": "133****4455",
"商品名称": "华为Mate 60 Pro",
"单价(元)": 6999,
"数量": 1,
"订单总额(元)": 6999,
"收货地址": "深圳市南山区粤海街道科技园",
"支付方式": "支付宝",
"订单状态": "待发货"
},
{
"id": "ORD20251230010",
"下单时间": "2025-12-30 11:15:33",
"收货人姓名": "吴刚",
"手机号码": "158****1122",
"商品名称": "公牛插座新国标8位",
"单价(元)": 45,
"数量": 2,
"订单总额(元)": 90,
"收货地址": "重庆市渝中区解放碑时代广场",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230011",
"下单时间": "2025-12-30 11:30:45",
"收货人姓名": "徐丽",
"手机号码": "139****8877",
"商品名称": "SK-II神仙水230ml",
"单价(元)": 1540,
"数量": 1,
"订单总额(元)": 1540,
"收货地址": "天津市和平区南京路189号",
"支付方式": "支付宝",
"订单状态": "已发货"
},
{
"id": "ORD20251230012",
"下单时间": "2025-12-30 12:05:10",
"收货人姓名": "孙勇",
"手机号码": "188****5566",
"商品名称": "得力A4打印纸70g 500张",
"单价(元)": 25,
"数量": 10,
"订单总额(元)": 250,
"收货地址": "陕西省西安市雁塔区小寨赛格国际",
"支付方式": "对公转账",
"订单状态": "已完成"
},
{
"id": "ORD20251230013",
"下单时间": "2025-12-30 12:20:25",
"收货人姓名": "马兰",
"手机号码": "151****3399",
"商品名称": "好想你红枣特级500g",
"单价(元)": 39.9,
"数量": 4,
"订单总额(元)": 159.6,
"收货地址": "河南省郑州市金水区花园路360广场",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230014",
"下单时间": "2025-12-30 12:45:50",
"收货人姓名": "朱晓",
"手机号码": "180****7766",
"商品名称": "戴森V12吸尘器",
"单价(元)": 4599,
"数量": 1,
"订单总额(元)": 4599,
"收货地址": "福建省厦门市思明区中山路",
"支付方式": "支付宝",
"订单状态": "待支付"
},
{
"id": "ORD20251230015",
"下单时间": "2025-12-30 13:10:05",
"收货人姓名": "何平",
"手机号码": "153****2288",
"商品名称": "五常大米 10kg",
"单价(元)": 89,
"数量": 2,
"订单总额(元)": 178,
"收货地址": "黑龙江省哈尔滨市道里区中央大街",
"支付方式": "微信支付",
"订单状态": "已发货"
},
{
"id": "ORD20251230016",
"下单时间": "2025-12-30 13:30:40",
"收货人姓名": "罗敏",
"手机号码": "131****6600",
"商品名称": "李宁超轻20跑鞋",
"单价(元)": 399,
"数量": 1,
"订单总额(元)": 399,
"收货地址": "山东省青岛市市南区五四广场",
"支付方式": "支付宝",
"订单状态": "已完成"
},
{
"id": "ORD20251230017",
"下单时间": "2025-12-30 13:55:15",
"收货人姓名": "高强",
"手机号码": "185****9988",
"商品名称": "索尼WH-1000XM5耳机",
"单价(元)": 2399,
"数量": 1,
"订单总额(元)": 2399,
"收货地址": "辽宁省沈阳市和平区太原街万达",
"支付方式": "微信支付",
"订单状态": "已取消"
},
{
"id": "ORD20251230018",
"下单时间": "2025-12-30 14:15:20",
"收货人姓名": "林峰",
"手机号码": "130****1199",
"商品名称": "雀巢速溶咖啡100条",
"单价(元)": 99,
"数量": 2,
"订单总额(元)": 198,
"收货地址": "云南省昆明市五华区南屏街",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230019",
"下单时间": "2025-12-30 14:40:55",
"收货人姓名": "郑华",
"手机号码": "155****4477",
"商品名称": "美的电饭煲4L",
"单价(元)": 299,
"数量": 1,
"订单总额(元)": 299,
"收货地址": "广东省佛山市顺德区北滘镇",
"支付方式": "支付宝",
"订单状态": "已发货"
},
{
"id": "ORD20251230020",
"下单时间": "2025-12-30 15:05:30",
"收货人姓名": "谢娜",
"手机号码": "187****8822",
"商品名称": "好孩子儿童安全座椅",
"单价(元)": 1299,
"数量": 1,
"订单总额(元)": 1299,
"收货地址": "安徽省合肥市蜀山区天鹅湖万达",
"支付方式": "银联云闪付",
"订单状态": "已完成"
},
{
"id": "ORD20251230021",
"下单时间": "2025-12-30 15:25:45",
"收货人姓名": "韩梅梅",
"手机号码": "134****5511",
"商品名称": "晨光中性笔芯黑色20支",
"单价(元)": 12,
"数量": 5,
"订单总额(元)": 60,
"收货地址": "北京市海淀区中关村大街",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230022",
"下单时间": "2025-12-30 15:50:10",
"收货人姓名": "唐杰",
"手机号码": "152****9933",
"商品名称": "九阳破壁机L18",
"单价(元)": 499,
"数量": 1,
"订单总额(元)": 499,
"收货地址": "山东省济南市历下区泉城广场",
"支付方式": "支付宝",
"订单状态": "待发货"
},
{
"id": "ORD20251230023",
"下单时间": "2025-12-30 16:10:25",
"收货人姓名": "许巍",
"手机号码": "181****2266",
"商品名称": "那曲虫草礼盒",
"单价(元)": 2888,
"数量": 1,
"订单总额(元)": 2888,
"收货地址": "西藏自治区拉萨市城关区北京中路",
"支付方式": "微信支付",
"订单状态": "已签收"
},
{
"id": "ORD20251230024",
"下单时间": "2025-12-30 16:35:40",
"收货人姓名": "沈梦",
"手机号码": "156****7700",
"商品名称": "蓝月亮洗衣液3kg",
"单价(元)": 45,
"数量": 4,
"订单总额(元)": 180,
"收货地址": "广西壮族自治区南宁市青秀区万象城",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230025",
"下单时间": "2025-12-30 17:00:55",
"收货人姓名": "曹操",
"手机号码": "132****3388",
"商品名称": "Kindle Paperwhite 5",
"单价(元)": 1099,
"数量": 1,
"订单总额(元)": 1099,
"收货地址": "河南省许昌市魏都区",
"支付方式": "支付宝",
"订单状态": "已退货"
},
{
"id": "ORD20251230026",
"下单时间": "2025-12-30 17:20:15",
"收货人姓名": "袁隆",
"手机号码": "199****6699",
"商品名称": "袁米海水稻米5kg",
"单价(元)": 68,
"数量": 2,
"订单总额(元)": 136,
"收货地址": "海南省三亚市吉阳区亚龙湾",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230027",
"下单时间": "2025-12-30 17:45:30",
"收货人姓名": "邓超",
"手机号码": "198****1177",
"商品名称": "乐高布加迪跑车",
"单价(元)": 2599,
"数量": 1,
"订单总额(元)": 2599,
"收货地址": "江西省南昌市红谷滩新区",
"支付方式": "支付宝",
"订单状态": "待支付"
},
{
"id": "ORD20251230028",
"下单时间": "2025-12-30 18:05:45",
"收货人姓名": "曾小贤",
"手机号码": "177****5588",
"商品名称": "卫龙辣条大礼包",
"单价(元)": 29.9,
"数量": 10,
"订单总额(元)": 299,
"收货地址": "上海市静安区愚园路",
"支付方式": "微信支付",
"订单状态": "已完成"
},
{
"id": "ORD20251230029",
"下单时间": "2025-12-30 18:30:10",
"收货人姓名": "彭彭",
"手机号码": "176****9922",
"商品名称": "任天堂Switch OLED日版",
"单价(元)": 1899,
"数量": 1,
"订单总额(元)": 1899,
"收货地址": "河北省石家庄市裕华区万达广场",
"支付方式": "支付宝",
"订单状态": "已发货"
},
{
"id": "ORD20251230030",
"下单时间": "2025-12-30 18:55:00",
"收货人姓名": "苏菲",
"手机号码": "133****0000",
"商品名称": "云南白药牙膏留兰香型",
"单价(元)": 28,
"数量": 3,
"订单总额(元)": 84,
"收货地址": "云南省大理白族自治州大理古城",
"支付方式": "微信支付",
"订单状态": "已完成"
}
]
}

View File

View File

@@ -0,0 +1,27 @@
# 业务流程模板
> 适用场景:项目有明确业务链路、角色协作、状态推进或关键路径时使用。
> 可不生成/可合并:若流程很短,可合并到项目说明清单或状态文档;若核心是页面导航而非业务流,可优先生成页面地图。
## 1. 文档目标
- 说明关键业务流程、触发条件与结果输出
- 帮助后续页面设计、数据设计与状态设计保持一致
## 2. 核心流程
```mermaid
flowchart TD
A["{{STEP_1}}"] --> B["{{STEP_2}}"]
B --> C["{{STEP_3}}"]
```
## 3. 流程说明
| 步骤 | 触发者 | 输入/前置条件 | 输出/结果 |
|------|------|------|------|
| `{{STEP_NAME}}` | `{{ACTOR}}` | `{{INPUT}}` | `{{OUTPUT}}` |
## 4. 异常与待确认
- `{{EXCEPTION_OR_OPEN_ISSUE}}`

View File

@@ -0,0 +1,25 @@
# 数据模型模板
> 适用场景:项目存在核心业务对象、数据表、字段映射或页面直连数据资产时使用。
> 可不生成/可合并:若项目只包含少量静态展示数据,可跳过;也可与业务流程文档合并为“业务与数据摘要”。
## 1. 文档目标
- 说明关键数据对象、数据来源与字段使用边界
- 避免页面、文档、数据表三者出现口径不一致
## 2. 核心对象
| 对象/数据表 | 来源 | 用途 | 关联文件 |
|------|------|------|------|
| `{{ENTITY_NAME}}` | `{{SOURCE}}` | `{{USAGE}}` | `{{RELATED_FILE}}` |
## 3. 关键字段摘要
| 字段 | 类型 | 含义 | 使用位置 |
|------|------|------|------|
| `{{FIELD_NAME}}` | `{{FIELD_TYPE}}` | `{{FIELD_DESC}}` | `{{FIELD_USAGE}}` |
## 4. 约束与待确认
- `{{RULE_OR_OPEN_ISSUE}}`

View File

@@ -0,0 +1,155 @@
# 主题设计规范 / Theme Design MD
> 本文档定义了当前主题的设计价值、能力边界与使用指南,帮助开发者和 AI 正确理解和应用该设计系统。
> 这是基于 [Google Stitch Design MD](https://stitch.withgoogle.com/docs/design-md/overview/) 格式规范的最佳实践。
## 设计系统概述
<!-- 简述品牌定位、核心价值与设计原则 -->
### 品牌定位
<!-- 描述该主题想要传达的情感与专业印象 -->
### 核心价值
<!-- 列出 3-4 个核心的设计价值主张 -->
1. **可用性** -
2. **一致性** -
3. **品牌感** -
### 设计原则
<!-- 具体的指导性设计原则 -->
| 原则 | 含义 |
|------|------|
| **原则名** | 原则解释 |
---
## 能力边界
### 适合的场景
<!-- 列出该主题适合的业务场景,如 ToB 后台、C 端电商等 -->
-
### 不适合的场景
<!-- 列出该主题不适合的场景,帮助系统避免错误应用 -->
-
---
## 色彩系统 (Colors)
### 品牌色 (Primary)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--primary` | `#000000` | 主按钮、品牌强调元素 |
| `--primary-foreground` | `#FFFFFF` | 主色背景上的文字 |
### 背景色 (Background)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--background` | `#FFFFFF` | 页面主背景 |
| `--card` | `#FFFFFF` | 卡片与主要区块背景 |
| `--muted` | `#F1F5F9` | 次级/禁用背景 |
### 文本色 (Text)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--foreground` | `#0F172A` | 主要正文 |
| `--muted-foreground` | `#64748B` | 次要/辅助正文 |
### 边框色 (Border)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--border` | `#E2E8F0` | 基础边框 |
### 语义色 (Semantic)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--destructive` | `#EF4444` | 危险操作、报错提示 |
---
## 字体系统 (Typography)
### 字体族
| 用途 | 字体 | CSS 变量 |
|------|------|---------|
| 主字体 | Inter, sans-serif | `--font-sans` |
| 等宽字体 | Fira Code, monospace | `--font-mono` |
### 文本层级
| 名称 | 字号 | 字重 | 行高 | 用途 |
|------|------|------|------|------|
| H1 | 30px | 600 | 1.25 | 页面主标题 |
| H2 | 24px | 600 | 1.3 | 区块标题 |
| Body | 14px | 400 | 1.5 | 默认正文 |
| Label | 14px | 500 | 1 | 按钮、表单标签 |
---
## 间距系统 (Spacing)
<!-- 定义布局的节奏,例如 4px 网格系统 -->
| Token | 值 | 用途 |
|-------|-----|------|
| `--spacing-2` | 8px | 紧凑元素内间距 |
| `--spacing-4` | 16px | 标准间距/组件边界 |
| `--spacing-6` | 24px | 区块间隔 |
| `--spacing-8` | 32px | 大型区块间隔 |
---
## 圆角与阴影 (Radii & Shadows)
### 圆角 (Radius)
| Token | 值 | 用途 |
|------|------|------|
| `--radius-sm` | 4px | 小标签、Checkbox |
| `--radius-md` | 8px | 按钮、输入框 |
| `--radius-lg` | 12px | 卡片、弹窗 |
### 阴影 (Shadows)
| 名称 | 值 | 用途 |
|------|-----|------|
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | 常规卡片悬浮 |
| `--shadow-md` | `0 4px 6px -1px rgba(0,0,0,0.1)` | 下拉菜单、弹窗 |
---
## 组件规范 (Components)
### Button 按钮
<!-- 描述主要组件的外观组合逻辑 -->
```css
/* Primary Button */
background: var(--primary);
color: var(--primary-foreground);
border-radius: var(--radius-md);
padding: 8px 16px;
font-weight: 500;
```
### Card 卡片
```css
/* Default Card */
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: none; /* 或 var(--shadow-sm) */
padding: 24px;
```
---
## 使用约束 (Constraints & Rules)
### 必须遵守 (Must)
1. **保持对比度** - 确保主要文本在背景色上有足够的对比度 (WCAG AA)。
2. **规范间距** - 仅使用间距系统中的定义,不要使用硬编码的 px。
### 建议做法 (Should)
1. **适度留白** - 使用足够的大间距分隔无关区块。
2. **分级清晰** - 按钮区分 Primary/Secondary/Ghost 状态。
### 禁止做法 (Must Not)
1. **滥用主色** - 不要将主色用作大面积背景,仅用于重要行动召唤(CTA)。
2. **过多字体组合** - 单页字体字重尽量不超过 3 种变体。

View File

@@ -0,0 +1,32 @@
# 信息架构模板
> 适用场景:项目存在明显的信息层级、模块边界、导航结构或内容组织关系时使用。
> 可不生成/可合并:若项目信息结构非常简单,可跳过;也可与页面地图或业务流程文档合并。
## 1. 文档目标
- 说明模块划分、信息层级与主要导航结构
- 明确哪些内容在总入口、哪些内容落到页面或专题文档
## 2. 模块结构
```text
{{IA_TREE}}
```
## 3. 模块说明
| 模块 | 子项/内容 | 说明 | 关联资产 |
| ----------------- | ---------------- | ----------------- | -------------------- |
| `{{MODULE_NAME}}` | `{{SUBMODULES}}` | `{{MODULE_DESC}}` | `{{RELATED_ASSETS}}` |
## 4. 边界与原则
- `{{BOUNDARY_RULE_1}}`
- `{{BOUNDARY_RULE_2}}`
## 5. 待确认项
- `{{OPEN_ISSUE}}`

View File

@@ -0,0 +1,56 @@
# lite-prd 轻量级 PRD 模板One-Pager
> 用途:用于快速沉淀功能需求,可直接复制到文档或 Issue 跟踪工具(如 Notion、Markdown、Jira
> 目标:在一页内讲清楚 Why / What / How / Edge Cases / DoD重点突出减少歧义与研发返工。
## 1. 需求概述The "Why" & "What"
- 需求名称:`[一句话描述功能]`
- 用户故事:`作为一个 [目标用户],我希望能够 [执行动作],以便于 [达成核心价值/解决什么痛点]。`
- 业务价值(可选):`[效率提升/转化提升/成本下降/体验优化等预期业务结果]`
## 2. 功能交互与核心定义The "How"
> 说明:明确功能的核心流程、状态流转和数据输入输出。
| 核心维度 | 定义细则 |
| --- | --- |
| 触发机制Trigger | `[用户或系统在什么条件下触发该核心流程]` |
| 前置条件Pre-condition | `[执行操作前系统/用户必须具备的状态参数或权限]` |
| 用户交互Input/Action | `[用户提供的关键输入、操作动作及限制]` |
| 系统反馈Output/Response | `[系统处理后的最终响应或呈现的 UI 结构]` |
推荐补充核心数据结构或交互流(按需裁剪):
```json
{
"type": "object",
"required": ["action", "status"],
"properties": {
"action": {
"type": "string",
"description": "触发核心操作"
},
"status": {
"type": "string",
"enum": ["pending", "success", "failed"]
}
}
}
```
## 3. 异常处理与兜底策略Edge Cases & Fallbacks
- 过渡状态:`[网络请求中的 Loading 样式、骨架屏、或者长时间处理时的进度反馈]`
- 异常/超时处理:`[接口报错、数据为空、网络超时等异常情况下的提示文案与空页面状态]`
- 边界条件限制:`[超长文本截断规则、极值处理、翻页边界、极速连击操作防护等]`
- 业务强约束:`[数据流转的权限隔离、黑灰产防护、强校验规则等不可突破的底线]`
## 4. 验收标准Definition of Done - DoD
- 核心链路:`前置条件满足 -> [核心业务动作] -> 系统正确处理结果 -> UI 渲染符合预期`
- 非功能性指标(按需):`如页面秒开率/特定接口响应时间 <= [X] 秒`
- 数据指标(按需):`核心埋点上报齐全,关键漏斗数据可被观测`
- 优先级验收项分类安排:
- `P0核心路线``[不实现就无法上线的最简闭环能力]`
- `P1体验路线``[锦上添花、在极端边界下的优雅反馈补充]`

View File

@@ -0,0 +1,10 @@
# 主动沉淀记忆日志
> 用途:用最少 token 记录主动沉淀记忆动作。
> 规则:一行一条,按时间倒序追加;无内容统一写 `-`。
> 字段:`时间 | kind | src | sum | doc | theme | data | git | todo`
> `kind``upd`=普通维护,`trim`=日志裁剪
{{DATE}} | upd | {{TRIGGER}} | {{SUMMARY}} | {{DOC_CHANGES}} | {{THEME_CHANGES}} | {{DATA_CHANGES}} | {{GIT_COMMIT}} | {{FOLLOW_UP}}
{{DATE}} | trim | sys | 删旧100行 | - | - | - | - | 保留较新记录

View File

@@ -0,0 +1,26 @@
# 页面地图模板
> 适用场景:项目包含多个页面、多个模块入口,或需要快速建立页面导航关系时使用。
> 可不生成/可合并:若项目只有单页或页面结构已在其他文档中足够清晰,可跳过;也可与信息架构文档合并。
## 1. 文档目标
- 说明页面层级、入口关系与页面用途
- 帮助人和 Agent 快速定位要查看的页面资产
## 2. 页面结构
```text
{{PAGE_TREE}}
```
## 3. 页面清单
| 页面/模块 | 路径或入口 | 用途 | 关联规格 |
|------|------|------|------|
| `{{PAGE_NAME}}` | `{{PAGE_PATH}}` | `{{PAGE_PURPOSE}}` | `{{RELATED_SPEC}}` |
## 4. 待确认项
- `{{OPEN_ISSUE}}`

View File

@@ -0,0 +1,25 @@
# 权限模型模板
> 适用场景:项目存在多角色、多权限边界、可见性差异或操作控制时使用。
> 可不生成/可合并:若项目只有单一角色或无明显权限差异,可跳过;也可与状态或业务流程文档合并。
## 1. 文档目标
- 说明角色边界、可见范围与关键操作权限
- 为页面展示和交互控制提供统一依据
## 2. 角色清单
| 角色 | 核心职责 | 主要关注点 |
|------|------|------|
| `{{ROLE_NAME}}` | `{{ROLE_RESPONSIBILITY}}` | `{{ROLE_FOCUS}}` |
## 3. 权限矩阵
| 操作/资源 | `{{ROLE_A}}` | `{{ROLE_B}}` | 规则说明 |
|------|------|------|------|
| `{{ACTION_OR_RESOURCE}}` | `Y/N` | `Y/N` | `{{RULE_DESC}}` |
## 4. 待确认项
- `{{OPEN_ISSUE}}`

View File

@@ -0,0 +1,276 @@
# RPD 示例(用户故事主导):新建文档模板功能
> 说明:这里使用 `RPD` 命名Requirements/Product Document。如果团队习惯 `PRD`,可直接替换标题,不影响结构。
## 0. 文档信息
- 版本:`v1.0`
- 状态:`Draft`
- 目标上线:`2026-03-31`
- 负责人:`产品经理 / 模板能力小组`
- 关联范围:`新建文档弹窗``模板选择``模板变量替换``文档初始化`
---
## 1. 背景与目标
### 1.1 背景问题
当前“新建文档”流程存在以下问题:
1. 只能从空白开始,产出质量依赖个人经验。
2. 不同文档PRD、用户故事、研究报告结构不统一协作成本高。
3. 新成员不清楚文档标准,反复返工。
### 1.2 业务目标
1. 将“新建文档到首版可评审”的平均时长缩短 `40%`
2. 模板创建文档占比提升到 `>= 70%`
3. 核心文档字段完整率(必填章节)达到 `>= 90%`
### 1.3 非目标
1. 本期不做模板在线协同编辑(多人同时编辑模板)。
2. 本期不做跨项目模板市场(仅项目内模板库)。
3. 本期不做智能自动写全篇(仅模板结构和变量预填)。
---
## 2. 用户角色
| 角色 | 核心诉求 | 使用频率 |
| --- | --- | --- |
| 产品经理PM | 快速产出高质量需求文档 | 高 |
| 交互设计师UX | 基于统一结构补充交互说明 | 中 |
| 技术负责人TL | 快速定位需求边界和验收标准 | 中 |
| 观察者Viewer | 查看文档,不可编辑模板 | 中 |
---
## 3. 用户故事(核心)
### US-01 按文档类型筛选模板
**As a** 产品经理
**I want to** 在新建文档时按“文档类型”筛选模板
**So that** 我能快速找到合适模板并开始填写
**验收标准**
1. Given 用户打开新建文档弹窗
When 选择 `文档类型=RPD`
Then 模板列表仅展示 `RPD` 相关模板。
2. Given 当前类型下没有模板
When 用户查看模板区
Then 展示“暂无模板”空状态,并保留“从空白创建”入口。
---
### US-02 预览模板结构
**As a** 产品经理
**I want to** 在选中模板前预览章节结构和说明
**So that** 我能判断模板是否符合当前需求
**验收标准**
1. Given 用户点击某模板卡片
When 打开预览
Then 可看到章节目录、示例段落、更新时间。
2. Given 预览打开
When 用户切换模板
Then 预览内容在 `500ms` 内更新完成。
---
### US-03 基于模板创建文档并替换变量
**As a** 产品经理
**I want to** 选择模板后自动替换基础变量(项目名、负责人、日期)
**So that** 我无需重复手工填写固定信息
**验收标准**
1. Given 用户选择模板并点击“创建”
When 系统生成文档
Then 模板中的 `{{project_name}}``{{owner}}``{{date}}` 被替换为当前上下文值。
2. Given 模板包含未提供值的变量
When 生成文档
Then 该变量保留占位并高亮提示待补充。
---
### US-04 模板推荐(可选增强)
**As a** 产品经理
**I want to** 看到“最近常用模板”和“同类型热门模板”
**So that** 我可以更快做选择
**验收标准**
1. Given 用户打开新建文档弹窗
When 有历史记录
Then 列表顶部展示最近使用模板(最多 `3` 个)。
2. Given 没有历史记录
When 查看模板区
Then 默认按“团队推荐”排序展示。
---
## 4. MVP 范围Story Mapping
| 优先级 | 用户活动 | 故事 | 本期是否纳入 |
| --- | --- | --- | --- |
| Must | 选择模板 | US-01 按类型筛选 | 是 |
| Must | 评估模板 | US-02 预览模板结构 | 是 |
| Must | 生成文档 | US-03 变量替换创建 | 是 |
| Should | 快速选择 | US-04 模板推荐 | 否(下期) |
---
## 5. 对应模型
### 5.1 Domain Model业务领域模型
| 实体 | 关键字段 | 说明 |
| --- | --- | --- |
| Template | id, name, doc_type, status | 模板主实体 |
| TemplateVersion | version, content, published_at | 模板版本 |
| TemplateVariable | key, required, default_value | 模板变量定义 |
| Document | id, title, content, source_template_id | 生成后的文档 |
| User | id, role, display_name | 使用者 |
| TemplateUsageLog | template_id, user_id, created_doc_id, created_at | 使用记录 |
```mermaid
classDiagram
class Template {
+string id
+string name
+string doc_type
+string status
}
class TemplateVersion {
+string id
+string template_id
+string version
+text content
}
class TemplateVariable {
+string id
+string version_id
+string key
+bool required
+string default_value
}
class Document {
+string id
+string title
+text content
+string source_template_id
}
class TemplateUsageLog {
+string id
+string template_id
+string user_id
+string created_doc_id
}
Template "1" --> "many" TemplateVersion
TemplateVersion "1" --> "many" TemplateVariable
Template "1" --> "many" Document
Template "1" --> "many" TemplateUsageLog
```
### 5.2 Business Flow / Process业务流程模型
```mermaid
flowchart TD
A["打开新建文档"] --> B["选择文档类型"]
B --> C["拉取模板列表"]
C --> D{"是否有模板?"}
D -- 是 --> E["查看模板预览"]
E --> F["选择模板并填写变量"]
F --> G["创建文档"]
G --> H["进入编辑器"]
D -- 否 --> I["从空白创建"]
I --> H
```
### 5.3 State Machine / Lifecycle状态与生命周期模型
```mermaid
stateDiagram-v2
[*] --> Draft
Draft --> Published: 发布
Published --> Deprecated: 下线
Deprecated --> Published: 重新启用
Published --> Archived: 归档
Archived --> [*]
```
### 5.4 Permission / Access Model权限模型
| 操作 | PM | UX | TL | Viewer |
| --- | --- | --- | --- | --- |
| 查看模板列表 | Y | Y | Y | Y |
| 预览模板 | Y | Y | Y | Y |
| 使用模板创建文档 | Y | Y | Y | N |
| 新增/编辑模板 | Y | N | N | N |
| 发布/下线模板 | Y | N | N | N |
### 5.5 Page Structure Model页面结构模型
| 页面/区域 | 子项 | 说明 |
| --- | --- | --- |
| 新建文档弹窗 | 文档类型选择器 | 切换模板集合 |
| 新建文档弹窗 | 模板列表区 | 卡片展示模板 |
| 新建文档弹窗 | 模板预览区 | 展示目录和示例 |
| 新建文档弹窗 | 变量输入区 | 创建前补齐变量 |
| 新建文档弹窗 | 操作区 | 取消 / 从空白创建 / 创建 |
### 5.6 Field Usage / Visibility Model字段可见性模型
| 字段 | PM | UX | TL | Viewer | 规则 |
| --- | --- | --- | --- | --- | --- |
| 模板名称 | 可见 | 可见 | 可见 | 可见 | 必显 |
| 模板类型 | 可见 | 可见 | 可见 | 可见 | 必显 |
| 模板变量输入 | 可编辑 | 可编辑 | 可编辑 | 不可见 | 仅创建者可操作 |
| 发布状态 | 可见可改 | 可见 | 可见 | 可见 | 仅 PM 可修改 |
### 5.7 Prototype Variant / Context Model原型变体模型
| 变体 | 适用场景 | 差异点 |
| --- | --- | --- |
| 简版Quick | 快速记录 | 仅模板列表 + 创建按钮 |
| 标准版Default | 日常创建 | 列表 + 预览 + 变量替换 |
| 高级版Advanced | 模板治理 | 增加版本说明、推荐排序、状态筛选 |
---
## 6. 非功能需求
1. 性能:模板列表接口 P95 响应时间 `< 300ms`
2. 可用性:从打开弹窗到进入编辑器,核心路径不超过 `3` 步。
3. 可观测性:记录模板使用率、创建成功率、空白创建比例。
4. 安全性:模板内容按项目空间隔离,禁止跨项目读取。
---
## 7. 验收清单Definition of Done
- [ ] US-01 ~ US-03 对应验收标准全部通过。
- [ ] 模板变量替换逻辑覆盖正常、缺失、非法三类输入。
- [ ] 新建文档流程埋点完整(曝光、点击、创建成功/失败)。
- [ ] 用户手册新增“如何使用模板创建文档”章节。
---
## 8. 使用说明(给模板作者)
1. 复制本文件,按实际业务替换“背景、用户故事、模型、指标”。
2. 每条用户故事必须带 `Given / When / Then` 验收标准。
3. 至少保留以下模型:`Domain``Flow``State``Permission`
4. 若为复杂业务,追加 `事件模型``数据血缘模型`

View File

@@ -0,0 +1,61 @@
# {{PROJECT_NAME}} 项目说明清单
> 用途:作为项目文档总入口,帮助人和 Agent 快速理解项目,并按需加载后续子文档。
> 说明:本模板应保持简洁,不在这里展开详细业务流程、信息架构、数据模型或页面规格。
> 上下文约束:本文件必须保持轻量;当内容明显重复,或文件达到 `1000` 行时,必须立即做“分包拆分 + 摘要汇总”优化,并优先控制在 `800` 行以内。总入口只保留高价值摘要、索引和必要待办,其余内容迁移到专题子文档。
## 1. 项目简介
- 项目名称:`{{PROJECT_NAME}}`
- 项目定位:`{{PROJECT_SUMMARY}}`
- 目标用户:`{{TARGET_USERS}}`
- 当前阶段:`{{PROJECT_STAGE}}`
## 2. 核心场景
- `{{CORE_SCENARIO_1}}`
- `{{CORE_SCENARIO_2}}`
- `{{CORE_SCENARIO_3}}`
## 3. 阅读顺序
1. 先阅读本文件,确认项目范围与索引
2. 再按需阅读专题子文档
3. 最后进入页面级 `spec.md`、需求文档、主题文档与数据表
## 4. 文档索引
| 文档 | 用途 | 是否必读 |
|------|------|---------|
| `src/docs/page-map.md` | 页面地图与入口导航 | `按需` |
| `src/docs/information-architecture.md` | 信息架构与模块边界 | `按需` |
| `src/docs/business-flow.md` | 业务流程与关键路径 | `按需` |
| `src/docs/data-model.md` | 核心数据对象与字段摘要 | `按需` |
| `src/docs/permission-model.md` | 权限边界与角色能力 | `按需` |
| `src/docs/state-lifecycle.md` | 状态流转与生命周期 | `按需` |
可根据项目复杂度灵活删减、合并或替换上述文档,并在此处同步更新索引。
## 5. 主题索引
- 默认主题:`{{DEFAULT_THEME}}`
- 主题文档:`{{THEME_DOCS}}`
- 主题目录:`{{THEME_PATHS}}`
## 6. 数据索引
- 关键数据表:`{{DATA_INDEX}}`
- 数据目录:`src/database/`
- 说明:只记录关键表与用途,不在本文件展开字段明细
## 7. 原型索引
- 关键页面或原型:`{{PROTOTYPE_INDEX}}`
- 页面级规格:`{{SPEC_INDEX}}`
- 需求文档:`{{PRD_INDEX}}`
## 8. 当前待补事项
- `{{OPEN_ITEM_1}}`
- `{{OPEN_ITEM_2}}`
- `{{OPEN_ITEM_3}}`

View File

@@ -0,0 +1,37 @@
# 设计 Review 日志
本文件记录每次设计 Review 的执行情况,供后续 Review 确定增量范围使用。
## 格式说明
每行一条日志,字段以 ` | ` 分隔,顺序如下:
```text
YYYY-MM-DD HH:mm | kind | scope | basis | violations | suggestions | todo
```
字段要求:
- `kind``review` 表示正常审查,`trim` 表示日志裁剪
- `scope`:审查范围摘要,尽量控制在 20 字以内(如 `3 prototypes, 1 component`
- `basis`:审查依据(主题名或设计规范文件名,如 `firecrawl/DESIGN.md`
- `violations`:违规数,格式为 `Nc/Nw/Ni`Critical/Warning/Info无违规写 `0/0/0`
- `suggestions`:主题扩展建议数,如 `3 items`,无建议写 `-`
- `todo`:后续待办或 `-`
记录原则:
- 能用短词就不用长句,能用文件名就不用解释性段落
- 未变化字段统一写 `-`
- 若一次审查涉及较多原型,优先记录数量摘要,不在日志中逐一列举
示例:
```text
2026-03-28 22:00 | review | 5 prototypes, 2 components | trae-design/DESIGN.md | 2/5/3 | 3 items | 提取 Accordion 组件
2026-03-29 10:30 | review | ref-app-home | firecrawl/DESIGN.md | 0/1/0 | - | -
2026-04-01 14:00 | trim | - | - | - | - | 删旧50行
```
## 日志记录

View File

@@ -0,0 +1,152 @@
# [组件/原型名称]
## 📋 业务与功能
### 1.1 核心目标
> 简要说明该组件/原型的核心定位、解决的问题、用户价值
[在此描述核心目标...]
### 1.2 功能清单
> 列出所有功能模块及其优先级
- **[功能模块1]**[功能描述]
- **[功能模块2]**[功能描述]
- **[功能模块3]**[功能描述]
### 1.3 交互要点
> 关键的交互触发点、反馈机制、状态变化
- [交互点1][触发条件] → [响应行为]
- [交互点2][触发条件] → [响应行为]
---
## 📊 内容规划
### 2.1 信息架构
> 模块划分、信息层级、内容组织方式
```
[原型/组件名称]
├── [模块A]
│ ├── [内容项A1]
│ └── [内容项A2]
├── [模块B]
└── [模块C]
```
### 2.2 数据来源
> 数据源优先级:用户提供数据 > 项目数据表 > 生成示例数据
- **数据类型**[列表/单据/配置/...]
- **数据源**[用户提供/数据表路径/生成]
- **关键字段**
- `field1`: [字段说明]
- `field2`: [字段说明]
### 2.3 内容示例
> (可选)重要的示例内容、文案语气、术语规范
[示例内容...]
---
## 🎨 布局与结构
### 3.1 整体布局
> 布局模式(单栏/双栏/网格/自由)、模块尺寸、比例约束
- **布局模式**[单栏/双栏/网格/自由]
- **容器宽度**[固定/流式/混合]
- **关键尺寸**
- [模块A][宽度/高度/比例]
- [模块B][宽度/高度/比例]
### 3.2 响应式适配
> (如适用)断点、适配策略
- **桌面端≥1200px**[布局描述]
- **平板端768-1199px**[布局调整]
- **移动端(<768px**[布局调整]
---
## 🎨 视觉规范
### 4.1 设计规范来源
> 优先级:用户规范 > 主题DESIGN.md> 内置interface-design / frontend-design
**设计规范来源**`[只写最终采用的一条:用户规范 <path>|主题 /src/themes/<name>|内置 interface-design内置 frontend-design]`
**说明**设计令牌Design Tokens将从上述规范中动态读取无需在此列出具体值。
### 4.2 自定义设计要点
> (可选)仅在有特殊的自定义设计要求时填写,如特殊色彩、特殊尺寸等
**自定义色彩**(如有):
- [自定义色1][色值] - [用途]
**自定义尺寸**(如有):
- [自定义尺寸1][值] - [用途]
**其他自定义规范**(如有):
- [说明...]
### 4.3 组件状态
> 交互元素的状态定义
- **默认态default**[描述]
- **悬停态hover**[描述]
...
- **加载态loading**[描述,如适用]
---
## ⚙️ Axure API 说明
> 如果不使用 Axure API可删除此部分
### 5.1 事件列表eventList
> 组件对外暴露的事件
| 事件名称 | Payload 类型 | 触发时机 | 说明 |
|---------|-------------|---------|------|
| `event_name` | `string` | [触发时机] | [说明] |
### 5.2 动作列表actionList
> 组件可被调用的动作
| 动作名称 | Params 类型 | 参数说明 | 功能描述 |
|---------|------------|---------|---------|
| `action_name` | `string` | [参数说明] | [功能描述] |
### 5.3 变量列表varList
> 组件内部状态变量
| 变量名称 | 类型 | 默认值 | 说明 |
|---------|-----|-------|------|
| `var_name` | `string/number/boolean/object` | [默认值] | [说明] |
**命名规范**:使用 snake_case小写字母、数字、下划线
### 5.4 配置项列表configList
> 组件配置项
| 配置项名称 | 类型 | 默认值 | 说明 |
|----------|-----|-------|------|
| `config_name` | `string/number/boolean/object` | [默认值] | [说明] |
### 5.5 数据项列表dataList
> 组件数据结构定义
**数据结构**
```typescript
{
field1: string; // [字段说明]
field2: number; // [字段说明]
field3: { // [嵌套对象说明]
subField1: string;
subField2: boolean;
};
}
```

View File

@@ -0,0 +1,31 @@
# 状态与生命周期模板
> 适用场景:项目存在业务状态流转、审批状态、任务生命周期或对象阶段变化时使用。
> 可不生成/可合并:若状态极少或已包含在业务流程文档中,可跳过;也可与业务流程文档合并。
## 1. 文档目标
- 说明对象从创建到结束的状态变化规则
- 帮助页面、数据、流程对齐状态口径
## 2. 状态流转
```mermaid
stateDiagram-v2
StateA: {{STATE_A}}
StateB: {{STATE_B}}
[*] --> StateA
StateA --> StateB: {{TRANSITION_EVENT}}
StateB --> [*]
```
## 3. 状态说明
| 状态 | 进入条件 | 可执行动作 | 退出条件 |
|------|------|------|------|
| `{{STATE_NAME}}` | `{{ENTER_CONDITION}}` | `{{AVAILABLE_ACTIONS}}` | `{{EXIT_CONDITION}}` |
## 4. 异常与待确认
- `{{OPEN_ISSUE}}`

View File

@@ -0,0 +1,99 @@
# 工作总结模板
本文件包含两种总结格式,生成时按用户选择使用对应模板。
---
## 格式 A精简版
适用于日报、站会汇报、快速同步。控制在 **20 行以内**,突出核心产出。
```markdown
# 工作总结 — YYYY-MM-DD
## 📌 今日产出
- ✅ 新增原型「xxx-page」— 完成首页布局与交互实现
- ✅ 更新组件「yyy-button」— 新增 loading 状态
- ✅ 新增文档「数据模型说明」
```
**字段说明**
| 字段 | 规则 |
|------|------|
| 产出条目 | 每条一行,格式:`✅ <变更类型><资源类型>「<名称>」— <一句话描述>` |
| 变更类型 | 新增 / 更新 / 删除 / 重构 |
| 资源类型 | 原型 / 组件 / 主题 / 文档 / 数据表 / 技能 / 公共模块 |
| 描述 | 15 字以内,提炼核心改动点 |
**注意**
- 若当日无变更,输出 `📌 今日无文件变更记录`
- Agent 仅输出基于文件变更记录的已完成产出,不推测「进行中」或「待办」
- 如果用户主动补充了计划或待办,可在末尾追加 `📋 补充` 板块
---
## 格式 B详尽版
适用于周报、里程碑回顾、团队交接。包含完整变更列表与统计数据。
```markdown
# 工作总结 — YYYY-MM-DD ~ YYYY-MM-DD
## 📊 概览
| 指标 | 数值 |
|------|------|
| 总变更文件数 | N |
| 新增原型 | N |
| 更新原型 | N |
| 新增/更新组件 | N |
| 文档变更 | N |
| 主题变更 | N |
| 数据表变更 | N |
## 📌 核心产出
### 原型与页面
| 原型 | 变更类型 | 关键改动 |
|------|---------|---------|
| `xxx-page` | 新增 | 首页布局、导航栏、数据展示卡片 |
| `yyy-page` | 更新 | 优化表单验证逻辑,新增错误提示 |
### 组件
| 组件 | 变更类型 | 关键改动 |
|------|---------|---------|
| `zzz-button` | 更新 | 新增 loading、disabled 状态 |
### 文档与资源
| 资源 | 变更类型 | 说明 |
|------|---------|------|
| `data-model.md` | 新增 | 订单数据模型文档 |
| `firecrawl/DESIGN.md` | 更新 | 补充阴影层级规范 |
## 💡 技术备注
- 首页原型使用了 `firecrawl` 主题的色彩体系
- 按钮组件新增的 loading 态复用了主题动画变量
```
**字段说明**
| 字段 | 规则 |
|------|------|
| 概览统计 | 自动从扫描结果中聚合,按类型计数 |
| 核心产出表格 | 按资源类型分组,每个变更文件一行 |
| 关键改动 | 从 commit message 或 spec.md 提取20 字以内 |
| 技术备注 | 值得记录的技术决策或依赖关系,如主题引用、组件复用等 |
**注意**
- 概览统计中数值为 0 的行可省略
- 技术备注板块仅在有值得记录的技术决策时展示
- 按变更时间倒序排列(最新的在前)
- Agent 仅输出基于文件变更和 commit 记录的已完成产出,不推测「进行中」或「后续计划」
- 如果用户主动补充了计划或待办,可在末尾追加 `📋 补充` 板块

View File

@@ -0,0 +1,59 @@
# 售后诉求一键提取lite-prd 演示)
> 侧重点:非结构化信息(文本/语音/图片)转结构化售后表单。
## 1. 需求概述The "Why" & "What"
- 需求名称:`AI 智能识别聊天记录并自动填报售后单`
- 用户故事:`作为一个收到瑕疵商品想退换货的用户,我希望能够在客服对话框直接发牢骚和破损照片,以便于系统能自动帮我填好复杂的退换货申请单(包括退款原因、金额等),我只需点击确认即可。`
- 业务价值:降低售后填单门槛,缩短用户完成申请路径,提升售后单提交转化率。
## 2. AI 交互核心定义The "How" for AI
固定枚举值 JSON 约束:
```json
{
"type": "object",
"required": ["intent", "reason", "amount", "confidence"],
"properties": {
"intent": {
"type": "string",
"enum": ["退货退款", "仅退款", "换货"]
},
"reason": {
"type": "string",
"enum": ["破损", "错发", "质量问题", "少件漏发", "其他"]
},
"amount": {
"type": "number",
"minimum": 0
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
}
},
"additionalProperties": false
}
```
## 3. 容错与兜底策略Fallback & Edge Cases
- 等待体验:对话框内展示“正在识别您的诉求并生成售后表单...”的加载卡片,并显示骨架屏,最长展示 15 秒。
- 超时处理:若请求超过 15 秒无响应,提示“识别超时,请重试或手动填写售后单”,并提供“重试识别”和“手动填写”双入口。
- 意图置信度低:若 `confidence < 0.8`,不自动提交表单,弹出半屏卡片引导用户选择:
- `[仅退款]`
- `[退货退款]`
- `[换货]`
- 金额超限拦截AI 建议退款金额绝不允许超过订单实付金额;若超限,系统强制覆盖为实付金额上限并记录修正日志。
- 幻觉/格式错误:若返回 JSON 解析失败,自动重试 1 次;重试后仍失败则提示“生成失败,请补充更明确的问题描述”,并允许手动编辑表单。
- 撤销机制:自动生成的表单提交前必须提供明确的“修改”入口,允许用户手动调整意图、原因、金额。
## 4. 验收标准Definition of Done - DoD
- 成功路径:用户发送含明确售后诉求的图文消息 -> 系统返回符合约束的结构化 JSON -> 对话框推送预填售后确认卡片 -> 用户确认后提交成功。
- 性能指标:平均响应时间 <= 3 秒P95 <= 8 秒;超时率(>15 秒)<= 2%。
- P0用户发送包含“退货”“破损”等强售后意图的图文后对话框能立刻推送已预填且原因正确的售后确认卡片。
- P1针对纯文本情绪表达如“这什么破东西”AI 能温和反问“非常抱歉,请问商品是哪里出现了问题?”,引导用户补充有效信息,而非盲目填单。

View File

@@ -0,0 +1,234 @@
# 需求评审工作台需求文档示例(用户故事驱动)
## 0. 文档信息
- 文档类型:`RPDRequirements/Product Document`
- 版本:`v1.0`
- 状态:`Ready for Review`
- 创建日期:`2026-02-25`
- 负责人:`产品平台组`
- 关联范围:`新建文档``评审流转``评论协作``状态跟踪`
---
## 1. 背景与目标
### 1.1 背景问题
当前需求评审流程主要靠 IM + 零散文档同步,导致:
1. 评审意见分散,无法形成可追踪闭环。
2. 用户故事与验收标准经常在评审中丢失或被弱化。
3. 需求状态(待评审/评审中/已通过)缺乏统一视图。
### 1.2 业务目标
1. 需求从“提交评审”到“评审完成”的周期缩短 `30%`
2. 含完整用户故事与验收标准的需求占比提升到 `>= 85%`
3. 评审意见闭环率(有结论且状态更新)达到 `>= 90%`
### 1.3 非目标
1. 本期不实现跨项目评审。
2. 本期不做自动生成 UI 原型,仅支持文档评审。
3. 本期不接入外部审批系统(如 OA/流程引擎)。
---
## 2. 用户角色
---
## 3. 用户故事(核心)
### US-01 提交需求进入评审
**As a** PM
**I want to** 将 RPD 文档提交到评审工作台
**So that** 相关角色可以在统一入口完成评审
**Acceptance Criteria**
1. Given PM 在文档页点击“提交评审”
When 必填字段(背景、用户故事、验收标准)完整
Then 文档状态变为 `in_review`,并通知已选评审人。
2. Given 文档缺少必填字段
When PM 提交评审
Then 阻止提交并高亮缺失项。
---
### US-02 按用户故事维度评论
**As a** 评审者
**I want to** 在指定用户故事或验收标准上评论
**So that** 反馈与需求上下文强绑定,避免歧义
**Acceptance Criteria**
1. Given 评审者打开文档
When 选中某条用户故事或某条 AC
Then 可发布结构化评论(问题类型、建议、优先级)。
2. Given 评论发布成功
When PM 查看评论列表
Then 可看到“对应故事编号”和“处理状态”。
---
### US-03 评审意见闭环
**As a** PM
**I want to** 对每条评论标记“接受/拒绝/延后”并补充说明
**So that** 评审过程可追踪、可复盘
**Acceptance Criteria**
1. Given 有未处理评论
When PM 逐条处理
Then 每条评论都有最终处理结论与处理人。
2. Given 所有评论已处理
When TL 点击“评审通过”
Then 文档状态变为 `approved` 并记录通过时间。
---
### US-04 评审看板追踪
**As a** 项目经理
**I want to** 在看板中按状态和负责人筛选需求
**So that** 我能快速识别阻塞项并推动协作
**Acceptance Criteria**
1. Given 进入评审看板
When 按状态筛选 `in_review`
Then 仅展示评审中需求。
2. Given 某需求超过 SLA48h未完成
When 看板刷新
Then 显示超时标记并置顶提醒。
---
## 4. MVP 范围Story Mapping
---
## 5. 对应模型
### 5.1 Domain Model业务领域模型
```mermaid
classDiagram
class RequirementDoc {
+string id
+string title
+string owner_id
+string status
}
class UserStory {
+string id
+string doc_id
+string story_code
}
class AcceptanceCriterion {
+string id
+string story_id
+text content
}
class ReviewComment {
+string id
+string target_type
+string target_id
+string status
}
class ReviewTask {
+string id
+string doc_id
+string reviewer_id
+string task_status
}
class ReviewDecision {
+string id
+string comment_id
+string decision
}
RequirementDoc "1" --> "many" UserStory
UserStory "1" --> "many" AcceptanceCriterion
RequirementDoc "1" --> "many" ReviewTask
RequirementDoc "1" --> "many" ReviewComment
ReviewComment "1" --> "0..1" ReviewDecision
```
### 5.2 Business Flow / Process业务流程模型
```mermaid
flowchart TD
A["PM 完成 RPD"] --> B["提交评审"]
B --> C["系统校验必填项"]
C --> D{"校验通过?"}
D -- 否 --> E["返回缺失项提示"]
D -- 是 --> F["创建评审任务并通知评审人"]
F --> G["评审者评论与建议"]
G --> H["PM 处理评论"]
H --> I{"是否全部闭环?"}
I -- 否 --> G
I -- 是 --> J["TL 确认通过"]
J --> K["状态=approved"]
```
### 5.3 State Machine / Lifecycle状态与生命周期模型
```mermaid
stateDiagram-v2
[*] --> Draft
Draft --> InReview: 提交评审
InReview --> ChangesRequested: 要求修改
ChangesRequested --> InReview: 重新提交
InReview --> Approved: 评审通过
Approved --> Archived: 归档
Archived --> [*]
```
### 5.4 Permission / Access Model权限模型
### 5.5 Page Structure Model页面结构模型
### 5.6 Field Usage / Visibility Model字段可见性模型
### 5.7 Prototype Variant / Context Model原型变体模型
---
## 6. 非功能需求
1. 性能:文档页评论面板打开时间 P95 `< 400ms`
2. 一致性:评论状态更新后 `<= 2s` 在看板同步可见。
3. 安全性:仅文档成员可见评论详情,支持操作审计日志。
4. 可用性:核心评审流程(提交 -> 评论 -> 闭环)不超过 `4` 个主要操作。
---
## 7. 验收清单Definition of Done
- [ ] 4 条用户故事均有可执行的 Given/When/Then 验收标准。
- [ ] 状态机从 `draft``approved` 流转完整可回放。
- [ ] 评论闭环数据可导出(含处理结论和处理人)。
- [ ] 关键埋点可用:提交评审、评论创建、评论闭环、评审通过。
---
## 8. 发布计划
### 8.1 v1.0(本期)
- 支持 RPD 提交评审、按故事评论、评论闭环、评审通过。
### 8.2 v1.1(下期)
- 增加评审看板和 SLA 超时提醒US-04
### 8.3 v2.0(后续)
- 增加跨项目评审视图与组织级质量报表。
&nbsp;

View File

@@ -0,0 +1,35 @@
export type ClassDictionary = Record<string, boolean | null | undefined>
export type ClassValue =
| string
| number
| null
| undefined
| false
| ClassDictionary
| ClassValue[]
function flattenClassValue(value: ClassValue, tokens: string[]) {
if (!value) return
if (typeof value === "string" || typeof value === "number") {
tokens.push(String(value))
return
}
if (Array.isArray(value)) {
value.forEach((item) => flattenClassValue(item, tokens))
return
}
Object.entries(value).forEach(([key, enabled]) => {
if (enabled) tokens.push(key)
})
}
export function cn(...inputs: ClassValue[]) {
const tokens: string[] = []
inputs.forEach((input) => flattenClassValue(input, tokens))
return [...new Set(tokens.join(" ").split(/\s+/).filter(Boolean))].join(" ")
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,548 @@
/**
* @name Antd 电商后台
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
* - /src/themes/antd-new/designToken.json (Ant Design 主题)
* - /skills/default-resource-recommendations/SKILL.md (Ant Design 组件库)
*/
import './style.css';
import React, { useState, useCallback, useImperativeHandle, forwardRef, useEffect, useRef } from 'react';
import * as echarts from 'echarts';
import {
Layout,
Card,
Row,
Col,
Statistic,
Table,
Tag,
Button,
Space,
List,
Avatar,
Typography,
Badge,
DatePicker,
theme,
Divider,
Tabs
} from 'antd';
import {
ArrowUpOutlined,
ArrowDownOutlined,
ShoppingOutlined,
UserOutlined,
MoneyCollectOutlined,
ShoppingCartOutlined,
EllipsisOutlined,
ReloadOutlined
} from '@ant-design/icons';
import type {
KeyDesc,
DataDesc,
ConfigItem,
Action,
EventItem,
AxureProps,
AxureHandle
} from '../../common/axure-types';
import SideMenu from '../../components/side-menu';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
// --- Chart Components ---
const SalesTrendChart = () => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
useEffect(() => {
if (!chartRef.current) return;
chartInstance.current = echarts.init(chartRef.current);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'line' }
},
grid: {
left: '20', // Reduced padding
right: '20',
bottom: '10',
top: '30',
containLabel: true,
borderWidth: 0
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#94a3b8' }
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: { type: 'dashed', color: '#f1f5f9' }
},
axisLabel: { color: '#94a3b8' }
},
series: [
{
name: '销售额',
type: 'line',
smooth: true,
showSymbol: false,
data: [12000, 13200, 10100, 13400, 9000, 23000, 21000],
itemStyle: { color: '#3b82f6' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
{ offset: 1, color: 'rgba(59, 130, 246, 0)' }
])
},
lineStyle: { width: 3 }
},
{
name: '到访用户',
type: 'line',
smooth: true,
showSymbol: false,
data: [2200, 1820, 1910, 2340, 2900, 3300, 3100],
itemStyle: { color: '#10b981' },
lineStyle: { width: 3 }
}
]
};
chartInstance.current.setOption(option);
// Use ResizeObserver for robust responsiveness
const resizeObserver = new ResizeObserver(() => {
chartInstance.current?.resize();
});
resizeObserver.observe(chartRef.current);
return () => {
resizeObserver.disconnect();
chartInstance.current?.dispose();
};
}, []);
return <div ref={chartRef} className="chart-container" />;
};
const CategoryPieChart = () => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
useEffect(() => {
if (!chartRef.current) return;
chartInstance.current = echarts.init(chartRef.current);
const option = {
tooltip: {
trigger: 'item'
},
legend: {
bottom: '0%',
left: 'center',
icon: 'circle'
},
series: [
{
name: '销售占比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: '电子产品', itemStyle: { color: '#3b82f6' } },
{ value: 735, name: '服装', itemStyle: { color: '#6366f1' } },
{ value: 580, name: '家居', itemStyle: { color: '#8b5cf6' } },
{ value: 484, name: '美妆', itemStyle: { color: '#ec4899' } },
{ value: 300, name: '其他', itemStyle: { color: '#cbd5e1' } }
]
}
]
};
chartInstance.current.setOption(option);
// Use ResizeObserver for robust responsiveness
const resizeObserver = new ResizeObserver(() => {
chartInstance.current?.resize();
});
resizeObserver.observe(chartRef.current);
return () => {
resizeObserver.disconnect();
chartInstance.current?.dispose();
};
}, []);
return <div ref={chartRef} className="chart-container" />;
};
// --- Definitions ---
const EVENT_LIST: EventItem[] = [
{ name: 'onOrderClick', desc: '点击订单时触发' },
{ name: 'onProductClick', desc: '点击商品时触发' }
];
const ACTION_LIST: Action[] = [
{ name: 'refreshData', desc: '刷新数据' }
];
const VAR_LIST: KeyDesc[] = [
{ name: 'selectedOrder', desc: '当前选中的订单' }
];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'input', attributeId: 'title', displayName: '页面标题', info: '显示在页面顶部的标题', initialValue: '电商后台' }
];
const DATA_LIST: DataDesc[] = [
{
name: 'orders',
desc: '最近订单',
keys: [
{ name: 'id', desc: '订单号' },
{ name: 'customer', desc: '客户' },
{ name: 'amount', desc: '金额' },
{ name: 'status', desc: '状态' },
{ name: 'date', desc: '日期' }
]
},
{
name: 'products',
desc: '热销商品',
keys: [
{ name: 'id', desc: '商品ID' },
{ name: 'name', desc: '商品名称' },
{ name: 'sales', desc: '销量' },
{ name: 'growth', desc: '增长率' }
]
}
];
const Component = forwardRef<AxureHandle, AxureProps>(function EcommerceDashboard(innerProps, ref) {
const dataSource = innerProps && innerProps.data ? innerProps.data : {};
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function' ? innerProps.onEvent : () => undefined;
const title = typeof configSource.title === 'string' && configSource.title ? configSource.title : '电商后台';
const { token } = theme.useToken();
const defaultOrders = [
{ id: 'ORD-2023001', customer: '张三', amount: 1299.00, status: 'completed', date: '2023-10-24' },
{ id: 'ORD-2023002', customer: '李四', amount: 899.50, status: 'processing', date: '2023-10-24' },
{ id: 'ORD-2023003', customer: '王五', amount: 2599.00, status: 'pending', date: '2023-10-23' },
{ id: 'ORD-2023004', customer: '赵六', amount: 128.00, status: 'rejected', date: '2023-10-23' },
{ id: 'ORD-2023005', customer: '孙七', amount: 599.00, status: 'completed', date: '2023-10-22' },
];
const defaultProducts = [
{ id: 1, name: '无线降噪耳机 Pro', sales: 1234, growth: 12 },
{ id: 2, name: '智能运动手表 X', sales: 892, growth: -5 },
{ id: 3, name: '超薄机械键盘', sales: 645, growth: 8 },
{ id: 4, name: '4K 高清显示器', sales: 432, growth: 24 },
{ id: 5, name: '人体工学座椅', sales: 321, growth: 2 },
{ id: 6, name: '桌面收纳套装', sales: 298, growth: 15 },
{ id: 7, name: 'Type-C 扩展坞', sales: 256, growth: 3 },
];
const orders = Array.isArray(dataSource.orders) ? dataSource.orders : defaultOrders;
const products = Array.isArray(dataSource.products) ? dataSource.products : defaultProducts;
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const emitEvent = useCallback((eventName: string, payload?: any) => {
try {
onEventHandler(eventName, payload);
} catch (error) {
console.warn('onEvent 调用失败:', error);
}
}, [onEventHandler]);
useImperativeHandle(ref, () => ({
getVar: (name: string) => {
const vars: Record<string, any> = { selectedOrder };
return vars[name];
},
fireAction: (name: string, params?: any) => {
if (name === 'refreshData') {
console.log('Refreshing data...');
}
},
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
}), [selectedOrder]);
const getStatusTag = useCallback((status: string) => {
switch (status) {
case 'completed': return <Tag color="success" bordered={false}></Tag>;
case 'processing': return <Tag color="processing" bordered={false}></Tag>;
case 'pending': return <Tag color="warning" bordered={false}></Tag>;
case 'rejected': return <Tag color="error" bordered={false}></Tag>;
default: return <Tag bordered={false}></Tag>;
}
}, []);
const columns = [
{ title: '订单号', dataIndex: 'id', key: 'id', render: (text: string) => <Text strong>{text}</Text> },
{ title: '客户', dataIndex: 'customer', key: 'customer' },
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
render: (val: number) => <Text>¥{val.toFixed(2)}</Text>
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status)
},
{ title: '日期', dataIndex: 'date', key: 'date', className: 'text-gray-500' },
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button type="link" size="small" onClick={() => {
setSelectedOrder(record);
emitEvent('onOrderClick', { order: record });
}}>
</Button>
)
},
];
return (
<Layout style={{ minHeight: '100vh', background: '#f5f7fa' }}>
<SideMenu title="电商后台" />
<Layout style={{ background: '#f5f7fa' }}>
<Layout.Content className="ecommerce-dashboard">
{/* Header Section */}
<div className="dashboard-header">
<div>
<Title level={3} style={{ margin: 0, fontWeight: 600 }}>{title}</Title>
<Text type="secondary" style={{ fontSize: 13 }}></Text>
</div>
<Space size="middle">
<RangePicker style={{ borderRadius: 6 }} />
<Button type="primary" icon={<ReloadOutlined />} style={{ borderRadius: 6 }}></Button>
<Button style={{ borderRadius: 6 }}></Button>
</Space>
</div>
{/* Metrics Section */}
<Row gutter={[20, 20]} className="metric-cards">
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} bodyStyle={{ padding: '20px 24px' }}>
<Statistic
title="总销售额"
value={126560}
precision={2}
valueStyle={{ color: '#1e293b', fontSize: 24, fontWeight: 'bold' }}
prefix={<span style={{ fontSize: 18, color: '#94a3b8', marginRight: 4 }}>¥</span>}
/>
<div className="metric-footer">
<Space>
<Text type="secondary"></Text>
<Text type="success" strong>+12%</Text>
<ArrowUpOutlined style={{ color: token.colorSuccess }} />
</Space>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} bodyStyle={{ padding: '20px 24px' }}>
<Statistic
title="到访用户"
value={8846}
valueStyle={{ color: '#1e293b', fontSize: 24, fontWeight: 'bold' }}
prefix={<UserOutlined style={{ fontSize: 18, color: '#3b82f6', marginRight: 8 }} />}
/>
<div className="metric-footer">
<Space>
<Text type="secondary"></Text>
<Text type="success" strong>+5%</Text>
<ArrowUpOutlined style={{ color: token.colorSuccess }} />
</Space>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} bodyStyle={{ padding: '20px 24px' }}>
<Statistic
title="支付订单"
value={1560}
valueStyle={{ color: '#1e293b', fontSize: 24, fontWeight: 'bold' }}
prefix={<ShoppingCartOutlined style={{ fontSize: 18, color: '#8b5cf6', marginRight: 8 }} />}
/>
<div className="metric-footer">
<Space>
<Text type="secondary"></Text>
<Text type="danger" strong>-8%</Text>
<ArrowDownOutlined style={{ color: token.colorError }} />
</Space>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} bodyStyle={{ padding: '20px 24px' }}>
<Statistic
title="转化率"
value={12.5}
precision={1}
valueStyle={{ color: '#1e293b', fontSize: 24, fontWeight: 'bold' }}
suffix="%"
prefix={<ShoppingOutlined style={{ fontSize: 18, color: '#ec4899', marginRight: 8 }} />}
/>
<div className="metric-footer">
<Space>
<Text type="secondary"></Text>
<Text type="success" strong>+2%</Text>
<ArrowUpOutlined style={{ color: token.colorSuccess }} />
</Space>
</div>
</Card>
</Col>
</Row>
{/* Charts Section */}
<Row gutter={[20, 20]} style={{ marginTop: 20 }}>
<Col xs={24} lg={16}>
<Card
title={<span style={{ fontWeight: 600 }}></span>}
bordered={false}
extra={
<Space>
<Tag color="blue" bordered={false}></Tag>
<Tag bordered={false}></Tag>
<Tag bordered={false}></Tag>
</Space>
}
style={{ height: '100%' }} // Full height
>
<SalesTrendChart />
</Card>
</Col>
<Col xs={24} lg={8}>
<Card title={<span style={{ fontWeight: 600 }}></span>} bordered={false} style={{ height: '100%' }}>
<CategoryPieChart />
</Card>
</Col>
</Row>
{/* Table Section */}
<Row gutter={[20, 20]} style={{ marginTop: 20 }}>
<Col xs={24} lg={16} style={{ display: 'flex' }}>
<Card
title={<span style={{ fontWeight: 600 }}></span>}
bordered={false}
extra={<Button type="link"></Button>}
style={{ width: '100%', display: 'flex', flexDirection: 'column' }} // Flex column layout
bodyStyle={{ flex: 1, padding: 24, overflow: 'hidden' }} // Restored padding, flex grow
>
<Table
columns={columns}
dataSource={orders}
rowKey="id"
pagination={false}
size="middle"
scroll={{ x: 600 }} // Add scroll for small screens
/>
</Card>
</Col>
<Col xs={24} lg={8} style={{ display: 'flex' }}>
<Card
title={<span style={{ fontWeight: 600 }}> Top 7</span>}
bordered={false}
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
bodyStyle={{ flex: 1, padding: '12px 24px' }}
>
<List
itemLayout="horizontal"
dataSource={products}
split={false}
renderItem={(item: any, index) => (
<List.Item style={{ padding: '10px 0' }}>
<List.Item.Meta
avatar={
<div style={{
width: 24,
height: 24,
borderRadius: '50%',
background: index < 3 ? '#3b82f6' : '#f1f5f9',
color: index < 3 ? '#fff' : '#64748b',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: 12
}}>
{index + 1}
</div>
}
title={<Text style={{ fontSize: 14 }}>{item.name}</Text>}
description={
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
<Text type="secondary" style={{ fontSize: 12 }}>: {item.sales}</Text>
<Text type={item.growth > 0 ? 'success' : 'danger'} style={{ fontSize: 12 }}>
{item.growth > 0 ? '+' : ''}{item.growth}%
</Text>
</div>
}
/>
</List.Item>
)}
/>
</Card>
</Col>
</Row>
</Layout.Content>
</Layout>
</Layout>
);
});
export default Component;

View File

@@ -0,0 +1,146 @@
# Antd 电商后台首页
## 📋 业务与功能
### 1.1 核心目标
页面用于展示电商运营关键指标、趋势与业务列表,支持高效浏览与决策。
### 1.2 功能清单
- **侧边菜单**:提供后台导航入口(使用 `SideMenu` 元素,默认内置菜单项)
- **页面头部**:显示标题、欢迎语、日期范围选择、刷新/导出按钮
- **指标卡片**:展示总销售额、访问量、支付订单、转化率及同比信息
- **销售趋势**:折线图展示销量与访问量趋势,带时间范围标签
- **品类占比**:环形饼图展示品类销售占比
- **最近订单**:订单表格展示与“详情”操作
- **热销商品**Top 商品列表与销量/增长率展示
### 1.3 交互要点
- 订单表格“详情”按钮:点击后选中订单并触发 `onOrderClick` 事件,携带订单数据
- 热销商品列表项:预留点击交互,触发 `onProductClick` 事件(由外部绑定行为)
- 刷新/导出按钮与时间范围标签:当前为展示占位,可接入动作与数据刷新逻辑
- 侧边菜单交互(展开/折叠/选中):由 `SideMenu` 组件内部处理
---
## 📊 内容规划
### 2.1 信息架构
```
Antd 电商后台首页
├── 侧边菜单
├── 页面头部
│ ├── 标题与欢迎语
│ └── 操作区(日期范围 / 刷新 / 导出)
├── 指标卡片区4项
├── 图表区
│ ├── 销售趋势折线图
│ └── 品类占比饼图
└── 列表区
├── 最近订单表格
└── 热销商品列表
```
### 2.2 数据来源
- **数据类型**:订单列表(`orders`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `id`: 订单号
- `customer`: 客户名称
- `amount`: 订单金额
- `status`: 订单状态completed/processing/pending/rejected
- `date`: 下单日期
- **数据类型**:热销商品列表(`products`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `id`: 商品 ID
- `name`: 商品名称
- `sales`: 销量
- `growth`: 增长率(百分比)
- **数据类型**:指标与图表数据
- **数据源**:内置示例(可扩展为外部数据源)
- **关键字段**:指标值、趋势序列、品类占比
### 2.3 内容示例
**订单示例**
- `ORD-2023001` / 张三 / ¥1299.00 / completed / 2023-10-24
**商品示例**
- 无线降噪耳机 Pro / 销量 1234 / +12%
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:双栏(左侧固定导航 + 右侧内容区)
- **容器宽度**:流式
- **关键尺寸**
- 侧边菜单宽度:展开 `240px` / 折叠 `64px`
- 指标卡片4 列栅格lg=6
- 图表区16/8 栅格分栏
- 列表区16/8 栅格分栏
### 3.2 响应式适配
- **桌面端≥1200px**:双栏布局,图表与列表保持 16/8 分栏
- **平板端768-1199px**:指标卡片两列排列,图表与列表纵向堆叠
- **移动端(<768px**:所有模块单列堆叠,表格启用横向滚动
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
- [x] 主题设计系统:`/src/themes/antd-new/`DESIGN.md + designToken.json + globals.css
### 4.2 自定义设计要点
**自定义色彩**(如有):
-
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
-
### 4.3 组件状态
- **默认态default**:卡片与图表为白底,表格与列表为普通展示
- **悬停态hover**:按钮、列表项、表格行显示轻微高亮
- **选中态selected**:侧边菜单与表格选中行高亮
- **加载态loading**:可用于数据刷新时的按钮或表格加载
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
### 5.2 动作列表actionList
### 5.3 变量列表varList
### 5.4 配置项列表configList
### 5.5 数据项列表dataList
**数据结构**
&nbsp;

View File

@@ -0,0 +1,68 @@
/**
* @name Antd 电商后台样式
* 页面布局与组件样式
*/
.ecommerce-dashboard {
padding: 24px;
background: #f5f7fa; /* Slightly lighter/cooler gray */
min-height: 100vh;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
/* Base Card Style - Minimalist Box Logic */
.ant-card {
border: 1px solid #eef0f5 !important; /* Subtle border */
border-radius: 12px !important; /* Slightly larger radius */
box-shadow: none !important; /* Remove default shadow */
transition: all 0.2s ease-in-out;
}
.ant-card:hover {
border-color: #dbe0e8 !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04) !important; /* Very subtle lift */
transform: translateY(-1px);
}
.metric-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f5f5f5;
font-size: 12px;
}
.ant-statistic-title {
font-size: 14px;
color: #64748b !important; /* Cool gray */
margin-bottom: 8px !important;
}
.ant-statistic-content-value {
font-weight: 600 !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.ant-table-wrapper {
background: white;
border-radius: 12px;
}
/* Chart Containers */
.chart-container {
height: 320px;
width: 100%;
}
/* Status Tags - refined look */
.ant-tag {
border-radius: 4px;
border: none;
padding: 0 8px;
font-weight: 500;
}

View File

@@ -0,0 +1,619 @@
/**
* @name 健身 App 首页
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
*/
import './style.css';
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
import { Activity, Flame, Timer, Zap } from 'lucide-react';
import type {
Action,
AxureHandle,
AxureProps,
ConfigItem,
DataDesc,
EventItem,
KeyDesc,
} from '../../common/axure-types';
const EVENT_LIST: EventItem[] = [
{ name: 'onCourseClick', desc: '点击课程卡片时触发' },
{ name: 'onStartWorkout', desc: '点击开始训练时触发' },
{ name: 'onTabChange', desc: '切换底部标签栏时触发' },
];
const ACTION_LIST: Action[] = [
{ name: 'refreshData', desc: '刷新首页数据' },
{ name: 'updateProgress', desc: '更新今日目标进度,参数:{ progress: number }' },
{ name: 'switchTab', desc: '切换标签页,参数:{ index: number }' },
];
const VAR_LIST: KeyDesc[] = [
{ name: 'currentTab', desc: '当前选中的标签页索引' },
{ name: 'todayProgress', desc: '今日目标完成进度(0-100)' },
];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'input', attributeId: 'userName', displayName: '用户名', info: '显示的用户名', initialValue: 'Alex' },
{ type: 'colorPicker', attributeId: 'accentColor', displayName: '强调色', info: 'App 的主要强调色', initialValue: '#a6ff00' },
{ type: 'inputNumber', attributeId: 'dailyGoal', displayName: '每日目标(kcal)', info: '每日卡路里消耗目标', initialValue: 500 },
];
const DATA_LIST: DataDesc[] = [
{
name: 'courses',
desc: '推荐课程列表',
keys: [
{ name: 'id', desc: '课程ID' },
{ name: 'title', desc: '课程标题' },
{ name: 'duration', desc: '时长(分钟)' },
{ name: 'level', desc: '难度等级' },
{ name: 'image', desc: '封面图片URL' },
{ name: 'category', desc: '分类标签' },
],
},
];
function isMobileEditorSafeMode(): boolean {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return false;
}
try {
const search = new URLSearchParams(window.location.search);
const editorMode = String(search.get('editor') || '').toLowerCase();
const hasTouch = ('ontouchstart' in window) || ((navigator.maxTouchPoints || 0) > 0);
return editorMode.indexOf('webeditor') >= 0 && hasTouch && window.innerWidth <= 768;
} catch (error) {
console.warn('检测移动编辑安全模式失败:', error);
return false;
}
}
function parseActionParams(params?: string): Record<string, unknown> | null {
if (!params) {
return null;
}
try {
return JSON.parse(params) as Record<string, unknown>;
} catch {
return null;
}
}
function getViewportHeight(): string | undefined {
if (typeof window === 'undefined') {
return undefined;
}
const nextHeight = window.visualViewport?.height || window.innerHeight;
return nextHeight > 0 ? `${Math.round(nextHeight)}px` : undefined;
}
type TabDefinition = {
label: string;
icon: React.ReactNode;
};
const TABS: TabDefinition[] = [
{
label: '首页',
icon: <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>,
},
{
label: '训练',
icon: <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M6 6h12"></path><path d="M4 10h16"></path><path d="M6 14h12"></path><path d="M9 18h6"></path></svg>,
},
{
label: '统计',
icon: <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>,
},
{
label: '我的',
icon: <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>,
},
];
const SUB_PAGE_PATH_BY_TAB: Record<number, string> = {
0: '',
1: 'workout',
2: 'analytics',
3: 'profile',
};
function getCurrentSubPagePath(): string {
if (typeof window === 'undefined') {
return '';
}
const pathParts = window.location.pathname.split('/').filter(Boolean);
if (pathParts[0] !== 'prototypes' || pathParts.length < 2) {
return '';
}
return pathParts.slice(2).join('/');
}
function resolveTabIndexFromLocation(): number {
const subPagePath = getCurrentSubPagePath();
switch (subPagePath) {
case 'workout':
return 1;
case 'analytics':
return 2;
case 'profile':
return 3;
default:
return 0;
}
}
function syncLocationForTab(index: number) {
if (typeof window === 'undefined') {
return;
}
const basePathParts = window.location.pathname.split('/').filter(Boolean).slice(0, 2);
if (basePathParts.length < 2) {
return;
}
const subPagePath = SUB_PAGE_PATH_BY_TAB[index] || '';
const nextPath = `/${basePathParts.join('/')}${subPagePath ? `/${subPagePath}` : ''}`;
const nextUrl = `${nextPath}${window.location.search}${window.location.hash}`;
if (`${window.location.pathname}${window.location.search}${window.location.hash}` !== nextUrl) {
window.history.replaceState(window.history.state, '', nextUrl);
}
}
function SummaryView({
accentColor,
dailyGoal,
todayProgress,
userName,
courses,
onCourseClick,
onStartWorkout,
}: {
accentColor: string;
dailyGoal: number;
todayProgress: number;
userName: string;
courses: any[];
onCourseClick: (course: any) => void;
onStartWorkout: () => void;
}) {
const radius = 25;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (todayProgress / 100) * circumference;
return (
<div className="demo-app-home-scroll-content">
<div className="demo-app-home-header">
<h1 className="demo-app-home-greeting">
Hi, <span style={{ color: accentColor }}>{userName}</span>
<div style={{ fontSize: 14, color: '#888', fontWeight: 'normal', marginTop: 4 }}>
</div>
</h1>
<div className="demo-app-home-avatar">
<img src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=80" alt="avatar" />
</div>
</div>
<div className="demo-app-home-stats">
<div className="demo-app-home-stat-card">
<div className="demo-app-home-stat-icon">
<Flame size={20} />
</div>
<div className="demo-app-home-stat-value">328</div>
<div className="demo-app-home-stat-label"></div>
</div>
<div className="demo-app-home-stat-card">
<div className="demo-app-home-stat-icon">
<Timer size={20} />
</div>
<div className="demo-app-home-stat-value">56</div>
<div className="demo-app-home-stat-label"></div>
</div>
<div className="demo-app-home-stat-card">
<div className="demo-app-home-stat-icon">
<Zap size={20} />
</div>
<div className="demo-app-home-stat-value">3</div>
<div className="demo-app-home-stat-label"></div>
</div>
</div>
<div className="demo-app-home-section">
<div className="demo-app-home-section-header">
<h2 className="demo-app-home-section-title"></h2>
<span className="demo-app-home-section-more"></span>
</div>
<div className="demo-app-home-plan-card">
<div className="demo-app-home-plan-progress">
<svg>
<circle className="demo-app-home-plan-progress-bg" cx="30" cy="30" r={radius} />
<circle
className="demo-app-home-plan-progress-bar"
cx="30"
cy="30"
r={radius}
style={{ strokeDashoffset, stroke: accentColor }}
/>
</svg>
<div className="demo-app-home-plan-icon">
<Activity size={24} />
</div>
</div>
<div className="demo-app-home-plan-info">
<div className="demo-app-home-plan-title"></div>
<div className="demo-app-home-plan-subtitle">
{Math.round(dailyGoal * todayProgress / 100)} / {dailyGoal} kcal
</div>
</div>
<button
className="demo-app-home-plan-action"
style={{ backgroundColor: accentColor }}
onClick={onStartWorkout}
>
</button>
</div>
</div>
<div className="demo-app-home-section">
<div className="demo-app-home-section-header">
<h2 className="demo-app-home-section-title"></h2>
<span className="demo-app-home-section-more"></span>
</div>
<div className="demo-app-home-course-list">
{courses.map((course: any) => (
<div
key={course.id}
className="demo-app-home-course-card"
onClick={() => onCourseClick(course)}
>
<img src={course.image} className="demo-app-home-course-bg" alt={course.title} />
<div className="demo-app-home-course-overlay">
<div className="demo-app-home-course-tag" style={{ backgroundColor: accentColor }}>{course.category}</div>
<div className="demo-app-home-course-title">{course.title}</div>
<div className="demo-app-home-course-meta">
<span>{course.duration} </span>
<span></span>
<span>{course.level}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
function AnalyticsView({ accentColor, todayProgress }: { accentColor: string; todayProgress: number }) {
return (
<div className="demo-app-home-scroll-content demo-app-home-scroll-content--tab">
<div className="demo-app-home-section">
<div className="demo-app-home-section-header">
<h2 className="demo-app-home-section-title"></h2>
<span className="demo-app-home-section-more"></span>
</div>
<div style={{
display: 'grid',
gap: 14,
color: '#f5f7fb',
}}>
<div style={{
borderRadius: 24,
padding: 20,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.08)',
}}>
<div style={{ fontSize: 14, color: '#9ca3af', marginBottom: 8 }}></div>
<div style={{ fontSize: 36, fontWeight: 700, color: accentColor }}>{todayProgress}%</div>
<div style={{ marginTop: 10, fontSize: 13, color: '#d1d5db' }}>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 }}>
{[
['训练完成', '12 次'],
['平均心率', '132 bpm'],
['最佳记录', '28 分钟 HIIT'],
['恢复指数', 'A'],
].map(([label, value]) => (
<div
key={label}
style={{
borderRadius: 20,
padding: 16,
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.08)',
}}
>
<div style={{ fontSize: 12, color: '#9ca3af', marginBottom: 6 }}>{label}</div>
<div style={{ fontSize: 18, fontWeight: 600 }}>{value}</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
function WorkoutView({ accentColor }: { accentColor: string }) {
const plans = [
{ title: '燃脂冲刺', duration: '28 分钟', highlight: '预计消耗 360 kcal' },
{ title: '核心塑形', duration: '18 分钟', highlight: '强化腹背与稳定性' },
{ title: '拉伸恢复', duration: '12 分钟', highlight: '训练后放松与恢复' },
];
return (
<div className="demo-app-home-scroll-content demo-app-home-scroll-content--tab">
<div className="demo-app-home-section">
<div className="demo-app-home-section-header">
<h2 className="demo-app-home-section-title"></h2>
<span className="demo-app-home-section-more"></span>
</div>
<div style={{ display: 'grid', gap: 14 }}>
{plans.map((plan, index) => (
<div
key={plan.title}
style={{
borderRadius: 24,
padding: 18,
background: index === 0 ? 'rgba(166,255,0,0.12)' : 'rgba(255,255,255,0.05)',
border: index === 0 ? `1px solid ${accentColor}` : '1px solid rgba(255,255,255,0.08)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
<div>
<div style={{ fontSize: 18, fontWeight: 600 }}>{plan.title}</div>
<div style={{ marginTop: 6, fontSize: 13, color: '#9ca3af' }}>{plan.highlight}</div>
</div>
<div style={{ fontSize: 13, color: '#d1d5db' }}>{plan.duration}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
function ProfileView({ accentColor, userName }: { accentColor: string; userName: string }) {
const profileCards = [
{ label: '会员等级', value: 'Pro 年度会员' },
{ label: '连续训练', value: '6 天' },
{ label: '恢复指数', value: 'A' },
{ label: '设备连接', value: '2 台' },
];
return (
<div className="demo-app-home-scroll-content demo-app-home-scroll-content--tab">
<div className="demo-app-home-section">
<div className="demo-app-home-section-header">
<h2 className="demo-app-home-section-title"></h2>
<span className="demo-app-home-section-more">{userName}</span>
</div>
<div style={{
borderRadius: 24,
padding: 20,
marginBottom: 14,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.08)',
}}>
<div style={{ fontSize: 14, color: '#9ca3af', marginBottom: 8 }}></div>
<div style={{ fontSize: 28, fontWeight: 700, color: accentColor }}>{userName}</div>
<div style={{ marginTop: 8, fontSize: 13, color: '#d1d5db' }}>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 }}>
{profileCards.map((card) => (
<div
key={card.label}
style={{
borderRadius: 20,
padding: 16,
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.08)',
}}
>
<div style={{ fontSize: 12, color: '#9ca3af', marginBottom: 6 }}>{card.label}</div>
<div style={{ fontSize: 18, fontWeight: 600 }}>{card.value}</div>
</div>
))}
</div>
</div>
</div>
);
}
const Component = forwardRef<AxureHandle, AxureProps>(function FitnessHome(innerProps, ref) {
const dataSource = innerProps?.data || {};
const configSource = innerProps?.config || {};
const onEventHandler = typeof innerProps?.onEvent === 'function' ? innerProps.onEvent : () => undefined;
const userName = typeof configSource.userName === 'string' && configSource.userName ? configSource.userName : 'Alex';
const accentColor = typeof configSource.accentColor === 'string' && configSource.accentColor ? configSource.accentColor : '#a6ff00';
const dailyGoal = typeof configSource.dailyGoal === 'number' ? configSource.dailyGoal : 500;
const mobileEditorSafeMode = isMobileEditorSafeMode();
const defaultCourses = [
{ id: 1, title: 'HIIT 高强度燃脂', duration: 20, level: 'K3', category: '减脂', image: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80' },
{ id: 2, title: '腹肌核心撕裂者', duration: 15, level: 'K2', category: '塑形', image: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80' },
{ id: 3, title: '全身拉伸放松', duration: 10, level: 'K1', category: '恢复', image: 'https://images.unsplash.com/photo-1518611012118-696072aa579a?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80' },
];
const courses = Array.isArray(dataSource.courses) ? dataSource.courses : defaultCourses;
const [currentTab, setCurrentTab] = useState<number>(() => resolveTabIndexFromLocation());
const [todayProgress, setTodayProgress] = useState<number>(65);
const [viewportHeight, setViewportHeight] = useState<string | undefined>(() => getViewportHeight());
const emitEvent = useCallback((eventName: string, payload?: Record<string, unknown>) => {
try {
onEventHandler(eventName, payload ? JSON.stringify(payload) : undefined);
} catch (error) {
console.warn('事件触发失败:', error);
}
}, [onEventHandler]);
const handleTabChange = useCallback((index: number) => {
syncLocationForTab(index);
setCurrentTab(index);
emitEvent('onTabChange', { index });
}, [emitEvent]);
const handleCourseClick = useCallback((course: any) => {
emitEvent('onCourseClick', { courseId: course?.id, title: course?.title });
}, [emitEvent]);
const handleStartWorkout = useCallback(() => {
syncLocationForTab(1);
setCurrentTab(1);
emitEvent('onStartWorkout', { source: 'summary' });
}, [emitEvent]);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const syncViewportHeight = () => {
setViewportHeight(getViewportHeight());
};
syncViewportHeight();
window.addEventListener('resize', syncViewportHeight);
window.addEventListener('orientationchange', syncViewportHeight);
window.visualViewport?.addEventListener('resize', syncViewportHeight);
return () => {
window.removeEventListener('resize', syncViewportHeight);
window.removeEventListener('orientationchange', syncViewportHeight);
window.visualViewport?.removeEventListener('resize', syncViewportHeight);
};
}, []);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const handleLocationChange = () => {
setCurrentTab(resolveTabIndexFromLocation());
};
handleLocationChange();
window.addEventListener('popstate', handleLocationChange);
return () => {
window.removeEventListener('popstate', handleLocationChange);
};
}, []);
useImperativeHandle(ref, () => ({
getVar(name: string) {
const vars: Record<string, unknown> = {
currentTab,
todayProgress,
};
return vars[name];
},
fireAction(name: string, params?: string) {
const payload = parseActionParams(params);
switch (name) {
case 'refreshData':
return;
case 'updateProgress': {
const nextProgress = typeof payload?.progress === 'number' ? payload.progress : NaN;
if (Number.isFinite(nextProgress)) {
setTodayProgress(Math.max(0, Math.min(100, Number(nextProgress))));
}
return;
}
case 'switchTab': {
const nextTab = typeof payload?.index === 'number' ? payload.index : NaN;
if (Number.isFinite(nextTab) && nextTab >= 0 && nextTab < TABS.length) {
syncLocationForTab(Number(nextTab));
setCurrentTab(Number(nextTab));
}
return;
}
default:
console.warn('未知的动作:', name);
}
},
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST,
}), [currentTab, todayProgress]);
let content: React.ReactNode;
if (currentTab === 1) {
content = <WorkoutView accentColor={accentColor} />;
} else if (currentTab === 2) {
content = <AnalyticsView accentColor={accentColor} todayProgress={todayProgress} />;
} else if (currentTab === 3) {
content = <ProfileView accentColor={accentColor} userName={userName} />;
} else {
content = (
<SummaryView
accentColor={accentColor}
dailyGoal={dailyGoal}
todayProgress={todayProgress}
userName={userName}
courses={courses}
onCourseClick={handleCourseClick}
onStartWorkout={handleStartWorkout}
/>
);
}
return (
<div
className={'demo-app-home-container ' + (mobileEditorSafeMode ? 'demo-app-home-container--editor-mobile-safe' : '')}
style={{
'--accent-color': accentColor,
'--app-home-viewport-height': viewportHeight,
} as React.CSSProperties}
>
{content}
<div className="demo-app-home-fab" style={{ backgroundColor: accentColor }}>
+
</div>
<div className="demo-app-home-tab-bar">
{TABS.map((tab, index) => (
<div
key={tab.label}
className={'demo-app-home-tab-item ' + (currentTab === index ? 'active' : '')}
style={{ color: currentTab === index ? accentColor : undefined }}
onClick={() => handleTabChange(index)}
>
<div className="demo-app-home-tab-icon">{tab.icon}</div>
<div className="demo-app-home-tab-label">{tab.label}</div>
</div>
))}
</div>
</div>
);
});
export default Component;

View File

@@ -0,0 +1,16 @@
{
"pages": [
{
"name": "本周统计",
"path": "analytics"
},
{
"name": "训练计划",
"path": "workout"
},
{
"name": "个人中心",
"path": "profile"
}
]
}

View File

@@ -0,0 +1,281 @@
# 健身 App 首页 - 产品需求文档PRD
## 1. 产品概述
### 1.1 产品定位
健身 App 首页是一款面向健身爱好者的移动端应用首页,旨在为用户提供直观的运动数据展示、目标管理和课程推荐功能。通过现代化的暗黑主题设计和流畅的交互体验,激励用户坚持运动,养成健康生活习惯。
### 1.2 目标用户
- **主要用户**18-45 岁的健身爱好者
- **使用场景**:日常健身打卡、查看运动数据、选择训练课程
- **用户特征**
- 关注个人健康和体型管理
- 习惯使用移动设备记录运动数据
- 喜欢视觉化的数据呈现方式
- 需要专业的训练指导
### 1.3 产品目标
- 提升用户运动积极性,增加日活跃度
- 通过数据可视化帮助用户了解运动进展
- 推荐个性化课程,提高课程转化率
- 打造沉浸式的运动体验,增强用户粘性
## 2. 业务需求
### 2.1 核心功能
#### 功能 1运动数据统计
**需求描述**
用户打开首页后,能够一目了然地看到今日的运动数据统计,包括卡路里消耗、运动时长和连续运动天数。
**业务价值**
- 让用户快速了解今日运动成果
- 通过连续天数激励用户保持运动习惯
- 数据可视化增强用户成就感
**功能要求**
- 显示三个核心指标卡路里消耗kcal、运动分钟数、连续运动天数
- 每个指标使用独立卡片展示,配有图标
- 数据实时更新,反映最新运动状态
#### 功能 2每日目标管理
**需求描述**
用户可以设定每日卡路里消耗目标,并通过环形进度条直观地查看目标完成进度。
**业务价值**
- 帮助用户设定明确的运动目标
- 通过可视化进度激励用户完成目标
- 提供快速启动训练的入口
**功能要求**
- 显示环形进度条,展示目标完成百分比
- 显示已完成和总目标的具体数值328/500 kcal
- 提供"开始训练"按钮,快速启动训练流程
- 支持自定义每日目标值
#### 功能 3课程推荐
**需求描述**
根据用户的运动偏好和历史数据,推荐适合的训练课程,用户可以浏览并选择感兴趣的课程。
**业务价值**
- 提高课程曝光率和转化率
- 帮助用户发现适合的训练内容
- 增加用户在 App 内的停留时间
**功能要求**
- 横向滚动展示课程列表,节省垂直空间
- 每个课程卡片显示:封面图、课程标题、时长、难度等级、分类标签
- 支持点击课程卡片查看详情
- 课程列表支持动态加载
#### 功能 4底部导航
**需求描述**
提供固定在底部的导航栏,用户可以快速切换到不同的功能模块。
**业务价值**
- 提供清晰的信息架构
- 降低用户操作成本
- 符合移动端应用的交互习惯
**功能要求**
- 固定在页面底部,始终可见
- 包含 4 个导航项:首页、计划、统计、我的
- 当前选中的导航项高亮显示
- 点击导航项切换到对应页面
### 2.2 用户场景
#### 场景 1晨间查看运动数据
**角色**:健身爱好者小李
**场景**
1. 早上起床后,小李打开健身 App
2. 首页显示昨日运动数据:消耗 450 kcal运动 60 分钟,连续 5 天
3. 小李看到连续天数,感到很有成就感
4. 查看今日目标进度为 0%,决定开始今天的训练
**预期结果**
- 用户能够快速了解运动进展
- 连续天数激励用户保持运动习惯
#### 场景 2选择训练课程
**角色**:健身新手小王
**场景**
1. 小王想要开始训练,但不知道做什么
2. 在首页看到"为你推荐"课程列表
3. 横向滑动浏览不同课程HIIT 燃脂、腹肌训练、拉伸放松
4. 看到"HIIT 高强度燃脂"课程,时长 20 分钟,难度 K3
5. 点击课程卡片,进入课程详情页
**预期结果**
- 用户能够轻松发现适合的课程
- 课程信息清晰,帮助用户做出选择
#### 场景 3完成每日目标
**角色**:上班族小张
**场景**
1. 下班后,小张打开 App 查看今日进度
2. 看到环形进度条显示 85%425/500 kcal
3. 距离目标还差 75 kcal
4. 点击"开始训练"按钮,选择一个 10 分钟的快速训练
5. 完成训练后,进度条更新为 100%
**预期结果**
- 用户能够清晰了解距离目标的差距
- 快速启动训练,完成每日目标
### 2.3 业务规则
#### 规则 1数据统计规则
- 卡路里消耗:累计当日所有训练的卡路里消耗
- 运动时长:累计当日所有训练的有效运动时间
- 连续天数:从最近一次运动日开始,连续运动的天数(中断则重置为 0
#### 规则 2目标进度计算
- 进度百分比 = (已完成卡路里 / 目标卡路里) × 100%
- 进度超过 100% 时,显示为 100%
- 目标值可由用户自定义,默认为 500 kcal
#### 规则 3课程推荐规则
- 根据用户历史训练数据推荐相似课程
- 优先推荐用户未完成的课程
- 课程列表至少展示 3 个课程
- 课程按推荐优先级排序
#### 规则 4导航切换规则
- 当前页面对应的导航项高亮显示
- 点击导航项时,切换到对应页面
- 首页为默认选中的导航项
## 3. 功能优先级
| 优先级 | 功能 | 理由 |
|--------|------|------|
| P0必须有| 运动数据统计 | 核心功能,用户最关注的信息 |
| P0必须有| 每日目标管理 | 核心功能,激励用户的关键 |
| P0必须有| 底部导航 | 基础交互,信息架构的基础 |
| P1应该有| 课程推荐 | 重要功能,提高课程转化率 |
| P2可以有| 浮动按钮 | 辅助功能,提供快捷操作入口 |
## 4. 非功能需求
### 4.1 性能要求
- 页面首次加载时间 < 2 秒
- 页面切换动画流畅,无卡顿
- 课程列表横向滚动流畅
### 4.2 兼容性要求
- 支持 iOS 12+ 和 Android 8+
- 适配主流移动设备屏幕尺寸375px - 428px 宽度)
- 支持深色模式
### 4.3 可用性要求
- 界面简洁直观,新用户无需引导即可使用
- 关键操作(如开始训练)触摸区域 ≥ 44×44 pt
- 色彩对比度符合 WCAG AA 标准
### 4.4 可维护性要求
- 主题色可配置,支持品牌定制
- 数据源可替换,支持不同的后端接口
- 组件化设计,便于复用和扩展
## 5. 设计要求
### 5.1 视觉风格
- **主题**:暗黑模式,营造专业运动氛围
- **色彩**
- 背景色:深灰黑色(#121212
- 强调色:霓虹绿(#a6ff00),可配置
- 文字色:白色(#ffffff)和灰色(#888888
- **字体**:系统默认字体,确保跨平台一致性
- **圆角**:统一使用 16px 圆角,营造柔和感
### 5.2 布局规范
- **间距**:统一使用 8px 栅格系统8、16、24px
- **卡片**:使用半透明背景和阴影,营造层次感
- **列表**:横向滚动列表隐藏滚动条,提供流畅体验
- **底部导航**:固定在底部,高度 64px含安全区
### 5.3 交互规范
- **点击反馈**:所有可点击元素提供视觉反馈(缩放、颜色变化)
- **加载状态**:数据加载时显示骨架屏或加载动画
- **错误提示**:网络错误或数据异常时,显示友好的错误提示
- **动画**:页面切换和状态变化使用流畅的过渡动画
## 6. 数据需求
### 6.1 输入数据
- **用户信息**:用户名、头像 URL
- **运动数据**:今日卡路里消耗、运动时长、连续天数
- **目标数据**:每日目标值、当前完成进度
- **课程数据**课程列表ID、标题、时长、难度、分类、封面图
### 6.2 输出数据
- **事件数据**:用户操作事件(点击课程、开始训练、切换导航)
- **状态数据**:当前选中的导航项、目标完成进度
## 7. 成功指标
### 7.1 用户指标
- 日活跃用户数DAU提升 20%
- 用户平均停留时长增加 30%
- 每日目标完成率达到 60%
### 7.2 业务指标
- 课程点击率CTR达到 15%
- 课程转化率提升 10%
- 用户连续运动天数平均值达到 7 天
### 7.3 体验指标
- 页面加载时间 < 2 秒
- 用户满意度评分 ≥ 4.5/5
- 崩溃率 < 0.1%
## 8. 风险与限制
### 8.1 技术风险
- 数据同步延迟可能导致进度显示不准确
- 图片加载失败影响课程展示效果
- 不同设备的性能差异影响动画流畅度
### 8.2 业务风险
- 课程推荐算法不准确,影响用户体验
- 目标设定过高或过低,影响用户积极性
- 竞品功能更新快,需要持续迭代
### 8.3 限制条件
- 本版本仅支持移动端,不支持桌面端
- 课程数据依赖后端接口,离线状态下功能受限
- 暂不支持社交功能(如好友排行榜)
## 9. 后续规划
### 9.1 短期规划1-3 个月)
- 增加运动数据趋势图表
- 支持自定义目标类型(步数、运动时长等)
- 优化课程推荐算法
### 9.2 中期规划3-6 个月)
- 增加社交功能(好友排行榜、运动打卡分享)
- 支持训练计划管理
- 增加运动提醒和激励机制
### 9.3 长期规划6-12 个月)
- 接入智能穿戴设备数据
- 提供 AI 私教功能
- 支持多语言和国际化
## 10. 附录
### 10.1 术语表
- **kcal**:千卡,能量单位,用于衡量卡路里消耗
- **HIIT**高强度间歇训练High-Intensity Interval Training
- **K1/K2/K3**课程难度等级K1 为入门K3 为高级
- **DAU**日活跃用户数Daily Active Users
- **CTR**点击率Click-Through Rate
### 10.2 参考资料
- 竞品分析Keep、Nike Training Club、Fitbit
- 设计参考Material Design、iOS Human Interface Guidelines
- 用户调研报告《2024 年健身 App 用户行为研究》

View File

@@ -0,0 +1,194 @@
# 健身 App 首页
## 📋 业务与功能
### 1.1 核心目标
页面聚焦用户训练入口与每日目标反馈,帮助用户快速进入训练状态。
页面强调沉浸式视觉与高频浏览体验,适配移动端单列布局。
### 1.2 功能清单
- **页面头部**:头像、问候语、用户名、通知图标
- **运动数据统计**:卡路里消耗、运动时长、连续天数等统计卡片
- **每日目标区域**:环形进度条、目标说明、开始训练按钮
- **课程推荐**:横向滚动课程卡片列表
- **底部导航栏**:首页、训练、统计、我的四个 Tab
### 1.3 交互要点
- 点击“开始训练”:触发 `onStartWorkout` 事件
- 点击课程卡片:触发 `onCourseClick` 事件,返回课程对象
- 点击底部导航项:触发 `onTabChange` 事件,更新当前 Tab
- 统计卡片按压时显示缩放反馈
- 通过动作 `updateProgress` 动态更新进度环与百分比
---
## 📊 内容规划
### 2.1 信息架构
```
健身 App 首页
├── 头部
│ ├── 头像与问候语
│ └── 通知入口
├── 运动数据统计(横向滑动)
├── 每日目标
│ ├── 环形进度
│ ├── 目标说明
│ └── 开始训练按钮
├── 课程推荐(横向滑动)
└── 底部导航
```
### 2.1.1 子页面路由映射
- 原型主入口 `/prototypes/ref-app-home` 对应首页视图
- 子页面 `训练计划` 对应已有训练视图,路由 path 为 `workout`
- 子页面 `本周统计` 对应已有统计视图,路由 path 为 `analytics`
- 子页面 `个人中心` 对应已有个人中心视图,路由 path 为 `profile`
- `pages.json` 仅登记以上三个二级页面,保持与当前原型内部已实现的视图一致
### 2.2 数据来源
- **数据类型**:课程列表(`courses`
- **数据源**用户提供props data/ 内置示例
- **关键字段**
- `id`: 课程 ID
- `title`: 课程标题
- `duration`: 时长(分钟)
- `level`: 难度等级
- `category`: 分类标签
- `image`: 封面图片 URL
- **数据类型**:进度与统计数据
- **数据源**:内置示例(可扩展为外部数据源)
- **关键字段**:今日进度、卡路里、时长、连续天数
### 2.3 内容示例
**课程示例**
- HIIT 高强度燃脂 / 20 分钟 / K3 / 减脂
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:单栏
- **容器宽度**:固定(最大宽度 420px居中
- **容器高度**:一屏高度(`100vh/100dvh`),内容区独立滚动
- **关键尺寸**
- 顶部区域:固定内边距与粘性定位
- 统计卡片:横向滚动,卡片最小宽度 100px
- 主体滚动区:允许纵向滚动,但隐藏滚动条视觉
- 底部导航:容器底部常驻,预留安全区,避免 `fixed` 在缩放容器中的偏移
### 3.2 响应式适配
- **桌面端≥1200px**:保持移动端宽度居中展示
- **平板端768-1199px**:保持单列,内容居中
- **移动端(<768px**:全宽展示,内容区滚动,底部导航常驻
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
### 4.2 自定义设计要点
**自定义色彩**(如有):
- `accentColor`:用于高亮、按钮与进度条(默认 `#a6ff00`
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
- 深色背景与霓虹强调色对比
### 4.3 组件状态
- **默认态default**:深色背景、卡片悬浮层次
- **悬停/按压态hover/active**:卡片轻微缩放、按钮高亮
- **选中态selected**:底部导航高亮
- **加载态loading**:可用于数据刷新与进度更新
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
| 事件名称 | Payload 类型 | 触发时机 | 说明 |
|---------|-------------|---------|------|
| `onCourseClick` | `Course` | 点击课程卡片时 | 返回课程对象 |
| `onStartWorkout` | `void` | 点击开始训练按钮时 | 无参数 |
| `onTabChange` | `{ index: number }` | 点击底部导航项时 | 返回选中的 Tab 索引 |
### 5.2 动作列表actionList
| 动作名称 | Params 类型 | 参数说明 | 功能描述 |
|---------|------------|---------|---------|
| `refreshData` | `void` | 无 | 刷新首页数据 |
| `updateProgress` | `{ progress: number }` | 进度值0-100 | 更新今日目标进度 |
### 5.3 变量列表varList
| 变量名称 | 类型 | 默认值 | 说明 |
|---------|-----|-------|------|
| `currentTab` | `number` | `0` | 当前选中 Tab 索引0-3 |
| `todayProgress` | `number` | `65` | 今日目标完成进度0-100 |
### 5.4 配置项列表configList
| 配置项名称 | 类型 | 默认值 | 说明 |
|----------|-----|-------|------|
| `userName` | `string` | `Alex` | 顶部显示的用户名 |
| `accentColor` | `string` | `#a6ff00` | App 主要强调色 |
| `dailyGoal` | `number` | `500` | 每日目标kcal |
### 5.5 数据项列表dataList
**数据结构**
```typescript
{
courses: Array<{
id: number; // 课程 ID
title: string; // 标题
duration: number; // 时长(分钟)
level: string; // 难度等级
category: string; // 分类
image: string; // 封面图
}>;
}
```
---
## 🧩 Pencil 设计稿
### 6.1 设计稿文件
- 文件路径:`/src/prototypes/ref-app-home/fitness-app-home.pen`
- 设计范围移动端首页单屏390 × 844
### 6.2 页面结构映射
- 状态栏62px时间与系统状态占位
- 顶部问候区:用户名、激励文案、头像
- 统计卡片区:卡路里、运动分钟(默认 56 分钟)、连续天数
- 今日计划区:环形进度、完成值、开始训练按钮
- 课程推荐区:横向滚动课程卡片(首屏可见 1.5 张)
- 底部导航4 个 Tab首页高亮
### 6.3 视觉要点落地
- 主背景:深色 `#121212`
- 强调色:霓虹绿 `#A6FF00`
- 卡片圆角16px/20px
- 导航与按钮:高亮态使用强调色,非高亮态使用中性灰

View File

@@ -0,0 +1,403 @@
/**
* @name 健身 App 首页样式
* 页面视觉与布局样式
*/
.demo-app-home-container {
background-color: #121212;
color: #ffffff;
height: var(--app-home-viewport-height, 100vh);
max-height: var(--app-home-viewport-height, 100vh);
min-height: var(--app-home-viewport-height, 100vh);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
position: relative;
overflow: hidden;
max-width: 420px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
.demo-app-home-scroll-content {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: calc(112px + env(safe-area-inset-bottom));
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
}
.demo-app-home-scroll-content--tab {
padding-top: 24px;
}
.demo-app-home-scroll-content::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.demo-app-home-container--editor-mobile-safe {
height: auto;
min-height: 100dvh;
max-height: none;
overflow: visible;
}
.demo-app-home-scroll-content--editor-mobile-safe {
min-height: auto;
overflow-y: visible;
overflow-x: visible;
-webkit-overflow-scrolling: auto;
padding-bottom: 24px;
}
.demo-app-home-scroll-content--editor-mobile-safe.demo-app-home-scroll-content--tab {
padding-top: 24px;
}
.demo-app-home-container--editor-mobile-safe .demo-app-home-header {
position: relative;
top: auto;
}
/* 头部区域 */
.demo-app-home-header {
padding: 24px 20px;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(180deg, rgba(18, 18, 18, 0.9) 0%, rgba(18, 18, 18, 0) 100%);
position: sticky;
top: 0;
z-index: 10;
}
.demo-app-home-greeting {
font-size: 24px;
font-weight: 700;
margin: 0;
}
.demo-app-home-greeting span {
color: #a6ff00;
/* 霓虹绿 */
}
.demo-app-home-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #333;
border: 2px solid #a6ff00;
overflow: hidden;
}
.demo-app-home-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 统计卡片区域 */
.demo-app-home-stats {
display: flex;
gap: 12px;
padding: 0 20px;
margin-bottom: 24px;
overflow-x: auto;
scrollbar-width: none;
/* Firefox */
}
.demo-app-home-stats::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
.demo-app-home-stat-card {
background-color: #1e1e1e;
border-radius: 16px;
padding: 16px;
min-width: 100px;
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: transform 0.2s;
}
.demo-app-home-stat-card:active {
transform: scale(0.98);
}
.demo-app-home-stat-icon {
font-size: 20px;
margin-bottom: 8px;
color: #a6ff00;
}
.demo-app-home-stat-value {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.demo-app-home-stat-label {
font-size: 12px;
color: #888;
}
/* 今日计划 */
.demo-app-home-section {
padding: 0 20px;
margin-bottom: 24px;
}
.demo-app-home-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.demo-app-home-section-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.demo-app-home-section-more {
font-size: 14px;
color: #a6ff00;
cursor: pointer;
}
.demo-app-home-plan-card {
background: linear-gradient(135deg, #2c2c2c 0%, #1e1e1e 100%);
border-radius: 20px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
position: relative;
overflow: hidden;
}
.demo-app-home-plan-card::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(166, 255, 0, 0.05));
pointer-events: none;
}
.demo-app-home-plan-progress {
position: relative;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.demo-app-home-plan-progress svg {
transform: rotate(-90deg);
width: 60px;
height: 60px;
}
.demo-app-home-plan-progress circle {
fill: none;
stroke-width: 6;
stroke-linecap: round;
}
.demo-app-home-plan-progress-bg {
stroke: #333;
}
.demo-app-home-plan-progress-bar {
stroke: #a6ff00;
stroke-dasharray: 157;
/* 2 * PI * 25 */
transition: stroke-dashoffset 0.5s ease;
}
.demo-app-home-plan-icon {
position: absolute;
font-size: 20px;
}
.demo-app-home-plan-info {
flex: 1;
}
.demo-app-home-plan-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.demo-app-home-plan-subtitle {
font-size: 13px;
color: #888;
}
.demo-app-home-plan-action {
background-color: #a6ff00;
color: #000;
border: none;
width: 36px;
height: 36px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
font-weight: bold;
}
/* 推荐课程 */
.demo-app-home-course-list {
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: none;
}
.demo-app-home-course-list::-webkit-scrollbar {
display: none;
}
.demo-app-home-course-card {
min-width: 240px;
height: 160px;
border-radius: 16px;
position: relative;
overflow: hidden;
cursor: pointer;
}
.demo-app-home-course-bg {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.demo-app-home-course-card:hover .demo-app-home-course-bg {
transform: scale(1.05);
}
.demo-app-home-course-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
}
.demo-app-home-course-tag {
background-color: #a6ff00;
color: #000;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
margin-bottom: 4px;
}
.demo-app-home-course-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.demo-app-home-course-meta {
font-size: 12px;
color: #ccc;
display: flex;
align-items: center;
gap: 8px;
}
/* 底部导航 */
.demo-app-home-tab-bar {
position: relative;
background-color: #1e1e1e;
padding: 8px 24px calc(16px + env(safe-area-inset-bottom));
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #333;
z-index: 30;
box-sizing: border-box;
flex-shrink: 0;
}
@supports (height: 100dvh) {
.demo-app-home-container {
height: var(--app-home-viewport-height, 100dvh);
max-height: var(--app-home-viewport-height, 100dvh);
min-height: var(--app-home-viewport-height, 100dvh);
}
}
.demo-app-home-tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
color: #666;
cursor: pointer;
transition: color 0.2s;
}
.demo-app-home-tab-item.active {
color: #a6ff00;
}
.demo-app-home-tab-icon {
font-size: 24px;
}
.demo-app-home-tab-label {
font-size: 10px;
font-weight: 500;
}
/* 浮动按钮 */
.demo-app-home-fab {
position: absolute;
bottom: calc(92px + env(safe-area-inset-bottom));
right: 20px;
width: 56px;
height: 56px;
border-radius: 28px;
background-color: #a6ff00;
color: #000;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 4px 16px rgba(166, 255, 0, 0.4);
cursor: pointer;
z-index: 40;
transition: transform 0.2s;
}
.demo-app-home-fab:active {
transform: scale(0.9);
}

View File

@@ -0,0 +1,208 @@
# Ant Design 主题规范
> 本文档定义了 antd-new 主题的设计价值、能力边界与使用指南,帮助开发者和 AI 正确理解和应用该设计系统。
## 设计系统价值
### 为什么选择 Ant Design
Ant Design 是由蚂蚁集团开发的企业级 UI 设计语言和 React 组件库,专为**中后台管理系统**设计。它的核心价值在于:
1. **降低决策成本** - 提供开箱即用的设计规范,减少设计与开发的沟通摩擦
2. **保证一致性** - 统一的视觉语言确保产品体验的连贯性
3. **提升效率** - 70+ 高质量组件覆盖绝大多数业务场景
4. **企业级品质** - 经过大规模生产环境验证,稳定可靠
### 设计原则
| 原则 | 含义 |
|------|------|
| **Stay on the Page** | 尽量在当前页面完成任务,减少页面跳转 |
| **React Immediately** | 即时反馈用户操作,让系统状态可见 |
| **Provide an Invitation** | 通过视觉引导帮助用户发现功能 |
| **Make it Direct** | 直接操作,所见即所得 |
| **Keep it Lightweight** | 保持界面轻量,避免信息过载 |
| **Use Transition** | 使用动效传达状态变化,建立空间感 |
---
## 能力边界
### ✅ 适合的场景
- 企业管理后台CRM、ERP、OA 等)
- 数据可视化仪表盘
- 表单密集型应用
- 内部工具和工作台
- B 端 SaaS 产品
### ❌ 不适合的场景
- C 端消费类产品(需要强品牌个性)
- 高度定制的创意设计
- 游戏或娱乐类界面
- 需要极致轻量的移动端 H5考虑使用 Ant Design Mobile
---
## 组件体系
### 按功能分类
| 类别 | 组件 | 典型用途 |
|------|------|---------|
| **数据录入** | Form, Input, Select, DatePicker, Upload, Checkbox, Radio, Switch, Slider, Rate, Transfer, Cascader, TreeSelect, ColorPicker | 表单填写、筛选条件 |
| **数据展示** | Table, List, Card, Descriptions, Tree, Timeline, Tag, Avatar, Badge, Statistic, Calendar, Image, Empty, Skeleton | 信息呈现、列表展示 |
| **反馈** | Modal, Drawer, Message, Notification, Alert, Progress, Popconfirm, Result, Spin | 操作结果、状态提示 |
| **导航** | Menu, Tabs, Breadcrumb, Pagination, Steps, Dropdown, Anchor, Affix | 页面导航、流程指引 |
| **布局** | Layout, Grid, Space, Divider, Flex, Splitter | 页面结构、间距控制 |
| **通用** | Button, Icon, Typography, FloatButton, ConfigProvider | 基础交互、全局配置 |
### 组件选择决策树
```
需要用户输入?
├─ 是 → 使用「数据录入」组件
│ ├─ 单行文本 → Input
│ ├─ 多行文本 → Input.TextArea
│ ├─ 数字 → InputNumber
│ ├─ 选择(少量选项)→ Radio / Checkbox
│ ├─ 选择(多选项)→ Select / Cascader
│ ├─ 日期时间 → DatePicker / TimePicker
│ └─ 文件 → Upload
└─ 否 → 需要展示数据?
├─ 是 → 使用「数据展示」组件
│ ├─ 表格数据 → Table
│ ├─ 列表数据 → List / Card
│ ├─ 详情信息 → Descriptions
│ ├─ 树形数据 → Tree
│ └─ 统计数值 → Statistic
└─ 否 → 需要导航/反馈?
├─ 导航 → Menu / Tabs / Breadcrumb
└─ 反馈 → Modal / Message / Notification
```
---
## 设计令牌Design Tokens
### 颜色系统
```
品牌色
├─ Primary: #1677ff (科技、理性、专业)
└─ 用于:主按钮、链接、选中态、强调元素
功能色
├─ Success: #52c41a (成功、正向反馈)
├─ Warning: #faad14 (警告、需要注意)
├─ Error: #ff4d4f (错误、危险操作)
└─ Info: #1677ff (信息提示,通常与主色一致)
中性色
├─ 标题文字: rgba(0,0,0,0.88)
├─ 正文文字: rgba(0,0,0,0.88)
├─ 次要文字: rgba(0,0,0,0.65)
├─ 禁用文字: rgba(0,0,0,0.25)
├─ 边框: #d9d9d9
└─ 背景: #ffffff
```
### 间距系统(基于 4px 网格)
| Token | 值 | 使用场景 |
|-------|-----|---------|
| `xxs` | 4px | 图标与文字间隙 |
| `xs` | 8px | 紧凑元素间距 |
| `sm` | 12px | 相关元素间距 |
| `md` | 16px | 标准间距(默认) |
| `lg` | 24px | 区块间距 |
| `xl` | 32px | 大区块间距 |
| `xxl` | 48px | 页面级间距 |
### 排版系统
| 层级 | 字号 | 使用场景 |
|------|------|---------|
| H1 | 38px | 页面大标题 |
| H2 | 30px | 区块标题 |
| H3 | 24px | 卡片标题 |
| H4 | 20px | 小标题 |
| H5 | 16px | 强调正文 |
| Body | 14px | 正文(默认) |
| Caption | 12px | 辅助说明 |
### 圆角与阴影
```
圆角
├─ xs: 2px (标签、小按钮)
├─ base: 6px (按钮、输入框、卡片)
└─ lg: 8px (弹窗、大卡片)
阴影(表达层级)
├─ 无阴影: 基础元素
├─ 小阴影: 悬浮卡片
├─ 中阴影: 下拉菜单、Popover
└─ 大阴影: Modal、Drawer
```
---
## 响应式断点
| 名称 | 范围 | 典型设备 |
|------|------|---------|
| `xs` | < 576px | 手机 |
| `sm` | ≥ 576px | 大屏手机/小平板 |
| `md` | ≥ 768px | 平板 |
| `lg` | ≥ 992px | 小型笔记本 |
| `xl` | ≥ 1200px | 桌面显示器 |
| `xxl` | ≥ 1600px | 大屏显示器 |
**注意**Ant Design 基于 24 栅格系统,使用 `<Row>``<Col>` 组件实现响应式布局。
---
## 使用约束
### 必须遵守
1. **不要修改组件内部样式** - 使用 ConfigProvider 或 Design Token 进行主题定制
2. **保持语义正确** - 按钮类型primary/default/text/link要符合其语义
3. **遵循表单规范** - 使用 Form 组件管理表单状态和校验
4. **注意性能** - 大数据表格使用虚拟滚动,避免一次性渲染过多数据
### 建议做法
1. **使用 Space 组件** - 管理元素间距,而非手写 margin
2. **善用 ConfigProvider** - 全局配置主题、国际化、尺寸等
3. **合理使用 Modal vs Drawer** - 轻量操作用 Modal复杂表单用 Drawer
4. **反馈层级分明** - 全局提示用 Message需要用户确认用 Modal
### 禁止做法
1. ❌ 不要在 Ant Design 组件上使用 `!important` 覆盖样式
2. ❌ 不要混用多个 UI 库(如同时使用 Ant Design 和 Material UI
3. ❌ 不要在循环中创建 Modal/Drawer 实例
4. ❌ 不要忽略 Form 的校验规则直接提交数据
---
## 版本说明
本规范基于 **Ant Design v5/v6** 标准,主要特性:
- CSS-in-JS 方案(@ant-design/cssinjs
- Design Token 系统
- 组件级主题定制
- 更好的 Tree Shaking 支持
---
## 相关资源
- [Ant Design 官方文档](https://ant.design)
- [组件 API 文档](https://ant.design/components/overview)
- [设计模式指南](https://ant.design/docs/spec/overview)
- [主题定制指南](https://ant.design/docs/react/customize-theme)

View File

@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Button as AntButton, Space, Divider } from 'antd';
import { MobileOutlined, DesktopOutlined, TabletOutlined } from '@ant-design/icons';
interface ButtonSectionProps {
tokens: Record<string, any>;
}
export const ButtonSection: React.FC<ButtonSectionProps> = () => {
const [viewport, setViewport] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
const ViewportFrame: React.FC<{ children: React.ReactNode }> = ({ children }) => {
let widthClass = 'w-full';
if (viewport === 'mobile') widthClass = 'w-[375px]';
if (viewport === 'tablet') widthClass = 'w-[768px]';
return (
<div className={`transition-all duration-300 mx-auto border-x border-b border-neutral-200 bg-white shadow-sm ${widthClass} min-h-[400px]`}>
{children}
</div>
);
};
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-2"> Button</h1>
<p className="text-neutral-600"></p>
</div>
{/* 预览区域 */}
<div className="border border-neutral-200 rounded-lg bg-white overflow-hidden shadow-sm">
<div className="flex items-center justify-between border-b border-neutral-200 bg-gray-50/50 px-4 py-2">
<div className="flex space-x-1">
<span className="w-2.5 h-2.5 rounded-full bg-neutral-200"></span>
<span className="w-2.5 h-2.5 rounded-full bg-neutral-200"></span>
<span className="w-2.5 h-2.5 rounded-full bg-neutral-200"></span>
</div>
<div className="flex space-x-4 text-neutral-400">
<button onClick={() => setViewport('mobile')} className={`p-1 hover:text-black ${viewport === 'mobile' ? 'text-black' : 'text-neutral-400'}`}>
<MobileOutlined style={{ fontSize: 16 }} />
</button>
<button onClick={() => setViewport('tablet')} className={`p-1 hover:text-black ${viewport === 'tablet' ? 'text-black' : 'text-neutral-400'}`}>
<TabletOutlined style={{ fontSize: 16 }} />
</button>
<button onClick={() => setViewport('desktop')} className={`p-1 hover:text-black ${viewport === 'desktop' ? 'text-black' : 'text-neutral-400'}`}>
<DesktopOutlined style={{ fontSize: 16 }} />
</button>
</div>
</div>
<div className="p-8 overflow-auto flex justify-center bg-neutral-100/40">
<ViewportFrame>
<div className="flex flex-col items-center justify-center h-full space-y-6 p-8">
<Space wrap>
<AntButton type="primary">Primary Button</AntButton>
<AntButton>Default Button</AntButton>
<AntButton type="dashed">Dashed Button</AntButton>
<AntButton type="text">Text Button</AntButton>
<AntButton type="link">Link Button</AntButton>
</Space>
<Divider></Divider>
<Space wrap>
<AntButton type="primary" size="large">Large</AntButton>
<AntButton type="primary">Default</AntButton>
<AntButton type="primary" size="small">Small</AntButton>
</Space>
<Divider></Divider>
<Space wrap>
<AntButton type="primary" loading>Loading</AntButton>
<AntButton type="primary" disabled>Disabled</AntButton>
<AntButton type="primary" danger>Danger</AntButton>
</Space>
</div>
</ViewportFrame>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,169 @@
import React from 'react';
import { Card as AntCard, Row, Col, Avatar, Space } from 'antd';
import { EditOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons';
const { Meta } = AntCard;
interface CardSectionProps {
tokens: Record<string, any>;
}
export const CardSection: React.FC<CardSectionProps> = ({ tokens }) => {
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-2"> Card</h1>
<p className="text-neutral-600"></p>
</div>
<div className="space-y-8">
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8}>
<AntCard title="默认卡片" bordered={false}>
<p></p>
<p></p>
<p></p>
</AntCard>
</Col>
<Col xs={24} sm={12} md={8}>
<AntCard title="带边框卡片" bordered={true}>
<p></p>
<p></p>
<p></p>
</AntCard>
</Col>
<Col xs={24} sm={12} md={8}>
<AntCard title="无标题卡片" bordered={false}>
<p></p>
<p></p>
<p></p>
</AntCard>
</Col>
</Row>
</div>
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8}>
<AntCard
hoverable
cover={
<div style={{
height: 200,
background: '#fafafa',
borderBottom: '1px dashed #d9d9d9',
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 14,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
</div>
}
>
<Meta
avatar={<Avatar style={{ backgroundColor: tokens.colorPrimary }}>U</Avatar>}
title="卡片标题"
description="这是卡片的描述信息"
/>
</AntCard>
</Col>
<Col xs={24} sm={12} md={8}>
<AntCard
hoverable
cover={
<div style={{
height: 200,
background: '#fafafa',
borderBottom: '1px dashed #d9d9d9',
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 14,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
</div>
}
>
<Meta
avatar={<Avatar style={{ backgroundColor: tokens.colorSuccess }}>U</Avatar>}
title="卡片标题"
description="这是卡片的描述信息"
/>
</AntCard>
</Col>
<Col xs={24} sm={12} md={8}>
<AntCard
hoverable
cover={
<div style={{
height: 200,
background: '#fafafa',
borderBottom: '1px dashed #d9d9d9',
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 14,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
</div>
}
>
<Meta
avatar={<Avatar style={{ backgroundColor: tokens.colorWarning }}>U</Avatar>}
title="卡片标题"
description="这是卡片的描述信息"
/>
</AntCard>
</Col>
</Row>
</div>
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8}>
<AntCard
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
]}
>
<Meta
avatar={<Avatar style={{ backgroundColor: tokens.colorPrimary }}>U</Avatar>}
title="卡片标题"
description="这是卡片的描述信息,可以包含更多内容"
/>
</AntCard>
</Col>
</Row>
</div>
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8}>
<AntCard loading>
<Meta
avatar={<Avatar>U</Avatar>}
title="卡片标题"
description="这是卡片的描述信息"
/>
</AntCard>
</Col>
</Row>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { Input as AntInput, Space, Divider } from 'antd';
import { UserOutlined, LockOutlined, SearchOutlined, EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
const { TextArea } = AntInput;
interface InputSectionProps {
tokens: Record<string, any>;
}
export const InputSection: React.FC<InputSectionProps> = ({ tokens }) => {
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-2"> Input</h1>
<p className="text-neutral-600"></p>
</div>
<div className="border border-neutral-200 rounded-lg bg-white shadow-sm">
<div className="p-8 space-y-6">
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<AntInput placeholder="请输入内容" />
<AntInput prefix={<UserOutlined />} placeholder="带前缀图标" />
<AntInput suffix={<SearchOutlined />} placeholder="带后缀图标" />
</Space>
</div>
<Divider />
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<AntInput.Password
prefix={<LockOutlined />}
placeholder="请输入密码"
iconRender={(visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
/>
</Space>
</div>
<Divider />
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<AntInput.Search placeholder="搜索内容" onSearch={(value) => console.log(value)} />
<AntInput.Search placeholder="带加载状态" loading />
<AntInput.Search placeholder="带按钮" enterButton />
</Space>
</div>
<Divider />
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<TextArea rows={4} placeholder="请输入多行文本" />
</div>
<Divider />
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<AntInput size="large" placeholder="大尺寸输入框" />
<AntInput placeholder="默认尺寸输入框" />
<AntInput size="small" placeholder="小尺寸输入框" />
</Space>
</div>
<Divider />
<div>
<h3 className="text-base font-semibold mb-4"></h3>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<AntInput placeholder="默认状态" />
<AntInput placeholder="禁用状态" disabled />
<AntInput placeholder="只读状态" readOnly />
<AntInput status="error" placeholder="错误状态" />
<AntInput status="warning" placeholder="警告状态" />
</Space>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,505 @@
{
"name": "Ant Design",
"description": "Ant Design v6 默认设计变量",
"blue": "#1677FF",
"purple": "#722ED1",
"cyan": "#13C2C2",
"green": "#52C41A",
"magenta": "#EB2F96",
"pink": "#EB2F96",
"red": "#F5222D",
"orange": "#FA8C16",
"yellow": "#FADB14",
"volcano": "#FA541C",
"geekblue": "#2F54EB",
"gold": "#FAAD14",
"lime": "#A0D911",
"colorPrimary": "#1677ff",
"colorSuccess": "#52c41a",
"colorWarning": "#faad14",
"colorError": "#ff4d4f",
"colorInfo": "#1677ff",
"colorLink": "#1677ff",
"colorTextBase": "#000",
"colorBgBase": "#fff",
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"lineWidth": 1,
"lineType": "solid",
"motionUnit": 0.1,
"motionBase": 0,
"motionEaseOutCirc": "cubic-bezier(0.08, 0.82, 0.17, 1)",
"motionEaseInOutCirc": "cubic-bezier(0.78, 0.14, 0.15, 0.86)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseOutBack": "cubic-bezier(0.12, 0.4, 0.29, 1.46)",
"motionEaseInBack": "cubic-bezier(0.71, -0.46, 0.88, 0.6)",
"motionEaseInQuint": "cubic-bezier(0.755, 0.05, 0.855, 0.06)",
"motionEaseOutQuint": "cubic-bezier(0.23, 1, 0.32, 1)",
"borderRadius": 6,
"sizeUnit": 4,
"sizeStep": 4,
"sizePopupArrow": 16,
"controlHeight": 32,
"zIndexBase": 0,
"zIndexPopupBase": 1000,
"opacityImage": 1,
"wireframe": false,
"motion": true,
"blue-1": "#e6f4ff",
"blue1": "#e6f4ff",
"blue-2": "#bae0ff",
"blue2": "#bae0ff",
"blue-3": "#91caff",
"blue3": "#91caff",
"blue-4": "#69b1ff",
"blue4": "#69b1ff",
"blue-5": "#4096ff",
"blue5": "#4096ff",
"blue-6": "#1677ff",
"blue6": "#1677ff",
"blue-7": "#0958d9",
"blue7": "#0958d9",
"blue-8": "#003eb3",
"blue8": "#003eb3",
"blue-9": "#002c8c",
"blue9": "#002c8c",
"blue-10": "#001d66",
"blue10": "#001d66",
"purple-1": "#f9f0ff",
"purple1": "#f9f0ff",
"purple-2": "#efdbff",
"purple2": "#efdbff",
"purple-3": "#d3adf7",
"purple3": "#d3adf7",
"purple-4": "#b37feb",
"purple4": "#b37feb",
"purple-5": "#9254de",
"purple5": "#9254de",
"purple-6": "#722ed1",
"purple6": "#722ed1",
"purple-7": "#531dab",
"purple7": "#531dab",
"purple-8": "#391085",
"purple8": "#391085",
"purple-9": "#22075e",
"purple9": "#22075e",
"purple-10": "#120338",
"purple10": "#120338",
"cyan-1": "#e6fffb",
"cyan1": "#e6fffb",
"cyan-2": "#b5f5ec",
"cyan2": "#b5f5ec",
"cyan-3": "#87e8de",
"cyan3": "#87e8de",
"cyan-4": "#5cdbd3",
"cyan4": "#5cdbd3",
"cyan-5": "#36cfc9",
"cyan5": "#36cfc9",
"cyan-6": "#13c2c2",
"cyan6": "#13c2c2",
"cyan-7": "#08979c",
"cyan7": "#08979c",
"cyan-8": "#006d75",
"cyan8": "#006d75",
"cyan-9": "#00474f",
"cyan9": "#00474f",
"cyan-10": "#002329",
"cyan10": "#002329",
"green-1": "#f6ffed",
"green1": "#f6ffed",
"green-2": "#d9f7be",
"green2": "#d9f7be",
"green-3": "#b7eb8f",
"green3": "#b7eb8f",
"green-4": "#95de64",
"green4": "#95de64",
"green-5": "#73d13d",
"green5": "#73d13d",
"green-6": "#52c41a",
"green6": "#52c41a",
"green-7": "#389e0d",
"green7": "#389e0d",
"green-8": "#237804",
"green8": "#237804",
"green-9": "#135200",
"green9": "#135200",
"green-10": "#092b00",
"green10": "#092b00",
"magenta-1": "#fff0f6",
"magenta1": "#fff0f6",
"magenta-2": "#ffd6e7",
"magenta2": "#ffd6e7",
"magenta-3": "#ffadd2",
"magenta3": "#ffadd2",
"magenta-4": "#ff85c0",
"magenta4": "#ff85c0",
"magenta-5": "#f759ab",
"magenta5": "#f759ab",
"magenta-6": "#eb2f96",
"magenta6": "#eb2f96",
"magenta-7": "#c41d7f",
"magenta7": "#c41d7f",
"magenta-8": "#9e1068",
"magenta8": "#9e1068",
"magenta-9": "#780650",
"magenta9": "#780650",
"magenta-10": "#520339",
"magenta10": "#520339",
"pink-1": "#fff0f6",
"pink1": "#fff0f6",
"pink-2": "#ffd6e7",
"pink2": "#ffd6e7",
"pink-3": "#ffadd2",
"pink3": "#ffadd2",
"pink-4": "#ff85c0",
"pink4": "#ff85c0",
"pink-5": "#f759ab",
"pink5": "#f759ab",
"pink-6": "#eb2f96",
"pink6": "#eb2f96",
"pink-7": "#c41d7f",
"pink7": "#c41d7f",
"pink-8": "#9e1068",
"pink8": "#9e1068",
"pink-9": "#780650",
"pink9": "#780650",
"pink-10": "#520339",
"pink10": "#520339",
"red-1": "#fff1f0",
"red1": "#fff1f0",
"red-2": "#ffccc7",
"red2": "#ffccc7",
"red-3": "#ffa39e",
"red3": "#ffa39e",
"red-4": "#ff7875",
"red4": "#ff7875",
"red-5": "#ff4d4f",
"red5": "#ff4d4f",
"red-6": "#f5222d",
"red6": "#f5222d",
"red-7": "#cf1322",
"red7": "#cf1322",
"red-8": "#a8071a",
"red8": "#a8071a",
"red-9": "#820014",
"red9": "#820014",
"red-10": "#5c0011",
"red10": "#5c0011",
"orange-1": "#fff7e6",
"orange1": "#fff7e6",
"orange-2": "#ffe7ba",
"orange2": "#ffe7ba",
"orange-3": "#ffd591",
"orange3": "#ffd591",
"orange-4": "#ffc069",
"orange4": "#ffc069",
"orange-5": "#ffa940",
"orange5": "#ffa940",
"orange-6": "#fa8c16",
"orange6": "#fa8c16",
"orange-7": "#d46b08",
"orange7": "#d46b08",
"orange-8": "#ad4e00",
"orange8": "#ad4e00",
"orange-9": "#873800",
"orange9": "#873800",
"orange-10": "#612500",
"orange10": "#612500",
"yellow-1": "#feffe6",
"yellow1": "#feffe6",
"yellow-2": "#ffffb8",
"yellow2": "#ffffb8",
"yellow-3": "#fffb8f",
"yellow3": "#fffb8f",
"yellow-4": "#fff566",
"yellow4": "#fff566",
"yellow-5": "#ffec3d",
"yellow5": "#ffec3d",
"yellow-6": "#fadb14",
"yellow6": "#fadb14",
"yellow-7": "#d4b106",
"yellow7": "#d4b106",
"yellow-8": "#ad8b00",
"yellow8": "#ad8b00",
"yellow-9": "#876800",
"yellow9": "#876800",
"yellow-10": "#614700",
"yellow10": "#614700",
"volcano-1": "#fff2e8",
"volcano1": "#fff2e8",
"volcano-2": "#ffd8bf",
"volcano2": "#ffd8bf",
"volcano-3": "#ffbb96",
"volcano3": "#ffbb96",
"volcano-4": "#ff9c6e",
"volcano4": "#ff9c6e",
"volcano-5": "#ff7a45",
"volcano5": "#ff7a45",
"volcano-6": "#fa541c",
"volcano6": "#fa541c",
"volcano-7": "#d4380d",
"volcano7": "#d4380d",
"volcano-8": "#ad2102",
"volcano8": "#ad2102",
"volcano-9": "#871400",
"volcano9": "#871400",
"volcano-10": "#610b00",
"volcano10": "#610b00",
"geekblue-1": "#f0f5ff",
"geekblue1": "#f0f5ff",
"geekblue-2": "#d6e4ff",
"geekblue2": "#d6e4ff",
"geekblue-3": "#adc6ff",
"geekblue3": "#adc6ff",
"geekblue-4": "#85a5ff",
"geekblue4": "#85a5ff",
"geekblue-5": "#597ef7",
"geekblue5": "#597ef7",
"geekblue-6": "#2f54eb",
"geekblue6": "#2f54eb",
"geekblue-7": "#1d39c4",
"geekblue7": "#1d39c4",
"geekblue-8": "#10239e",
"geekblue8": "#10239e",
"geekblue-9": "#061178",
"geekblue9": "#061178",
"geekblue-10": "#030852",
"geekblue10": "#030852",
"gold-1": "#fffbe6",
"gold1": "#fffbe6",
"gold-2": "#fff1b8",
"gold2": "#fff1b8",
"gold-3": "#ffe58f",
"gold3": "#ffe58f",
"gold-4": "#ffd666",
"gold4": "#ffd666",
"gold-5": "#ffc53d",
"gold5": "#ffc53d",
"gold-6": "#faad14",
"gold6": "#faad14",
"gold-7": "#d48806",
"gold7": "#d48806",
"gold-8": "#ad6800",
"gold8": "#ad6800",
"gold-9": "#874d00",
"gold9": "#874d00",
"gold-10": "#613400",
"gold10": "#613400",
"lime-1": "#fcffe6",
"lime1": "#fcffe6",
"lime-2": "#f4ffb8",
"lime2": "#f4ffb8",
"lime-3": "#eaff8f",
"lime3": "#eaff8f",
"lime-4": "#d3f261",
"lime4": "#d3f261",
"lime-5": "#bae637",
"lime5": "#bae637",
"lime-6": "#a0d911",
"lime6": "#a0d911",
"lime-7": "#7cb305",
"lime7": "#7cb305",
"lime-8": "#5b8c00",
"lime8": "#5b8c00",
"lime-9": "#3f6600",
"lime9": "#3f6600",
"lime-10": "#254000",
"lime10": "#254000",
"colorText": "rgba(0,0,0,0.88)",
"colorTextSecondary": "rgba(0,0,0,0.65)",
"colorTextTertiary": "rgba(0,0,0,0.45)",
"colorTextQuaternary": "rgba(0,0,0,0.25)",
"colorFill": "rgba(0,0,0,0.15)",
"colorFillSecondary": "rgba(0,0,0,0.06)",
"colorFillTertiary": "rgba(0,0,0,0.04)",
"colorFillQuaternary": "rgba(0,0,0,0.02)",
"colorBgSolid": "rgb(0,0,0)",
"colorBgSolidHover": "rgba(0,0,0,0.75)",
"colorBgSolidActive": "rgba(0,0,0,0.95)",
"colorBgLayout": "#f5f5f5",
"colorBgContainer": "#ffffff",
"colorBgElevated": "#ffffff",
"colorBgSpotlight": "rgba(0,0,0,0.85)",
"colorBgBlur": "transparent",
"colorBorder": "#d9d9d9",
"colorBorderSecondary": "#f0f0f0",
"colorPrimaryBg": "#e6f4ff",
"colorPrimaryBgHover": "#bae0ff",
"colorPrimaryBorder": "#91caff",
"colorPrimaryBorderHover": "#69b1ff",
"colorPrimaryHover": "#4096ff",
"colorPrimaryActive": "#0958d9",
"colorPrimaryTextHover": "#4096ff",
"colorPrimaryText": "#1677ff",
"colorPrimaryTextActive": "#0958d9",
"colorSuccessBg": "#f6ffed",
"colorSuccessBgHover": "#d9f7be",
"colorSuccessBorder": "#b7eb8f",
"colorSuccessBorderHover": "#95de64",
"colorSuccessHover": "#95de64",
"colorSuccessActive": "#389e0d",
"colorSuccessTextHover": "#73d13d",
"colorSuccessText": "#52c41a",
"colorSuccessTextActive": "#389e0d",
"colorErrorBg": "#fff2f0",
"colorErrorBgHover": "#fff1f0",
"colorErrorBgFilledHover": "#ffdfdc",
"colorErrorBgActive": "#ffccc7",
"colorErrorBorder": "#ffccc7",
"colorErrorBorderHover": "#ffa39e",
"colorErrorHover": "#ff7875",
"colorErrorActive": "#d9363e",
"colorErrorTextHover": "#ff7875",
"colorErrorText": "#ff4d4f",
"colorErrorTextActive": "#d9363e",
"colorWarningBg": "#fffbe6",
"colorWarningBgHover": "#fff1b8",
"colorWarningBorder": "#ffe58f",
"colorWarningBorderHover": "#ffd666",
"colorWarningHover": "#ffd666",
"colorWarningActive": "#d48806",
"colorWarningTextHover": "#ffc53d",
"colorWarningText": "#faad14",
"colorWarningTextActive": "#d48806",
"colorInfoBg": "#e6f4ff",
"colorInfoBgHover": "#bae0ff",
"colorInfoBorder": "#91caff",
"colorInfoBorderHover": "#69b1ff",
"colorInfoHover": "#69b1ff",
"colorInfoActive": "#0958d9",
"colorInfoTextHover": "#4096ff",
"colorInfoText": "#1677ff",
"colorInfoTextActive": "#0958d9",
"colorLinkHover": "#69b1ff",
"colorLinkActive": "#0958d9",
"colorBgMask": "rgba(0,0,0,0.45)",
"colorWhite": "#fff",
"fontSizeSM": 12,
"fontSizeLG": 16,
"fontSizeXL": 20,
"fontSizeHeading1": 38,
"fontSizeHeading2": 30,
"fontSizeHeading3": 24,
"fontSizeHeading4": 20,
"fontSizeHeading5": 16,
"lineHeight": 1.5714285714285714,
"lineHeightLG": 1.5,
"lineHeightSM": 1.6666666666666667,
"fontHeight": 22,
"fontHeightLG": 24,
"fontHeightSM": 20,
"lineHeightHeading1": 1.2105263157894737,
"lineHeightHeading2": 1.2666666666666666,
"lineHeightHeading3": 1.3333333333333333,
"lineHeightHeading4": 1.4,
"lineHeightHeading5": 1.5,
"sizeXXL": 48,
"sizeXL": 32,
"sizeLG": 24,
"sizeMD": 20,
"sizeMS": 16,
"size": 16,
"sizeSM": 12,
"sizeXS": 8,
"sizeXXS": 4,
"controlHeightSM": 24,
"controlHeightXS": 16,
"controlHeightLG": 40,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"lineWidthBold": 2,
"borderRadiusXS": 2,
"borderRadiusSM": 4,
"borderRadiusLG": 8,
"borderRadiusOuter": 4,
"colorFillContent": "rgba(0,0,0,0.06)",
"colorFillContentHover": "rgba(0,0,0,0.15)",
"colorFillAlter": "rgba(0,0,0,0.02)",
"colorBgContainerDisabled": "rgba(0,0,0,0.04)",
"colorBorderBg": "#ffffff",
"colorSplit": "rgba(5,5,5,0.06)",
"colorTextPlaceholder": "rgba(0,0,0,0.25)",
"colorTextDisabled": "rgba(0,0,0,0.25)",
"colorTextHeading": "rgba(0,0,0,0.88)",
"colorTextLabel": "rgba(0,0,0,0.65)",
"colorTextDescription": "rgba(0,0,0,0.45)",
"colorTextLightSolid": "#fff",
"colorHighlight": "#ff4d4f",
"colorBgTextHover": "rgba(0,0,0,0.06)",
"colorBgTextActive": "rgba(0,0,0,0.15)",
"colorIcon": "rgba(0,0,0,0.45)",
"colorIconHover": "rgba(0,0,0,0.88)",
"colorErrorOutline": "rgba(255,38,5,0.06)",
"colorWarningOutline": "rgba(255,215,5,0.1)",
"fontSizeIcon": 12,
"lineWidthFocus": 3,
"controlOutlineWidth": 2,
"controlInteractiveSize": 16,
"controlItemBgHover": "rgba(0,0,0,0.04)",
"controlItemBgActive": "#e6f4ff",
"controlItemBgActiveHover": "#bae0ff",
"controlItemBgActiveDisabled": "rgba(0,0,0,0.15)",
"controlTmpOutline": "rgba(0,0,0,0.02)",
"controlOutline": "rgba(5,145,255,0.1)",
"fontWeightStrong": 600,
"opacityLoading": 0.65,
"linkDecoration": "none",
"linkHoverDecoration": "none",
"linkFocusDecoration": "none",
"controlPaddingHorizontal": 12,
"controlPaddingHorizontalSM": 8,
"paddingXXS": 4,
"paddingXS": 8,
"paddingSM": 12,
"padding": 16,
"paddingMD": 20,
"paddingLG": 24,
"paddingXL": 32,
"paddingContentHorizontalLG": 24,
"paddingContentVerticalLG": 16,
"paddingContentHorizontal": 16,
"paddingContentVertical": 12,
"paddingContentHorizontalSM": 16,
"paddingContentVerticalSM": 8,
"marginXXS": 4,
"marginXS": 8,
"marginSM": 12,
"margin": 16,
"marginMD": 20,
"marginLG": 24,
"marginXL": 32,
"marginXXL": 48,
"boxShadow": "\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowSecondary": "\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowTertiary": "\n 0 1px 2px 0 rgba(0, 0, 0, 0.03),\n 0 1px 6px -1px rgba(0, 0, 0, 0.02),\n 0 2px 4px 0 rgba(0, 0, 0, 0.02)\n ",
"screenXS": 480,
"screenXSMin": 480,
"screenXSMax": 575,
"screenSM": 576,
"screenSMMin": 576,
"screenSMMax": 767,
"screenMD": 768,
"screenMDMin": 768,
"screenMDMax": 991,
"screenLG": 992,
"screenLGMin": 992,
"screenLGMax": 1199,
"screenXL": 1200,
"screenXLMin": 1200,
"screenXLMax": 1599,
"screenXXL": 1600,
"screenXXLMin": 1600,
"boxShadowPopoverArrow": "2px 2px 5px rgba(0, 0, 0, 0.05)",
"boxShadowCard": "\n 0 1px 2px -2px rgba(0,0,0,0.16),\n 0 3px 6px 0 rgba(0,0,0,0.12),\n 0 5px 12px 4px rgba(0,0,0,0.09)\n ",
"boxShadowDrawerRight": "\n -6px 0 16px 0 rgba(0, 0, 0, 0.08),\n -3px 0 6px -4px rgba(0, 0, 0, 0.12),\n -9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowDrawerLeft": "\n 6px 0 16px 0 rgba(0, 0, 0, 0.08),\n 3px 0 6px -4px rgba(0, 0, 0, 0.12),\n 9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowDrawerUp": "\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowDrawerDown": "\n 0 -6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 -3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 -9px 28px 8px rgba(0, 0, 0, 0.05)\n ",
"boxShadowTabsOverflowLeft": "inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)",
"boxShadowTabsOverflowRight": "inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)",
"boxShadowTabsOverflowTop": "inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)",
"boxShadowTabsOverflowBottom": "inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)"
}

View File

@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import { Row, Col, Typography, message } from 'antd';
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
const { Title, Text } = Typography;
interface ColorsProps {
tokens: Record<string, any>;
}
const ColorItem = ({ name, value, description }: { name: string, value: string, description?: string }) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(value);
setCopied(true);
message.success(`已复制: ${value}`);
setTimeout(() => setCopied(false), 2000);
};
return (
<div
onClick={handleCopy}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: 12,
borderRadius: 8,
border: '1px solid #f0f0f0',
cursor: 'pointer',
transition: 'all 0.2s',
}}
className="color-item-hover"
>
<div
style={{
width: 48,
height: 48,
borderRadius: 8,
background: value,
boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.06)',
flexShrink: 0
}}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<Text strong ellipsis>{name}</Text>
{copied ? <CheckOutlined style={{ color: '#52c41a' }} /> : <CopyOutlined style={{ color: '#00000040', fontSize: 12 }} />}
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Text type="secondary" style={{ fontSize: 12 }}>{value}</Text>
{description && <Text type="secondary" style={{ fontSize: 12, marginTop: 2 }}>{description}</Text>}
</div>
</div>
</div>
);
};
export const Colors: React.FC<ColorsProps> = ({ tokens }) => {
const brandColors = [
{ name: 'colorPrimary', desc: '品牌色 (Brand Color)' },
{ name: 'colorInfo', desc: '信息色 (Info Color)' },
];
const functionalColors = [
{ name: 'colorSuccess', desc: '成功色 (Success Color)' },
{ name: 'colorWarning', desc: '警告色 (Warning Color)' },
{ name: 'colorError', desc: '错误色 (Error Color)' },
];
const baseColors = [
{ name: 'colorTextBase', desc: '基础文本色 (Text Base)' },
{ name: 'colorBgBase', desc: '基础背景色 (Background Base)' },
{ name: 'colorLink', desc: '链接色 (Link Color)' },
];
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Colors</h1>
<p className="text-neutral-600 mb-8 max-w-2xl">
访
</p>
<Title level={5} style={{ marginTop: 24 }}></Title>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{brandColors.map(c => (
<Col xs={24} sm={12} md={8} key={c.name}>
<ColorItem name={c.name} value={tokens[c.name]} description={c.desc} />
</Col>
))}
</Row>
<Title level={5}></Title>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{functionalColors.map(c => (
<Col xs={24} sm={12} md={8} key={c.name}>
<ColorItem name={c.name} value={tokens[c.name]} description={c.desc} />
</Col>
))}
</Row>
<Title level={5}></Title>
<Row gutter={[16, 16]}>
{baseColors.map(c => (
<Col xs={24} sm={12} md={8} key={c.name}>
<ColorItem name={c.name} value={tokens[c.name]} description={c.desc} />
</Col>
))}
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Row, Col, Card, Input, message } from 'antd';
import * as Icons from '@ant-design/icons';
interface IconsProps {
tokens: Record<string, any>;
}
export const IconsSection: React.FC<IconsProps> = ({ tokens }) => {
const [searchText, setSearchText] = React.useState('');
// 常用图标列表
const commonIcons = [
'HomeOutlined', 'UserOutlined', 'SettingOutlined', 'SearchOutlined',
'HeartOutlined', 'StarOutlined', 'LikeOutlined', 'MessageOutlined',
'BellOutlined', 'MailOutlined', 'PhoneOutlined', 'CameraOutlined',
'FileOutlined', 'FolderOutlined', 'SaveOutlined', 'DeleteOutlined',
'EditOutlined', 'CopyOutlined', 'CheckOutlined', 'CloseOutlined',
'PlusOutlined', 'MinusOutlined', 'UpOutlined', 'DownOutlined',
'LeftOutlined', 'RightOutlined', 'MenuOutlined', 'AppstoreOutlined',
];
const handleCopyIconName = (iconName: string) => {
navigator.clipboard.writeText(`<${iconName} />`);
message.success(`已复制: <${iconName} />`);
};
const filteredIcons = commonIcons.filter(name =>
name.toLowerCase().includes(searchText.toLowerCase())
);
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Icons</h1>
<p className="text-neutral-600 mb-8">
Ant Design 使 @ant-design/icons
</p>
<div className="mb-6">
<Input.Search
placeholder="搜索图标..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ maxWidth: 400 }}
size="large"
/>
</div>
<Row gutter={[16, 16]}>
{filteredIcons.map((iconName) => {
const IconComponent = (Icons as any)[iconName];
if (!IconComponent) return null;
return (
<Col xs={12} sm={8} md={6} lg={4} key={iconName}>
<Card
hoverable
onClick={() => handleCopyIconName(iconName)}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<IconComponent style={{ fontSize: 32 }} />
<div style={{
marginTop: 12,
fontSize: 12,
color: tokens.colorTextSecondary,
wordBreak: 'break-word'
}}>
{iconName}
</div>
</Card>
</Col>
);
})}
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { Row, Col, Card } from 'antd';
interface RadiusProps {
tokens: Record<string, any>;
}
export const Radius: React.FC<RadiusProps> = ({ tokens }) => {
const radiusValues = [
{ name: 'borderRadiusXS', value: tokens.borderRadiusXS, label: '超小圆角' },
{ name: 'borderRadiusSM', value: tokens.borderRadiusSM, label: '小圆角' },
{ name: 'borderRadius', value: tokens.borderRadius, label: '基础圆角' },
{ name: 'borderRadiusLG', value: tokens.borderRadiusLG, label: '大圆角' },
{ name: 'borderRadiusOuter', value: tokens.borderRadiusOuter, label: '外层圆角' },
];
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Border Radius</h1>
<p className="text-neutral-600 mb-8">
</p>
<Row gutter={[24, 24]}>
{radiusValues.map((item) => (
<Col xs={24} sm={12} md={8} key={item.name}>
<Card>
<div style={{ marginBottom: 16 }}>
<div style={{ fontWeight: 500, marginBottom: 4 }}>{item.label}</div>
<code style={{ fontSize: 12, color: tokens.colorTextSecondary }}>
{item.name}: {item.value}px
</code>
</div>
<div
style={{
width: '100%',
height: 120,
background: '#e2e8f0',
border: '1px dashed #cbd5e1',
borderRadius: item.value,
}}
/>
</Card>
</Col>
))}
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { Card, Row, Col, message, Tooltip } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
interface ShadowsProps {
tokens: Record<string, any>;
}
const ShadowCard = ({ name, label, value, tokens }: { name: string, label: string, value: string, tokens: Record<string, any> }) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(value);
setCopied(true);
message.success('已复制阴影值');
setTimeout(() => setCopied(false), 2000);
};
return (
<Card
hoverable
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
bodyStyle={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
}}
>
<div
style={{
width: 80,
height: 80,
borderRadius: 8,
backgroundColor: '#fff',
boxShadow: value,
marginBottom: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
color: tokens.colorTextSecondary,
}}
>
Preview
</div>
<div style={{ textAlign: 'center', width: '100%' }}>
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>
{label}
</div>
<div style={{ fontSize: 12, color: tokens.colorTextSecondary, marginBottom: 16 }}>
{name}
</div>
<Tooltip title="复制阴影值">
<div
onClick={handleCopy}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
color: copied ? tokens.colorSuccess : tokens.colorPrimary,
fontSize: 12,
padding: '4px 8px',
background: copied ? tokens.colorSuccessBg : tokens.colorPrimaryBg,
borderRadius: 4,
transition: 'all 0.2s',
}}
>
{copied ? <CheckOutlined /> : <CopyOutlined />}
<span>{copied ? '已复制' : '复制配置'}</span>
</div>
</Tooltip>
</div>
</Card>
);
};
export const Shadows: React.FC<ShadowsProps> = ({ tokens }) => {
const shadows = [
{ name: 'boxShadow', label: '基础阴影', value: tokens.boxShadow },
{ name: 'boxShadowSecondary', label: '次级阴影', value: tokens.boxShadowSecondary },
{ name: 'boxShadowTertiary', label: '三级阴影', value: tokens.boxShadowTertiary },
];
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Shadows</h1>
<p className="text-neutral-600 mb-8">
</p>
<Row gutter={[24, 24]}>
{shadows.map((shadow) => (
<Col xs={24} sm={12} md={8} key={shadow.name}>
<ShadowCard
name={shadow.name}
label={shadow.label}
value={shadow.value}
tokens={tokens}
/>
</Col>
))}
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Row, Col, Typography } from 'antd';
const { Text } = Typography;
interface SpacingProps {
tokens: Record<string, any>;
}
export const Spacing: React.FC<SpacingProps> = ({ tokens }) => {
const sizes = ['XXS', 'XS', 'SM', 'MD', 'LG', 'XL', 'XXL'];
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Spacing</h1>
<p className="text-neutral-600 mb-8">
{tokens.sizeUnit}px
</p>
<Row gutter={[16, 16]}>
{sizes.map(size => {
const key = `size${size}`;
const value = tokens[key];
if (!value) return null;
return (
<Col xs={24} sm={12} md={8} key={key}>
<div style={{
padding: 16,
border: '1px solid #f0f0f0',
borderRadius: 8
}}>
<div style={{ marginBottom: 12 }}>
<Text strong>{key}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>{value}px</Text>
</div>
<div
style={{
width: value,
height: value,
background: '#1677ff',
borderRadius: 4
}}
/>
</div>
</Col>
);
})}
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Descriptions, Typography } from 'antd';
const { Text } = Typography;
interface TypographyProps {
tokens: Record<string, any>;
}
export const TypographySection: React.FC<TypographyProps> = ({ tokens }) => {
return (
<div className="space-y-12 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-4"> Typography</h1>
<p className="text-neutral-600 mb-8">
</p>
<Descriptions bordered column={1}>
<Descriptions.Item label="字体家族 (Font Family)">
<Text code copyable style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{tokens.fontFamily}
</Text>
<div style={{ marginTop: 8, fontSize: 16, fontFamily: tokens.fontFamily }}>
The quick brown fox jumps over the lazy dog.
</div>
</Descriptions.Item>
<Descriptions.Item label="基础字号 (Font Size)">
<Text code>{tokens.fontSize}px</Text>
</Descriptions.Item>
<Descriptions.Item label="基础行高 (Line Height)">
<Text code>{tokens.lineHeight || 1.5}</Text>
</Descriptions.Item>
</Descriptions>
</div>
</div>
);
};

View File

@@ -0,0 +1,164 @@
/**
* @name Ant Design System
*
* 基于 Ant Design 官方规范的设计系统演示
* 参考https://ant.design/docs/spec/values-cn
*/
import './style.css';
import React, { useEffect, useState } from 'react';
import { ConfigProvider } from 'antd';
import { ThemeShell, NavGroup, NavItem, MarkdownViewer } from '../../common/ThemeShell';
import tokens from './designToken.json';
// Import Foundations
import { Colors } from './foundations/Colors';
import { TypographySection } from './foundations/Typography';
import { Spacing } from './foundations/Spacing';
import { IconsSection } from './foundations/Icons';
import { Shadows } from './foundations/Shadows';
import { Radius } from './foundations/Radius';
// Import Components
import { ButtonSection } from './components/Button';
import { InputSection } from './components/Input';
import { CardSection } from './components/Card';
// Import Templates
import { LoginTemplate } from './templates/LoginTemplate';
import { DashboardTemplate } from './templates/DashboardTemplate';
// Navigation Groups
const NAV_GROUPS: NavGroup[] = [
{ id: 'docs', title: '说明', order: 1 },
{ id: 'foundation', title: '基础要素', order: 2 },
{ id: 'components', title: '组件', order: 3 },
{ id: 'templates', title: '模板', order: 4 },
];
// Navigation Items
const NAV_ITEMS: NavItem[] = [
{ id: 'design-spec', label: '设计规范 Design Spec', groupId: 'docs' },
{ id: 'colors', label: '色彩 Colors', groupId: 'foundation' },
{ id: 'typography', label: '排版 Typography', groupId: 'foundation' },
{ id: 'spacing', label: '间距 Spacing', groupId: 'foundation' },
{ id: 'icons', label: '图标 Icons', groupId: 'foundation' },
{ id: 'shadows', label: '阴影 Shadows', groupId: 'foundation' },
{ id: 'radius', label: '圆角 Radius', groupId: 'foundation' },
{ id: 'buttons', label: '按钮 Button', groupId: 'components' },
{ id: 'inputs', label: '输入框 Input', groupId: 'components' },
{ id: 'cards', label: '卡片 Card', groupId: 'components' },
{ id: 'login', label: '登录页 Login', groupId: 'templates' },
{ id: 'dashboard', label: '仪表盘 Dashboard', groupId: 'templates' },
];
// ============ Component ============
const Component: React.FC = () => {
const [activeTab, setActiveTab] = useState('design-spec');
const [designSpec, setDesignSpec] = useState<string>('');
const baseTokens = tokens as Record<string, any>;
useEffect(() => {
fetch(new URL('./DESIGN.md', import.meta.url).href)
.then(res => res.text())
.then(text => setDesignSpec(text))
.catch(err => console.error('Failed to load Design Spec:', err));
}, []);
const renderContent = () => {
switch (activeTab) {
case 'design-spec':
return designSpec ? <MarkdownViewer content={designSpec} /> : (
<div className="text-center py-12" style={{ color: 'rgba(0, 0, 0, 0.45)' }}>...</div>
);
case 'colors':
return <Colors tokens={baseTokens} />;
case 'typography':
return <TypographySection tokens={baseTokens} />;
case 'spacing':
return <Spacing tokens={baseTokens} />;
case 'icons':
return <IconsSection tokens={baseTokens} />;
case 'shadows':
return <Shadows tokens={baseTokens} />;
case 'radius':
return <Radius tokens={baseTokens} />;
case 'buttons':
return <ButtonSection tokens={baseTokens} />;
case 'inputs':
return <InputSection tokens={baseTokens} />;
case 'cards':
return <CardSection tokens={baseTokens} />;
case 'login':
return <LoginTemplate />;
case 'dashboard':
return <DashboardTemplate />;
default:
return (
<div className="flex flex-col items-center justify-center h-[60vh] text-center px-4">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-6" style={{ color: 'rgba(0, 0, 0, 0.25)' }}>
<span className="text-2xl font-mono">;</span>
</div>
<h2 className="mt-0 text-xl font-semibold mb-2" style={{ color: 'rgba(0, 0, 0, 0.88)' }}> Work in Progress</h2>
<p style={{ color: 'rgba(0, 0, 0, 0.45)' }}>
<span className="font-medium" style={{ color: 'rgba(0, 0, 0, 0.65)' }}>{activeTab}</span> ...
</p>
</div>
);
}
};
return (
<ConfigProvider
theme={{
token: {
colorPrimary: baseTokens.colorPrimary || '#1677ff',
borderRadius: typeof baseTokens.borderRadius === 'number' ? baseTokens.borderRadius : 6,
fontSize: typeof baseTokens.fontSize === 'number' ? baseTokens.fontSize : 14,
},
}}
>
<ThemeShell
brand={{
name: 'Ant Design',
subtitle: 'Design System',
logoBgColor: '#1677ff',
logoTextColor: '#ffffff',
}}
groups={NAV_GROUPS}
items={NAV_ITEMS}
activeId={activeTab}
onNavigate={setActiveTab}
sidebar={{
defaultOpen: true,
collapsible: true,
width: 256,
}}
className="antd-new-theme"
>
<div className="max-w-5xl mx-auto">
{renderContent()}
</div>
</ThemeShell>
</ConfigProvider>
);
};
export default Component;

View File

@@ -0,0 +1,37 @@
@import "tailwindcss";
.antd-new-theme {
color: rgba(0, 0, 0, 0.88);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
.canvas-shell {
background: #f5f5f5;
}
.canvas-topbar {
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.canvas-nav {
background: #fff;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #e5e7eb;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #d1d5db;
}

View File

@@ -0,0 +1,147 @@
import React from 'react';
import { Card, Row, Col, Statistic, Table, Progress } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined, UserOutlined, ShoppingCartOutlined, DollarOutlined, EyeOutlined } from '@ant-design/icons';
export const DashboardTemplate: React.FC = () => {
const columns = [
{
title: '产品名称',
dataIndex: 'name',
key: 'name',
},
{
title: '销量',
dataIndex: 'sales',
key: 'sales',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<span style={{ color: status === '热销' ? '#52c41a' : '#faad14' }}>
{status}
</span>
),
},
];
const data = [
{ key: '1', name: '产品 A', sales: 1234, status: '热销' },
{ key: '2', name: '产品 B', sales: 987, status: '正常' },
{ key: '3', name: '产品 C', sales: 756, status: '正常' },
{ key: '4', name: '产品 D', sales: 543, status: '热销' },
];
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-2"> Dashboard</h1>
<p className="text-neutral-600"></p>
</div>
<div className="p-8 border border-neutral-200 rounded-lg bg-white shadow-sm">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-neutral-900"></h2>
<p className="text-sm text-neutral-500"> 30 </p>
</div>
<div className="flex gap-2">
<button className="px-3 py-1.5 text-xs rounded-full border border-neutral-200 bg-white/80 text-neutral-600 hover:text-neutral-900"></button>
<button className="px-3 py-1.5 text-xs rounded-full border border-neutral-900 bg-neutral-900 text-white hover:bg-neutral-800"></button>
</div>
</div>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card bordered={true}>
<Statistic
title="总用户数"
value={11280}
prefix={<UserOutlined />}
valueStyle={{ color: '#0f172a' }}
suffix={
<span style={{ fontSize: 14, color: '#52c41a' }}>
<ArrowUpOutlined /> 12%
</span>
}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={true}>
<Statistic
title="总订单数"
value={9362}
prefix={<ShoppingCartOutlined />}
valueStyle={{ color: '#0f172a' }}
suffix={
<span style={{ fontSize: 14, color: '#52c41a' }}>
<ArrowUpOutlined /> 8%
</span>
}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={true}>
<Statistic
title="总收入"
value={93826}
prefix={<DollarOutlined />}
valueStyle={{ color: '#0f172a' }}
suffix={
<span style={{ fontSize: 14, color: '#ff4d4f' }}>
<ArrowDownOutlined /> 3%
</span>
}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={true}>
<Statistic
title="页面浏览量"
value={128450}
prefix={<EyeOutlined />}
valueStyle={{ color: '#0f172a' }}
suffix={
<span style={{ fontSize: 14, color: '#52c41a' }}>
<ArrowUpOutlined /> 15%
</span>
}
/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 24 }}>
<Col xs={24} lg={16}>
<Card title="销售数据" bordered={true}>
<Table columns={columns} dataSource={data} pagination={false} />
</Card>
</Col>
<Col xs={24} lg={8}>
<Card title="完成进度" bordered={true}>
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8 }}> A</div>
<Progress percent={75} status="active" />
</div>
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8 }}> B</div>
<Progress percent={60} />
</div>
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8 }}> C</div>
<Progress percent={90} status="success" />
</div>
<div>
<div style={{ marginBottom: 8 }}> D</div>
<Progress percent={45} />
</div>
</Card>
</Col>
</Row>
</div>
</div>
);
};

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Form, Input, Button, Checkbox, Card } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
export const LoginTemplate: React.FC = () => {
const onFinish = (values: any) => {
console.log('Received values:', values);
};
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold text-neutral-900 mb-2"> Login</h1>
<p className="text-neutral-600"></p>
</div>
<div className="flex items-center justify-center min-h-[520px] bg-[radial-gradient(circle_at_top,_#fff7ed,_#f8fafc_55%)] rounded-lg p-8 border border-neutral-200">
<Card className="w-full max-w-[420px]" bordered={false}>
<div className="text-center mb-6">
<div className="mx-auto mb-4 h-12 w-12 rounded-2xl bg-neutral-900 text-white flex items-center justify-center text-lg font-semibold">
AX
</div>
<h2 className="mt-0 text-2xl font-semibold text-neutral-900 mb-1"></h2>
<p className="text-sm text-neutral-500"></p>
</div>
<Form
name="login"
initialValues={{ remember: true }}
onFinish={onFinish}
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
size="large"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码!' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
size="large"
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox></Checkbox>
</Form.Item>
<a className="float-right text-sm text-neutral-500 hover:text-neutral-800"></a>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block size="large">
</Button>
</Form.Item>
<div className="text-center text-sm text-neutral-500">
<a className="text-neutral-800 hover:text-neutral-900"></a>
</div>
</Form>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,244 @@
# Firecrawl 设计规范
> 本文档定义了 Firecrawl 主题的设计价值、能力边界与使用指南,帮助开发者和 AI 正确理解和应用该设计系统。
## 设计系统价值
### 品牌定位
Firecrawl 是一款面向开发者的爬取与解析工具产品,视觉风格以 **清爽浅色、清晰层级、橙色强调** 为特征,强调效率、可读性与可靠性。主色为活力橙色 `#FA5D19`,形成强烈的行动导向与品牌记忆点。
### 核心价值
1. **高可读性** - 浅色背景与高对比文本保证信息密度下的易读性
2. **聚焦关键操作** - 橙色仅用于关键交互,提高可辨识度
3. **轻量、克制** - 界面元素简洁,避免过度装饰
4. **稳定、可信赖** - 统一的线框与层级系统增强专业感
### 设计原则
| 原则 | 含义 |
|------|------|
| **白底优先** | 页面以浅色背景为主,突出信息内容 |
| **橙色强调** | 主色用于关键按钮与链接 |
| **层级清晰** | 通过卡片、边框、阴影区分结构 |
| **一致节奏** | 以 2/4/8px 的间距网格构建布局节奏 |
---
## 能力边界
### 适合的场景
- 开发者工具与控制台
- API/数据平台后台
- 文档与配置密集的产品
- 轻量业务操作台
### 不适合的场景
- 高度品牌化的视觉创意站点
- 强游戏化或娱乐化界面
- 重度暗色主题产品
---
## 色彩系统
### 主色调 (Primary)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--primary` | `#FA5D19` | 品牌色、按钮、强调元素 |
| `--primary-foreground` | `#FFFFFF` | Primary 上的文字颜色 |
### 背景色 (Background)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--background` | `#F9F9F9` | 页面主背景 |
| `--card` | `#FFFFFF` | 卡片/区块背景 |
| `--popover` | `#FFFFFF` | 悬浮卡片/弹窗背景 |
| `--muted` | `rgba(0,0,0,0.04)` | 输入框/次级背景 |
### 文本色 (Text)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--foreground` | `#262626` | 主要文本 |
| `--muted-foreground` | `rgba(38,38,38,0.64)` | 次要文本 |
| `--subtle` | `rgba(38,38,38,0.48)` | 辅助文本、占位符 |
### 边框色 (Border)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--border` | `#E5E7EB` | 卡片边框 |
| `--border-subtle` | `#EDEDED` | 分割线 |
### 语义色 (Semantic)
| 变量 | 色值 | 用途 |
|------|------|------|
| `--destructive` | `#EF4444` | 错误、危险操作 |
| `--accent` | `#FA5D19` | 强调色(与 primary 一致) |
---
## 字体系统
### 字体族
| 用途 | 字体 | CSS 变量 |
|------|------|---------|
| 主字体 | Suisse | `--font-sans` |
| 等宽字体 | Geist Mono | `--font-mono` |
### 文本样式
| 名称 | 字号 | 字重 | 行高 | 用途 |
|------|------|------|------|------|
| H1 | 28px | 500 | 1.4 | 页面主标题 |
| H2 | 20px | 450 | 1.4 | 区块标题 |
| Body | 16px | 400 | 1.5 | 默认正文 |
| Body Small | 14px | 400 | 1.55 | 次要正文 |
| Label | 14px | 450 | 1.4 | 按钮、标签 |
| Code | 14px | 400 | 1.55 | 代码文本 |
---
## 间距系统
**2/4/8px** 为基础节奏:
| Token | 值 | 用途 |
|-------|-----|------|
| `--spacing-1` | 2px | 细微间隔 |
| `--spacing-2` | 4px | 紧凑间距 |
| `--spacing-3` | 6px | 小间距 |
| `--spacing-4` | 8px | 元素内间距 |
| `--spacing-6` | 12px | 组件间距 |
| `--spacing-8` | 16px | 标准间距 |
| `--spacing-10` | 20px | 区块内间距 |
| `--spacing-12` | 24px | 区块间距 |
| `--spacing-20` | 40px | 页面级间距 |
---
## 圆角系统
Firecrawl 使用中等圆角,强调轻量与亲和:
| Token | 值 | 用途 |
|------|------|------|
| `--radius-sm` | 4px | 标签、输入框 |
| `--radius-md` | 6px | 按钮 |
| `--radius-lg` | 10px | 卡片 |
| `--radius-xl` | 12px | 弹窗 |
| `--radius-2xl` | 16px | 大容器 |
| `--radius-full` | 999px | 胶囊按钮 |
---
## 阴影系统
| 名称 | 值 | 用途 |
|------|-----|------|
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | 轻量卡片 |
| `--shadow-md` | `0 2px 12px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.56)` | 浮层、弹窗 |
---
## 图标系统
- **风格**:线性图标为主,线宽 1.5px
- **尺寸**16/20/24px
- **颜色**:默认 `--muted-foreground`,高亮使用 `--primary`
---
## 组件规范
### Button 按钮
#### Primary Button
```css
background: var(--primary); /* #FA5D19 */
color: var(--primary-foreground); /* #FFFFFF */
border-radius: 6px;
padding: 10px 20px;
font-size: 14px;
font-weight: 450;
```
#### Secondary Button
```css
background: var(--secondary); /* #EDEDED */
color: var(--secondary-foreground); /* #262626 */
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 20px;
```
#### Ghost Button
```css
background: transparent;
color: var(--foreground);
border: 1px solid var(--border);
border-radius: 6px;
```
### Card 卡片
```css
background: var(--card); /* #FFFFFF */
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px;
box-shadow: var(--shadow-sm);
```
### Input 输入框
```css
background: var(--muted); /* rgba(0,0,0,0.04) */
color: var(--foreground);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
```
---
## 使用约束
### 必须遵守
1. **主色节制使用** - 仅用于关键操作或强调信息
2. **保持浅色基调** - 不使用大面积深色背景
3. **间距统一** - 使用 spacing token 组合布局
4. **文本对比度** - 关键文本必须清晰可读
### 建议做法
1. **卡片结构** - 使用卡片拆分信息层级
2. **轻量阴影** - 避免过重的阴影造成视觉噪音
3. **按钮分级** - Primary 只用于最重要操作
### 禁止做法
1. **不要过度使用橙色** - 会削弱强调效果
2. **不要使用纯黑文本** - 与整体轻量风格冲突
3. **不要使用过大圆角** - 与整体简洁风格不符
---
## 文件结构
```
src/themes/firecrawl/
├── globals.css # Tailwind CSS 变量定义
├── index.tsx # 主题演示页
└── DESIGN-SPEC.md # 本文档
```

View File

@@ -0,0 +1,3 @@
{
"name": "Firecrawl Design"
}

View File

@@ -0,0 +1,110 @@
@import "tailwindcss";
/* Firecrawl Design Tokens - Light theme */
:root {
/* Background Colors */
--background: #f9f9f9;
--foreground: #262626;
/* Surface Colors */
--card: #ffffff;
--card-foreground: #262626;
--popover: #ffffff;
--popover-foreground: #262626;
/* Primary - Firecrawl Orange */
--primary: #fa5d19;
--primary-foreground: #ffffff;
/* Secondary */
--secondary: #ededed;
--secondary-foreground: #262626;
/* Muted */
--muted: rgba(0, 0, 0, 0.04);
--muted-foreground: rgba(38, 38, 38, 0.64);
/* Accent */
--accent: #fa5d19;
--accent-foreground: #ffffff;
/* Subtle */
--subtle: rgba(38, 38, 38, 0.48);
/* Destructive */
--destructive: #ef4444;
--destructive-foreground: #ffffff;
/* Border */
--border: #e5e7eb;
--border-subtle: #ededed;
--input: rgba(0, 0, 0, 0.1);
--ring: #fa5d19;
/* Radius */
--radius: 0.5rem;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-full: 999px;
/* Spacing */
--spacing-1: 2px;
--spacing-2: 4px;
--spacing-3: 6px;
--spacing-4: 8px;
--spacing-6: 12px;
--spacing-8: 16px;
--spacing-10: 20px;
--spacing-12: 24px;
--spacing-20: 40px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 12px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.56);
}
@theme inline {
--font-sans: "Suisse", "Suisse Fallback", ui-sans-serif, system-ui, sans-serif;
--font-mono: "GeistMono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-subtle: var(--subtle);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-border-subtle: var(--border-subtle);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: var(--radius-sm);
--radius-md: var(--radius-md);
--radius-lg: var(--radius-lg);
--radius-xl: var(--radius-xl);
--radius-2xl: var(--radius-2xl);
--radius-full: var(--radius-full);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,596 @@
import './globals.css';
import React, { useEffect, useState } from 'react';
import { ThemeShell, NavGroup, NavItem, MarkdownViewer } from '../../common/ThemeShell';
/**
* Firecrawl 主题演示页
*/
const groups: NavGroup[] = [
{ id: 'docs', title: '说明', order: 1 },
{ id: 'foundation', title: '基础', order: 2 },
{ id: 'components', title: '组件', order: 3 },
];
const items: NavItem[] = [
{ id: 'design-spec', label: '设计规范 Design Spec', groupId: 'docs' },
{ id: 'colors', label: '色彩系统', groupId: 'foundation' },
{ id: 'typography', label: '字体系统', groupId: 'foundation' },
{ id: 'spacing', label: '间距', groupId: 'foundation' },
{ id: 'radius', label: '圆角', groupId: 'foundation' },
{ id: 'shadows', label: '阴影', groupId: 'foundation' },
{ id: 'icons', label: '图标', groupId: 'foundation' },
{ id: 'buttons', label: '按钮', groupId: 'components' },
{ id: 'cards', label: '卡片', groupId: 'components' },
{ id: 'inputs', label: '输入框', groupId: 'components' },
];
function ColorSwatch({
name,
color,
hex,
textDark = false,
light = false
}: {
name: string;
color: string;
hex: string;
textDark?: boolean;
light?: boolean;
}) {
return (
<div style={{ textAlign: 'center' }}>
<div style={{
width: '80px',
height: '80px',
backgroundColor: color,
borderRadius: '8px',
marginBottom: '8px',
border: light ? '1px solid var(--border)' : 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{textDark && (
<span style={{ fontSize: '12px', color: '#262626', fontWeight: 500 }}>Aa</span>
)}
</div>
<div style={{ fontSize: '12px', fontWeight: 500, marginBottom: '2px' }}>{name}</div>
<div style={{ fontSize: '11px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>{hex}</div>
</div>
);
}
function ColorsSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Colors)</h2>
<div style={{ marginBottom: '32px' }}>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Primary
</h3>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<ColorSwatch name="primary" color="var(--primary)" hex="#FA5D19" />
<ColorSwatch name="primary-foreground" color="var(--primary-foreground)" hex="#FFFFFF" light />
</div>
</div>
<div style={{ marginBottom: '32px' }}>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Background & Surface
</h3>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<ColorSwatch name="background" color="var(--background)" hex="#F9F9F9" light />
<ColorSwatch name="card" color="var(--card)" hex="#FFFFFF" light />
<ColorSwatch name="popover" color="var(--popover)" hex="#FFFFFF" light />
<ColorSwatch name="muted" color="var(--muted)" hex="rgba(0,0,0,0.04)" light />
</div>
</div>
<div style={{ marginBottom: '32px' }}>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Text Colors
</h3>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<ColorSwatch name="foreground" color="var(--foreground)" hex="#262626" textDark />
<ColorSwatch name="muted-foreground" color="var(--muted-foreground)" hex="rgba(38,38,38,0.64)" textDark />
<ColorSwatch name="subtle" color="var(--subtle)" hex="rgba(38,38,38,0.48)" textDark />
</div>
</div>
<div>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Semantic Colors
</h3>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<ColorSwatch name="destructive" color="var(--destructive)" hex="#EF4444" />
<ColorSwatch name="accent" color="var(--accent)" hex="#FA5D19" />
</div>
</div>
</div>
);
}
function TypographySection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Typography)</h2>
<div style={{
backgroundColor: 'var(--card)',
borderRadius: '12px',
padding: '24px',
border: '1px solid var(--border)',
boxShadow: 'var(--shadow-sm)'
}}>
<div style={{ marginBottom: '24px' }}>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>H1 / 28px / 500</span>
<p style={{ fontSize: '28px', fontWeight: 500, lineHeight: 1.4 }}>Firecrawl UI Spec</p>
</div>
<div style={{ marginBottom: '24px' }}>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>H2 / 20px / 450</span>
<p style={{ fontSize: '20px', fontWeight: 450, lineHeight: 1.4 }}>Section Heading</p>
</div>
<div style={{ marginBottom: '24px' }}>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>Body / 16px / 400</span>
<p style={{ fontSize: '16px', fontWeight: 400, lineHeight: 1.5 }}>The quick brown fox jumps over the lazy dog. </p>
</div>
<div style={{ marginBottom: '24px' }}>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>Body Small / 14px / 400</span>
<p style={{ fontSize: '14px', fontWeight: 400, lineHeight: 1.55, color: 'var(--muted-foreground)' }}></p>
</div>
<div style={{ marginBottom: '24px' }}>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>Label / 14px / 450</span>
<p style={{ fontSize: '14px', fontWeight: 450, lineHeight: 1.4, letterSpacing: '0.01em' }}>Label Text</p>
</div>
<div>
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>Code / 14px / Geist Mono</span>
<p style={{ fontSize: '14px', fontWeight: 400, lineHeight: 1.55, fontFamily: 'var(--font-mono)' }}>curl -X POST https://api.firecrawl.dev</p>
</div>
</div>
</div>
);
}
function SpacingSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Spacing)</h2>
<div>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
2/4/8px Rhythm
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{[
{ name: '--spacing-1', value: 'var(--spacing-1)' },
{ name: '--spacing-2', value: 'var(--spacing-2)' },
{ name: '--spacing-3', value: 'var(--spacing-3)' },
{ name: '--spacing-4', value: 'var(--spacing-4)' },
{ name: '--spacing-6', value: 'var(--spacing-6)' },
{ name: '--spacing-8', value: 'var(--spacing-8)' },
{ name: '--spacing-10', value: 'var(--spacing-10)' },
{ name: '--spacing-12', value: 'var(--spacing-12)' },
{ name: '--spacing-20', value: 'var(--spacing-20)' },
].map(({ name, value }) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: value,
height: '14px',
backgroundColor: 'var(--primary)',
borderRadius: '2px'
}} />
<span style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>
{name}: {value}
</span>
</div>
))}
</div>
</div>
</div>
);
}
function RadiusSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Radius)</h2>
<div>
<h3 style={{ fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground)', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Border Radius Tokens
</h3>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
{[
{ name: '--radius-sm', value: 'var(--radius-sm)' },
{ name: '--radius-md', value: 'var(--radius-md)' },
{ name: '--radius-lg', value: 'var(--radius-lg)' },
{ name: '--radius-xl', value: 'var(--radius-xl)' },
{ name: '--radius-2xl', value: 'var(--radius-2xl)' },
{ name: '--radius-full', value: 'var(--radius-full)' },
].map(({ name, value }) => (
<div key={name} style={{ textAlign: 'center' }}>
<div style={{
width: '56px',
height: '56px',
backgroundColor: 'var(--primary)',
borderRadius: value,
marginBottom: '8px'
}} />
<div style={{ fontSize: '11px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>
{name}
</div>
<div style={{ fontSize: '12px', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{value}
</div>
</div>
))}
</div>
</div>
</div>
);
}
function ShadowsSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Shadows)</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '20px' }}>
{[
{ name: '--shadow-sm', value: 'var(--shadow-sm)', desc: '轻量卡片、列表' },
{ name: '--shadow-md', value: 'var(--shadow-md)', desc: '浮层、弹窗' },
].map(({ name, value, desc }) => (
<div key={name} style={{
backgroundColor: 'var(--card)',
borderRadius: '10px',
border: '1px solid var(--border)',
padding: '18px',
boxShadow: value
}}>
<div style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)', marginBottom: '8px' }}>
{name}
</div>
<div style={{ fontSize: '14px', fontWeight: 450, marginBottom: '6px' }}>{desc}</div>
<div style={{ fontSize: '12px', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{value}
</div>
</div>
))}
</div>
</div>
);
}
function IconsSection() {
const iconStyle: React.CSSProperties = {
width: '24px',
height: '24px',
color: 'var(--muted-foreground)'
};
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Icons)</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px' }}>
{[
{ name: 'Spark', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 3l2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3z" />
</svg>
) },
{ name: 'Search', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" />
</svg>
) },
{ name: 'Terminal', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 6l6 6-6 6" />
<path d="M12 18h8" />
</svg>
) },
{ name: 'Globe', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3a12 12 0 0 1 0 18a12 12 0 0 1 0-18z" />
</svg>
) },
{ name: 'Code', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 7L3 12l5 5" />
<path d="M16 7l5 5-5 5" />
</svg>
) },
{ name: 'Webhook', svg: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 12a4 4 0 0 1 4-4h3" />
<path d="M20 12a4 4 0 0 0-4-4h-3" />
<path d="M4 12a4 4 0 0 0 4 4h3" />
<path d="M20 12a4 4 0 0 1-4 4h-3" />
<circle cx="8" cy="8" r="1.5" />
<circle cx="16" cy="8" r="1.5" />
<circle cx="8" cy="16" r="1.5" />
<circle cx="16" cy="16" r="1.5" />
</svg>
) },
].map(({ name, svg }) => (
<div key={name} style={{
backgroundColor: 'var(--card)',
borderRadius: '12px',
border: '1px solid var(--border)',
padding: '16px',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<div style={iconStyle}>{svg}</div>
<div>
<div style={{ fontSize: '14px', fontWeight: 450 }}>{name}</div>
<div style={{ fontSize: '11px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>24px / 1.5px</div>
</div>
</div>
))}
</div>
</div>
);
}
function ButtonsSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Buttons)</h2>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
<button style={{
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
padding: '10px 20px',
borderRadius: '6px',
border: 'none',
fontSize: '14px',
fontWeight: 450,
cursor: 'pointer',
boxShadow: '0 2px 4px rgba(250, 93, 25, 0.12)'
}}>
Primary Button
</button>
<button style={{
backgroundColor: 'var(--secondary)',
color: 'var(--secondary-foreground)',
padding: '10px 20px',
borderRadius: '6px',
border: '1px solid var(--border)',
fontSize: '14px',
fontWeight: 450,
cursor: 'pointer'
}}>
Secondary Button
</button>
<button style={{
backgroundColor: 'transparent',
color: 'var(--foreground)',
padding: '10px 20px',
borderRadius: '6px',
border: '1px solid var(--border)',
fontSize: '14px',
fontWeight: 450,
cursor: 'pointer'
}}>
Ghost Button
</button>
<button style={{
backgroundColor: 'var(--destructive)',
color: 'var(--destructive-foreground)',
padding: '10px 20px',
borderRadius: '6px',
border: 'none',
fontSize: '14px',
fontWeight: 450,
cursor: 'pointer'
}}>
Destructive
</button>
</div>
</div>
);
}
function CardsSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Cards)</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '24px' }}>
<div style={{
backgroundColor: 'var(--card)',
borderRadius: '10px',
padding: '20px',
border: '1px solid var(--border)',
boxShadow: 'var(--shadow-sm)'
}}>
<h3 style={{ fontSize: '18px', fontWeight: 500, marginBottom: '8px' }}>Card Title</h3>
<p style={{ fontSize: '14px', color: 'var(--muted-foreground)', lineHeight: 1.6 }}>
使 card
</p>
</div>
<div style={{
backgroundColor: 'var(--card)',
borderRadius: '12px',
padding: '20px',
border: '1px solid var(--border)',
boxShadow: 'var(--shadow-md)'
}}>
<h3 style={{ fontSize: '18px', fontWeight: 500, marginBottom: '8px' }}>Elevated Card</h3>
<p style={{ fontSize: '14px', color: 'var(--muted-foreground)', lineHeight: 1.6 }}>
使
</p>
</div>
<div style={{
backgroundColor: 'var(--card)',
borderRadius: '10px',
padding: '20px',
border: '1px solid var(--border)',
borderLeftColor: 'var(--primary)',
borderLeftWidth: '4px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: 500, marginBottom: '8px', color: 'var(--primary)' }}>Accent Card</h3>
<p style={{ fontSize: '14px', color: 'var(--muted-foreground)', lineHeight: 1.6 }}>
使
</p>
</div>
</div>
</div>
);
}
function InputsSection() {
return (
<div>
<h2 style={{ fontSize: '24px', fontWeight: 500, marginTop: 0, marginBottom: '24px' }}> (Inputs)</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '420px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: 450, marginBottom: '6px' }}>
Default Input
</label>
<input
type="text"
placeholder="请输入内容..."
style={{
width: '100%',
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid var(--border)',
fontSize: '14px',
outline: 'none'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: 450, marginBottom: '6px' }}>
Textarea
</label>
<textarea
placeholder="请输入多行内容..."
rows={3}
style={{
width: '100%',
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid var(--border)',
fontSize: '14px',
outline: 'none',
resize: 'vertical'
}}
/>
</div>
</div>
</div>
);
}
function renderContent(activeId: string) {
switch (activeId) {
case 'design-spec':
return null;
case 'colors':
return <ColorsSection />;
case 'typography':
return <TypographySection />;
case 'spacing':
return <SpacingSection />;
case 'radius':
return <RadiusSection />;
case 'shadows':
return <ShadowsSection />;
case 'icons':
return <IconsSection />;
case 'buttons':
return <ButtonsSection />;
case 'cards':
return <CardsSection />;
case 'inputs':
return <InputsSection />;
default:
return <ColorsSection />;
}
}
function Component() {
const [activeId, setActiveId] = useState('design-spec');
const [designSpec, setDesignSpec] = useState<string>('');
useEffect(() => {
fetch(new URL('./DESIGN.md', import.meta.url).href)
.then(res => res.text())
.then(text => setDesignSpec(text))
.catch(err => console.error('Failed to load Design Spec:', err));
}, []);
return (
<ThemeShell
brand={{
name: 'Firecrawl',
subtitle: 'Design System',
logoBgColor: '#fa5d19',
logoTextColor: '#ffffff',
}}
groups={groups}
items={items}
activeId={activeId}
onNavigate={setActiveId}
sidebar={{
width: 240,
defaultOpen: true,
collapsible: true,
}}
theme={{
mode: 'light',
colors: {
bgPrimary: '#ffffff',
bgSecondary: '#f9f9f9',
bgTertiary: '#ededed',
bgHover: '#f3f3f3',
bgActive: '#e7e7e7',
textPrimary: '#262626',
textSecondary: 'rgba(38, 38, 38, 0.72)',
textTertiary: 'rgba(38, 38, 38, 0.56)',
textMuted: 'rgba(38, 38, 38, 0.4)',
border: '#e5e7eb',
borderLight: '#ededed',
activeIndicator: '#fa5d19',
},
}}
style={{
fontFamily: 'var(--font-sans)',
}}
>
{activeId === 'design-spec' ? (
designSpec ? <MarkdownViewer content={designSpec} /> : (
<div style={{ textAlign: 'center', padding: '48px 0', color: 'var(--muted-foreground)' }}>...</div>
)
) : (
renderContent(activeId)
)}
</ThemeShell>
);
}
export default Component;

View File

@@ -0,0 +1,65 @@
{
"name": "TRAE Design",
"description": "TRAE 智能 AI IDE 设计系统 - 深色主题,以亮绿色为品牌主色调,强调科技感与专业性",
"token": {
"colorPrimary": "var(--primary)",
"colorBgBase": "var(--background)",
"colorTextBase": "var(--foreground)",
"colorBgContainer": "var(--card)",
"colorBgElevated": "var(--popover)",
"colorBgLayout": "var(--muted)",
"colorTextSecondary": "var(--muted-foreground)",
"colorTextTertiary": "var(--subtle)",
"colorBorder": "var(--border)",
"colorBorderSecondary": "var(--border-subtle)",
"colorError": "var(--destructive)",
"colorSuccess": "var(--primary)",
"borderRadius": 4,
"borderRadiusSM": 2,
"borderRadiusLG": 6,
"fontFamily": "Inter, PingFang SC, system-ui, sans-serif",
"fontFamilyCode": "JetBrains Mono, monospace"
},
"colors": {
"primary": "#32F08C",
"background": "#0A0B0D",
"foreground": "#F5F9FE",
"card": "#121314",
"popover": "#171A1C",
"muted": "#1E1F23",
"mutedForeground": "#A6AAB5",
"subtle": "#787D87",
"border": "rgba(255, 255, 255, 0.2)",
"borderSubtle": "rgba(237, 239, 242, 0.13)",
"destructive": "#EF4444"
},
"typography": {
"fontSans": "Inter, PingFang SC, system-ui, sans-serif",
"fontMono": "JetBrains Mono, monospace",
"sizes": {
"display": "72px",
"h1": "56px",
"h2": "40px",
"h3": "24px",
"h4": "20px",
"bodyLarge": "18px",
"body": "16px",
"bodySmall": "14px"
}
},
"spacing": {
"1": "4px",
"2": "8px",
"3": "12px",
"4": "16px",
"6": "24px",
"8": "32px",
"12": "48px"
},
"radius": {
"sm": "2px",
"md": "4px",
"lg": "6px",
"xl": "8px"
}
}

View File

@@ -0,0 +1,124 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* TRAE Design Tokens - 深色主题配色 */
:root {
/* Background Colors */
--background: #0a0b0d;
--foreground: #f5f9fe;
/* Surface Colors */
--card: #121314;
--card-foreground: #f5f9fe;
--popover: #171a1c;
--popover-foreground: #f5f9fe;
/* Primary - TRAE Brand Green */
--primary: #32f08c;
--primary-foreground: #0a0b0d;
/* Secondary */
--secondary: rgba(237, 239, 242, 0.18);
--secondary-foreground: #f5f9fe;
/* Muted */
--muted: #1e1f23;
--muted-foreground: #a6aab5;
/* Accent */
--accent: #32f08c;
--accent-foreground: #0a0b0d;
/* Subtle - 更浅的文本 */
--subtle: #787d87;
/* Destructive */
--destructive: #ef4444;
--destructive-foreground: #fef2f2;
/* Border */
--border: rgba(255, 255, 255, 0.2);
--border-subtle: rgba(237, 239, 242, 0.13);
--input: rgba(255, 255, 255, 0.2);
--ring: #32f08c;
/* Chart Colors */
--chart-1: #32f08c;
--chart-2: #a6aab5;
--chart-3: #787d87;
--chart-4: #171a1c;
--chart-5: #121314;
/* Radius - TRAE uses smaller radius */
--radius: 0.25rem;
/* Sidebar */
--sidebar: #121314;
--sidebar-foreground: #f5f9fe;
--sidebar-primary: #32f08c;
--sidebar-primary-foreground: #0a0b0d;
--sidebar-accent: #171a1c;
--sidebar-accent-foreground: #f5f9fe;
--sidebar-border: rgba(255, 255, 255, 0.2);
--sidebar-ring: #32f08c;
}
/* 移除 .dark 变体,因为 TRAE 默认就是深色主题 */
@theme inline {
/* 使用 Inter 和 JetBrains Mono 字体 */
--font-sans: "Inter", "PingFang SC", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-subtle: var(--subtle);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-border-subtle: var(--border-subtle);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* TRAE 使用更小的圆角 */
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 6px;
--radius-xl: 8px;
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,357 @@
import './globals.css';
import React from 'react';
import { ConfigProvider } from 'antd';
import tokens from './designToken.json';
const themeTokens = (tokens as { token?: Record<string, any> }).token || {};
const containerStyle: React.CSSProperties = {
maxWidth: 1152,
margin: '0 auto',
padding: '0 24px',
};
const sectionStyle: React.CSSProperties = {
padding: '48px 0',
borderBottom: '1px solid var(--border-subtle)',
};
const sectionTitleStyle: React.CSSProperties = {
fontSize: '24px',
fontWeight: 600,
color: 'var(--foreground)',
};
const sectionSubtitleStyle: React.CSSProperties = {
fontSize: '12px',
color: 'var(--primary)',
fontFamily: 'var(--font-mono)',
marginBottom: '6px',
};
const chipStyle: React.CSSProperties = {
fontSize: '12px',
color: 'var(--subtle)',
fontFamily: 'var(--font-mono)',
};
function SectionHeader({ title, subtitle }: { title: string; subtitle: string }) {
return (
<div style={{ marginBottom: '32px' }}>
<div style={sectionSubtitleStyle}>{subtitle}</div>
<div style={sectionTitleStyle}>{title}</div>
</div>
);
}
function ColorSwatch({
name,
value,
hex,
textDark = false,
}: {
name: string;
value: string;
hex: string;
textDark?: boolean;
}) {
return (
<div style={{ textAlign: 'left' }}>
<div
style={{
width: '140px',
height: '88px',
borderRadius: '10px',
backgroundColor: value,
border: '1px solid var(--border-subtle)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '10px',
}}
>
<span style={{ fontSize: '12px', fontWeight: 600, color: textDark ? '#0a0b0d' : 'var(--foreground)' }}>
Aa
</span>
</div>
<div style={{ fontSize: '13px', fontWeight: 500, color: 'var(--foreground)' }}>{name}</div>
<div style={{ fontSize: '12px', color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>{hex}</div>
</div>
);
}
function TypographyRow({
label,
size,
weight,
lineHeight,
sample,
style,
}: {
label: string;
size: string;
weight: number;
lineHeight: string;
sample: string;
style?: React.CSSProperties;
}) {
return (
<div style={{ marginBottom: '20px' }}>
<div style={chipStyle}>{label} / {size} / {weight} / {lineHeight}</div>
<div style={{ fontSize: size, fontWeight: weight, lineHeight, color: 'var(--foreground)', ...style }}>
{sample}
</div>
</div>
);
}
const Component: React.FC = () => {
return (
<ConfigProvider theme={{ token: themeTokens }}>
<main style={{ minHeight: '100vh', background: 'var(--background)', color: 'var(--foreground)', fontFamily: 'var(--font-sans)' }}>
<header style={{ borderBottom: '1px solid var(--border-subtle)', padding: '24px 0' }}>
<div style={containerStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '32px',
height: '32px',
borderRadius: '8px',
background: 'var(--primary)',
color: 'var(--primary-foreground)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'var(--font-mono)',
fontSize: '14px',
fontWeight: 700,
}}>
T
</div>
<div style={{ fontSize: '18px', fontWeight: 600 }}>TRAE Design System</div>
</div>
</div>
</header>
<section style={{ ...sectionStyle, paddingTop: '64px', paddingBottom: '64px' }}>
<div style={containerStyle}>
<div style={{ fontSize: '48px', fontWeight: 600, lineHeight: 1.1 }}>
<span style={{ color: 'var(--primary)' }}>&</span> Design Tokens
</div>
<p style={{ marginTop: '16px', fontSize: '18px', color: 'var(--muted-foreground)', maxWidth: '620px' }}>
TRAE AI IDE 绿
</p>
</div>
</section>
<section style={sectionStyle}>
<div style={containerStyle}>
<SectionHeader title="色彩系统" subtitle="Color System" />
<div style={{ display: 'grid', gap: '28px' }}>
<div>
<div style={chipStyle}>Primary</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px', marginTop: '12px' }}>
<ColorSwatch name="Primary" value="var(--primary)" hex="#32F08C" />
<ColorSwatch name="Primary Foreground" value="var(--primary-foreground)" hex="#0A0B0D" textDark />
</div>
</div>
<div>
<div style={chipStyle}>Background & Surface</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px', marginTop: '12px' }}>
<ColorSwatch name="Background" value="var(--background)" hex="#0A0B0D" />
<ColorSwatch name="Surface" value="var(--card)" hex="#121314" />
<ColorSwatch name="Surface Elevated" value="var(--popover)" hex="#171A1C" />
<ColorSwatch name="Surface Subtle" value="var(--muted)" hex="#1E1F23" />
</div>
</div>
<div>
<div style={chipStyle}>Text</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px', marginTop: '12px' }}>
<ColorSwatch name="Foreground" value="var(--foreground)" hex="#F5F9FE" textDark />
<ColorSwatch name="Muted Foreground" value="var(--muted-foreground)" hex="#A6AAB5" textDark />
<ColorSwatch name="Subtle Foreground" value="var(--subtle)" hex="#787D87" textDark />
</div>
</div>
<div>
<div style={chipStyle}>Border & Semantic</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px', marginTop: '12px' }}>
<ColorSwatch name="Border" value="var(--border)" hex="rgba(255,255,255,0.2)" />
<ColorSwatch name="Border Subtle" value="var(--border-subtle)" hex="rgba(237,239,242,0.13)" />
<ColorSwatch name="Destructive" value="var(--destructive)" hex="#EF4444" />
</div>
</div>
</div>
</div>
</section>
<section style={sectionStyle}>
<div style={containerStyle}>
<SectionHeader title="字体系统" subtitle="Typography" />
<div style={{
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: '14px',
padding: '28px',
boxShadow: '0 3px 9px rgba(0,0,0,0.08)',
}}>
<TypographyRow label="Display" size="72px" weight={500} lineHeight="1.1" sample="TRAE Design System" />
<TypographyRow label="H1" size="56px" weight={600} lineHeight="1.2" sample="Intelligent AI IDE" />
<TypographyRow label="H2" size="40px" weight={600} lineHeight="1.2" sample="Section Heading" />
<TypographyRow label="H3" size="24px" weight={600} lineHeight="1.3" sample="Card Title" />
<TypographyRow label="Body Large" size="18px" weight={500} lineHeight="1.6" sample="TRAE 通过智能协作提升开发效率。" />
<TypographyRow label="Body" size="16px" weight={400} lineHeight="1.6" sample="The quick brown fox jumps over the lazy dog." />
<TypographyRow label="Body Small" size="14px" weight={400} lineHeight="1.6" sample="辅助说明文本与注释内容。" style={{ color: 'var(--muted-foreground)' }} />
<TypographyRow label="Label" size="14px" weight={500} lineHeight="1.2" sample="Button Label" />
<TypographyRow label="Code" size="15px" weight={500} lineHeight="1.2" sample={'git commit -m "init"'} style={{ fontFamily: 'var(--font-mono)' }} />
</div>
</div>
</section>
<section style={sectionStyle}>
<div style={containerStyle}>
<SectionHeader title="间距系统" subtitle="Spacing" />
<div style={{ display: 'grid', gap: '14px' }}>
{[
{ name: 'spacing-1', value: '4px' },
{ name: 'spacing-2', value: '8px' },
{ name: 'spacing-3', value: '12px' },
{ name: 'spacing-4', value: '16px' },
{ name: 'spacing-6', value: '24px' },
{ name: 'spacing-8', value: '32px' },
{ name: 'spacing-12', value: '48px' },
{ name: 'spacing-25', value: '100px' },
].map(item => (
<div key={item.name} style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: item.value, height: '12px', borderRadius: '4px', background: 'var(--primary)' }} />
<span style={chipStyle}>{item.name}: {item.value}</span>
</div>
))}
</div>
</div>
</section>
<section style={sectionStyle}>
<div style={containerStyle}>
<SectionHeader title="按钮规范" subtitle="Buttons" />
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<button
style={{
background: 'var(--primary)',
color: 'var(--primary-foreground)',
borderRadius: '4px',
padding: '10px 24px',
border: 'none',
fontWeight: 600,
}}
>
Primary Button
</button>
<button
style={{
background: 'var(--secondary)',
color: 'var(--foreground)',
borderRadius: '4px',
padding: '10px 24px',
border: '1px solid var(--border)',
fontWeight: 600,
}}
>
Secondary Button
</button>
<button
style={{
background: 'transparent',
color: 'var(--muted-foreground)',
borderRadius: '4px',
padding: '10px 24px',
border: '1px solid var(--border)',
fontWeight: 500,
}}
>
Ghost Button
</button>
</div>
</div>
</section>
<section style={{ ...sectionStyle, borderBottom: 'none' }}>
<div style={containerStyle}>
<SectionHeader title="组件示例" subtitle="Components" />
<div style={{ display: 'grid', gap: '24px', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))' }}>
<div style={{
background: 'var(--card)',
borderRadius: '12px',
border: '1px solid var(--border)',
padding: '20px',
}}>
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '6px' }}> Card</div>
<div style={{ fontSize: '14px', color: 'var(--muted-foreground)', marginBottom: '14px' }}>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<span style={{ padding: '6px 10px', background: 'var(--muted)', borderRadius: '6px', fontSize: '12px' }}>Tag</span>
<span style={{ padding: '6px 10px', background: 'var(--muted)', borderRadius: '6px', fontSize: '12px' }}>Status</span>
</div>
</div>
<div style={{
background: 'var(--card)',
borderRadius: '12px',
border: '1px solid var(--border)',
padding: '20px',
}}>
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '12px' }}> Input</div>
<input
style={{
width: '100%',
background: 'var(--muted)',
border: '1px solid var(--border)',
color: 'var(--foreground)',
padding: '10px 12px',
borderRadius: '8px',
marginBottom: '12px',
}}
placeholder="输入内容..."
/>
<div style={{ fontSize: '12px', color: 'var(--subtle)' }}></div>
</div>
<div style={{
background: 'var(--popover)',
borderRadius: '12px',
border: '1px solid var(--border)',
padding: '20px',
}}>
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '12px' }}> Code</div>
<div style={{
background: 'var(--background)',
borderRadius: '10px',
padding: '12px',
fontFamily: 'var(--font-mono)',
fontSize: '12px',
color: 'var(--muted-foreground)',
}}>
npx create-axhub-app
</div>
</div>
</div>
</div>
</section>
<footer style={{ borderTop: '1px solid var(--border-subtle)', padding: '32px 0' }}>
<div style={containerStyle}>
<div style={{ fontSize: '12px', color: 'var(--muted-foreground)' }}>
TRAE Design System v1.0 · Built with Design Tokens
</div>
</div>
</footer>
</main>
</ConfigProvider>
);
};
export default Component;

31
axhub-make/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,31 @@
/// <reference types="vite/client" />
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.woff2' {
const src: string;
export default src;
}