feat: 完善代理商端业务逻辑与前后端框架
主要更新: - 更新代理商端文档,明确项目由品牌方分配流程 - 新增Brief配置详情页(已配置)设计稿 - 完善工作台紧急待办中品牌新任务功能 - 整理Pencil设计文件中代理商端页面顺序 - 新增后端FastAPI框架及核心API - 新增前端Next.js页面和组件库 - 添加.gitignore排除构建和缓存文件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d52509d630
commit
e4959d584f
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* BottomNav 组件测试
|
||||
* 测试覆盖: role 渲染、active 状态、基础样式
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { BottomNav } from './BottomNav';
|
||||
|
||||
const mockedUsePathname = vi.mocked(usePathname);
|
||||
|
||||
describe('BottomNav', () => {
|
||||
// ==================== 基础渲染测试 ====================
|
||||
describe('基础渲染', () => {
|
||||
it('渲染导航栏', () => {
|
||||
const { container } = render(<BottomNav />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('渲染所有导航项', () => {
|
||||
render(<BottomNav role="creator" />);
|
||||
expect(screen.getByText('任务')).toBeInTheDocument();
|
||||
expect(screen.getByText('消息')).toBeInTheDocument();
|
||||
expect(screen.getByText('我的')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('渲染图标', () => {
|
||||
const { container } = render(<BottomNav />);
|
||||
const icons = container.querySelectorAll('svg');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Active 状态测试 ====================
|
||||
describe('Active 状态', () => {
|
||||
beforeEach(() => {
|
||||
mockedUsePathname.mockReturnValue('/creator/messages');
|
||||
});
|
||||
|
||||
it('激活项使用高亮颜色', () => {
|
||||
render(<BottomNav role="creator" />);
|
||||
const activeLink = screen.getByText('消息').closest('a');
|
||||
expect(activeLink).toHaveClass('text-text-primary');
|
||||
});
|
||||
|
||||
it('非激活项使用次要颜色', () => {
|
||||
render(<BottomNav role="creator" />);
|
||||
const inactiveLink = screen.getByText('任务').closest('a');
|
||||
expect(inactiveLink).toHaveClass('text-text-secondary');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 样式测试 ====================
|
||||
describe('样式', () => {
|
||||
it('固定定位在底部', () => {
|
||||
const { container } = render(<BottomNav />);
|
||||
const root = container.firstChild as HTMLElement;
|
||||
expect(root).toHaveClass('fixed', 'bottom-0', 'left-0', 'right-0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +1,89 @@
|
||||
/**
|
||||
* BottomNav 底部导航组件 (移动端)
|
||||
* 设计稿参考: UIDesignSpec.md 3.6
|
||||
*/
|
||||
import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
'use client'
|
||||
|
||||
export interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
href?: string;
|
||||
badge?: number;
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ClipboardList, Bell, User, Scan, ListTodo, LayoutDashboard, Settings } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface NavItem {
|
||||
icon: React.ElementType
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export interface BottomNavProps {
|
||||
items: NavItem[];
|
||||
activeId: string;
|
||||
onItemClick?: (id: string) => void;
|
||||
className?: string;
|
||||
// 达人端导航项
|
||||
const creatorNavItems: NavItem[] = [
|
||||
{ icon: ClipboardList, label: '任务', href: '/creator' },
|
||||
{ icon: Bell, label: '消息', href: '/creator/messages' },
|
||||
{ icon: User, label: '我的', href: '/creator/profile' },
|
||||
]
|
||||
|
||||
// 代理商端导航项
|
||||
const agencyNavItems: NavItem[] = [
|
||||
{ icon: LayoutDashboard, label: '工作台', href: '/agency' },
|
||||
{ icon: ListTodo, label: '任务', href: '/agency/tasks' },
|
||||
{ icon: Scan, label: '审核', href: '/agency/review' },
|
||||
{ icon: Bell, label: '消息', href: '/agency/messages' },
|
||||
{ icon: User, label: '我的', href: '/agency/profile' },
|
||||
]
|
||||
|
||||
// 品牌方端导航项
|
||||
const brandNavItems: NavItem[] = [
|
||||
{ icon: LayoutDashboard, label: '看板', href: '/brand' },
|
||||
{ icon: Settings, label: '配置', href: '/brand/rules' },
|
||||
{ icon: Bell, label: '消息', href: '/brand/messages' },
|
||||
{ icon: User, label: '我的', href: '/brand/profile' },
|
||||
]
|
||||
|
||||
interface BottomNavProps {
|
||||
role?: 'creator' | 'agency' | 'brand'
|
||||
}
|
||||
|
||||
export const BottomNav: React.FC<BottomNavProps> = ({
|
||||
items,
|
||||
activeId,
|
||||
onItemClick,
|
||||
className = '',
|
||||
}) => {
|
||||
export function BottomNav({ role = 'creator' }: BottomNavProps) {
|
||||
const pathname = usePathname() || ''
|
||||
|
||||
const navItems = role === 'creator'
|
||||
? creatorNavItems
|
||||
: role === 'agency'
|
||||
? agencyNavItems
|
||||
: brandNavItems
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === `/${role}`) {
|
||||
return pathname === href || pathname === `/${role}/`
|
||||
}
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
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;
|
||||
<div className="fixed bottom-0 left-0 right-0 z-bottom-nav bottom-nav-gradient h-[95px] flex flex-col justify-end px-[21px] pb-[21px] pt-3">
|
||||
<div className="flex items-center justify-around bg-bg-elevated rounded-[31px] h-[62px] p-1 nav-shadow border border-border-subtle">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
|
||||
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>
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-1 w-14 h-full',
|
||||
active ? 'text-text-primary' : 'text-text-secondary'
|
||||
)}
|
||||
</div>
|
||||
<span className="text-nav">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
>
|
||||
<Icon className={cn('w-6 h-6', active && 'text-text-primary')} strokeWidth={active ? 2 : 1.5} />
|
||||
<span className={cn(
|
||||
'text-[10px]',
|
||||
active ? 'font-semibold' : 'font-medium'
|
||||
)}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomNav;
|
||||
export default BottomNav
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Sidebar 组件测试
|
||||
* 测试覆盖: role 渲染、active 状态、基础样式
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
const mockedUsePathname = vi.mocked(usePathname);
|
||||
|
||||
describe('Sidebar', () => {
|
||||
// ==================== 基础渲染测试 ====================
|
||||
describe('基础渲染', () => {
|
||||
it('渲染侧边栏', () => {
|
||||
const { container } = render(<Sidebar />);
|
||||
expect(container.querySelector('aside')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('渲染默认 creator 导航项', () => {
|
||||
render(<Sidebar role="creator" />);
|
||||
expect(screen.getByText('我的任务')).toBeInTheDocument();
|
||||
expect(screen.getByText('消息中心')).toBeInTheDocument();
|
||||
expect(screen.getByText('个人中心')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Role 测试 ====================
|
||||
describe('Role', () => {
|
||||
it('渲染 agency 导航项', () => {
|
||||
render(<Sidebar role="agency" />);
|
||||
expect(screen.getByText('工作台')).toBeInTheDocument();
|
||||
expect(screen.getByText('审核决策')).toBeInTheDocument();
|
||||
expect(screen.getByText('Brief 配置')).toBeInTheDocument();
|
||||
expect(screen.getByText('达人管理')).toBeInTheDocument();
|
||||
expect(screen.getByText('数据报表')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('渲染 brand 导航项', () => {
|
||||
render(<Sidebar role="brand" />);
|
||||
expect(screen.getByText('数据看板')).toBeInTheDocument();
|
||||
expect(screen.getByText('AI 配置')).toBeInTheDocument();
|
||||
expect(screen.getByText('规则配置')).toBeInTheDocument();
|
||||
expect(screen.getByText('终审台')).toBeInTheDocument();
|
||||
expect(screen.getByText('代理商管理')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Active 状态测试 ====================
|
||||
describe('Active 状态', () => {
|
||||
beforeEach(() => {
|
||||
mockedUsePathname.mockReturnValue('/creator/messages');
|
||||
});
|
||||
|
||||
it('激活项使用高亮样式', () => {
|
||||
render(<Sidebar role="creator" />);
|
||||
const activeLink = screen.getByText('消息中心').closest('a');
|
||||
expect(activeLink).toHaveClass('bg-bg-elevated', 'text-text-primary', 'font-semibold');
|
||||
});
|
||||
|
||||
it('非激活项使用默认样式', () => {
|
||||
render(<Sidebar role="creator" />);
|
||||
const inactiveLink = screen.getByText('我的任务').closest('a');
|
||||
expect(inactiveLink).toHaveClass('text-text-secondary');
|
||||
expect(inactiveLink).not.toHaveClass('text-text-primary');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 样式测试 ====================
|
||||
describe('样式', () => {
|
||||
it('固定定位在左侧', () => {
|
||||
const { container } = render(<Sidebar />);
|
||||
const aside = container.querySelector('aside');
|
||||
expect(aside).toHaveClass('fixed', 'left-0', 'top-0', 'bottom-0');
|
||||
});
|
||||
|
||||
it('应用正确宽度', () => {
|
||||
const { container } = render(<Sidebar />);
|
||||
const aside = container.querySelector('aside');
|
||||
expect(aside).toHaveClass('w-[260px]');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,136 +1,98 @@
|
||||
/**
|
||||
* Sidebar 侧边栏导航组件 (桌面端)
|
||||
* 设计稿参考: UIDesignSpec.md 3.7
|
||||
*/
|
||||
import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
'use client'
|
||||
|
||||
export interface SidebarItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
href?: string;
|
||||
badge?: number;
|
||||
children?: SidebarItem[];
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ShieldCheck, ListTodo, User, LayoutDashboard, Scan, BarChart3, Settings, FileText, Users, Bell } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface NavItem {
|
||||
icon: React.ElementType
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export interface SidebarSection {
|
||||
title?: string;
|
||||
items: SidebarItem[];
|
||||
// 达人端导航项
|
||||
const creatorNavItems: NavItem[] = [
|
||||
{ icon: ListTodo, label: '我的任务', href: '/creator' },
|
||||
{ icon: Bell, label: '消息中心', href: '/creator/messages' },
|
||||
{ icon: User, label: '个人中心', href: '/creator/profile' },
|
||||
]
|
||||
|
||||
// 代理商端导航项
|
||||
const agencyNavItems: NavItem[] = [
|
||||
{ icon: LayoutDashboard, label: '工作台', href: '/agency' },
|
||||
{ icon: Scan, label: '审核决策', href: '/agency/review' },
|
||||
{ icon: FileText, label: 'Brief 配置', href: '/agency/briefs' },
|
||||
{ icon: Users, label: '达人管理', href: '/agency/creators' },
|
||||
{ icon: BarChart3, label: '数据报表', href: '/agency/reports' },
|
||||
]
|
||||
|
||||
// 品牌方端导航项
|
||||
const brandNavItems: NavItem[] = [
|
||||
{ icon: LayoutDashboard, label: '数据看板', href: '/brand' },
|
||||
{ icon: Settings, label: 'AI 配置', href: '/brand/ai-config' },
|
||||
{ icon: FileText, label: '规则配置', href: '/brand/rules' },
|
||||
{ icon: FileCheck, label: '终审台', href: '/brand/final-review' },
|
||||
{ icon: Users, label: '代理商管理', href: '/brand/agencies' },
|
||||
]
|
||||
|
||||
interface SidebarProps {
|
||||
role?: 'creator' | 'agency' | 'brand'
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
logo?: React.ReactNode;
|
||||
sections: SidebarSection[];
|
||||
activeId: string;
|
||||
onItemClick?: (id: string) => void;
|
||||
footer?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
export function Sidebar({ role = 'creator' }: SidebarProps) {
|
||||
const pathname = usePathname() || ''
|
||||
|
||||
const navItems = role === 'creator'
|
||||
? creatorNavItems
|
||||
: role === 'agency'
|
||||
? agencyNavItems
|
||||
: brandNavItems
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === `/${role}`) {
|
||||
return pathname === href || pathname === `/${role}/`
|
||||
}
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
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}
|
||||
<aside className="fixed left-0 top-0 bottom-0 z-sidebar w-[260px] bg-bg-card flex flex-col">
|
||||
{/* Logo 区域 */}
|
||||
<div className="flex items-center gap-3 px-6 py-6">
|
||||
<div className="w-9 h-9 rounded-[10px] bg-gradient-to-br from-accent-indigo to-[#4F46E5] flex items-center justify-center">
|
||||
<ShieldCheck className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xl font-bold text-text-primary">秒思</span>
|
||||
</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 className="flex-1 px-4 py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-[10px] transition-colors',
|
||||
active
|
||||
? 'bg-bg-elevated text-text-primary font-semibold'
|
||||
: 'text-text-secondary hover:bg-bg-elevated/50'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="text-[15px]">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</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;
|
||||
export default Sidebar
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* StatusBar 组件测试
|
||||
* 测试覆盖: 时间显示、状态图标、自定义样式
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { StatusBar } from './StatusBar';
|
||||
|
||||
describe('StatusBar', () => {
|
||||
// ==================== 基础渲染测试 ====================
|
||||
describe('基础渲染', () => {
|
||||
it('渲染状态栏', () => {
|
||||
const { container } = render(<StatusBar />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('默认显示时间 9:41', () => {
|
||||
render(<StatusBar />);
|
||||
expect(screen.getByText('9:41')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('渲染状态图标(信号、WiFi、电池)', () => {
|
||||
const { container } = render(<StatusBar />);
|
||||
const icons = container.querySelectorAll('svg');
|
||||
expect(icons).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Time 属性测试 ====================
|
||||
describe('Time 属性', () => {
|
||||
it('支持自定义时间', () => {
|
||||
render(<StatusBar time="10:30" />);
|
||||
expect(screen.getByText('10:30')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('时间使用正确样式', () => {
|
||||
render(<StatusBar time="12:00" />);
|
||||
const timeElement = screen.getByText('12:00');
|
||||
expect(timeElement).toHaveClass('text-text-primary', 'font-semibold', 'text-[17px]');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 样式测试 ====================
|
||||
describe('样式', () => {
|
||||
it('应用固定高度', () => {
|
||||
const { container } = render(<StatusBar />);
|
||||
expect(container.firstChild).toHaveClass('h-[44px]');
|
||||
});
|
||||
|
||||
it('支持自定义 className', () => {
|
||||
const { container } = render(<StatusBar className="custom-status" />);
|
||||
expect(container.firstChild).toHaveClass('custom-status');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,25 @@
|
||||
/**
|
||||
* StatusBar 状态栏组件 (移动端)
|
||||
* 设计稿参考: UIDesignSpec.md 3.1
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Signal, Wifi, BatteryFull } from 'lucide-react';
|
||||
'use client'
|
||||
|
||||
export interface StatusBarProps {
|
||||
time?: string;
|
||||
className?: string;
|
||||
import { Signal, Wifi, BatteryFull } from 'lucide-react'
|
||||
|
||||
interface StatusBarProps {
|
||||
time?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const StatusBar: React.FC<StatusBarProps> = ({
|
||||
time = '9:41',
|
||||
className = '',
|
||||
}) => {
|
||||
export function StatusBar({ time = '9:41', className = '' }: StatusBarProps) {
|
||||
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">
|
||||
<div className={`flex items-center justify-between h-[44px] px-6 w-full ${className}`}>
|
||||
<span className="text-text-primary font-semibold text-[17px]" style={{ fontFamily: 'Inter' }}>
|
||||
{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 className="flex items-center gap-1.5">
|
||||
<Signal className="w-[18px] h-[18px] text-text-primary" />
|
||||
<Wifi className="w-[18px] h-[18px] text-text-primary" />
|
||||
<BatteryFull className="w-6 h-[18px] text-text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusBar;
|
||||
export default StatusBar
|
||||
|
||||
Reference in New Issue
Block a user