feat: 基础迁移完成了
This commit is contained in:
16
nkebao/craco.config.js
Normal file
16
nkebao/craco.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
alias: {
|
||||
'@': 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'),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3,24 +3,85 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "latest",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "latest",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "latest",
|
||||
"@radix-ui/react-checkbox": "latest",
|
||||
"@radix-ui/react-collapsible": "latest",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "latest",
|
||||
"@radix-ui/react-dropdown-menu": "latest",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-icons": "latest",
|
||||
"@radix-ui/react-label": "latest",
|
||||
"@radix-ui/react-menubar": "^1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "latest",
|
||||
"@radix-ui/react-progress": "latest",
|
||||
"@radix-ui/react-radio-group": "latest",
|
||||
"@radix-ui/react-scroll-area": "latest",
|
||||
"@radix-ui/react-select": "latest",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "latest",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "latest",
|
||||
"@radix-ui/react-tabs": "latest",
|
||||
"@radix-ui/react-toast": "latest",
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "latest",
|
||||
"@tanstack/react-table": "latest",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.6.0",
|
||||
"chart.js": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "latest",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "latest",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.54.1",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
"recharts": "latest",
|
||||
"regenerator-runtime": "latest",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.6",
|
||||
"web-vitals": "^2.1.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -1,23 +1,87 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './App.css';
|
||||
import { fetchDeviceList } from '@/api/devices';
|
||||
import { fetchWechatAccountList } from '@/api/wechat-accounts';
|
||||
import { fetchScenes } from '@/api/scenarios';
|
||||
import TestComponent from '@/components/TestComponent';
|
||||
import type { Device } from '@/types/device';
|
||||
import type { WechatFriend } from '@/types/wechat';
|
||||
|
||||
function App() {
|
||||
const [devices, setDevices] = useState<any[]>([]);
|
||||
const [wechatAccounts, setWechatAccounts] = useState<any[]>([]);
|
||||
const [scenes, setScenes] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const testAPIs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 测试设备API
|
||||
const deviceResponse = await fetchDeviceList(1, 5);
|
||||
console.log('设备API响应:', deviceResponse);
|
||||
setDevices(deviceResponse.data?.list || []);
|
||||
|
||||
// 测试微信账号API
|
||||
const wechatResponse = await fetchWechatAccountList({ page: 1, limit: 5 });
|
||||
console.log('微信账号API响应:', wechatResponse);
|
||||
setWechatAccounts(wechatResponse.data?.list || []);
|
||||
|
||||
// 测试场景API
|
||||
const sceneResponse = await fetchScenes({ page: 1, limit: 5 });
|
||||
console.log('场景API响应:', sceneResponse);
|
||||
setScenes(sceneResponse.data || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('API测试失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 页面加载时自动测试API
|
||||
testAPIs();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
<h1>nkebao2 - 基础文件迁移测试</h1>
|
||||
<p>这是一个测试页面,用于验证从Cunkebao迁移过来的基础文件是否正常工作</p>
|
||||
|
||||
<TestComponent
|
||||
title="路径别名测试"
|
||||
className="mb-4 bg-blue-50"
|
||||
/>
|
||||
|
||||
<button onClick={testAPIs} disabled={loading}>
|
||||
{loading ? '测试中...' : '重新测试API'}
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: '20px', textAlign: 'left', maxWidth: '800px' }}>
|
||||
<h3>测试结果:</h3>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>设备API (共 {devices.length} 条):</h4>
|
||||
<pre style={{ fontSize: '12px', background: '#f5f5f5', padding: '10px', borderRadius: '4px' }}>
|
||||
{JSON.stringify(devices.slice(0, 2), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>微信账号API (共 {wechatAccounts.length} 条):</h4>
|
||||
<pre style={{ fontSize: '12px', background: '#f5f5f5', padding: '10px', borderRadius: '4px' }}>
|
||||
{JSON.stringify(wechatAccounts.slice(0, 2), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>场景API (共 {scenes.length} 条):</h4>
|
||||
<pre style={{ fontSize: '12px', background: '#f5f5f5', padding: '10px', borderRadius: '4px' }}>
|
||||
{JSON.stringify(scenes.slice(0, 2), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
|
||||
147
nkebao/src/api/devices.ts
Normal file
147
nkebao/src/api/devices.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { get, post, put, del } from './request';
|
||||
import type { ApiResponse, PaginatedResponse } from '@/types/common';
|
||||
import type {
|
||||
Device,
|
||||
DeviceStats,
|
||||
DeviceTaskRecord,
|
||||
QueryDeviceParams,
|
||||
CreateDeviceParams,
|
||||
UpdateDeviceParams,
|
||||
DeviceStatus,
|
||||
ServerDevice,
|
||||
ServerDevicesResponse
|
||||
} from '@/types/device';
|
||||
|
||||
const API_BASE = "/devices";
|
||||
|
||||
// 获取设备列表 - 连接到服务器/v1/devices接口
|
||||
export const fetchDeviceList = async (page: number = 1, limit: number = 20, keyword?: string): Promise<ServerDevicesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 获取设备详情 - 连接到服务器/v1/devices/:id接口
|
||||
export const fetchDeviceDetail = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
};
|
||||
|
||||
// 获取设备关联的微信账号
|
||||
export const fetchDeviceRelatedAccounts = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/v1/wechats/related-device/${id}`);
|
||||
};
|
||||
|
||||
// 获取设备操作记录
|
||||
export const fetchDeviceHandleLogs = async (id: string | number, page: number = 1, limit: number = 10): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/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<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>(`/v1/devices/task-config`, config);
|
||||
};
|
||||
|
||||
// 删除设备
|
||||
export const deleteDevice = async (id: number): Promise<ApiResponse<any>> => {
|
||||
return del<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
};
|
||||
|
||||
// 设备管理API
|
||||
export const deviceApi = {
|
||||
// 创建设备
|
||||
async create(params: CreateDeviceParams): Promise<ApiResponse<Device>> {
|
||||
return post<ApiResponse<Device>>(`${API_BASE}`, params);
|
||||
},
|
||||
|
||||
// 更新设备
|
||||
async update(params: UpdateDeviceParams): Promise<ApiResponse<Device>> {
|
||||
return put<ApiResponse<Device>>(`${API_BASE}/${params.id}`, params);
|
||||
},
|
||||
|
||||
// 获取设备详情
|
||||
async getById(id: string): Promise<ApiResponse<Device>> {
|
||||
return get<ApiResponse<Device>>(`${API_BASE}/${id}`);
|
||||
},
|
||||
|
||||
// 查询设备列表
|
||||
async query(params: QueryDeviceParams): Promise<ApiResponse<PaginatedResponse<Device>>> {
|
||||
// 创建一个新对象,用于构建URLSearchParams
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 按需将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();
|
||||
return get<ApiResponse<PaginatedResponse<Device>>>(`${API_BASE}?${queryString}`);
|
||||
},
|
||||
|
||||
// 删除设备
|
||||
async delete(id: string): Promise<ApiResponse<void>> {
|
||||
return del<ApiResponse<void>>(`${API_BASE}/${id}`);
|
||||
},
|
||||
|
||||
// 重启设备
|
||||
async restart(id: string): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/${id}/restart`);
|
||||
},
|
||||
|
||||
// 解绑设备
|
||||
async unbind(id: string): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/${id}/unbind`);
|
||||
},
|
||||
|
||||
// 获取设备统计数据
|
||||
async getStats(id: string): Promise<ApiResponse<DeviceStats>> {
|
||||
return get<ApiResponse<DeviceStats>>(`${API_BASE}/${id}/stats`);
|
||||
},
|
||||
|
||||
// 获取设备任务记录
|
||||
async getTaskRecords(id: string, page = 1, pageSize = 20): Promise<ApiResponse<PaginatedResponse<DeviceTaskRecord>>> {
|
||||
return get<ApiResponse<PaginatedResponse<DeviceTaskRecord>>>(`${API_BASE}/${id}/tasks?page=${page}&pageSize=${pageSize}`);
|
||||
},
|
||||
|
||||
// 批量更新设备标签
|
||||
async updateTags(ids: string[], tags: string[]): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/tags`, { deviceIds: ids, tags });
|
||||
},
|
||||
|
||||
// 批量导出设备数据
|
||||
async exportDevices(ids: string[]): Promise<Blob> {
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE || 'http://localhost:3000/api'}${API_BASE}/export`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ deviceIds: ids }),
|
||||
});
|
||||
return response.blob();
|
||||
},
|
||||
|
||||
// 检查设备在线状态
|
||||
async checkStatus(ids: string[]): Promise<ApiResponse<Record<string, DeviceStatus>>> {
|
||||
return post<ApiResponse<Record<string, DeviceStatus>>>(`${API_BASE}/status`, { deviceIds: ids });
|
||||
},
|
||||
};
|
||||
61
nkebao/src/api/request.ts
Normal file
61
nkebao/src/api/request.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_BASE || 'http://localhost:3000/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加token等认证信息
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
// 统一错误处理
|
||||
console.error('API请求错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 封装GET请求
|
||||
export const get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.get(url, config);
|
||||
};
|
||||
|
||||
// 封装POST请求
|
||||
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.post(url, data, config);
|
||||
};
|
||||
|
||||
// 封装PUT请求
|
||||
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.put(url, data, config);
|
||||
};
|
||||
|
||||
// 封装DELETE请求
|
||||
export const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.delete(url, config);
|
||||
};
|
||||
|
||||
// 导出request实例
|
||||
export { request };
|
||||
export default request;
|
||||
120
nkebao/src/api/scenarios.ts
Normal file
120
nkebao/src/api/scenarios.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { get } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 服务器返回的场景数据类型
|
||||
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<ScenesResponse> => {
|
||||
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 get<ScenesResponse>(`/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<ApiResponse<SceneItem>> => {
|
||||
try {
|
||||
return await get<ApiResponse<SceneItem>>(`/v1/plan/scenes/${id}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching scene detail:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取场景详情失败",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
};
|
||||
219
nkebao/src/api/wechat-accounts.ts
Normal file
219
nkebao/src/api/wechat-accounts.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { get, post, put } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 添加接口返回数据类型定义
|
||||
interface WechatAccountSummary {
|
||||
accountAge: string;
|
||||
activityLevel: {
|
||||
allTimes: number;
|
||||
dayTimes: number;
|
||||
};
|
||||
accountWeight: {
|
||||
scope: number;
|
||||
ageWeight: number;
|
||||
activityWeigth: number;
|
||||
restrictWeight: number;
|
||||
realNameWeight: number;
|
||||
};
|
||||
statistics: {
|
||||
todayAdded: number;
|
||||
addLimit: number;
|
||||
};
|
||||
restrictions: {
|
||||
id: number;
|
||||
level: string;
|
||||
reason: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface WechatAccountSummaryResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: WechatAccountSummary;
|
||||
}
|
||||
|
||||
interface ServerWechatAccountsResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface QueryWechatAccountParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信账号列表
|
||||
* @param params 查询参数
|
||||
* @returns 微信账号列表响应
|
||||
*/
|
||||
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ServerWechatAccountsResponse> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
// 添加查询参数
|
||||
if (params.page) queryParams.append('page', params.page.toString());
|
||||
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params.keyword) queryParams.append('nickname', params.keyword); // 使用nickname作为关键词搜索参数
|
||||
if (params.sort) queryParams.append('sort', params.sort);
|
||||
if (params.order) queryParams.append('order', params.order);
|
||||
|
||||
// 发起API请求
|
||||
return get<ServerWechatAccountsResponse>(`/v1/wechats?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新微信账号状态
|
||||
* @returns 刷新结果
|
||||
*/
|
||||
export const refreshWechatAccounts = async (): Promise<{ code: number; msg: string; data: any }> => {
|
||||
return put<{ code: number; msg: string; data: any }>('/v1/wechats/refresh', {});
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行微信好友转移
|
||||
* @param sourceId 源微信账号ID
|
||||
* @param targetId 目标微信账号ID
|
||||
* @returns 转移结果
|
||||
*/
|
||||
export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<{ code: number; msg: string; data: any }> => {
|
||||
return post<{ code: number; msg: string; data: any }>('/v1/wechats/transfer-friends', {
|
||||
source_id: sourceId,
|
||||
target_id: targetId
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 将服务器返回的微信账号数据转换为前端使用的格式
|
||||
* @param serverAccount 服务器返回的微信账号数据
|
||||
* @returns 前端使用的微信账号数据
|
||||
*/
|
||||
export const transformWechatAccount = (serverAccount: any): any => {
|
||||
// 从deviceInfo中提取设备信息
|
||||
let deviceId = '';
|
||||
let deviceName = '';
|
||||
|
||||
if (serverAccount.deviceInfo) {
|
||||
// 尝试解析设备信息字符串
|
||||
const deviceInfo = serverAccount.deviceInfo.split(' ');
|
||||
if (deviceInfo.length > 0) {
|
||||
// 提取数字部分作为设备ID,确保是整数
|
||||
const possibleId = deviceInfo[0].trim();
|
||||
// 验证是否为数字
|
||||
deviceId = /^\d+$/.test(possibleId) ? possibleId : '';
|
||||
|
||||
// 提取设备名称
|
||||
if (deviceInfo.length > 1) {
|
||||
deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '').trim() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果从deviceInfo无法获取有效的设备ID,使用imei作为备选
|
||||
if (!deviceId && serverAccount.imei) {
|
||||
deviceId = serverAccount.imei;
|
||||
}
|
||||
|
||||
// 如果仍然没有设备ID,使用微信账号的ID作为最后的备选
|
||||
if (!deviceId && serverAccount.id) {
|
||||
deviceId = serverAccount.id.toString();
|
||||
}
|
||||
|
||||
// 如果没有设备名称,使用备用名称
|
||||
if (!deviceName) {
|
||||
deviceName = serverAccount.deviceMemo || '未命名设备';
|
||||
}
|
||||
|
||||
// 假设每天最多可添加20个好友
|
||||
const maxDailyAdds = 20;
|
||||
const todayAdded = serverAccount.todayNewFriendCount || 0;
|
||||
|
||||
return {
|
||||
id: serverAccount.id.toString(),
|
||||
avatar: serverAccount.avatar || '',
|
||||
nickname: serverAccount.nickname || serverAccount.accountNickname || '未命名',
|
||||
wechatId: serverAccount.wechatId || '',
|
||||
deviceId,
|
||||
deviceName,
|
||||
friendCount: serverAccount.totalFriend || 0,
|
||||
todayAdded,
|
||||
remainingAdds: serverAccount.canAddFriendCount || (maxDailyAdds - todayAdded),
|
||||
maxDailyAdds,
|
||||
status: serverAccount.wechatAlive === 1 ? "normal" : "abnormal" as "normal" | "abnormal",
|
||||
lastActive: new Date().toLocaleString() // 服务端未提供,使用当前时间
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信好友列表
|
||||
* @param wechatId 微信账号ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param searchQuery 搜索关键词
|
||||
* @returns 好友列表数据
|
||||
*/
|
||||
export const fetchWechatFriends = async (wechatId: string, page: number = 1, pageSize: number = 20, searchQuery: string = '') => {
|
||||
try {
|
||||
return get(`/v1/wechats/${wechatId}/friends?page=${page}&limit=${pageSize}${searchQuery ? `&search=${searchQuery}` : ''}`);
|
||||
} catch (error) {
|
||||
console.error("获取好友列表失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信账号概览信息
|
||||
* @param id 微信账号ID
|
||||
* @returns 微信账号概览信息
|
||||
*/
|
||||
export const fetchWechatAccountSummary = async (wechatIdid: string): Promise<WechatAccountSummaryResponse> => {
|
||||
try {
|
||||
return get<WechatAccountSummaryResponse>(`/v1/wechats/${wechatIdid}/summary`);
|
||||
} catch (error) {
|
||||
console.error("获取账号概览失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取好友详情信息
|
||||
* @param wechatId 微信账号ID
|
||||
* @param friendId 好友ID
|
||||
* @returns 好友详情信息
|
||||
*/
|
||||
export interface WechatFriendDetail {
|
||||
id: number;
|
||||
avatar: string;
|
||||
nickname: string;
|
||||
region: string;
|
||||
wechatId: string;
|
||||
addDate: string;
|
||||
tags: string[];
|
||||
playDate: string;
|
||||
memo: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface WechatFriendDetailResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: WechatFriendDetail;
|
||||
}
|
||||
|
||||
export const fetchWechatFriendDetail = async (wechatId: string): Promise<WechatFriendDetailResponse> => {
|
||||
try {
|
||||
return get<WechatFriendDetailResponse>(`/v1/wechats/${wechatId}`);
|
||||
} catch (error) {
|
||||
console.error("获取好友详情失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
23
nkebao/src/components/TestComponent.tsx
Normal file
23
nkebao/src/components/TestComponent.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/utils';
|
||||
|
||||
interface TestComponentProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TestComponent: React.FC<TestComponentProps> = ({ title, className }) => {
|
||||
return (
|
||||
<div className={cn('p-4 border rounded-lg', className)}>
|
||||
<h3 className="text-lg font-semibold mb-2">{title}</h3>
|
||||
<p className="text-gray-600">
|
||||
这是一个测试组件,用于验证路径别名 @/ 是否正常工作。
|
||||
</p>
|
||||
<div className="mt-2 text-sm text-blue-600">
|
||||
✓ 成功使用 @/utils 导入工具函数
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestComponent;
|
||||
43
nkebao/src/types/common.ts
Normal file
43
nkebao/src/types/common.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// API响应格式
|
||||
export interface ApiResponse<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T | null
|
||||
}
|
||||
|
||||
// 分页响应格式
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
// 通用查询参数
|
||||
export interface QueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
dateRange?: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
|
||||
// 通用状态枚举
|
||||
export enum Status {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
PENDING = "pending",
|
||||
COMPLETED = "completed",
|
||||
FAILED = "failed",
|
||||
DRAFT = "draft",
|
||||
}
|
||||
|
||||
// 通用基础实体
|
||||
export interface BaseEntity {
|
||||
id: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
123
nkebao/src/types/device.ts
Normal file
123
nkebao/src/types/device.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// 设备状态枚举
|
||||
export enum DeviceStatus {
|
||||
ONLINE = "online",
|
||||
OFFLINE = "offline",
|
||||
BUSY = "busy",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
// 设备类型枚举
|
||||
export enum DeviceType {
|
||||
ANDROID = "android",
|
||||
IOS = "ios",
|
||||
}
|
||||
|
||||
// 服务端API返回的设备类型
|
||||
export interface ServerDevice {
|
||||
id: number;
|
||||
imei: string;
|
||||
memo: string;
|
||||
wechatId: string;
|
||||
alive: number;
|
||||
totalFriend: number;
|
||||
}
|
||||
|
||||
// 服务端API返回的设备列表响应
|
||||
export interface ServerDevicesResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
list: ServerDevice[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 设备基础信息
|
||||
export interface Device {
|
||||
id: string
|
||||
name: string
|
||||
imei: string
|
||||
type: DeviceType
|
||||
status: DeviceStatus
|
||||
wechatId: string
|
||||
friendCount: number
|
||||
battery: number
|
||||
lastActive: string
|
||||
addFriendStatus: "normal" | "abnormal"
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 设备统计信息
|
||||
export interface DeviceStats {
|
||||
totalTasks: number
|
||||
completedTasks: number
|
||||
failedTasks: number
|
||||
todayNewFriends: number
|
||||
totalNewFriends: number
|
||||
onlineTime: number // 在线时长(分钟)
|
||||
}
|
||||
|
||||
// 设备任务记录
|
||||
export interface DeviceTaskRecord {
|
||||
id: string
|
||||
deviceId: string
|
||||
taskType: string
|
||||
status: "pending" | "running" | "completed" | "failed"
|
||||
startTime: string
|
||||
endTime?: string
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 设备查询参数
|
||||
export interface QueryDeviceParams {
|
||||
keyword?: string
|
||||
status?: DeviceStatus
|
||||
type?: DeviceType
|
||||
tags?: string[]
|
||||
page?: number
|
||||
pageSize?: number
|
||||
dateRange?: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
|
||||
// 创建设备参数
|
||||
export interface CreateDeviceParams {
|
||||
name: string
|
||||
imei: string
|
||||
type: DeviceType
|
||||
wechatId?: string
|
||||
remark?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
// 更新设备参数
|
||||
export interface UpdateDeviceParams {
|
||||
id: string
|
||||
name?: string
|
||||
wechatId?: string
|
||||
remark?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface DeviceResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
devices: Device[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeviceSelectResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
success: boolean
|
||||
deviceIds: string[]
|
||||
}
|
||||
}
|
||||
35
nkebao/src/types/wechat.ts
Normal file
35
nkebao/src/types/wechat.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 微信好友类型定义
|
||||
export interface WechatFriend {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
avatar: string
|
||||
gender?: "male" | "female"
|
||||
customer?: string
|
||||
alias?: string
|
||||
ownerNickname?: string
|
||||
ownerAlias?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// 微信群组类型定义
|
||||
export interface WechatGroup {
|
||||
id: string
|
||||
name: string
|
||||
memberCount: number
|
||||
avatar: string
|
||||
owner: string
|
||||
customer: string
|
||||
}
|
||||
|
||||
// 微信群成员类型定义
|
||||
export interface WechatGroupMember {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
avatar: string
|
||||
gender?: "male" | "female"
|
||||
role?: "owner" | "admin" | "member"
|
||||
joinTime?: string
|
||||
groupId?: string
|
||||
}
|
||||
46
nkebao/src/utils/index.ts
Normal file
46
nkebao/src/utils/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// 通用工具函数
|
||||
export const cn = (...classes: (string | undefined | null | false)[]) => {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
export const formatDate = (date: string | Date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN');
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
export const formatTime = (date: string | Date) => {
|
||||
return new Date(date).toLocaleTimeString('zh-CN');
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
export const formatDateTime = (date: string | Date) => {
|
||||
return new Date(date).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
// 节流函数
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let inThrottle: boolean;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), wait);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -18,7 +18,18 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["*"],
|
||||
"@/components/*": ["components/*"],
|
||||
"@/api/*": ["api/*"],
|
||||
"@/types/*": ["types/*"],
|
||||
"@/hooks/*": ["hooks/*"],
|
||||
"@/utils/*": ["utils/*"],
|
||||
"@/styles/*": ["styles/*"],
|
||||
"@/pages/*": ["pages/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user