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:
parent
dd06502004
commit
f166c04422
22
frontend/components/index.ts
Normal file
22
frontend/components/index.ts
Normal 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';
|
||||
61
frontend/components/layout/DesktopLayout.tsx
Normal file
61
frontend/components/layout/DesktopLayout.tsx
Normal file
@ -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<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;
|
||||
66
frontend/components/layout/MobileLayout.tsx
Normal file
66
frontend/components/layout/MobileLayout.tsx
Normal 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;
|
||||
81
frontend/components/navigation/BottomNav.tsx
Normal file
81
frontend/components/navigation/BottomNav.tsx
Normal 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;
|
||||
136
frontend/components/navigation/Sidebar.tsx
Normal file
136
frontend/components/navigation/Sidebar.tsx
Normal 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;
|
||||
41
frontend/components/navigation/StatusBar.tsx
Normal file
41
frontend/components/navigation/StatusBar.tsx
Normal 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;
|
||||
104
frontend/components/ui/Button.tsx
Normal file
104
frontend/components/ui/Button.tsx
Normal 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;
|
||||
81
frontend/components/ui/Card.tsx
Normal file
81
frontend/components/ui/Card.tsx
Normal 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;
|
||||
112
frontend/components/ui/Input.tsx
Normal file
112
frontend/components/ui/Input.tsx
Normal 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;
|
||||
166
frontend/components/ui/Modal.tsx
Normal file
166
frontend/components/ui/Modal.tsx
Normal 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;
|
||||
129
frontend/components/ui/ProgressBar.tsx
Normal file
129
frontend/components/ui/ProgressBar.tsx
Normal 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;
|
||||
90
frontend/components/ui/Select.tsx
Normal file
90
frontend/components/ui/Select.tsx
Normal 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;
|
||||
98
frontend/components/ui/Tag.tsx
Normal file
98
frontend/components/ui/Tag.tsx
Normal 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;
|
||||
66
frontend/constants/colors.ts
Normal file
66
frontend/constants/colors.ts
Normal 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;
|
||||
92
frontend/constants/icons.ts
Normal file
92
frontend/constants/icons.ts
Normal 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;
|
||||
69
frontend/constants/index.ts
Normal file
69
frontend/constants/index.ts
Normal 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
48
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
434
frontend/styles/globals.css
Normal file
434
frontend/styles/globals.css
Normal 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
163
frontend/tailwind.config.js
Normal 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
33
frontend/tsconfig.json
Normal 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
412
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
|
||||
- [ ] 状态标签使用 `<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 格式) |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user