diff --git a/.Cursorignore b/.Cursorignore deleted file mode 100644 index a78c99e43..000000000 --- a/.Cursorignore +++ /dev/null @@ -1,9 +0,0 @@ -Server/runtime/ -*.png -*.jpg -*.jpeg -*.gif -*.bmp -*.webp -*.ico -*.svg \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d70491c31..000000000 --- a/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea -.next -.vscode -vendor -Backend/dist -Backend/node_modules -Store_vue/node_modules -Store_vue/unpackage -Server/.specstory/ -Store_vue/.specstory/ -*.zip -*.cursorindexingignore -*.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ca6fe0685 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/Cunkebao/.babelrc b/Cunkebao/.babelrc deleted file mode 100644 index beaa8513e..000000000 --- a/Cunkebao/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["next/babel"] -} - diff --git a/Cunkebao/.gitignore b/Cunkebao/.gitignore index e39c24cf6..4d29575de 100644 --- a/Cunkebao/.gitignore +++ b/Cunkebao/.gitignore @@ -2,28 +2,22 @@ # dependencies /node_modules +/.pnp +.pnp.js -# next.js -/.next/ -/out/ - -/.history/ +# testing +/coverage # production /build -# debug +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* - -# env files -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/Cunkebao/.htaccess b/Cunkebao/.htaccess deleted file mode 100644 index 0c54fb9f0..000000000 --- a/Cunkebao/.htaccess +++ /dev/null @@ -1 +0,0 @@ -# 请将伪静态规则或自定义Apache配置填写到此处 diff --git a/Cunkebao/.well-known/acme-challenge/nKNDFrnOs9-cf7FZzW7Cg4MpIxhtE8pZ1etiT_7NPWA b/Cunkebao/.well-known/acme-challenge/nKNDFrnOs9-cf7FZzW7Cg4MpIxhtE8pZ1etiT_7NPWA deleted file mode 100644 index 2f056a639..000000000 --- a/Cunkebao/.well-known/acme-challenge/nKNDFrnOs9-cf7FZzW7Cg4MpIxhtE8pZ1etiT_7NPWA +++ /dev/null @@ -1 +0,0 @@ -nKNDFrnOs9-cf7FZzW7Cg4MpIxhtE8pZ1etiT_7NPWA.2wKeISBpPoGcCt72TdgVrBMxtnnieXof2OZkRdHKCJI \ No newline at end of file diff --git a/Cunkebao/README.md b/Cunkebao/README.md new file mode 100644 index 000000000..be60268f4 --- /dev/null +++ b/Cunkebao/README.md @@ -0,0 +1,266 @@ +# 内客宝 - 智能获客管理平台 + +## 📋 项目简介 + +内客宝是一个专业的微信获客和流量管理平台,基于 React 技术栈构建。平台提供智能化的客户获取、管理和运营解决方案,集成了多种自动化工具,帮助企业高效管理微信营销活动。 + +## 🚀 技术栈详解 + +### 核心框架 +- **React 18.2.0** - 现代化的用户界面库 +- **TypeScript 4.9.5** - 类型安全的 JavaScript 超集 +- **Create React App (CRA) 5.0.1** - React 应用脚手架 +- **React Router DOM 6.20.0** - 客户端路由管理 + +### 构建工具 +- **CRACO 7.1.0** - Create React App Configuration Override + - 支持自定义 webpack 配置 + - 路径别名配置 + - 构建优化 + +### UI 组件库 +- **Radix UI** - 无样式的可访问组件库 + - 完整的组件生态系统(30+ 组件) + - 优秀的无障碍访问支持 + - 高度可定制 +- **Tailwind CSS 3.4.17** - 实用优先的 CSS 框架 + - 响应式设计支持 + - 自定义主题配置 + - 原子化 CSS 类 + +### 图标和样式 +- **Lucide React 0.454.0** - 精美的图标库 +- **Tailwind CSS Animate** - CSS 动画库 +- **Class Variance Authority** - 组件变体管理 +- **Tailwind Merge** - Tailwind 类名合并工具 + +### 状态管理和表单 +- **React Hook Form 7.54.1** - 高性能表单库 +- **Zod 3.24.1** - TypeScript 优先的模式验证 +- **@hookform/resolvers 3.9.1** - 表单验证解析器 + +### 数据可视化 +- **Recharts** - 基于 React 的图表库 +- **Chart.js 4.5.0** - 灵活的图表库 +- **@ant-design/plots** - Ant Design 图表组件 + +### HTTP 请求和数据处理 +- **Axios 1.6.0** - HTTP 客户端 +- **Crypto-js 4.2.0** - 加密库 +- **Date-fns** - 日期处理库 +- **XLSX 0.18.5** - Excel 文件处理 + +### 通知和反馈 +- **React Hot Toast 2.5.2** - 轻量级通知库 +- **Sonner 1.7.4** - 现代化 Toast 组件 + +### 高级组件 +- **@tanstack/react-table** - 功能强大的表格组件 +- **Embla Carousel React 8.5.1** - 轮播组件 +- **React Resizable Panels 2.1.7** - 可调整大小的面板 +- **Vaul 0.9.6** - 抽屉组件 +- **Input OTP 1.4.1** - OTP 输入组件 +- **React Day Picker** - 日期选择器 + +### 开发工具 +- **PostCSS 8** - CSS 后处理器 +- **Autoprefixer 10.4.20** - CSS 前缀自动添加 +- **ESLint** - 代码质量检查 +- **Jest** - 单元测试框架 +- **Testing Library** - React 测试工具 + +## 📁 项目结构 + +``` +nkebao/ +├── public/ # 静态资源 +├── src/ # 源代码 +│ ├── api/ # API 接口封装 +│ ├── components/ # 全局组件 +│ │ ├── ui/ # UI 基础组件 +│ │ └── icons/ # 图标组件 +│ ├── config/ # 配置文件 +│ ├── contexts/ # React Context +│ ├── hooks/ # 自定义 Hooks +│ ├── pages/ # 页面组件 +│ │ ├── workspace/ # 工作台模块 +│ │ │ ├── auto-like/ # 自动点赞 +│ │ │ ├── auto-group/ # 自动建群 +│ │ │ ├── group-push/ # 群消息推送 +│ │ │ ├── moments-sync/ # 朋友圈同步 +│ │ │ ├── ai-assistant/ # AI 对话助手 +│ │ │ └── traffic-distribution/ # 流量分发 +│ │ ├── devices/ # 设备管理 +│ │ ├── scenarios/ # 场景管理 +│ │ ├── content/ # 内容管理 +│ │ └── ... +│ ├── types/ # TypeScript 类型定义 +│ ├── utils/ # 工具函数 +│ ├── App.tsx # 应用根组件 +│ └── index.tsx # 应用入口 +├── craco.config.js # CRACO 配置 +├── tailwind.config.js # Tailwind CSS 配置 +├── tsconfig.json # TypeScript 配置 +└── package.json # 项目依赖 +``` + +## 🎯 核心功能模块 + +### 工作台 (Workspace) +- **自动点赞** - 智能点赞管理和配置 +- **自动建群** - 群组自动化创建和管理 +- **群消息推送** - 群组消息批量发送 +- **朋友圈同步** - 内容同步和发布 +- **AI 对话助手** - 智能客服和对话管理 +- **流量分发** - 流量分配和策略管理 + +### 设备管理 (Devices) +- 设备状态监控和配置 +- 设备性能分析 +- 设备权限管理 + +### 场景管理 (Scenarios) +- 营销场景配置 +- 自动化流程设计 +- 场景效果分析 + +### 内容管理 (Content) +- 内容创建与编辑 +- 内容模板管理 +- 内容发布调度 + +### 其他模块 +- 用户管理 (Users) +- 订单管理 (Orders) +- 流量池管理 (Traffic Pool) +- 联系人导入 (Contact Import) + +## 🛠️ 开发指南 + +### 环境要求 +- **Node.js** 16+ +- **npm** 或 **yarn** + +### 安装依赖 +```bash +# 使用 npm +npm install + +# 使用 yarn +yarn install +``` + +### 开发环境启动 +```bash +# 使用 npm +npm start + +# 使用 yarn +yarn start +``` + +### 构建生产版本 +```bash +# 使用 npm +npm run build + +# 使用 yarn +yarn build +``` + +### 运行测试 +```bash +# 使用 npm +npm test + +# 使用 yarn +yarn test +``` + +## 🔧 配置说明 + +### 路径别名配置 +项目使用 CRACO 配置了路径别名: +```javascript +'@': path.resolve(__dirname, 'src'), +'@/components': path.resolve(__dirname, 'src/components'), +'@/api': path.resolve(__dirname, 'src/api'), +'@/types': path.resolve(__dirname, 'src/types'), +'@/hooks': path.resolve(__dirname, 'src/hooks'), +'@/utils': path.resolve(__dirname, 'src/utils'), +'@/styles': path.resolve(__dirname, 'src/styles'), +'@/pages': path.resolve(__dirname, 'src/pages'), +``` + +### Tailwind CSS 配置 +- 自定义字体大小和间距 +- 响应式断点配置 +- 主题颜色系统 + +### TypeScript 配置 +- 严格模式启用 +- 路径映射配置 +- JSX 支持 + +## 📱 响应式设计 + +项目采用移动优先的响应式设计: +- 支持桌面端、平板端、移动端 +- 自适应布局组件 +- 触摸友好的交互设计 + +## 🎨 UI 设计系统 + +### 设计原则 +- 简洁现代的设计风格 +- 一致的用户体验 +- 无障碍访问支持 + +### 组件库特点 +- 基于 Radix UI 的高质量组件 +- 完整的表单组件系统 +- 数据展示组件 +- 导航和布局组件 + +## 🔒 安全特性 + +- 身份验证和授权 +- API 请求拦截 +- 数据验证和清理 +- 加密功能支持 + +## 📊 性能优化 + +- 代码分割和懒加载 +- 组件优化 +- 缓存策略 +- 包大小优化 + +## 🧪 测试策略 + +- 单元测试 (Jest + Testing Library) +- 组件测试 +- 集成测试支持 + +## 🤝 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证。 + +## 📞 联系方式 + +如有问题或建议,请联系开发团队。 + +--- + +**项目名称**: 内客宝 (nkebao2) +**版本**: 0.1.0 +**技术栈**: React + TypeScript + CRA + Tailwind CSS +**最后更新**: 2024年12月 \ No newline at end of file diff --git a/Cunkebao/api/devices.ts b/Cunkebao/api/devices.ts deleted file mode 100644 index 64da66da3..000000000 --- a/Cunkebao/api/devices.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { api } from "@/lib/api"; -import type { - ApiResponse, - Device, - DeviceStats, - DeviceTaskRecord, - PaginatedResponse, - QueryDeviceParams, - CreateDeviceParams, - UpdateDeviceParams, - DeviceStatus, - ServerDevice, - ServerDevicesResponse -} from "@/types/device" - -const API_BASE = "/api/devices" - -// 获取设备列表 - 连接到服务器/v1/devices接口 -export const fetchDeviceList = async (page: number = 1, limit: number = 20, keyword?: string): Promise => { - const params = new URLSearchParams(); - params.append('page', page.toString()); - params.append('limit', limit.toString()); - - if (keyword) { - params.append('keyword', keyword); - } - - return api.get(`/v1/devices?${params.toString()}`); -}; - -// 获取设备详情 - 连接到服务器/v1/devices/:id接口 -export const fetchDeviceDetail = async (id: string | number): Promise> => { - return api.get>(`/v1/devices/${id}`); -}; - -// 获取设备关联的微信账号 -export const fetchDeviceRelatedAccounts = async (id: string | number): Promise> => { - return api.get>(`/v1/wechats/related-device/${id}`); -}; - -// 获取设备操作记录 -export const fetchDeviceHandleLogs = async (id: string | number, page: number = 1, limit: number = 10): Promise> => { - return api.get>(`/v1/devices/${id}/handle-logs?page=${page}&limit=${limit}`); -}; - -// 更新设备任务配置 -export const updateDeviceTaskConfig = async ( - config: { - deviceId: string | number; - autoAddFriend?: boolean; - autoReply?: boolean; - momentsSync?: boolean; - aiChat?: boolean; - } -): Promise> => { - return api.post>(`/v1/devices/task-config`, { - ...config - }); -}; - -// 删除设备 -export const deleteDevice = async (id: number): Promise> => { - return api.delete>(`/v1/devices/${id}`); -}; - -// 设备管理API -export const deviceApi = { - // 创建设备 - async create(params: CreateDeviceParams): Promise> { - const response = await fetch(`${API_BASE}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }) - return response.json() - }, - - // 更新设备 - async update(params: UpdateDeviceParams): Promise> { - const response = await fetch(`${API_BASE}/${params.id}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }) - return response.json() - }, - - // 获取设备详情 - async getById(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}`) - return response.json() - }, - - // 查询设备列表 - async query(params: QueryDeviceParams): Promise>> { - // 创建一个新对象,用于构建URLSearchParams - const queryParams: Record = {}; - - // 按需将params中的属性添加到queryParams - if (params.keyword) queryParams.keyword = params.keyword; - if (params.status) queryParams.status = params.status; - if (params.type) queryParams.type = params.type; - if (params.page) queryParams.page = params.page.toString(); - if (params.pageSize) queryParams.pageSize = params.pageSize.toString(); - - // 特殊处理需要JSON序列化的属性 - if (params.tags) queryParams.tags = JSON.stringify(params.tags); - if (params.dateRange) queryParams.dateRange = JSON.stringify(params.dateRange); - - // 构建查询字符串 - const queryString = new URLSearchParams(queryParams).toString(); - const response = await fetch(`${API_BASE}?${queryString}`) - return response.json() - }, - - // 删除设备 - async delete(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}`, { - method: "DELETE", - }) - return response.json() - }, - - // 重启设备 - async restart(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/restart`, { - method: "POST", - }) - return response.json() - }, - - // 解绑设备 - async unbind(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/unbind`, { - method: "POST", - }) - return response.json() - }, - - // 获取设备统计数据 - async getStats(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/stats`) - return response.json() - }, - - // 获取设备任务记录 - async getTaskRecords(id: string, page = 1, pageSize = 20): Promise>> { - const response = await fetch(`${API_BASE}/${id}/tasks?page=${page}&pageSize=${pageSize}`) - return response.json() - }, - - // 批量更新设备标签 - async updateTags(ids: string[], tags: string[]): Promise> { - const response = await fetch(`${API_BASE}/tags`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ deviceIds: ids, tags }), - }) - return response.json() - }, - - // 批量导出设备数据 - async exportDevices(ids: string[]): Promise { - const response = await fetch(`${API_BASE}/export`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ deviceIds: ids }), - }) - return response.blob() - }, - - // 检查设备在线状态 - async checkStatus(ids: string[]): Promise>> { - const response = await fetch(`${API_BASE}/status`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ deviceIds: ids }), - }) - return response.json() - }, -} - diff --git a/Cunkebao/api/route.ts b/Cunkebao/api/route.ts deleted file mode 100644 index f82d21d8b..000000000 --- a/Cunkebao/api/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from "next/server" -import type { CreateScenarioParams, QueryScenarioParams, ScenarioBase, ApiResponse } from "@/types/scenario" - -// 获客场景路由处理 -export async function POST(request: Request) { - try { - const body: CreateScenarioParams = await request.json() - - // TODO: 实现创建场景的具体逻辑 - const scenario: ScenarioBase = { - id: "generated-id", - ...body, - status: "draft", - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - creator: "current-user-id", - } - - const response: ApiResponse = { - code: 0, - message: "创建成功", - data: scenario, - } - - return NextResponse.json(response) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "创建失败", - data: null, - }, - { status: 500 }, - ) - } -} - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url) - const params: QueryScenarioParams = { - type: searchParams.get("type") as any, - status: searchParams.get("status") as any, - keyword: searchParams.get("keyword") || undefined, - dateRange: searchParams.get("dateRange") ? JSON.parse(searchParams.get("dateRange")!) : undefined, - page: Number(searchParams.get("page")) || 1, - pageSize: Number(searchParams.get("pageSize")) || 20, - } - - // TODO: 实现查询场景列表的具体逻辑 - - return NextResponse.json({ - code: 0, - message: "查询成功", - data: { - items: [], - total: 0, - page: params.page, - pageSize: params.pageSize, - totalPages: 0, - }, - }) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "查询失败", - data: null, - }, - { status: 500 }, - ) - } -} - diff --git a/Cunkebao/api/scenarios.ts b/Cunkebao/api/scenarios.ts deleted file mode 100644 index ed77aeb53..000000000 --- a/Cunkebao/api/scenarios.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { request } from "@/lib/api" - -export interface ApiResponse { - code: number; - msg: string; - data: T | null; -} - -// 服务器返回的场景数据类型 -export interface SceneItem { - id: number; - name: string; - image: string; - status: number; - createTime: number; - updateTime: number | null; - deleteTime: number | null; -} - -// 服务器返回的场景列表响应类型 -export interface ScenesResponse { - code: number; - msg: string; - data: SceneItem[]; -} - -// 前端使用的场景数据类型 -export interface Channel { - id: string; - name: string; - icon: string; - stats: { - daily: number; - growth: number; - }; - link?: string; - plans?: Plan[]; -} - -// 计划类型 -export interface Plan { - id: string; - name: string; - isNew?: boolean; - status: "active" | "paused" | "completed"; - acquisitionCount: number; -} - -/** - * 获取获客场景列表 - * - * @param params 查询参数 - * @returns 获客场景列表 - */ -export const fetchScenes = async (params: { - page?: number; - limit?: number; - keyword?: string; -} = {}): Promise => { - const { page = 1, limit = 10, keyword = "" } = params; - - const queryParams = new URLSearchParams(); - queryParams.append("page", String(page)); - queryParams.append("limit", String(limit)); - - if (keyword) { - queryParams.append("keyword", keyword); - } - - try { - return await request(`/v1/plan/scenes?${queryParams.toString()}`); - } catch (error) { - console.error("Error fetching scenes:", error); - // 返回一个错误响应 - return { - code: 500, - msg: "获取场景列表失败", - data: [] - }; - } -}; - -/** - * 将服务器返回的场景数据转换为前端展示需要的格式 - * - * @param item 服务器返回的场景数据 - * @returns 前端展示的场景数据 - */ -export const transformSceneItem = (item: SceneItem): Channel => { - // 为每个场景生成随机的"今日"数据和"增长百分比" - const dailyCount = Math.floor(Math.random() * 100); - const growthPercent = Math.floor(Math.random() * 40) - 10; // -10% 到 30% 的随机值 - - // 默认图标(如果服务器没有返回) - const defaultIcon = "/assets/icons/poster-icon.svg"; - - return { - id: String(item.id), - name: item.name, - icon: item.image || defaultIcon, - stats: { - daily: dailyCount, - growth: growthPercent - } - }; -}; - -/** - * 获取场景详情 - * - * @param id 场景ID - * @returns 场景详情 - */ -export const fetchSceneDetail = async (id: string | number): Promise> => { - try { - return await request>(`/v1/plan/scenes/${id}`); - } catch (error) { - console.error("Error fetching scene detail:", error); - return { - code: 500, - msg: "获取场景详情失败", - data: null - }; - } -}; - diff --git a/Cunkebao/app/api/acquisition/[planId]/orders/route.ts b/Cunkebao/app/api/acquisition/[planId]/orders/route.ts deleted file mode 100644 index 57754319b..000000000 --- a/Cunkebao/app/api/acquisition/[planId]/orders/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextResponse } from "next/server" -import type { OrderFormData } from "@/types/acquisition" - -export async function POST(request: Request, { params }: { params: { planId: string } }) { - try { - const data: OrderFormData = await request.json() - - // 这里应该添加实际的数据库存储逻辑 - console.log("Received order:", data, "for plan:", params.planId) - - // 模拟成功响应 - return NextResponse.json({ - success: true, - message: "订单已成功提交", - }) - } catch (error) { - return NextResponse.json({ success: false, message: "订单提交失败" }, { status: 500 }) - } -} - diff --git a/Cunkebao/app/api/auth.ts b/Cunkebao/app/api/auth.ts deleted file mode 100644 index c17c48c59..000000000 --- a/Cunkebao/app/api/auth.ts +++ /dev/null @@ -1,69 +0,0 @@ -// API请求工具函数 -import { toast } from "@/components/ui/use-toast" - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com" - -// 带有认证的请求函数 -export async function authFetch(url: string, options: RequestInit = {}) { - const token = localStorage.getItem("token") - - // 合并headers - let headers = { ...options.headers } - - // 如果有token,添加到请求头 - if (token) { - headers = { - ...headers, - Token: `${token}`, - } - } - - try { - const response = await fetch(`${API_BASE_URL}${url}`, { - ...options, - headers, - }) - - const data = await response.json() - - // 检查token是否过期(仅当有token时) - if (token && (data.code === 401 || data.code === 403)) { - // 清除token - localStorage.removeItem("token") - - // 暂时不重定向到登录页 - // if (typeof window !== "undefined") { - // window.location.href = "/login" - // } - - console.warn("登录已过期") - } - - return data - } catch (error) { - console.error("API请求错误:", error) - toast({ - variant: "destructive", - title: "请求失败", - description: error instanceof Error ? error.message : "网络错误,请稍后重试", - }) - throw error - } -} - -// 不需要认证的请求函数 -export async function publicFetch(url: string, options: RequestInit = {}) { - try { - const response = await fetch(`${API_BASE_URL}${url}`, options) - return await response.json() - } catch (error) { - console.error("API请求错误:", error) - toast({ - variant: "destructive", - title: "请求失败", - description: error instanceof Error ? error.message : "网络错误,请稍后重试", - }) - throw error - } -} - diff --git a/Cunkebao/app/api/devices/route.ts b/Cunkebao/app/api/devices/route.ts deleted file mode 100644 index 6de78c121..000000000 --- a/Cunkebao/app/api/devices/route.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NextResponse } from "next/server" -import type { - CreateDeviceParams, - QueryDeviceParams, - Device, - ApiResponse, - DeviceStatus, - DeviceType, -} from "@/types/device" - -// 设备管理路由处理 -export async function POST(request: Request) { - try { - const body: CreateDeviceParams = await request.json() - - // TODO: 实现创建设备的具体逻辑 - const device: Device = { - id: "generated-id", - ...body, - status: DeviceStatus.OFFLINE, - lastOnlineTime: new Date().toISOString(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } - - const response: ApiResponse = { - code: 0, - message: "创建成功", - data: device, - } - - return NextResponse.json(response) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "创建失败", - data: null, - }, - { status: 500 }, - ) - } -} - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url) - const params: QueryDeviceParams = { - keyword: searchParams.get("keyword") || undefined, - status: (searchParams.get("status") as DeviceStatus) || undefined, - type: (searchParams.get("type") as DeviceType) || undefined, - tags: searchParams.get("tags") ? JSON.parse(searchParams.get("tags")!) : undefined, - dateRange: searchParams.get("dateRange") ? JSON.parse(searchParams.get("dateRange")!) : undefined, - page: Number(searchParams.get("page")) || 1, - pageSize: Number(searchParams.get("pageSize")) || 20, - } - - // TODO: 实现查询设备列表的具体逻辑 - - return NextResponse.json({ - code: 0, - message: "查询成功", - data: { - items: [], - total: 0, - page: params.page, - pageSize: params.pageSize, - totalPages: 0, - }, - }) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "查询失败", - data: null, - }, - { status: 500 }, - ) - } -} - diff --git a/Cunkebao/app/api/docs/scenarios/page.tsx b/Cunkebao/app/api/docs/scenarios/page.tsx deleted file mode 100644 index f53009ef4..000000000 --- a/Cunkebao/app/api/docs/scenarios/page.tsx +++ /dev/null @@ -1,405 +0,0 @@ -"use client" - -import { useState } from "react" -import { ChevronLeft, Copy, Check, Info } from "lucide-react" -import { Button } from "@/components/ui/button" -import { useRouter } from "next/navigation" -import { useToast } from "@/components/ui/use-toast" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { getApiGuideForScenario } from "@/docs/api-guide" -import { Badge } from "@/components/ui/badge" -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://yishi.com' - -export default function ApiDocPage({ params }: { params: { channel: string; id: string } }) { - const router = useRouter() - const { toast } = useToast() - const [copiedExample, setCopiedExample] = useState(null) - - const apiGuide = getApiGuideForScenario(params.id, params.channel) - - // 假设 fullUrl 和 apiKey 可通过 props 或接口获取,这里用演示值 - const [apiKey] = useState("naxf1-82h2f-vdwcm-rrhpm-q9hd1") - const [fullUrl] = useState("/v1/api/scenarios") - const testUrl = fullUrl.startsWith("http") ? fullUrl : `${API_BASE_URL}${fullUrl}` - - const copyToClipboard = (text: string, exampleId: string) => { - navigator.clipboard.writeText(text) - setCopiedExample(exampleId) - - toast({ - title: "已复制代码", - description: "代码示例已复制到剪贴板", - }) - - setTimeout(() => { - setCopiedExample(null) - }, 2000) - } - - return ( -
-
-
-
- -

