Add frontend component library and UI development tasks

- Create Tailwind CSS configuration with design tokens from UIDesignSpec
- Create globals.css with CSS variables and component styles
- Add React component library:
  - UI components: Button, Card, Tag, Input, Select, ProgressBar, Modal
  - Navigation: BottomNav, Sidebar, StatusBar
  - Layout: MobileLayout, DesktopLayout
- Add constants for colors, icons, and layout
- Update tasks.md with 31 UI development tasks linked to design node IDs
- Configure package.json, tsconfig.json, and postcss.config.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-02-03 17:44:22 +08:00
parent dd06502004
commit f166c04422
22 changed files with 2509 additions and 1 deletions

View File

@ -0,0 +1,22 @@
/**
* SmartAudit
* UIDesignSpec.md
*/
// UI 基础组件
export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from './ui/Button';
export { Card, CardHeader, CardTitle, CardContent, CardFooter, type CardProps } from './ui/Card';
export { Tag, SuccessTag, PendingTag, WarningTag, ErrorTag, type TagProps, type TagStatus } from './ui/Tag';
export { Input, SearchInput, PasswordInput, type InputProps } from './ui/Input';
export { Select, type SelectProps, type SelectOption } from './ui/Select';
export { ProgressBar, CircularProgress, type ProgressBarProps, type CircularProgressProps } from './ui/ProgressBar';
export { Modal, ConfirmModal, type ModalProps, type ConfirmModalProps } from './ui/Modal';
// 导航组件
export { BottomNav, type BottomNavProps, type NavItem } from './navigation/BottomNav';
export { Sidebar, type SidebarProps, type SidebarItem, type SidebarSection } from './navigation/Sidebar';
export { StatusBar, type StatusBarProps } from './navigation/StatusBar';
// 布局组件
export { MobileLayout, type MobileLayoutProps } from './layout/MobileLayout';
export { DesktopLayout, type DesktopLayoutProps } from './layout/DesktopLayout';

View File

@ -0,0 +1,61 @@
/**
* DesktopLayout
* 设计稿参考: UIDesignSpec.md 3.2
* 尺寸: 1440x900260px
*/
import React from 'react';
import { Sidebar, SidebarSection } from '../navigation/Sidebar';
export interface DesktopLayoutProps {
children: React.ReactNode;
logo?: React.ReactNode;
sidebarSections: SidebarSection[];
activeNavId: string;
onNavItemClick?: (id: string) => void;
sidebarFooter?: React.ReactNode;
headerContent?: React.ReactNode;
className?: string;
contentClassName?: string;
}
export const DesktopLayout: React.FC<DesktopLayoutProps> = ({
children,
logo,
sidebarSections,
activeNavId,
onNavItemClick,
sidebarFooter,
headerContent,
className = '',
contentClassName = '',
}) => {
return (
<div className={`min-h-screen bg-bg-page ${className}`}>
{/* Sidebar */}
<Sidebar
logo={logo}
sections={sidebarSections}
activeId={activeNavId}
onItemClick={onNavItemClick}
footer={sidebarFooter}
/>
{/* Main Content */}
<div className="ml-sidebar">
{/* Header (optional) */}
{headerContent && (
<header className="px-8 py-4 border-b border-border-subtle bg-bg-page sticky top-0 z-10">
{headerContent}
</header>
)}
{/* Content Area */}
<main className={`p-8 ${contentClassName}`}>
{children}
</main>
</div>
</div>
);
};
export default DesktopLayout;

View File

@ -0,0 +1,66 @@
/**
* MobileLayout
* 设计稿参考: UIDesignSpec.md 3.1
* 尺寸: 402x874
*/
import React from 'react';
import { StatusBar } from '../navigation/StatusBar';
import { BottomNav, NavItem } from '../navigation/BottomNav';
export interface MobileLayoutProps {
children: React.ReactNode;
navItems?: NavItem[];
activeNavId?: string;
onNavItemClick?: (id: string) => void;
showStatusBar?: boolean;
showBottomNav?: boolean;
className?: string;
contentClassName?: string;
}
export const MobileLayout: React.FC<MobileLayoutProps> = ({
children,
navItems = [],
activeNavId = '',
onNavItemClick,
showStatusBar = true,
showBottomNav = true,
className = '',
contentClassName = '',
}) => {
return (
<div
className={`
min-h-screen bg-bg-page
flex flex-col
${className}
`}
>
{/* Status Bar */}
{showStatusBar && <StatusBar />}
{/* Content Area */}
<main
className={`
flex-1 overflow-y-auto
px-6 py-4
${showBottomNav ? 'pb-[99px]' : ''}
${contentClassName}
`}
>
{children}
</main>
{/* Bottom Navigation */}
{showBottomNav && navItems.length > 0 && (
<BottomNav
items={navItems}
activeId={activeNavId}
onItemClick={onNavItemClick}
/>
)}
</div>
);
};
export default MobileLayout;

View File

@ -0,0 +1,81 @@
/**
* BottomNav ()
* 设计稿参考: UIDesignSpec.md 3.6
*/
import React from 'react';
import { LucideIcon } from 'lucide-react';
export interface NavItem {
id: string;
label: string;
icon: LucideIcon;
href?: string;
badge?: number;
}
export interface BottomNavProps {
items: NavItem[];
activeId: string;
onItemClick?: (id: string) => void;
className?: string;
}
export const BottomNav: React.FC<BottomNavProps> = ({
items,
activeId,
onItemClick,
className = '',
}) => {
return (
<nav
className={`
fixed bottom-0 left-0 right-0 z-bottom-nav
flex justify-around items-center
h-bottom-nav px-[21px] py-3
safe-area-bottom
${className}
`}
style={{
background: 'linear-gradient(180deg, transparent 0%, #0B0B0E 50%)',
}}
>
{items.map((item) => {
const isActive = item.id === activeId;
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => onItemClick?.(item.id)}
className={`
flex flex-col items-center gap-1
transition-colors duration-200
${isActive ? 'text-accent-indigo' : 'text-text-secondary'}
`}
>
<div className="relative">
<Icon size={24} />
{item.badge !== undefined && item.badge > 0 && (
<span
className="
absolute -top-1 -right-1
min-w-[16px] h-4 px-1
flex items-center justify-center
bg-accent-coral text-white
text-[10px] font-semibold
rounded-full
"
>
{item.badge > 99 ? '99+' : item.badge}
</span>
)}
</div>
<span className="text-nav">{item.label}</span>
</button>
);
})}
</nav>
);
};
export default BottomNav;

View File

@ -0,0 +1,136 @@
/**
* Sidebar ()
* 设计稿参考: UIDesignSpec.md 3.7
*/
import React from 'react';
import { LucideIcon } from 'lucide-react';
export interface SidebarItem {
id: string;
label: string;
icon: LucideIcon;
href?: string;
badge?: number;
children?: SidebarItem[];
}
export interface SidebarSection {
title?: string;
items: SidebarItem[];
}
export interface SidebarProps {
logo?: React.ReactNode;
sections: SidebarSection[];
activeId: string;
onItemClick?: (id: string) => void;
footer?: React.ReactNode;
className?: string;
}
export const Sidebar: React.FC<SidebarProps> = ({
logo,
sections,
activeId,
onItemClick,
footer,
className = '',
}) => {
return (
<aside
className={`
fixed left-0 top-0 bottom-0 z-sidebar
w-sidebar bg-bg-card
flex flex-col
border-r border-border-subtle
${className}
`}
>
{/* Logo */}
{logo && (
<div className="px-4 py-5 border-b border-border-subtle">
{logo}
</div>
)}
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4 px-3">
{sections.map((section, sectionIndex) => (
<div key={sectionIndex} className="mb-6">
{section.title && (
<h4 className="px-3 mb-2 text-small text-text-tertiary uppercase tracking-wider">
{section.title}
</h4>
)}
<ul className="space-y-1">
{section.items.map((item) => (
<SidebarNavItem
key={item.id}
item={item}
isActive={item.id === activeId}
onClick={() => onItemClick?.(item.id)}
/>
))}
</ul>
</div>
))}
</nav>
{/* Footer */}
{footer && (
<div className="px-4 py-4 border-t border-border-subtle">
{footer}
</div>
)}
</aside>
);
};
interface SidebarNavItemProps {
item: SidebarItem;
isActive: boolean;
onClick: () => void;
}
const SidebarNavItem: React.FC<SidebarNavItemProps> = ({
item,
isActive,
onClick,
}) => {
const Icon = item.icon;
return (
<li>
<button
onClick={onClick}
className={`
w-full flex items-center gap-2.5
px-3 py-2.5 rounded-btn
transition-colors duration-200
${isActive
? 'bg-bg-elevated text-accent-indigo font-semibold'
: 'text-text-secondary hover:bg-bg-elevated'
}
`}
>
<Icon size={20} className="flex-shrink-0" />
<span className="flex-1 text-left text-body">{item.label}</span>
{item.badge !== undefined && item.badge > 0 && (
<span
className="
min-w-[20px] h-5 px-1.5
flex items-center justify-center
bg-accent-coral text-white
text-[11px] font-semibold
rounded-full
"
>
{item.badge > 99 ? '99+' : item.badge}
</span>
)}
</button>
</li>
);
};
export default Sidebar;

View File

@ -0,0 +1,41 @@
/**
* StatusBar ()
* 设计稿参考: UIDesignSpec.md 3.1
*/
import React from 'react';
import { Signal, Wifi, BatteryFull } from 'lucide-react';
export interface StatusBarProps {
time?: string;
className?: string;
}
export const StatusBar: React.FC<StatusBarProps> = ({
time = '9:41',
className = '',
}) => {
return (
<div
className={`
flex items-center justify-between
h-status-bar px-6
bg-bg-page safe-area-top
${className}
`}
>
{/* Time */}
<span className="text-body font-semibold text-text-primary">
{time}
</span>
{/* Status Icons */}
<div className="flex items-center gap-1">
<Signal size={16} className="text-text-primary" />
<Wifi size={16} className="text-text-primary" />
<BatteryFull size={16} className="text-text-primary" />
</div>
</div>
);
};
export default StatusBar;

View File

@ -0,0 +1,104 @@
/**
* Button
* 设计稿参考: UIDesignSpec.md 3.4
*/
import React from 'react';
import { LucideIcon } from 'lucide-react';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
icon?: LucideIcon;
iconPosition?: 'left' | 'right';
loading?: boolean;
fullWidth?: boolean;
children?: React.ReactNode;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-accent-indigo text-white hover:opacity-90 active:opacity-80',
secondary: 'bg-bg-elevated text-text-secondary hover:bg-opacity-80',
danger: 'bg-accent-coral text-white hover:opacity-90 active:opacity-80',
success: 'bg-accent-green text-white hover:opacity-90 active:opacity-80',
ghost: 'bg-transparent text-text-secondary hover:bg-bg-elevated',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-small',
md: 'px-4 py-2.5 text-body',
lg: 'px-6 py-3 text-section-title',
};
const iconSizes: Record<ButtonSize, number> = {
sm: 14,
md: 16,
lg: 20,
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
icon: Icon,
iconPosition = 'left',
loading = false,
fullWidth = false,
children,
className = '',
disabled,
...props
}) => {
const isDisabled = disabled || loading;
const iconSize = iconSizes[size];
return (
<button
className={`
inline-flex items-center justify-center gap-2 font-semibold
rounded-btn transition-all duration-200
${variantStyles[variant]}
${sizeStyles[size]}
${fullWidth ? 'w-full' : ''}
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
${className}
`}
disabled={isDisabled}
{...props}
>
{loading && (
<svg
className="animate-spin"
width={iconSize}
height={iconSize}
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{!loading && Icon && iconPosition === 'left' && (
<Icon size={iconSize} />
)}
{children}
{!loading && Icon && iconPosition === 'right' && (
<Icon size={iconSize} />
)}
</button>
);
};
export default Button;

View File

@ -0,0 +1,81 @@
/**
* Card
* 设计稿参考: UIDesignSpec.md 3.3
*/
import React from 'react';
export interface CardProps {
children: React.ReactNode;
className?: string;
variant?: 'default' | 'elevated';
padding?: 'mobile' | 'desktop' | 'none';
onClick?: () => void;
hoverable?: boolean;
}
const paddingStyles = {
mobile: 'p-[14px_16px]',
desktop: 'p-[16px_20px]',
none: 'p-0',
};
export const Card: React.FC<CardProps> = ({
children,
className = '',
variant = 'default',
padding = 'mobile',
onClick,
hoverable = false,
}) => {
return (
<div
className={`
bg-bg-card rounded-card
${paddingStyles[padding]}
${variant === 'elevated' ? 'bg-bg-elevated shadow-elevated' : ''}
${hoverable ? 'cursor-pointer transition-all duration-200 hover:bg-bg-elevated hover:shadow-card' : ''}
${onClick ? 'cursor-pointer' : ''}
${className}
`}
onClick={onClick}
>
{children}
</div>
);
};
export const CardHeader: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className = '' }) => (
<div className={`flex items-center justify-between mb-3 ${className}`}>
{children}
</div>
);
export const CardTitle: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className = '' }) => (
<h3 className={`text-section-title text-text-primary font-semibold ${className}`}>
{children}
</h3>
);
export const CardContent: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className = '' }) => (
<div className={className}>{children}</div>
);
export const CardFooter: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className = '' }) => (
<div className={`mt-4 pt-4 border-t border-border-subtle ${className}`}>
{children}
</div>
);
export default Card;

