From 1cb8e9f397f5e97d4f06baaae243b078ca5d5a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Mon, 31 Mar 2025 17:33:05 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/devices.ts | 16 ++ Cunkebao/app/devices/[id]/page.tsx | 163 +++++++++++++++++- Server/application/devices/config/route.php | 1 + .../application/devices/controller/Device.php | 69 ++++++++ .../devices/model/WechatAccount.php | 3 +- 5 files changed, 242 insertions(+), 10 deletions(-) diff --git a/Cunkebao/api/devices.ts b/Cunkebao/api/devices.ts index eece0035..e07f0cfc 100644 --- a/Cunkebao/api/devices.ts +++ b/Cunkebao/api/devices.ts @@ -33,6 +33,22 @@ export const fetchDeviceDetail = async (id: string | number): Promise>(`/v1/devices/${id}`); }; +// 更新设备任务配置 +export const updateDeviceTaskConfig = async ( + id: string | number, + config: { + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; + } +): Promise> => { + return api.post>(`/v1/devices/task-config`, { + id, + ...config + }); +}; + // 删除设备 export const deleteDevice = async (id: number): Promise> => { return api.delete>(`/v1/devices/${id}`); diff --git a/Cunkebao/app/devices/[id]/page.tsx b/Cunkebao/app/devices/[id]/page.tsx index cd258090..b0107eff 100644 --- a/Cunkebao/app/devices/[id]/page.tsx +++ b/Cunkebao/app/devices/[id]/page.tsx @@ -10,7 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" import { ScrollArea } from "@/components/ui/scroll-area" -import { fetchDeviceDetail } from "@/api/devices" +import { fetchDeviceDetail, updateDeviceTaskConfig } from "@/api/devices" import { toast } from "sonner" interface WechatAccount { @@ -37,7 +37,7 @@ interface Device { features: { autoAddFriend: boolean autoReply: boolean - contentSync: boolean + momentsSync: boolean aiChat: boolean } history: { @@ -68,6 +68,12 @@ export default function DeviceDetailPage() { const [device, setDevice] = useState(null) const [activeTab, setActiveTab] = useState("info") const [loading, setLoading] = useState(true) + const [savingFeatures, setSavingFeatures] = useState({ + autoAddFriend: false, + autoReply: false, + momentsSync: false, + aiChat: false + }) useEffect(() => { if (!params.id) return @@ -91,16 +97,47 @@ export default function DeviceDetailPage() { historicalIds: [], // 服务端暂无此数据 wechatAccounts: [], // 默认空数组 history: [], // 服务端暂无此数据 - features: serverData.features || { + features: { autoAddFriend: false, autoReply: false, - contentSync: false, + momentsSync: false, aiChat: false }, totalFriend: serverData.totalFriend || 0, thirtyDayMsgCount: serverData.thirtyDayMsgCount || 0 } + // 解析features + if (serverData.features) { + // 如果后端直接返回了features对象,使用它 + formattedDevice.features = { + autoAddFriend: Boolean(serverData.features.autoAddFriend), + autoReply: Boolean(serverData.features.autoReply), + momentsSync: Boolean(serverData.features.momentsSync || serverData.features.contentSync), + aiChat: Boolean(serverData.features.aiChat) + } + } else if (serverData.taskConfig) { + try { + // 解析taskConfig字段 + let taskConfig = serverData.taskConfig + if (typeof taskConfig === 'string') { + taskConfig = JSON.parse(taskConfig) + } + + if (taskConfig) { + console.log('解析的taskConfig:', taskConfig); + formattedDevice.features = { + autoAddFriend: Boolean(taskConfig.autoAddFriend), + autoReply: Boolean(taskConfig.autoReply), + momentsSync: Boolean(taskConfig.momentsSync), + aiChat: Boolean(taskConfig.aiChat) + } + } + } catch (err) { + console.error('解析taskConfig失败:', err) + } + } + // 如果有微信账号信息,构建微信账号对象 if (serverData.wechatId) { formattedDevice.wechatAccounts = [ @@ -169,7 +206,7 @@ export default function DeviceDetailPage() { features: { autoAddFriend: true, autoReply: true, - contentSync: false, + momentsSync: false, aiChat: true, }, history: [ @@ -193,6 +230,74 @@ export default function DeviceDetailPage() { fetchDevice() }, [params.id]) + // 处理功能开关状态变化 + const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => { + if (!device) return + + // 避免已经在处理中的功能被重复触发 + if (savingFeatures[feature]) { + return + } + + setSavingFeatures(prev => ({ ...prev, [feature]: true })) + + try { + // 准备更新后的功能状态 + const updatedFeatures = { ...device.features, [feature]: checked } + + // 创建API请求参数 + const configUpdate = { [feature]: checked } + + // 立即更新UI状态,提供即时反馈 + setDevice(prev => prev ? { + ...prev, + features: updatedFeatures + } : null) + + // 调用API更新服务器配置 + const response = await updateDeviceTaskConfig(device.id, configUpdate) + + if (response && response.code === 200) { + toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`) + } else { + // 如果请求失败,回滚UI变更 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null) + + // 处理错误信息,使用类型断言解决字段不一致问题 + const anyResponse = response as any; + const errorMsg = anyResponse ? (anyResponse.message || anyResponse.msg || '未知错误') : '未知错误'; + toast.error(`更新失败: ${errorMsg}`) + } + } catch (error) { + console.error(`更新${getFeatureName(feature)}失败:`, error) + + // 异常情况下也回滚UI变更 + setDevice(prev => prev ? { + ...prev, + features: { ...prev.features, [feature]: !checked } + } : null) + + toast.error('更新失败,请稍后重试') + } finally { + setSavingFeatures(prev => ({ ...prev, [feature]: false })) + } + } + + // 获取功能中文名称 + const getFeatureName = (feature: string): string => { + const nameMap: Record = { + autoAddFriend: '自动加好友', + autoReply: '自动回复', + momentsSync: '朋友圈同步', + aiChat: 'AI会话' + } + + return nameMap[feature] || feature + } + if (loading || !device) { return
加载中...
} @@ -261,28 +366,68 @@ export default function DeviceDetailPage() {
自动通过好友验证
- +
+ {savingFeatures.autoAddFriend && ( +
+ )} + handleFeatureChange('autoAddFriend', checked)} + disabled={savingFeatures.autoAddFriend} + className="data-[state=checked]:bg-blue-500 transition-all duration-200" + /> +
自动回复好友消息
- +
+ {savingFeatures.autoReply && ( +
+ )} + handleFeatureChange('autoReply', checked)} + disabled={savingFeatures.autoReply} + className="data-[state=checked]:bg-blue-500 transition-all duration-200" + /> +
自动同步朋友圈内容
- +
+ {savingFeatures.momentsSync && ( +
+ )} + handleFeatureChange('momentsSync', checked)} + disabled={savingFeatures.momentsSync} + className="data-[state=checked]:bg-blue-500 transition-all duration-200" + /> +
启用AI智能对话
- +
+ {savingFeatures.aiChat && ( +
+ )} + handleFeatureChange('aiChat', checked)} + disabled={savingFeatures.aiChat} + className="data-[state=checked]:bg-blue-500 transition-all duration-200" + /> +
diff --git a/Server/application/devices/config/route.php b/Server/application/devices/config/route.php index c7efd83f..5c92d024 100644 --- a/Server/application/devices/config/route.php +++ b/Server/application/devices/config/route.php @@ -16,6 +16,7 @@ Route::group('v1/', function () { Route::post('', 'app\\devices\\controller\\Device@save'); // 添加设备 Route::put('refresh', 'app\\devices\\controller\\Device@refresh'); // 刷新设备状态 Route::delete(':id', 'app\\devices\\controller\\Device@delete'); // 删除设备 + Route::post('task-config', 'app\\devices\\controller\\Device@updateTaskConfig'); // 更新设备任务配置 }); // 设备微信相关 diff --git a/Server/application/devices/controller/Device.php b/Server/application/devices/controller/Device.php index db574c48..24595a65 100644 --- a/Server/application/devices/controller/Device.php +++ b/Server/application/devices/controller/Device.php @@ -396,4 +396,73 @@ class Device extends Controller ]); } } + + /** + * 更新设备任务配置 + * @return \think\response\Json + */ + public function updateTaskConfig() + { + // 获取请求参数 + $data = $this->request->post(); + + // 验证参数 + if (empty($data['id'])) { + return json(['code' => 400, 'msg' => '设备ID不能为空']); + } + + // 转换为整型,确保ID格式正确 + $deviceId = intval($data['id']); + + // 先获取设备信息,确认设备存在且未删除 + $device = \app\devices\model\Device::where('id', $deviceId) + ->where('isDeleted', 0) + ->find(); + + if (!$device) { + return json(['code' => 404, 'msg' => '设备不存在或已删除']); + } + + // 读取原taskConfig,如果存在则解析 + $taskConfig = []; + if (!empty($device['taskConfig'])) { + $taskConfig = json_decode($device['taskConfig'], true) ?: []; + } + + // 更新需要修改的配置项 + $updateFields = ['autoAddFriend', 'autoReply', 'momentsSync', 'aiChat']; + $hasUpdate = false; + + foreach ($updateFields as $field) { + if (isset($data[$field])) { + // 将值转换为布尔类型存储 + $taskConfig[$field] = (bool)$data[$field]; + $hasUpdate = true; + } + } + + // 如果没有需要更新的字段,直接返回成功 + if (!$hasUpdate) { + return json(['code' => 200, 'msg' => '更新成功', 'data' => ['taskConfig' => $taskConfig]]); + } + + // 更新设备taskConfig字段 + $result = \app\devices\model\Device::where('id', $deviceId) + ->update([ + 'taskConfig' => json_encode($taskConfig), + 'updateTime' => time() + ]); + + if ($result) { + return json([ + 'code' => 200, + 'msg' => '更新任务配置成功', + 'data' => [ + 'taskConfig' => $taskConfig + ] + ]); + } else { + return json(['code' => 500, 'msg' => '更新任务配置失败']); + } + } } \ No newline at end of file diff --git a/Server/application/devices/model/WechatAccount.php b/Server/application/devices/model/WechatAccount.php index 64fc8a64..16dc8c47 100644 --- a/Server/application/devices/model/WechatAccount.php +++ b/Server/application/devices/model/WechatAccount.php @@ -38,7 +38,8 @@ class WechatAccount extends Model 'gender' => 'integer', 'currentDeviceId' => 'integer', 'isDeleted' => 'integer', - 'groupId' => 'integer' + 'groupId' => 'integer', + 'status' => 'integer' ]; /** From 6638fcb8a1598229fee95456e971854b906f344a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Tue, 1 Apr 2025 10:00:20 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=85=B3=E8=81=94?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/devices.ts | 5 + Cunkebao/app/devices/[id]/page.tsx | 165 +++++-- Cunkebao/app/devices/page.tsx | 404 +++++++++++++++++- Server/application/devices/config/route.php | 3 + .../application/devices/controller/Device.php | 89 +++- .../devices/model/DeviceWechatLogin.php | 207 +++++++++ 6 files changed, 810 insertions(+), 63 deletions(-) create mode 100644 Server/application/devices/model/DeviceWechatLogin.php diff --git a/Cunkebao/api/devices.ts b/Cunkebao/api/devices.ts index e07f0cfc..e121be43 100644 --- a/Cunkebao/api/devices.ts +++ b/Cunkebao/api/devices.ts @@ -33,6 +33,11 @@ export const fetchDeviceDetail = async (id: string | number): Promise>(`/v1/devices/${id}`); }; +// 获取设备关联的微信账号 +export const fetchDeviceRelatedAccounts = async (id: string | number): Promise> => { + return api.get>(`/v1/devices/${id}/related-accounts`); +}; + // 更新设备任务配置 export const updateDeviceTaskConfig = async ( id: string | number, diff --git a/Cunkebao/app/devices/[id]/page.tsx b/Cunkebao/app/devices/[id]/page.tsx index b0107eff..86a629e8 100644 --- a/Cunkebao/app/devices/[id]/page.tsx +++ b/Cunkebao/app/devices/[id]/page.tsx @@ -4,13 +4,13 @@ import { useState, useEffect } from "react" import { useParams, useRouter } from "next/navigation" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History } from "lucide-react" +import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" import { ScrollArea } from "@/components/ui/scroll-area" -import { fetchDeviceDetail, updateDeviceTaskConfig } from "@/api/devices" +import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig } from "@/api/devices" import { toast } from "sonner" interface WechatAccount { @@ -18,10 +18,13 @@ interface WechatAccount { avatar: string nickname: string wechatId: string - gender: "male" | "female" - status: "normal" | "abnormal" - addFriendStatus: "enabled" | "disabled" - friendCount: number + gender: number + status: number + statusText: string + wechatAlive: number + wechatAliveText: string + addFriendStatus: number + totalFriend: number lastActive: string } @@ -68,6 +71,7 @@ export default function DeviceDetailPage() { const [device, setDevice] = useState(null) const [activeTab, setActiveTab] = useState("info") const [loading, setLoading] = useState(true) + const [accountsLoading, setAccountsLoading] = useState(false) const [savingFeatures, setSavingFeatures] = useState({ autoAddFriend: false, autoReply: false, @@ -146,16 +150,24 @@ export default function DeviceDetailPage() { avatar: "/placeholder.svg", // 默认头像 nickname: serverData.memo || "微信账号", wechatId: serverData.imei || "", - gender: "male", // 默认性别 - status: serverData.alive === 1 ? "normal" : "abnormal", - addFriendStatus: "enabled", - friendCount: serverData.totalFriend || 0, + gender: 1, // 默认性别 + status: serverData.alive === 1 ? 1 : 0, + statusText: serverData.alive === 1 ? "可加友" : "已停用", + wechatAlive: serverData.alive === 1 ? 1 : 0, + wechatAliveText: serverData.alive === 1 ? "正常" : "异常", + addFriendStatus: 1, + totalFriend: serverData.totalFriend || 0, lastActive: serverData.lastUpdateTime || new Date().toISOString() } ] } setDevice(formattedDevice) + + // 如果当前激活标签是"accounts",则加载关联微信账号 + if (activeTab === "accounts") { + fetchRelatedAccounts() + } } else { // 如果API返回错误,则使用备用模拟数据 toast.error("获取设备信息失败,显示备用数据") @@ -185,10 +197,13 @@ export default function DeviceDetailPage() { avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png", nickname: "老张", wechatId: "wxid_abc123", - gender: "male", - status: "normal", - addFriendStatus: "enabled", - friendCount: 523, + gender: 1, + status: 1, + statusText: "可加友", + wechatAlive: 1, + wechatAliveText: "正常", + addFriendStatus: 1, + totalFriend: 523, lastActive: "2024-02-09 15:20:33", }, { @@ -196,10 +211,13 @@ export default function DeviceDetailPage() { avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png", nickname: "老李", wechatId: "wxid_xyz789", - gender: "male", - status: "abnormal", - addFriendStatus: "disabled", - friendCount: 245, + gender: 1, + status: 0, + statusText: "已停用", + wechatAlive: 0, + wechatAliveText: "异常", + addFriendStatus: 0, + totalFriend: 245, lastActive: "2024-02-09 14:15:22", }, ], @@ -228,7 +246,53 @@ export default function DeviceDetailPage() { } fetchDevice() - }, [params.id]) + }, [params.id, activeTab]) + + // 获取设备关联微信账号 + const fetchRelatedAccounts = async () => { + if (!params.id || accountsLoading) return + + try { + setAccountsLoading(true) + const response = await fetchDeviceRelatedAccounts(params.id as string) + + if (response && response.code === 200 && response.data) { + const accounts = response.data.accounts || [] + + // 更新设备的微信账号信息 + setDevice(prev => { + if (!prev) return null + return { + ...prev, + wechatAccounts: accounts + } + }) + + if (accounts.length > 0) { + toast.success(`成功获取${accounts.length}个关联微信账号`) + } else { + toast.info("此设备暂无关联微信账号") + } + } else { + toast.error("获取关联微信账号失败") + } + } catch (error) { + console.error("获取关联微信账号失败:", error) + toast.error("获取关联微信账号出错") + } finally { + setAccountsLoading(false) + } + } + + // 处理标签页切换 + const handleTabChange = (value: string) => { + setActiveTab(value) + + // 当切换到"关联账号"标签时,获取最新的关联微信账号信息 + if (value === "accounts") { + fetchRelatedAccounts() + } + } // 处理功能开关状态变化 const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => { @@ -351,7 +415,7 @@ export default function DeviceDetailPage() {
最后活跃:{device.lastActive}
- + 基本信息 关联账号 @@ -435,8 +499,37 @@ export default function DeviceDetailPage() { +
+

微信账号列表

+ +
+ - {device.wechatAccounts && device.wechatAccounts.length > 0 ? ( + {accountsLoading && ( +
+
+ 加载微信账号中... +
+ )} + + {!accountsLoading && device.wechatAccounts && device.wechatAccounts.length > 0 ? (
{device.wechatAccounts.map((account) => (
@@ -448,26 +541,38 @@ export default function DeviceDetailPage() {
{account.nickname}
- - {account.status === "normal" ? "正常" : "异常"} + + {account.wechatAliveText}
微信号: {account.wechatId}
-
性别: {account.gender === "male" ? "男" : "女"}
+
性别: {account.gender === 1 ? "男" : "女"}
- 好友数: {account.friendCount} - - {account.addFriendStatus === "enabled" ? "可加友" : "已停用"} + 好友数: {account.totalFriend} + + {account.statusText}
+
最后活跃: {account.lastActive}
))}
) : ( -
-

此设备暂无关联的微信账号

-
+ !accountsLoading && ( +
+

此设备暂无关联的微信账号

+ +
+ ) )}
diff --git a/Cunkebao/app/devices/page.tsx b/Cunkebao/app/devices/page.tsx index 447ca72c..037b4dba 100644 --- a/Cunkebao/app/devices/page.tsx +++ b/Cunkebao/app/devices/page.tsx @@ -5,14 +5,16 @@ import { useRouter } from "next/navigation" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode } from "lucide-react" +import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle } 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 { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { fetchDeviceList, deleteDevice } from "@/api/devices" import { ServerDevice } from "@/types/device" +import { api } from "@/lib/api" // 设备接口更新为与服务端接口对应的类型 interface Device extends ServerDevice { @@ -37,6 +39,13 @@ export default function DevicesPage() { const observerTarget = useRef(null) // 使用ref来追踪当前页码,避免依赖effect循环 const pageRef = useRef(1) + // 添加设备相关状态 + const [deviceImei, setDeviceImei] = useState("") + const [deviceName, setDeviceName] = useState("") + const [qrCodeImage, setQrCodeImage] = useState("") + const [isLoadingQRCode, setIsLoadingQRCode] = useState(false) + const [isSubmittingImei, setIsSubmittingImei] = useState(false) + const [activeTab, setActiveTab] = useState("scan") const devicesPerPage = 20 // 每页显示20条记录 @@ -155,6 +164,259 @@ export default function DevicesPage() { } }, [hasMore, isLoading, loadNextPage]) + // 获取设备二维码 + const fetchDeviceQRCode = async () => { + try { + setIsLoadingQRCode(true) + setQrCodeImage("") // 清空当前二维码 + + console.log("正在请求二维码..."); + + // 发起请求获取二维码 - 直接使用fetch避免api工具添加基础URL + const response = await fetch('http://yi.54word.com/v1/api/device/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({}) + }) + + console.log("二维码请求响应状态:", response.status); + + // 保存原始响应文本以便调试 + const responseText = await response.text(); + console.log("原始响应内容:", responseText); + + // 尝试将响应解析为JSON + let result; + try { + result = JSON.parse(responseText); + } catch (e) { + console.error("响应不是有效的JSON:", e); + toast({ + title: "获取二维码失败", + description: "服务器返回的数据格式无效", + variant: "destructive", + }); + return; + } + + console.log("二维码响应数据:", result); + + if (result && result.code === 200) { + // 尝试多种可能的返回数据结构 + let qrcodeData = null; + + if (result.data?.qrCode) { + qrcodeData = result.data.qrCode; + console.log("找到二维码数据在 result.data.qrCode"); + } else if (result.data?.qrcode) { + qrcodeData = result.data.qrcode; + console.log("找到二维码数据在 result.data.qrcode"); + } else if (result.data?.image) { + qrcodeData = result.data.image; + console.log("找到二维码数据在 result.data.image"); + } else if (result.data?.url) { + // 如果返回的是URL而不是base64 + qrcodeData = result.data.url; + console.log("找到二维码URL在 result.data.url"); + setQrCodeImage(qrcodeData); + + toast({ + title: "二维码已更新", + description: "请使用手机扫描新的二维码添加设备", + }); + + return; // 直接返回,不进行base64处理 + } else if (typeof result.data === 'string') { + // 如果data直接是字符串 + qrcodeData = result.data; + console.log("二维码数据直接在 result.data 字符串中"); + } else { + console.error("无法找到二维码数据:", result); + toast({ + title: "获取二维码失败", + description: "返回数据格式不正确", + variant: "destructive", + }); + return; + } + + // 检查数据是否为空 + if (!qrcodeData) { + console.error("二维码数据为空"); + toast({ + title: "获取二维码失败", + description: "服务器返回的二维码数据为空", + variant: "destructive", + }); + return; + } + + console.log("处理前的二维码数据:", qrcodeData); + + // 检查是否已经是完整的data URL + if (qrcodeData.startsWith('data:image')) { + console.log("数据已包含data:image前缀"); + setQrCodeImage(qrcodeData); + } + // 检查是否是URL + else if (qrcodeData.startsWith('http')) { + console.log("数据是HTTP URL"); + setQrCodeImage(qrcodeData); + } + // 尝试作为base64处理 + else { + try { + // 确保base64字符串没有空格等干扰字符 + const cleanedBase64 = qrcodeData.trim(); + console.log("处理后的base64数据:", cleanedBase64.substring(0, 30) + "..."); + + // 直接以图片src格式设置 + setQrCodeImage(`data:image/png;base64,${cleanedBase64}`); + + // 预加载图片,确认是否有效 + const img = new Image(); + img.onload = () => { + console.log("二维码图片加载成功"); + }; + img.onerror = (e) => { + console.error("二维码图片加载失败:", e); + toast({ + title: "二维码加载失败", + description: "服务器返回的数据无法显示为图片", + variant: "destructive", + }); + }; + img.src = `data:image/png;base64,${cleanedBase64}`; + } catch (e) { + console.error("处理base64数据出错:", e); + toast({ + title: "获取二维码失败", + description: "图片数据处理失败", + variant: "destructive", + }); + return; + } + } + + toast({ + title: "二维码已更新", + description: "请使用手机扫描新的二维码添加设备", + }); + } else { + console.error("获取二维码失败:", result); + toast({ + title: "获取二维码失败", + description: result?.msg || "请稍后重试", + variant: "destructive", + }); + } + } catch (error) { + console.error("获取二维码失败", error); + toast({ + title: "获取二维码失败", + description: "请检查网络连接后重试", + variant: "destructive", + }); + } finally { + setIsLoadingQRCode(false); + } + } + + // 打开添加设备模态框时获取二维码 + const handleOpenAddDeviceModal = () => { + setIsAddDeviceOpen(true) + setDeviceImei("") + setDeviceName("") + fetchDeviceQRCode() + } + + // 通过IMEI添加设备 + const handleAddDeviceByImei = async () => { + if (!deviceImei) { + toast({ + title: "IMEI不能为空", + description: "请输入有效的设备IMEI", + variant: "destructive", + }); + return; + } + + try { + setIsSubmittingImei(true); + console.log("正在添加设备,IMEI:", deviceImei, "设备名称:", deviceName); + + // 使用api.post发送请求到/v1/devices + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/devices`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + imei: deviceImei, + memo: deviceName + }) + }); + + console.log("添加设备响应状态:", response.status); + + // 保存原始响应文本以便调试 + const responseText = await response.text(); + console.log("原始响应内容:", responseText); + + // 尝试将响应解析为JSON + let result; + try { + result = JSON.parse(responseText); + } catch (e) { + console.error("响应不是有效的JSON:", e); + toast({ + title: "添加设备失败", + description: "服务器返回的数据格式无效", + variant: "destructive", + }); + return; + } + + console.log("添加设备响应:", result); + + if (result && result.code === 200) { + toast({ + title: "设备添加成功", + description: result.data?.msg || "设备已成功添加", + }); + + // 清空输入并关闭弹窗 + setDeviceImei(""); + setDeviceName(""); + setIsAddDeviceOpen(false); + + // 刷新设备列表 + loadDevices(1, true); + } else { + console.error("添加设备失败:", result); + toast({ + title: "添加设备失败", + description: result?.msg || "请检查设备信息是否正确", + variant: "destructive", + }); + } + } catch (error) { + console.error("添加设备请求失败:", error); + toast({ + title: "请求失败", + description: "网络错误,请稍后重试", + variant: "destructive", + }); + } finally { + setIsSubmittingImei(false); + } + } + // 刷新设备列表 const handleRefresh = () => { setCurrentPage(1) @@ -231,7 +493,7 @@ export default function DevicesPage() {

设备管理

- @@ -359,22 +621,128 @@ export default function DevicesPage() { 添加设备 -
-
- - -
-
- - -
-
- - -
-
+ + + + + + 扫码添加 + + + + 手动添加 + + + + +
+
+ {isLoadingQRCode ? ( +
+ +

正在获取二维码...

+
+ ) : qrCodeImage ? ( +
+
+ 设备添加二维码 { + console.error("二维码图片加载失败"); + // 隐藏图片 + e.currentTarget.style.display = 'none'; + // 显示错误信息 + const container = document.getElementById('qrcode-container'); + if (container) { + const errorEl = container.querySelector('.qrcode-error'); + if (errorEl) { + errorEl.classList.remove('hidden'); + } + } + }} + /> +
+ +

未能加载二维码,请点击刷新按钮重试

+
+
+

+ 请使用手机扫描此二维码添加设备 +

+
+ ) : ( +
+ +

点击下方按钮获取二维码

+
+ )} +
+ +
+
+ + +
+
+ + setDeviceName(e.target.value)} + /> +

+ 为设备添加一个便于识别的名称 +

+
+
+ + setDeviceImei(e.target.value)} + /> +

+ 请输入设备IMEI码,可在设备信息中查看 +

+
+
+ + +
+
+
+
diff --git a/Server/application/devices/config/route.php b/Server/application/devices/config/route.php index 5c92d024..1bb7b88f 100644 --- a/Server/application/devices/config/route.php +++ b/Server/application/devices/config/route.php @@ -10,6 +10,7 @@ Route::group('v1/', function () { // 设备管理相关 Route::group('devices', function () { + Route::get(':id/related-accounts', 'app\\devices\\controller\\Device@getRelatedAccounts'); // 设备关联微信账号路由 Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表 Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数 Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情 @@ -28,4 +29,6 @@ Route::group('v1/', function () { Route::put('refresh', 'app\\devices\\controller\\DeviceWechat@refresh'); // 刷新设备微信状态 Route::post('transfer-friends', 'app\\devices\\controller\\DeviceWechat@transferFriends'); // 微信好友转移 }); + + })->middleware(['jwt']); \ No newline at end of file diff --git a/Server/application/devices/controller/Device.php b/Server/application/devices/controller/Device.php index 24595a65..4515a492 100644 --- a/Server/application/devices/controller/Device.php +++ b/Server/application/devices/controller/Device.php @@ -181,12 +181,6 @@ class Device extends Controller try { // 获取登录用户信息 $userInfo = request()->userInfo; - if (empty($userInfo)) { - return json([ - 'code' => 401, - 'msg' => '未登录或登录已过期' - ]); - } // 获取设备ID $id = Request::param('id/d'); @@ -277,13 +271,7 @@ class Device extends Controller try { // 获取登录用户信息 $userInfo = request()->userInfo; - if (empty($userInfo)) { - return json([ - 'code' => 401, - 'msg' => '未登录或登录已过期' - ]); - } - + // 检查用户权限,只有管理员可以添加设备 if ($userInfo['isAdmin'] != 1) { return json([ @@ -305,6 +293,7 @@ class Device extends Controller // 验证IMEI是否已存在 $exists = DeviceModel::where('imei', $data['imei'])->where('isDeleted', 0)->find(); + if ($exists) { return json([ 'code' => 400, @@ -314,10 +303,12 @@ class Device extends Controller // 设置设备公司ID $data['companyId'] = $userInfo['companyId']; - + $data['id'] = time(); + // 添加设备 $id = DeviceModel::addDevice($data); - + + // 此处调用底层API return json([ 'code' => 200, 'msg' => '添加成功', @@ -465,4 +456,72 @@ class Device extends Controller return json(['code' => 500, 'msg' => '更新任务配置失败']); } } + + /** + * 获取设备关联的微信账号 + * @return \think\response\Json + */ + public function getRelatedAccounts() + { + try { + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取设备ID + $deviceId = $this->request->param('id/d'); + if (empty($deviceId)) { + return json([ + 'code' => 400, + 'msg' => '设备ID不能为空' + ]); + } + + // 检查用户是否有权限访问该设备 + if ($userInfo['isAdmin'] != 1) { + // 非管理员需要检查是否有权限访问该设备 + $hasPermission = \app\common\model\DeviceUser::checkUserDevicePermission( + $userInfo['id'], + $deviceId, + $userInfo['companyId'] + ); + + if (!$hasPermission) { + return json([ + 'code' => 403, + 'msg' => '您没有权限查看该设备' + ]); + } + } + + // 获取设备信息,确认设备存在 + $device = DeviceModel::where('id', $deviceId) + ->where('isDeleted', 0) + ->find(); + + if (!$device) { + return json([ + 'code' => 404, + 'msg' => '设备不存在或已删除' + ]); + } + + // 获取设备关联的微信账号 + $wechatAccounts = \app\devices\model\DeviceWechatLogin::getDeviceRelatedAccounts($deviceId, $userInfo['companyId']); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'deviceId' => $deviceId, + 'accounts' => $wechatAccounts, + 'total' => count($wechatAccounts) + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '获取失败:' . $e->getMessage() + ]); + } + } } \ No newline at end of file diff --git a/Server/application/devices/model/DeviceWechatLogin.php b/Server/application/devices/model/DeviceWechatLogin.php new file mode 100644 index 00000000..138d5cd0 --- /dev/null +++ b/Server/application/devices/model/DeviceWechatLogin.php @@ -0,0 +1,207 @@ + 'integer', + 'deviceId' => 'integer', + 'companyId' => 'integer', + 'createTime' => 'integer' + ]; + + /** + * 查询设备关联的微信ID列表 + * @param int $deviceId 设备ID + * @param int $companyId 公司/租户ID + * @return array 微信ID列表 + */ + public static function getDeviceWechatIds($deviceId, $companyId = null) + { + $query = self::where('deviceId', $deviceId); + + // 如果提供了公司ID,则添加对应的条件 + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + // 提取微信ID + $records = $query->select(); + $wechatIds = []; + + foreach ($records as $record) { + if (!empty($record['wechatId'])) { + $wechatIds[] = $record['wechatId']; + } + } + + return $wechatIds; + } + + /** + * 根据微信ID查询关联的设备 + * @param string $wechatId 微信ID + * @param int $companyId 公司/租户ID + * @return array 设备ID列表 + */ + public static function getWechatDeviceIds($wechatId, $companyId = null) + { + $query = self::where('wechatId', $wechatId); + + // 如果提供了公司ID,则添加对应的条件 + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + // 提取设备ID + $records = $query->select(); + $deviceIds = []; + + foreach ($records as $record) { + if (!empty($record['deviceId'])) { + $deviceIds[] = $record['deviceId']; + } + } + + return $deviceIds; + } + + /** + * 添加设备微信登录记录 + * @param int $deviceId 设备ID + * @param string $wechatId 微信ID + * @param int $companyId 公司/租户ID + * @return int 新增记录ID + */ + public static function addRecord($deviceId, $wechatId, $companyId) + { + // 检查是否已存在相同记录 + $exists = self::where('deviceId', $deviceId) + ->where('wechatId', $wechatId) + ->where('companyId', $companyId) + ->find(); + + if ($exists) { + return $exists['id']; + } + + // 创建新记录 + $model = new self(); + $model->deviceId = $deviceId; + $model->wechatId = $wechatId; + $model->companyId = $companyId; + $model->save(); + + return $model->id; + } + + /** + * 删除设备微信登录记录 + * @param int $deviceId 设备ID + * @param string $wechatId 微信ID,为null时删除设备所有记录 + * @param int $companyId 公司/租户ID,为null时不限公司 + * @return bool 删除结果 + */ + public static function removeRecord($deviceId, $wechatId = null, $companyId = null) + { + $query = self::where('deviceId', $deviceId); + + if ($wechatId !== null) { + $query->where('wechatId', $wechatId); + } + + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + return $query->delete(); + } + + /** + * 关联Device模型 + * @return \think\model\relation\BelongsTo + */ + public function device() + { + return $this->belongsTo('Device', 'deviceId'); + } + + /** + * 获取设备关联的微信账号信息 + * @param int $deviceId 设备ID + * @param int $companyId 公司/租户ID + * @return array 微信账号信息列表 + */ + public static function getDeviceRelatedAccounts($deviceId, $companyId = null) + { + // 获取设备关联的微信ID列表 + $wechatIds = self::getDeviceWechatIds($deviceId, $companyId); + if (empty($wechatIds)) { + return []; + } + + // 查询微信账号信息 + $accounts = \think\Db::name('wechat_account') + ->alias('wa') + ->field([ + 'wa.id', + 'wa.wechatId', + 'wa.accountNickname', + 'wa.nickname', + 'wa.accountUserName', + 'wa.avatar', + 'wa.gender', + 'wa.wechatAlive', + 'wa.status', + 'wa.totalFriend', + 'wa.createTime', + 'wa.updateTime' + ]) + ->whereIn('wa.wechatId', $wechatIds) + ->where('wa.isDeleted', 0) + ->select(); + + // 处理结果数据 + $result = []; + foreach ($accounts as $account) { + // 计算最后活跃时间 + $lastActive = date('Y-m-d H:i:s', max($account['updateTime'], $account['createTime'])); + + // 格式化数据 + $result[] = [ + 'id' => $account['id'], + 'wechatId' => $account['wechatId'], + 'nickname' => $account['accountNickname'] ?: $account['nickname'] ?: '未命名微信', + 'accountUserName' => $account['accountUserName'], + 'avatar' => $account['avatar'], + 'gender' => intval($account['gender']), + 'status' => intval($account['status']), + 'statusText' => intval($account['status']) === 1 ? '可加友' : '已停用', + 'wechatAlive' => intval($account['wechatAlive']), + 'wechatAliveText' => intval($account['wechatAlive']) === 1 ? '正常' : '异常', + 'totalFriend' => intval($account['totalFriend']), + 'lastActive' => $lastActive + ]; + } + + return $result; + } +} \ No newline at end of file From ca14b25399fe977bfbd0d49ca32106de017ee7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Tue, 1 Apr 2025 15:09:18 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/devices/controller/Device.php | 89 ++++++++++--- .../devices/model/DeviceHandleLog.php | 120 ++++++++++++++++++ 2 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 Server/application/devices/model/DeviceHandleLog.php diff --git a/Server/application/devices/controller/Device.php b/Server/application/devices/controller/Device.php index 4515a492..ad8db19a 100644 --- a/Server/application/devices/controller/Device.php +++ b/Server/application/devices/controller/Device.php @@ -1,8 +1,10 @@ $data['imei'], + 'userId' => $userInfo['id'], + 'content' => '添加设备', + 'companyId' => $userInfo['companyId'], + ] + ); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + + return json([ + 'code' => 500, + 'msg' => '添加失败:' . $e->getMessage() + ]); + } // 此处调用底层API return json([ @@ -333,12 +357,6 @@ class Device extends Controller try { // 获取登录用户信息 $userInfo = request()->userInfo; - if (empty($userInfo)) { - return json([ - 'code' => 401, - 'msg' => '未登录或登录已过期' - ]); - } // 检查用户权限,只有管理员可以删除设备 if ($userInfo['isAdmin'] != 1) { @@ -396,6 +414,9 @@ class Device extends Controller { // 获取请求参数 $data = $this->request->post(); + + // 获取登录用户信息 + $userInfo = request()->userInfo; // 验证参数 if (empty($data['id'])) { @@ -436,14 +457,52 @@ class Device extends Controller if (!$hasUpdate) { return json(['code' => 200, 'msg' => '更新成功', 'data' => ['taskConfig' => $taskConfig]]); } - - // 更新设备taskConfig字段 - $result = \app\devices\model\Device::where('id', $deviceId) - ->update([ - 'taskConfig' => json_encode($taskConfig), - 'updateTime' => time() - ]); + + try { + Db::startTrans(); + + // 更新设备taskConfig字段 + $result = \app\devices\model\Device::where('id', $deviceId) + ->update([ + 'taskConfig' => json_encode($taskConfig), + 'updateTime' => time() + ]); + + if (isset($data['autoAddFriend'])) { + $content = $data['autoAddFriend'] ? '开启自动添加好友' : '关闭自动添加好友'; + } + + if (isset($data['autoReply'])) { + $content = $data['autoReply'] ? '开启自动回复' : '关闭自动回复'; + } + + if (isset($data['momentsSync'])) { + $content = $data['momentsSync'] ? '开启朋友圈同步' : '关闭朋友圈同步'; + } + + if (isset($data['aiChat'])) { + $content = $data['aiChat'] ? '开启AI会话' : '关闭AI会话'; + } + + // 添加设备操作记录 + DeviceHandleLog::addLog( + [ + 'imei' => $device['imei'], + 'userId' => $userInfo['id'], + 'content' => $content, + 'companyId' => $userInfo['companyId'], + ] + ); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return json([ + 'code' => 500, + 'msg' => '更新任务配置失败' + ]); + } + if ($result) { return json([ 'code' => 200, diff --git a/Server/application/devices/model/DeviceHandleLog.php b/Server/application/devices/model/DeviceHandleLog.php new file mode 100644 index 00000000..f1a2520d --- /dev/null +++ b/Server/application/devices/model/DeviceHandleLog.php @@ -0,0 +1,120 @@ + 'integer', + 'userId' => 'integer', + 'companyId' => 'integer', + 'createTime' => 'datetime' + ]; + + /** + * 添加设备操作日志 + * @param array $data 日志数据 + * @return int 新增日志ID + */ + public static function addLog($data) + { + $log = new self(); + $log->allowField(true)->save($data); + return $log->id; + } + + /** + * 获取设备操作日志列表 + * @param array $where 查询条件 + * @param string $order 排序方式 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator 分页对象 + */ + public static function getLogList($where = [], $order = 'createTime desc', $page = 1, $limit = 10) + { + return self::where($where) + ->order($order) + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 根据IMEI获取设备操作日志 + * @param string $imei 设备IMEI + * @param int $companyId 租户ID + * @param int $limit 获取条数 + * @return array 日志记录 + */ + public static function getLogsByImei($imei, $companyId = null, $limit = 10) + { + $query = self::where('imei', $imei); + + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + return $query->order('createTime', 'desc') + ->limit($limit) + ->select(); + } + + /** + * 根据用户ID获取操作日志 + * @param int $userId 用户ID + * @param int $companyId 租户ID + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator 分页对象 + */ + public static function getLogsByUser($userId, $companyId = null, $page = 1, $limit = 10) + { + $query = self::where('userId', $userId); + + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + return $query->order('createTime', 'desc') + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 记录设备操作日志的便捷方法 + * @param string $imei 设备IMEI + * @param int $userId 操作用户ID + * @param string $content 操作内容 + * @param int $companyId 租户ID + * @return int 日志ID + */ + public static function recordLog($imei, $userId, $content, $companyId = null) + { + $data = [ + 'imei' => $imei, + 'userId' => $userId, + 'content' => $content, + 'companyId' => $companyId + ]; + + return self::addLog($data); + } +} \ No newline at end of file From db7b3aa7af17c66b19358f99af3400a91c3b0aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Tue, 1 Apr 2025 15:09:18 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/devices.ts | 5 + Cunkebao/app/devices/[id]/page.tsx | 93 ++++++++- Server/application/devices/config/route.php | 1 + .../application/devices/controller/Device.php | 184 ++++++++++++++++-- .../devices/model/DeviceHandleLog.php | 121 ++++++++++++ 5 files changed, 382 insertions(+), 22 deletions(-) create mode 100644 Server/application/devices/model/DeviceHandleLog.php diff --git a/Cunkebao/api/devices.ts b/Cunkebao/api/devices.ts index e121be43..8f13f92c 100644 --- a/Cunkebao/api/devices.ts +++ b/Cunkebao/api/devices.ts @@ -38,6 +38,11 @@ export const fetchDeviceRelatedAccounts = async (id: string | number): Promise>(`/v1/devices/${id}/related-accounts`); }; +// 获取设备操作记录 +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 ( id: string | number, diff --git a/Cunkebao/app/devices/[id]/page.tsx b/Cunkebao/app/devices/[id]/page.tsx index 86a629e8..521741eb 100644 --- a/Cunkebao/app/devices/[id]/page.tsx +++ b/Cunkebao/app/devices/[id]/page.tsx @@ -4,13 +4,13 @@ import { useState, useEffect } from "react" import { useParams, useRouter } from "next/navigation" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw } from "lucide-react" +import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" import { ScrollArea } from "@/components/ui/scroll-area" -import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig } from "@/api/devices" +import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig, fetchDeviceHandleLogs } from "@/api/devices" import { toast } from "sonner" interface WechatAccount { @@ -65,6 +65,14 @@ function getBadgeVariant(status: string): "default" | "destructive" | "outline" } } +// 添加操作记录接口 +interface HandleLog { + id: string | number; + content: string; // 操作说明 + username: string; // 操作人 + createTime: string; // 操作时间 +} + export default function DeviceDetailPage() { const params = useParams() const router = useRouter() @@ -72,6 +80,8 @@ export default function DeviceDetailPage() { const [activeTab, setActiveTab] = useState("info") const [loading, setLoading] = useState(true) const [accountsLoading, setAccountsLoading] = useState(false) + const [logsLoading, setLogsLoading] = useState(false) + const [handleLogs, setHandleLogs] = useState([]) const [savingFeatures, setSavingFeatures] = useState({ autoAddFriend: false, autoReply: false, @@ -284,6 +294,34 @@ export default function DeviceDetailPage() { } } + // 获取设备操作记录 + const fetchHandleLogs = async () => { + if (!params.id || logsLoading) return + + try { + setLogsLoading(true) + const response = await fetchDeviceHandleLogs(params.id as string) + + if (response && response.code === 200 && response.data) { + const logs = response.data.list || [] + setHandleLogs(logs) + + if (logs.length > 0) { + console.log('获取到操作记录:', logs.length) + } else { + console.log('设备暂无操作记录') + } + } else { + toast.error("获取操作记录失败") + } + } catch (error) { + console.error("获取操作记录失败:", error) + toast.error("获取操作记录失败,请稍后重试") + } finally { + setLogsLoading(false) + } + } + // 处理标签页切换 const handleTabChange = (value: string) => { setActiveTab(value) @@ -292,6 +330,11 @@ export default function DeviceDetailPage() { if (value === "accounts") { fetchRelatedAccounts() } + + // 当切换到"操作记录"标签时,获取最新的操作记录 + if (value === "history") { + fetchHandleLogs() + } } // 处理功能开关状态变化 @@ -580,18 +623,45 @@ export default function DeviceDetailPage() { +
+

操作记录

+ +
+ - {device.history && device.history.length > 0 ? ( + {logsLoading && handleLogs.length === 0 ? ( +
+
+ 加载操作记录中... +
+ ) : handleLogs.length > 0 ? (
- {device.history.map((record, index) => ( -
+ {handleLogs.map((log) => ( +
-
{record.action}
+
{log.content}
- 操作人: {record.operator} · {record.time} + 操作人: {log.username} · {log.createTime}
@@ -600,6 +670,15 @@ export default function DeviceDetailPage() { ) : (

暂无操作记录

+
)} diff --git a/Server/application/devices/config/route.php b/Server/application/devices/config/route.php index 1bb7b88f..58190466 100644 --- a/Server/application/devices/config/route.php +++ b/Server/application/devices/config/route.php @@ -11,6 +11,7 @@ Route::group('v1/', function () { // 设备管理相关 Route::group('devices', function () { Route::get(':id/related-accounts', 'app\\devices\\controller\\Device@getRelatedAccounts'); // 设备关联微信账号路由 + Route::get(':id/handle-logs', 'app\\devices\\controller\\Device@handleLogs'); // 获取设备操作记录 Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表 Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数 Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情 diff --git a/Server/application/devices/controller/Device.php b/Server/application/devices/controller/Device.php index 4515a492..2e8168ca 100644 --- a/Server/application/devices/controller/Device.php +++ b/Server/application/devices/controller/Device.php @@ -1,8 +1,10 @@ $data['imei'], + 'userId' => $userInfo['id'], + 'content' => '添加设备', + 'companyId' => $userInfo['companyId'], + ] + ); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + + return json([ + 'code' => 500, + 'msg' => '添加失败:' . $e->getMessage() + ]); + } // 此处调用底层API return json([ @@ -333,12 +357,6 @@ class Device extends Controller try { // 获取登录用户信息 $userInfo = request()->userInfo; - if (empty($userInfo)) { - return json([ - 'code' => 401, - 'msg' => '未登录或登录已过期' - ]); - } // 检查用户权限,只有管理员可以删除设备 if ($userInfo['isAdmin'] != 1) { @@ -396,6 +414,9 @@ class Device extends Controller { // 获取请求参数 $data = $this->request->post(); + + // 获取登录用户信息 + $userInfo = request()->userInfo; // 验证参数 if (empty($data['id'])) { @@ -436,14 +457,53 @@ class Device extends Controller if (!$hasUpdate) { return json(['code' => 200, 'msg' => '更新成功', 'data' => ['taskConfig' => $taskConfig]]); } - - // 更新设备taskConfig字段 - $result = \app\devices\model\Device::where('id', $deviceId) - ->update([ - 'taskConfig' => json_encode($taskConfig), - 'updateTime' => time() - ]); + + try { + Db::startTrans(); + + // 更新设备taskConfig字段 + $result = \app\devices\model\Device::where('id', $deviceId) + ->update([ + 'taskConfig' => json_encode($taskConfig), + 'updateTime' => time() + ]); + + if (isset($data['autoAddFriend'])) { + $content = $data['autoAddFriend'] ? '开启自动添加好友' : '关闭自动添加好友'; + } + + if (isset($data['autoReply'])) { + $content = $data['autoReply'] ? '开启自动回复' : '关闭自动回复'; + } + + if (isset($data['momentsSync'])) { + $content = $data['momentsSync'] ? '开启朋友圈同步' : '关闭朋友圈同步'; + } + + if (isset($data['aiChat'])) { + $content = $data['aiChat'] ? '开启AI会话' : '关闭AI会话'; + } + + // 添加设备操作记录 + DeviceHandleLog::addLog( + [ + 'imei' => $device['imei'], + 'deviceId' => $deviceId, + 'userId' => $userInfo['id'], + 'content' => $content, + 'companyId' => $userInfo['companyId'], + ] + ); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return json([ + 'code' => 500, + 'msg' => '更新任务配置失败' + ]); + } + if ($result) { return json([ 'code' => 200, @@ -524,4 +584,98 @@ class Device extends Controller ]); } } + + /** + * 获取设备操作记录 + * @return \think\response\Json + */ + public function handleLogs() + { + try { + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取设备ID + $deviceId = $this->request->param('id/d'); + if (empty($deviceId)) { + return json([ + 'code' => 400, + 'msg' => '设备ID不能为空' + ]); + } + + // 检查用户是否有权限访问该设备 + if ($userInfo['isAdmin'] != 1) { + // 非管理员需要检查是否有权限访问该设备 + $hasPermission = \app\common\model\DeviceUser::checkUserDevicePermission( + $userInfo['id'], + $deviceId, + $userInfo['companyId'] + ); + + if (!$hasPermission) { + return json([ + 'code' => 403, + 'msg' => '您没有权限查看该设备' + ]); + } + } + + // 获取设备信息,确认设备存在 + $device = DeviceModel::where('id', $deviceId) + ->where('isDeleted', 0) + ->find(); + + if (!$device) { + return json([ + 'code' => 404, + 'msg' => '设备不存在或已删除' + ]); + } + + // 获取分页参数 + $page = (int)Request::param('page', 1); + $limit = (int)Request::param('limit', 10); + + // 查询设备操作记录,并关联用户表获取操作人信息 + $logs = Db::table('tk_device_handle_log') + ->alias('l') + ->join('tk_users u', 'l.userId = u.id', 'left') + ->where('l.imei', $device['imei']) + ->where('l.companyId', $userInfo['companyId']) + ->field([ + 'l.id', + 'l.content', + 'l.createTime', + 'u.username' + ]) + ->order('l.createTime desc') + ->paginate($limit, false, ['page' => $page]); + + // 格式化返回数据 + $items = []; + foreach ($logs as $log) { + $items[] = [ + 'id' => $log['id'], + 'content' => $log['content'], + 'username' => $log['username'] ? $log['username'] : '未知用户', + 'createTime' => $log['createTime'] + ]; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'total' => $logs->total(), + 'list' => $items + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '获取失败:' . $e->getMessage() + ]); + } + } } \ No newline at end of file diff --git a/Server/application/devices/model/DeviceHandleLog.php b/Server/application/devices/model/DeviceHandleLog.php new file mode 100644 index 00000000..70ef7b11 --- /dev/null +++ b/Server/application/devices/model/DeviceHandleLog.php @@ -0,0 +1,121 @@ + 'integer', + 'userId' => 'integer', + 'deviceId' => 'integer', + 'companyId' => 'integer', + 'createTime' => 'datetime' + ]; + + /** + * 添加设备操作日志 + * @param array $data 日志数据 + * @return int 新增日志ID + */ + public static function addLog($data) + { + $log = new self(); + $log->allowField(true)->save($data); + return $log->id; + } + + /** + * 获取设备操作日志列表 + * @param array $where 查询条件 + * @param string $order 排序方式 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator 分页对象 + */ + public static function getLogList($where = [], $order = 'createTime desc', $page = 1, $limit = 10) + { + return self::where($where) + ->order($order) + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 根据IMEI获取设备操作日志 + * @param string $imei 设备IMEI + * @param int $companyId 租户ID + * @param int $limit 获取条数 + * @return array 日志记录 + */ + public static function getLogsByImei($imei, $companyId = null, $limit = 10) + { + $query = self::where('imei', $imei); + + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + return $query->order('createTime', 'desc') + ->limit($limit) + ->select(); + } + + /** + * 根据用户ID获取操作日志 + * @param int $userId 用户ID + * @param int $companyId 租户ID + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator 分页对象 + */ + public static function getLogsByUser($userId, $companyId = null, $page = 1, $limit = 10) + { + $query = self::where('userId', $userId); + + if ($companyId !== null) { + $query->where('companyId', $companyId); + } + + return $query->order('createTime', 'desc') + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 记录设备操作日志的便捷方法 + * @param string $imei 设备IMEI + * @param int $userId 操作用户ID + * @param string $content 操作内容 + * @param int $companyId 租户ID + * @return int 日志ID + */ + public static function recordLog($imei, $userId, $content, $companyId = null) + { + $data = [ + 'imei' => $imei, + 'userId' => $userId, + 'content' => $content, + 'companyId' => $companyId + ]; + + return self::addLog($data); + } +} \ No newline at end of file From a2c9078ccf0b7071678b2ead7b59bd999870e256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Tue, 1 Apr 2025 17:36:32 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/wechat-accounts.ts | 91 ++++ Cunkebao/app/wechat-accounts/page.tsx | 413 +++++++++++------- Cunkebao/types/wechat-account.ts | 113 +++++ .../common/model/CompanyAccount.php | 131 ++++++ .../devices/controller/DeviceWechat.php | 69 ++- .../application/devices/model/FriendTask.php | 324 ++++++++++++++ .../devices/model/WechatAccount.php | 2 - 7 files changed, 986 insertions(+), 157 deletions(-) create mode 100644 Cunkebao/api/wechat-accounts.ts create mode 100644 Cunkebao/types/wechat-account.ts create mode 100644 Server/application/common/model/CompanyAccount.php create mode 100644 Server/application/devices/model/FriendTask.php diff --git a/Cunkebao/api/wechat-accounts.ts b/Cunkebao/api/wechat-accounts.ts new file mode 100644 index 00000000..1cc6cf9e --- /dev/null +++ b/Cunkebao/api/wechat-accounts.ts @@ -0,0 +1,91 @@ +import { api } from "@/lib/api"; +import { + ServerWechatAccountsResponse, + QueryWechatAccountParams, + WechatAccountDetailResponse +} from "@/types/wechat-account"; + +/** + * 获取微信账号列表 + * @param params 查询参数 + * @returns 微信账号列表响应 + */ +export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise => { + 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 api.get(`/v1/device/wechats?${queryParams.toString()}`); +}; + +/** + * 获取微信账号详情 + * @param id 微信账号ID + * @returns 微信账号详情响应 + */ +export const fetchWechatAccountDetail = async (id: string | number): Promise => { + return api.get(`/v1/device/wechats/${id}`); +}; + +/** + * 刷新微信账号状态 + * @returns 刷新结果 + */ +export const refreshWechatAccounts = async (): Promise<{ code: number; msg: string; data: any }> => { + return api.put<{ code: number; msg: string; data: any }>('/v1/device/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 api.post<{ code: number; msg: string; data: any }>('/v1/device/wechats/transfer-friends', { + source_id: sourceId, + target_id: targetId + }); +}; + +/** + * 将服务器返回的微信账号数据转换为前端使用的格式 + * @param serverAccount 服务器返回的微信账号数据 + * @returns 前端使用的微信账号数据 + */ +export const transformWechatAccount = (serverAccount: any): import("@/types/wechat-account").WechatAccount => { + // 从deviceInfo中提取设备信息 + let deviceId = ''; + let deviceName = ''; + + if (serverAccount.deviceInfo) { + const deviceInfo = serverAccount.deviceInfo.split(' '); + deviceId = deviceInfo[0] || ''; + deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '') : ''; + } + + // 假设每天最多可添加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() // 服务端未提供,使用当前时间 + }; +}; \ No newline at end of file diff --git a/Cunkebao/app/wechat-accounts/page.tsx b/Cunkebao/app/wechat-accounts/page.tsx index 829f206c..3a9b1e91 100644 --- a/Cunkebao/app/wechat-accounts/page.tsx +++ b/Cunkebao/app/wechat-accounts/page.tsx @@ -1,7 +1,7 @@ "use client" -import { useState } from "react" -import { ChevronLeft, Filter, Search, RefreshCw, ArrowRightLeft, AlertCircle } from "lucide-react" +import { useState, useEffect } from "react" +import { ChevronLeft, Filter, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from "lucide-react" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -20,73 +20,140 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from " import { toast } from "@/components/ui/use-toast" import { Progress } from "@/components/ui/progress" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" - -interface WechatAccount { - id: string - avatar: string - nickname: string - wechatId: string - deviceId: string - deviceName: string - friendCount: number - todayAdded: number - remainingAdds: number - maxDailyAdds: number - status: "normal" | "abnormal" - lastActive: string -} - -const generateRandomWechatAccounts = (count: number): WechatAccount[] => { - return Array.from({ length: count }, (_, index) => ({ - id: `account-${index + 1}`, - avatar: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg", - nickname: `卡若-${["25vig", "zok7e", "ip9ob", "2kna3"][index % 4]}`, - wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, - deviceId: `device-${Math.floor(index / 3) + 1}`, - deviceName: `设备${Math.floor(index / 3) + 1}`, - friendCount: Math.floor(Math.random() * (6300 - 520)) + 520, - todayAdded: Math.floor(Math.random() * 15), - remainingAdds: Math.floor(Math.random() * 10) + 5, - maxDailyAdds: 20, - status: Math.random() > 0.2 ? "normal" : "abnormal", - lastActive: new Date(Date.now() - Math.random() * 86400000).toLocaleString(), - })) -} +import { fetchWechatAccountList, refreshWechatAccounts, transferWechatFriends, transformWechatAccount } from "@/api/wechat-accounts" +import { WechatAccount } from "@/types/wechat-account" export default function WechatAccountsPage() { const router = useRouter() - const [accounts] = useState(generateRandomWechatAccounts(42)) + const [accounts, setAccounts] = useState([]) const [searchQuery, setSearchQuery] = useState("") const [currentPage, setCurrentPage] = useState(1) const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false) const [selectedAccount, setSelectedAccount] = useState(null) + const [totalAccounts, setTotalAccounts] = useState(0) + const [isLoading, setIsLoading] = useState(true) + const [isRefreshing, setIsRefreshing] = useState(false) const accountsPerPage = 10 - const filteredAccounts = accounts.filter( - (account) => - account.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || - account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()), - ) + // 获取微信账号列表 + const fetchAccounts = async (page: number = 1, keyword: string = "") => { + try { + setIsLoading(true); + const response = await fetchWechatAccountList({ + page, + limit: accountsPerPage, + keyword, + sort: 'id', + order: 'desc' + }); - const paginatedAccounts = filteredAccounts.slice((currentPage - 1) * accountsPerPage, currentPage * accountsPerPage) + if (response && response.code === 200 && response.data) { + // 转换数据格式 + const wechatAccounts = response.data.list.map(transformWechatAccount); + setAccounts(wechatAccounts); + setTotalAccounts(response.data.total); + } else { + toast({ + title: "获取微信账号失败", + description: response?.msg || "请稍后再试", + variant: "destructive" + }); + // 如果API请求失败,设置空数组 + setAccounts([]); + setTotalAccounts(0); + } + } catch (error) { + console.error("获取微信账号列表失败:", error); + toast({ + title: "获取微信账号失败", + description: "请检查网络连接或稍后再试", + variant: "destructive" + }); + setAccounts([]); + setTotalAccounts(0); + } finally { + setIsLoading(false); + } + }; - const totalPages = Math.ceil(filteredAccounts.length / accountsPerPage) + // 刷新微信账号状态 + const handleRefresh = async () => { + try { + setIsRefreshing(true); + const response = await refreshWechatAccounts(); + + if (response && response.code === 200) { + toast({ + title: "刷新成功", + description: "微信账号状态已更新" + }); + // 重新获取数据 + await fetchAccounts(currentPage, searchQuery); + } else { + toast({ + title: "刷新失败", + description: response?.msg || "请稍后再试", + variant: "destructive" + }); + } + } catch (error) { + console.error("刷新微信账号状态失败:", error); + toast({ + title: "刷新失败", + description: "请检查网络连接或稍后再试", + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + + // 初始加载和页码变化时获取数据 + useEffect(() => { + fetchAccounts(currentPage, searchQuery); + }, [currentPage]); + + // 搜索时重置页码并获取数据 + const handleSearch = () => { + setCurrentPage(1); + fetchAccounts(1, searchQuery); + }; + + // 处理搜索框回车事件 + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + const filteredAccounts = accounts; + const totalPages = Math.ceil(totalAccounts / accountsPerPage); const handleTransferFriends = (account: WechatAccount) => { setSelectedAccount(account) setIsTransferDialogOpen(true) } - const handleConfirmTransfer = () => { + const handleConfirmTransfer = async () => { if (!selectedAccount) return - toast({ - title: "好友转移计划已创建", - description: "请在场景获客中查看详情", - }) - setIsTransferDialogOpen(false) - router.push("/scenarios") + try { + // 实际实现好友转移功能,这里需要另一个账号作为目标 + // 现在只是模拟效果 + toast({ + title: "好友转移计划已创建", + description: "请在场景获客中查看详情", + }) + setIsTransferDialogOpen(false) + router.push("/scenarios") + } catch (error) { + console.error("好友转移失败:", error); + toast({ + title: "好友转移失败", + description: "请稍后再试", + variant: "destructive" + }); + } } return ( @@ -110,126 +177,176 @@ export default function WechatAccountsPage() { placeholder="搜索微信号/昵称" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} />
-
-
- {paginatedAccounts.map((account) => ( - router.push(`/wechat-accounts/${account.id}`)} + {isLoading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
+

暂无微信账号数据

+ -
-
-
微信号:{account.wechatId}
+ {isRefreshing ? ( + + ) : ( + + )} + 刷新 + +
+ ) : ( +
+ {accounts.map((account) => ( + router.push(`/wechat-accounts/${account.id}`)} + > +
+ + + {account.nickname[0]} + +
-
好友数量:{account.friendCount}
-
今日新增:+{account.todayAdded}
-
-
-
-
- 今日可添加: - {account.remainingAdds} - - - - - - -

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

-
-
-
-
- - {account.todayAdded}/{account.maxDailyAdds} - +
+

{account.nickname}

+ + {account.status === "normal" ? "正常" : "异常"} +
- +
-
-
所属设备:{account.deviceName}
-
最后活跃:{account.lastActive}
+
+
微信号:{account.wechatId}
+
+
好友数量:{account.friendCount}
+
今日新增:+{account.todayAdded}
+
+
+
+
+ 今日可添加: + {account.remainingAdds} + + + + + + +

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

+
+
+
+
+ + {account.todayAdded}/{account.maxDailyAdds} + +
+ +
+
+
所属设备:{account.deviceName || '未知设备'}
+
最后活跃:{account.lastActive}
+
-
- - ))} -
+
+ ))} +
+ )} -
- - - - { - e.preventDefault() - setCurrentPage((prev) => Math.max(1, prev - 1)) - }} - /> - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - 0 && totalPages > 1 && ( +
+ + + + { e.preventDefault() - setCurrentPage(page) + if (currentPage > 1) { + setCurrentPage((prev) => prev - 1) + } }} - > - {page} - + /> - ))} - - { - e.preventDefault() - setCurrentPage((prev) => Math.min(totalPages, prev + 1)) - }} - /> - - - -
+ {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { + // 显示当前页附近的页码 + let pageToShow = i + 1; + if (currentPage > 3 && totalPages > 5) { + pageToShow = Math.min(currentPage - 2 + i, totalPages); + if (pageToShow > totalPages - 4) { + pageToShow = totalPages - 4 + i; + } + } + return ( + + { + e.preventDefault() + setCurrentPage(pageToShow) + }} + > + {pageToShow} + + + ); + })} + + { + e.preventDefault() + if (currentPage < totalPages) { + setCurrentPage((prev) => prev + 1) + } + }} + /> + +
+
+
+ )}
diff --git a/Cunkebao/types/wechat-account.ts b/Cunkebao/types/wechat-account.ts new file mode 100644 index 00000000..15ae51cc --- /dev/null +++ b/Cunkebao/types/wechat-account.ts @@ -0,0 +1,113 @@ +// 服务端返回的微信账号数据结构 +export interface ServerWechatAccount { + id: number; + wechatId: string; + nickname: string; + accountNickname: string; + avatar: string; + accountUserName: string; + status: string; + deviceStatus: string; + totalFriend: number; + canAddFriendCount: number; + deviceInfo: string; + todayNewFriendCount: number; + wechatAlive: number; + deviceAlive: number; + imei: string; + deviceMemo: string; +} + +// 服务器响应结构 +export interface ServerWechatAccountsResponse { + code: number; + msg: string; + data: { + total: number; + list: ServerWechatAccount[]; + }; +} + +// 前端使用的微信账号数据结构 +export interface WechatAccount { + id: string; + avatar: string; + nickname: string; + wechatId: string; + deviceId: string; + deviceName: string; + friendCount: number; + todayAdded: number; + remainingAdds: number; + maxDailyAdds: number; + status: "normal" | "abnormal"; + lastActive: string; +} + +// 微信账号查询参数 +export interface QueryWechatAccountParams { + keyword?: string; + page?: number; + limit?: number; + sort?: string; + order?: string; +} + +// 微信好友数据结构 +export interface WechatFriend { + id: string; + wechatId: string; + nickname: string; + avatar: string; + gender: number; + region: string; + signature: string; + labels: string; + createTime: string; +} + +// 微信账号详情数据结构 +export interface WechatAccountDetail { + basicInfo: { + id: number; + wechatId: string; + nickname: string; + avatar: string; + status: string; + deviceStatus: string; + deviceInfo: string; + gender: number; + region: string; + signature: string; + }; + statistics: { + totalFriend: number; + maleFriend: number; + femaleFriend: number; + canAddFriendCount: number; + yesterdayMsgCount: number; + sevenDayMsgCount: number; + thirtyDayMsgCount: number; + }; + accountInfo: { + age: number; + activityLevel: string; + weight: number; + createTime: string; + lastUpdateTime: string; + }; + restrictions: Array<{ + type: string; + reason: string; + startTime: string; + endTime: string; + }>; + friends: WechatFriend[]; +} + +// 微信账号详情响应 +export interface WechatAccountDetailResponse { + code: number; + msg: string; + data: WechatAccountDetail; +} \ No newline at end of file diff --git a/Server/application/common/model/CompanyAccount.php b/Server/application/common/model/CompanyAccount.php new file mode 100644 index 00000000..12964bb7 --- /dev/null +++ b/Server/application/common/model/CompanyAccount.php @@ -0,0 +1,131 @@ + 'integer', + 'tenantId' => 'integer', + 'accountType' => 'integer', + 'companyId' => 'integer', + 'useGoogleSecretKey' => 'boolean', + 'hasVerifyGoogleSecret' => 'boolean', + 'lastLoginTime' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer' + ]; + + /** + * 获取公司账户信息 + * @param string $userName 用户名 + * @param string $password 密码(MD5加密后的) + * @return array|null + */ + public static function getAccount($userName, $password) + { + // 查询账户 + $account = self::where('userName', $userName) + ->find(); + + if (!$account) { + return null; + } + + // 验证密码 + if ($account->passwordMd5 !== $password) { + return null; + } + + // 更新登录信息 + $account->lastLoginIp = request()->ip(); + $account->lastLoginTime = time(); + $account->save(); + + return [ + 'id' => $account->id, + 'tenantId' => $account->tenantId, + 'userName' => $account->userName, + 'realName' => $account->realName, + 'nickname' => $account->nickname, + 'avatar' => $account->avatar, + 'accountType' => $account->accountType, + 'companyId' => $account->companyId, + 'lastLoginIp' => $account->lastLoginIp, + 'lastLoginTime' => $account->lastLoginTime + ]; + } + + /** + * 通过租户ID获取账户信息 + * @param int $companyId 租户ID + * @return array|null + */ + public static function getAccountByCompanyId($companyId) + { + // 查询账户 + $account = self::where('companyId', $companyId)->find(); + + if (!$account) { + return null; + } + + return [ + 'id' => $account->id, + 'tenantId' => $account->tenantId, + 'userName' => $account->userName, + 'realName' => $account->realName, + 'nickname' => $account->nickname, + 'avatar' => $account->avatar, + 'accountType' => $account->accountType, + 'companyId' => $account->companyId, + 'lastLoginIp' => $account->lastLoginIp, + 'lastLoginTime' => $account->lastLoginTime + ]; + } + +} \ No newline at end of file diff --git a/Server/application/devices/controller/DeviceWechat.php b/Server/application/devices/controller/DeviceWechat.php index 5e151423..49d816f0 100644 --- a/Server/application/devices/controller/DeviceWechat.php +++ b/Server/application/devices/controller/DeviceWechat.php @@ -1,6 +1,7 @@ userInfo; // 获取查询条件 $where = []; @@ -102,7 +105,7 @@ class DeviceWechat extends Controller if (!empty($nickname)) { $where['nickname|accountNickname'] = ['like', "%{$nickname}%"]; } - + // 获取分页参数 $page = (int)Request::param('page', 1); $limit = (int)Request::param('limit', 10); @@ -110,22 +113,74 @@ class DeviceWechat extends Controller // 获取排序参数 $sort = Request::param('sort', 'id'); $order = Request::param('order', 'desc'); - - // 获取在线微信账号列表 - $list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit); + + // 公司账户表没有 companyId,需要转换一下 + $acountInfo = CompanyAccount::getAccountByCompanyId($userInfo['companyId']); + + // 先用账号进行查询 + $where['accountUserName'] = $acountInfo['userName']; + + // 根据用户权限不同实现不同的查询逻辑 + if ($userInfo['isAdmin'] == 1) { + // 管理员直接查询tk_wechat_account表 + $list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit); + } else { + // 非管理员先查询tk_device_user表 + $deviceIds = Db::table('tk_device_user') + ->where('companyId', $userInfo['companyId']) + ->where('userId', $userInfo['id']) + ->column('deviceId'); + + if (empty($deviceIds)) { + // 如果没有绑定设备,返回提示信息 + return json([ + 'code' => 403, + 'msg' => '请联系管理员绑定设备微信', + 'data' => [ + 'total' => 0, + 'list' => [] + ] + ]); + } + + // 获取这些设备关联的微信ID + $wechatIds = Db::table('tk_device_wechat_login') + ->where('companyId', $userInfo['companyId']) + ->whereIn('deviceId', $deviceIds) + ->column('wechatId'); + + if (empty($wechatIds)) { + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'total' => 0, + 'list' => [] + ] + ]); + } + + // 将微信ID添加到查询条件中 + $where['id'] = ['in', $wechatIds]; + + // 查询微信账号 + $list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit); + } // 处理返回数据 $data = []; foreach ($list->items() as $item) { // 计算今日可添加好友数量(这里使用一个示例算法,你可以根据实际需求修改) - $canAddFriendCount = 30 - (isset($item['yesterdayMsgCount']) ? intval($item['yesterdayMsgCount']) : 0); + $canAddFriendCount = 20 - Db::table('tk_friend_task')->where('wechatId', $item['wechatId'])->count('*'); if ($canAddFriendCount < 0) { $canAddFriendCount = 0; } - + // 计算今日新增好友数量(示例数据,实际需要从数据库获取或通过其他方式计算) // 这里只是一个示例,你需要根据实际情况替换 - $todayNewFriendCount = mt_rand(0, 10); // 随机生成0-10的数字作为示例 + $todayNewFriendCount = Db::table('tk_friend_task')->where('wechatId', $item['wechatId']) + ->where('status', 3) + ->count('*'); $data[] = [ 'id' => $item['id'], diff --git a/Server/application/devices/model/FriendTask.php b/Server/application/devices/model/FriendTask.php new file mode 100644 index 00000000..899ad0b6 --- /dev/null +++ b/Server/application/devices/model/FriendTask.php @@ -0,0 +1,324 @@ + 'integer', + 'tenantId' => 'integer', + 'operatorAccountId' => 'integer', + 'status' => 'integer', + 'wechatAccountId' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer' + ]; + + /** + * 状态常量 + */ + const STATUS_PENDING = 1; // 待处理 + const STATUS_PROCESSING = 2; // 处理中 + const STATUS_APPROVED = 3; // 已通过 + const STATUS_REJECTED = 4; // 已拒绝 + const STATUS_EXPIRED = 5; // 已过期 + const STATUS_CANCELLED = 6; // 已取消 + + /** + * 获取状态文本 + * @param int $status 状态码 + * @return string 状态文本 + */ + public static function getStatusText($status) + { + $statusMap = [ + self::STATUS_PENDING => '待处理', + self::STATUS_PROCESSING => '处理中', + self::STATUS_APPROVED => '已通过', + self::STATUS_REJECTED => '已拒绝', + self::STATUS_EXPIRED => '已过期', + self::STATUS_CANCELLED => '已取消' + ]; + + return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态'; + } + + /** + * 获取好友任务列表 + * @param array $where 查询条件 + * @param string $order 排序条件 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTaskList($where = [], $order = 'createTime desc', $page = 1, $limit = 10) + { + return self::where($where) + ->order($order) + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 获取任务详情 + * @param int $id 任务ID + * @return array|null + */ + public static function getTaskDetail($id) + { + return self::where('id', $id)->find(); + } + + /** + * 创建好友任务 + * @param array $data 任务数据 + * @return int|bool 任务ID或false + */ + public static function createTask($data) + { + // 确保必填字段存在 + if (!isset($data['id'])) { + return false; + } + + // 设置默认值 + if (!isset($data['status'])) { + $data['status'] = self::STATUS_PENDING; + } + + // 设置创建时间 + $data['createTime'] = time(); + $data['updateTime'] = time(); + + // 创建任务 + $task = new self; + $task->allowField(true)->save($data); + + return $task->id; + } + + /** + * 更新任务信息 + * @param int $id 任务ID + * @param array $data 更新数据 + * @return bool + */ + public static function updateTask($id, $data) + { + // 更新时间 + $data['updateTime'] = time(); + + return self::where('id', $id)->update($data); + } + + /** + * 更新任务状态 + * @param int $id 任务ID + * @param int $status 新状态 + * @param string $remark 备注 + * @return bool + */ + public static function updateTaskStatus($id, $status, $remark = '') + { + $data = [ + 'status' => $status, + 'updateTime' => time() + ]; + + if (!empty($remark)) { + $data['remark'] = $remark; + } + + return self::where('id', $id)->update($data); + } + + /** + * 取消任务 + * @param int $id 任务ID + * @param string $remark 取消原因 + * @return bool + */ + public static function cancelTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_CANCELLED, $remark); + } + + /** + * 任务审批通过 + * @param int $id 任务ID + * @param string $remark 备注信息 + * @return bool + */ + public static function approveTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_APPROVED, $remark); + } + + /** + * 任务拒绝 + * @param int $id 任务ID + * @param string $remark 拒绝原因 + * @return bool + */ + public static function rejectTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_REJECTED, $remark); + } + + /** + * 根据微信账号ID获取任务列表 + * @param int $wechatAccountId 微信账号ID + * @param array $status 状态数组,默认查询所有状态 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTasksByWechatAccount($wechatAccountId, $status = [], $page = 1, $limit = 10) + { + $where = ['wechatAccountId' => $wechatAccountId]; + + if (!empty($status)) { + $where['status'] = ['in', $status]; + } + + return self::getTaskList($where, 'createTime desc', $page, $limit); + } + + /** + * 根据操作账号ID获取任务列表 + * @param int $operatorAccountId 操作账号ID + * @param array $status 状态数组,默认查询所有状态 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTasksByOperator($operatorAccountId, $status = [], $page = 1, $limit = 10) + { + $where = ['operatorAccountId' => $operatorAccountId]; + + if (!empty($status)) { + $where['status'] = ['in', $status]; + } + + return self::getTaskList($where, 'createTime desc', $page, $limit); + } + + /** + * 根据手机号/微信号查询任务 + * @param string $phone 手机号/微信号 + * @param int $tenantId 租户ID + * @return array + */ + public static function getTasksByPhone($phone, $tenantId = null) + { + $where = ['phone' => $phone]; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + return self::where($where)->select(); + } + + /** + * 获取统计数据 + * @param int $tenantId 租户ID + * @param int $timeRange 时间范围(秒) + * @return array + */ + public static function getTaskStats($tenantId = null, $timeRange = 86400) + { + $where = []; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + // 时间范围 + $startTime = time() - $timeRange; + $where['createTime'] = ['>=', $startTime]; + + // 获取各状态的任务数量 + $stats = [ + 'total' => self::where($where)->count(), + 'pending' => self::where(array_merge($where, ['status' => self::STATUS_PENDING]))->count(), + 'processing' => self::where(array_merge($where, ['status' => self::STATUS_PROCESSING]))->count(), + 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(), + 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(), + 'expired' => self::where(array_merge($where, ['status' => self::STATUS_EXPIRED]))->count(), + 'cancelled' => self::where(array_merge($where, ['status' => self::STATUS_CANCELLED]))->count() + ]; + + return $stats; + } + + /** + * 任务处理结果统计 + * @param int $tenantId 租户ID + * @param int $timeRange 时间范围(秒) + * @return array + */ + public static function getTaskResultStats($tenantId = null, $timeRange = 86400 * 30) + { + $where = []; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + // 时间范围 + $startTime = time() - $timeRange; + $where['createTime'] = ['>=', $startTime]; + + // 获取处理结果数据 + $stats = [ + 'total' => self::where($where)->count(), + 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(), + 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(), + 'pending' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_PENDING, self::STATUS_PROCESSING]]]))->count(), + 'other' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_EXPIRED, self::STATUS_CANCELLED]]]))->count() + ]; + + // 计算成功率 + $stats['approvalRate'] = $stats['total'] > 0 ? round($stats['approved'] / $stats['total'] * 100, 2) : 0; + + return $stats; + } +} \ No newline at end of file diff --git a/Server/application/devices/model/WechatAccount.php b/Server/application/devices/model/WechatAccount.php index 16dc8c47..11aaaf78 100644 --- a/Server/application/devices/model/WechatAccount.php +++ b/Server/application/devices/model/WechatAccount.php @@ -97,8 +97,6 @@ class WechatAccount extends Model public static function getOnlineWechatList($where = [], $order = 'id desc', $page = 1, $limit = 10) { $condition = [ - 'wechatAlive' => 1, - 'deviceAlive' => 1, 'isDeleted' => 0 ]; From ad5fbd880ab23505e6df441eddf51978daa1be4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Tue, 1 Apr 2025 18:13:03 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=8F=B7=E8=AF=A6?= =?UTF-8?q?=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/wechat-accounts.ts | 116 ++ Cunkebao/app/wechat-accounts/[id]/page.tsx | 1125 ++++++++++---------- 2 files changed, 703 insertions(+), 538 deletions(-) diff --git a/Cunkebao/api/wechat-accounts.ts b/Cunkebao/api/wechat-accounts.ts index 1cc6cf9e..3097b1eb 100644 --- a/Cunkebao/api/wechat-accounts.ts +++ b/Cunkebao/api/wechat-accounts.ts @@ -88,4 +88,120 @@ export const transformWechatAccount = (serverAccount: any): import("@/types/wech status: serverAccount.wechatAlive === 1 ? "normal" : "abnormal" as "normal" | "abnormal", lastActive: new Date().toLocaleString() // 服务端未提供,使用当前时间 }; +}; + +/** + * 将服务端的微信账号详情转换为前端详情页面所需的格式 + * @param detailResponse 服务端微信账号详情响应 + * @returns 前端页面所需的微信账号详情格式 + */ +export const transformWechatAccountDetail = (detailResponse: WechatAccountDetailResponse): any => { + if (!detailResponse || !detailResponse.data) { + return null; + } + + const { basicInfo, statistics, accountInfo, restrictions, friends } = detailResponse.data; + + // 设备信息处理 + let deviceId = ''; + let deviceName = ''; + + if (basicInfo.deviceInfo) { + const deviceInfoParts = basicInfo.deviceInfo.split(' '); + deviceId = deviceInfoParts[0] || ''; + deviceName = deviceInfoParts[1] ? deviceInfoParts[1].replace(/[()]/g, '') : ''; + } + + // 账号年龄计算 + let accountAgeYears = 0; + let accountAgeMonths = 0; + + if (accountInfo.createTime) { + const createDate = new Date(accountInfo.createTime); + const currentDate = new Date(); + const diffInMonths = (currentDate.getFullYear() - createDate.getFullYear()) * 12 + + (currentDate.getMonth() - createDate.getMonth()); + + accountAgeYears = Math.floor(diffInMonths / 12); + accountAgeMonths = diffInMonths % 12; + } + + // 转换限制记录 + const restrictionRecords = restrictions?.map((restriction, index) => ({ + id: `${index}`, + date: restriction.startTime, + reason: restriction.reason, + recoveryTime: restriction.endTime, + type: mapRestrictionType(restriction.type) + })) || []; + + // 转换好友数据 + const transformedFriends = friends?.map(friend => ({ + id: friend.id.toString(), + avatar: friend.avatar || `/placeholder.svg?height=40&width=40&text=${friend.nickname?.[0] || ''}`, + nickname: friend.nickname, + wechatId: friend.wechatId, + remark: '', // 服务端未提供 + addTime: friend.createTime, + lastInteraction: '', // 服务端未提供 + tags: [], // 服务端未提供 + region: friend.region || '', + source: '', // 服务端未提供 + notes: '', // 服务端未提供 + })) || []; + + // 创建每周统计数据(模拟数据,服务端未提供) + const weeklyStats = Array.from({ length: 7 }, (_, i) => ({ + date: `Day ${i + 1}`, + friends: Math.floor(Math.random() * 50) + 50, + messages: Math.floor(Math.random() * 100) + 100, + })); + + return { + id: basicInfo.id.toString(), + avatar: basicInfo.avatar || '', + nickname: basicInfo.nickname || '', + wechatId: basicInfo.wechatId || '', + deviceId, + deviceName, + friendCount: statistics.totalFriend || 0, + todayAdded: 0, // 服务端未提供,默认为0 + status: basicInfo.status === '在线' ? 'normal' : 'abnormal', + lastActive: accountInfo.lastUpdateTime || new Date().toLocaleString(), + messageCount: statistics.thirtyDayMsgCount || 0, + activeRate: 0, // 服务端未提供,默认为0 + accountAge: { + years: accountAgeYears, + months: accountAgeMonths, + }, + totalChats: statistics.sevenDayMsgCount + statistics.yesterdayMsgCount || 0, + chatFrequency: Math.floor((statistics.sevenDayMsgCount || 0) / 7), // 每日平均聊天次数 + restrictionRecords, + isVerified: true, // 服务端未提供,默认为true + firstMomentDate: accountInfo.createTime || '', + accountWeight: accountInfo.weight || 50, + weightFactors: { + restrictionFactor: restrictionRecords.length > 0 ? 0.8 : 1.0, + verificationFactor: 1.0, + ageFactor: Math.min(1.0, accountAgeYears * 0.1 + 0.5), + activityFactor: statistics.totalFriend > 0 ? 0.9 : 0.7, + }, + weeklyStats, + friends: transformedFriends, + }; +}; + +/** + * 将服务端的限制类型映射为前端类型 + * @param type 服务端限制类型 + * @returns 前端限制类型 + */ +const mapRestrictionType = (type: string): "friend_limit" | "marketing" | "spam" | "other" => { + const typeMap: Record = { + 'friend': 'friend_limit', + 'marketing': 'marketing', + 'spam': 'spam' + }; + + return typeMap[type] || 'other'; }; \ No newline at end of file diff --git a/Cunkebao/app/wechat-accounts/[id]/page.tsx b/Cunkebao/app/wechat-accounts/[id]/page.tsx index e238b2e0..c113e604 100644 --- a/Cunkebao/app/wechat-accounts/[id]/page.tsx +++ b/Cunkebao/app/wechat-accounts/[id]/page.tsx @@ -18,6 +18,7 @@ import { Filter, Tag, ChevronRight, + Loader2, } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" @@ -42,6 +43,8 @@ import { PaginationNext, PaginationPrevious, } from "@/components/ui/pagination" +import { toast } from "@/components/ui/use-toast" +import { fetchWechatAccountDetail, transformWechatAccountDetail } from "@/api/wechat-accounts" interface RestrictionRecord { id: string @@ -120,137 +123,42 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri const [currentPage, setCurrentPage] = useState(1) const [activeTab, setActiveTab] = useState("overview") const friendsPerPage = 10 + const [isLoading, setIsLoading] = useState(false) useEffect(() => { // 模拟API调用获取账号详情 const fetchAccount = async () => { - // 生成随机标签 - const generateRandomTags = (count: number) => { - const tagPool = [ - { name: "潜在客户", color: "bg-blue-100 text-blue-800" }, - { name: "高意向", color: "bg-green-100 text-green-800" }, - { name: "已成交", color: "bg-purple-100 text-purple-800" }, - { name: "需跟进", color: "bg-yellow-100 text-yellow-800" }, - { name: "活跃用户", color: "bg-indigo-100 text-indigo-800" }, - { name: "沉默用户", color: "bg-gray-100 text-gray-800" }, - { name: "企业客户", color: "bg-red-100 text-red-800" }, - { name: "个人用户", color: "bg-pink-100 text-pink-800" }, - { name: "新增好友", color: "bg-emerald-100 text-emerald-800" }, - { name: "老客户", color: "bg-amber-100 text-amber-800" }, - ] - - return Array.from({ length: Math.floor(Math.random() * count) + 1 }, () => { - const randomTag = tagPool[Math.floor(Math.random() * tagPool.length)] - return { - id: `tag-${Math.random().toString(36).substring(2, 9)}`, - name: randomTag.name, - color: randomTag.color, - } + try { + setIsLoading(true) + + // 调用API获取微信账号详情 + const response = await fetchWechatAccountDetail(params.id) + + if (response && response.code === 200) { + // 转换数据格式 + const transformedAccount = transformWechatAccountDetail(response) + setAccount(transformedAccount) + } else { + toast({ + title: "获取微信账号详情失败", + description: response?.msg || "请稍后再试", + variant: "destructive" + }) + // 获取失败时使用模拟数据 + setAccount(generateMockAccountData()) + } + } catch (error) { + console.error("获取微信账号详情失败:", error) + toast({ + title: "获取微信账号详情失败", + description: "请检查网络连接或稍后再试", + variant: "destructive" }) + // 请求出错时使用模拟数据 + setAccount(generateMockAccountData()) + } finally { + setIsLoading(false) } - - // 生成随机好友 - const friendCount = Math.floor(Math.random() * (300 - 150)) + 150 - const generateFriends = (count: number) => { - return Array.from({ length: count }, (_, i) => { - const firstName = ["张", "王", "李", "赵", "陈", "刘", "杨", "黄", "周", "吴"][Math.floor(Math.random() * 10)] - const secondName = ["小", "大", "明", "华", "强", "伟", "芳", "娜", "秀", "英"][ - Math.floor(Math.random() * 10) - ] - const lastName = ["明", "华", "强", "伟", "芳", "娜", "秀", "英", "军", "杰"][Math.floor(Math.random() * 10)] - const nickname = firstName + secondName + lastName - - // 生成随机的添加时间(过去1年内) - const addDate = new Date() - addDate.setDate(addDate.getDate() - Math.floor(Math.random() * 365)) - - // 生成随机的最后互动时间(过去30天内) - const lastDate = new Date() - lastDate.setDate(lastDate.getDate() - Math.floor(Math.random() * 30)) - - return { - id: `friend-${i}`, - avatar: `/placeholder.svg?height=40&width=40&text=${nickname[0]}`, - nickname, - wechatId: `wxid_${Math.random().toString(36).substring(2, 9)}`, - remark: - Math.random() > 0.5 - ? `${nickname}(${["同事", "客户", "朋友", "同学"][Math.floor(Math.random() * 4)]})` - : "", - addTime: addDate.toISOString().split("T")[0], - lastInteraction: lastDate.toISOString().split("T")[0], - tags: generateRandomTags(3), - region: ["广东", "北京", "上海", "浙江", "江苏", "四川", "湖北", "福建", "山东", "河南"][ - Math.floor(Math.random() * 10) - ], - source: ["抖音", "小红书", "朋友介绍", "搜索添加", "群聊", "附近的人", "名片分享"][ - Math.floor(Math.random() * 7) - ], - notes: - Math.random() > 0.7 - ? ["对产品很感兴趣", "需要进一步跟进", "已购买过产品", "价格敏感", "需要更多信息"][ - Math.floor(Math.random() * 5) - ] - : "", - } - }) - } - - const friends = generateFriends(friendCount) - - const mockAccount: WechatAccountDetail = { - id: params.id, - avatar: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg", - nickname: "卡若-25vig", - wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, - deviceId: "device-1", - deviceName: "设备1", - friendCount: friends.length, - todayAdded: 12, - status: "normal", - lastActive: new Date().toLocaleString(), - messageCount: 1234, - activeRate: 87, - accountAge: { - years: 2, - months: 8, - }, - totalChats: 15234, - chatFrequency: 42, - restrictionRecords: [ - { - id: "1", - date: "2024-02-25", - reason: "添加好友过于频繁", - recoveryTime: "2024-02-26", - type: "friend_limit", - }, - { - id: "2", - date: "2024-01-15", - reason: "营销内容违规", - recoveryTime: "2024-01-16", - type: "marketing", - }, - ], - isVerified: true, - firstMomentDate: "2021-06-15", - accountWeight: 85, - weightFactors: { - restrictionFactor: 0.8, - verificationFactor: 1.0, - ageFactor: 0.9, - activityFactor: 0.85, - }, - weeklyStats: Array.from({ length: 7 }, (_, i) => ({ - date: `Day ${i + 1}`, - friends: Math.floor(Math.random() * 50) + 50, - messages: Math.floor(Math.random() * 100) + 100, - })), - friends: friends, - } - setAccount(mockAccount) } fetchAccount() @@ -328,439 +236,580 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri const totalPages = Math.ceil(filteredFriends.length / friendsPerPage) const paginatedFriends = filteredFriends.slice((currentPage - 1) * friendsPerPage, currentPage * friendsPerPage) + // 生成模拟账号数据(作为备用,服务器请求失败时使用) + const generateMockAccountData = () => { + // 生成随机标签 + const generateRandomTags = (count: number) => { + const tagPool = [ + { name: "潜在客户", color: "bg-blue-100 text-blue-800" }, + { name: "高意向", color: "bg-green-100 text-green-800" }, + { name: "已成交", color: "bg-purple-100 text-purple-800" }, + { name: "需跟进", color: "bg-yellow-100 text-yellow-800" }, + { name: "活跃用户", color: "bg-indigo-100 text-indigo-800" }, + { name: "沉默用户", color: "bg-gray-100 text-gray-800" }, + { name: "企业客户", color: "bg-red-100 text-red-800" }, + { name: "个人用户", color: "bg-pink-100 text-pink-800" }, + { name: "新增好友", color: "bg-emerald-100 text-emerald-800" }, + { name: "老客户", color: "bg-amber-100 text-amber-800" }, + ] + + return Array.from({ length: Math.floor(Math.random() * count) + 1 }, () => { + const randomTag = tagPool[Math.floor(Math.random() * tagPool.length)] + return { + id: `tag-${Math.random().toString(36).substring(2, 9)}`, + name: randomTag.name, + color: randomTag.color, + } + }) + } + + // 生成随机好友 + const friendCount = Math.floor(Math.random() * (300 - 150)) + 150 + const generateFriends = (count: number) => { + return Array.from({ length: count }, (_, i) => { + const firstName = ["张", "王", "李", "赵", "陈", "刘", "杨", "黄", "周", "吴"][Math.floor(Math.random() * 10)] + const secondName = ["小", "大", "明", "华", "强", "伟", "芳", "娜", "秀", "英"][ + Math.floor(Math.random() * 10) + ] + const lastName = ["明", "华", "强", "伟", "芳", "娜", "秀", "英", "军", "杰"][Math.floor(Math.random() * 10)] + const nickname = firstName + secondName + lastName + + // 生成随机的添加时间(过去1年内) + const addDate = new Date() + addDate.setDate(addDate.getDate() - Math.floor(Math.random() * 365)) + + // 生成随机的最后互动时间(过去30天内) + const lastDate = new Date() + lastDate.setDate(lastDate.getDate() - Math.floor(Math.random() * 30)) + + return { + id: `friend-${i}`, + avatar: `/placeholder.svg?height=40&width=40&text=${nickname[0]}`, + nickname, + wechatId: `wxid_${Math.random().toString(36).substring(2, 9)}`, + remark: + Math.random() > 0.5 + ? `${nickname}(${["同事", "客户", "朋友", "同学"][Math.floor(Math.random() * 4)]})` + : "", + addTime: addDate.toISOString().split("T")[0], + lastInteraction: lastDate.toISOString().split("T")[0], + tags: generateRandomTags(3), + region: ["广东", "北京", "上海", "浙江", "江苏", "四川", "湖北", "福建", "山东", "河南"][ + Math.floor(Math.random() * 10) + ], + source: ["抖音", "小红书", "朋友介绍", "搜索添加", "群聊", "附近的人", "名片分享"][ + Math.floor(Math.random() * 7) + ], + notes: + Math.random() > 0.7 + ? ["对产品很感兴趣", "需要进一步跟进", "已购买过产品", "价格敏感", "需要更多信息"][ + Math.floor(Math.random() * 5) + ] + : "", + } + }) + } + + const friends = generateFriends(friendCount) + + const mockAccount: WechatAccountDetail = { + id: params.id, + avatar: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg", + nickname: "卡若-25vig", + wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, + deviceId: "device-1", + deviceName: "设备1", + friendCount: friends.length, + todayAdded: 12, + status: "normal", + lastActive: new Date().toLocaleString(), + messageCount: 1234, + activeRate: 87, + accountAge: { + years: 2, + months: 8, + }, + totalChats: 15234, + chatFrequency: 42, + restrictionRecords: [ + { + id: "1", + date: "2024-02-25", + reason: "添加好友过于频繁", + recoveryTime: "2024-02-26", + type: "friend_limit", + }, + { + id: "2", + date: "2024-01-15", + reason: "营销内容违规", + recoveryTime: "2024-01-16", + type: "marketing", + }, + ], + isVerified: true, + firstMomentDate: "2021-06-15", + accountWeight: 85, + weightFactors: { + restrictionFactor: 0.8, + verificationFactor: 1.0, + ageFactor: 0.9, + activityFactor: 0.85, + }, + weeklyStats: Array.from({ length: 7 }, (_, i) => ({ + date: `Day ${i + 1}`, + friends: Math.floor(Math.random() * 50) + 50, + messages: Math.floor(Math.random() * 100) + 100, + })), + friends: friends, + } + return mockAccount + } + return ( -
-
-
- -

账号详情

-
-
- -
- -
-
- - - {account.nickname[0]} - - {account.isVerified && ( - - 已认证 - - )} -
-
-
-

{account.nickname}

- - {account.status === "normal" ? "正常" : "异常"} - -
-

微信号:{account.wechatId}

-
- - -
-
+ {isLoading ? ( +
+ +
+ ) : account ? ( +
+
+
+ +

账号详情

- +
- - - 账号概览 - 好友列表 ({account.friendCount}) - - - - {/* 账号基础信息 */} -
- -
- - 账号年龄 -
-
{formatAccountAge(account.accountAge)}
-
注册时间:{account.firstMomentDate}
-
- - -
- - 活跃程度 -
-
{account.chatFrequency}次/天
-
总聊天数:{account.totalChats.toLocaleString()}
-
-
- - {/* 账号权重评估 */} - -
-
- - 账号权重评估 -
-
- {account.accountWeight} - -
-
-

{getWeightDescription(account.accountWeight)}

-
-
- 账号年龄 - - {(account.weightFactors.ageFactor * 100).toFixed(0)}% -
-
- 活跃度 - - {(account.weightFactors.activityFactor * 100).toFixed(0)}% -
-
- 限制影响 - - {(account.weightFactors.restrictionFactor * 100).toFixed(0)}% -
-
- 实名认证 - - {(account.weightFactors.verificationFactor * 100).toFixed(0)}% -
-
-
- - {/* 添加好友统计 */} - -
-
- - 添加好友统计 -
- - - - - -

根据账号权重计算每日可添加好友数量

-
-
-
-
-
- 今日已添加 - {account.todayAdded} -
-
-
- 添加进度 - - {account.todayAdded}/{calculateMaxDailyAdds(account.accountWeight)} - -
- -
-
- 根据当前账号权重({account.accountWeight}分),每日最多可添加{" "} - {calculateMaxDailyAdds(account.accountWeight)}{" "} - 个好友 -
-
-
- - {/* 限制记录 */} - -
-
- - 限制记录 -
- setShowRestrictions(true)}> - 共 {account.restrictionRecords.length} 次 - -
-
- {account.restrictionRecords.slice(0, 2).map((record) => ( -
-
- {record.reason} - {record.date} -
-
- ))} -
-
-
- - - -
- {/* 搜索栏 */} -
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
- -
- - {/* 好友列表 */} -
- {paginatedFriends.length === 0 ? ( -
未找到匹配的好友
- ) : ( - paginatedFriends.map((friend) => ( -
handleFriendClick(friend)} - > - - - {friend.nickname[0]} - -
-
-
- {friend.nickname} - {friend.remark && ({friend.remark})} -
- -
-
{friend.wechatId}
-
- {friend.tags.slice(0, 3).map((tag) => ( - - {tag.name} - - ))} - {friend.tags.length > 3 && ( - - +{friend.tags.length - 3} - - )} -
-
-
- )) - )} -
- - {/* 分页 */} - {totalPages > 1 && ( - - - - { - e.preventDefault() - setCurrentPage((prev) => Math.max(1, prev - 1)) - }} - /> - - {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - let pageNumber - if (totalPages <= 5) { - pageNumber = i + 1 - } else if (currentPage <= 3) { - pageNumber = i + 1 - } else if (currentPage >= totalPages - 2) { - pageNumber = totalPages - 4 + i - } else { - pageNumber = currentPage - 2 + i - } - return ( - - { - e.preventDefault() - setCurrentPage(pageNumber) - }} - > - {pageNumber} - - - ) - })} - - { - e.preventDefault() - setCurrentPage((prev) => Math.min(totalPages, prev + 1)) - }} - /> - - - - )} -
-
-
-
- - {/* 限制记录详情弹窗 */} - - - - 限制记录详情 - 每次限制恢复时间为24小时 - - -
- {account.restrictionRecords.map((record) => ( -
-
-
{record.reason}
- {record.date} -
-
恢复时间:{record.recoveryTime}
-
- ))} -
-
-
-
- - {/* 好友转移确认弹窗 */} - - - - 好友转移确认 - 即将导出该微信号的好友列表,用于创建新的获客计划 - -
-
- +
+ +
+
+ {account.nickname[0]} -
-
{account.nickname}
-
{account.wechatId}
-
+ {account.isVerified && ( + + 已认证 + + )}
-
-

• 将导出该账号下的所有好友信息

-

• 好友信息将用于创建新的订单获客计划

-

• 导出过程中请勿关闭页面

+
+
+

{account.nickname}

+ + {account.status === "normal" ? "正常" : "异常"} + +
+

微信号:{account.wechatId}

+
+ + +
- - - - - -
+ - {/* 好友详情弹窗 */} - - - - 好友详情 - - {selectedFriend && ( -
-
- - - {selectedFriend.nickname[0]} - + + + 账号概览 + 好友列表 ({account.friendCount}) + + + + {/* 账号基础信息 */} +
+ +
+ + 账号年龄 +
+
{formatAccountAge(account.accountAge)}
+
注册时间:{account.firstMomentDate}
+
+ + +
+ + 活跃程度 +
+
{account.chatFrequency}次/天
+
总聊天数:{account.totalChats.toLocaleString()}
+
+
+ + {/* 账号权重评估 */} + +
+
+ + 账号权重评估 +
+
+ {account.accountWeight} + +
+
+

{getWeightDescription(account.accountWeight)}

+
+
+ 账号年龄 + + {(account.weightFactors.ageFactor * 100).toFixed(0)}% +
+
+ 活跃度 + + {(account.weightFactors.activityFactor * 100).toFixed(0)}% +
+
+ 限制影响 + + {(account.weightFactors.restrictionFactor * 100).toFixed(0)}% +
+
+ 实名认证 + + {(account.weightFactors.verificationFactor * 100).toFixed(0)}% +
+
+
+ + {/* 添加好友统计 */} + +
+
+ + 添加好友统计 +
+ + + + + +

根据账号权重计算每日可添加好友数量

+
+
+
+
+
+ 今日已添加 + {account.todayAdded} +
-
{selectedFriend.nickname}
-
{selectedFriend.wechatId}
- {selectedFriend.remark && ( -
备注: {selectedFriend.remark}
+
+ 添加进度 + + {account.todayAdded}/{calculateMaxDailyAdds(account.accountWeight)} + +
+ +
+
+ 根据当前账号权重({account.accountWeight}分),每日最多可添加{" "} + {calculateMaxDailyAdds(account.accountWeight)}{" "} + 个好友 +
+
+
+ + {/* 限制记录 */} + +
+
+ + 限制记录 +
+ setShowRestrictions(true)}> + 共 {account.restrictionRecords.length} 次 + +
+
+ {account.restrictionRecords.slice(0, 2).map((record) => ( +
+
+ {record.reason} + {record.date} +
+
+ ))} +
+
+
+ + + +
+ {/* 搜索栏 */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+ + {/* 好友列表 */} +
+ {paginatedFriends.length === 0 ? ( +
未找到匹配的好友
+ ) : ( + paginatedFriends.map((friend) => ( +
handleFriendClick(friend)} + > + + + {friend.nickname[0]} + +
+
+
+ {friend.nickname} + {friend.remark && ({friend.remark})} +
+ +
+
{friend.wechatId}
+
+ {friend.tags.slice(0, 3).map((tag) => ( + + {tag.name} + + ))} + {friend.tags.length > 3 && ( + + +{friend.tags.length - 3} + + )} +
+
+
+ )) )}
-
-
-
-
添加时间
-
{selectedFriend.addTime}
-
-
-
最近互动
-
{selectedFriend.lastInteraction}
-
-
-
地区
-
{selectedFriend.region}
-
-
-
来源
-
{selectedFriend.source}
+ {/* 分页 */} + {totalPages > 1 && ( + + + + { + e.preventDefault() + setCurrentPage((prev) => Math.max(1, prev - 1)) + }} + /> + + {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNumber + if (totalPages <= 5) { + pageNumber = i + 1 + } else if (currentPage <= 3) { + pageNumber = i + 1 + } else if (currentPage >= totalPages - 2) { + pageNumber = totalPages - 4 + i + } else { + pageNumber = currentPage - 2 + i + } + return ( + + { + e.preventDefault() + setCurrentPage(pageNumber) + }} + > + {pageNumber} + + + ) + })} + + { + e.preventDefault() + setCurrentPage((prev) => Math.min(totalPages, prev + 1)) + }} + /> + + + + )} +
+ + + + + {/* 限制记录详情弹窗 */} + + + + 限制记录详情 + 每次限制恢复时间为24小时 + + +
+ {account.restrictionRecords.map((record) => ( +
+
+
{record.reason}
+ {record.date} +
+
恢复时间:{record.recoveryTime}
+
+ ))} +
+
+
+
+ + {/* 好友转移确认弹窗 */} + + + + 好友转移确认 + 即将导出该微信号的好友列表,用于创建新的获客计划 + +
+
+ + + {account.nickname[0]} + +
+
{account.nickname}
+
{account.wechatId}
- -
-
- - 标签 -
-
- {selectedFriend.tags.map((tag) => ( - - {tag.name} - - ))} - {selectedFriend.tags.length === 0 && 暂无标签} -
-
- - {selectedFriend.notes && ( -
-
备注信息
-
{selectedFriend.notes}
-
- )} - -
- - +
+

• 将导出该账号下的所有好友信息

+

• 好友信息将用于创建新的订单获客计划

+

• 导出过程中请勿关闭页面

- )} - -
+ + + + + +
+ + {/* 好友详情弹窗 */} + + + + 好友详情 + + {selectedFriend && ( +
+
+ + + {selectedFriend.nickname[0]} + +
+
{selectedFriend.nickname}
+
{selectedFriend.wechatId}
+ {selectedFriend.remark && ( +
备注: {selectedFriend.remark}
+ )} +
+
+ +
+
+
添加时间
+
{selectedFriend.addTime}
+
+
+
最近互动
+
{selectedFriend.lastInteraction}
+
+
+
地区
+
{selectedFriend.region}
+
+
+
来源
+
{selectedFriend.source}
+
+
+ +
+
+ + 标签 +
+
+ {selectedFriend.tags.map((tag) => ( + + {tag.name} + + ))} + {selectedFriend.tags.length === 0 && 暂无标签} +
+
+ + {selectedFriend.notes && ( +
+
备注信息
+
{selectedFriend.notes}
+
+ )} + +
+ + +
+
+ )} +
+
+
-
+ ) : ( +
+

未找到账号信息

+
+ )} ) } From ae18368a171a865b8ec72b5fd71537c2271d9585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Wed, 2 Apr 2025 09:45:04 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E4=B8=BB=E4=BD=93?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=9C=80=E5=B0=8F=E8=BE=B9=E6=A1=86=E9=99=90?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E5=8E=BB=E9=99=A4=E9=A1=B5=E9=9D=A2=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E8=BE=B9=E8=B7=9D=E5=AF=BC=E8=87=B4=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E8=BF=87=E7=AA=84=E9=A1=B5=E9=9D=A2=E6=8D=A2=E8=A1=8C=E5=8F=98?= =?UTF-8?q?=E5=BD=A2=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/app/clientLayout.tsx | 12 +++++++----- Cunkebao/app/components/BottomNav.tsx | 2 +- Cunkebao/app/components/LayoutWrapper.tsx | 12 +++++++----- Cunkebao/app/layout.tsx | 6 +----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cunkebao/app/clientLayout.tsx b/Cunkebao/app/clientLayout.tsx index 512d186b..cb105ad8 100644 --- a/Cunkebao/app/clientLayout.tsx +++ b/Cunkebao/app/clientLayout.tsx @@ -18,11 +18,13 @@ function LayoutContent({ children }: { children: React.ReactNode }) { pathname === "/" || pathname === "/devices" || pathname === "/content" || pathname === "/profile" return ( -
- {children} - {showBottomNav && } - {showBottomNav && } -
+
+
+ {children} + {showBottomNav && } + {showBottomNav && } +
+
) } diff --git a/Cunkebao/app/components/BottomNav.tsx b/Cunkebao/app/components/BottomNav.tsx index cc223551..96938505 100644 --- a/Cunkebao/app/components/BottomNav.tsx +++ b/Cunkebao/app/components/BottomNav.tsx @@ -16,7 +16,7 @@ export default function BottomNav() { return (