计划接口文档

-
-
-
- -
- - - 接口说明 - 本接口用于将外部客户数据导入到存客宝计划。请使用 apiKey 进行身份认证,建议仅在服务端调用。 - - -
-
- -

- 支持多种编程语言和平台集成。接口地址和参数请参考下方说明。 -

-
-
-

- 安全提示: 请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。 -

-
-
-
-
- - - - 签名规则 - - -
-
- 签名参数: sign -
-
- 签名算法: 将所有请求参数(排除 sign 和 apiKey)按参数名升序排序,直接拼接参数值(不含等号和&),对该字符串进行 MD5 加密,得到中间串,再拼接 apiKey,最后再进行一次 MD5 加密,结果作为 sign 参数传递。 -
-
- 签名步骤: -
    -
  1. 去除 sign 和 apiKey 参数,将其余所有参数(如 name、phone、timestamp、source、remark、tags)按参数名升序排序
  2. -
  3. 直接拼接参数值,如 value1value2value3...
  4. -
  5. 对拼接后的字符串进行 MD5 加密,得到中间串
  6. -
  7. 将中间串与 apiKey 直接拼接
  8. -
  9. 对拼接后的字符串再进行一次 MD5 加密,结果即为 sign
  10. -
-
-
- 示例: -
-{`参数:
-  name=张三
-  phone=18888888888
-  timestamp=1700000000
-  apiKey=naxf1-82h2f-vdwcm-rrhpm-q9hd1
-
-排序后拼接(排除apiKey,直接拼接值):
-  张三188888888881700000000
-
-第一步MD5:
-  md5(张三188888888881700000000) = 123456abcdef...
-
-拼接apiKey:
-  123456abcdef...naxf1-82h2f-vdwcm-rrhpm-q9hd1
-
-第二步MD5:
-  sign=md5(123456abcdef...naxf1-82h2f-vdwcm-rrhpm-q9hd1)`}
-                
-
-
注意:所有参数均需参与签名(除 sign 和 apiKey),且参数值需为原始值(不可 URL 编码)。
-
-
-
- - - - 接口地址 - - -
- {testUrl} -
-
-
必要参数: phone (电话), timestamp (时间戳), apiKey (接口密钥)
-
可选参数: name (姓名), source (来源), remark (备注), tags (标签)
-
请求方式: POSTGET
-
-
-
- - - - 请求参数 - - -
-
phone (string, 必填): 客户电话
-
timestamp (int, 必填): 当前时间戳,精确到秒(如 1700000000,建议用 Math.floor(Date.now() / 1000) 获取)
-
apiKey (string, 必填): 接口密钥
-
name (string, 可选): 客户姓名
-
source (string, 可选): 来源
-
remark (string, 可选): 备注
-
tags (string, 可选): 标签
-
-
-
- - - - 响应示例 - - -
-{`{
-  "code": 200,
-  "msg": "导入成功",
-  "data": {
-    "customerId": "123456"
-  }
-}`}
-            
-
-
- - - - - - 代码示例 - 以下是不同编程语言的接口调用示例 - - - - - cURL - Python - Node.js - PHP - Java - - -
{`curl -X POST 'http://yishi.com/v1/plan/api/scenariosz' \
-  -d "phone=18888888888" \
-  -d "timestamp=1700000000" \
-  -d "name=张三" \
-  -d "apiKey=naxf1-82h2f-vdwcm-rrhpm-q9hd1" \
-  -d "sign=请用签名算法生成"`}
-
- -
{`import hashlib
-import time
-import requests
-
-def gen_sign(params, api_key):
-    data = {k: v for k, v in params.items() if k not in ('sign', 'apiKey')}
-    s = ''.join([str(data[k]) for k in sorted(data)])
-    first = hashlib.md5(s.encode('utf-8')).hexdigest()
-    return hashlib.md5((first + api_key).encode('utf-8')).hexdigest()
-
-api_key = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1'
-params = {
-    'phone': '18888888888',
-    'timestamp': int(time.time()),
-    'name': '张三',
-}
-params['apiKey'] = api_key
-params['sign'] = gen_sign(params, api_key)
-resp = requests.post('http://yishi.com/v1/plan/api/scenariosz', data=params)
-print(resp.json())`}
-
- -
{`const axios = require('axios');
-const crypto = require('crypto');
-
-function genSign(params, apiKey) {
-  const data = {...params};
-  delete data.sign;
-  delete data.apiKey;
-  const keys = Object.keys(data).sort();
-  let str = '';
-  keys.forEach(k => { str += data[k]; });
-  const first = crypto.createHash('md5').update(str).digest('hex');
-  return crypto.createHash('md5').update(first + apiKey).digest('hex');
-}
-
-const apiKey = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1';
-const params = {
-  phone: '18888888888',
-  timestamp: Math.floor(Date.now() / 1000),
-  name: '张三',
-};
-params.apiKey = apiKey;
-params.sign = genSign(params, apiKey);
-
-axios.post('http://yishi.com/v1/plan/api/scenariosz', params)
-  .then(res => console.log(res.data));`}
-
- -
{` '18888888888',
-    'timestamp' => time(),
-    'name' => '张三',
-    // 'source' => '', 'remark' => '', 'tags' => ''
-];
-$apiKey = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1';
-$params['apiKey'] = $apiKey;
-$params['sign'] = md5_sign($params, $apiKey);
-
-$url = '${API_BASE_URL}/v1/api/scenarios';
-
-$ch = curl_init();
-curl_setopt($ch, CURLOPT_URL, $url);
-curl_setopt($ch, CURLOPT_POST, 1);
-curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-$response = curl_exec($ch);
-curl_close($ch);
-echo $response;
-?>`}
-
- -
{`import java.security.MessageDigest;
-import java.util.*;
-import java.net.*;
-import java.io.*;
-
-public class ApiSignDemo {
-    public static String md5(String s) throws Exception {
-        MessageDigest md = MessageDigest.getInstance("MD5");
-        byte[] array = md.digest(s.getBytes("UTF-8"));
-        StringBuilder sb = new StringBuilder();
-        for (byte b : array) sb.append(String.format("%02x", b));
-        return sb.toString();
-    }
-    public static void main(String[] args) throws Exception {
-        Map params = new HashMap<>();
-        params.put("phone", "18888888888");
-        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
-        params.put("name", "张三");
-        // params.put("source", ""); params.put("remark", ""); params.put("tags", "");
-        String apiKey = "naxf1-82h2f-vdwcm-rrhpm-q9hd1";
-        // 排序并拼接
-        List keys = new ArrayList<>(params.keySet());
-        keys.remove("sign");
-        keys.remove("apiKey");
-        Collections.sort(keys);
-        StringBuilder sb = new StringBuilder();
-        for (String k : keys) {
-            sb.append(params.get(k));
-        }
-        String first = md5(sb.toString());
-        String sign = md5(first + apiKey);
-        params.put("apiKey", apiKey);
-        params.put("sign", sign);
-        // 发送POST请求
-        StringBuilder postData = new StringBuilder();
-        for (Map.Entry entry : params.entrySet()) {
-            if (postData.length() > 0) postData.append("&");
-            postData.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
-            postData.append("=");
-            postData.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
-        }
-        URL url = new URL("${API_BASE_URL}/v1/api/scenarios");
-        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-        conn.setRequestMethod("POST");
-        conn.setDoOutput(true);
-        OutputStream os = conn.getOutputStream();
-        os.write(postData.toString().getBytes("UTF-8"));
-        os.flush(); os.close();
-        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-        String line; StringBuilder resp = new StringBuilder();
-        while ((line = in.readLine()) != null) resp.append(line);
-        in.close();
-        System.out.println(resp.toString());
-    }
-}`}
-
-
-
-
- -
-

