diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index a9fafab2..6590d77b 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { AuthProvider } from './contexts/AuthContext'; +import { ToastProvider } from './components/ui/toast'; +import ProtectedRoute from './components/ProtectedRoute'; import LayoutWrapper from './components/LayoutWrapper'; +import { initInterceptors } from './api'; import Home from './pages/Home'; import Login from './pages/login/Login'; import Devices from './pages/devices/Devices'; @@ -25,35 +29,47 @@ import ContactImport from './pages/contact-import/ContactImport'; import Content from './pages/content/Content'; function App() { + // 初始化HTTP拦截器 + useEffect(() => { + const cleanup = initInterceptors(); + return cleanup; + }, []); + return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 你可以继续添加更多路由 */} - - + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* 你可以继续添加更多路由 */} + + + + + ); } diff --git a/nkebao/src/api/auth.ts b/nkebao/src/api/auth.ts new file mode 100644 index 00000000..848730e9 --- /dev/null +++ b/nkebao/src/api/auth.ts @@ -0,0 +1,82 @@ +import { request } from './request'; +import type { ApiResponse } from '@/types/common'; + +// 登录响应数据类型 +export interface LoginResponse { + token: string; + token_expired: string; + member: { + id: number; + username: string; + account: string; + avatar?: string; + s2_accountId: string; + }; +} + +// 验证码响应类型 +export interface VerificationCodeResponse { + code: string; + expire_time: string; +} + +// 认证相关API +export const authApi = { + // 账号密码登录 + login: async (account: string, password: string) => { + const response = await request.post>('/v1/auth/login', { + account, + password, + typeId: 1 // 默认使用用户类型1 + }); + return response as unknown as ApiResponse; + }, + + // 验证码登录 + loginWithCode: async (account: string, code: string) => { + const response = await request.post>('/v1/auth/login/code', { + account, + code, + typeId: 1 + }); + return response as unknown as ApiResponse; + }, + + // 发送验证码 + sendVerificationCode: async (account: string) => { + const response = await request.post>('/v1/auth/send-code', { + account, + type: 'login' // 登录验证码 + }); + return response as unknown as ApiResponse; + }, + + // 获取用户信息 + getUserInfo: async () => { + const response = await request.get>('/v1/auth/info'); + return response as unknown as ApiResponse; + }, + + // 刷新Token + refreshToken: async () => { + const response = await request.post>('/v1/auth/refresh', {}); + return response as unknown as ApiResponse<{ token: string; token_expired: string }>; + }, + + // 微信登录 + wechatLogin: async (code: string) => { + const response = await request.post>('/v1/auth/wechat', { + code + }); + return response as unknown as ApiResponse; + }, + + // Apple登录 + appleLogin: async (identityToken: string, authorizationCode: string) => { + const response = await request.post>('/v1/auth/apple', { + identity_token: identityToken, + authorization_code: authorizationCode + }); + return response as unknown as ApiResponse; + }, +}; \ No newline at end of file diff --git a/nkebao/src/api/devices.ts b/nkebao/src/api/devices.ts index 17604187..ed7e8458 100644 --- a/nkebao/src/api/devices.ts +++ b/nkebao/src/api/devices.ts @@ -8,7 +8,6 @@ import type { CreateDeviceParams, UpdateDeviceParams, DeviceStatus, - ServerDevice, ServerDevicesResponse } from '@/types/device'; diff --git a/nkebao/src/api/index.ts b/nkebao/src/api/index.ts new file mode 100644 index 00000000..9bfffc5a --- /dev/null +++ b/nkebao/src/api/index.ts @@ -0,0 +1,13 @@ +// 导出所有API相关的内容 +export * from './auth'; +export * from './utils'; +export * from './interceptors'; +export * from './request'; + +// 导出现有的API模块 +export * from './devices'; +export * from './scenarios'; +export * from './wechat-accounts'; + +// 默认导出request实例 +export { default as request } from './request'; \ No newline at end of file diff --git a/nkebao/src/api/interceptors.ts b/nkebao/src/api/interceptors.ts new file mode 100644 index 00000000..c07c4570 --- /dev/null +++ b/nkebao/src/api/interceptors.ts @@ -0,0 +1,152 @@ +import { refreshAuthToken, isTokenExpiringSoon, clearToken } from './utils'; + +// Token过期处理 +export const handleTokenExpired = () => { + if (typeof window !== 'undefined') { + // 清除本地存储 + clearToken(); + + // 跳转到登录页面 + setTimeout(() => { + window.location.href = '/login'; + }, 0); + } +}; + +// 显示API错误,但不会重定向 +export const showApiError = (error: any, defaultMessage: string = '请求失败') => { + if (typeof window === 'undefined') return; // 服务端不处理 + + let errorMessage = defaultMessage; + + // 尝试从各种可能的错误格式中获取消息 + if (error) { + if (typeof error === 'string') { + errorMessage = error; + } else if (error instanceof Error) { + errorMessage = error.message || defaultMessage; + } else if (typeof error === 'object') { + // 尝试从API响应中获取错误消息 + errorMessage = error.msg || error.message || error.error || defaultMessage; + } + } + + // 显示错误消息 + console.error('API错误:', errorMessage); + + // 这里可以集成toast系统 + // 由于toast context在组件层级,这里暂时用console + // 实际项目中可以通过事件系统或其他方式集成 +}; + +// 请求拦截器 - 检查token是否需要刷新 +export const requestInterceptor = async (): Promise => { + if (typeof window === 'undefined') { + return true; + } + + // 检查token是否即将过期 + if (isTokenExpiringSoon()) { + try { + console.log('Token即将过期,尝试刷新...'); + const success = await refreshAuthToken(); + if (!success) { + console.log('Token刷新失败,需要重新登录'); + handleTokenExpired(); + return false; + } + console.log('Token刷新成功'); + } catch (error) { + console.error('Token刷新过程中出错:', error); + handleTokenExpired(); + return false; + } + } + + return true; +}; + +// 响应拦截器 - 处理常见错误 +export const responseInterceptor = (response: any, result: any) => { + // 处理401未授权 + if (response?.status === 401 || (result && result.code === 401)) { + handleTokenExpired(); + throw new Error('登录已过期,请重新登录'); + } + + // 处理403禁止访问 + if (response?.status === 403 || (result && result.code === 403)) { + throw new Error('没有权限访问此资源'); + } + + // 处理404未找到 + if (response?.status === 404 || (result && result.code === 404)) { + throw new Error('请求的资源不存在'); + } + + // 处理500服务器错误 + if (response?.status >= 500 || (result && result.code >= 500)) { + throw new Error('服务器内部错误,请稍后重试'); + } + + return result; +}; + +// 错误拦截器 - 统一错误处理 +export const errorInterceptor = (error: any) => { + console.error('API请求错误:', error); + + let errorMessage = '网络请求失败,请稍后重试'; + + if (error) { + if (typeof error === 'string') { + errorMessage = error; + } else if (error instanceof Error) { + errorMessage = error.message; + } else if (error.name === 'TypeError' && error.message.includes('fetch')) { + errorMessage = '网络连接失败,请检查网络设置'; + } else if (error.name === 'AbortError') { + errorMessage = '请求已取消'; + } + } + + showApiError(error, errorMessage); + throw new Error(errorMessage); +}; + +// 网络状态监听 +export const setupNetworkListener = () => { + if (typeof window === 'undefined') return; + + const handleOnline = () => { + console.log('网络已连接'); + // 可以在这里添加网络恢复后的处理逻辑 + }; + + const handleOffline = () => { + console.log('网络已断开'); + showApiError(null, '网络连接已断开,请检查网络设置'); + }; + + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + + // 返回清理函数 + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; +}; + +// 初始化拦截器 +export const initInterceptors = () => { + // 设置网络监听 + const cleanupNetwork = setupNetworkListener(); + + // 返回清理函数 + return () => { + if (cleanupNetwork) { + cleanupNetwork(); + } + }; +}; \ No newline at end of file diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index e0d3d9f8..f152a431 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,8 +1,9 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors'; // 创建axios实例 const request: AxiosInstance = axios.create({ - baseURL: process.env.REACT_APP_API_BASE || 'http://localhost:3000/api', + baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com', timeout: 10000, headers: { 'Content-Type': 'application/json', @@ -11,12 +12,21 @@ const request: AxiosInstance = axios.create({ // 请求拦截器 request.interceptors.request.use( - (config) => { - // 可以在这里添加token等认证信息 + async (config) => { + // 检查token是否需要刷新 + if (config.headers.Authorization) { + const shouldContinue = await requestInterceptor(); + if (!shouldContinue) { + throw new Error('请求被拦截,需要重新登录'); + } + } + + // 添加token到请求头 const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } + return config; }, (error) => { @@ -27,12 +37,14 @@ request.interceptors.request.use( // 响应拦截器 request.interceptors.response.use( (response: AxiosResponse) => { - return response.data; + // 处理响应数据 + const result = response.data; + const processedResult = responseInterceptor(response, result); + return processedResult; }, (error) => { // 统一错误处理 - console.error('API请求错误:', error); - return Promise.reject(error); + return errorInterceptor(error); } ); diff --git a/nkebao/src/api/scenarios.ts b/nkebao/src/api/scenarios.ts index 5a425345..c6a6cc9a 100644 --- a/nkebao/src/api/scenarios.ts +++ b/nkebao/src/api/scenarios.ts @@ -12,13 +12,6 @@ export interface SceneItem { deleteTime: number | null; } -// 服务器返回的场景列表响应类型 -export interface ScenesResponse { - code: number; - message: string; - data: SceneItem[]; -} - // 前端使用的场景数据类型 export interface Channel { id: string; @@ -51,7 +44,7 @@ export const fetchScenes = async (params: { page?: number; limit?: number; keyword?: string; -} = {}): Promise => { +} = {}): Promise> => { const { page = 1, limit = 10, keyword = "" } = params; const queryParams = new URLSearchParams(); @@ -63,7 +56,7 @@ export const fetchScenes = async (params: { } try { - return await get(`/v1/plan/scenes?${queryParams.toString()}`); + return await get>(`/v1/plan/scenes?${queryParams.toString()}`); } catch (error) { console.error("Error fetching scenes:", error); // 返回一个错误响应 diff --git a/nkebao/src/api/utils.ts b/nkebao/src/api/utils.ts new file mode 100644 index 00000000..f53ddfd9 --- /dev/null +++ b/nkebao/src/api/utils.ts @@ -0,0 +1,100 @@ +import { authApi } from './auth'; + +// 设置token到localStorage +export const setToken = (token: string) => { + if (typeof window !== 'undefined') { + localStorage.setItem('token', token); + } +}; + +// 获取token +export const getToken = (): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; +}; + +// 清除token +export const clearToken = () => { + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + localStorage.removeItem('userInfo'); + localStorage.removeItem('token_expired'); + localStorage.removeItem('s2_accountId'); + } +}; + +// 验证token是否有效 +export const validateToken = async (): Promise => { + try { + const response = await authApi.getUserInfo(); + return response.code === 200; + } catch (error) { + console.error('Token验证失败:', error); + return false; + } +}; + +// 刷新令牌 +export const refreshAuthToken = async (): Promise => { + if (typeof window === 'undefined') { + return false; + } + + try { + const response = await authApi.refreshToken(); + if (response.code === 200 && response.data?.token) { + setToken(response.data.token); + // 更新过期时间 + if (response.data.token_expired) { + localStorage.setItem('token_expired', response.data.token_expired); + } + return true; + } + return false; + } catch (error) { + console.error('刷新Token失败:', error); + return false; + } +}; + +// 检查token是否即将过期 +export const isTokenExpiringSoon = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + + const tokenExpired = localStorage.getItem('token_expired'); + if (!tokenExpired) return true; + + try { + const expiredTime = new Date(tokenExpired).getTime(); + const currentTime = new Date().getTime(); + // 提前10分钟认为即将过期 + return currentTime >= (expiredTime - 10 * 60 * 1000); + } catch (error) { + console.error('解析token过期时间失败:', error); + return true; + } +}; + +// 检查token是否已过期 +export const isTokenExpired = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + + const tokenExpired = localStorage.getItem('token_expired'); + if (!tokenExpired) return true; + + try { + const expiredTime = new Date(tokenExpired).getTime(); + const currentTime = new Date().getTime(); + // 提前5分钟认为过期,给刷新留出时间 + return currentTime >= (expiredTime - 5 * 60 * 1000); + } catch (error) { + console.error('解析token过期时间失败:', error); + return true; + } +}; \ No newline at end of file diff --git a/nkebao/src/api/wechat-accounts.ts b/nkebao/src/api/wechat-accounts.ts index ae5da2cc..c5e62463 100644 --- a/nkebao/src/api/wechat-accounts.ts +++ b/nkebao/src/api/wechat-accounts.ts @@ -27,23 +27,6 @@ interface WechatAccountSummary { }[]; } -interface WechatAccountSummaryResponse { - code: number; - message: string; - data: WechatAccountSummary; -} - -interface ServerWechatAccountsResponse { - code: number; - message: string; - data: { - list: any[]; - total: number; - page: number; - limit: number; - }; -} - interface QueryWechatAccountParams { page?: number; limit?: number; @@ -57,7 +40,12 @@ interface QueryWechatAccountParams { * @param params 查询参数 * @returns 微信账号列表响应 */ -export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise => { +export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise> => { const queryParams = new URLSearchParams(); // 添加查询参数 @@ -68,15 +56,20 @@ export const fetchWechatAccountList = async (params: QueryWechatAccountParams = if (params.order) queryParams.append('order', params.order); // 发起API请求 - return get(`/v1/wechats?${queryParams.toString()}`); + return get>(`/v1/wechats?${queryParams.toString()}`); }; /** * 刷新微信账号状态 * @returns 刷新结果 */ -export const refreshWechatAccounts = async (): Promise<{ code: number; message: string; data: any }> => { - return put<{ code: number; message: string; data: any }>('/v1/wechats/refresh', {}); +export const refreshWechatAccounts = async (): Promise> => { + return put>('/v1/wechats/refresh', {}); }; /** @@ -85,8 +78,8 @@ export const refreshWechatAccounts = async (): Promise<{ code: number; message: * @param targetId 目标微信账号ID * @returns 转移结果 */ -export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<{ code: number; message: string; data: any }> => { - return post<{ code: number; message: string; data: any }>('/v1/wechats/transfer-friends', { +export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise> => { + return post>('/v1/wechats/transfer-friends', { source_id: sourceId, target_id: targetId }); @@ -175,9 +168,9 @@ export const fetchWechatFriends = async (wechatId: string, page: number = 1, pag * @param id 微信账号ID * @returns 微信账号概览信息 */ -export const fetchWechatAccountSummary = async (wechatIdid: string): Promise => { +export const fetchWechatAccountSummary = async (wechatId: string): Promise> => { try { - return get(`/v1/wechats/${wechatIdid}/summary`); + return get>(`/v1/wechats/${wechatId}/summary`); } catch (error) { console.error("获取账号概览失败:", error); throw error; @@ -198,20 +191,13 @@ export interface WechatFriendDetail { wechatId: string; addDate: string; tags: string[]; - playDate: string; memo: string; source: string; } -interface WechatFriendDetailResponse { - code: number; - message: string; - data: WechatFriendDetail; -} - -export const fetchWechatFriendDetail = async (wechatId: string): Promise => { +export const fetchWechatFriendDetail = async (wechatId: string): Promise> => { try { - return get(`/v1/wechats/${wechatId}`); + return get>(`/v1/wechats/${wechatId}/friend-detail`); } catch (error) { console.error("获取好友详情失败:", error); throw error; diff --git a/nkebao/src/components/LayoutWrapper.tsx b/nkebao/src/components/LayoutWrapper.tsx index c17462eb..7ca75d61 100644 --- a/nkebao/src/components/LayoutWrapper.tsx +++ b/nkebao/src/components/LayoutWrapper.tsx @@ -2,6 +2,26 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import BottomNav from './BottomNav'; +// 不需要底部导航的页面路径 +const NO_BOTTOM_NAV_PATHS = [ + '/login', + '/register', + '/forgot-password', + '/reset-password', + '/devices', + '/devices/', + '/wechat-accounts', + '/wechat-accounts/', + '/scenarios/new', + '/scenarios/', + '/plans/', + '/workspace/auto-group/', + '/workspace/moments-sync/', + '/workspace/traffic-distribution/', + '/404', + '/500' +]; + interface LayoutWrapperProps { children: React.ReactNode; } @@ -9,16 +29,23 @@ interface LayoutWrapperProps { export default function LayoutWrapper({ children }: LayoutWrapperProps) { const location = useLocation(); - // 只在四个主页显示底部导航栏:首页、场景获客、工作台和我的 - const mainPages = ["/", "/scenarios", "/workspace", "/profile"]; - const showBottomNav = mainPages.includes(location.pathname); + // 检查当前路径是否需要底部导航 + const shouldShowBottomNav = !NO_BOTTOM_NAV_PATHS.some(path => + location.pathname.startsWith(path) + ); + // 如果是登录页面,直接渲染内容(不显示底部导航) + if (location.pathname === '/login') { + return <>{children}; + } + + // 其他页面显示底部导航 return ( -
-
+
+
{children} - {showBottomNav && } -
+
+ {shouldShowBottomNav && } ); } \ No newline at end of file diff --git a/nkebao/src/components/ProtectedRoute.tsx b/nkebao/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..569834b9 --- /dev/null +++ b/nkebao/src/components/ProtectedRoute.tsx @@ -0,0 +1,71 @@ +import React, { useEffect } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +// 不需要登录的公共页面路径 +const PUBLIC_PATHS = [ + '/login', + '/register', + '/forgot-password', + '/reset-password', + '/404', + '/500' +]; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isLoading } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + // 检查当前路径是否是公共页面 + const isPublicPath = PUBLIC_PATHS.some(path => + location.pathname.startsWith(path) + ); + + useEffect(() => { + // 如果正在加载,不进行任何跳转 + if (isLoading) { + return; + } + + // 如果未登录且不是公共页面,重定向到登录页面 + if (!isAuthenticated && !isPublicPath) { + // 保存当前URL,登录后可以重定向回来 + const returnUrl = encodeURIComponent(window.location.href); + navigate(`/login?returnUrl=${returnUrl}`, { replace: true }); + return; + } + + // 如果已登录且在登录页面,重定向到首页 + if (isAuthenticated && location.pathname === '/login') { + navigate('/', { replace: true }); + return; + } + }, [isAuthenticated, isLoading, location.pathname, navigate, isPublicPath]); + + // 如果正在加载,显示加载状态 + if (isLoading) { + return ( +
+
加载中...
+
+ ); + } + + // 如果未登录且不是公共页面,不渲染内容(等待重定向) + if (!isAuthenticated && !isPublicPath) { + return null; + } + + // 如果已登录且在登录页面,不渲染内容(等待重定向) + if (isAuthenticated && location.pathname === '/login') { + return null; + } + + // 其他情况正常渲染 + return <>{children}; +} \ No newline at end of file diff --git a/nkebao/src/components/icons/AppleIcon.tsx b/nkebao/src/components/icons/AppleIcon.tsx new file mode 100644 index 00000000..67e3fa19 --- /dev/null +++ b/nkebao/src/components/icons/AppleIcon.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export function AppleIcon(props: React.SVGProps) { + return ( + + + + ); +} + +export default AppleIcon; \ No newline at end of file diff --git a/nkebao/src/components/icons/WeChatIcon.tsx b/nkebao/src/components/icons/WeChatIcon.tsx new file mode 100644 index 00000000..0b50a5ab --- /dev/null +++ b/nkebao/src/components/icons/WeChatIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export function WeChatIcon(props: React.SVGProps) { + return ( + + + + + ); +} + +export default WeChatIcon; \ No newline at end of file diff --git a/nkebao/src/components/ui/toast.tsx b/nkebao/src/components/ui/toast.tsx new file mode 100644 index 00000000..36ab7f32 --- /dev/null +++ b/nkebao/src/components/ui/toast.tsx @@ -0,0 +1,88 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; +import { X } from 'lucide-react'; + +interface Toast { + id: string; + title: string; + description?: string; + variant?: 'default' | 'destructive'; +} + +interface ToastContextType { + toast: (toast: Omit) => void; +} + +const ToastContext = createContext(undefined); + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; + +interface ToastProviderProps { + children: ReactNode; +} + +export function ToastProvider({ children }: ToastProviderProps) { + const [toasts, setToasts] = useState([]); + + const toast = (newToast: Omit) => { + const id = Math.random().toString(36).substr(2, 9); + const toastWithId = { ...newToast, id }; + + setToasts(prev => [...prev, toastWithId]); + + // 自动移除toast + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, 5000); + }; + + const removeToast = (id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }; + + return ( + + {children} +
+ {toasts.map((toast) => ( +
+
+
+

+ {toast.title} +

+ {toast.description && ( +

+ {toast.description} +

+ )} +
+ +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/contexts/AuthContext.tsx b/nkebao/src/contexts/AuthContext.tsx new file mode 100644 index 00000000..bad49d5b --- /dev/null +++ b/nkebao/src/contexts/AuthContext.tsx @@ -0,0 +1,244 @@ +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { validateToken, refreshAuthToken } from '../api'; + +// 安全的localStorage访问方法 +const safeLocalStorage = { + getItem: (key: string): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem(key); + } + return null; + }, + setItem: (key: string, value: string): void => { + if (typeof window !== 'undefined') { + localStorage.setItem(key, value); + } + }, + removeItem: (key: string): void => { + if (typeof window !== 'undefined') { + localStorage.removeItem(key); + } + } +}; + +interface User { + id: number; + username: string; + account?: string; + avatar?: string; + s2_accountId?: string; +} + +interface AuthContextType { + isAuthenticated: boolean; + token: string | null; + user: User | null; + login: (token: string, userData: User) => void; + logout: () => void; + updateToken: (newToken: string) => void; + isLoading: boolean; +} + +// 创建默认上下文 +const AuthContext = createContext({ + isAuthenticated: false, + token: null, + user: null, + login: () => {}, + logout: () => {}, + updateToken: () => {}, + isLoading: true +}); + +export const useAuth = () => useContext(AuthContext); + +interface AuthProviderProps { + children: ReactNode; +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [token, setToken] = useState(null); + const [user, setUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isInitialized, setIsInitialized] = useState(false); + const navigate = useNavigate(); + + // 检查token是否过期 + const isTokenExpired = (): boolean => { + const tokenExpired = safeLocalStorage.getItem("token_expired"); + if (!tokenExpired) return true; + + try { + const expiredTime = new Date(tokenExpired).getTime(); + const currentTime = new Date().getTime(); + // 提前5分钟认为过期,给刷新留出时间 + return currentTime >= (expiredTime - 5 * 60 * 1000); + } catch (error) { + console.error('解析token过期时间失败:', error); + return true; + } + }; + + // 验证token有效性 + const verifyToken = async (): Promise => { + try { + const isValid = await validateToken(); + return isValid; + } catch (error) { + console.error('Token验证失败:', error); + return false; + } + }; + + // 尝试刷新token + const tryRefreshToken = async (): Promise => { + try { + const success = await refreshAuthToken(); + if (success) { + const newToken = safeLocalStorage.getItem("token"); + if (newToken) { + setToken(newToken); + return true; + } + } + return false; + } catch (error) { + console.error('刷新token失败:', error); + return false; + } + }; + + // 初始化认证状态 + useEffect(() => { + setIsLoading(true); + + const initAuth = async () => { + try { + const storedToken = safeLocalStorage.getItem("token"); + + if (storedToken) { + // 检查token是否过期 + if (isTokenExpired()) { + console.log('Token已过期,尝试刷新...'); + const refreshSuccess = await tryRefreshToken(); + if (!refreshSuccess) { + console.log('Token刷新失败,需要重新登录'); + handleLogout(); + return; + } + } + + // 验证token有效性 + const isValid = await verifyToken(); + if (!isValid) { + console.log('Token无效,需要重新登录'); + handleLogout(); + return; + } + + // 获取用户信息 + const userDataStr = safeLocalStorage.getItem("userInfo"); + if (userDataStr) { + try { + const userData = JSON.parse(userDataStr) as User; + setToken(storedToken); + setUser(userData); + setIsAuthenticated(true); + } catch (parseError) { + console.error('解析用户数据失败:', parseError); + handleLogout(); + } + } else { + console.warn('找到token但没有用户信息,尝试保持登录状态'); + setToken(storedToken); + setIsAuthenticated(true); + } + } + } catch (error) { + console.error("初始化认证状态时出错:", error); + handleLogout(); + } finally { + setIsLoading(false); + setIsInitialized(true); + } + }; + + initAuth(); + }, []); + + // 定期检查token状态 + useEffect(() => { + if (!isAuthenticated) return; + + const checkTokenInterval = setInterval(async () => { + if (isTokenExpired()) { + console.log('检测到token即将过期,尝试刷新...'); + const refreshSuccess = await tryRefreshToken(); + if (!refreshSuccess) { + console.log('Token刷新失败,登出用户'); + handleLogout(); + } + } + }, 60000); // 每分钟检查一次 + + return () => clearInterval(checkTokenInterval); + }, [isAuthenticated]); + + const handleLogout = () => { + // 先清除所有认证相关的状态 + safeLocalStorage.removeItem("token"); + safeLocalStorage.removeItem("token_expired"); + safeLocalStorage.removeItem("s2_accountId"); + safeLocalStorage.removeItem("userInfo"); + safeLocalStorage.removeItem("user"); + setToken(null); + setUser(null); + setIsAuthenticated(false); + + // 跳转到登录页面 + navigate('/login'); + }; + + const login = (newToken: string, userData: User) => { + safeLocalStorage.setItem("token", newToken); + safeLocalStorage.setItem("userInfo", JSON.stringify(userData)); + if (userData.s2_accountId) { + safeLocalStorage.setItem("s2_accountId", userData.s2_accountId); + } + setToken(newToken); + setUser(userData); + setIsAuthenticated(true); + }; + + const logout = () => { + handleLogout(); + }; + + // 用于刷新 token 的方法 + const updateToken = (newToken: string) => { + safeLocalStorage.setItem("token", newToken); + setToken(newToken); + }; + + return ( + + {isLoading && isInitialized ? ( +
+
加载中...
+
+ ) : ( + children + )} +
+ ); +} \ No newline at end of file diff --git a/nkebao/src/hooks/useAuthGuard.ts b/nkebao/src/hooks/useAuthGuard.ts new file mode 100644 index 00000000..881e5190 --- /dev/null +++ b/nkebao/src/hooks/useAuthGuard.ts @@ -0,0 +1,80 @@ +import { useEffect } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +// 不需要登录的公共页面路径 +const PUBLIC_PATHS = [ + '/login', + '/register', + '/forgot-password', + '/reset-password', + '/404', + '/500' +]; + +/** + * 认证守卫Hook + * 用于在组件中检查用户是否已登录 + * @param requireAuth 是否需要认证,默认为true + * @param redirectTo 未认证时重定向的路径,默认为'/login' + */ +export function useAuthGuard(requireAuth: boolean = true, redirectTo: string = '/login') { + const { isAuthenticated, isLoading } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + // 检查当前路径是否是公共页面 + const isPublicPath = PUBLIC_PATHS.some(path => + location.pathname.startsWith(path) + ); + + useEffect(() => { + // 如果正在加载,不进行任何跳转 + if (isLoading) { + return; + } + + // 如果需要认证但未登录且不是公共页面 + if (requireAuth && !isAuthenticated && !isPublicPath) { + // 保存当前URL,登录后可以重定向回来 + const returnUrl = encodeURIComponent(window.location.href); + navigate(`${redirectTo}?returnUrl=${returnUrl}`, { replace: true }); + return; + } + + // 如果已登录但在登录页面,重定向到首页 + if (isAuthenticated && location.pathname === '/login') { + navigate('/', { replace: true }); + return; + } + }, [isAuthenticated, isLoading, location.pathname, navigate, requireAuth, redirectTo, isPublicPath]); + + return { + isAuthenticated, + isLoading, + isPublicPath, + // 是否应该显示内容 + shouldRender: !isLoading && (isAuthenticated || isPublicPath || !requireAuth) + }; +} + +/** + * 简单的认证检查Hook + * 只返回认证状态,不进行自动重定向 + */ +export function useAuthCheck() { + const { isAuthenticated, isLoading } = useAuth(); + const location = useLocation(); + + const isPublicPath = PUBLIC_PATHS.some(path => + location.pathname.startsWith(path) + ); + + return { + isAuthenticated, + isLoading, + isPublicPath, + // 是否需要认证 + requiresAuth: !isPublicPath + }; +} \ No newline at end of file diff --git a/nkebao/src/pages/Home.tsx b/nkebao/src/pages/Home.tsx index f29946de..e18860be 100644 --- a/nkebao/src/pages/Home.tsx +++ b/nkebao/src/pages/Home.tsx @@ -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(null); - const chartInstance = useRef(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(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 (
diff --git a/nkebao/src/pages/login/Login.tsx b/nkebao/src/pages/login/Login.tsx index c8f30ad7..ff405497 100644 --- a/nkebao/src/pages/login/Login.tsx +++ b/nkebao/src/pages/login/Login.tsx @@ -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
登录页
; + 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({ + 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) => { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleCheckboxChange = (e: React.ChangeEvent) => { + 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 ( +
+
+ {/* 标题 */} +
+

欢迎登录

+

你所在地区仅支持 手机号 / 微信 / Apple 登录

+
+ + {/* 标签页切换 */} +
+ + +
+ +
+ {/* 手机号输入 */} +
+ +
+ + + + +86 + +
+
+ + {/* 密码输入 */} + {activeTab === 'password' && ( +
+ +
+ + +
+
+ )} + + {/* 验证码输入 */} + {activeTab === 'verification' && ( +
+ +
+ + +
+
+ )} + + {/* 用户协议 */} +
+ + +
+ + {/* 登录按钮 */} + + + {/* 分割线 */} +
+
+
+
+
+ 其他登录方式 +
+
+ + {/* 第三方登录 */} +
+ + +
+
+
+
+ ); } \ No newline at end of file diff --git a/nkebao/src/pages/plans/Plans.tsx b/nkebao/src/pages/plans/Plans.tsx index 525d85ec..1b8a7d34 100644 --- a/nkebao/src/pages/plans/Plans.tsx +++ b/nkebao/src/pages/plans/Plans.tsx @@ -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; diff --git a/nkebao/src/pages/profile/Profile.tsx b/nkebao/src/pages/profile/Profile.tsx index 3ca72a49..46540578 100644 --- a/nkebao/src/pages/profile/Profile.tsx +++ b/nkebao/src/pages/profile/Profile.tsx @@ -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
个人中心页
; + const { user, logout, isAuthenticated } = useAuth(); + const { toast } = useToast(); + + const handleLogout = () => { + logout(); + toast({ + title: '已退出登录', + description: '感谢使用存客宝', + }); + }; + + const menuItems = [ + { + icon: , + title: '个人信息', + description: '查看和编辑个人资料', + onClick: () => { + toast({ + title: '功能开发中', + description: '个人信息编辑功能正在开发中', + }); + } + }, + { + icon: , + title: '账户设置', + description: '密码、安全设置等', + onClick: () => { + toast({ + title: '功能开发中', + description: '账户设置功能正在开发中', + }); + } + }, + { + icon: , + title: '隐私安全', + description: '隐私设置和安全选项', + onClick: () => { + toast({ + title: '功能开发中', + description: '隐私安全功能正在开发中', + }); + } + }, + { + icon: , + title: '消息通知', + description: '通知设置和消息管理', + onClick: () => { + toast({ + title: '功能开发中', + description: '消息通知功能正在开发中', + }); + } + } + ]; + + if (!isAuthenticated) { + return ( +
+
请先登录
+
+ ); + } + + return ( +
+
+ {/* 用户信息卡片 */} +
+
+
+ {user?.avatar ? ( + 头像 + ) : ( + + )} +
+
+

+ {user?.username || '用户'} +

+

+ {user?.account || '暂无手机号'} +

+ {user?.s2_accountId && ( +

+ ID: {user.s2_accountId} +

+ )} +
+
+
+ + {/* 菜单列表 */} +
+ {menuItems.map((item, index) => ( +
+ + {index < menuItems.length - 1 && ( +
+ )} +
+ ))} +
+ + {/* 退出登录按钮 */} +
+ +
+ + {/* 版本信息 */} +
+

存客宝 v1.0.0

+

© 2024 存客宝. All rights reserved.

+
+
+
+ ); } \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/ScenarioDetail.tsx b/nkebao/src/pages/scenarios/ScenarioDetail.tsx index ea6ec520..653570ac 100644 --- a/nkebao/src/pages/scenarios/ScenarioDetail.tsx +++ b/nkebao/src/pages/scenarios/ScenarioDetail.tsx @@ -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(null); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); diff --git a/nkebao/src/pages/scenarios/Scenarios.tsx b/nkebao/src/pages/scenarios/Scenarios.tsx index 2fcf6733..528213b7 100644 --- a/nkebao/src/pages/scenarios/Scenarios.tsx +++ b/nkebao/src/pages/scenarios/Scenarios.tsx @@ -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 () => {