feat: 本次提交更新内容如下
记录列表搞好了
This commit is contained in:
@@ -72,10 +72,21 @@ export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
|
|||||||
export async function fetchLikeRecords(
|
export async function fetchLikeRecords(
|
||||||
workbenchId: string,
|
workbenchId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20,
|
||||||
|
keyword?: string
|
||||||
): Promise<PaginatedResponse<LikeRecord>> {
|
): Promise<PaginatedResponse<LikeRecord>> {
|
||||||
try {
|
try {
|
||||||
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?workbenchId=${workbenchId}&page=${page}&limit=${limit}`);
|
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) {
|
if (res.code === 200 && res.data) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { requestInterceptor, responseInterceptor, errorInterceptor } from './int
|
|||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com',
|
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com',
|
||||||
timeout: 10000,
|
timeout: 20000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const NO_BOTTOM_NAV_PATHS = [
|
|||||||
'/workspace/auto-group/',
|
'/workspace/auto-group/',
|
||||||
'/workspace/moments-sync/',
|
'/workspace/moments-sync/',
|
||||||
'/workspace/traffic-distribution/',
|
'/workspace/traffic-distribution/',
|
||||||
|
'/workspace/auto-like',
|
||||||
'/404',
|
'/404',
|
||||||
'/500'
|
'/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,
|
Search,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
ChevronDown,
|
ChevronLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
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 justify-between p-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||||
<ChevronDown className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-lg font-medium">自动点赞</h1>
|
<h1 className="text-lg font-medium">自动点赞</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,453 +1,246 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Edit,
|
|
||||||
Trash2,
|
|
||||||
Copy,
|
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
Settings,
|
|
||||||
Calendar,
|
|
||||||
Eye,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Avatar } from '@/components/ui/avatar';
|
||||||
import {
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
DropdownMenu,
|
import { Separator } from '@/components/ui/separator';
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import Layout from '@/components/Layout';
|
import Layout from '@/components/Layout';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import BottomNav from '@/components/BottomNav';
|
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import '@/components/Layout.css';
|
import '@/components/Layout.css';
|
||||||
import {
|
import {
|
||||||
fetchAutoLikeTaskDetail,
|
|
||||||
toggleAutoLikeTask,
|
|
||||||
deleteAutoLikeTask,
|
|
||||||
copyAutoLikeTask,
|
|
||||||
fetchLikeRecords,
|
fetchLikeRecords,
|
||||||
LikeTask,
|
|
||||||
LikeRecord,
|
LikeRecord,
|
||||||
} from '@/api/autoLike';
|
} 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() {
|
export default function AutoLikeDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [task, setTask] = useState<LikeTask | null>(null);
|
|
||||||
const [records, setRecords] = useState<LikeRecord[]>([]);
|
const [records, setRecords] = useState<LikeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
const fetchTaskDetail = useCallback(async () => {
|
const fetchRecords = useCallback(async (page: number = 1, keyword?: string) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
setRecordsLoading(true);
|
||||||
try {
|
try {
|
||||||
const taskData = await fetchAutoLikeTaskDetail(id);
|
const response = await fetchLikeRecords(id, page, pageSize, keyword);
|
||||||
if (taskData) {
|
setRecords(response.list || []);
|
||||||
setTask(taskData);
|
setTotal(response.total || 0);
|
||||||
} else {
|
setCurrentPage(page);
|
||||||
toast({
|
|
||||||
title: '任务不存在',
|
|
||||||
description: '该任务可能已被删除',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
navigate('/workspace/auto-like');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取任务详情失败:', error);
|
console.error('获取点赞记录失败:', error);
|
||||||
toast({
|
toast({
|
||||||
title: '获取失败',
|
title: '获取点赞记录失败',
|
||||||
description: '请稍后重试',
|
description: '请稍后重试',
|
||||||
variant: 'destructive',
|
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 {
|
} finally {
|
||||||
setRecordsLoading(false);
|
setRecordsLoading(false);
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id, toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchTaskDetail();
|
fetchRecords(1);
|
||||||
fetchRecords();
|
|
||||||
}
|
}
|
||||||
}, [id, fetchTaskDetail, fetchRecords]);
|
}, [id, fetchRecords]); // 加上 fetchRecords 依赖
|
||||||
|
|
||||||
const handleToggleStatus = async () => {
|
const handleSearch = () => {
|
||||||
if (!task) return;
|
setCurrentPage(1);
|
||||||
|
fetchRecords(1, searchTerm);
|
||||||
// 先更新本地状态
|
|
||||||
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 () => {
|
const handleRefresh = () => {
|
||||||
if (!task || !window.confirm('确定要删除该任务吗?')) return;
|
fetchRecords(currentPage, searchTerm);
|
||||||
|
|
||||||
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 () => {
|
const handlePageChange = (newPage: number) => {
|
||||||
if (!task) return;
|
fetchRecords(newPage, searchTerm);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="任务详情"
|
title="点赞记录"
|
||||||
defaultBackPath="/workspace/auto-like"
|
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="bg-gray-50 min-h-screen pb-20">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{/* 任务基本信息 */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center space-x-2">
|
||||||
<CardTitle className="flex items-center">
|
<div className="relative flex-1">
|
||||||
<ThumbsUp className="h-5 w-5 mr-2" />
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
{task.name}
|
<Input
|
||||||
</CardTitle>
|
placeholder="搜索好友昵称或内容"
|
||||||
<div className="flex items-center space-x-2">
|
className="pl-9"
|
||||||
<Badge className={getStatusColor(Number(task.status))}>
|
value={searchTerm}
|
||||||
{getStatusText(Number(task.status))}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
</Badge>
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
<Switch
|
|
||||||
checked={Number(task.status) === 1}
|
|
||||||
onCheckedChange={handleToggleStatus}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={recordsLoading}>
|
||||||
</CardHeader>
|
<RefreshCw className={`h-4 w-4 ${recordsLoading ? 'animate-spin' : ''}`} />
|
||||||
<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>
|
</Button>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface LikeRecord {
|
|||||||
wechatFriendId: string;
|
wechatFriendId: string;
|
||||||
likeTime: string;
|
likeTime: string;
|
||||||
content: string;
|
content: string;
|
||||||
resUrls: string;
|
resUrls: string[];
|
||||||
momentTime: string;
|
momentTime: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
operatorName: string;
|
operatorName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user