集成指南

- - - - 集简云平台集成 - - -
    -
  1. 登录集简云平台
  2. -
  3. 导航至"应用集成" > "外部接口"
  4. -
  5. 选择"添加新接口",输入存客宝接口信息
  6. -
  7. 配置回调参数,将"X-API-KEY"设置为您的API密钥
  8. -
  9. - 设置接口URL为: - - {testUrl} - -
  10. -
  11. 映射必要字段(name, phone等)
  12. -
  13. 保存并启用集成
  14. -
-
-
- - - - 问题排查 - - -
-

接口认证失败

-

- 请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。 -

-
- -
-

数据格式错误

-

- 确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。 -

-
- -
-

请求频率限制

-

- 单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。 -

-
-
-
-
-
-
- ) -} - diff --git a/Cunkebao/app/api/scenarios/route.ts b/Cunkebao/app/api/scenarios/route.ts deleted file mode 100644 index f82d21d8b..000000000 --- a/Cunkebao/app/api/scenarios/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from "next/server" -import type { CreateScenarioParams, QueryScenarioParams, ScenarioBase, ApiResponse } from "@/types/scenario" - -// 获客场景路由处理 -export async function POST(request: Request) { - try { - const body: CreateScenarioParams = await request.json() - - // TODO: 实现创建场景的具体逻辑 - const scenario: ScenarioBase = { - id: "generated-id", - ...body, - status: "draft", - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - creator: "current-user-id", - } - - const response: ApiResponse = { - code: 0, - message: "创建成功", - data: scenario, - } - - return NextResponse.json(response) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "创建失败", - data: null, - }, - { status: 500 }, - ) - } -} - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url) - const params: QueryScenarioParams = { - type: searchParams.get("type") as any, - status: searchParams.get("status") as any, - keyword: searchParams.get("keyword") || undefined, - dateRange: searchParams.get("dateRange") ? JSON.parse(searchParams.get("dateRange")!) : undefined, - page: Number(searchParams.get("page")) || 1, - pageSize: Number(searchParams.get("pageSize")) || 20, - } - - // TODO: 实现查询场景列表的具体逻辑 - - return NextResponse.json({ - code: 0, - message: "查询成功", - data: { - items: [], - total: 0, - page: params.page, - pageSize: params.pageSize, - totalPages: 0, - }, - }) - } catch (error) { - return NextResponse.json( - { - code: 500, - message: "查询失败", - data: null, - }, - { status: 500 }, - ) - } -} - diff --git a/Cunkebao/app/api/users/route.ts b/Cunkebao/app/api/users/route.ts deleted file mode 100644 index 31046e372..000000000 --- a/Cunkebao/app/api/users/route.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { NextResponse } from "next/server" -import type { TrafficUser } from "@/types/traffic" - -// 中文名字生成器数据 -const familyNames = [ - "张", - "王", - "李", - "赵", - "陈", - "刘", - "杨", - "黄", - "周", - "吴", - "朱", - "孙", - "马", - "胡", - "郭", - "林", - "何", - "高", - "梁", - "郑", - "罗", - "宋", - "谢", - "唐", - "韩", - "曹", - "许", - "邓", - "萧", - "冯", -] -const givenNames1 = [ - "志", - "建", - "文", - "明", - "永", - "春", - "秀", - "金", - "水", - "玉", - "国", - "立", - "德", - "海", - "和", - "荣", - "伟", - "新", - "英", - "佳", -] -const givenNames2 = [ - "华", - "平", - "军", - "强", - "辉", - "敏", - "峰", - "磊", - "超", - "艳", - "娜", - "霞", - "燕", - "娟", - "静", - "丽", - "涛", - "洋", - "勇", - "龙", -] - -// 生成固定的用户数据池 -const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => { - const familyName = familyNames[Math.floor(Math.random() * familyNames.length)] - const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)] - const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)] - const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1 - - // 生成随机时间(在过去7天内) - const date = new Date() - date.setDate(date.getDate() - Math.floor(Math.random() * 7)) - - return { - id: `${Date.now()}-${i}`, - avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`, - nickname: fullName, - wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, - phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`, - region: [ - "广东深圳", - "浙江杭州", - "江苏苏州", - "北京", - "上海", - "四川成都", - "湖北武汉", - "福建厦门", - "山东青岛", - "河南郑州", - ][Math.floor(Math.random() * 10)], - note: [ - "咨询产品价格", - "对产品很感兴趣", - "准备购买", - "需要更多信息", - "想了解优惠活动", - "询问产品规格", - "要求产品demo", - "索要产品目录", - "询问售后服务", - "要求上门演示", - ][Math.floor(Math.random() * 10)], - status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"], - addTime: date.toISOString(), - source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页"][Math.floor(Math.random() * 6)], - assignedTo: "", - category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"], - tags: [], - } -}) - -// 计算今日新增数量 -const todayStart = new Date() -todayStart.setHours(0, 0, 0, 0) -const todayUsers = userPool.filter((user) => new Date(user.addTime) >= todayStart) - -// 生成微信好友数据池 -const generateWechatFriends = (wechatId: string, count: number) => { - return Array.from({ length: count }, (_, i) => { - const familyName = familyNames[Math.floor(Math.random() * familyNames.length)] - const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)] - const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)] - const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1 - - // 生成随机时间(在过去30天内) - const date = new Date() - date.setDate(date.getDate() - Math.floor(Math.random() * 30)) - - return { - id: `wechat-${wechatId}-${i}`, - avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`, - nickname: fullName, - wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, - phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`, - region: [ - "广东深圳", - "浙江杭州", - "江苏苏州", - "北京", - "上海", - "四川成都", - "湖北武汉", - "福建厦门", - "山东青岛", - "河南郑州", - ][Math.floor(Math.random() * 10)], - note: [ - "咨询产品价格", - "对产品很感兴趣", - "准备购买", - "需要更多信息", - "想了解优惠活动", - "询问产品规格", - "要求产品demo", - "索要产品目录", - "询问售后服务", - "要求上门演示", - ][Math.floor(Math.random() * 10)], - status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"], - addTime: date.toISOString(), - source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页", "微信好友"][ - Math.floor(Math.random() * 7) - ], - assignedTo: "", - category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"], - tags: [], - } - }) -} - -// 微信好友数据缓存 -const wechatFriendsCache = new Map() - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const page = Number.parseInt(searchParams.get("page") || "1") - const pageSize = Number.parseInt(searchParams.get("pageSize") || "10") - const search = searchParams.get("search") || "" - const category = searchParams.get("category") || "all" - const source = searchParams.get("source") || "all" - const status = searchParams.get("status") || "all" - const startDate = searchParams.get("startDate") - const endDate = searchParams.get("endDate") - const wechatSource = searchParams.get("wechatSource") || "" - - let filteredUsers = [...userPool] - - // 如果有微信来源参数,生成或获取微信好友数据 - if (wechatSource) { - if (!wechatFriendsCache.has(wechatSource)) { - // 生成150-300个随机好友 - const friendCount = Math.floor(Math.random() * (300 - 150)) + 150 - wechatFriendsCache.set(wechatSource, generateWechatFriends(wechatSource, friendCount)) - } - filteredUsers = wechatFriendsCache.get(wechatSource) || [] - } - - // 应用过滤条件 - filteredUsers = filteredUsers.filter((user) => { - const matchesSearch = search - ? user.nickname.toLowerCase().includes(search.toLowerCase()) || - user.wechatId.toLowerCase().includes(search.toLowerCase()) || - user.phone.includes(search) - : true - - const matchesCategory = category === "all" ? true : user.category === category - const matchesSource = source === "all" ? true : user.source === source - const matchesStatus = status === "all" ? true : user.status === status - - const matchesDate = - startDate && endDate - ? new Date(user.addTime) >= new Date(startDate) && new Date(user.addTime) <= new Date(endDate) - : true - - return matchesSearch && matchesCategory && matchesSource && matchesStatus && matchesDate - }) - - // 按添加时间倒序排序 - filteredUsers.sort((a, b) => new Date(b.addTime).getTime() - new Date(a.addTime).getTime()) - - // 计算分页 - const total = filteredUsers.length - const totalPages = Math.ceil(total / pageSize) - const start = (page - 1) * pageSize - const end = start + pageSize - const users = filteredUsers.slice(start, end) - - // 计算分类统计 - const categoryStats = { - potential: userPool.filter((user) => user.category === "potential").length, - customer: userPool.filter((user) => user.category === "customer").length, - lost: userPool.filter((user) => user.category === "lost").length, - } - - // 模拟网络延迟 - await new Promise((resolve) => setTimeout(resolve, 500)) - - return NextResponse.json({ - users, - pagination: { - total, - totalPages, - currentPage: page, - pageSize, - }, - stats: { - total: wechatSource ? filteredUsers.length : userPool.length, - todayNew: wechatSource ? Math.floor(filteredUsers.length * 0.1) : todayUsers.length, - categoryStats, - }, - }) -} - diff --git a/Cunkebao/app/clientLayout.tsx b/Cunkebao/app/clientLayout.tsx deleted file mode 100644 index cb105ad8d..000000000 --- a/Cunkebao/app/clientLayout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client" - -import "./globals.css" -import BottomNav from "./components/BottomNav" -import "regenerator-runtime/runtime" -import type React from "react" -import ErrorBoundary from "./components/ErrorBoundary" -import { VideoTutorialButton } from "@/components/VideoTutorialButton" -import { AuthProvider } from "@/app/components/AuthProvider" -import { usePathname } from "next/navigation" - -// 创建一个包装组件来使用 usePathname hook -function LayoutContent({ children }: { children: React.ReactNode }) { - const pathname = usePathname() - - // 只在主页路径显示底部导航栏 - const showBottomNav = - pathname === "/" || pathname === "/devices" || pathname === "/content" || pathname === "/profile" - - return ( -
-
- {children} - {showBottomNav && } - {showBottomNav && } -
-
- ) -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - - - {children} - - - - - ) -} - diff --git a/Cunkebao/app/components/AIRewriteModal.tsx b/Cunkebao/app/components/AIRewriteModal.tsx deleted file mode 100644 index ffec94baf..000000000 --- a/Cunkebao/app/components/AIRewriteModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client" - -import { useState } from "react" -import { X } from "lucide-react" -import { Button } from "./ui/button" -import { Card } from "./ui/card" - -interface AIRewriteModalProps { - isOpen: boolean - onClose: () => void - originalContent: string -} - -export function AIRewriteModal({ isOpen, onClose, originalContent }: AIRewriteModalProps) { - const [rewrittenContent, setRewrittenContent] = useState("") - - const handleRewrite = async () => { - // 这里应该调用 AI 改写 API - // 为了演示,我们只是简单地反转字符串 - setRewrittenContent(originalContent.split("").reverse().join("")) - } - - if (!isOpen) return null - - return ( -
- -
-

