feat: 本次提交更新内容如下

朋友圈同步列表完成
This commit is contained in:
笔记本里的永平
2025-07-21 16:37:53 +08:00
parent 15e72309cf
commit 20791a0ab5
3 changed files with 835 additions and 10 deletions

View File

@@ -1,8 +1,186 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
import React, { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Button, Switch, message, Spin, Badge } from "antd";
import {
ArrowLeftOutlined,
EditOutlined,
ClockCircleOutlined,
DatabaseOutlined,
MobileOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import request from "@/api/request";
interface MomentsSyncTask {
id: string;
name: string;
status: 1 | 2;
deviceCount: number;
syncCount: number;
lastSyncTime: string;
createTime: string;
creatorName: string;
updateTime?: string;
maxSyncPerDay?: number;
syncInterval?: number;
timeRange?: { start: string; end: string };
contentTypes?: string[];
targetTags?: string[];
todaySyncCount?: number;
totalSyncCount?: number;
syncMode?: string;
config?: {
devices?: string[];
contentLibraryNames?: string[];
syncCount?: number;
};
}
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
const MomentsSyncDetail: React.FC = () => {
return <PlaceholderPage title="朋友圈同步详情" />;
const { id } = useParams();
const navigate = useNavigate();
const [task, setTask] = useState<MomentsSyncTask | null>(null);
const [loading, setLoading] = useState(false);
const fetchTaskDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await request("/v1/workbench/detail", { id }, "GET");
if (res) setTask(res);
} catch {
message.error("获取任务详情失败");
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
if (id) fetchTaskDetail();
}, [id, fetchTaskDetail]);
const handleToggleStatus = async () => {
if (!task || !id) return;
try {
const newStatus = task.status === 1 ? 2 : 1;
await request(
"/v1/workbench/update-status",
{ id, status: newStatus },
"POST"
);
setTask({ ...task, status: newStatus });
message.success(newStatus === 1 ? "任务已开启" : "任务已暂停");
} catch {
message.error("操作失败");
}
};
const handleEdit = () => {
if (id) navigate(`/workspace/moments-sync/edit/${id}`);
};
if (loading) {
return (
<Layout>
<div className={style.detailLoading}>
<Spin size="large" />
</div>
</Layout>
);
}
if (!task) {
return (
<Layout>
<div className={style.detailLoading}>
<div></div>
<Button onClick={() => navigate("/workspace/moments-sync")}>
</Button>
</div>
</Layout>
);
}
return (
<Layout
header={
<div className={style.headerBar}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/workspace/moments-sync")}
className={style.backBtn}
/>
<span className={style.title}></span>
<Button
icon={<EditOutlined />}
onClick={handleEdit}
className={style.editBtn}
>
</Button>
</div>
}
>
<div className={style.detailBg}>
<div className={style.detailCard}>
<div className={style.detailTop}>
<div className={style.detailTitle}>{task.name}</div>
<span
className={
task.status === 1
? style.statusPill + " " + style.statusActive
: style.statusPill + " " + style.statusPaused
}
>
{getStatusText(task.status)}
</span>
<Switch
checked={task.status === 1}
onChange={handleToggleStatus}
className={style.switchBtn}
size="small"
/>
</div>
<div className={style.detailInfoRow}>
<div className={style.infoCol}>
{task.config?.devices?.length || 0}
</div>
<div className={style.infoCol}>
{task.config?.contentLibraryNames?.join(",") || "-"}
</div>
</div>
<div className={style.detailInfoRow}>
<div className={style.infoCol}>
{task.syncCount || 0}
</div>
<div className={style.infoCol}>{task.creatorName}</div>
</div>
<div className={style.detailBottom}>
<div className={style.bottomLeft}>
<ClockCircleOutlined className={style.clockIcon} />
{task.lastSyncTime || "无"}
</div>
<div className={style.bottomRight}>{task.createTime}</div>
</div>
</div>
{/* 可继续补充更多详情卡片,如同步设置、同步记录等 */}
</div>
</Layout>
);
};
export default MomentsSyncDetail;

View File

