feat: 登录模块完成
This commit is contained in:
@@ -1,142 +1,48 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Smartphone, Users, Activity, Bell } from 'lucide-react';
|
||||
Chart.register(...registerables);
|
||||
import { Bell, Smartphone, Users, Activity } from 'lucide-react';
|
||||
|
||||
// 模拟数据
|
||||
const stats = {
|
||||
totalDevices: 12,
|
||||
totalWechatAccounts: 8,
|
||||
onlineWechatAccounts: 6,
|
||||
};
|
||||
|
||||
const scenarioFeatures = [
|
||||
{
|
||||
id: "douyin",
|
||||
name: "抖音获客",
|
||||
value: 156,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
|
||||
color: "bg-red-100",
|
||||
},
|
||||
{
|
||||
id: "xiaohongshu",
|
||||
name: "小红书获客",
|
||||
value: 89,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||
color: "bg-pink-100",
|
||||
},
|
||||
{
|
||||
id: "gongzhonghao",
|
||||
name: "公众号获客",
|
||||
value: 234,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
|
||||
color: "bg-green-100",
|
||||
},
|
||||
{
|
||||
id: "haibao",
|
||||
name: "海报获客",
|
||||
value: 167,
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
|
||||
color: "bg-blue-100",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const chartInstance = useRef<Chart | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 统计数据状态
|
||||
const [stats, setStats] = useState({
|
||||
totalDevices: 0,
|
||||
onlineDevices: 0,
|
||||
totalWechatAccounts: 0,
|
||||
onlineWechatAccounts: 0,
|
||||
});
|
||||
|
||||
// 业务场景数据
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取统计数据
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
// 这里可以调用实际的API
|
||||
// const response = await fetch('/api/stats');
|
||||
// const data = await response.json();
|
||||
|
||||
// 模拟数据
|
||||
setStats({
|
||||
totalDevices: 42,
|
||||
onlineDevices: 35,
|
||||
totalWechatAccounts: 42,
|
||||
onlineWechatAccounts: 35,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current) {
|
||||
if (chartInstance.current) chartInstance.current.destroy();
|
||||
chartInstance.current = new Chart(chartRef.current, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
datasets: [
|
||||
{
|
||||
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)',
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#333',
|
||||
bodyColor: '#666',
|
||||
borderColor: '#ddd',
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (context) => `获客数量: ${context.parsed.y}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (chartInstance.current) chartInstance.current.destroy();
|
||||
};
|
||||
}, []);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const handleDevicesClick = () => {
|
||||
navigate('/devices');
|
||||
@@ -146,6 +52,44 @@ export default function Home() {
|
||||
navigate('/wechat-accounts');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current) {
|
||||
const ctx = chartRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
// 清除之前的图表
|
||||
ctx.clearRect(0, 0, chartRef.current.width, chartRef.current.height);
|
||||
|
||||
// 绘制简单的柱状图
|
||||
const data = [12, 19, 15, 25, 22, 30, 28];
|
||||
const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
const barWidth = 30;
|
||||
const barSpacing = 20;
|
||||
const startX = 50;
|
||||
const startY = 200;
|
||||
|
||||
ctx.fillStyle = '#3B82F6';
|
||||
data.forEach((value, index) => {
|
||||
const x = startX + index * (barWidth + barSpacing);
|
||||
const height = (value / 30) * 150;
|
||||
const y = startY - height;
|
||||
|
||||
ctx.fillRect(x, y, barWidth, height);
|
||||
|
||||
// 绘制数值
|
||||
ctx.fillStyle = '#374151';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(value.toString(), x + barWidth / 2, y - 5);
|
||||
|
||||
// 绘制标签
|
||||
ctx.fillText(labels[index], x + barWidth / 2, startY + 20);
|
||||
|
||||
ctx.fillStyle = '#3B82F6';
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
|
||||
@@ -1,5 +1,432 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Eye, EyeOff, Phone } from 'lucide-react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useToast } from '../../components/ui/toast';
|
||||
import { authApi } from '../../api';
|
||||
import WeChatIcon from '../../components/icons/WeChatIcon';
|
||||
import AppleIcon from '../../components/icons/AppleIcon';
|
||||
|
||||
// 定义登录表单类型
|
||||
interface LoginForm {
|
||||
phone: string;
|
||||
password: string;
|
||||
verificationCode: string;
|
||||
agreeToTerms: boolean;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
return <div>登录页</div>;
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'password' | 'verification'>('password');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [form, setForm] = useState<LoginForm>({
|
||||
phone: '',
|
||||
password: '',
|
||||
verificationCode: '',
|
||||
agreeToTerms: false,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { toast } = useToast();
|
||||
const { login } = useAuth();
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm((prev) => ({ ...prev, agreeToTerms: e.target.checked }));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (!form.phone) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请输入手机号',
|
||||
description: '手机号不能为空',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(form.phone)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '手机号格式错误',
|
||||
description: '请输入正确的11位手机号',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!form.agreeToTerms) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请同意用户协议',
|
||||
description: '需要同意用户协议和隐私政策才能继续',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTab === 'password' && !form.password) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请输入密码',
|
||||
description: '密码不能为空',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTab === 'verification' && !form.verificationCode) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请输入验证码',
|
||||
description: '验证码不能为空',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (activeTab === 'password') {
|
||||
// 发送账号密码登录请求
|
||||
const response = await authApi.login(form.phone, form.password);
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
// 保存登录信息
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('token_expired', response.data.token_expired);
|
||||
localStorage.setItem('s2_accountId', response.data.member.s2_accountId);
|
||||
|
||||
// 保存用户信息
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data.member));
|
||||
|
||||
// 调用认证上下文的登录方法
|
||||
login(response.data.token, response.data.member);
|
||||
|
||||
// 显示成功提示
|
||||
toast({
|
||||
title: '登录成功',
|
||||
description: '欢迎回来!',
|
||||
});
|
||||
|
||||
// 跳转到首页或重定向URL
|
||||
const returnUrl = searchParams.get('returnUrl');
|
||||
if (returnUrl) {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '登录失败');
|
||||
}
|
||||
} else {
|
||||
// 验证码登录
|
||||
const response = await authApi.loginWithCode(form.phone, form.verificationCode);
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
// 保存登录信息
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('token_expired', response.data.token_expired);
|
||||
localStorage.setItem('s2_accountId', response.data.member.s2_accountId);
|
||||
|
||||
// 保存用户信息
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data.member));
|
||||
|
||||
// 调用认证上下文的登录方法
|
||||
login(response.data.token, response.data.member);
|
||||
|
||||
// 显示成功提示
|
||||
toast({
|
||||
title: '登录成功',
|
||||
description: '欢迎回来!',
|
||||
});
|
||||
|
||||
// 跳转到首页或重定向URL
|
||||
const returnUrl = searchParams.get('returnUrl');
|
||||
if (returnUrl) {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '登录失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '登录失败',
|
||||
description: error instanceof Error ? error.message : '请稍后重试',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendVerificationCode = async () => {
|
||||
if (!form.phone) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请输入手机号',
|
||||
description: '发送验证码需要手机号',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(form.phone)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '手机号格式错误',
|
||||
description: '请输入正确的11位手机号',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await authApi.sendVerificationCode(form.phone);
|
||||
|
||||
if (response.code === 200) {
|
||||
toast({
|
||||
title: '验证码已发送',
|
||||
description: '请查收短信验证码',
|
||||
});
|
||||
setCountdown(60); // 开始60秒倒计时
|
||||
} else {
|
||||
throw new Error(response.message || '发送失败');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '发送失败',
|
||||
description: error instanceof Error ? error.message : '请稍后重试',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWechatLogin = () => {
|
||||
// 微信登录逻辑
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '微信登录功能正在开发中,请使用其他方式登录',
|
||||
});
|
||||
};
|
||||
|
||||
const handleAppleLogin = () => {
|
||||
// Apple登录逻辑
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: 'Apple登录功能正在开发中,请使用其他方式登录',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center px-4 py-8">
|
||||
<div className="max-w-md w-full">
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">欢迎登录</h1>
|
||||
<p className="text-gray-600 text-sm">你所在地区仅支持 手机号 / 微信 / Apple 登录</p>
|
||||
</div>
|
||||
|
||||
{/* 标签页切换 */}
|
||||
<div className="flex border-b border-gray-200 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab('password')}
|
||||
className={`flex-1 py-3 text-center border-b-2 transition-colors font-medium ${
|
||||
activeTab === 'password'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
密码登录
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('verification')}
|
||||
className={`flex-1 py-3 text-center border-b-2 transition-colors font-medium ${
|
||||
activeTab === 'verification'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
验证码登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
{/* 手机号输入 */}
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
手机号
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={form.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入手机号"
|
||||
className="w-full pl-16 pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 transition-colors"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 flex items-center gap-1 text-sm">
|
||||
<Phone className="h-4 w-4" />
|
||||
+86
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 密码输入 */}
|
||||
{activeTab === 'password' && (
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
密码
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入密码"
|
||||
className="w-full pl-4 pr-12 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 transition-colors"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 验证码输入 */}
|
||||
{activeTab === 'verification' && (
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
验证码
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="verificationCode"
|
||||
value={form.verificationCode}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入验证码"
|
||||
className="w-full pl-4 pr-32 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 transition-colors"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendVerificationCode}
|
||||
disabled={isLoading || countdown > 0}
|
||||
className={`absolute right-2 top-1/2 -translate-y-1/2 px-4 h-10 rounded-md text-sm font-medium transition-colors ${
|
||||
countdown > 0
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-50 text-blue-500 hover:bg-blue-100'
|
||||
}`}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户协议 */}
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
checked={form.agreeToTerms}
|
||||
onChange={handleCheckboxChange}
|
||||
disabled={isLoading}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5"
|
||||
/>
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm text-gray-600 leading-relaxed cursor-pointer"
|
||||
>
|
||||
我已阅读并同意
|
||||
<a href="#" className="text-blue-500 hover:text-blue-600 mx-1">《存客宝用户协议》</a>
|
||||
和
|
||||
<a href="#" className="text-blue-500 hover:text-blue-600 mx-1">《隐私政策》</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
登录中...
|
||||
</div>
|
||||
) : (
|
||||
'登录'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className="relative my-8">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-white text-gray-500">其他登录方式</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<div className="flex justify-center space-x-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleWechatLogin}
|
||||
className="flex flex-col items-center space-y-2 p-4 text-gray-500 hover:text-gray-700 transition-colors rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<WeChatIcon className="h-8 w-8" />
|
||||
<span className="text-xs">微信</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAppleLogin}
|
||||
className="flex flex-col items-center space-y-2 p-4 text-gray-500 hover:text-gray-700 transition-colors rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<AppleIcon className="h-8 w-8" />
|
||||
<span className="text-xs">Apple</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, Users, TrendingUp, Calendar } from 'lucide-react';
|
||||
import { Plus, Calendar } from 'lucide-react';
|
||||
|
||||
interface Plan {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,153 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useToast } from '../../components/ui/toast';
|
||||
import { LogOut, User, Settings, Shield, Bell } from 'lucide-react';
|
||||
|
||||
export default function Profile() {
|
||||
return <div>个人中心页</div>;
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
toast({
|
||||
title: '已退出登录',
|
||||
description: '感谢使用存客宝',
|
||||
});
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
icon: <User className="h-5 w-5" />,
|
||||
title: '个人信息',
|
||||
description: '查看和编辑个人资料',
|
||||
onClick: () => {
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '个人信息编辑功能正在开发中',
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
title: '账户设置',
|
||||
description: '密码、安全设置等',
|
||||
onClick: () => {
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '账户设置功能正在开发中',
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
title: '隐私安全',
|
||||
description: '隐私设置和安全选项',
|
||||
onClick: () => {
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '隐私安全功能正在开发中',
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <Bell className="h-5 w-5" />,
|
||||
title: '消息通知',
|
||||
description: '通知设置和消息管理',
|
||||
onClick: () => {
|
||||
toast({
|
||||
title: '功能开发中',
|
||||
description: '消息通知功能正在开发中',
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="text-gray-500">请先登录</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-md mx-auto space-y-6">
|
||||
{/* 用户信息卡片 */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
{user?.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt="头像"
|
||||
className="w-16 h-16 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="h-8 w-8 text-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{user?.username || '用户'}
|
||||
</h2>
|
||||
<p className="text-gray-500">
|
||||
{user?.account || '暂无手机号'}
|
||||
</p>
|
||||
{user?.s2_accountId && (
|
||||
<p className="text-sm text-gray-400">
|
||||
ID: {user.s2_accountId}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 菜单列表 */}
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
{menuItems.map((item, index) => (
|
||||
<div key={index}>
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className="w-full flex items-center space-x-4 p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="text-gray-400">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
<p className="text-sm text-gray-500">{item.description}</p>
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
{index < menuItems.length - 1 && (
|
||||
<div className="border-b border-gray-100 mx-4" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 退出登录按钮 */}
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center space-x-4 p-4 text-red-600 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
<span className="font-medium">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 版本信息 */}
|
||||
<div className="text-center text-sm text-gray-400">
|
||||
<p>存客宝 v1.0.0</p>
|
||||
<p className="mt-1">© 2024 存客宝. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Plus, Users, TrendingUp, Calendar, Settings } from 'lucide-react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Plus, Users, TrendingUp, Calendar } from 'lucide-react';
|
||||
|
||||
interface Plan {
|
||||
id: string;
|
||||
@@ -26,7 +26,6 @@ interface ScenarioData {
|
||||
export default function ScenarioDetail() {
|
||||
const { scenarioId } = useParams<{ scenarioId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [scenario, setScenario] = useState<ScenarioData | null>(null);
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -17,36 +17,36 @@ export default function Scenarios() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// AI智能获客用本地 mock 数据
|
||||
const aiScenarios = [
|
||||
{
|
||||
id: "ai-friend",
|
||||
name: "AI智能加友",
|
||||
icon: "🤖",
|
||||
count: 245,
|
||||
growth: "+18.5%",
|
||||
description: "智能分析目标用户画像,自动筛选优质客户",
|
||||
path: "/scenarios/ai-friend",
|
||||
},
|
||||
{
|
||||
id: "ai-group",
|
||||
name: "AI群引流",
|
||||
icon: "🤖",
|
||||
count: 178,
|
||||
growth: "+15.2%",
|
||||
description: "智能群管理,提高群活跃度,增强获客效果",
|
||||
path: "/scenarios/ai-group",
|
||||
},
|
||||
{
|
||||
id: "ai-conversion",
|
||||
name: "AI场景转化",
|
||||
icon: "🤖",
|
||||
count: 134,
|
||||
growth: "+12.8%",
|
||||
description: "多场景智能营销,提升获客转化率",
|
||||
path: "/scenarios/ai-conversion",
|
||||
},
|
||||
];
|
||||
// AI智能获客用本地 mock 数据(暂时注释掉,可以后续启用)
|
||||
// const aiScenarios = [
|
||||
// {
|
||||
// id: "ai-friend",
|
||||
// name: "AI智能加友",
|
||||
// icon: "🤖",
|
||||
// count: 245,
|
||||
// growth: "+18.5%",
|
||||
// description: "智能分析目标用户画像,自动筛选优质客户",
|
||||
// path: "/scenarios/ai-friend",
|
||||
// },
|
||||
// {
|
||||
// id: "ai-group",
|
||||
// name: "AI群引流",
|
||||
// icon: "🤖",
|
||||
// count: 178,
|
||||
// growth: "+15.2%",
|
||||
// description: "智能群管理,提高群活跃度,增强获客效果",
|
||||
// path: "/scenarios/ai-group",
|
||||
// },
|
||||
// {
|
||||
// id: "ai-conversion",
|
||||
// name: "AI场景转化",
|
||||
// icon: "🤖",
|
||||
// count: 134,
|
||||
// growth: "+12.8%",
|
||||
// description: "多场景智能营销,提升获客转化率",
|
||||
// path: "/scenarios/ai-conversion",
|
||||
// },
|
||||
// ];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchScenarios = async () => {
|
||||
|
||||
Reference in New Issue
Block a user