AI 内容改写

- -
-
-
-

原始内容:

-

{originalContent}

-
-
-

改写后内容:

-

{rewrittenContent || '点击"开始改写"按钮生成内容'}

-
-
-
- - -
-
-
- ) -} - diff --git a/Cunkebao/app/components/AuthProvider.tsx b/Cunkebao/app/components/AuthProvider.tsx deleted file mode 100644 index 8f5877add..000000000 --- a/Cunkebao/app/components/AuthProvider.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client" - -import { createContext, useContext, useEffect, useState, type ReactNode } from "react" -import { useRouter } from "next/navigation" -import { validateToken } from "@/lib/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; -} - -interface AuthContextType { - isAuthenticated: boolean - token: string | null - user: User | null - login: (token: string, userData: User) => void - logout: () => void - updateToken: (newToken: string) => void -} - -// 创建默认上下文 -const AuthContext = createContext({ - isAuthenticated: false, - token: null, - user: null, - login: () => {}, - logout: () => {}, - updateToken: () => {} -}) - -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) - // 初始页面加载时显示为false,避免在服务端渲染和客户端水合时不匹配 - const [isLoading, setIsLoading] = useState(false) - const [isInitialized, setIsInitialized] = useState(false) - const router = useRouter() - - // 初始化认证状态 - useEffect(() => { - // 仅在客户端执行初始化 - setIsLoading(true) - - const initAuth = async () => { - try { - const storedToken = safeLocalStorage.getItem("token") - - if (storedToken) { - // 首先尝试从localStorage获取用户信息 - const userDataStr = safeLocalStorage.getItem("userInfo") - if (userDataStr) { - try { - // 如果能解析用户数据,先设置登录状态 - const userData = JSON.parse(userDataStr) as User - setToken(storedToken) - setUser(userData) - setIsAuthenticated(true) - - // 然后在后台尝试验证token,但不影响当前登录状态 - validateToken().then(isValid => { - // 只有在确认token绝对无效时才登出 - // 网络错误等情况默认保持登录状态 - if (isValid === false) { - console.warn('验证token失败,但仍允许用户保持登录状态') - } - }).catch(error => { - // 捕获所有验证过程中的错误,并记录日志 - console.error('验证token过程中出错:', error) - // 网络错误等不会导致登出 - }) - } catch (parseError) { - // 用户数据无法解析,需要清除 - console.error('解析用户数据失败:', parseError) - handleLogout() - } - } else { - // 有token但没有用户信息,可能是部分数据丢失 - console.warn('找到token但没有用户信息,尝试保持登录状态') - - // 尝试验证token并获取用户信息 - try { - const isValid = await validateToken() - if (isValid) { - // 如果token有效,尝试从API获取用户信息 - // 这里简化处理,直接使用token - setToken(storedToken) - setIsAuthenticated(true) - } else { - // token确认无效,清除 - handleLogout() - } - } catch (error) { - // 验证过程出错,记录日志但不登出 - console.error('验证token过程中出错:', error) - // 保留token,允许用户继续使用 - setToken(storedToken) - setIsAuthenticated(true) - } - } - } - } catch (error) { - console.error("初始化认证状态时出错:", error) - // 非401错误不应强制登出 - if (error instanceof Error && - (error.message.includes('401') || - error.message.includes('未授权') || - error.message.includes('token'))) { - handleLogout() - } - } finally { - setIsLoading(false) - setIsInitialized(true) - } - } - - initAuth() - }, []) // 空依赖数组,仅在组件挂载时执行一次 - - 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) - - // 使用 window.location 而不是 router.push,避免状态更新和路由跳转的竞态条件 - if (typeof window !== 'undefined') { - window.location.href = '/login' - } - } - - const login = (newToken: string, userData: User) => { - safeLocalStorage.setItem("token", newToken) - safeLocalStorage.setItem("userInfo", JSON.stringify(userData)) - setToken(newToken) - setUser(userData) - setIsAuthenticated(true) - } - - const logout = () => { - handleLogout() - } - - // 用于刷新 token 的方法 - const updateToken = (newToken: string) => { - safeLocalStorage.setItem("token", newToken) - setToken(newToken) - } - - return ( - - {isLoading && isInitialized ? ( -
加载中...
- ) : ( - children - )} -
- ) -} - diff --git a/Cunkebao/app/components/BindDouyinQRCode.tsx b/Cunkebao/app/components/BindDouyinQRCode.tsx deleted file mode 100644 index 41b568091..000000000 --- a/Cunkebao/app/components/BindDouyinQRCode.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client" - -import { useState } from "react" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { QrCode } from "lucide-react" - -export function BindDouyinQRCode() { - const [isOpen, setIsOpen] = useState(false) - - return ( - <> - - - - - 绑定抖音号 - -
-
- 抖音二维码 -
-

请使用抖音APP扫描二维码进行绑定

-
-
-
- - ) -} - diff --git a/Cunkebao/app/components/BottomNav.tsx b/Cunkebao/app/components/BottomNav.tsx deleted file mode 100644 index 969385051..000000000 --- a/Cunkebao/app/components/BottomNav.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client" - -import Link from "next/link" -import { usePathname } from "next/navigation" -import { Home, Users, User, Briefcase } from "lucide-react" - -const navItems = [ - { href: "/", icon: Home, label: "首页" }, - { href: "/scenarios", icon: Users, label: "场景获客" }, - { href: "/workspace", icon: Briefcase, label: "工作台" }, - { href: "/profile", icon: User, label: "我的" }, -] - -export default function BottomNav() { - const pathname = usePathname() - - return ( - - ) -} - diff --git a/Cunkebao/app/components/Charts.tsx b/Cunkebao/app/components/Charts.tsx deleted file mode 100644 index 9a2ff235d..000000000 --- a/Cunkebao/app/components/Charts.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client" - -import { - LineChart as RechartsLineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from "recharts" -import { BarChart as RechartsBarChart, Bar } from "recharts" - -const lineData = [ - { name: "周一", 新增微信号: 12 }, - { name: "周二", 新增微信号: 19 }, - { name: "周三", 新增微信号: 3 }, - { name: "周四", 新增微信号: 5 }, - { name: "周五", 新增微信号: 2 }, - { name: "周六", 新增微信号: 3 }, - { name: "周日", 新增微信号: 10 }, -] - -const barData = [ - { name: "周一", 新增好友: 120 }, - { name: "周二", 新增好友: 190 }, - { name: "周三", 新增好友: 30 }, - { name: "周四", 新增好友: 50 }, - { name: "周五", 新增好友: 20 }, - { name: "周六", 新增好友: 30 }, - { name: "周日", 新增好友: 100 }, -] - -export function LineChart() { - return ( - - - - - - - - - - ) -} - -export function BarChart() { - return ( - - - - - - - - - - ) -} - -export function TrendChart({ data, dataKey = "value", height = 300 }) { - return ( -
- - - - - - - - - -
- ) -} - diff --git a/Cunkebao/app/components/ChatMessage.tsx b/Cunkebao/app/components/ChatMessage.tsx deleted file mode 100644 index 45b239f64..000000000 --- a/Cunkebao/app/components/ChatMessage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Avatar } from "@/components/ui/avatar" -import { cn } from "@/lib/utils" - -interface ChatMessageProps { - content: string - isUser: boolean - timestamp: Date - avatar?: string -} - -export function ChatMessage({ content, isUser, timestamp, avatar }: ChatMessageProps) { - return ( -
- {!isUser && ( - -
- AI -
-
- )} - -
-

{content}

-
- {timestamp.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} -
-
- - {isUser && avatar && ( - - User - - )} -
- ) -} - diff --git a/Cunkebao/app/components/CircleSync/ContentSelector.tsx b/Cunkebao/app/components/CircleSync/ContentSelector.tsx deleted file mode 100644 index 762189211..000000000 --- a/Cunkebao/app/components/CircleSync/ContentSelector.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card } from "../ui/card" -import { Button } from "../ui/button" -import { Input } from "../ui/input" -import { ChevronLeft, Search, Plus } from "lucide-react" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table" - -interface ContentLibrary { - id: string - name: string - type: string - count: number -} - -const mockLibraries: ContentLibrary[] = [ - { - id: "1", - name: "卡若朋友圈", - type: "朋友圈", - count: 307, - }, - { - id: "2", - name: "业务推广内容", - type: "朋友圈", - count: 156, - }, -] - -export function ContentSelector({ onPrev, onFinish }) { - const [selectedLibraries, setSelectedLibraries] = useState([]) - - return ( - -
-

选择内容库