@@ -1,13 +1,273 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Switch, Input, message, Badge, Dropdown, Menu } from "antd";
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined,
MoreOutlined,
ClockCircleOutlined,
ArrowLeftOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import request from "@/api/request";
interface MomentsSyncTask {
id: string;
name: string;
status: 1 | 2;
deviceCount: number;
syncCount: number;
lastSyncTime: string;
createTime: string;
creatorName: string;
contentLib?: string;
config?: { devices?: string[]; contentLibraryNames?: string[] };
}
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
const MomentsSync: React.FC = () => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [tasks, setTasks] = useState<MomentsSyncTask[]>([]);
const fetchTasks = async () => {
setLoading(true);
try {
const res = await request(
"/v1/workbench/list",
{ type: 2, page: 1, limit: 100 },
"GET"
);
setTasks(res.list || []);
} catch (e) {
message.error("获取任务失败");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
const handleDelete = async (id: string) => {
if (!window.confirm("确定要删除该任务吗?")) return;
try {
await request("/v1/workbench/delete", { id }, "DELETE");
message.success("删除成功");
fetchTasks();
} catch {
message.error("删除失败");
}
};
const handleCopy = async (id: string) => {
try {
await request("/v1/workbench/copy", { id }, "POST");
message.success("复制成功");
fetchTasks();
} catch {
message.error("复制失败");
}
};
const handleToggle = async (id: string, status: number) => {
const newStatus = status === 1 ? 2 : 1;
try {
await request(
"/v1/workbench/update-status",
{ id, status: newStatus },
"POST"
);
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, status: newStatus } : t))
);
message.success("操作成功");
} catch {
message.error("操作失败");
}
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// 菜单
const getMenu = (task: MomentsSyncTask) => (
<Menu>
<Menu.Item
key="view"
icon={<EyeOutlined />}
onClick={() => navigate(`/workspace/moments-sync/${task.id}`)}
>
</Menu.Item>
<Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={() => navigate(`/workspace/moments-sync/edit/${task.id}`)}
>
</Menu.Item>
<Menu.Item
key="copy"
icon={<CopyOutlined />}
onClick={() => handleCopy(task.id)}
>
</Menu.Item>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={() => handleDelete(task.id)}
danger
>
</Menu.Item>
</Menu>
);
return (
<PlaceholderPage
title="朋友圈同步"
showAddButton
addButtonText="新建同步"
/>
<Layout
header={
<>
<div className={style.headerBar}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/workspace")}
className={style.backBtn}
/>
<span className={style.title}></span>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/workspace/moments-sync/new")}
className={style.addBtn}
>
</Button>
</div>
<div className={style.searchBar}>
<Input
placeholder="搜索任务名称"
prefix={<SearchOutlined />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onPressEnter={fetchTasks}
className={style.searchInput}
/>
<Button
icon={<ReloadOutlined />}
onClick={fetchTasks}
loading={loading}
/>
</div>
</>
}
>
<div className={style.pageBg}>
<div className={style.taskList}>
{filteredTasks.length === 0 ? (
<div className={style.emptyBox}>
<span style={{ fontSize: 40, color: "#ddd" }}>
<ClockCircleOutlined />
</span>
<div className={style.emptyText}></div>
<Button
type="primary"
onClick={() => navigate("/workspace/moments-sync/new")}
>
</Button>
</div>
) : (
filteredTasks.map((task) => (
<div key={task.id} className={style.itemCard}>
<div className={style.itemTop}>
<div className={style.itemTitle}>
<span className={style.itemName}>{task.name}</span>
<span
className={
task.status === 1
? style.statusPill + " " + style.statusActive
: style.statusPill + " " + style.statusPaused
}
>
{getStatusText(task.status)}
</span>
</div>
<div className={style.itemActions}>
<Switch
checked={task.status === 1}
onChange={() => handleToggle(task.id, task.status)}
className={style.switchBtn}
size="small"
/>
<Dropdown
overlay={getMenu(task)}
trigger={["click"]}
placement="bottomRight"
>
<Button
type="text"
icon={<MoreOutlined />}
className={style.moreBtn}
/>
</Dropdown>
</div>
</div>
<div className={style.itemInfoRow}>
<div className={style.infoCol}>
{task.config?.devices?.length || 0}
</div>
<div className={style.infoCol}>
{task.syncCount || 0}
</div>
</div>
<div className={style.itemInfoRow}>
<div className={style.infoCol}>
{task.config?.contentLibraryNames?.join(",") ||
task.contentLib ||
"默认内容库"}
</div>
<div className={style.infoCol}>
{task.creatorName}
</div>
</div>
<div className={style.itemBottom}>
<div className={style.bottomLeft}>
<ClockCircleOutlined className={style.clockIcon} />
{task.lastSyncTime || "无"}
</div>
<div className={style.bottomRight}>
{task.createTime}
</div>
</div>
</div>
))
)}
</div>
</div>
</Layout>
);
};

View File

