Merge branch 'develop' of https://gitee.com/Tyssen/yi-shi into develop
This commit is contained in:
@@ -33,6 +33,16 @@ export const fetchDeviceDetail = async (id: string | number): Promise<ApiRespons
|
||||
return api.get<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
};
|
||||
|
||||
// 获取设备关联的微信账号
|
||||
export const fetchDeviceRelatedAccounts = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||
return api.get<ApiResponse<any>>(`/v1/devices/${id}/related-accounts`);
|
||||
};
|
||||
|
||||
// 获取设备操作记录
|
||||
export const fetchDeviceHandleLogs = async (id: string | number, page: number = 1, limit: number = 10): Promise<ApiResponse<any>> => {
|
||||
return api.get<ApiResponse<any>>(`/v1/devices/${id}/handle-logs?page=${page}&limit=${limit}`);
|
||||
};
|
||||
|
||||
// 更新设备任务配置
|
||||
export const updateDeviceTaskConfig = async (
|
||||
id: string | number,
|
||||
|
||||
207
Cunkebao/api/wechat-accounts.ts
Normal file
207
Cunkebao/api/wechat-accounts.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { api } from "@/lib/api";
|
||||
import {
|
||||
ServerWechatAccountsResponse,
|
||||
QueryWechatAccountParams,
|
||||
WechatAccountDetailResponse
|
||||
} from "@/types/wechat-account";
|
||||
|
||||
/**
|
||||
* 获取微信账号列表
|
||||
* @param params 查询参数
|
||||
* @returns 微信账号列表响应
|
||||
*/
|
||||
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ServerWechatAccountsResponse> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
// 添加查询参数
|
||||
if (params.page) queryParams.append('page', params.page.toString());
|
||||
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params.keyword) queryParams.append('nickname', params.keyword); // 使用nickname作为关键词搜索参数
|
||||
if (params.sort) queryParams.append('sort', params.sort);
|
||||
if (params.order) queryParams.append('order', params.order);
|
||||
|
||||
// 发起API请求
|
||||
return api.get<ServerWechatAccountsResponse>(`/v1/device/wechats?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信账号详情
|
||||
* @param id 微信账号ID
|
||||
* @returns 微信账号详情响应
|
||||
*/
|
||||
export const fetchWechatAccountDetail = async (id: string | number): Promise<WechatAccountDetailResponse> => {
|
||||
return api.get<WechatAccountDetailResponse>(`/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() // 服务端未提供,使用当前时间
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 将服务端的微信账号详情转换为前端详情页面所需的格式
|
||||
* @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<string, "friend_limit" | "marketing" | "spam" | "other"> = {
|
||||
'friend': 'friend_limit',
|
||||
'marketing': 'marketing',
|
||||
'spam': 'spam'
|
||||
};
|
||||
|
||||
return typeMap[type] || 'other';
|
||||
};
|
||||
@@ -18,11 +18,13 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
pathname === "/" || pathname === "/devices" || pathname === "/content" || pathname === "/profile"
|
||||
|
||||
return (
|
||||
<main className="max-w-[390px] mx-auto bg-white min-h-screen flex flex-col relative">
|
||||
{children}
|
||||
{showBottomNav && <BottomNav />}
|
||||
{showBottomNav && <VideoTutorialButton />}
|
||||
</main>
|
||||
<div className="mx-auto w-full">
|
||||
<main className="w-full mx-auto bg-white min-h-screen flex flex-col relative lg:max-w-7xl xl:max-w-[1200px]">
|
||||
{children}
|
||||
{showBottomNav && <BottomNav />}
|
||||
{showBottomNav && <VideoTutorialButton />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function BottomNav() {
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200">
|
||||
<div className="max-w-md mx-auto flex justify-around">
|
||||
<div className="w-full mx-auto flex justify-around">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
|
||||
@@ -13,11 +13,13 @@ export default function LayoutWrapper({ children }: { children: React.ReactNode
|
||||
const showBottomNav = mainPages.includes(pathname)
|
||||
|
||||
return (
|
||||
<main className="max-w-[390px] mx-auto bg-white min-h-screen flex flex-col relative">
|
||||
{children}
|
||||
{showBottomNav && <BottomNav />}
|
||||
{showBottomNav && <VideoTutorialButton />}
|
||||
</main>
|
||||
<div className="mx-auto w-full">
|
||||
<main className="w-full mx-auto bg-white min-h-screen flex flex-col relative lg:max-w-7xl xl:max-w-[1200px]">
|
||||
{children}
|
||||
{showBottomNav && <BottomNav />}
|
||||
{showBottomNav && <VideoTutorialButton />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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, 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, updateDeviceTaskConfig } from "@/api/devices"
|
||||
import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig, fetchDeviceHandleLogs } 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
|
||||
}
|
||||
|
||||
@@ -62,12 +65,23 @@ 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()
|
||||
const [device, setDevice] = useState<Device | null>(null)
|
||||
const [activeTab, setActiveTab] = useState("info")
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [accountsLoading, setAccountsLoading] = useState(false)
|
||||
const [logsLoading, setLogsLoading] = useState(false)
|
||||
const [handleLogs, setHandleLogs] = useState<HandleLog[]>([])
|
||||
const [savingFeatures, setSavingFeatures] = useState({
|
||||
autoAddFriend: false,
|
||||
autoReply: false,
|
||||
@@ -146,16 +160,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 +207,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 +221,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 +256,86 @@ 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 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)
|
||||
|
||||
// 当切换到"关联账号"标签时,获取最新的关联微信账号信息
|
||||
if (value === "accounts") {
|
||||
fetchRelatedAccounts()
|
||||
}
|
||||
|
||||
// 当切换到"操作记录"标签时,获取最新的操作记录
|
||||
if (value === "history") {
|
||||
fetchHandleLogs()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理功能开关状态变化
|
||||
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
|
||||
@@ -351,7 +458,7 @@ export default function DeviceDetailPage() {
|
||||
<div className="mt-2 text-sm text-gray-500">最后活跃:{device.lastActive}</div>
|
||||
</Card>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="info">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="accounts">关联账号</TabsTrigger>
|
||||
@@ -435,8 +542,37 @@ export default function DeviceDetailPage() {
|
||||
|
||||
<TabsContent value="accounts">
|
||||
<Card className="p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-md font-medium">微信账号列表</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchRelatedAccounts}
|
||||
disabled={accountsLoading}
|
||||
>
|
||||
{accountsLoading ? (
|
||||
<>
|
||||
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
|
||||
刷新中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-300px)]">
|
||||
{device.wechatAccounts && device.wechatAccounts.length > 0 ? (
|
||||
{accountsLoading && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin mr-2"></div>
|
||||
<span className="text-gray-500">加载微信账号中...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!accountsLoading && device.wechatAccounts && device.wechatAccounts.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{device.wechatAccounts.map((account) => (
|
||||
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
@@ -448,26 +584,38 @@ export default function DeviceDetailPage() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-medium truncate">{account.nickname}</div>
|
||||
<Badge variant={getBadgeVariant(account.status)}>
|
||||
{account.status === "normal" ? "正常" : "异常"}
|
||||
<Badge variant={account.wechatAlive === 1 ? "default" : "destructive"}>
|
||||
{account.wechatAliveText}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">微信号: {account.wechatId}</div>
|
||||
<div className="text-sm text-gray-500">性别: {account.gender === "male" ? "男" : "女"}</div>
|
||||
<div className="text-sm text-gray-500">性别: {account.gender === 1 ? "男" : "女"}</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm text-gray-500">好友数: {account.friendCount}</span>
|
||||
<Badge variant={getBadgeVariant(account.addFriendStatus)}>
|
||||
{account.addFriendStatus === "enabled" ? "可加友" : "已停用"}
|
||||
<span className="text-sm text-gray-500">好友数: {account.totalFriend}</span>
|
||||
<Badge variant={account.status === 1 ? "outline" : "secondary"}>
|
||||
{account.statusText}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">最后活跃: {account.lastActive}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>此设备暂无关联的微信账号</p>
|
||||
</div>
|
||||
!accountsLoading && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>此设备暂无关联的微信账号</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={fetchRelatedAccounts}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
@@ -475,18 +623,45 @@ export default function DeviceDetailPage() {
|
||||
|
||||
<TabsContent value="history">
|
||||
<Card className="p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-md font-medium">操作记录</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchHandleLogs}
|
||||
disabled={logsLoading}
|
||||
>
|
||||
{logsLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
加载中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-300px)]">
|
||||
{device.history && device.history.length > 0 ? (
|
||||
{logsLoading && handleLogs.length === 0 ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin mr-2"></div>
|
||||
<span className="text-gray-500">加载操作记录中...</span>
|
||||
</div>
|
||||
) : handleLogs.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{device.history.map((record, index) => (
|
||||
<div key={index} className="flex items-start space-x-3">
|
||||
{handleLogs.map((log) => (
|
||||
<div key={log.id} className="flex items-start space-x-3">
|
||||
<div className="p-2 bg-blue-50 rounded-full">
|
||||
<History className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{record.action}</div>
|
||||
<div className="text-sm font-medium">{log.content}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
操作人: {record.operator} · {record.time}
|
||||
操作人: {log.username} · {log.createTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,6 +670,15 @@ export default function DeviceDetailPage() {
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>暂无操作记录</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={fetchHandleLogs}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -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<HTMLDivElement>(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() {
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">设备管理</h1>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => setIsAddDeviceOpen(true)}>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleOpenAddDeviceModal}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加设备
|
||||
</Button>
|
||||
@@ -359,22 +621,128 @@ export default function DevicesPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加设备</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">设备名称</label>
|
||||
<Input placeholder="请输入设备名称" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">IMEI</label>
|
||||
<Input placeholder="请输入设备IMEI" />
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setIsAddDeviceOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button>添加</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="scan" value={activeTab} onValueChange={setActiveTab} className="mt-4">
|
||||
<TabsList className="grid grid-cols-2 w-full">
|
||||
<TabsTrigger value="scan" className="flex items-center">
|
||||
<QrCode className="h-4 w-4 mr-2" />
|
||||
扫码添加
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual" className="flex items-center">
|
||||
<Smartphone className="h-4 w-4 mr-2" />
|
||||
手动添加
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="scan" className="space-y-4 py-4">
|
||||
<div className="flex flex-col items-center justify-center p-6 space-y-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow-md border border-gray-200 w-full max-w-[280px] min-h-[280px] flex flex-col items-center justify-center">
|
||||
{isLoadingQRCode ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-3">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="text-sm text-gray-500">正在获取二维码...</p>
|
||||
</div>
|
||||
) : qrCodeImage ? (
|
||||
<div id="qrcode-container" className="flex flex-col items-center space-y-3">
|
||||
<div className="relative w-64 h-64 flex items-center justify-center">
|
||||
<img
|
||||
src={qrCodeImage}
|
||||
alt="设备添加二维码"
|
||||
className="w-full h-full object-contain"
|
||||
onError={(e) => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="qrcode-error hidden absolute inset-0 flex flex-col items-center justify-center text-center text-red-500 bg-white">
|
||||
<AlertTriangle className="h-10 w-10 mb-2" />
|
||||
<p>未能加载二维码,请点击刷新按钮重试</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-center text-gray-600 mt-2">
|
||||
请使用手机扫描此二维码添加设备
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-500">
|
||||
<QrCode className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p>点击下方按钮获取二维码</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={fetchDeviceQRCode}
|
||||
disabled={isLoadingQRCode}
|
||||
className="w-48"
|
||||
>
|
||||
{isLoadingQRCode ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
获取中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
刷新二维码
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual" className="space-y-4 py-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">设备名称</label>
|
||||
<Input
|
||||
placeholder="请输入设备名称"
|
||||
value={deviceName}
|
||||
onChange={(e) => setDeviceName(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
为设备添加一个便于识别的名称
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">设备IMEI</label>
|
||||
<Input
|
||||
placeholder="请输入设备IMEI"
|
||||
value={deviceImei}
|
||||
onChange={(e) => setDeviceImei(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
请输入设备IMEI码,可在设备信息中查看
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setIsAddDeviceOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddDeviceByImei}
|
||||
disabled={isSubmittingImei || !deviceImei.trim()}
|
||||
>
|
||||
{isSubmittingImei ? (
|
||||
<>
|
||||
<div className="w-4 h-4 mr-2 rounded-full border-2 border-white border-t-transparent animate-spin"></div>
|
||||
提交中...
|
||||
</>
|
||||
) : "添加"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,4 @@ export default function RootLayout({
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
import './globals.css'
|
||||
}
|
||||
@@ -213,6 +213,7 @@ const aiScenarios = [
|
||||
daily: 245,
|
||||
growth: 18.5,
|
||||
},
|
||||
link: "/scenarios/ai-friend",
|
||||
plans: [
|
||||
{
|
||||
id: "ai-plan-1",
|
||||
@@ -232,6 +233,7 @@ const aiScenarios = [
|
||||
daily: 178,
|
||||
growth: 15.2,
|
||||
},
|
||||
link: "/scenarios/ai-group",
|
||||
plans: [
|
||||
{
|
||||
id: "ai-plan-2",
|
||||
@@ -250,6 +252,7 @@ const aiScenarios = [
|
||||
daily: 134,
|
||||
growth: 12.8,
|
||||
},
|
||||
link: "/scenarios/ai-conversion",
|
||||
plans: [
|
||||
{
|
||||
id: "ai-plan-3",
|
||||
@@ -296,7 +299,7 @@ export default function ScenariosPage() {
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50">
|
||||
<div className="max-w-[390px] mx-auto bg-white min-h-screen">
|
||||
<div className="w-full mx-auto bg-white min-h-screen lg:max-w-7xl xl:max-w-[1200px]">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex justify-between items-center p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -315,7 +318,7 @@ export default function ScenariosPage() {
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Traditional channels */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{channels.map((channel) => (
|
||||
<div key={channel.id} className="flex flex-col">
|
||||
<Card
|
||||
@@ -362,7 +365,7 @@ export default function ScenariosPage() {
|
||||
<h2 className="text-lg font-medium">AI智能获客</h2>
|
||||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full">Beta</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{aiScenarios.map((scenario) => (
|
||||
<div key={scenario.id} className="flex flex-col">
|
||||
<Card
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<WechatAccount[]>(generateRandomWechatAccounts(42))
|
||||
const [accounts, setAccounts] = useState<WechatAccount[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false)
|
||||
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(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<HTMLInputElement>) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{paginatedAccounts.map((account) => (
|
||||
<Card
|
||||
key={account.id}
|
||||
className="p-4 hover:shadow-lg transition-all cursor-pointer"
|
||||
onClick={() => router.push(`/wechat-accounts/${account.id}`)}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<div className="text-center py-20 text-gray-500">
|
||||
<p>暂无微信账号数据</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-4"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<Avatar className="h-12 w-12 ring-2 ring-offset-2 ring-blue-500/20">
|
||||
<AvatarImage src={account.avatar} />
|
||||
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium truncate">{account.nickname}</h3>
|
||||
<Badge variant={account.status === "normal" ? "success" : "destructive"}>
|
||||
{account.status === "normal" ? "正常" : "异常"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleTransferFriends(account)
|
||||
}}
|
||||
>
|
||||
<ArrowRightLeft className="h-4 w-4 mr-2" />
|
||||
好友转移
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-gray-500 space-y-1">
|
||||
<div>微信号:{account.wechatId}</div>
|
||||
{isRefreshing ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">
|
||||
{accounts.map((account) => (
|
||||
<Card
|
||||
key={account.id}
|
||||
className="p-4 hover:shadow-lg transition-all cursor-pointer"
|
||||
onClick={() => router.push(`/wechat-accounts/${account.id}`)}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<Avatar className="h-12 w-12 ring-2 ring-offset-2 ring-blue-500/20">
|
||||
<AvatarImage src={account.avatar} />
|
||||
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>好友数量:{account.friendCount}</div>
|
||||
<div className="text-green-600">今日新增:+{account.todayAdded}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>今日可添加:</span>
|
||||
<span className="font-medium">{account.remainingAdds}</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AlertCircle className="h-4 w-4 text-gray-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>每日最多添加 {account.maxDailyAdds} 个好友</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{account.todayAdded}/{account.maxDailyAdds}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium truncate">{account.nickname}</h3>
|
||||
<Badge variant={account.status === "normal" ? "outline" : "destructive"}>
|
||||
{account.status === "normal" ? "正常" : "异常"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Progress value={(account.todayAdded / account.maxDailyAdds) * 100} className="h-2" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleTransferFriends(account)
|
||||
}}
|
||||
>
|
||||
<ArrowRightLeft className="h-4 w-4 mr-2" />
|
||||
好友转移
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 pt-2">
|
||||
<div>所属设备:{account.deviceName}</div>
|
||||
<div>最后活跃:{account.lastActive}</div>
|
||||
<div className="mt-1 text-sm text-gray-500 space-y-1">
|
||||
<div>微信号:{account.wechatId}</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>好友数量:{account.friendCount}</div>
|
||||
<div className="text-green-600">今日新增:+{account.todayAdded}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>今日可添加:</span>
|
||||
<span className="font-medium">{account.remainingAdds}</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AlertCircle className="h-4 w-4 text-gray-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>每日最多添加 {account.maxDailyAdds} 个好友</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{account.todayAdded}/{account.maxDailyAdds}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={(account.todayAdded / account.maxDailyAdds) * 100} className="h-2" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 pt-2">
|
||||
<div>所属设备:{account.deviceName || '未知设备'}</div>
|
||||
<div>最后活跃:{account.lastActive}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage((prev) => Math.max(1, prev - 1))
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
{!isLoading && accounts.length > 0 && totalPages > 1 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
isActive={currentPage === page}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(page)
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage((prev) => prev - 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
/>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
{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 (
|
||||
<PaginationItem key={pageToShow}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={currentPage === pageToShow}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(pageToShow)
|
||||
}}
|
||||
>
|
||||
{pageToShow}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
})}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage((prev) => prev + 1)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={isTransferDialogOpen} onOpenChange={setIsTransferDialogOpen}>
|
||||
|
||||
113
Cunkebao/types/wechat-account.ts
Normal file
113
Cunkebao/types/wechat-account.ts
Normal file
@@ -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;
|
||||
}
|
||||
131
Server/application/common/model/CompanyAccount.php
Normal file
131
Server/application/common/model/CompanyAccount.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 公司账户模型类
|
||||
*/
|
||||
class CompanyAccount extends Model
|
||||
{
|
||||
/**
|
||||
* 数据表名
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'tk_company_account';
|
||||
|
||||
/**
|
||||
* 主键
|
||||
* @var string
|
||||
*/
|
||||
protected $pk = 'id';
|
||||
|
||||
/**
|
||||
* 自动写入时间戳
|
||||
* @var bool
|
||||
*/
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
/**
|
||||
* 创建时间字段
|
||||
* @var string
|
||||
*/
|
||||
protected $createTime = 'createTime';
|
||||
|
||||
/**
|
||||
* 更新时间字段
|
||||
* @var string
|
||||
*/
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
/**
|
||||
* 隐藏属性
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['passwordMd5', 'passwordLocal', 'secret'];
|
||||
|
||||
/**
|
||||
* 字段类型
|
||||
* @var array
|
||||
*/
|
||||
protected $type = [
|
||||
'id' => '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
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,8 @@ 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'); // 获取设备详情
|
||||
@@ -28,4 +30,6 @@ Route::group('v1/', function () {
|
||||
Route::put('refresh', 'app\\devices\\controller\\DeviceWechat@refresh'); // 刷新设备微信状态
|
||||
Route::post('transfer-friends', 'app\\devices\\controller\\DeviceWechat@transferFriends'); // 微信好友转移
|
||||
});
|
||||
|
||||
|
||||
})->middleware(['jwt']);
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
namespace app\devices\controller;
|
||||
|
||||
use app\devices\model\DeviceHandleLog;
|
||||
use think\Controller;
|
||||
use app\devices\model\Device as DeviceModel;
|
||||
use think\Db;
|
||||
use think\facade\Request;
|
||||
use app\common\util\JwtUtil;
|
||||
|
||||
@@ -181,12 +183,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 +273,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 +295,7 @@ class Device extends Controller
|
||||
|
||||
// 验证IMEI是否已存在
|
||||
$exists = DeviceModel::where('imei', $data['imei'])->where('isDeleted', 0)->find();
|
||||
|
||||
if ($exists) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
@@ -314,10 +305,34 @@ class Device extends Controller
|
||||
|
||||
// 设置设备公司ID
|
||||
$data['companyId'] = $userInfo['companyId'];
|
||||
|
||||
// 添加设备
|
||||
$id = DeviceModel::addDevice($data);
|
||||
|
||||
$data['id'] = time();
|
||||
|
||||
try {
|
||||
Db::startTrans();
|
||||
|
||||
// 添加设备
|
||||
$id = DeviceModel::addDevice($data);
|
||||
|
||||
// 添加设备操作记录
|
||||
DeviceHandleLog::addLog(
|
||||
[
|
||||
'imei' => $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([
|
||||
'code' => 200,
|
||||
'msg' => '添加成功',
|
||||
@@ -342,12 +357,6 @@ class Device extends Controller
|
||||
try {
|
||||
// 获取登录用户信息
|
||||
$userInfo = request()->userInfo;
|
||||
if (empty($userInfo)) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '未登录或登录已过期'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查用户权限,只有管理员可以删除设备
|
||||
if ($userInfo['isAdmin'] != 1) {
|
||||
@@ -405,6 +414,9 @@ class Device extends Controller
|
||||
{
|
||||
// 获取请求参数
|
||||
$data = $this->request->post();
|
||||
|
||||
// 获取登录用户信息
|
||||
$userInfo = request()->userInfo;
|
||||
|
||||
// 验证参数
|
||||
if (empty($data['id'])) {
|
||||
@@ -445,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,
|
||||
@@ -465,4 +516,166 @@ 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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备操作记录
|
||||
* @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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace app\devices\controller;
|
||||
|
||||
use app\common\model\CompanyAccount;
|
||||
use think\Controller;
|
||||
use app\devices\model\WechatAccount;
|
||||
use think\facade\Request;
|
||||
@@ -88,6 +89,8 @@ class DeviceWechat extends Controller
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取登录用户信息
|
||||
$userInfo = request()->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'],
|
||||
|
||||
121
Server/application/devices/model/DeviceHandleLog.php
Normal file
121
Server/application/devices/model/DeviceHandleLog.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace app\devices\model;
|
||||
|
||||
use think\Model;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 设备操作日志模型类
|
||||
*/
|
||||
class DeviceHandleLog extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'device_handle_log';
|
||||
protected $prefix = 'tk_';
|
||||
|
||||
// 设置主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = 'datetime';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = false;
|
||||
|
||||
// 定义字段类型
|
||||
protected $type = [
|
||||
'id' => '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);
|
||||
}
|
||||
}
|
||||
207
Server/application/devices/model/DeviceWechatLogin.php
Normal file
207
Server/application/devices/model/DeviceWechatLogin.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
namespace app\devices\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 设备微信登录记录模型类
|
||||
*/
|
||||
class DeviceWechatLogin extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'device_wechat_login';
|
||||
|
||||
// 设置主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createTime';
|
||||
|
||||
// 定义字段类型
|
||||
protected $type = [
|
||||
'id' => '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;
|
||||
}
|
||||
}
|
||||
324
Server/application/devices/model/FriendTask.php
Normal file
324
Server/application/devices/model/FriendTask.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
namespace app\devices\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 添加好友任务记录模型
|
||||
*/
|
||||
class FriendTask extends Model
|
||||
{
|
||||
/**
|
||||
* 数据表名
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'tk_friend_task';
|
||||
|
||||
/**
|
||||
* 主键
|
||||
* @var string
|
||||
*/
|
||||
protected $pk = 'id';
|
||||
|
||||
/**
|
||||
* 自动写入时间戳
|
||||
* @var bool
|
||||
*/
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
/**
|
||||
* 创建时间字段
|
||||
* @var string
|
||||
*/
|
||||
protected $createTime = 'createTime';
|
||||
|
||||
/**
|
||||
* 更新时间字段
|
||||
* @var string
|
||||
*/
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
/**
|
||||
* 字段类型
|
||||
* @var array
|
||||
*/
|
||||
protected $type = [
|
||||
'id' => '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;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ class WechatAccount extends Model
|
||||
'gender' => 'integer',
|
||||
'currentDeviceId' => 'integer',
|
||||
'isDeleted' => 'integer',
|
||||
'groupId' => 'integer'
|
||||
'groupId' => 'integer',
|
||||
'status' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -96,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
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user