- -
- -
-
- } /> -
-
- - - - - 选择 - 内容库名称 - 类型 - 内容数量 - 操作 - - - - {mockLibraries.map((library) => ( - - - { - if (e.target.checked) { - setSelectedLibraries([...selectedLibraries, library.id]) - } else { - setSelectedLibraries(selectedLibraries.filter((id) => id !== library.id)) - } - }} - /> - - {library.name} - {library.type} - {library.count} - - - - - ))} - -
- -
- - -
-
- ) -} - diff --git a/Cunkebao/app/components/CircleSync/DeviceSelector.tsx b/Cunkebao/app/components/CircleSync/DeviceSelector.tsx deleted file mode 100644 index 55fa79f2e..000000000 --- a/Cunkebao/app/components/CircleSync/DeviceSelector.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card } from "../ui/card" -import { Button } from "../ui/button" -import { Input } from "../ui/input" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table" -import { ChevronLeft, ChevronRight, Search, Plus } from "lucide-react" - -interface Device { - id: string - imei: string - status: "online" | "offline" - friendStatus: string -} - -const mockDevices: Device[] = [ - { - id: "1", - imei: "123456789012345", - status: "online", - friendStatus: "正常", - }, - { - id: "2", - imei: "987654321098765", - status: "offline", - friendStatus: "异常", - }, -] - -export function DeviceSelector({ onNext, onPrev }) { - const [selectedDevices, setSelectedDevices] = useState([]) - - return ( - -
-

选择推送设备

- -
- -
-
- } - /> -
-
- - - - - 选择 - 设备IMEI/备注/手机号 - 在线状态 - 加友状态 - 操作 - - - - {mockDevices.map((device) => ( - - - { - if (e.target.checked) { - setSelectedDevices([...selectedDevices, device.id]) - } else { - setSelectedDevices(selectedDevices.filter((id) => id !== device.id)) - } - }} - /> - - {device.imei} - - - {device.status === "online" ? "在线" : "离线"} - - - - - {device.friendStatus} - - - - - - - ))} - -
- -
- - -
-
- ) -} - diff --git a/Cunkebao/app/components/CircleSync/TaskSetup.tsx b/Cunkebao/app/components/CircleSync/TaskSetup.tsx deleted file mode 100644 index 52b959bd4..000000000 --- a/Cunkebao/app/components/CircleSync/TaskSetup.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card } from "../ui/card" -import { Button } from "../ui/button" -import { Input } from "../ui/input" -import { Switch } from "../ui/switch" -import { Label } from "../ui/label" -import { ChevronLeft, ChevronRight, Plus, Minus } from "lucide-react" - -interface TaskSetupProps { - onNext?: () => void - onPrev?: () => void - step: number -} - -export function TaskSetup({ onNext, onPrev, step }: TaskSetupProps) { - const [syncCount, setSyncCount] = useState(5) - const [startTime, setStartTime] = useState("06:00") - const [endTime, setEndTime] = useState("23:59") - const [isEnabled, setIsEnabled] = useState(true) - const [accountType, setAccountType] = useState("business") // business or personal - - return ( - -
-

朋友圈同步任务

-
- - -
-
- -
-
- - -
- -
- -
- setStartTime(e.target.value)} className="w-32" /> - - setEndTime(e.target.value)} className="w-32" /> -
-
- -
- -
- - {syncCount} - - 条朋友圈 -
-
- -
- -
- - -
-
-
- -
- {step > 1 ? ( - - ) : ( -
- )} - -
- - ) -} - diff --git a/Cunkebao/app/components/DeviceSelector.tsx b/Cunkebao/app/components/DeviceSelector.tsx deleted file mode 100644 index ebd1ba62c..000000000 --- a/Cunkebao/app/components/DeviceSelector.tsx +++ /dev/null @@ -1,257 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Filter, Search, RefreshCw, AlertCircle } from "lucide-react" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Checkbox } from "@/components/ui/checkbox" -import { toast } from "@/components/ui/use-toast" -import { Progress } from "@/components/ui/progress" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination" -import { ImeiDisplay } from "@/components/ImeiDisplay" - -interface WechatAccount { - wechatId: string - nickname: string - remainingAdds: number - maxDailyAdds: number - todayAdded: number -} - -interface Device { - id: string - imei: string - name: string - status: "online" | "offline" - wechatAccounts: WechatAccount[] - usedInPlans: number -} - -interface DeviceSelectorProps { - onSelect: (selectedDevices: string[]) => void - initialSelectedDevices?: string[] - excludeUsedDevices?: boolean -} - -export function DeviceSelector({ - onSelect, - initialSelectedDevices = [], - excludeUsedDevices = true, -}: DeviceSelectorProps) { - const [devices, setDevices] = useState([]) - const [selectedDevices, setSelectedDevices] = useState(initialSelectedDevices) - const [searchQuery, setSearchQuery] = useState("") - const [statusFilter, setStatusFilter] = useState("all") - const [currentPage, setCurrentPage] = useState(1) - const devicesPerPage = 10 - - useEffect(() => { - // 模拟获取设备数据 - const fetchDevices = async () => { - await new Promise((resolve) => setTimeout(resolve, 500)) - const mockDevices = Array.from({ length: 42 }, (_, i) => ({ - id: `device-${i + 1}`, - imei: `IMEI-${Math.random().toString(36).substr(2, 9)}`, - name: `设备 ${i + 1}`, - status: Math.random() > 0.3 ? "online" : "offline", - wechatAccounts: Array.from({ length: Math.floor(Math.random() * 2) + 1 }, (_, j) => ({ - wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, - nickname: `微信号 ${j + 1}`, - remainingAdds: Math.floor(Math.random() * 10) + 5, - maxDailyAdds: 20, - todayAdded: Math.floor(Math.random() * 15), - })), - usedInPlans: Math.floor(Math.random() * 3), - })) - setDevices(mockDevices) - } - - fetchDevices() - }, []) - - const handleRefresh = () => { - toast({ - title: "刷新成功", - description: "设备列表已更新", - }) - } - - const filteredDevices = devices.filter((device) => { - const matchesSearch = - device.name.toLowerCase().includes(searchQuery.toLowerCase()) || - device.imei.toLowerCase().includes(searchQuery.toLowerCase()) || - device.wechatAccounts.some((account) => account.wechatId.toLowerCase().includes(searchQuery.toLowerCase())) - const matchesStatus = statusFilter === "all" || device.status === statusFilter - const matchesUsage = !excludeUsedDevices || device.usedInPlans === 0 - return matchesSearch && matchesStatus && matchesUsage - }) - - const paginatedDevices = filteredDevices.slice((currentPage - 1) * devicesPerPage, currentPage * devicesPerPage) - - const handleSelectAll = () => { - if (selectedDevices.length === paginatedDevices.length) { - setSelectedDevices([]) - } else { - setSelectedDevices(paginatedDevices.map((device) => device.id)) - } - onSelect(selectedDevices) - } - - const handleDeviceSelect = (deviceId: string) => { - const updatedSelection = selectedDevices.includes(deviceId) - ? selectedDevices.filter((id) => id !== deviceId) - : [...selectedDevices, deviceId] - setSelectedDevices(updatedSelection) - onSelect(updatedSelection) - } - - return ( -
-
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
- - -
- -
- - -
- -
- {paginatedDevices.map((device) => ( - -
- handleDeviceSelect(device.id)} - /> -
-
-
{device.name}
-
- {device.status === "online" ? "在线" : "离线"} -
-
-
- IMEI: - -
-
- {device.wechatAccounts.map((account) => ( -
-
- {account.nickname} - {account.wechatId} -
-
-
-
- 今日可添加: - {account.remainingAdds} - - - - - - -

每日最多添加 {account.maxDailyAdds} 个好友

-
-
-
-
- - {account.todayAdded}/{account.maxDailyAdds} - -
- -
-
- ))} -
- {!excludeUsedDevices && device.usedInPlans > 0 && ( -
已用于 {device.usedInPlans} 个计划
- )} -
-
-
- ))} -
- - - - - { - e.preventDefault() - setCurrentPage((prev) => Math.max(1, prev - 1)) - }} - /> - - {Array.from({ length: Math.ceil(filteredDevices.length / devicesPerPage) }, (_, i) => i + 1).map((page) => ( - - { - e.preventDefault() - setCurrentPage(page) - }} - > - {page} - - - ))} - - { - e.preventDefault() - setCurrentPage((prev) => Math.min(Math.ceil(filteredDevices.length / devicesPerPage), prev + 1)) - }} - /> - - - -
- ) -} - diff --git a/Cunkebao/app/components/ErrorBoundary.tsx b/Cunkebao/app/components/ErrorBoundary.tsx deleted file mode 100644 index fec4f4697..000000000 --- a/Cunkebao/app/components/ErrorBoundary.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client" - -import React, { type ErrorInfo } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" - -interface ErrorBoundaryProps { - children: React.ReactNode -} - -interface ErrorBoundaryState { - hasError: boolean - error?: Error -} - -class ErrorBoundary extends React.Component { - constructor(props: ErrorBoundaryProps) { - super(props) - this.state = { hasError: false } - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("Uncaught error:", error, errorInfo) - } - - render() { - if (this.state.hasError) { - return ( - -

出错了

-

抱歉,应用程序遇到了一个错误。

-

{this.state.error?.message}

- -
- ) - } - - return this.props.children - } -} - -export default ErrorBoundary - diff --git a/Cunkebao/app/components/FileUploader.tsx b/Cunkebao/app/components/FileUploader.tsx deleted file mode 100644 index 6ef40946a..000000000 --- a/Cunkebao/app/components/FileUploader.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client" - -import type React from "react" - -import { useState, useRef } from "react" -import { Button } from "@/components/ui/button" -import { Upload, X } from "lucide-react" -import { Progress } from "@/components/ui/progress" - -interface FileUploaderProps { - onFileUploaded: (file: File) => void - acceptedTypes?: string - maxSize?: number // in MB -} - -export function FileUploader({ - onFileUploaded, - acceptedTypes = ".pdf,.doc,.docx,.jpg,.jpeg,.png", - maxSize = 10, // 10MB default -}: FileUploaderProps) { - const [dragActive, setDragActive] = useState(false) - const [selectedFile, setSelectedFile] = useState(null) - const [uploadProgress, setUploadProgress] = useState(0) - const [error, setError] = useState(null) - const inputRef = useRef(null) - - const handleDrag = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - if (e.type === "dragenter" || e.type === "dragover") { - setDragActive(true) - } else if (e.type === "dragleave") { - setDragActive(false) - } - } - - const validateFile = (file: File): boolean => { - // 检查文件类型 - const fileType = file.name.split(".").pop()?.toLowerCase() || "" - const isValidType = acceptedTypes.includes(fileType) - - // 检查文件大小 - const isValidSize = file.size <= maxSize * 1024 * 1024 - - if (!isValidType) { - setError(`不支持的文件类型。请上传 ${acceptedTypes} 格式的文件。`) - return false - } - - if (!isValidSize) { - setError(`文件过大。最大支持 ${maxSize}MB。`) - return false - } - - return true - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragActive(false) - - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - const file = e.dataTransfer.files[0] - handleFile(file) - } - } - - const handleChange = (e: React.ChangeEvent) => { - e.preventDefault() - if (e.target.files && e.target.files[0]) { - const file = e.target.files[0] - handleFile(file) - } - } - - const handleFile = (file: File) => { - setError(null) - - if (validateFile(file)) { - setSelectedFile(file) - simulateUpload(file) - } - } - - const simulateUpload = (file: File) => { - // 模拟上传进度 - setUploadProgress(0) - const interval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 100) { - clearInterval(interval) - onFileUploaded(file) - return 100 - } - return prev + 10 - }) - }, 200) - } - - const handleButtonClick = () => { - inputRef.current?.click() - } - - const cancelUpload = () => { - setSelectedFile(null) - setUploadProgress(0) - setError(null) - } - - return ( -
- {!selectedFile ? ( -
- - -

拖拽文件到此处,或

- -

- 支持 {acceptedTypes.replace(/\./g, "")} 格式,最大 {maxSize}MB -

-
- ) : ( -
-
-
{selectedFile.name}
- -
- -
{uploadProgress}%
-
- )} - - {error &&
{error}
} -
- ) -} - diff --git a/Cunkebao/app/components/LayoutWrapper.tsx b/Cunkebao/app/components/LayoutWrapper.tsx deleted file mode 100644 index 6e2e4353f..000000000 --- a/Cunkebao/app/components/LayoutWrapper.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import { usePathname } from "next/navigation" -import BottomNav from "./BottomNav" -import { VideoTutorialButton } from "@/components/VideoTutorialButton" -import type React from "react" -import { createContext, useContext, useState, useEffect } from "react" - -// 创建视图模式上下文 -const ViewModeContext = createContext<{ viewMode: "desktop" | "mobile" }>({ viewMode: "desktop" }) - -// 创建视图模式钩子函数 -export function useViewMode() { - const context = useContext(ViewModeContext) - if (!context) { - throw new Error("useViewMode must be used within a LayoutWrapper") - } - return context -} - -export default function LayoutWrapper({ children }: { children: React.ReactNode }) { - const pathname = usePathname() - const [viewMode, setViewMode] = useState<"desktop" | "mobile">("desktop") - - // 检测视图模式 - useEffect(() => { - const checkViewMode = () => { - setViewMode(window.innerWidth < 768 ? "mobile" : "desktop") - } - - // 初始检测 - checkViewMode() - - // 监听窗口大小变化 - window.addEventListener("resize", checkViewMode) - - return () => { - window.removeEventListener("resize", checkViewMode) - } - }, []) - - // 只在四个主页显示底部导航栏:首页、场景获客、工作台和我的 - const mainPages = ["/", "/scenarios", "/workspace", "/profile"] - const showBottomNav = mainPages.includes(pathname) - - return ( - -
-
- {children} - {showBottomNav && } - {showBottomNav && } -
-
-
- ) -} - diff --git a/Cunkebao/app/components/SpeechToTextProcessor.tsx b/Cunkebao/app/components/SpeechToTextProcessor.tsx deleted file mode 100644 index 9f76b7c99..000000000 --- a/Cunkebao/app/components/SpeechToTextProcessor.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { toast } from "@/components/ui/use-toast" - -interface SpeechToTextProcessorProps { - audioUrl: string - onTranscriptReady: (transcript: string) => void - onQuestionExtracted: (question: string) => void - enabled: boolean -} - -export function SpeechToTextProcessor({ - audioUrl, - onTranscriptReady, - onQuestionExtracted, - enabled = true, -}: SpeechToTextProcessorProps) { - const [isProcessing, setIsProcessing] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (!enabled || !audioUrl) return - - const processAudio = async () => { - try { - setIsProcessing(true) - setError(null) - - // 模拟API调用延迟 - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // 模拟转录结果 - const mockTranscript = ` -客服: 您好,这里是XX公司客服,请问有什么可以帮到您? -客户: 请问贵公司的产品有什么特点? -客服: 我们的产品主要有以下几个特点:首先,质量非常可靠;其次,价格比较有竞争力;第三,售后服务非常完善。 -客户: 那你们的价格是怎么样的? -客服: 我们有多种套餐可以选择,基础版每月只需99元,高级版每月299元,具体可以根据您的需求来选择。 -客户: 好的,我了解了,谢谢。 -客服: 不客气,如果您有兴趣,我可以添加您的微信,给您发送更详细的产品资料。 -客户: 可以的,谢谢。 -客服: 好的,稍后我会添加您为好友,再次感谢您的咨询。 -` - onTranscriptReady(mockTranscript) - - // 提取首句问题 - const questionMatch = mockTranscript.match(/客户: (.*?)\n/) - if (questionMatch && questionMatch[1]) { - onQuestionExtracted(questionMatch[1]) - } else { - onQuestionExtracted("未识别到有效问题") - } - - setIsProcessing(false) - } catch (err) { - setError("处理音频时出错") - setIsProcessing(false) - toast({ - title: "处理失败", - description: "语音转文字处理失败,请重试", - variant: "destructive", - }) - } - } - - processAudio() - }, [audioUrl, enabled, onTranscriptReady, onQuestionExtracted]) - - return null // 这是一个功能性组件,不渲染任何UI -} - diff --git a/Cunkebao/app/components/TrafficTeamSettings.tsx b/Cunkebao/app/components/TrafficTeamSettings.tsx deleted file mode 100644 index a26c32612..000000000 --- a/Cunkebao/app/components/TrafficTeamSettings.tsx +++ /dev/null @@ -1,177 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" -import { Plus, Pencil, Trash2 } from "lucide-react" - -interface TrafficTeam { - id: string - name: string - commission: number -} - -interface TrafficTeamSettingsProps { - formData?: any - onChange: (data: any) => void -} - -export function TrafficTeamSettings({ formData = {}, onChange }: TrafficTeamSettingsProps) { - // Initialize teams with an empty array if formData.trafficTeams is undefined - const [teams, setTeams] = useState([]) - const [isAddTeamOpen, setIsAddTeamOpen] = useState(false) - const [editingTeam, setEditingTeam] = useState(null) - const [newTeam, setNewTeam] = useState>({ - name: "", - commission: 0, - }) - - // Initialize teams state safely - useEffect(() => { - if (formData && Array.isArray(formData.trafficTeams)) { - setTeams(formData.trafficTeams) - } else { - // If formData.trafficTeams is undefined or not an array, initialize with empty array - // Also update the parent formData to include the empty trafficTeams array - setTeams([]) - onChange({ ...formData, trafficTeams: [] }) - } - }, [formData]) - - const handleAddTeam = () => { - if (!newTeam.name) return - - const updatedTeams = [...teams] - - if (editingTeam) { - const index = updatedTeams.findIndex((team) => team.id === editingTeam.id) - if (index !== -1) { - updatedTeams[index] = { - ...updatedTeams[index], - name: newTeam.name || updatedTeams[index].name, - commission: newTeam.commission !== undefined ? newTeam.commission : updatedTeams[index].commission, - } - } - } else { - updatedTeams.push({ - id: Date.now().toString(), - name: newTeam.name, - commission: newTeam.commission || 0, - }) - } - - setTeams(updatedTeams) - setIsAddTeamOpen(false) - setNewTeam({ name: "", commission: 0 }) - setEditingTeam(null) - - // Ensure we're creating a new object for formData to trigger proper updates - const updatedFormData = { ...(formData || {}), trafficTeams: updatedTeams } - onChange(updatedFormData) - } - - const handleEditTeam = (team: TrafficTeam) => { - setEditingTeam(team) - setNewTeam(team) - setIsAddTeamOpen(true) - } - - const handleDeleteTeam = (teamId: string) => { - const updatedTeams = teams.filter((team) => team.id !== teamId) - setTeams(updatedTeams) - - // Ensure we're creating a new object for formData to trigger proper updates - const updatedFormData = { ...(formData || {}), trafficTeams: updatedTeams } - onChange(updatedFormData) - } - - return ( - -
-
-

打粉团队设置

- -
- -
- - - - 团队名称 - 佣金比例 - 操作 - - - - {teams.length === 0 ? ( - - - 暂无数据 - - - ) : ( - teams.map((team) => ( - - {team.name} - {team.commission}% - -
- - -
-
-
- )) - )} -
-
-
-
- - - - - {editingTeam ? "编辑团队" : "添加团队"} - -
-
- - setNewTeam({ ...newTeam, name: e.target.value })} - placeholder="请输入团队名称" - /> -
-
- - setNewTeam({ ...newTeam, commission: Number(e.target.value) })} - placeholder="请输入佣金比例" - /> -
-
- - - - -
-
-
- ) -} - diff --git a/Cunkebao/app/components/VoiceRecognition.tsx b/Cunkebao/app/components/VoiceRecognition.tsx deleted file mode 100644 index 013a17eef..000000000 --- a/Cunkebao/app/components/VoiceRecognition.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" - -interface VoiceRecognitionProps { - onResult: (text: string) => void - onStop: () => void -} - -export function VoiceRecognition({ onResult, onStop }: VoiceRecognitionProps) { - const [isListening, setIsListening] = useState(true) - - useEffect(() => { - // 模拟语音识别 - const timer = setTimeout(() => { - const mockResults = [ - "你好,我想了解一下私域运营的基本策略", - "请帮我分析一下最近的销售数据", - "我需要一份客户画像分析报告", - "如何提高朋友圈内容的互动率?", - "帮我生成一个营销方案", - ] - - const randomResult = mockResults[Math.floor(Math.random() * mockResults.length)] - onResult(randomResult) - setIsListening(false) - }, 2000) - - return () => { - clearTimeout(timer) - } - }, [onResult]) - - useEffect(() => { - if (!isListening) { - onStop() - } - }, [isListening, onStop]) - - return ( -
-
-
-
-
-
-
-
-
-

正在聆听...

-

请说出您的问题或指令,语音识别将自动结束

- -
-
-
- ) -} - diff --git a/Cunkebao/app/components/acquisition/AcquisitionPlanChart.tsx b/Cunkebao/app/components/acquisition/AcquisitionPlanChart.tsx deleted file mode 100644 index b354b8fb7..000000000 --- a/Cunkebao/app/components/acquisition/AcquisitionPlanChart.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Area, AreaChart, Legend } from "recharts" - -interface AcquisitionPlanChartProps { - data: { date: string; customers: number }[] -} - -export function AcquisitionPlanChart({ data }: AcquisitionPlanChartProps) { - const [chartData, setChartData] = useState([]) - - // 生成更真实的数据 - useEffect(() => { - if (!data || data.length === 0) return - - // 使用获客计划中的获客数和添加数作为指标,模拟近7天数据 - const enhancedData = data.map((item) => { - // 添加数通常是获客数的一定比例,这里使用70%-90%的随机比例 - const addRate = 0.7 + Math.random() * 0.2 - const addedCount = Math.round(item.customers * addRate) - - return { - date: item.date, - 获客数: item.customers, - 添加数: addedCount, - } - }) - - setChartData(enhancedData) - }, [data]) - - // 如果没有数据,显示空状态 - if (!data || data.length === 0 || chartData.length === 0) { - return
暂无数据
- } - - return ( -
- - - - - - - - - - - - - - - - - {value}} - /> - - - - -
- ) -} - diff --git a/Cunkebao/app/components/acquisition/DailyAcquisitionChart.tsx b/Cunkebao/app/components/acquisition/DailyAcquisitionChart.tsx deleted file mode 100644 index f1e1d26f2..000000000 --- a/Cunkebao/app/components/acquisition/DailyAcquisitionChart.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client" - -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" - -interface DailyAcquisitionData { - date: string - acquired: number - added: number -} - -interface DailyAcquisitionChartProps { - data: DailyAcquisitionData[] - height?: number -} - -export function DailyAcquisitionChart({ data, height = 200 }: DailyAcquisitionChartProps) { - return ( -
- - - - - - - - - - - -
- ) -} - diff --git a/Cunkebao/app/components/acquisition/DeviceTreeChart.tsx b/Cunkebao/app/components/acquisition/DeviceTreeChart.tsx deleted file mode 100644 index daf48e40f..000000000 --- a/Cunkebao/app/components/acquisition/DeviceTreeChart.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts" - -// 模拟数据 -const weeklyData = [ - { day: "周一", 获客数: 12, 添加好友: 8 }, - { day: "周二", 获客数: 18, 添加好友: 12 }, - { day: "周三", 获客数: 15, 添加好友: 10 }, - { day: "周四", 获客数: 25, 添加好友: 18 }, - { day: "周五", 获客数: 30, 添加好友: 22 }, - { day: "周六", 获客数: 18, 添加好友: 14 }, - { day: "周日", 获客数: 15, 添加好友: 11 }, -] - -const monthlyData = [ - { day: "1月", 获客数: 120, 添加好友: 85 }, - { day: "2月", 获客数: 180, 添加好友: 130 }, - { day: "3月", 获客数: 150, 添加好友: 110 }, - { day: "4月", 获客数: 250, 添加好友: 180 }, - { day: "5月", 获客数: 300, 添加好友: 220 }, - { day: "6月", 获客数: 280, 添加好友: 210 }, -] - -export function DeviceTreeChart() { - const [period, setPeriod] = useState("week") - - const data = period === "week" ? weeklyData : monthlyData - - return ( - - -
- 获客趋势 - - - 本周 - 本月 - - -
-
- - - - - - - - - - - - -
- ) -} - diff --git a/Cunkebao/app/components/acquisition/ExpandableAcquisitionCard.tsx b/Cunkebao/app/components/acquisition/ExpandableAcquisitionCard.tsx deleted file mode 100644 index 9764ee9aa..000000000 --- a/Cunkebao/app/components/acquisition/ExpandableAcquisitionCard.tsx +++ /dev/null @@ -1,205 +0,0 @@ -"use client" - -import { useState } from "react" -import { ChevronDown, ChevronUp, MoreVertical, Copy, Pencil, Trash2, Clock, Link } from "lucide-react" -import { Button } from "@/components/ui/button" -import { useRouter } from "next/navigation" -import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts" -import { Card } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { DailyAcquisitionChart } from "./DailyAcquisitionChart" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" - -interface Task { - id: string - name: string - status: "running" | "paused" | "completed" - stats: { - devices: number - acquired: number - added: number - } - lastUpdated: string - executionTime: string - nextExecutionTime: string - trend: { date: string; customers: number }[] - dailyData?: { date: string; acquired: number; added: number }[] -} - -interface ExpandableAcquisitionCardProps { - task: Task - channel: string - onCopy: (taskId: string) => void - onDelete: (taskId: string) => void - onOpenSettings?: (taskId: string) => void - onStatusChange?: (taskId: string, newStatus: "running" | "paused") => void -} - -export function ExpandableAcquisitionCard({ - task, - channel, - onCopy, - onDelete, - onOpenSettings, - onStatusChange, -}: ExpandableAcquisitionCardProps) { - const router = useRouter() - const [expanded, setExpanded] = useState(false) - - const { devices: deviceCount, acquired: acquiredCount, added: addedCount } = task.stats - const passRate = calculatePassRate(acquiredCount, addedCount) - - const handleEdit = (taskId: string) => { - router.push(`/scenarios/${channel}/edit/${taskId}`) - } - - const toggleTaskStatus = () => { - if (onStatusChange) { - onStatusChange(task.id, task.status === "running" ? "paused" : "running") - } - } - - return ( -
- -
-
-

{task.name}

- - {task.status === "running" ? "进行中" : "已暂停"} - -
-
- - - - - - handleEdit(task.id)}> - - 编辑计划 - - onCopy(task.id)}> - - 复制计划 - - {onOpenSettings && ( - onOpenSettings(task.id)}> - - 计划接口 - - )} - onDelete(task.id)} className="text-red-600"> - - 删除计划 - - - -
-
- - - -
- - - - - - - - - -
- -
-
- - 上次执行:{task.lastUpdated} -
-
- - 下次执行:{task.nextExecutionTime} -
-
-
- - {expanded && task.dailyData && ( -
-

每日获客数据

-
- -
-
- )} - -
- -
-
- ) -} - -// 计算通过率 -function calculatePassRate(acquired: number, added: number) { - if (acquired === 0) return 0 - return Math.round((added / acquired) * 100) -} - diff --git a/Cunkebao/app/components/acquisition/NewAcquisitionPlanForm.tsx b/Cunkebao/app/components/acquisition/NewAcquisitionPlanForm.tsx deleted file mode 100644 index cf91166d2..000000000 --- a/Cunkebao/app/components/acquisition/NewAcquisitionPlanForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client" - -import type React from "react" - -import { useState } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { TrafficTeamSettings } from "@/app/components/TrafficTeamSettings" - -interface NewAcquisitionPlanFormProps { - onSubmit: (data: any) => void - onCancel: () => void -} - -export function NewAcquisitionPlanForm({ onSubmit, onCancel }: NewAcquisitionPlanFormProps) { - const [formData, setFormData] = useState({ - name: "", - description: "", - trafficTeams: [], // Initialize trafficTeams as an empty array - }) - - const [currentStep, setCurrentStep] = useState(1) - - const handleChange = (data: any) => { - setFormData(data) - } - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - onSubmit(formData) - } - - return ( -
-
- {/* 步骤指示器 */} -
- {[1, 2, 3, 4].map((step) => ( -
= step ? "text-primary" : "text-gray-400"}`} - > -
= step ? "bg-primary text-white" : "bg-gray-100 text-gray-400" - }`} - > - {step} -
-
- {step === 1 && "基础设置"} - {step === 2 && "好友设置"} - {step === 3 && "消息设置"} - {step === 4 && "流量标签"} -
-
- ))} -
-
- - -
-