@@ -0,0 +1,387 @@
.pageBg {
background: #f8f6f3;
padding-bottom: 24px;
}
.headerBar {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border-bottom: 1px solid #f0f0f0;
padding: 0 16px;
height: 56px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #188eee;
}
.backBtn {
border: none;
background: none;
box-shadow: none;
color: #666;
font-size: 18px;
margin-right: 8px;
}
.addBtn {
margin-left: 8px;
}
.searchBar {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 16px 0 16px;
background: #f8f6f3;
}
.searchInput {
flex: 1;
min-width: 0;
}
.taskList {
padding: 16px;
}
.taskCard {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 16px;
padding: 16px;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 16px rgba(24,142,238,0.10);
}
}
.taskCardTop {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.taskName {
font-size: 16px;
font-weight: 500;
color: #222;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.switchBtn {
margin-left: 8px;
}
.actionBtn {
margin-left: 4px;
}
.taskCardInfo {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
font-size: 13px;
color: #666;
margin-top: 8px;
}
.emptyBox {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 40px 0 32px 0;
text-align: center;
color: #bbb;
margin-top: 40px;
}
.emptyText {
font-size: 16px;
color: #888;
margin: 16px 0 20px 0;
}
.itemCard {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 16px;
padding: 16px 16px 12px 16px;
transition: box-shadow 0.2s;
position: relative;
&:hover {
box-shadow: 0 4px 16px rgba(24,142,238,0.10);
}
}
.itemTop {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.itemTitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 500;
color: #222;
}
.itemName {
font-size: 15px;
font-weight: 500;
color: #222;
margin-right: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.statusBadge {
margin-left: 2px;
.ant-badge-status-dot {
width: 8px;
height: 8px;
}
.ant-badge-status-success {
background: #19c37d;
}
.ant-badge-status-default {
background: #bdbdbd;
}
.ant-badge-status-text {
font-size: 12px;
font-weight: 400;
padding: 0 6px;
border-radius: 8px;
background: #f5f5f5;
color: #222;
}
}
.itemActions {
display: flex;
align-items: center;
gap: 6px;
}
.switchBtn {
margin-right: 2px;
}
.moreBtn {
margin-left: 2px;
color: #888;
font-size: 18px;
background: none;
border: none;
box-shadow: none;
}
.itemInfoRow {
display: flex;
font-size: 13px;
color: #666;
margin-bottom: 2px;
}
.infoCol {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itemBottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px solid #f3f3f3;
margin-top: 8px;
padding-top: 6px;
}
.bottomLeft {
display: flex;
align-items: center;
gap: 4px;
}
.clockIcon {
color: #bdbdbd;
font-size: 14px;
margin-right: 2px;
}
.bottomRight {
text-align: right;
}
// 覆盖Antd Dropdown菜单样式
.ant-dropdown-menu {
border-radius: 10px !important;
box-shadow: 0 4px 16px rgba(0,0,0,0.12) !important;
min-width: 110px !important;
padding: 6px 0 !important;
}
.ant-dropdown-menu-item {
font-size: 14px !important;
padding: 7px 16px !important;
border-radius: 6px !important;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
}
.ant-dropdown-menu-item:hover {
background: #f5f5f5 !important;
}
.ant-dropdown-menu-item-danger {
color: #e53e3e !important;
}
.detailBg {
background: #f8f6f3;
min-height: 100vh;
padding: 24px 0 32px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.detailCard {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 20px 20px 12px 20px;
width: 100%;
max-width: 480px;
margin-bottom: 24px;
}
.detailTop {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.detailTitle {
font-size: 18px;
font-weight: 600;
color: #222;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusBadge {
margin-left: 2px;
.ant-badge-status-dot {
width: 8px;
height: 8px;
}
.ant-badge-status-success {
background: #19c37d;
}
.ant-badge-status-default {
background: #bdbdbd;
}
.ant-badge-status-text {
font-size: 12px;
font-weight: 400;
padding: 0 6px;
border-radius: 8px;
background: #f5f5f5;
color: #222;
}
}
.switchBtn {
margin-left: 8px;
}
.detailInfoRow {
display: flex;
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.infoCol {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.detailBottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px solid #f3f3f3;
margin-top: 10px;
padding-top: 6px;
}
.bottomLeft {
display: flex;
align-items: center;
gap: 4px;
}
.clockIcon {
color: #bdbdbd;
font-size: 14px;
margin-right: 2px;
}
.bottomRight {
text-align: right;
}
.detailLoading {
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-size: 16px;
gap: 16px;
}
.statusPill {
display: inline-block;
min-width: 48px;
height: 20px;
line-height: 20px;
font-size: 10px;
border-radius: 12px;
text-align: center;
margin-left: 6px;
box-sizing: border-box;
}
.statusActive {
background: #19c37d;
color: #fff;
}
.statusPaused {
background: #e5e7eb;
color: #888;
}