diff --git a/nkebao/src/api/autoLike.ts b/nkebao/src/api/autoLike.ts new file mode 100644 index 00000000..8ee3f4ba --- /dev/null +++ b/nkebao/src/api/autoLike.ts @@ -0,0 +1,173 @@ +import { request } from "./request"; +import { + LikeTask, + CreateLikeTaskData, + UpdateLikeTaskData, + LikeRecord, + ApiResponse, + PaginatedResponse, +} from "@/types/auto-like"; + +// 获取自动点赞任务列表 +export async function fetchAutoLikeTasks(): Promise { + try { + const res = await request>>({ + url: "/v1/workbench/list", + method: "GET", + params: { + 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 { + try { + console.log(`Fetching task detail for id: ${id}`); + const res = await request({ + url: "/v1/workbench/detail", + method: "GET", + params: { id }, + }); + console.log("Task detail API response:", res); + + if (res.code === 200) { + if (res.data) { + if (typeof res.data === "object") { + return res.data; + } else { + console.error( + "Task detail API response data is not an object:", + res.data + ); + return null; + } + } else { + console.error("Task detail API response missing data field:", res); + return null; + } + } + + console.error("Task detail API error:", res.msg || "Unknown error"); + return null; + } catch (error) { + console.error("获取任务详情失败:", error); + return null; + } +} + +// 创建自动点赞任务 +export async function createAutoLikeTask( + data: CreateLikeTaskData +): Promise { + return request({ + url: "/v1/workbench/create", + method: "POST", + data: { + ...data, + type: 1, // 自动点赞类型 + }, + }); +} + +// 更新自动点赞任务 +export async function updateAutoLikeTask( + data: UpdateLikeTaskData +): Promise { + return request({ + url: "/v1/workbench/update", + method: "POST", + data: { + ...data, + type: 1, // 自动点赞类型 + }, + }); +} + +// 删除自动点赞任务 +export async function deleteAutoLikeTask(id: string): Promise { + return request({ + url: "/v1/workbench/delete", + method: "DELETE", + params: { id }, + }); +} + +// 切换任务状态 +export async function toggleAutoLikeTask( + id: string, + status: string +): Promise { + return request({ + url: "/v1/workbench/update-status", + method: "POST", + data: { id, status }, + }); +} + +// 复制自动点赞任务 +export async function copyAutoLikeTask(id: string): Promise { + return request({ + url: "/v1/workbench/copy", + method: "POST", + data: { id }, + }); +} + +// 获取点赞记录 +export async function fetchLikeRecords( + workbenchId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise> { + try { + const params: any = { + workbenchId, + page: page.toString(), + limit: limit.toString(), + }; + + if (keyword) { + params.keyword = keyword; + } + + const res = await request>>({ + url: "/v1/workbench/records", + method: "GET", + params, + }); + + if (res.code === 200 && res.data) { + return res.data; + } + + return { + list: [], + total: 0, + page: 1, + limit: 20, + }; + } catch (error) { + console.error("获取点赞记录失败:", error); + return { + list: [], + total: 0, + page: 1, + limit: 20, + }; + } +} diff --git a/nkebao/src/pages/workspace/auto-like/AutoLike.tsx b/nkebao/src/pages/workspace/auto-like/AutoLike.tsx deleted file mode 100644 index cc452a1c..00000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLike.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { NavBar, Button } from "antd-mobile"; -import { PlusOutlined } from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; - -const AutoLike: React.FC = () => { - return ( - window.history.back()} - left={ -
- 自动点赞 -
- } - right={ - - } - /> - } - footer={} - > -
-

自动点赞页面

-

此页面正在开发中...

-
-
- ); -}; - -export default AutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx b/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx deleted file mode 100644 index 08811261..00000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const AutoLikeDetail: React.FC = () => { - return ; -}; - -export default AutoLikeDetail; diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx b/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx deleted file mode 100644 index 12bf9b7e..00000000 --- a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { NavBar } from "antd-mobile"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; - -const NewAutoLike: React.FC = () => { - return ( - window.history.back()} - > -
- 新建自动点赞 -
- - } - footer={} - > -
-