基本信息

-
- - setFormData({ ...formData, name: e.target.value })} - required - /> -
-
- - setFormData({ ...formData, description: e.target.value })} - /> -
-
-
- - - -
- - -
- - ) -} - diff --git a/Cunkebao/app/components/auth-check.tsx b/Cunkebao/app/components/auth-check.tsx deleted file mode 100644 index 114002b90..000000000 --- a/Cunkebao/app/components/auth-check.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { useRouter, usePathname } from "next/navigation" - -export function AuthCheck({ children }: { children: React.ReactNode }) { - const router = useRouter() - const pathname = usePathname() - - useEffect(() => { - // 排除不需要登录的页面 - const publicPaths = ['/login', '/register', '/forgot-password'] - if (publicPaths.includes(pathname)) { - return - } - - const token = localStorage.getItem('token') - if (!token) { - // 如果没有token,重定向到登录页面,并携带当前页面URL作为回调 - const currentPath = window.location.pathname + window.location.search - router.push(`/login?redirect=${encodeURIComponent(currentPath)}`) - return - } - }, [router, pathname]) - - return <>{children} -} \ No newline at end of file diff --git a/Cunkebao/app/components/device-grid.tsx b/Cunkebao/app/components/device-grid.tsx deleted file mode 100644 index 097b2dce0..000000000 --- a/Cunkebao/app/components/device-grid.tsx +++ /dev/null @@ -1,341 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Card } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Checkbox } from "@/components/ui/checkbox" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Battery, Smartphone, MessageCircle, Users, Clock, Search, Power, RefreshCcw, Settings, AlertTriangle } from "lucide-react" -import { ImeiDisplay } from "@/components/ImeiDisplay" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" - -export interface Device { - id: string - imei: string - name: string - status: "online" | "offline" - battery: number - wechatId: string - friendCount: number - todayAdded: number - messageCount: number - lastActive: string - addFriendStatus: "normal" | "abnormal" -} - -interface DeviceGridProps { - devices: Device[] - selectable?: boolean - selectedDevices?: string[] - onSelect?: (deviceIds: string[]) => void - itemsPerRow?: number -} - -export function DeviceGrid({ - devices, - selectable = false, - selectedDevices = [], - onSelect, - itemsPerRow = 2, -}: DeviceGridProps) { - const [selectedDevice, setSelectedDevice] = useState(null) - const [searchTerm, setSearchTerm] = useState("") - const [filteredDevices, setFilteredDevices] = useState(devices) - - useEffect(() => { - const filtered = devices.filter( - (device) => - device.name.toLowerCase().includes(searchTerm.toLowerCase()) || - device.imei.includes(searchTerm) || - device.wechatId.toLowerCase().includes(searchTerm.toLowerCase()) - ) - setFilteredDevices(filtered) - }, [searchTerm, devices]) - - const handleSelectAll = () => { - if (selectedDevices.length === filteredDevices.length) { - onSelect?.([]) - } else { - onSelect?.(filteredDevices.map((d) => d.id)) - } - } - - return ( -
-
-
- - setSearchTerm(e.target.value)} - /> -
- {selectable && ( -
-
- 0} - onCheckedChange={handleSelectAll} - /> - 全选 -
- 已选择 {selectedDevices.length} 个设备 -
- )} -
- -
- {filteredDevices.map((device) => ( - { - if (selectable) { - const newSelection = selectedDevices.includes(device.id) - ? selectedDevices.filter((id) => id !== device.id) - : [...selectedDevices, device.id] - onSelect?.(newSelection) - } else { - setSelectedDevice(device) - } - }} - > -
- {selectable && ( - e.stopPropagation()} - /> - )} -
-
-
-
- {device.name} - {device.addFriendStatus === "abnormal" && ( - - 加友异常 - - )} -
-
- -
-
- {device.status === "online" ? "在线" : "离线"} -
- -
-
-
- -
-
- - {device.battery}% -
-
- - {device.friendCount} -
-
- - {device.messageCount} -
-
- - +{device.todayAdded} -
-
- -
-
- IMEI: - -
-
- 微信号: - {device.wechatId} -
-
-
-
- - ))} -
- - setSelectedDevice(null)}> - - - 设备详情 - - {selectedDevice && ( -
-
-
-
- -
-
-

- {selectedDevice.name} - -
-
- {selectedDevice.status === "online" ? "在线" : "离线"} -
- -

-
- IMEI: - 微信号: {selectedDevice.wechatId} -
-
-
-
- - - -
-
- - - - 状态信息 - 统计数据 - 任务管理 - 运行日志 - - -
-
-
电池电量
-
- - {selectedDevice.battery}% -
-
-
-
好友数量
-
- - {selectedDevice.friendCount} -
-
-
-
今日新增
-
- - +{selectedDevice.todayAdded} -
-
-
-
消息数量
-
- - {selectedDevice.messageCount} -
-
-
- - {selectedDevice.addFriendStatus === "abnormal" && ( -
-
- - 加友异常警告 -
-

- 该设备当前存在加友异常情况,请检查设备状态和相关配置。 -

-
- )} -
- -
- 统计数据开发中... -
-
- -
- 任务管理开发中... -
-
- -
- 运行日志开发中... -
-
-
-
- )} -
-
-
- ) -} - diff --git a/Cunkebao/app/components/device-selection-dialog.tsx b/Cunkebao/app/components/device-selection-dialog.tsx deleted file mode 100644 index db65cd37d..000000000 --- a/Cunkebao/app/components/device-selection-dialog.tsx +++ /dev/null @@ -1,206 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Search, RefreshCw, Filter } from "lucide-react" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Checkbox } from "@/components/ui/checkbox" -import { api } from "@/lib/api" - -interface Device { - id: string - name: string - imei: string - status: "online" | "offline" - wechatAccounts: { - wechatId: string - nickname: string - remainingAdds: number - maxDailyAdds: number - }[] -} - -interface DeviceSelectionDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - selectedDevices: string[] - onSelect: (deviceIds: string[]) => void -} - -export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) { - const [devices, setDevices] = useState([]) - const [loading, setLoading] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - const [statusFilter, setStatusFilter] = useState("all") - const [selectedDeviceIds, setSelectedDeviceIds] = useState([]) - - useEffect(() => { - if (open) setSelectedDeviceIds(selectedDevices) - }, [open, selectedDevices]) - - useEffect(() => { - if (!open) return - const fetchDevices = async () => { - setLoading(true) - try { - const params = [] - if (searchQuery) params.push(`keyword=${encodeURIComponent(searchQuery)}`) - if (statusFilter !== "all") params.push(`status=${statusFilter}`) - params.push("page=1", "limit=100") - const url = `/v1/devices?${params.join("&")}` - const response = await api.get(url) - const list = response.data?.list || response.data?.items || [] - const devices = list.map((device: any) => ({ - id: device.id?.toString() || device.id, - imei: device.imei || "", - name: device.memo || device.name || `设备_${device.id}`, - status: device.alive === 1 || device.status === "online" ? "online" : "offline", - wechatAccounts: [ - { - wechatId: device.wechatId || device.wxid || "", - nickname: device.nickname || "", - remainingAdds: device.remainingAdds || 0, - maxDailyAdds: device.maxDailyAdds || 0, - }, - ], - })) - setDevices(devices) - } catch { - setDevices([]) - } finally { - setLoading(false) - } - } - fetchDevices() - }, [open, searchQuery, statusFilter]) - - const handleSelectDevice = (deviceId: string) => { - setSelectedDeviceIds((prev) => - prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId] - ) - } - - const handleSelectAll = () => { - if (selectedDeviceIds.length === filteredDevices.length) { - setSelectedDeviceIds([]) - } else { - setSelectedDeviceIds(filteredDevices.map((device) => device.id)) - } - } - - const handleConfirm = () => { - onSelect(selectedDeviceIds) - onOpenChange(false) - } - - const handleCancel = () => { - setSelectedDeviceIds(selectedDevices) - onOpenChange(false) - } - - const filteredDevices = devices.filter((device) => { - const searchLower = searchQuery.toLowerCase() - const matchesSearch = - (device.name || '').toLowerCase().includes(searchLower) || - (device.imei || '').toLowerCase().includes(searchLower) || - (device.wechatAccounts[0]?.wechatId || '').toLowerCase().includes(searchLower) - const matchesStatus = - statusFilter === "all" || - (statusFilter === "online" && device.status === "online") || - (statusFilter === "offline" && device.status === "offline") - return matchesSearch && matchesStatus - }) - - return ( - - - - 选择设备 - -
- {/* 搜索和筛选 */} -
- setSearchQuery(e.target.value)} - className="flex-1 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100" - /> - -
- {/* 设备列表 */} -
- {loading ? ( -
加载中...
- ) : filteredDevices.length === 0 ? ( -
暂无设备
- ) : ( - filteredDevices.map(device => { - const checked = selectedDeviceIds.includes(device.id) - const wx = device.wechatAccounts[0] || {} - return ( - - ) - }) - )} -
- {/* 确认按钮 */} -
- -
-
-
-
- ) -} - diff --git a/Cunkebao/app/components/poster-selector.tsx b/Cunkebao/app/components/poster-selector.tsx deleted file mode 100644 index a95849700..000000000 --- a/Cunkebao/app/components/poster-selector.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client" - -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Check } from "lucide-react" - -interface PosterTemplate { - id: string - title: string - type: "领取" | "了解" - imageUrl: string -} - -interface PosterSelectorProps { - open: boolean - onOpenChange: (open: boolean) => void - onSelect: (template: PosterTemplate) => void -} - -const templates: PosterTemplate[] = [ - { - id: "1", - title: "点击领取", - type: "领取", - imageUrl: "/placeholder.svg?height=400&width=300", - }, - { - id: "2", - title: "点击了解", - type: "了解", - imageUrl: "/placeholder.svg?height=400&width=300", - }, - // ... 其他模板 -] - -export function PosterSelector({ open, onOpenChange, onSelect }: PosterSelectorProps) { - return ( - - - - 选择海报 - - -
-

点击下方海报使用该模板

