From f166c04422eb9c1f0b373592da21ac76b39358a9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Feb 2026 17:44:22 +0800 Subject: [PATCH] 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 --- frontend/components/index.ts | 22 + frontend/components/layout/DesktopLayout.tsx | 61 +++ frontend/components/layout/MobileLayout.tsx | 66 +++ frontend/components/navigation/BottomNav.tsx | 81 ++++ frontend/components/navigation/Sidebar.tsx | 136 ++++++ frontend/components/navigation/StatusBar.tsx | 41 ++ frontend/components/ui/Button.tsx | 104 +++++ frontend/components/ui/Card.tsx | 81 ++++ frontend/components/ui/Input.tsx | 112 +++++ frontend/components/ui/Modal.tsx | 166 +++++++ frontend/components/ui/ProgressBar.tsx | 129 ++++++ frontend/components/ui/Select.tsx | 90 ++++ frontend/components/ui/Tag.tsx | 98 +++++ frontend/constants/colors.ts | 66 +++ frontend/constants/icons.ts | 92 ++++ frontend/constants/index.ts | 69 +++ frontend/package.json | 48 ++ frontend/postcss.config.js | 6 + frontend/styles/globals.css | 434 +++++++++++++++++++ frontend/tailwind.config.js | 163 +++++++ frontend/tsconfig.json | 33 ++ tasks.md | 412 +++++++++++++++++- 22 files changed, 2509 insertions(+), 1 deletion(-) create mode 100644 frontend/components/index.ts create mode 100644 frontend/components/layout/DesktopLayout.tsx create mode 100644 frontend/components/layout/MobileLayout.tsx create mode 100644 frontend/components/navigation/BottomNav.tsx create mode 100644 frontend/components/navigation/Sidebar.tsx create mode 100644 frontend/components/navigation/StatusBar.tsx create mode 100644 frontend/components/ui/Button.tsx create mode 100644 frontend/components/ui/Card.tsx create mode 100644 frontend/components/ui/Input.tsx create mode 100644 frontend/components/ui/Modal.tsx create mode 100644 frontend/components/ui/ProgressBar.tsx create mode 100644 frontend/components/ui/Select.tsx create mode 100644 frontend/components/ui/Tag.tsx create mode 100644 frontend/constants/colors.ts create mode 100644 frontend/constants/icons.ts create mode 100644 frontend/constants/index.ts create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/styles/globals.css create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json diff --git a/frontend/components/index.ts b/frontend/components/index.ts new file mode 100644 index 0000000..4645f70 --- /dev/null +++ b/frontend/components/index.ts @@ -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'; diff --git a/frontend/components/layout/DesktopLayout.tsx b/frontend/components/layout/DesktopLayout.tsx new file mode 100644 index 0000000..9220dae --- /dev/null +++ b/frontend/components/layout/DesktopLayout.tsx @@ -0,0 +1,61 @@ +/** + * DesktopLayout 桌面端布局组件 + * 设计稿参考: UIDesignSpec.md 3.2 + * 尺寸: 1440x900,侧边栏260px + */ +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 = ({ + children, + logo, + sidebarSections, + activeNavId, + onNavItemClick, + sidebarFooter, + headerContent, + className = '', + contentClassName = '', +}) => { + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Header (optional) */} + {headerContent && ( +
+ {headerContent} +
+ )} + + {/* Content Area */} +
+ {children} +
+
+
+ ); +}; + +export default DesktopLayout; diff --git a/frontend/components/layout/MobileLayout.tsx b/frontend/components/layout/MobileLayout.tsx new file mode 100644 index 0000000..ddbcc72 --- /dev/null +++ b/frontend/components/layout/MobileLayout.tsx @@ -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 = ({ + children, + navItems = [], + activeNavId = '', + onNavItemClick, + showStatusBar = true, + showBottomNav = true, + className = '', + contentClassName = '', +}) => { + return ( +
+ {/* Status Bar */} + {showStatusBar && } + + {/* Content Area */} +
+ {children} +
+ + {/* Bottom Navigation */} + {showBottomNav && navItems.length > 0 && ( + + )} +
+ ); +}; + +export default MobileLayout; diff --git a/frontend/components/navigation/BottomNav.tsx b/frontend/components/navigation/BottomNav.tsx new file mode 100644 index 0000000..9bcbb72 --- /dev/null +++ b/frontend/components/navigation/BottomNav.tsx @@ -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 = ({ + items, + activeId, + onItemClick, + className = '', +}) => { + return ( + + ); +}; + +export default BottomNav; diff --git a/frontend/components/navigation/Sidebar.tsx b/frontend/components/navigation/Sidebar.tsx new file mode 100644 index 0000000..86ecf51 --- /dev/null +++ b/frontend/components/navigation/Sidebar.tsx @@ -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 = ({ + logo, + sections, + activeId, + onItemClick, + footer, + className = '', +}) => { + return ( + + ); +}; + +interface SidebarNavItemProps { + item: SidebarItem; + isActive: boolean; + onClick: () => void; +} + +const SidebarNavItem: React.FC = ({ + item, + isActive, + onClick, +}) => { + const Icon = item.icon; + + return ( +
  • + +
  • + ); +}; + +export default Sidebar; diff --git a/frontend/components/navigation/StatusBar.tsx b/frontend/components/navigation/StatusBar.tsx new file mode 100644 index 0000000..5de5b89 --- /dev/null +++ b/frontend/components/navigation/StatusBar.tsx @@ -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 = ({ + time = '9:41', + className = '', +}) => { + return ( +
    + {/* Time */} + + {time} + + + {/* Status Icons */} +
    + + + +
    +
    + ); +}; + +export default StatusBar; diff --git a/frontend/components/ui/Button.tsx b/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..0143ad0 --- /dev/null +++ b/frontend/components/ui/Button.tsx @@ -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 { + variant?: ButtonVariant; + size?: ButtonSize; + icon?: LucideIcon; + iconPosition?: 'left' | 'right'; + loading?: boolean; + fullWidth?: boolean; + children?: React.ReactNode; +} + +const variantStyles: Record = { + 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 = { + 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 = { + sm: 14, + md: 16, + lg: 20, +}; + +export const Button: React.FC = ({ + 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 ( + + ); +}; + +export default Button; diff --git a/frontend/components/ui/Card.tsx b/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..e9d47e5 --- /dev/null +++ b/frontend/components/ui/Card.tsx @@ -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 = ({ + children, + className = '', + variant = 'default', + padding = 'mobile', + onClick, + hoverable = false, +}) => { + return ( +
    + {children} +
    + ); +}; + +export const CardHeader: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => ( +
    + {children} +
    +); + +export const CardTitle: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => ( +

    + {children} +

    +); + +export const CardContent: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => ( +
    {children}
    +); + +export const CardFooter: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = '' }) => ( +
    + {children} +
    +); + +export default Card; diff --git a/frontend/components/ui/Input.tsx b/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..4095b67 --- /dev/null +++ b/frontend/components/ui/Input.tsx @@ -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 { + label?: string; + error?: string; + hint?: string; + leftIcon?: LucideIcon; + rightIcon?: LucideIcon; + onRightIconClick?: () => void; + fullWidth?: boolean; +} + +export const Input = forwardRef(({ + label, + error, + hint, + leftIcon: LeftIcon, + rightIcon: RightIcon, + onRightIconClick, + fullWidth = true, + className = '', + disabled, + ...props +}, ref) => { + return ( +
    + {label && ( + + )} +
    + {LeftIcon && ( + + )} + + {RightIcon && ( + + )} +
    + {error && ( +

    {error}

    + )} + {hint && !error && ( +

    {hint}

    + )} +
    + ); +}); + +Input.displayName = 'Input'; + +// 搜索输入框 +export const SearchInput = forwardRef>( + (props, ref) => ( + + ) +); + +SearchInput.displayName = 'SearchInput'; + +// 密码输入框 +export const PasswordInput = forwardRef>( + (props, ref) => { + const [showPassword, setShowPassword] = React.useState(false); + + return ( + setShowPassword(!showPassword)} + {...props} + /> + ); + } +); + +PasswordInput.displayName = 'PasswordInput'; + +export default Input; diff --git a/frontend/components/ui/Modal.tsx b/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..64ce66d --- /dev/null +++ b/frontend/components/ui/Modal.tsx @@ -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 = ({ + 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 ( +
    + {/* Overlay */} +
    + + {/* Modal Content */} +
    + {/* Header */} + {(title || showCloseButton) && ( +
    + {title && ( +

    + {title} +

    + )} + {showCloseButton && ( + + )} +
    + )} + + {/* Body */} +
    + {children} +
    + + {/* Footer */} + {footer && ( +
    + {footer} +
    + )} +
    +
    + ); +}; + +// 确认弹窗 +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 = ({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = '确认', + cancelText = '取消', + variant = 'default', + loading = false, +}) => { + return ( + + + + + } + > +

    {message}

    +
    + ); +}; + +export default Modal; diff --git a/frontend/components/ui/ProgressBar.tsx b/frontend/components/ui/ProgressBar.tsx new file mode 100644 index 0000000..8d82e6a --- /dev/null +++ b/frontend/components/ui/ProgressBar.tsx @@ -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 = ({ + value, + max = 100, + showLabel = false, + size = 'md', + variant = 'default', + className = '', +}) => { + const percentage = Math.min(100, Math.max(0, (value / max) * 100)); + + return ( +
    + {showLabel && ( +
    + 进度 + {Math.round(percentage)}% +
    + )} +
    +
    +
    +
    + ); +}; + +// 环形进度条 (用于审核中状态) +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 = ({ + 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 ( +
    + + {/* Background circle */} + + {/* Progress circle */} + + + {showLabel && ( +
    + + {Math.round(percentage)}% + + {label && ( + {label} + )} +
    + )} +
    + ); +}; + +export default ProgressBar; diff --git a/frontend/components/ui/Select.tsx b/frontend/components/ui/Select.tsx new file mode 100644 index 0000000..efe086b --- /dev/null +++ b/frontend/components/ui/Select.tsx @@ -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, 'children'> { + label?: string; + error?: string; + hint?: string; + options: SelectOption[]; + placeholder?: string; + fullWidth?: boolean; +} + +export const Select = forwardRef(({ + label, + error, + hint, + options, + placeholder, + fullWidth = true, + className = '', + disabled, + ...props +}, ref) => { + return ( +
    + {label && ( + + )} +
    + + +
    + {error && ( +

    {error}

    + )} + {hint && !error && ( +

    {hint}

    + )} +
    + ); +}); + +Select.displayName = 'Select'; + +export default Select; diff --git a/frontend/components/ui/Tag.tsx b/frontend/components/ui/Tag.tsx new file mode 100644 index 0000000..65384eb --- /dev/null +++ b/frontend/components/ui/Tag.tsx @@ -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 = { + 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 = { + 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 = ({ + 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 ( + + {showIcon && IconComponent && ( + + )} + {children} + + ); +}; + +// 预定义的状态标签 +export const SuccessTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({ + children, + size, +}) => {children}; + +export const PendingTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({ + children, + size, +}) => {children}; + +export const WarningTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({ + children, + size, +}) => {children}; + +export const ErrorTag: React.FC<{ children: React.ReactNode; size?: TagSize }> = ({ + children, + size, +}) => {children}; + +export default Tag; diff --git a/frontend/constants/colors.ts b/frontend/constants/colors.ts new file mode 100644 index 0000000..ba6d9f8 --- /dev/null +++ b/frontend/constants/colors.ts @@ -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; diff --git a/frontend/constants/icons.ts b/frontend/constants/icons.ts new file mode 100644 index 0000000..9618ce9 --- /dev/null +++ b/frontend/constants/icons.ts @@ -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; diff --git a/frontend/constants/index.ts b/frontend/constants/index.ts new file mode 100644 index 0000000..0bc67bf --- /dev/null +++ b/frontend/constants/index.ts @@ -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; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f9b63a8 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css new file mode 100644 index 0000000..22bc6a6 --- /dev/null +++ b/frontend/styles/globals.css @@ -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); +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..de399cb --- /dev/null +++ b/frontend/tailwind.config.js @@ -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: [], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7a80e0d --- /dev/null +++ b/frontend/tsconfig.json @@ -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"] +} diff --git a/tasks.md b/tasks.md index 9d403ad..23464d3 100644 --- a/tasks.md +++ b/tasks.md @@ -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 +- [ ] 状态标签使用 `` 组件(通过=绿色,处理中=紫色,错误=红色) +- [ ] 按钮使用 `