feat: 本次提交更新内容如下
记录列表搞好了
This commit is contained in:
@@ -1,90 +1,101 @@
|
||||
import { get, post, del } from './request';
|
||||
import {
|
||||
LikeTask,
|
||||
CreateLikeTaskData,
|
||||
UpdateLikeTaskData,
|
||||
LikeRecord,
|
||||
ApiResponse,
|
||||
PaginatedResponse
|
||||
} from '@/types/auto-like';
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
||||
try {
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data.list || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取自动点赞任务失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个任务详情
|
||||
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
|
||||
try {
|
||||
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建自动点赞任务
|
||||
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/create', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 更新自动点赞任务
|
||||
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 删除自动点赞任务
|
||||
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return del('/v1/workbench/delete', { params: { id } });
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update-status', { id, status });
|
||||
}
|
||||
|
||||
// 复制自动点赞任务
|
||||
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/copy', { id });
|
||||
}
|
||||
|
||||
// 获取点赞记录
|
||||
export async function fetchLikeRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20
|
||||
): Promise<PaginatedResponse<LikeRecord>> {
|
||||
try {
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?workbenchId=${workbenchId}&page=${page}&limit=${limit}`);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return { list: [], total: 0, page, limit };
|
||||
} catch (error) {
|
||||
console.error('获取点赞记录失败:', error);
|
||||
return { list: [], total: 0, page, limit };
|
||||
}
|
||||
}
|
||||
|
||||
import { get, post, del } from './request';
|
||||
import {
|
||||
LikeTask,
|
||||
CreateLikeTaskData,
|
||||
UpdateLikeTaskData,
|
||||
LikeRecord,
|
||||
ApiResponse,
|
||||
PaginatedResponse
|
||||
} from '@/types/auto-like';
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
||||
try {
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data.list || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取自动点赞任务失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个任务详情
|
||||
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
|
||||
try {
|
||||
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建自动点赞任务
|
||||
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/create', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 更新自动点赞任务
|
||||
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 删除自动点赞任务
|
||||
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return del('/v1/workbench/delete', { params: { id } });
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update-status', { id, status });
|
||||
}
|
||||
|
||||
// 复制自动点赞任务
|
||||
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/copy', { id });
|
||||
}
|
||||
|
||||
// 获取点赞记录
|
||||
export async function fetchLikeRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
keyword?: string
|
||||
): Promise<PaginatedResponse<LikeRecord>> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
workbenchId,
|
||||
page: page.toString(),
|
||||
limit: limit.toString()
|
||||
});
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?${params.toString()}`);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return { list: [], total: 0, page, limit };
|
||||
} catch (error) {
|
||||
console.error('获取点赞记录失败:', error);
|
||||
return { list: [], total: 0, page, limit };
|
||||
}
|
||||
}
|
||||
|
||||
export type { LikeTask, LikeRecord, CreateLikeTaskData };
|
||||
@@ -4,7 +4,7 @@ import { requestInterceptor, responseInterceptor, errorInterceptor } from './int
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com',
|
||||
timeout: 10000,
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ const NO_BOTTOM_NAV_PATHS = [
|
||||
'/workspace/auto-group/',
|
||||
'/workspace/moments-sync/',
|
||||
'/workspace/traffic-distribution/',
|
||||
'/workspace/auto-like',
|
||||
'/404',
|
||||
'/500'
|
||||
];
|
||||
|
||||
28
nkebao/src/components/ui/separator.tsx
Normal file
28
nkebao/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import { cn } from "../../utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
15
nkebao/src/components/ui/skeleton.tsx
Normal file
15
nkebao/src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "../../utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Search,
|
||||
RefreshCw,
|
||||
ThumbsUp,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
} from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -209,7 +209,7 @@ export default function AutoLike() {
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">自动点赞</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,455 +1,248 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Edit,
|
||||
Trash2,
|
||||
Copy,
|
||||
ThumbsUp,
|
||||
Settings,
|
||||
Calendar,
|
||||
Eye,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import '@/components/Layout.css';
|
||||
import {
|
||||
fetchAutoLikeTaskDetail,
|
||||
toggleAutoLikeTask,
|
||||
deleteAutoLikeTask,
|
||||
copyAutoLikeTask,
|
||||
fetchLikeRecords,
|
||||
LikeTask,
|
||||
LikeRecord,
|
||||
} from '@/api/autoLike';
|
||||
|
||||
export default function AutoLikeDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [task, setTask] = useState<LikeTask | null>(null);
|
||||
const [records, setRecords] = useState<LikeRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||
|
||||
const fetchTaskDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const taskData = await fetchAutoLikeTaskDetail(id);
|
||||
if (taskData) {
|
||||
setTask(taskData);
|
||||
} else {
|
||||
toast({
|
||||
title: '任务不存在',
|
||||
description: '该任务可能已被删除',
|
||||
variant: 'destructive',
|
||||
});
|
||||
navigate('/workspace/auto-like');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
toast({
|
||||
title: '获取失败',
|
||||
description: '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id, toast, navigate]);
|
||||
|
||||
const fetchRecords = useCallback(async () => {
|
||||
if (!id) return;
|
||||
|
||||
setRecordsLoading(true);
|
||||
try {
|
||||
const response = await fetchLikeRecords(id, 1, 20);
|
||||
setRecords(response.list || []);
|
||||
} catch (error) {
|
||||
console.error('获取点赞记录失败:', error);
|
||||
} finally {
|
||||
setRecordsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
fetchRecords();
|
||||
}
|
||||
}, [id, fetchTaskDetail, fetchRecords]);
|
||||
|
||||
const handleToggleStatus = async () => {
|
||||
if (!task) return;
|
||||
|
||||
// 先更新本地状态
|
||||
const newStatus = (Number(task.status) === 1 ? 2 : 1) as 1 | 2;
|
||||
const originalStatus = Number(task.status) as 1 | 2;
|
||||
setTask(prev => prev ? { ...prev, status: newStatus } : null);
|
||||
|
||||
try {
|
||||
const response = await toggleAutoLikeTask(task.id, String(newStatus));
|
||||
|
||||
if (response.code === 200) {
|
||||
toast({ title: '操作成功' });
|
||||
// 成功时保持本地状态,不重新获取数据
|
||||
} else {
|
||||
// 请求失败,回退本地状态
|
||||
setTask(prev => prev ? { ...prev, status: originalStatus } : null);
|
||||
toast({
|
||||
title: '操作失败',
|
||||
description: response.msg || '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
// 请求异常,回退本地状态
|
||||
setTask(prev => prev ? { ...prev, status: originalStatus } : null);
|
||||
toast({
|
||||
title: '操作失败',
|
||||
description: '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!task || !window.confirm('确定要删除该任务吗?')) return;
|
||||
|
||||
try {
|
||||
const response = await deleteAutoLikeTask(task.id);
|
||||
if (response.code === 200) {
|
||||
toast({ title: '删除成功' });
|
||||
navigate('/workspace/auto-like');
|
||||
} else {
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: response.msg || '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const response = await copyAutoLikeTask(task.id);
|
||||
if (response.code === 200) {
|
||||
toast({ title: '复制成功' });
|
||||
navigate('/workspace/auto-like');
|
||||
} else {
|
||||
toast({
|
||||
title: '复制失败',
|
||||
description: response.msg || '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
toast({
|
||||
title: '复制失败',
|
||||
description: '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 2:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '进行中';
|
||||
case 2:
|
||||
return '已暂停';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<PageHeader title="任务详情" defaultBackPath="/workspace/auto-like" />}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4">
|
||||
<Card className="p-8 text-center">
|
||||
<RefreshCw className="h-12 w-12 text-gray-300 mx-auto mb-3 animate-spin" />
|
||||
<p className="text-gray-500 text-lg font-medium mb-2">加载中...</p>
|
||||
<p className="text-gray-400 text-sm">请稍候,正在获取任务详情</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title="任务详情"
|
||||
defaultBackPath="/workspace/auto-like"
|
||||
rightContent={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
操作
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => navigate(`/workspace/auto-like/${task.id}/edit`)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 任务基本信息 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center">
|
||||
<ThumbsUp className="h-5 w-5 mr-2" />
|
||||
{task.name}
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={getStatusColor(Number(task.status))}>
|
||||
{getStatusText(Number(task.status))}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={Number(task.status) === 1}
|
||||
onCheckedChange={handleToggleStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">创建时间</div>
|
||||
<div className="font-medium">{task.createTime}</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">创建人</div>
|
||||
<div className="font-medium">{task.creator}</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">执行设备</div>
|
||||
<div className="font-medium">{task.deviceCount} 个</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">目标人群</div>
|
||||
<div className="font-medium">{task.targetGroup}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 执行进度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Calendar className="h-5 w-5 mr-2" />
|
||||
执行进度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">今日已点赞</span>
|
||||
<span className="font-medium">
|
||||
{task.todayLikeCount} / {task.maxLikesPerDay}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(task.todayLikeCount / task.maxLikesPerDay) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-500">总点赞数</div>
|
||||
<div className="font-medium">{task.totalLikeCount} 次</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">上次点赞</div>
|
||||
<div className="font-medium">{task.lastLikeTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 任务配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Settings className="h-5 w-5 mr-2" />
|
||||
任务配置
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">点赞间隔</div>
|
||||
<div className="font-medium">{task.likeInterval} 秒</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">每日最大点赞数</div>
|
||||
<div className="font-medium">{task.maxLikesPerDay} 次</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">单个好友最大点赞数</div>
|
||||
<div className="font-medium">{task.friendMaxLikes} 次</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-500">执行时间段</div>
|
||||
<div className="font-medium">
|
||||
{task.timeRange.start} - {task.timeRange.end}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-500">内容类型</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.contentTypes.map((type) => (
|
||||
<Badge key={type} variant="outline">
|
||||
{type === 'text' ? '文字' : type === 'image' ? '图片' : type === 'video' ? '视频' : '链接'}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{task.targetTags.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-500">目标标签</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.targetTags.map((tag) => (
|
||||
<Badge key={tag} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.enableFriendTags && task.friendTags && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-500">好友标签</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">{task.friendTags}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 点赞记录 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center">
|
||||
<Eye className="h-5 w-5 mr-2" />
|
||||
点赞记录
|
||||
</CardTitle>
|
||||
<Button variant="outline" size="sm" onClick={fetchRecords}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recordsLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<RefreshCw className="h-6 w-6 text-gray-400 mx-auto animate-spin" />
|
||||
<p className="text-gray-500 text-sm mt-2">加载中...</p>
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无点赞记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{records.slice(0, 10).map((record) => (
|
||||
<div key={record.id} className="flex items-center space-x-3 p-3 border rounded-lg">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<ThumbsUp className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium truncate">
|
||||
{record.friendName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{record.content}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{record.likeTime}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
ThumbsUp,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Filter,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import '@/components/Layout.css';
|
||||
import {
|
||||
fetchLikeRecords,
|
||||
LikeRecord,
|
||||
} from '@/api/autoLike';
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AutoLikeDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { toast } = useToast();
|
||||
const [records, setRecords] = useState<LikeRecord[]>([]);
|
||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
const fetchRecords = useCallback(async (page: number = 1, keyword?: string) => {
|
||||
if (!id) return;
|
||||
setRecordsLoading(true);
|
||||
try {
|
||||
const response = await fetchLikeRecords(id, page, pageSize, keyword);
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error('获取点赞记录失败:', error);
|
||||
toast({
|
||||
title: '获取点赞记录失败',
|
||||
description: '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setRecordsLoading(false);
|
||||
}
|
||||
}, [id, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchRecords(1);
|
||||
}
|
||||
}, [id, fetchRecords]); // 加上 fetchRecords 依赖
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchRecords(1, searchTerm);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchRecords(currentPage, searchTerm);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
fetchRecords(newPage, searchTerm);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title="点赞记录"
|
||||
defaultBackPath="/workspace/auto-like"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4 space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索好友昵称或内容"
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={recordsLoading}>
|
||||
<RefreshCw className={`h-4 w-4 ${recordsLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{recordsLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<div className="flex space-x-2 mt-3">
|
||||
<Skeleton className="h-20 w-20" />
|
||||
<Skeleton className="h-20 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无点赞记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{records.map((record) => (
|
||||
<div key={record.id} className="p-4 mb-4 bg-white rounded-2xl shadow-sm">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3 max-w-[65%]">
|
||||
<Avatar>
|
||||
<img
|
||||
src={record.friendAvatar || "https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"}
|
||||
alt={record.friendName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate" title={record.friendName}>
|
||||
{record.friendName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">内容发布者</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-blue-50 whitespace-nowrap shrink-0">
|
||||
{formatDate(record.momentTime || record.likeTime)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="mb-3">
|
||||
{record.content && (
|
||||
<p className="text-gray-700 mb-3 whitespace-pre-line">
|
||||
{record.content}
|
||||
</p>
|
||||
)}
|
||||
{Array.isArray(record.resUrls) && record.resUrls.length > 0 && (
|
||||
<div className={`grid gap-2 ${
|
||||
record.resUrls.length === 1 ? "grid-cols-1" :
|
||||
record.resUrls.length === 2 ? "grid-cols-2" :
|
||||
record.resUrls.length <= 3 ? "grid-cols-3" :
|
||||
record.resUrls.length <= 6 ? "grid-cols-3 grid-rows-2" :
|
||||
"grid-cols-3 grid-rows-3"
|
||||
}`}>
|
||||
{record.resUrls.slice(0, 9).map((image: string, idx: number) => (
|
||||
<div key={idx} className="relative aspect-square rounded-md overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={`内容图片 ${idx + 1}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center mt-4 p-2 bg-gray-50 rounded-md">
|
||||
<Avatar className="h-8 w-8 mr-2 shrink-0">
|
||||
<img
|
||||
src={record.operatorAvatar || "https://api.dicebear.com/7.x/avataaars/svg?seed=operator"}
|
||||
alt={record.operatorName}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</Avatar>
|
||||
<div className="text-sm min-w-0">
|
||||
<span className="font-medium truncate inline-block max-w-full" title={record.operatorName}>
|
||||
{record.operatorName}
|
||||
</span>
|
||||
<span className="text-gray-500 ml-2">点赞了这条内容</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{/* 分页 */}
|
||||
{records.length > 0 && total > pageSize && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className="mx-1"
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span className="mx-4 py-2 text-sm text-gray-500">
|
||||
第 {currentPage} 页,共 {Math.ceil(total / pageSize)} 页
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage >= Math.ceil(total / pageSize)}
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className="mx-1"
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +1,118 @@
|
||||
// 自动点赞任务状态
|
||||
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
|
||||
|
||||
// 内容类型
|
||||
export type ContentType = 'text' | 'image' | 'video' | 'link';
|
||||
|
||||
// 设备信息
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'online' | 'offline';
|
||||
lastActive: string;
|
||||
}
|
||||
|
||||
// 好友信息
|
||||
export interface Friend {
|
||||
id: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
avatar: string;
|
||||
tags: string[];
|
||||
region: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
// 点赞记录
|
||||
export interface LikeRecord {
|
||||
id: string;
|
||||
workbenchId: string;
|
||||
momentsId: string;
|
||||
snsId: string;
|
||||
wechatAccountId: string;
|
||||
wechatFriendId: string;
|
||||
likeTime: string;
|
||||
content: string;
|
||||
resUrls: string;
|
||||
momentTime: string;
|
||||
userName: string;
|
||||
operatorName: string;
|
||||
operatorAvatar: string;
|
||||
friendName: string;
|
||||
friendAvatar: string;
|
||||
}
|
||||
|
||||
// 自动点赞任务
|
||||
export interface LikeTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: LikeTaskStatus;
|
||||
deviceCount: number;
|
||||
targetGroup: string;
|
||||
likeCount: number;
|
||||
lastLikeTime: string;
|
||||
createTime: string;
|
||||
creator: string;
|
||||
likeInterval: number;
|
||||
maxLikesPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
contentTypes: ContentType[];
|
||||
targetTags: string[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
todayLikeCount: number;
|
||||
totalLikeCount: number;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
// 创建任务数据
|
||||
export interface CreateLikeTaskData {
|
||||
name: string;
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends?: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags?: string;
|
||||
enableFriendTags: boolean;
|
||||
targetTags: string[];
|
||||
}
|
||||
|
||||
// 更新任务数据
|
||||
export interface UpdateLikeTaskData extends CreateLikeTaskData {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 任务配置
|
||||
export interface TaskConfig {
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
}
|
||||
|
||||
// API 响应格式
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
// 自动点赞任务状态
|
||||
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
|
||||
|
||||
// 内容类型
|
||||
export type ContentType = 'text' | 'image' | 'video' | 'link';
|
||||
|
||||
// 设备信息
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'online' | 'offline';
|
||||
lastActive: string;
|
||||
}
|
||||
|
||||
// 好友信息
|
||||
export interface Friend {
|
||||
id: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
avatar: string;
|
||||
tags: string[];
|
||||
region: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
// 点赞记录
|
||||
export interface LikeRecord {
|
||||
id: string;
|
||||
workbenchId: string;
|
||||
momentsId: string;
|
||||
snsId: string;
|
||||
wechatAccountId: string;
|
||||
wechatFriendId: string;
|
||||
likeTime: string;
|
||||
content: string;
|
||||
resUrls: string[];
|
||||
momentTime: string;
|
||||
userName: string;
|
||||
operatorName: string;
|
||||
operatorAvatar: string;
|
||||
friendName: string;
|
||||
friendAvatar: string;
|
||||
}
|
||||
|
||||
// 自动点赞任务
|
||||
export interface LikeTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: LikeTaskStatus;
|
||||
deviceCount: number;
|
||||
targetGroup: string;
|
||||
likeCount: number;
|
||||
lastLikeTime: string;
|
||||
createTime: string;
|
||||
creator: string;
|
||||
likeInterval: number;
|
||||
maxLikesPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
contentTypes: ContentType[];
|
||||
targetTags: string[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
todayLikeCount: number;
|
||||
totalLikeCount: number;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
// 创建任务数据
|
||||
export interface CreateLikeTaskData {
|
||||
name: string;
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends?: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags?: string;
|
||||
enableFriendTags: boolean;
|
||||
targetTags: string[];
|
||||
}
|
||||
|
||||
// 更新任务数据
|
||||
export interface UpdateLikeTaskData extends CreateLikeTaskData {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 任务配置
|
||||
export interface TaskConfig {
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
}
|
||||
|
||||
// API 响应格式
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
Reference in New Issue
Block a user