-
- {templates.map((template) => ( -
{ - onSelect(template) - onOpenChange(false) - }} - > -
- {template.title} -
-
- -
-
-
{template.title}
-
{template.type}类型
-
-
- ))} -
-
- -
- - -
-
-
- ) -} - diff --git a/Cunkebao/app/components/traffic-pool-selector.tsx b/Cunkebao/app/components/traffic-pool-selector.tsx deleted file mode 100644 index 5b63e7a68..000000000 --- a/Cunkebao/app/components/traffic-pool-selector.tsx +++ /dev/null @@ -1,324 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Search, Filter } from "lucide-react" -import { Checkbox } from "@/components/ui/checkbox" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface UserTag { - id: string - name: string - color: string -} - -interface TrafficUser { - id: string - avatar: string - nickname: string - wechatId: string - phone: string - region: string - note: string - status: "pending" | "added" | "failed" - addTime: string - source: string - assignedTo: string - category: "potential" | "customer" | "lost" - tags: UserTag[] -} - -interface TrafficPoolSelectorProps { - open: boolean - onOpenChange: (open: boolean) => void - selectedUsers: TrafficUser[] - onSelect: (users: TrafficUser[]) => void -} - -export function TrafficPoolSelector({ open, onOpenChange, selectedUsers, onSelect }: TrafficPoolSelectorProps) { - const [users, setUsers] = useState([]) - const [loading, setLoading] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - const [activeCategory, setActiveCategory] = useState("all") - const [sourceFilter, setSourceFilter] = useState("all") - const [tagFilter, setTagFilter] = useState("all") - const [selectedUserIds, setSelectedUserIds] = useState([]) - - // 初始化已选用户 - useEffect(() => { - if (open) { - setSelectedUserIds(selectedUsers.map((user) => user.id)) - } - }, [open, selectedUsers]) - - // 模拟获取用户数据 - useEffect(() => { - if (!open) return - - const fetchUsers = async () => { - setLoading(true) - try { - // 模拟API请求 - await new Promise((resolve) => setTimeout(resolve, 800)) - - // 生成模拟数据 - const mockUsers: TrafficUser[] = Array.from({ length: 30 }, (_, i) => { - // 随机标签 - const tagPool = [ - { id: "tag1", name: "潜在客户", color: "bg-blue-100 text-blue-800" }, - { id: "tag2", name: "高意向", color: "bg-green-100 text-green-800" }, - { id: "tag3", name: "已成交", color: "bg-purple-100 text-purple-800" }, - { id: "tag4", name: "需跟进", color: "bg-yellow-100 text-yellow-800" }, - { id: "tag5", name: "活跃用户", color: "bg-indigo-100 text-indigo-800" }, - { id: "tag6", name: "沉默用户", color: "bg-gray-100 text-gray-800" }, - { id: "tag7", name: "企业客户", color: "bg-red-100 text-red-800" }, - { id: "tag8", name: "个人用户", color: "bg-pink-100 text-pink-800" }, - ] - - const randomTags = Array.from( - { length: Math.floor(Math.random() * 3) + 1 }, - () => tagPool[Math.floor(Math.random() * tagPool.length)], - ) - - // 确保标签唯一 - const uniqueTags = randomTags.filter((tag, index, self) => index === self.findIndex((t) => t.id === tag.id)) - - const sources = ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号"] - const statuses = ["pending", "added", "failed"] as const - - return { - id: `user-${i + 1}`, - avatar: `/placeholder.svg?height=40&width=40`, - nickname: `用户${i + 1}`, - wechatId: `wxid_${Math.random().toString(36).substring(2, 10)}`, - phone: `1${Math.floor(Math.random() * 9 + 1)}${Array(9) - .fill(0) - .map(() => Math.floor(Math.random() * 10)) - .join("")}`, - region: ["北京", "上海", "广州", "深圳", "杭州"][Math.floor(Math.random() * 5)], - note: Math.random() > 0.7 ? `这是用户${i + 1}的备注信息` : "", - status: statuses[Math.floor(Math.random() * 3)], - addTime: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000).toISOString(), - source: sources[Math.floor(Math.random() * sources.length)], - assignedTo: Math.random() > 0.5 ? `销售${Math.floor(Math.random() * 5) + 1}` : "", - category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as - | "potential" - | "customer" - | "lost", - tags: uniqueTags, - } - }) - - setUsers(mockUsers) - } catch (error) { - console.error("Failed to fetch users:", error) - } finally { - setLoading(false) - } - } - - fetchUsers() - }, [open]) - - // 过滤用户 - const filteredUsers = users.filter((user) => { - const matchesSearch = - searchQuery === "" || - user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || - user.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) || - user.phone.includes(searchQuery) - - const matchesCategory = activeCategory === "all" || user.category === activeCategory - - const matchesSource = sourceFilter === "all" || user.source === sourceFilter - - const matchesTag = tagFilter === "all" || user.tags.some((tag) => tag.id === tagFilter) - - return matchesSearch && matchesCategory && matchesSource && matchesTag - }) - - // 处理选择用户 - const handleSelectUser = (userId: string) => { - setSelectedUserIds((prev) => (prev.includes(userId) ? prev.filter((id) => id !== userId) : [...prev, userId])) - } - - // 处理全选 - const handleSelectAll = () => { - if (selectedUserIds.length === filteredUsers.length) { - setSelectedUserIds([]) - } else { - setSelectedUserIds(filteredUsers.map((user) => user.id)) - } - } - - // 处理确认选择 - const handleConfirm = () => { - const selectedUsersList = users.filter((user) => selectedUserIds.includes(user.id)) - onSelect(selectedUsersList) - onOpenChange(false) - } - - // 获取所有标签选项 - const allTags = Array.from(new Set(users.flatMap((user) => user.tags).map((tag) => JSON.stringify(tag)))).map( - (tag) => JSON.parse(tag) as UserTag, - ) - - // 获取所有来源选项 - const allSources = Array.from(new Set(users.map((user) => user.source))) - - return ( - - - - 选择流量池用户 - - -
- {/* 搜索和筛选区域 */} -
-
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
- -
- - {/* 分类标签页 */} - - - 全部 - 潜在客户 - 已转化 - 已流失 - - - - {/* 筛选器 */} -
- - - - - -
-
- - {/* 用户列表 */} - - {loading ? ( -
-
-
- ) : filteredUsers.length === 0 ? ( -
- {searchQuery || activeCategory !== "all" || sourceFilter !== "all" || tagFilter !== "all" - ? "没有符合条件的用户" - : "暂无用户数据"} -
- ) : ( -
- {filteredUsers.map((user) => ( - -
- handleSelectUser(user.id)} - id={`user-${user.id}`} - /> -
-
- -
- {user.status === "added" ? "已添加" : user.status === "pending" ? "待处理" : "已失败"} -
-
-
微信号: {user.wechatId}
-
来源: {user.source}
- - {/* 标签展示 */} -
- {user.tags.map((tag) => ( - - {tag.name} - - ))} -
-
-
-
- ))} -
- )} -
-
- - -
- 已选择 {selectedUserIds.length} 个用户 -
-
- - -
-
-
-
- ) -} - diff --git a/Cunkebao/app/components/ui-templates/card-grid.tsx b/Cunkebao/app/components/ui-templates/card-grid.tsx deleted file mode 100644 index 14453fa0f..000000000 --- a/Cunkebao/app/components/ui-templates/card-grid.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client" - -import type React from "react" -import { cn } from "@/app/lib/utils" - -interface CardGridProps { - children: React.ReactNode - className?: string - columns?: { - sm?: number - md?: number - lg?: number - xl?: number - } - gap?: "none" | "sm" | "md" | "lg" -} - -/** - * 自适应卡片网格组件 - * 根据屏幕尺寸自动调整卡片布局 - */ -export function CardGrid({ children, className, columns = { sm: 1, md: 2, lg: 3, xl: 4 }, gap = "md" }: CardGridProps) { - // 根据gap参数设置间距 - const gapClasses = { - none: "gap-0", - sm: "gap-2", - md: "gap-4", - lg: "gap-6", - } - - // 根据columns参数设置网格列数 - const getGridCols = () => { - const cols = [] - if (columns.sm) cols.push(`grid-cols-${columns.sm}`) - if (columns.md) cols.push(`md:grid-cols-${columns.md}`) - if (columns.lg) cols.push(`lg:grid-cols-${columns.lg}`) - if (columns.xl) cols.push(`xl:grid-cols-${columns.xl}`) - return cols.join(" ") - } - - return
{children}
-} diff --git a/Cunkebao/app/components/ui-templates/cards.tsx b/Cunkebao/app/components/ui-templates/cards.tsx deleted file mode 100644 index 543268a72..000000000 --- a/Cunkebao/app/components/ui-templates/cards.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client" -import { Card } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Clock, TrendingUp, Users } from "lucide-react" - -interface StatsCardProps { - title: string - value: string | number - description?: string - trend?: number - trendLabel?: string - className?: string - valueClassName?: string -} - -/** - * 统计数据卡片 - * 用于展示关键指标数据 - */ -export function StatsCard({ - title, - value, - description, - trend, - trendLabel, - className = "", - valueClassName = "text-xl font-bold text-blue-600", -}: StatsCardProps) { - return ( - -
{title}
-
{value}
- {description &&
{description}
} - {trend !== undefined && ( -
= 0 ? "text-green-600" : "text-red-600"}`}> - {trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}% {trendLabel || ""} -
- )} -
- ) -} - -interface DistributionPlanCardProps { - id: string - name: string - status: "active" | "paused" | "completed" - source: string - sourceIcon: string - targetGroups: string[] - totalUsers: number - dailyAverage: number - lastUpdated: string - createTime: string - creator: string - onView?: (id: string) => void - onEdit?: (id: string) => void - onDelete?: (id: string) => void - onToggleStatus?: (id: string, status: "active" | "paused") => void -} - -/** - * 流量分发计划卡片 - * 用于展示流量分发计划信息 - */ -export function DistributionPlanCard({ - id, - name, - status, - source, - sourceIcon, - targetGroups, - totalUsers, - dailyAverage, - lastUpdated, - createTime, - creator, - onView, - onEdit, - onDelete, - onToggleStatus, -}: DistributionPlanCardProps) { - return ( - -
-
- {sourceIcon} -

{name}

- - {status === "active" ? "进行中" : status === "completed" ? "已完成" : "已暂停"} - -
-
- {onToggleStatus && status !== "completed" && ( - - )} - {onView && ( - - )} - {onEdit && ( - - )} -
-
- -
-
-
目标人群:{targetGroups.join(", ")}
-
总流量:{totalUsers} 人
-
-
-
日均获取:{dailyAverage} 人
-
创建人:{creator}
-
-
- -
-
- - 上次更新:{lastUpdated} -
-
创建时间:{createTime}
-
-
- ) -} diff --git a/Cunkebao/app/components/ui-templates/form-layout.tsx b/Cunkebao/app/components/ui-templates/form-layout.tsx deleted file mode 100644 index 0ca625eff..000000000 --- a/Cunkebao/app/components/ui-templates/form-layout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client" - -import type React from "react" -import { cn } from "@/app/lib/utils" - -interface FormLayoutProps { - children: React.ReactNode - className?: string - layout?: "vertical" | "horizontal" | "responsive" - labelWidth?: string - gap?: "none" | "sm" | "md" | "lg" -} - -/** - * 自适应表单布局组件 - * 支持垂直、水平和响应式布局 - */ -export function FormLayout({ - children, - className, - layout = "responsive", - labelWidth = "w-32", - gap = "md", -}: FormLayoutProps) { - // 根据gap参数设置间距 - const gapClasses = { - none: "space-y-0", - sm: "space-y-2", - md: "space-y-4", - lg: "space-y-6", - } - - // 根据layout参数设置布局类 - const getLayoutClasses = () => { - switch (layout) { - case "horizontal": - return "form-horizontal" - case "vertical": - return "form-vertical" - case "responsive": - return "form-vertical md:form-horizontal" - default: - return "form-vertical" - } - } - - return ( -
- {children} -
- ) -} - -export default FormLayout - -interface FormItemProps { - children: React.ReactNode - label?: React.ReactNode - className?: string - required?: boolean - error?: string -} - -/** - * 表单项组件 - * 配合FormLayout使用 - */ -export function FormItem({ children, label, className, required, error }: FormItemProps) { - return ( -
- {label && ( -
- {label} - {required && *} -
- )} -
- {children} - {error &&
{error}
} -
-
- ) -} diff --git a/Cunkebao/app/components/ui-templates/forms.tsx b/Cunkebao/app/components/ui-templates/forms.tsx deleted file mode 100644 index 71d00342d..000000000 --- a/Cunkebao/app/components/ui-templates/forms.tsx +++ /dev/null @@ -1,273 +0,0 @@ -"use client" - -/** - * 表单组件模板 - * - * 包含项目中常用的各种表单组件 - */ - -import type React from "react" -import { Label } from "@/components/ui/label" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" -import { Slider } from "@/components/ui/slider" -import { Switch } from "@/components/ui/switch" - -interface FormFieldProps { - label: string - htmlFor: string - required?: boolean - description?: string - error?: string - children: React.ReactNode -} - -/** - * 表单字段容器 - * 用于包装表单控件 - */ -export function FormField({ label, htmlFor, required = false, description, error, children }: FormFieldProps) { - return ( -
- - {children} - {description &&

{description}

} - {error &&

{error}

} -
- ) -} - -interface TextInputFieldProps { - label: string - id: string - value: string - onChange: (value: string) => void - placeholder?: string - required?: boolean - description?: string - error?: string - type?: string -} - -/** - * 文本输入字段 - * 用于文本输入 - */ -export function TextInputField({ - label, - id, - value, - onChange, - placeholder, - required = false, - description, - error, - type = "text", -}: TextInputFieldProps) { - return ( - - onChange(e.target.value)} placeholder={placeholder} /> - - ) -} - -interface TextareaFieldProps { - label: string - id: string - value: string - onChange: (value: string) => void - placeholder?: string - required?: boolean - description?: string - error?: string -} - -/** - * 多行文本输入字段 - * 用于多行文本输入 - */ -export function TextareaField({ - label, - id, - value, - onChange, - placeholder, - required = false, - description, - error, -}: TextareaFieldProps) { - return ( - -