View File

@ -0,0 +1,112 @@
/**
* Input
* 设计稿参考: UIDesignSpec.md
*/
import React, { forwardRef } from 'react';
import { LucideIcon, Search, Eye, EyeOff } from 'lucide-react';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
leftIcon?: LucideIcon;
rightIcon?: LucideIcon;
onRightIconClick?: () => void;
fullWidth?: boolean;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(({
label,
error,
hint,
leftIcon: LeftIcon,
rightIcon: RightIcon,
onRightIconClick,
fullWidth = true,
className = '',
disabled,
...props
}, ref) => {
return (
<div className={`${fullWidth ? 'w-full' : ''}`}>
{label && (
<label className="block mb-1.5 text-caption text-text-secondary">
{label}
</label>
)}
<div className="relative">
{LeftIcon && (
<LeftIcon
size={18}
className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary"
/>
)}
<input
ref={ref}
className={`
w-full bg-bg-elevated text-text-primary
border border-border-subtle rounded-btn
px-4 py-2.5 text-body
transition-colors duration-200
placeholder:text-text-tertiary
focus:outline-none focus:border-accent-indigo
disabled:opacity-50 disabled:cursor-not-allowed
${LeftIcon ? 'pl-10' : ''}
${RightIcon ? 'pr-10' : ''}
${error ? 'border-accent-coral focus:border-accent-coral' : ''}
${className}
`}
disabled={disabled}
{...props}
/>
{RightIcon && (
<button
type="button"
onClick={onRightIconClick}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
>
<RightIcon size={18} />
</button>
)}
</div>
{error && (
<p className="mt-1 text-small text-accent-coral">{error}</p>
)}
{hint && !error && (
<p className="mt-1 text-small text-text-tertiary">{hint}</p>
)}
</div>
);
});
Input.displayName = 'Input';
// 搜索输入框
export const SearchInput = forwardRef<HTMLInputElement, Omit<InputProps, 'leftIcon'>>(
(props, ref) => (
<Input ref={ref} leftIcon={Search} placeholder="搜索..." {...props} />
)
);
SearchInput.displayName = 'SearchInput';
// 密码输入框
export const PasswordInput = forwardRef<HTMLInputElement, Omit<InputProps, 'type' | 'rightIcon'>>(
(props, ref) => {
const [showPassword, setShowPassword] = React.useState(false);
return (
<Input
ref={ref}
type={showPassword ? 'text' : 'password'}
rightIcon={showPassword ? EyeOff : Eye}
onRightIconClick={() => setShowPassword(!showPassword)}
{...props}
/>
);
}
);
PasswordInput.displayName = 'PasswordInput';
export default Input;

View File

@ -0,0 +1,166 @@
/**
* Modal
* 设计稿参考: UIDesignSpec.md
*/
import React, { useEffect, useCallback } from 'react';
import { X } from 'lucide-react';
import { Button } from './Button';
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
footer?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
closeOnOverlay?: boolean;
closeOnEsc?: boolean;
showCloseButton?: boolean;
className?: string;
}
const sizeStyles = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
full: 'max-w-[90vw] max-h-[90vh]',
};
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
footer,
size = 'md',
closeOnOverlay = true,
closeOnEsc = true,
showCloseButton = true,
className = '',
}) => {
// Handle ESC key
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (closeOnEsc && e.key === 'Escape') {
onClose();
}
}, [closeOnEsc, onClose]);
useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, handleKeyDown]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-modal flex items-center justify-center">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-xs"
onClick={closeOnOverlay ? onClose : undefined}
/>
{/* Modal Content */}
<div
className={`
relative w-full mx-4
bg-bg-card rounded-card
shadow-elevated
animate-scale-in
${sizeStyles[size]}
${className}
`}
>
{/* Header */}
{(title || showCloseButton) && (
<div className="flex items-center justify-between px-5 py-4 border-b border-border-subtle">
{title && (
<h2 className="text-section-title text-text-primary font-semibold">
{title}
</h2>
)}
{showCloseButton && (
<button
onClick={onClose}
className="p-1 text-text-tertiary hover:text-text-primary transition-colors"
>
<X size={20} />
</button>
)}
</div>
)}
{/* Body */}
<div className="px-5 py-4 max-h-[60vh] overflow-y-auto">
{children}
</div>
{/* Footer */}
{footer && (
<div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-3">
{footer}
</div>
)}
</div>
</div>
);
};
// 确认弹窗
export interface ConfirmModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'default' | 'danger';
loading?: boolean;
}
export const ConfirmModal: React.FC<ConfirmModalProps> = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = '确认',
cancelText = '取消',
variant = 'default',
loading = false,
}) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="sm"
footer={
<>
<Button variant="secondary" onClick={onClose} disabled={loading}>
{cancelText}
</Button>
<Button
variant={variant === 'danger' ? 'danger' : 'primary'}
onClick={onConfirm}
loading={loading}
>
{confirmText}
</Button>
</>
}
>
<p className="text-body text-text-secondary">{message}</p>
</Modal>
);
};
export default Modal;

View File

@ -0,0 +1,129 @@
/**
* ProgressBar
*
*/
import React from 'react';
export interface ProgressBarProps {
value: number; // 0-100
max?: number;
showLabel?: boolean;
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
className?: string;
}
const sizeStyles = {
sm: 'h-1',
md: 'h-2',
lg: 'h-3',
};
const variantStyles = {
default: 'bg-accent-indigo',
success: 'bg-accent-green',
warning: 'bg-accent-amber',
error: 'bg-accent-coral',
};
export const ProgressBar: React.FC<ProgressBarProps> = ({
value,
max = 100,
showLabel = false,
size = 'md',
variant = 'default',
className = '',
}) => {
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
return (
<div className={`w-full ${className}`}>
{showLabel && (
<div className="flex justify-between mb-1">
<span className="text-small text-text-secondary"></span>
<span className="text-small text-text-primary">{Math.round(percentage)}%</span>
</div>
)}
<div className={`w-full bg-bg-elevated rounded-full overflow-hidden ${sizeStyles[size]}`}>
<div
className={`h-full rounded-full transition-all duration-300 ${variantStyles[variant]}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
};
// 环形进度条 (用于审核中状态)
export interface CircularProgressProps {
value: number; // 0-100
size?: number;
strokeWidth?: number;
variant?: 'default' | 'success' | 'warning' | 'error';
showLabel?: boolean;
label?: string;
className?: string;
}
const circularVariantColors = {
default: '#6366F1',
success: '#32D583',
warning: '#F59E0B',
error: '#E85A4F',
};
export const CircularProgress: React.FC<CircularProgressProps> = ({
value,
size = 120,
strokeWidth = 8,
variant = 'default',
showLabel = true,
label,
className = '',
}) => {
const percentage = Math.min(100, Math.max(0, value));
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - (percentage / 100) * circumference;
return (
<div className={`relative inline-flex items-center justify-center ${className}`}>
<svg width={size} height={size} className="-rotate-90">
{/* Background circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="#27272A"
strokeWidth={strokeWidth}
/>
{/* Progress circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={circularVariantColors[variant]}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
className="transition-all duration-500"
/>
</svg>
{showLabel && (
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-card-title text-text-primary">
{Math.round(percentage)}%
</span>
{label && (
<span className="text-small text-text-tertiary mt-1">{label}</span>
)}
</div>
)}
</div>
);
};
export default ProgressBar;

View File

@ -0,0 +1,90 @@
/**
* Select
* 设计稿参考: UIDesignSpec.md
*/
import React, { forwardRef } from 'react';
import { ChevronDown } from 'lucide-react';
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
label?: string;
error?: string;
hint?: string;
options: SelectOption[];
placeholder?: string;
fullWidth?: boolean;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(({
label,
error,
hint,
options,
placeholder,
fullWidth = true,
className = '',
disabled,
...props
}, ref) => {
return (
<div className={`${fullWidth ? 'w-full' : ''}`}>
{label && (
<label className="block mb-1.5 text-caption text-text-secondary">
{label}
</label>
)}
<div className="relative">
<select
ref={ref}
className={`
w-full bg-bg-elevated text-text-primary
border border-border-subtle rounded-btn
px-4 py-2.5 text-body
appearance-none cursor-pointer
transition-colors duration-200
focus:outline-none focus:border-accent-indigo
disabled:opacity-50 disabled:cursor-not-allowed
${error ? 'border-accent-coral focus:border-accent-coral' : ''}
${className}
`}
disabled={disabled}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option
key={option.value}
value={option.value}
disabled={option.disabled}
>
{option.label}
</option>
))}
</select>
<ChevronDown
size={18}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary pointer-events-none"
/>
</div>
{error && (
<p className="mt-1 text-small text-accent-coral">{error}</p>
)}
{hint && !error && (
<p className="mt-1 text-small text-text-tertiary">{hint}</p>
)}
</div>
);
});
Select.displayName = 'Select';
export default Select;

View File

@ -0,0 +1,98 @@
/**
* Tag
* 设计稿参考: UIDesignSpec.md 3.5
*/
import React from 'react';
import { LucideIcon, Check, Clock, AlertTriangle, XCircle } from 'lucide-react';
export type TagStatus = 'success' | 'pending' | 'warning' | 'error';
export type TagSize = 'sm' | 'md';
export interface TagProps {
status: TagStatus;
children: React.ReactNode;
size?: TagSize;
icon?: LucideIcon | boolean;
className?: string;
}
const statusStyles: Record<TagStatus, { bg: string; text: string; defaultIcon: LucideIcon }> = {
success: {
bg: 'bg-status-success',
text: 'text-accent-green',
defaultIcon: Check,
},
pending: {
bg: 'bg-status-pending',
text: 'text-accent-indigo',
defaultIcon: Clock,
},
warning: {
bg: 'bg-status-warning',
text: 'text-accent-amber',
defaultIcon: AlertTriangle,
},
error: {
bg: 'bg-status-error',
text: 'text-accent-coral',
defaultIcon: XCircle,
},
};
const sizeStyles: Record<TagSize, { padding: string; text: string; iconSize: number }> = {
sm: { padding: 'px-1.5 py-0.5', text: 'text-[11px]', iconSize: 12 },
md: { padding: 'px-2 py-1', text: 'text-small', iconSize: 14 },
};
export const Tag: React.FC<TagProps> = ({
status,
children,
size = 'md',
icon,
className = '',
}) => {
const styles = statusStyles[status];
const sizeStyle = sizeStyles[size];
const showIcon = icon !== false;
const IconComponent = icon === true || icon === undefined ? styles.defaultIcon : icon;
return (
<span
className={`
inline-flex items-center gap-1 font-medium rounded-tag
${styles.bg} ${styles.text}
${sizeStyle.padding} ${sizeStyle.text}
${className}
`}
>
{showIcon && IconComponent && (
<IconComponent size={sizeStyle.iconSize} />
)}
{children}
</span>
);
};
// 预定义的状态标签
export const SuccessTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({
children,
size,
}) => <Tag status="success" size={size}>{children}</Tag>;
export const PendingTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({
children,
size,
}) => <Tag status="pending" size={size}>{children}</Tag>;
export const WarningTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({
children,
size,
}) => <Tag status="warning" size={size}>{children}</Tag>;
export const ErrorTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({
children,
size,
}) => <Tag status="error" size={size}>{children}</Tag>;
export default Tag;

View File

@ -0,0 +1,66 @@
/**
*
* 设计稿参考: UIDesignSpec.md 1.1
*/
// 背景色
export const BG_COLORS = {
page: '#0B0B0E',
card: '#16161A',
elevated: '#1A1A1E',
} as const;
// 文字色
export const TEXT_COLORS = {
primary: '#FAFAF9',
secondary: '#A1A1AA',
tertiary: '#71717A',
} as const;
// 强调色
export const ACCENT_COLORS = {
indigo: '#6366F1',
green: '#32D583',
coral: '#E85A4F',
amber: '#F59E0B',
} as const;
// 边框色
export const BORDER_COLORS = {
subtle: '#27272A',
} as const;
// 状态色 (带透明度用于背景)
export const STATUS_COLORS = {
success: {
bg: 'rgba(50, 213, 131, 0.125)',
text: '#32D583',
},
pending: {
bg: 'rgba(99, 102, 241, 0.125)',
text: '#6366F1',
},
warning: {
bg: 'rgba(245, 158, 11, 0.125)',
text: '#F59E0B',
},
error: {
bg: 'rgba(232, 90, 79, 0.125)',
text: '#E85A4F',
},
} as const;
// CSS 变量名映射
export const CSS_VARS = {
bgPage: '--bg-page',
bgCard: '--bg-card',
bgElevated: '--bg-elevated',
textPrimary: '--text-primary',
textSecondary: '--text-secondary',
textTertiary: '--text-tertiary',
accentIndigo: '--accent-indigo',
accentGreen: '--accent-green',
accentCoral: '--accent-coral',
accentAmber: '--accent-amber',
borderSubtle: '--border-subtle',
} as const;

View File

@ -0,0 +1,92 @@
/**
*
* 设计稿参考: UIDesignSpec.md 2.2
* 使 Lucide Icons
*/
// 导航图标映射
export const NAV_ICONS = {
// 通用导航
home: 'house', // 工作台/首页
tasks: 'clipboard-list', // 任务
review: 'circle-check', // 审核/审批
reviewDesk: 'clipboard-check', // 审核台
messages: 'bell', // 消息/通知
profile: 'user', // 个人中心
// 侧边栏导航
creators: 'users', // 达人管理
dashboard: 'chart-column', // 数据看板/报表
sentiment: 'triangle-alert', // 舆情预警
agencies: 'building-2', // 代理商管理
auditLog: 'scroll-text', // 审计日志
settings: 'settings', // 系统设置
brief: 'file-text', // Brief管理
versionCompare: 'git-compare', // 版本比对
} as const;
// 操作图标映射
export const ACTION_ICONS = {
filter: 'sliders-horizontal', // 筛选
search: 'search', // 搜索
add: 'plus', // 添加
edit: 'pencil', // 编辑
view: 'eye', // 查看
download: 'download', // 下载/导出
arrowRight: 'chevron-right', // 箭头右
arrowDown: 'chevron-down', // 箭头下
} as const;
// 状态栏图标映射
export const STATUS_BAR_ICONS = {
signal: 'signal',
wifi: 'wifi',
battery: 'battery-full',
} as const;
// 其他图标映射
export const MISC_ICONS = {
security: 'shield-check', // 隐私与安全
info: 'info', // 关于我们
help: 'message-circle', // 帮助与反馈
} as const;
// 图标尺寸常量
export const ICON_SIZES = {
bottomNav: 24,
sidebar: 20,
button: 16,
statusBar: 16,
} as const;
// Lucide React 图标导入映射 (便于动态使用)
export const LUCIDE_ICON_MAP = {
'house': 'House',
'clipboard-list': 'ClipboardList',
'circle-check': 'CircleCheck',
'clipboard-check': 'ClipboardCheck',
'bell': 'Bell',
'user': 'User',
'users': 'Users',
'chart-column': 'ChartColumn',
'triangle-alert': 'TriangleAlert',
'building-2': 'Building2',
'scroll-text': 'ScrollText',
'settings': 'Settings',
'file-text': 'FileText',
'git-compare': 'GitCompare',
'sliders-horizontal': 'SlidersHorizontal',
'search': 'Search',
'plus': 'Plus',
'pencil': 'Pencil',
'eye': 'Eye',
'download': 'Download',
'chevron-right': 'ChevronRight',
'chevron-down': 'ChevronDown',
'signal': 'Signal',
'wifi': 'Wifi',
'battery-full': 'BatteryFull',
'shield-check': 'ShieldCheck',
'info': 'Info',
'message-circle': 'MessageCircle',
} as const;

View File

@ -0,0 +1,69 @@
/**
*
*/
export * from './colors';
export * from './icons';
// 布局尺寸常量
export const LAYOUT = {
// 移动端
mobile: {
width: 402,
height: 874,
statusBarHeight: 44,
bottomNavHeight: 83,
paddingX: 24,
paddingY: 16,
},
// 桌面端
desktop: {
minWidth: 1280,
maxWidth: 1440,
height: 900,
sidebarWidth: 260,
padding: 32,
},
// 响应式断点
breakpoints: {
mobile: 768,
tablet: 1024,
desktop: 1280,
},
} as const;
// 圆角常量
export const RADIUS = {
card: 12,
btn: 8,
tag: 4,
} as const;
// 间距常量
export const SPACING = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
'2xl': 24,
'3xl': 32,
} as const;
// 字体大小常量
export const FONT_SIZES = {
pageTitle: 24,
cardTitle: 22,
sectionTitle: 16,
body: 15,
caption: 13,
small: 12,
nav: 11,
} as const;
// 动画时长常量
export const ANIMATION = {
fast: 150,
normal: 200,
slow: 300,
} as const;

48
frontend/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "smartaudit-frontend",
"version": "1.0.0",
"description": "SmartAudit AI 营销内容合规审核平台 - 前端",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"lucide-react": "^0.300.0",
"zustand": "^4.4.0",
"axios": "^1.6.0",
"@uppy/core": "^3.8.0",
"@uppy/tus": "^3.4.0",
"@uppy/react": "^3.2.0",
"socket.io-client": "^4.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0",
"eslint": "^8.55.0",
"eslint-config-next": "^14.0.0",
"vitest": "^1.0.0",
"@testing-library/react": "^14.1.0",
"@testing-library/jest-dom": "^6.1.0",
"@vitejs/plugin-react": "^4.2.0",
"jsdom": "^23.0.0",
"@vitest/coverage-v8": "^1.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

434
frontend/styles/globals.css Normal file
View File

@ -0,0 +1,434 @@
/* SmartAudit - 全局样式文件 */
/* 基于 UIDesignSpec.md 设计规范 */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ========================================
1. CSS Variables (设计令牌)
======================================== */
:root {
/* 背景色 */
--bg-page: #0B0B0E;
--bg-card: #16161A;
--bg-elevated: #1A1A1E;
/* 文字色 */
--text-primary: #FAFAF9;
--text-secondary: #A1A1AA;
--text-tertiary: #71717A;
/* 强调色 */
--accent-indigo: #6366F1;
--accent-green: #32D583;
--accent-coral: #E85A4F;
--accent-amber: #F59E0B;
/* 边框色 */
--border-subtle: #27272A;
/* 字体 */
--font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
/* 圆角 */
--radius-card: 12px;
--radius-btn: 8px;
--radius-tag: 4px;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 20px;
--spacing-2xl: 24px;
--spacing-3xl: 32px;
}
/* ========================================
2. Base Styles (基础样式)
======================================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-family);
background-color: var(--bg-page);
color: var(--text-primary);
line-height: 1.5;
}
/* ========================================
3. Typography (字体系统)
======================================== */
.text-page-title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.text-card-title {
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
}
.text-section-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.text-body {
font-size: 15px;
font-weight: 400;
color: var(--text-primary);
}
.text-caption {
font-size: 13px;
font-weight: 400;
color: var(--text-secondary);
}
.text-small {
font-size: 12px;
font-weight: 400;
color: var(--text-tertiary);
}
.text-nav {
font-size: 11px;
font-weight: 400;
}
/* ========================================
4. Component Styles (组件样式)
======================================== */
/* Card 卡片 */
@layer components {
.card {
@apply bg-bg-card rounded-card;
padding: 14px 16px;
}
.card-desktop {
@apply bg-bg-card rounded-card;
padding: 16px 20px;
}
}
/* Button 按钮 */
@layer components {
.btn {
@apply inline-flex items-center justify-center font-semibold transition-all duration-200;
border-radius: var(--radius-btn);
}
.btn-primary {
@apply btn bg-accent-indigo text-white;
padding: 10px 16px;
}
.btn-primary:hover {
@apply opacity-90;
}
.btn-primary:active {
@apply opacity-80;
}
.btn-secondary {
@apply btn bg-bg-elevated text-text-secondary;
padding: 8px 16px;
}
.btn-secondary:hover {
@apply bg-opacity-80;
}
.btn-danger {
@apply btn bg-accent-coral text-white;
padding: 10px 16px;
}
.btn-success {
@apply btn bg-accent-green text-white;
padding: 10px 16px;
}
}
/* Status Tags 状态标签 */
@layer components {
.tag {
@apply inline-flex items-center px-2 py-1 text-xs font-medium;
border-radius: var(--radius-tag);
}
.tag-success {
background-color: rgba(50, 213, 131, 0.125);
color: var(--accent-green);
}
.tag-pending {
background-color: rgba(99, 102, 241, 0.125);
color: var(--accent-indigo);
}
.tag-error {
background-color: rgba(232, 90, 79, 0.125);
color: var(--accent-coral);
}
.tag-warning {
background-color: rgba(245, 158, 11, 0.125);
color: var(--accent-amber);
}
}
/* Bottom Navigation 底部导航 (移动端) */
@layer components {
.bottom-nav {
@apply fixed bottom-0 left-0 right-0 flex justify-around items-center;
height: 83px;
padding: 12px 21px;
background: linear-gradient(180deg, transparent 0%, var(--bg-page) 50%);
}
.nav-item {
@apply flex flex-col items-center gap-1 text-text-secondary;
}
.nav-item.active {
@apply text-accent-indigo;
}
.nav-item-icon {
@apply w-6 h-6;
}
.nav-item-label {
@apply text-nav;
}
}
/* Sidebar Navigation 侧边栏导航 (桌面端) */
@layer components {
.sidebar {
@apply fixed left-0 top-0 bottom-0 bg-bg-card flex flex-col;
width: 260px;
}
.sidebar-nav-item {
@apply flex items-center gap-2.5 px-3 py-2.5 rounded-btn text-text-secondary transition-colors;
}
.sidebar-nav-item:hover {
@apply bg-bg-elevated;
}
.sidebar-nav-item.active {
@apply bg-bg-elevated text-accent-indigo font-semibold;
}
.sidebar-nav-icon {
@apply w-5 h-5;
}
}
/* Status Bar 状态栏 (移动端) */
@layer components {
.status-bar {
@apply flex items-center justify-between px-6;
height: 44px;
background-color: var(--bg-page);
}
.status-bar-time {
@apply text-body font-semibold;
}
.status-bar-icons {
@apply flex items-center gap-1;
}
}
/* ========================================
5. Layout Utilities (布局工具类)
======================================== */
/* Mobile Layout */
.mobile-layout {
@apply min-h-screen flex flex-col;
padding: 16px 24px;
padding-bottom: 99px; /* 83px bottom nav + 16px padding */
}
/* Desktop Layout */
.desktop-layout {
@apply min-h-screen;
margin-left: 260px; /* sidebar width */
padding: 32px;
}
/* Content Area */
.content-area {
@apply flex flex-col;
gap: var(--spacing-lg);
}
.content-area-desktop {
@apply flex flex-col;
gap: var(--spacing-2xl);
}
/* ========================================
6. Form Elements (表单元素)
======================================== */
@layer components {
.input {
@apply w-full bg-bg-elevated text-text-primary border border-border-subtle rounded-btn px-4 py-2.5 text-body;
transition: border-color 0.2s;
}
.input:focus {
@apply outline-none border-accent-indigo;
}
.input::placeholder {
@apply text-text-tertiary;
}
.select {
@apply input appearance-none cursor-pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23A1A1AA' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 40px;
}
.textarea {
@apply input resize-none;
min-height: 100px;
}
}
/* ========================================
7. Animations (动画)
======================================== */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out;
}
.animate-slide-up {
animation: slideUp 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* ========================================
8. Scrollbar Styles (滚动条样式)
======================================== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-card);
}
::-webkit-scrollbar-thumb {
background: var(--border-subtle);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* ========================================
9. Responsive Breakpoints (响应式断点)
======================================== */
/* Mobile: < 768px (default) */
/* Tablet: 768px - 1024px */
/* Desktop: > 1024px */
@media (min-width: 768px) {
.mobile-only {
display: none;
}
}
@media (max-width: 767px) {
.desktop-only {
display: none;
}
}
/* ========================================
10. Utility Classes (工具类)
======================================== */
.truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.truncate-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.glass-effect {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* Safe area for iOS */
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);
}
.safe-area-top {
padding-top: env(safe-area-inset-top, 0);
}

163
frontend/tailwind.config.js Normal file
View File

@ -0,0 +1,163 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
// ========================================
// Colors - 颜色系统
// ========================================
colors: {
// 背景色
'bg-page': '#0B0B0E',
'bg-card': '#16161A',
'bg-elevated': '#1A1A1E',
// 文字色
'text-primary': '#FAFAF9',
'text-secondary': '#A1A1AA',
'text-tertiary': '#71717A',
// 强调色
'accent-indigo': '#6366F1',
'accent-green': '#32D583',
'accent-coral': '#E85A4F',
'accent-amber': '#F59E0B',
// 边框色
'border-subtle': '#27272A',
// 状态色 (带透明度)
'status-success': 'rgba(50, 213, 131, 0.125)',
'status-pending': 'rgba(99, 102, 241, 0.125)',
'status-error': 'rgba(232, 90, 79, 0.125)',
'status-warning': 'rgba(245, 158, 11, 0.125)',
},
// ========================================
// Typography - 字体系统
// ========================================
fontFamily: {
sans: ['DM Sans', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
},
fontSize: {
// 页面标题
'page-title': ['24px', { lineHeight: '1.3', fontWeight: '700' }],
// 卡片标题
'card-title': ['22px', { lineHeight: '1.3', fontWeight: '700' }],
// 区块标题
'section-title': ['16px', { lineHeight: '1.4', fontWeight: '600' }],
// 正文内容
'body': ['15px', { lineHeight: '1.5', fontWeight: '400' }],
// 辅助文字
'caption': ['13px', { lineHeight: '1.5', fontWeight: '400' }],
// 小标签
'small': ['12px', { lineHeight: '1.4', fontWeight: '400' }],
// 底部导航
'nav': ['11px', { lineHeight: '1.3', fontWeight: '400' }],
},
// ========================================
// Spacing - 间距系统
// ========================================
spacing: {
'4.5': '18px',
'13': '52px',
'15': '60px',
'18': '72px',
'21': '84px', // bottom nav padding
'22': '88px',
'65': '260px', // sidebar width
'83': '332px',
},
// ========================================
// Border Radius - 圆角系统
// ========================================
borderRadius: {
'card': '12px',
'btn': '8px',
'tag': '4px',
},
// ========================================
// Sizes - 尺寸
// ========================================
width: {
'sidebar': '260px',
'mobile': '402px',
},
height: {
'status-bar': '44px',
'bottom-nav': '83px',
'mobile': '874px',
},
minHeight: {
'screen-mobile': '874px',
},
maxWidth: {
'mobile': '402px',
'desktop': '1440px',
},
// ========================================
// Box Shadow - 阴影
// ========================================
boxShadow: {
'card': '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2)',
'elevated': '0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3)',
'inner-subtle': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.2)',
},
// ========================================
// Animations - 动画
// ========================================
animation: {
'fade-in': 'fadeIn 0.2s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'scale-in': 'scaleIn 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
// ========================================
// Backdrop Blur - 毛玻璃效果
// ========================================
backdropBlur: {
xs: '2px',
},
// ========================================
// Z-Index - 层级
// ========================================
zIndex: {
'sidebar': '40',
'bottom-nav': '50',
'modal': '60',
'toast': '70',
},
},
},
plugins: [],
};

33
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/constants/*": ["./constants/*"],
"@/styles/*": ["./styles/*"],
"@/lib/*": ["./lib/*"],
"@/hooks/*": ["./hooks/*"],
"@/types/*": ["./types/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

412
tasks.md
View File

@ -2030,7 +2030,415 @@ graph TD
---
## 10. 相关文档
## 10. UI 开发任务 (关联设计稿)
> 所有 UI 开发任务必须严格对照设计稿 `pencil-new.pen` 实现,使用 Pencil MCP 工具访问设计稿节点
### 10.1 设计稿与组件库
| 资源 | 路径 | 说明 |
| --- | --- | --- |
| 设计稿文件 | `pencil-new.pen` | Pencil 格式设计稿 |
| 设计规范文档 | `UIDesignSpec.md` | 颜色、字体、间距、组件规范 |
| Tailwind 配置 | `frontend/tailwind.config.js` | 设计令牌 Tailwind 映射 |
| 全局样式 | `frontend/styles/globals.css` | CSS Variables 与组件样式 |
| 组件库 | `frontend/components/` | React 基础组件 |
| 常量定义 | `frontend/constants/` | 颜色、图标、布局常量 |
### 10.2 UI 开发检查清单
开发每个页面时,必须对照以下检查项:
- [ ] 背景色使用 `bg-bg-page` (#0B0B0E)
- [ ] 卡片使用 `bg-bg-card` (#16161A) + `rounded-card` (12px)
- [ ] 字体统一使用 DM Sans (`font-sans`)
- [ ] 图标使用 Lucide按照 `constants/icons.ts` 映射表选择
- [ ] 移动端底部导航高度 83px渐变背景
- [ ] 桌面端侧边栏宽度 260px
- [ ] 状态标签使用 `<Tag>` 组件(通过=绿色,处理中=紫色,错误=红色)
- [ ] 按钮使用 `<Button>` 组件(主要=紫色,次要=深灰色)
- [ ] 卡片使用 `<Card>` 组件
- [ ] 布局使用 `<MobileLayout>``<DesktopLayout>` 组件
### 10.3 达人端 UI 任务
#### TASK-UI-C01: 达人端 - 任务列表 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `PjBJD` |
| **优先级** | P0 |
| **依赖** | TASK-007, TASK-026 |
| **关联任务** | TASK-026 |
**实现要点:**
- 使用 `MobileLayout` 组件
- 任务卡片使用 `Card` 组件
- 状态标签使用 `Tag` 组件
- 底部导航:工作台/任务/审核/消息/我的
---
#### TASK-UI-C02: 达人端 - 智能上传 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `ZelCS` |
| **优先级** | P0 |
| **依赖** | TASK-015, TASK-027 |
| **关联任务** | TASK-027 |
**实现要点:**
- 脚本输入文本区域
- 视频上传区域(拖拽/点击)
- 上传进度条使用 `ProgressBar` 组件
---
#### TASK-UI-C03: 达人端 - 审核结果 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `Vn3VU` |
| **优先级** | P0 |
| **依赖** | TASK-028 |
| **关联任务** | TASK-028 |
**实现要点:**
- 结果横幅(通过/警告/驳回)
- 视频播放器带时间戳标注
- 修改清单组件
---
#### TASK-UI-C04: 达人端 - AI审核中 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `lzdm4` |
| **优先级** | P0 |
| **依赖** | TASK-024 |
| **关联任务** | TASK-024 |
**实现要点:**
- 使用 `CircularProgress` 环形进度组件
- 步骤列表展示当前处理阶段
- "离开并稍后查看"按钮
---
#### TASK-UI-C05: 达人端 - 消息中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `pF15t` |
| **优先级** | P1 |
| **依赖** | TASK-030 |
| **关联任务** | TASK-030 |
---
#### TASK-UI-C06: 达人端 - 历史记录 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `ZKEFl` |
| **优先级** | P2 |
---
#### TASK-UI-C07: 达人端 - 个人中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `zCdM1` |
| **优先级** | P2 |
---
#### TASK-UI-C08: 达人端 - 任务列表 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `HD3eK` |
| **优先级** | P0 |
| **依赖** | TASK-006 |
**实现要点:**
- 使用 `DesktopLayout` 组件
- 侧边栏导航使用 `Sidebar` 组件
---
#### TASK-UI-C09: 达人端 - 智能上传 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `N79bL` |
| **优先级** | P0 |
---
#### TASK-UI-C10: 达人端 - 审核结果 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `3niUa` |
| **优先级** | P0 |
---
#### TASK-UI-C11: 达人端 - AI审核中 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `bxAKT` |
| **优先级** | P0 |
---
#### TASK-UI-C12: 达人端 - 消息中心 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `8XKLP` |
| **优先级** | P1 |
---
### 10.4 代理商端 UI 任务
#### TASK-UI-A01: 代理商端 - 工作台 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `RX8V9` |
| **优先级** | P0 |
| **依赖** | TASK-031 |
| **关联任务** | TASK-031 |
**实现要点:**
- 使用 `DesktopLayout` 组件
- 待办事项卡片、项目进度条、最近活动列表
---
#### TASK-UI-A02: 代理商端 - 审核决策台 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `2u8Bq` |
| **优先级** | P0 |
| **依赖** | TASK-032, TASK-033, TASK-034 |
| **关联任务** | TASK-032, TASK-033, TASK-034 |
**实现要点:**
- 视频播放器带智能进度条(红/黄/绿点)
- AI 检查单区块
- 驳回/通过操作按钮
---
#### TASK-UI-A03: 代理商端 - Brief配置中心 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `djd2K` |
| **优先级** | P0 |
| **依赖** | TASK-012 |
| **关联任务** | TASK-012 |
---
#### TASK-UI-A04: 代理商端 - 达人管理 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `5cFMX` |
| **优先级** | P1 |
---
#### TASK-UI-A05: 代理商端 - 数据报表 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `An8gw` |
| **优先级** | P1 |
| **关联任务** | TASK-059 |
---
#### TASK-UI-A06: 代理商端 - 版本比对 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `NDmYh` |
| **优先级** | P2 |
| **关联任务** | TASK-054, TASK-055 |
---
#### TASK-UI-A07: 代理商端 - 工作台 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `VuH3F` |
| **优先级** | P0 |
| **依赖** | TASK-037A |
| **关联任务** | TASK-037A |
---
#### TASK-UI-A08: 代理商端 - 快捷审核 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `lrHaj` |
| **优先级** | P0 |
| **依赖** | TASK-037B |
| **关联任务** | TASK-037B |
---
#### TASK-UI-A09: 代理商端 - 任务列表 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `c6SPa` |
| **优先级** | P1 |
| **关联任务** | TASK-037C |
---
#### TASK-UI-A10: 代理商端 - 消息中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `9Us9g` |
| **优先级** | P1 |
| **关联任务** | TASK-037C |
---
#### TASK-UI-A11: 代理商端 - 个人中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `8OCZ3` |
| **优先级** | P2 |
---
### 10.5 品牌方端 UI 任务
#### TASK-UI-B01: 品牌方端 - 数据看板 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `xUM9m` |
| **优先级** | P0 |
| **依赖** | TASK-037D |
| **关联任务** | TASK-037D |
**实现要点:**
- 趋势图与问题分布饼图
- 代理商对比柱状图
- 风险预警区
---
#### TASK-UI-B02: 品牌方端 - AI服务配置 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `4ppiJ` |
| **优先级** | P0 |
| **依赖** | TASK-005-F |
| **关联任务** | TASK-005-F |
**实现要点:**
- AI 提供商下拉选择
- 模型配置(文本/视觉/音频)
- API Key 输入与测试连接
---
#### TASK-UI-B03: 品牌方端 - 规则配置 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `nhHSF` |
| **优先级** | P0 |
| **依赖** | TASK-037E |
| **关联任务** | TASK-037E |
---
#### TASK-UI-B04: 品牌方端 - 审计日志 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `jELTK` |
| **优先级** | P1 |
| **关联任务** | TASK-050 |
---
#### TASK-UI-B05: 品牌方端 - 代理商管理 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `2jnnO` |
| **优先级** | P1 |
| **关联任务** | TASK-068 |
---
#### TASK-UI-B06: 品牌方端 - 舆情预警 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `NjCe7` |
| **优先级** | P2 |
| **关联任务** | TASK-060 |
---
#### TASK-UI-B07: 品牌方端 - 系统设置 (Desktop)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `4nVj4` |
| **优先级** | P2 |
---
#### TASK-UI-B08: 品牌方端 - 数据看板 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `lpVdV` |
| **优先级** | P0 |
| **依赖** | TASK-037F |
| **关联任务** | TASK-037F |
---
#### TASK-UI-B09: 品牌方端 - 舆情预警 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `wWAel` |
| **优先级** | P1 |
| **关联任务** | TASK-037G |
---
#### TASK-UI-B10: 品牌方端 - 审批中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `OueOe` |
| **优先级** | P1 |
| **关联任务** | TASK-037H |
---
#### TASK-UI-B11: 品牌方端 - 消息中心 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `1w9xC` |
| **优先级** | P2 |
---
#### TASK-UI-B12: 品牌方端 - 我的 (Mobile)
| 属性 | 内容 |
| --- | --- |
| **设计稿节点ID** | `OJBbT` |
| **优先级** | P2 |
---
### 10.6 UI 任务优先级汇总
| 优先级 | 任务数量 | 任务列表 |
| --- | --- | --- |
| **P0** | 14 | TASK-UI-C01~C04, C08~C11, A01~A03, A07~A08, B01~B03, B08 |
| **P1** | 9 | TASK-UI-C05, C12, A04~A05, A09~A10, B04~B05, B09~B10 |
| **P2** | 8 | TASK-UI-C06~C07, A06, A11, B06~B07, B11~B12 |
---
## 11. 相关文档
| 文档 | 说明 |
| --- | --- |
@ -2038,5 +2446,7 @@ graph TD
| FeatureSummary.md | 功能清单与优先级 |
| DevelopmentPlan.md | 开发计划与技术架构 |
| UIDesign.md | UI 设计规范 |
| UIDesignSpec.md | UI 设计规范(详细版) |
| User_Role_Interfaces.md | 用户角色与界面规范 |
| AIProviderConfig.md | AI 服务配置架构设计 |
| pencil-new.pen | 设计稿文件Pencil 格式) |