新建自动点赞页面

-

此页面正在开发中...

-
-
- ); -}; - -export default NewAutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/list/api.ts b/nkebao/src/pages/workspace/auto-like/list/api.ts new file mode 100644 index 00000000..930902b4 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/api.ts @@ -0,0 +1,63 @@ +import request from "@/api/request"; +import { + LikeTask, + CreateLikeTaskData, + UpdateLikeTaskData, + LikeRecord, + PaginatedResponse, +} from "@/types/auto-like"; + +// 获取自动点赞任务列表 +export function fetchAutoLikeTasks( + params = { type: 1, page: 1, limit: 100 } +): Promise { + return request("/v1/workbench/list", params, "GET"); +} + +// 获取单个任务详情 +export function fetchAutoLikeTaskDetail(id: string): Promise { + return request("/v1/workbench/detail", { id }, "GET"); +} + +// 创建自动点赞任务 +export function createAutoLikeTask(data: CreateLikeTaskData): Promise { + return request("/v1/workbench/create", { ...data, type: 1 }, "POST"); +} + +// 更新自动点赞任务 +export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise { + return request("/v1/workbench/update", { ...data, type: 1 }, "POST"); +} + +// 删除自动点赞任务 +export function deleteAutoLikeTask(id: string): Promise { + return request("/v1/workbench/delete", { id }, "DELETE"); +} + +// 切换任务状态 +export function toggleAutoLikeTask(id: string, status: string): Promise { + return request("/v1/workbench/update-status", { id, status }, "POST"); +} + +// 复制自动点赞任务 +export function copyAutoLikeTask(id: string): Promise { + return request("/v1/workbench/copy", { id }, "POST"); +} + +// 获取点赞记录 +export function fetchLikeRecords( + workbenchId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise> { + const params: any = { + workbenchId, + page: page.toString(), + limit: limit.toString(), + }; + if (keyword) { + params.keyword = keyword; + } + return request("/v1/workbench/records", params, "GET"); +} diff --git a/nkebao/src/pages/workspace/auto-like/list/index.module.scss b/nkebao/src/pages/workspace/auto-like/list/index.module.scss new file mode 100644 index 00000000..d70e7a45 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/index.module.scss @@ -0,0 +1,342 @@ +.header { + background: white; + border-bottom: 1px solid #e5e5e5; + position: sticky; + top: 0; + z-index: 10; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.header-title { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.search-bar { + display: flex; + gap: 12px; + align-items: center; + padding: 16px; +} + +.search-input-wrapper { + position: relative; + flex: 1; + + .ant-input { + border-radius: 8px; + height: 40px; + } +} +.refresh-btn { + height: 40px; + width: 40px; + padding: 0; + border-radius: 8px; +} + +.new-task-btn { + height: 32px; + padding: 0 12px; + border-radius: 6px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; +} + + +.task-list { + padding: 0 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.task-card { + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s; + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } +} + +.task-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.task-title-section { + display: flex; + align-items: center; + gap: 8px; +} + +.task-name { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.task-status { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + + &.active { + background: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; + } + + &.inactive { + background: #f5f5f5; + color: #666; + border: 1px solid #d9d9d9; + } +} + +.task-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; + + &:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + } + } + + input:checked + .slider { + background-color: #1890ff; + } + + input:checked + .slider:before { + transform: translateX(20px); + } +} + +.menu-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + border-radius: 4px; + color: #666; + + &:hover { + background: #f5f5f5; + } +} + +.menu-dropdown { + position: absolute; + right: 0; + top: 28px; + background: white; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 100; + min-width: 120px; + padding: 4px; + border: 1px solid #e5e5e5; +} + +.menu-item { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + border-radius: 4px; + font-size: 14px; + gap: 8px; + transition: background 0.2s; + + &:hover { + background: #f5f5f5; + } + + &.danger { + color: #ff4d4f; + + &:hover { + background: #fff2f0; + } + } +} + +.task-info { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.info-section { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.info-label { + font-size: 16px; + color: #666; +} + +.info-value { + font-size: 16px; + color: #333; + font-weight: 600; +} + +.task-stats { + display: flex; + justify-content: space-between; + font-size: 14px; + color: #666; + border-top: 1px solid #f0f0f0; + padding-top: 10px; +} + +.stats-item { + display: flex; + align-items: center; + gap: 8px; +} + +.stats-icon { + font-size: 16px; + + &.blue { + color: #1890ff; + } + + &.green { + color: #52c41a; + } +} + +.stats-label { + font-weight: 500; +} + +.stats-value { + color: #333; + font-weight: 600; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.empty-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.empty-text { + font-size: 16px; + margin-bottom: 8px; +} + +.empty-subtext { + font-size: 14px; + color: #999; +} + +// 移动端适配 +@media (max-width: 768px) { + .task-info { + grid-template-columns: 1fr; + gap: 16px; + } + + .task-stats { + gap: 12px; + align-items: flex-start; + } + + .header-content { + padding: 12px 16px; + } + + .search-section { + padding: 12px 16px; + } + + .task-list { + padding: 0 12px; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/list/index.tsx b/nkebao/src/pages/workspace/auto-like/list/index.tsx new file mode 100644 index 00000000..7e5f78e9 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/index.tsx @@ -0,0 +1,410 @@ +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { + NavBar, + Button, + Toast, + SpinLoading, + Dialog, + Popup, + Card, + Tag, +} from "antd-mobile"; +import { Input } from "antd"; +import { + PlusOutlined, + CopyOutlined, + DeleteOutlined, + SettingOutlined, + SearchOutlined, + ReloadOutlined, + EyeOutlined, + EditOutlined, + MoreOutlined, + LikeOutlined, + ClockCircleOutlined, +} from "@ant-design/icons"; +import { LeftOutline } from "antd-mobile-icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchAutoLikeTasks, + deleteAutoLikeTask, + toggleAutoLikeTask, + copyAutoLikeTask, +} from "./api"; +import { LikeTask } from "@/types/auto-like"; +import style from "./index.module.scss"; + +// 卡片菜单组件 +interface CardMenuProps { + onView: () => void; + onEdit: () => void; + onCopy: () => void; + onDelete: () => void; +} + +const CardMenu: React.FC = ({ + onView, + onEdit, + onCopy, + onDelete, +}) => { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + if (open) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
{ + onView(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 查看 +
+
{ + onEdit(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 编辑 +
+
{ + onCopy(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 复制 +
+
{ + onDelete(); + setOpen(false); + }} + className={`${style["menu-item"]} ${style["danger"]}`} + > + + 删除 +
+
+ )} +
+ ); +}; + +const AutoLike: React.FC = () => { + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(false); + + // 获取任务列表 + const fetchTasks = async () => { + setLoading(true); + try { + const Res: any = await fetchAutoLikeTasks(); + // 直接就是任务数组,无需再解包 + const mappedTasks = Res?.list?.map((task: any) => ({ + ...task, + status: task.status || 2, // 默认为关闭状态 + deviceCount: task.deviceCount || 0, + targetGroup: task.targetGroup || "全部好友", + likeInterval: task.likeInterval || 60, + maxLikesPerDay: task.maxLikesPerDay || 100, + lastLikeTime: task.lastLikeTime || "暂无", + createTime: task.createTime || "", + updateTime: task.updateTime || "", + todayLikeCount: task.todayLikeCount || 0, + totalLikeCount: task.totalLikeCount || 0, + })); + setTasks(mappedTasks); + } catch (error) { + console.error("获取自动点赞任务失败:", error); + Toast.show({ + content: "获取任务列表失败", + position: "top", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTasks(); + }, []); + + // 删除任务 + const handleDelete = async (id: string) => { + const result = await Dialog.confirm({ + content: "确定要删除这个任务吗?", + confirmText: "删除", + cancelText: "取消", + }); + + if (result) { + try { + await deleteAutoLikeTask(id); + Toast.show({ + content: "删除成功", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "删除失败", + position: "top", + }); + } + } + }; + + // 编辑任务 + const handleEdit = (taskId: string) => { + navigate(`/workspace/auto-like/edit/${taskId}`); + }; + + // 查看任务 + const handleView = (taskId: string) => { + navigate(`/workspace/auto-like/detail/${taskId}`); + }; + + // 复制任务 + const handleCopy = async (id: string) => { + try { + await copyAutoLikeTask(id); + Toast.show({ + content: "复制成功", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "复制失败", + position: "top", + }); + } + }; + + // 切换任务状态 + const toggleTaskStatus = async (id: string, status: number) => { + try { + const newStatus = status === 1 ? "2" : "1"; + await toggleAutoLikeTask(id, newStatus); + Toast.show({ + content: status === 1 ? "已暂停" : "已启动", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "操作失败", + position: "top", + }); + } + }; + + // 创建新任务 + const handleCreateNew = () => { + navigate("/workspace/auto-like/new"); + }; + + // 过滤任务 + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + + window.history.back()} + left={ +
+ 自动点赞 +
+ } + right={ + + } + /> + {/* 搜索栏 */} +
+
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ + } + > +
+ {/* 任务列表 */} +
+ {loading ? ( +
+ +
加载中...
+
+ ) : filteredTasks.length === 0 ? ( +
+
+ +
+
暂无自动点赞任务
+
+ 点击右上角按钮创建新任务 +
+
+ ) : ( + filteredTasks.map((task) => ( + +
+
+

{task.name}

+ + {Number(task.status) === 1 ? "进行中" : "已暂停"} + +
+
+ + handleView(task.id)} + onEdit={() => handleEdit(task.id)} + onCopy={() => handleCopy(task.id)} + onDelete={() => handleDelete(task.id)} + /> +
+
+ +
+
+
+ 执行设备 + + {task.deviceCount} 个 + +
+
+ 目标人群 + + {task.targetGroup} + +
+
+ 更新时间 + + {task.updateTime} + +
+
+
+
+ 点赞间隔 + + {task.likeInterval} 秒 + +
+
+ 每日上限 + + {task.maxLikesPerDay} 次 + +
+
+ 创建时间 + + {task.createTime} + +
+
+
+ +
+
+ + 今日点赞: + + {task.lastLikeTime} + +
+
+ + 总点赞数: + + {task.totalLikeCount || 0} + +
+
+
+ )) + )} +
+
+
+ ); +}; + +export default AutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx new file mode 100644 index 00000000..bd6c9d5e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -0,0 +1,437 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + NavBar, + Button, + Toast, + SpinLoading, + Form, + Input, + Switch, + Stepper, + Card, + Tag, +} from "antd-mobile"; +import { Input as AntInput, TimePicker, Select } from "antd"; +import { + PlusOutlined, + MinusOutlined, + CheckOutlined, + TagOutlined, + ClockCircleOutlined, + LikeOutlined, + UserOutlined, + SettingOutlined, +} from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + createAutoLikeTask, + updateAutoLikeTask, + fetchAutoLikeTaskDetail, +} from "@/api/autoLike"; +import { + CreateLikeTaskData, + UpdateLikeTaskData, + ContentType, +} from "@/types/auto-like"; +import style from "./new.module.scss"; + +const { Option } = Select; + +const NewAutoLike: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEditMode = !!id; + const [currentStep, setCurrentStep] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(isEditMode); + const [autoEnabled, setAutoEnabled] = useState(false); + + const [formData, setFormData] = useState({ + name: "", + interval: 5, + maxLikes: 200, + startTime: "08:00", + endTime: "22:00", + contentTypes: ["text", "image", "video"], + devices: [], + friends: [], + targetTags: [], + friendMaxLikes: 10, + enableFriendTags: false, + friendTags: "", + }); + + // 如果是编辑模式,获取任务详情 + useEffect(() => { + if (isEditMode && id) { + fetchTaskDetail(); + } + }, [id, isEditMode]); + + // 获取任务详情 + const fetchTaskDetail = async () => { + try { + const taskDetail = await fetchAutoLikeTaskDetail(id!); + if (taskDetail) { + const taskAny = taskDetail as any; + const config = taskAny.config || taskAny; + + setFormData({ + name: taskDetail.name || "", + interval: config.likeInterval || config.interval || 5, + maxLikes: config.maxLikesPerDay || config.maxLikes || 200, + startTime: config.timeRange?.start || config.startTime || "08:00", + endTime: config.timeRange?.end || config.endTime || "22:00", + contentTypes: config.contentTypes || ["text", "image", "video"], + devices: config.devices || [], + friends: config.friends || [], + targetTags: config.targetTags || [], + friendMaxLikes: config.friendMaxLikes || 10, + enableFriendTags: config.enableFriendTags || false, + friendTags: config.friendTags || "", + }); + + const status = taskAny.status; + setAutoEnabled(status === 1 || status === "running"); + } + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + position: "top", + }); + } finally { + setIsLoading(false); + } + }; + + const handleUpdateFormData = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const handleNext = () => { + if (currentStep < 3) { + setCurrentStep(currentStep + 1); + } + }; + + const handlePrev = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + + const handleComplete = async () => { + if (!formData.name.trim()) { + Toast.show({ + content: "请输入任务名称", + position: "top", + }); + return; + } + + if (formData.devices.length === 0) { + Toast.show({ + content: "请选择执行设备", + position: "top", + }); + return; + } + + setIsSubmitting(true); + try { + if (isEditMode && id) { + const updateData: UpdateLikeTaskData = { + ...formData, + id, + }; + await updateAutoLikeTask(updateData); + Toast.show({ + content: "更新成功", + position: "top", + }); + } else { + await createAutoLikeTask(formData); + Toast.show({ + content: "创建成功", + position: "top", + }); + } + navigate("/workspace/auto-like"); + } catch (error) { + Toast.show({ + content: isEditMode ? "更新失败" : "创建失败", + position: "top", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleContentTypeChange = (type: ContentType) => { + const newTypes = formData.contentTypes.includes(type) + ? formData.contentTypes.filter((t) => t !== type) + : [...formData.contentTypes, type]; + handleUpdateFormData({ contentTypes: newTypes }); + }; + + const renderStepIndicator = () => ( +
+
+
= 1 ? style["active"] : ""}`} + > +
1
+
基础设置
+
+
= 2 ? style["active"] : ""}`} + > +
2
+
设备选择
+
+
= 3 ? style["active"] : ""}`} + > +
3
+
好友设置
+
+
+
+ ); + + const renderBasicSettings = () => ( +
+ +
+ + handleUpdateFormData({ name: value })} + className={style["form-input"]} + /> +
+ +
+ +
+ + + {formData.interval} 秒 + + +
+
+ +
+ +
+ + + {formData.maxLikes} 次 + + +
+
+ +
+ +
+ handleUpdateFormData({ startTime: value })} + className={style["time-input"]} + /> + + handleUpdateFormData({ endTime: value })} + className={style["time-input"]} + /> +
+
+ +
+ +
+ {(["text", "image", "video", "link"] as ContentType[]).map( + (type) => ( + handleContentTypeChange(type)} + className={style["content-type-tag"]} + > + {type === "text" && "文字"} + {type === "image" && "图片"} + {type === "video" && "视频"} + {type === "link" && "链接"} + + ) + )} +
+
+ +
+ + +
+
+
+ ); + + const renderDeviceSelection = () => ( +
+ +
+ +
设备选择功能开发中...
+
+ 当前已选择 {formData.devices.length} 个设备 +
+
+
+
+ ); + + const renderFriendSettings = () => ( +
+ +
+ +
好友设置功能开发中...
+
+ 当前已选择 {formData.friends.length} 个好友 +
+
+
+
+ ); + + if (isLoading) { + return ( + window.history.back()} + left={ +
+ {isEditMode ? "编辑任务" : "新建任务"} +
+ } + /> + } + footer={} + > +
+ +
加载中...
+
+
+ ); + } + + return ( + window.history.back()} + left={ +
+ {isEditMode ? "编辑任务" : "新建任务"} +
+ } + /> + } + footer={} + > +
+ {renderStepIndicator()} + + {currentStep === 1 && renderBasicSettings()} + {currentStep === 2 && renderDeviceSelection()} + {currentStep === 3 && renderFriendSettings()} + +
+ {currentStep > 1 && ( + + )} + + {currentStep < 3 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default NewAutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/new/new.module.scss b/nkebao/src/pages/workspace/auto-like/new/new.module.scss new file mode 100644 index 00000000..5b25188e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/new/new.module.scss @@ -0,0 +1,301 @@ +.new-page { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 80px; +} + +.step-indicator { + background: white; + padding: 20px 16px; + border-bottom: 1px solid #f0f0f0; +} + +.step-list { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 20px; + left: 20px; + right: 20px; + height: 2px; + background: #e5e5e5; + z-index: 1; + } +} + +.step-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: relative; + z-index: 2; + + &.active { + .step-number { + background: #1890ff; + color: white; + border-color: #1890ff; + } + + .step-label { + color: #1890ff; + font-weight: 600; + } + } +} + +.step-number { + width: 40px; + height: 40px; + border-radius: 50%; + background: white; + border: 2px solid #e5e5e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + color: #999; + transition: all 0.3s; +} + +.step-label { + font-size: 12px; + color: #666; + text-align: center; + transition: all 0.3s; +} + +.step-content { + padding: 16px; +} + +.form-card { + background: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.form-item { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } +} + +.form-label { + display: block; + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.form-input { + width: 100%; + height: 40px; + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.stepper-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.stepper-btn { + width: 32px; + height: 32px; + padding: 0; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #d9d9d9; + background: white; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } +} + +.stepper-value { + font-size: 14px; + font-weight: 600; + color: #333; + min-width: 60px; + text-align: center; +} + +.time-range { + display: flex; + align-items: center; + gap: 12px; +} + +.time-input { + flex: 1; + height: 40px; + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.time-separator { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.content-types { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.content-type-tag { + cursor: pointer; + transition: all 0.2s; + + &:hover { + transform: translateY(-1px); + } +} + +.form-switch { + margin-left: auto; +} + +.placeholder-content { + text-align: center; + padding: 40px 20px; + color: #666; +} + +.placeholder-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.placeholder-text { + font-size: 16px; + margin-bottom: 8px; + color: #333; +} + +.placeholder-subtext { + font-size: 14px; + color: #999; +} + +.step-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 16px; + border-top: 1px solid #f0f0f0; + display: flex; + gap: 12px; + z-index: 10; +} + +.prev-btn { + flex: 1; + height: 44px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} + +.next-btn, +.complete-btn { + flex: 1; + height: 44px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +// 移动端适配 +@media (max-width: 768px) { + .step-list { + &::before { + left: 15px; + right: 15px; + } + } + + .step-number { + width: 36px; + height: 36px; + font-size: 14px; + } + + .step-label { + font-size: 11px; + } + + .form-card { + padding: 16px; + } + + .stepper-wrapper { + gap: 8px; + } + + .stepper-btn { + width: 28px; + height: 28px; + } + + .time-range { + flex-direction: column; + gap: 8px; + } + + .content-types { + gap: 6px; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/record/index.tsx b/nkebao/src/pages/workspace/auto-like/record/index.tsx new file mode 100644 index 00000000..4d52913a --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/record/index.tsx @@ -0,0 +1,297 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { NavBar, Button, Toast, SpinLoading, Card, Avatar } from "antd-mobile"; +import { Input } from "antd"; +import InfiniteList from "@/components/InfiniteList/InfiniteList"; +import { + SearchOutlined, + ReloadOutlined, + LikeOutlined, + UserOutlined, +} from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { fetchLikeRecords, fetchAutoLikeTaskDetail } from "@/api/autoLike"; +import { LikeRecord, LikeTask } from "@/types/auto-like"; +import style from "./record.module.scss"; + +// 格式化日期 +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; + } +}; + +const AutoLikeDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [records, setRecords] = useState([]); + const [taskDetail, setTaskDetail] = useState(null); + const [recordsLoading, setRecordsLoading] = useState(false); + const [taskLoading, setTaskLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + const [hasMore, setHasMore] = useState(true); + const pageSize = 10; + + // 获取任务详情 + const fetchTaskDetail = async () => { + if (!id) return; + setTaskLoading(true); + try { + const detail = await fetchAutoLikeTaskDetail(id); + setTaskDetail(detail); + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + position: "top", + }); + } finally { + setTaskLoading(false); + } + }; + + // 获取点赞记录 + const fetchRecords = async ( + page: number = 1, + isLoadMore: boolean = false + ) => { + if (!id) return; + + if (!isLoadMore) { + setRecordsLoading(true); + } + + try { + const response = await fetchLikeRecords(id, page, pageSize, searchTerm); + const newRecords = response.list || []; + + if (isLoadMore) { + setRecords((prev) => [...prev, ...newRecords]); + } else { + setRecords(newRecords); + } + + setTotal(response.total || 0); + setCurrentPage(page); + setHasMore(newRecords.length === pageSize); + } catch (error) { + Toast.show({ + content: "获取点赞记录失败", + position: "top", + }); + } finally { + setRecordsLoading(false); + } + }; + + useEffect(() => { + fetchTaskDetail(); + fetchRecords(1, false); + }, [id]); + + const handleSearch = () => { + setCurrentPage(1); + fetchRecords(1, false); + }; + + const handleRefresh = () => { + fetchRecords(currentPage, false); + }; + + const handleLoadMore = async () => { + if (hasMore && !recordsLoading) { + await fetchRecords(currentPage + 1, true); + } + }; + + const renderRecordItem = (record: LikeRecord) => ( + +
+
+ + + +
+
+ {record.friendName} +
+
内容发布者
+
+
+
+ {formatDate(record.momentTime || record.likeTime)} +
+
+ +
+ {record.content && ( +

{record.content}

+ )} + + {Array.isArray(record.resUrls) && record.resUrls.length > 0 && ( +
+ {record.resUrls.slice(0, 9).map((image: string, idx: number) => ( +
+ {`内容图片 +
+ ))} +
+ )} +
+ +
+ + + +
+ + {record.operatorName} + + 点赞了这条内容 +
+
+
+ ); + + return ( + window.history.back()} + left={ +
+ 点赞记录 +
+ } + /> + } + footer={} + > +
+ {/* 任务信息卡片 */} + {taskDetail && ( +
+
+

{taskDetail.name}

+ + {Number(taskDetail.status) === 1 ? "进行中" : "已暂停"} + +
+
+
+ + 今日点赞: + + {taskDetail.todayLikeCount || 0} + +
+
+ + 总点赞数: + + {taskDetail.totalLikeCount || 0} + +
+
+
+ )} + + {/* 搜索区域 */} +
+
+
+ + setSearchTerm(e.target.value)} + onPressEnter={handleSearch} + /> +
+ + +
+
+ + {/* 记录列表 */} +
+ {recordsLoading && currentPage === 1 ? ( +
+ +
加载中...
+
+ ) : records.length === 0 ? ( +
+ +
暂无点赞记录
+
+ ) : ( + + )} +
+
+
+ ); +}; + +export default AutoLikeDetail; diff --git a/nkebao/src/pages/workspace/auto-like/record/record.module.scss b/nkebao/src/pages/workspace/auto-like/record/record.module.scss new file mode 100644 index 00000000..9bc931b6 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/record/record.module.scss @@ -0,0 +1,351 @@ +.detail-page { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 80px; +} + +.task-info-card { + background: white; + margin: 16px; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.task-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.task-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin: 0; +} + +.task-status { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + + &.active { + background: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; + } + + &.inactive { + background: #f5f5f5; + color: #666; + border: 1px solid #d9d9d9; + } +} + +.task-stats { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: #666; +} + +.stat-icon { + font-size: 16px; + color: #1890ff; +} + +.stat-label { + font-weight: 500; +} + +.stat-value { + color: #333; + font-weight: 600; +} + +.search-section { + padding: 0 16px 16px; +} + +.search-wrapper { + display: flex; + align-items: center; + gap: 8px; + background: white; + border-radius: 8px; + padding: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.search-input-wrapper { + position: relative; + flex: 1; +} + +.search-input { + width: 100%; + height: 36px; + padding: 0 12px 0 32px; + border: 1px solid #d9d9d9; + border-radius: 6px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.search-icon { + position: absolute; + left: 8px; + top: 50%; + transform: translateY(-50%); + color: #999; + font-size: 14px; +} + +.search-btn { + height: 36px; + padding: 0 12px; + border-radius: 6px; + font-size: 14px; + white-space: nowrap; +} + +.refresh-btn { + height: 36px; + width: 36px; + padding: 0; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #d9d9d9; + background: white; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.records-section { + padding: 0 16px; +} + +.records-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.record-card { + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.record-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; + max-width: 65%; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.user-details { + min-width: 0; +} + +.user-name { + font-size: 14px; + font-weight: 600; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-role { + font-size: 12px; + color: #666; + margin-top: 2px; +} + +.record-time { + font-size: 12px; + color: #666; + background: #f8f9fa; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; +} + +.record-content { + margin-bottom: 12px; +} + +.content-text { + font-size: 14px; + color: #333; + line-height: 1.5; + margin-bottom: 12px; + white-space: pre-line; +} + +.content-images { + display: grid; + gap: 4px; + + &.single { + grid-template-columns: 1fr; + } + + &.double { + grid-template-columns: 1fr 1fr; + } + + &.multiple { + grid-template-columns: repeat(3, 1fr); + } +} + +.image-item { + aspect-ratio: 1; + border-radius: 6px; + overflow: hidden; +} + +.content-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.like-info { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #f8f9fa; + border-radius: 6px; +} + +.operator-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + flex-shrink: 0; +} + +.like-text { + font-size: 14px; + color: #666; + min-width: 0; +} + +.operator-name { + font-weight: 600; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + max-width: 100%; +} + +.like-action { + margin-left: 4px; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.empty-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.empty-text { + font-size: 16px; + color: #999; +} + +// 移动端适配 +@media (max-width: 768px) { + .task-stats { + flex-direction: column; + gap: 12px; + } + + .search-wrapper { + flex-direction: column; + gap: 12px; + } + + .search-btn { + width: 100%; + } + + .user-info { + max-width: 60%; + } + + .content-images { + &.multiple { + grid-template-columns: repeat(2, 1fr); + } + } +} \ No newline at end of file diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index b7ccc7c2..20e199b9 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -1,7 +1,7 @@ import Workspace from "@/pages/workspace/main"; -import AutoLike from "@/pages/workspace/auto-like/AutoLike"; -import NewAutoLike from "@/pages/workspace/auto-like/NewAutoLike"; -import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; +import ListAutoLike from "@/pages/workspace/auto-like/list"; +import NewAutoLike from "@/pages/workspace/auto-like/new"; +import RecordAutoLike from "@/pages/workspace/auto-like/record"; import AutoGroup from "@/pages/workspace/auto-group/AutoGroup"; import AutoGroupDetail from "@/pages/workspace/auto-group/Detail"; import GroupPush from "@/pages/workspace/group-push/GroupPush"; @@ -24,7 +24,7 @@ const workspaceRoutes = [ // 自动点赞 { path: "/workspace/auto-like", - element: , + element: , auth: true, }, { @@ -34,7 +34,7 @@ const workspaceRoutes = [ }, { path: "/workspace/auto-like/:id", - element: , + element: , auth: true, }, { diff --git a/nkebao/src/types/auto-like.ts b/nkebao/src/types/auto-like.ts new file mode 100644 index 00000000..bace06ec --- /dev/null +++ b/nkebao/src/types/auto-like.ts @@ -0,0 +1,119 @@ +// 自动点赞任务状态 +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 { + code: number; + msg: string; + data: T; +} + +// 分页响应类型 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +}