feat: 本次提交更新内容如下
样式没问题了,库存流量
This commit is contained in:
@@ -1,12 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, Users, User, Briefcase } from 'lucide-react';
|
||||
import { Home, Users, LayoutGrid, User } from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "首页", id: "home" },
|
||||
{ href: "/scenarios", icon: Users, label: "场景获客", id: "scenarios" },
|
||||
{ href: "/workspace", icon: Briefcase, label: "工作台", id: "workspace" },
|
||||
{ href: "/profile", icon: User, label: "我的", id: "profile" },
|
||||
{
|
||||
id: "home",
|
||||
name: "首页",
|
||||
href: "/",
|
||||
icon: Home,
|
||||
active: (pathname: string) => pathname === "/",
|
||||
},
|
||||
{
|
||||
id: "scenarios",
|
||||
name: "场景获客",
|
||||
href: "/scenarios",
|
||||
icon: Users,
|
||||
active: (pathname: string) => pathname.startsWith("/scenarios"),
|
||||
},
|
||||
{
|
||||
id: "workspace",
|
||||
name: "工作台",
|
||||
href: "/workspace",
|
||||
icon: LayoutGrid,
|
||||
active: (pathname: string) => pathname.startsWith("/workspace"),
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
name: "我的",
|
||||
href: "/profile",
|
||||
icon: User,
|
||||
active: (pathname: string) => pathname.startsWith("/profile"),
|
||||
},
|
||||
];
|
||||
|
||||
interface BottomNavProps {
|
||||
@@ -17,26 +41,26 @@ export default function BottomNav({ activeTab }: BottomNavProps) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 z-50 h-16">
|
||||
<div className="w-full h-full mx-auto flex justify-around items-center">
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 safe-area-pb">
|
||||
<div className="flex justify-around items-center h-16 max-w-md mx-auto">
|
||||
{navItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const isActive = activeTab ? activeTab === item.id : location.pathname === item.href;
|
||||
const isActive = activeTab ? activeTab === item.id : item.active(location.pathname);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={`flex flex-col items-center justify-center py-2 px-3 h-full ${
|
||||
isActive ? "text-blue-500" : "text-gray-500"
|
||||
className={`flex flex-col items-center justify-center flex-1 h-full transition-colors ${
|
||||
isActive ? "text-blue-500" : "text-gray-500 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-6 h-6" />
|
||||
<span className="text-xs mt-1">{item.label}</span>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
<span className="text-xs mt-1">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
296
nkebao/src/components/UnifiedHeader.tsx
Normal file
296
nkebao/src/components/UnifiedHeader.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import React from 'react';
|
||||
import { ChevronLeft, Settings, Bell, Search, RefreshCw, Filter, Plus, MoreVertical } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
interface HeaderAction {
|
||||
type: 'button' | 'icon' | 'search' | 'custom';
|
||||
icon?: React.ComponentType<any>;
|
||||
label?: string;
|
||||
onClick?: () => void;
|
||||
variant?: 'default' | 'ghost' | 'outline' | 'destructive' | 'secondary';
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||
className?: string;
|
||||
content?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface UnifiedHeaderProps {
|
||||
/** 页面标题 */
|
||||
title: string;
|
||||
/** 是否显示返回按钮 */
|
||||
showBack?: boolean;
|
||||
/** 返回按钮文本 */
|
||||
backText?: string;
|
||||
/** 自定义返回逻辑 */
|
||||
onBack?: () => void;
|
||||
/** 默认返回路径 */
|
||||
defaultBackPath?: string;
|
||||
/** 右侧操作按钮 */
|
||||
actions?: HeaderAction[];
|
||||
/** 自定义右侧内容 */
|
||||
rightContent?: React.ReactNode;
|
||||
/** 是否显示搜索框 */
|
||||
showSearch?: boolean;
|
||||
/** 搜索框占位符 */
|
||||
searchPlaceholder?: string;
|
||||
/** 搜索值 */
|
||||
searchValue?: string;
|
||||
/** 搜索回调 */
|
||||
onSearchChange?: (value: string) => void;
|
||||
/** 是否显示底部边框 */
|
||||
showBorder?: boolean;
|
||||
/** 背景样式 */
|
||||
background?: 'white' | 'transparent' | 'blur';
|
||||
/** 自定义CSS类名 */
|
||||
className?: string;
|
||||
/** 标题样式类名 */
|
||||
titleClassName?: string;
|
||||
/** 标题颜色 */
|
||||
titleColor?: 'default' | 'blue' | 'gray';
|
||||
/** 是否居中标题 */
|
||||
centerTitle?: boolean;
|
||||
/** 头部高度 */
|
||||
height?: 'default' | 'compact' | 'tall';
|
||||
}
|
||||
|
||||
const UnifiedHeader: React.FC<UnifiedHeaderProps> = ({
|
||||
title,
|
||||
showBack = true,
|
||||
backText = '返回',
|
||||
onBack,
|
||||
defaultBackPath = '/',
|
||||
actions = [],
|
||||
rightContent,
|
||||
showSearch = false,
|
||||
searchPlaceholder = '搜索...',
|
||||
searchValue = '',
|
||||
onSearchChange,
|
||||
showBorder = true,
|
||||
background = 'white',
|
||||
className = '',
|
||||
titleClassName = '',
|
||||
titleColor = 'default',
|
||||
centerTitle = false,
|
||||
height = 'default',
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else if (defaultBackPath) {
|
||||
navigate(defaultBackPath);
|
||||
} else {
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 背景样式
|
||||
const backgroundClasses = {
|
||||
white: 'bg-white',
|
||||
transparent: 'bg-transparent',
|
||||
blur: 'bg-white/80 backdrop-blur-sm',
|
||||
};
|
||||
|
||||
// 高度样式
|
||||
const heightClasses = {
|
||||
default: 'h-14',
|
||||
compact: 'h-12',
|
||||
tall: 'h-16',
|
||||
};
|
||||
|
||||
// 标题颜色样式
|
||||
const titleColorClasses = {
|
||||
default: 'text-gray-900',
|
||||
blue: 'text-blue-600',
|
||||
gray: 'text-gray-600',
|
||||
};
|
||||
|
||||
const headerClasses = [
|
||||
backgroundClasses[background],
|
||||
heightClasses[height],
|
||||
showBorder ? 'border-b border-gray-200' : '',
|
||||
'sticky top-0 z-50',
|
||||
className,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const titleClasses = [
|
||||
'text-lg font-semibold',
|
||||
titleColorClasses[titleColor],
|
||||
centerTitle ? 'text-center' : '',
|
||||
titleClassName,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
// 渲染操作按钮
|
||||
const renderAction = (action: HeaderAction, index: number) => {
|
||||
if (action.type === 'custom' && action.content) {
|
||||
return <div key={index}>{action.content}</div>;
|
||||
}
|
||||
|
||||
if (action.type === 'search') {
|
||||
return (
|
||||
<div key={index} className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange?.(e.target.value)}
|
||||
className="pl-9 w-48"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IconComponent = action.icon || MoreVertical;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant || 'ghost'}
|
||||
size={action.size || 'icon'}
|
||||
onClick={action.onClick}
|
||||
className={action.className}
|
||||
>
|
||||
<IconComponent className="h-5 w-5" />
|
||||
{action.label && action.size !== 'icon' && (
|
||||
<span className="ml-2">{action.label}</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={headerClasses}>
|
||||
<div className="flex items-center justify-between px-4 h-full">
|
||||
{/* 左侧:返回按钮和标题 */}
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
{showBack && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 hover:bg-gray-100"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
{!centerTitle && (
|
||||
<h1 className={titleClasses}>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 中间:居中标题 */}
|
||||
{centerTitle && (
|
||||
<div className="flex-1 flex justify-center">
|
||||
<h1 className={titleClasses}>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 右侧:搜索框、操作按钮、自定义内容 */}
|
||||
<div className="flex items-center space-x-2 flex-1 justify-end">
|
||||
{showSearch && !actions.some(a => a.type === 'search') && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange?.(e.target.value)}
|
||||
className="pl-9 w-48"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actions.map((action, index) => renderAction(action, index))}
|
||||
|
||||
{rightContent && (
|
||||
<div className="flex items-center space-x-2">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
// 预设的常用Header配置
|
||||
export const HeaderPresets = {
|
||||
// 基础页面Header(有返回按钮)
|
||||
basic: (title: string, onBack?: () => void): UnifiedHeaderProps => ({
|
||||
title,
|
||||
showBack: true,
|
||||
onBack,
|
||||
titleColor: 'blue',
|
||||
}),
|
||||
|
||||
// 主页Header(无返回按钮)
|
||||
main: (title: string, actions?: HeaderAction[]): UnifiedHeaderProps => ({
|
||||
title,
|
||||
showBack: false,
|
||||
titleColor: 'blue',
|
||||
actions: actions || [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Bell,
|
||||
onClick: () => console.log('Notifications'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
// 搜索页面Header
|
||||
search: (title: string, searchValue: string, onSearchChange: (value: string) => void): UnifiedHeaderProps => ({
|
||||
title,
|
||||
showBack: true,
|
||||
showSearch: true,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
titleColor: 'blue',
|
||||
}),
|
||||
|
||||
// 列表页面Header(带刷新和添加)
|
||||
list: (title: string, onRefresh?: () => void, onAdd?: () => void): UnifiedHeaderProps => ({
|
||||
title,
|
||||
showBack: true,
|
||||
titleColor: 'blue',
|
||||
actions: [
|
||||
...(onRefresh ? [{
|
||||
type: 'icon' as const,
|
||||
icon: RefreshCw,
|
||||
onClick: onRefresh,
|
||||
}] : []),
|
||||
...(onAdd ? [{
|
||||
type: 'button' as const,
|
||||
icon: Plus,
|
||||
label: '新建',
|
||||
size: 'sm' as const,
|
||||
onClick: onAdd,
|
||||
}] : []),
|
||||
],
|
||||
}),
|
||||
|
||||
// 设置页面Header
|
||||
settings: (title: string): UnifiedHeaderProps => ({
|
||||
title,
|
||||
showBack: true,
|
||||
titleColor: 'blue',
|
||||
actions: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Settings,
|
||||
onClick: () => console.log('Settings'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
export default UnifiedHeader;
|
||||
17
nkebao/src/hooks/use-debounce.ts
Normal file
17
nkebao/src/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -1,87 +1,287 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Bell, Smartphone, Users, Activity } from 'lucide-react';
|
||||
import { Bell, Smartphone, Users, Activity, MessageSquare, TrendingUp } from 'lucide-react';
|
||||
import Chart from 'chart.js/auto';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import UnifiedHeader, { HeaderPresets } from '@/components/UnifiedHeader';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
// 模拟数据
|
||||
const stats = {
|
||||
totalDevices: 12,
|
||||
totalWechatAccounts: 8,
|
||||
onlineWechatAccounts: 6,
|
||||
};
|
||||
// API接口定义
|
||||
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com";
|
||||
|
||||
const scenarioFeatures = [
|
||||
{
|
||||
id: "3",
|
||||
name: "抖音获客",
|
||||
value: 156,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
|
||||
color: "bg-red-100",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "小红书获客",
|
||||
value: 89,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||
color: "bg-pink-100",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "公众号获客",
|
||||
value: 234,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
|
||||
color: "bg-green-100",
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
name: "海报获客",
|
||||
value: 167,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
|
||||
color: "bg-blue-100",
|
||||
},
|
||||
];
|
||||
// 统一的API请求客户端
|
||||
async function apiRequest<T>(url: string): Promise<T> {
|
||||
try {
|
||||
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
console.log("发送API请求:", url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
mode: "cors",
|
||||
});
|
||||
|
||||
console.log("API响应状态:", response.status, response.statusText);
|
||||
|
||||
// 检查响应头的Content-Type
|
||||
const contentType = response.headers.get("content-type");
|
||||
console.log("响应Content-Type:", contentType);
|
||||
|
||||
if (!response.ok) {
|
||||
// 如果是401未授权,清除本地存储
|
||||
if (response.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("userInfo");
|
||||
}
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// 检查是否是JSON响应
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const text = await response.text();
|
||||
console.log("非JSON响应内容:", text.substring(0, 200));
|
||||
throw new Error("服务器返回了非JSON格式的数据,可能是HTML错误页面");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("API响应数据:", data);
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.code && data.code !== 200 && data.code !== 0) {
|
||||
throw new Error(data.message || "请求失败");
|
||||
}
|
||||
|
||||
return data.data || data;
|
||||
} catch (error) {
|
||||
console.error("API请求失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const chartInstance = useRef<any>(null);
|
||||
|
||||
// 统一设备数据
|
||||
const [stats, setStats] = useState({
|
||||
totalDevices: 0,
|
||||
onlineDevices: 0,
|
||||
totalWechatAccounts: 0,
|
||||
onlineWechatAccounts: 0,
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [apiError, setApiError] = useState("");
|
||||
|
||||
// 场景获客数据
|
||||
const scenarioFeatures = [
|
||||
{
|
||||
id: "douyin",
|
||||
name: "抖音获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
|
||||
color: "bg-blue-100 text-blue-600",
|
||||
value: 156,
|
||||
growth: 12,
|
||||
},
|
||||
{
|
||||
id: "xiaohongshu",
|
||||
name: "小红书获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||
color: "bg-red-100 text-red-600",
|
||||
value: 89,
|
||||
growth: 8,
|
||||
},
|
||||
{
|
||||
id: "gongzhonghao",
|
||||
name: "公众号获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
|
||||
color: "bg-green-100 text-green-600",
|
||||
value: 234,
|
||||
growth: 15,
|
||||
},
|
||||
{
|
||||
id: "haibao",
|
||||
name: "海报获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
|
||||
color: "bg-orange-100 text-orange-600",
|
||||
value: 167,
|
||||
growth: 10,
|
||||
},
|
||||
];
|
||||
|
||||
// 今日数据统计
|
||||
const todayStats = [
|
||||
{
|
||||
title: "朋友圈同步",
|
||||
value: "12",
|
||||
icon: <MessageSquare className="h-4 w-4" />,
|
||||
color: "text-purple-600",
|
||||
path: "/workspace/moments-sync",
|
||||
},
|
||||
{
|
||||
title: "群发任务",
|
||||
value: "8",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
color: "text-orange-600",
|
||||
path: "/workspace/group-push",
|
||||
},
|
||||
{
|
||||
title: "获客转化",
|
||||
value: "85%",
|
||||
icon: <TrendingUp className="h-4 w-4" />,
|
||||
color: "text-green-600",
|
||||
path: "/scenarios",
|
||||
},
|
||||
{
|
||||
title: "系统活跃度",
|
||||
value: "98%",
|
||||
icon: <Activity className="h-4 w-4" />,
|
||||
color: "text-blue-600",
|
||||
path: "/workspace",
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// 获取统计数据
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setApiError("");
|
||||
|
||||
// 检查是否有token
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
console.log("未找到登录token,使用默认数据");
|
||||
setStats({
|
||||
totalDevices: 42,
|
||||
onlineDevices: 35,
|
||||
totalWechatAccounts: 42,
|
||||
onlineWechatAccounts: 35,
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试请求API数据
|
||||
try {
|
||||
// 并行请求多个接口
|
||||
const [deviceStatsResult, wechatStatsResult] = await Promise.allSettled([
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`),
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`),
|
||||
]);
|
||||
|
||||
const newStats = {
|
||||
totalDevices: 0,
|
||||
onlineDevices: 0,
|
||||
totalWechatAccounts: 0,
|
||||
onlineWechatAccounts: 0,
|
||||
};
|
||||
|
||||
// 处理设备统计数据
|
||||
if (deviceStatsResult.status === "fulfilled") {
|
||||
const deviceData = deviceStatsResult.value as any;
|
||||
newStats.totalDevices = deviceData.total || 0;
|
||||
newStats.onlineDevices = deviceData.online || 0;
|
||||
} else {
|
||||
console.warn("设备统计API失败:", deviceStatsResult.reason);
|
||||
}
|
||||
|
||||
// 处理微信号统计数据
|
||||
if (wechatStatsResult.status === "fulfilled") {
|
||||
const wechatData = wechatStatsResult.value as any;
|
||||
newStats.totalWechatAccounts = wechatData.total || 0;
|
||||
newStats.onlineWechatAccounts = wechatData.active || 0;
|
||||
} else {
|
||||
console.warn("微信号统计API失败:", wechatStatsResult.reason);
|
||||
}
|
||||
|
||||
setStats(newStats);
|
||||
} catch (apiError) {
|
||||
console.warn("API请求失败,使用默认数据:", apiError);
|
||||
setApiError(apiError instanceof Error ? apiError.message : "API连接失败");
|
||||
|
||||
// 使用默认数据
|
||||
setStats({
|
||||
totalDevices: 42,
|
||||
onlineDevices: 35,
|
||||
totalWechatAccounts: 42,
|
||||
onlineWechatAccounts: 35,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取统计数据失败:", error);
|
||||
setApiError(error instanceof Error ? error.message : "数据加载失败");
|
||||
|
||||
// 使用默认数据
|
||||
setStats({
|
||||
totalDevices: 42,
|
||||
onlineDevices: 35,
|
||||
totalWechatAccounts: 42,
|
||||
onlineWechatAccounts: 35,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
|
||||
// 定时刷新数据(每30秒)
|
||||
const interval = setInterval(fetchStats, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []); // 移除stats依赖
|
||||
|
||||
const handleDevicesClick = () => {
|
||||
navigate('/devices');
|
||||
navigate('/profile/devices');
|
||||
};
|
||||
|
||||
const handleWechatClick = () => {
|
||||
navigate('/wechat-accounts');
|
||||
};
|
||||
|
||||
// 使用Chart.js创建图表
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
if (chartRef.current && !isLoading) {
|
||||
// 如果已经有图表实例,先销毁它
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
}
|
||||
|
||||
// 销毁旧实例
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
chartInstance.current = null;
|
||||
}
|
||||
const ctx = chartRef.current.getContext("2d");
|
||||
|
||||
// 添加null检查
|
||||
if (!ctx) return;
|
||||
|
||||
const ctx = chartRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
// 创建新的图表实例
|
||||
chartInstance.current = new Chart(ctx, {
|
||||
type: 'line',
|
||||
type: "line",
|
||||
data: {
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||
datasets: [
|
||||
{
|
||||
label: '获客数量',
|
||||
data: [12, 19, 15, 25, 22, 30, 28],
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
||||
borderColor: 'rgba(59, 130, 246, 1)',
|
||||
label: "获客数量",
|
||||
data: [120, 150, 180, 200, 230, 210, 190],
|
||||
backgroundColor: "rgba(59, 130, 246, 0.2)",
|
||||
borderColor: "rgba(59, 130, 246, 1)",
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: 'rgba(59, 130, 246, 1)',
|
||||
pointBackgroundColor: "rgba(59, 130, 246, 1)",
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
@@ -94,15 +294,15 @@ export default function Home() {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#333',
|
||||
bodyColor: '#666',
|
||||
borderColor: '#ddd',
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||
titleColor: "#333",
|
||||
bodyColor: "#666",
|
||||
borderColor: "#ddd",
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (context: any) => `获客数量: ${context.parsed.y}`,
|
||||
label: (context) => `获客数量: ${context.parsed.y}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -111,21 +311,11 @@ export default function Home() {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12, // 0.75rem
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12, // 0.75rem
|
||||
},
|
||||
color: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -133,91 +323,126 @@ export default function Home() {
|
||||
});
|
||||
}
|
||||
|
||||
// 卸载时销毁
|
||||
// 组件卸载时清理图表实例
|
||||
return () => {
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
chartInstance.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div className="bg-white border-b">
|
||||
<div className="flex justify-between items-center p-4">
|
||||
<h1 className="text-xl font-semibold text-blue-600">存客宝</h1>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Card key={i} className="p-3 bg-white animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-6 bg-gray-200 rounded"></div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div className="bg-white border-b">
|
||||
<div className="flex justify-between items-center p-4">
|
||||
<h1 className="text-xl font-semibold text-blue-600">存客宝</h1>
|
||||
<button className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<Bell className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UnifiedHeader
|
||||
title="存客宝"
|
||||
showBack={false}
|
||||
titleColor="blue"
|
||||
rightContent={
|
||||
<>
|
||||
{apiError && (
|
||||
<div className="text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded mr-2">
|
||||
API连接异常,显示默认数据
|
||||
</div>
|
||||
)}
|
||||
<button className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<Bell className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50">
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
className="p-4 bg-white hover:shadow-lg transition-all cursor-pointer rounded shadow"
|
||||
onClick={handleDevicesClick}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-500 mb-2">设备数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-blue-600">{stats.totalDevices}</span>
|
||||
<Smartphone className="w-6 h-6 text-blue-600" />
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="cursor-pointer" onClick={handleDevicesClick}>
|
||||
<Card className="p-3 bg-white hover:shadow-md transition-all">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">设备数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.totalDevices}</span>
|
||||
<Smartphone className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div
|
||||
className="p-4 bg-white hover:shadow-lg transition-all cursor-pointer rounded shadow"
|
||||
onClick={handleWechatClick}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-500 mb-2">微信号数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-blue-600">{stats.totalWechatAccounts}</span>
|
||||
<Users className="w-6 h-6 text-blue-600" />
|
||||
<div className="cursor-pointer" onClick={handleWechatClick}>
|
||||
<Card className="p-3 bg-white hover:shadow-md transition-all">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">微信号数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.totalWechatAccounts}</span>
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="p-4 bg-white rounded shadow">
|
||||
<Card className="p-3 bg-white">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-500 mb-2">在线微信号</span>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-2xl font-bold text-blue-600">{stats.onlineWechatAccounts}</span>
|
||||
<Activity className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="bg-blue-600 h-1 rounded-full"
|
||||
style={{ width: `${(stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100}%` }}
|
||||
></div>
|
||||
<span className="text-xs text-gray-500 mb-1">在线微信号</span>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.onlineWechatAccounts}</span>
|
||||
<Activity className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
stats.totalWechatAccounts > 0 ? (stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100 : 0
|
||||
}
|
||||
className="h-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 场景获客统计 */}
|
||||
<div className="p-4 bg-white rounded shadow">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold">场景获客统计</h2>
|
||||
<Card className="p-4 bg-white">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h2 className="text-base font-semibold">场景获客统计</h2>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{scenarioFeatures
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 4) // 只显示前4个
|
||||
.map((scenario) => (
|
||||
<div
|
||||
key={scenario.id}
|
||||
className="block flex-1 cursor-pointer"
|
||||
key={scenario.id}
|
||||
className="block flex-1 cursor-pointer"
|
||||
onClick={() => navigate(`/scenarios/${scenario.id}?name=${encodeURIComponent(scenario.name)}`)}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-2">
|
||||
<div className={`w-12 h-12 rounded-full ${scenario.color} flex items-center justify-center`}>
|
||||
<img src={scenario.icon || "/placeholder.svg"} alt={scenario.name} className="w-6 h-6" />
|
||||
<div className="flex flex-col items-center text-center space-y-1">
|
||||
<div className={`w-10 h-10 rounded-full ${scenario.color} flex items-center justify-center`}>
|
||||
<img src={scenario.icon || "/placeholder.svg"} alt={scenario.name} className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="text-sm font-medium">{scenario.value}</div>
|
||||
<div className="text-xs text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis w-full">
|
||||
@@ -227,15 +452,37 @@ export default function Home() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 今日数据统计 */}
|
||||
<Card className="p-4 bg-white">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h2 className="text-base font-semibold">今日数据</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{todayStats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={() => stat.path && navigate(stat.path)}
|
||||
>
|
||||
<div className={`p-2 rounded-full bg-white ${stat.color}`}>{stat.icon}</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{stat.value}</div>
|
||||
<div className="text-xs text-gray-500">{stat.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 每日获客趋势 */}
|
||||
<div className="p-4 bg-white rounded shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">每日获客趋势</h2>
|
||||
<div className="w-full h-64 relative">
|
||||
<Card className="p-4 bg-white">
|
||||
<h2 className="text-base font-semibold mb-3">每日获客趋势</h2>
|
||||
<div className="w-full h-48 relative">
|
||||
<canvas ref={chartRef} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronRight, Settings, Bell, LogOut } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { ChevronRight, Settings, Bell, LogOut, Smartphone, MessageCircle, Database, FolderOpen } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
@@ -9,21 +9,21 @@ import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/devices', label: '设备管理' },
|
||||
{ href: '/wechat-accounts', label: '微信号管理' },
|
||||
{ href: '/traffic-pool', label: '流量池' },
|
||||
{ href: '/content', label: '内容库' },
|
||||
];
|
||||
|
||||
export default function Profile() {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [stats, setStats] = useState({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
traffic: 8,
|
||||
content: 156,
|
||||
});
|
||||
|
||||
// 从localStorage获取用户信息
|
||||
useEffect(() => {
|
||||
@@ -33,6 +33,82 @@ export default function Profile() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 用户信息
|
||||
const currentUserInfo = {
|
||||
name: userInfo?.username || user?.username || "卡若",
|
||||
email: userInfo?.email || "zhangsan@example.com",
|
||||
role: "管理员",
|
||||
joinDate: "2023-01-15",
|
||||
lastLogin: "2024-01-20 14:30",
|
||||
};
|
||||
|
||||
// 功能模块数据
|
||||
const functionModules = [
|
||||
{
|
||||
id: "devices",
|
||||
title: "设备管理",
|
||||
description: "管理您的设备和微信账号",
|
||||
icon: <Smartphone className="h-5 w-5 text-blue-500" />,
|
||||
count: stats.devices,
|
||||
path: "/devices",
|
||||
bgColor: "bg-blue-50",
|
||||
},
|
||||
{
|
||||
id: "wechat",
|
||||
title: "微信号管理",
|
||||
description: "管理微信账号和好友",
|
||||
icon: <MessageCircle className="h-5 w-5 text-green-500" />,
|
||||
count: stats.wechat,
|
||||
path: "/wechat-accounts",
|
||||
bgColor: "bg-green-50",
|
||||
},
|
||||
{
|
||||
id: "traffic",
|
||||
title: "流量池",
|
||||
description: "管理用户流量池和分组",
|
||||
icon: <Database className="h-5 w-5 text-purple-500" />,
|
||||
count: stats.traffic,
|
||||
path: "/traffic-pool",
|
||||
bgColor: "bg-purple-50",
|
||||
},
|
||||
{
|
||||
id: "content",
|
||||
title: "内容库",
|
||||
description: "管理营销内容和素材",
|
||||
icon: <FolderOpen className="h-5 w-5 text-orange-500" />,
|
||||
count: stats.content,
|
||||
path: "/content",
|
||||
bgColor: "bg-orange-50",
|
||||
},
|
||||
];
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 这里可以调用实际的API
|
||||
// const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([
|
||||
// getDeviceStats(),
|
||||
// getWechatStats(),
|
||||
// getTrafficStats(),
|
||||
// getContentStats(),
|
||||
// ]);
|
||||
|
||||
// 暂时使用模拟数据
|
||||
setStats({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
traffic: 8,
|
||||
content: 156,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载统计数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem('token');
|
||||
@@ -43,11 +119,15 @@ export default function Profile() {
|
||||
logout();
|
||||
navigate('/login');
|
||||
toast({
|
||||
title: '已退出登录',
|
||||
description: '感谢使用存客宝',
|
||||
title: '退出成功',
|
||||
description: '您已安全退出系统',
|
||||
});
|
||||
};
|
||||
|
||||
const handleFunctionClick = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
@@ -55,88 +135,105 @@ export default function Profile() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div className="bg-white/80 backdrop-blur-sm border-b">
|
||||
<div className="flex justify-between items-center p-4">
|
||||
<h1 className="text-xl font-semibold text-blue-600">我的</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UnifiedHeader
|
||||
title="我的"
|
||||
showBack={false}
|
||||
titleColor="blue"
|
||||
actions={[
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Bell,
|
||||
onClick: () => console.log('Notifications'),
|
||||
},
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Settings,
|
||||
onClick: () => console.log('Settings'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gradient-to-b from-blue-50 to-white">
|
||||
<div className="p-4 space-y-6">
|
||||
{/* 用户信息卡片 */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={userInfo?.avatar || user?.avatar || ''} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xl font-semibold shadow-lg">
|
||||
{(userInfo?.username || user?.username || '用户').slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-blue-600">
|
||||
{userInfo?.username || user?.username || '用户'}
|
||||
</h2>
|
||||
<p className="text-gray-500">
|
||||
账号: {userInfo?.account || user?.account || Math.floor(10000000 + Math.random() * 90000000).toString()}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '编辑资料功能正在开发中',
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑资料
|
||||
</Button>
|
||||
<div className="bg-gray-50 pb-16">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 用户信息卡片 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={userInfo?.avatar || user?.avatar || ''} />
|
||||
<AvatarFallback className="bg-gray-200 text-gray-600 text-lg font-medium">
|
||||
{currentUserInfo.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<h2 className="text-lg font-medium">{currentUserInfo.name}</h2>
|
||||
<span className="px-2 py-1 text-xs bg-gradient-to-r from-orange-400 to-orange-500 text-white rounded-full font-medium shadow-sm">
|
||||
{currentUserInfo.role}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">{currentUserInfo.email}</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div>最近登录: {currentUserInfo.lastLogin}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 功能菜单 */}
|
||||
<Card className="divide-y">
|
||||
{menuItems.map((item) => (
|
||||
<div
|
||||
key={item.href || item.label}
|
||||
className="p-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => (item.href ? navigate(item.href) : null)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{item.label}</span>
|
||||
{/* 我的功能 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-2">
|
||||
{functionModules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
className="flex items-center p-4 rounded-lg border hover:bg-gray-50 cursor-pointer transition-colors w-full"
|
||||
onClick={() => handleFunctionClick(module.path)}
|
||||
>
|
||||
<div className={`p-2 rounded-lg ${module.bgColor} mr-3`}>{module.icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{module.title}</div>
|
||||
<div className="text-xs text-gray-500">{module.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2 py-1 text-xs bg-gray-50 text-gray-700 rounded-full border border-gray-200 font-medium shadow-sm">
|
||||
{module.count}
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 退出登录按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full text-red-500 hover:text-red-600 hover:bg-red-50 mt-6"
|
||||
onClick={() => setShowLogoutDialog(true)}
|
||||
>
|
||||
<LogOut className="w-5 h-5 mr-2" />
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
{/* 退出登录 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-red-600 border-red-200 hover:bg-red-50 bg-transparent"
|
||||
onClick={() => setShowLogoutDialog(true)}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 退出登录确认对话框 */}
|
||||
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, TrendingUp, Loader2 } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import { fetchScenes, type SceneItem } from '@/api/scenarios';
|
||||
@@ -80,7 +80,7 @@ export default function Scenarios() {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
<UnifiedHeader
|
||||
title="场景获客"
|
||||
showBack={false}
|
||||
/>
|
||||
@@ -101,7 +101,7 @@ export default function Scenarios() {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
<UnifiedHeader
|
||||
title="场景获客"
|
||||
showBack={false}
|
||||
/>
|
||||
@@ -126,18 +126,19 @@ export default function Scenarios() {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
<UnifiedHeader
|
||||
title="场景获客"
|
||||
showBack={false}
|
||||
rightContent={
|
||||
<button
|
||||
onClick={handleNewPlan}
|
||||
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新建计划
|
||||
</button>
|
||||
}
|
||||
titleColor="blue"
|
||||
actions={[
|
||||
{
|
||||
type: 'button',
|
||||
icon: Plus,
|
||||
label: '新建计划',
|
||||
size: 'sm',
|
||||
onClick: handleNewPlan,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
@@ -106,8 +106,9 @@ export default function Workspace() {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
<UnifiedHeader
|
||||
title="工作台"
|
||||
titleColor="blue"
|
||||
defaultBackPath="/"
|
||||
/>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user