feat: 基础迁移完成了

This commit is contained in:
许永平
2025-07-03 18:09:07 +08:00
parent 47ff85bafa
commit 2564e3cfac
13 changed files with 989 additions and 20 deletions

View File

@@ -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
View 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
View 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
View 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
};
}
};

View 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;
}
};

View 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;

View 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
View 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[]
}
}

View 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
View 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);
}
};
};