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

暂存一下
This commit is contained in:
笔记本里的永平
2025-07-23 17:41:52 +08:00
parent 63f95c8bfa
commit 1e94525191
4 changed files with 761 additions and 711 deletions

View File

@@ -1,371 +1,371 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile";
import { SettingOutlined, RedoOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import {
fetchDeviceDetail,
fetchDeviceRelatedAccounts,
fetchDeviceHandleLogs,
updateDeviceTaskConfig,
} from "@/api/devices";
import type { Device, WechatAccount, HandleLog } from "@/types/device";
const DeviceDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [device, setDevice] = useState<Device | null>(null);
const [tab, setTab] = useState("info");
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
const [accountsLoading, setAccountsLoading] = useState(false);
const [logs, setLogs] = useState<HandleLog[]>([]);
const [logsLoading, setLogsLoading] = useState(false);
const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>(
{}
);
// 获取设备详情
const loadDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await fetchDeviceDetail(id);
setDevice(res);
} catch (e: any) {
Toast.show({ content: e.message || "获取设备详情失败", position: "top" });
} finally {
setLoading(false);
}
}, [id]);
// 获取关联账号
const loadAccounts = useCallback(async () => {
if (!id) return;
setAccountsLoading(true);
try {
const res = await fetchDeviceRelatedAccounts(id);
setAccounts(Array.isArray(res.accounts) ? res.accounts : []);
} catch (e: any) {
Toast.show({ content: e.message || "获取关联账号失败", position: "top" });
} finally {
setAccountsLoading(false);
}
}, [id]);
// 获取操作日志
const loadLogs = useCallback(async () => {
if (!id) return;
setLogsLoading(true);
try {
const res = await fetchDeviceHandleLogs(id, 1, 20);
setLogs(Array.isArray(res.list) ? res.list : []);
} catch (e: any) {
Toast.show({ content: e.message || "获取操作日志失败", position: "top" });
} finally {
setLogsLoading(false);
}
}, [id]);
useEffect(() => {
loadDetail();
// eslint-disable-next-line
}, [id]);
useEffect(() => {
if (tab === "accounts") loadAccounts();
if (tab === "logs") loadLogs();
// eslint-disable-next-line
}, [tab]);
// 功能开关
const handleFeatureChange = async (
feature: keyof Device["features"],
checked: boolean
) => {
if (!id) return;
setFeatureSaving((prev) => ({ ...prev, [feature]: true }));
try {
await updateDeviceTaskConfig({ deviceId: id, [feature]: checked });
setDevice((prev) =>
prev
? {
...prev,
features: { ...prev.features, [feature]: checked },
}
: prev
);
Toast.show({
content: `${getFeatureName(feature)}${checked ? "开启" : "关闭"}`,
});
} catch (e: any) {
Toast.show({ content: e.message || "设置失败", position: "top" });
} finally {
setFeatureSaving((prev) => ({ ...prev, [feature]: false }));
}
};
const getFeatureName = (feature: string) => {
const map: Record<string, string> = {
autoAddFriend: "自动加好友",
autoReply: "自动回复",
momentsSync: "朋友圈同步",
aiChat: "AI会话",
};
return map[feature] || feature;
};
return (
<Layout
header={
<NavBar
onBack={() => navigate(-1)}
style={{ background: "#fff" }}
right={
<Button size="small" color="primary">
<SettingOutlined />
</Button>
}
>
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</span>
</NavBar>
}
loading={loading}
>
{!device ? (
<div style={{ padding: 32, textAlign: "center", color: "#888" }}>
<SpinLoading style={{ "--size": "32px" }} />
<div style={{ marginTop: 16 }}>...</div>
</div>
) : (
<div style={{ padding: 12 }}>
{/* 基本信息卡片 */}
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
marginBottom: 16,
boxShadow: "0 1px 4px #eee",
}}
>
<div style={{ fontWeight: 600, fontSize: 18 }}>
{device.memo || "未命名设备"}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
IMEI: {device.imei}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
: {device.wechatId || "未绑定"}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
: {device.totalFriend ?? "-"}
</div>
<div
style={{
fontSize: 13,
color:
device.status === "online" || device.alive === 1
? "#52c41a"
: "#aaa",
marginTop: 4,
}}
>
{device.status === "online" || device.alive === 1
? "在线"
: "离线"}
</div>
</div>
{/* 标签页 */}
<Tabs activeKey={tab} onChange={setTab} style={{ marginBottom: 12 }}>
<Tabs.Tab title="功能开关" key="info" />
<Tabs.Tab title="关联账号" key="accounts" />
<Tabs.Tab title="操作日志" key="logs" />
</Tabs>
{/* 功能开关 */}
{tab === "info" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
display: "flex",
flexDirection: "column",
gap: 18,
}}
>
{["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map(
(f) => (
<div
key={f}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div>
<div style={{ fontWeight: 500 }}>{getFeatureName(f)}</div>
</div>
<Switch
checked={
!!device.features?.[f as keyof Device["features"]]
}
loading={!!featureSaving[f]}
onChange={(checked) =>
handleFeatureChange(
f as keyof Device["features"],
checked
)
}
/>
</div>
)
)}
</div>
)}
{/* 关联账号 */}
{tab === "accounts" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
}}
>
{accountsLoading ? (
<div
style={{ textAlign: "center", color: "#888", padding: 32 }}
>
<SpinLoading />
</div>
) : accounts.length === 0 ? (
<div
style={{ textAlign: "center", color: "#aaa", padding: 32 }}
>
</div>
) : (
<div
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
{accounts.map((acc) => (
<div
key={acc.id}
style={{
display: "flex",
alignItems: "center",
gap: 12,
background: "#f7f8fa",
borderRadius: 8,
padding: 10,
}}
>
<img
src={acc.avatar || "/placeholder.svg"}
alt={acc.nickname}
style={{
width: 40,
height: 40,
borderRadius: 20,
background: "#eee",
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>{acc.nickname}</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {acc.wechatId}
</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {acc.totalFriend}
</div>
<div style={{ fontSize: 12, color: "#aaa" }}>
: {acc.lastActive}
</div>
</div>
<span
style={{
fontSize: 12,
color: acc.wechatAlive === 1 ? "#52c41a" : "#aaa",
}}
>
{acc.wechatAliveText}
</span>
</div>
))}
</div>
)}
<div style={{ textAlign: "center", marginTop: 16 }}>
<Button size="small" onClick={loadAccounts}>
<RedoOutlined />
</Button>
</div>
</div>
)}
{/* 操作日志 */}
{tab === "logs" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
}}
>
{logsLoading ? (
<div
style={{ textAlign: "center", color: "#888", padding: 32 }}
>
<SpinLoading />
</div>
) : logs.length === 0 ? (
<div
style={{ textAlign: "center", color: "#aaa", padding: 32 }}
>
</div>
) : (
<div
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
{logs.map((log) => (
<div
key={log.id}
style={{
display: "flex",
flexDirection: "column",
gap: 2,
background: "#f7f8fa",
borderRadius: 8,
padding: 10,
}}
>
<div style={{ fontWeight: 500 }}>{log.content}</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {log.username} · {log.createTime}
</div>
</div>
))}
</div>
)}
<div style={{ textAlign: "center", marginTop: 16 }}>
<Button size="small" onClick={loadLogs}>
<RedoOutlined />
</Button>
</div>
</div>
)}
</div>
)}
</Layout>
);
};
export default DeviceDetail;
import React, { useEffect, useState, useCallback, useRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile";
import { SettingOutlined, RedoOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import {
fetchDeviceDetail,
fetchDeviceRelatedAccounts,
fetchDeviceHandleLogs,
updateDeviceTaskConfig,
} from "@/api/devices";
import type { Device, WechatAccount, HandleLog } from "@/types/device";
const DeviceDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [device, setDevice] = useState<Device | null>(null);
const [tab, setTab] = useState("info");
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
const [accountsLoading, setAccountsLoading] = useState(false);
const [logs, setLogs] = useState<HandleLog[]>([]);
const [logsLoading, setLogsLoading] = useState(false);
const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>(
{}
);
// 获取设备详情
const loadDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await fetchDeviceDetail(id);
setDevice(res);
} catch (e: any) {
Toast.show({ content: e.message || "获取设备详情失败", position: "top" });
} finally {
setLoading(false);
}
}, [id]);
// 获取关联账号
const loadAccounts = useCallback(async () => {
if (!id) return;
setAccountsLoading(true);
try {
const res = await fetchDeviceRelatedAccounts(id);
setAccounts(Array.isArray(res.accounts) ? res.accounts : []);
} catch (e: any) {
Toast.show({ content: e.message || "获取关联账号失败", position: "top" });
} finally {
setAccountsLoading(false);
}
}, [id]);
// 获取操作日志
const loadLogs = useCallback(async () => {
if (!id) return;
setLogsLoading(true);
try {
const res = await fetchDeviceHandleLogs(id, 1, 20);
setLogs(Array.isArray(res.list) ? res.list : []);
} catch (e: any) {
Toast.show({ content: e.message || "获取操作日志失败", position: "top" });
} finally {
setLogsLoading(false);
}
}, [id]);
useEffect(() => {
loadDetail();
// eslint-disable-next-line
}, [id]);
useEffect(() => {
if (tab === "accounts") loadAccounts();
if (tab === "logs") loadLogs();
// eslint-disable-next-line
}, [tab]);
// 功能开关
const handleFeatureChange = async (
feature: keyof Device["features"],
checked: boolean
) => {
if (!id) return;
setFeatureSaving((prev) => ({ ...prev, [feature]: true }));
try {
await updateDeviceTaskConfig({ deviceId: id, [feature]: checked });
setDevice((prev) =>
prev
? {
...prev,
features: { ...prev.features, [feature]: checked },
}
: prev
);
Toast.show({
content: `${getFeatureName(feature)}${checked ? "开启" : "关闭"}`,
});
} catch (e: any) {
Toast.show({ content: e.message || "设置失败", position: "top" });
} finally {
setFeatureSaving((prev) => ({ ...prev, [feature]: false }));
}
};
const getFeatureName = (feature: string) => {
const map: Record<string, string> = {
autoAddFriend: "自动加好友",
autoReply: "自动回复",
momentsSync: "朋友圈同步",
aiChat: "AI会话",
};
return map[feature] || feature;
};
return (
<Layout
header={
<NavBar
onBack={() => navigate(-1)}
style={{ background: "#fff" }}
right={
<Button size="small" color="primary">
<SettingOutlined />
</Button>
}
>
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</span>
</NavBar>
}
loading={loading}
>
{!device ? (
<div style={{ padding: 32, textAlign: "center", color: "#888" }}>
<SpinLoading style={{ "--size": "32px" }} />
<div style={{ marginTop: 16 }}>...</div>
</div>
) : (
<div style={{ padding: 12 }}>
{/* 基本信息卡片 */}
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
marginBottom: 16,
boxShadow: "0 1px 4px #eee",
}}
>
<div style={{ fontWeight: 600, fontSize: 18 }}>
{device.memo || "未命名设备"}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
IMEI: {device.imei}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
: {device.wechatId || "未绑定"}
</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>
: {device.totalFriend ?? "-"}
</div>
<div
style={{
fontSize: 13,
color:
device.status === "online" || device.alive === 1
? "#52c41a"
: "#aaa",
marginTop: 4,
}}
>
{device.status === "online" || device.alive === 1
? "在线"
: "离线"}
</div>
</div>
{/* 标签页 */}
<Tabs activeKey={tab} onChange={setTab} style={{ marginBottom: 12 }}>
<Tabs.Tab title="功能开关" key="info" />
<Tabs.Tab title="关联账号" key="accounts" />
<Tabs.Tab title="操作日志" key="logs" />
</Tabs>
{/* 功能开关 */}
{tab === "info" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
display: "flex",
flexDirection: "column",
gap: 18,
}}
>
{["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map(
(f) => (
<div
key={f}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div>
<div style={{ fontWeight: 500 }}>{getFeatureName(f)}</div>
</div>
<Switch
checked={
!!device.features?.[f as keyof Device["features"]]
}
loading={!!featureSaving[f]}
onChange={(checked) =>
handleFeatureChange(
f as keyof Device["features"],
checked
)
}
/>
</div>
)
)}
</div>
)}
{/* 关联账号 */}
{tab === "accounts" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
}}
>
{accountsLoading ? (
<div
style={{ textAlign: "center", color: "#888", padding: 32 }}
>
<SpinLoading />
</div>
) : accounts.length === 0 ? (
<div
style={{ textAlign: "center", color: "#aaa", padding: 32 }}
>
</div>
) : (
<div
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
{accounts.map((acc) => (
<div
key={acc.id}
style={{
display: "flex",
alignItems: "center",
gap: 12,
background: "#f7f8fa",
borderRadius: 8,
padding: 10,
}}
>
<img
src={acc.avatar || "/placeholder.svg"}
alt={acc.nickname}
style={{
width: 40,
height: 40,
borderRadius: 20,
background: "#eee",
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>{acc.nickname}</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {acc.wechatId}
</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {acc.totalFriend}
</div>
<div style={{ fontSize: 12, color: "#aaa" }}>
: {acc.lastActive}
</div>
</div>
<span
style={{
fontSize: 12,
color: acc.wechatAlive === 1 ? "#52c41a" : "#aaa",
}}
>
{acc.wechatAliveText}
</span>
</div>
))}
</div>
)}
<div style={{ textAlign: "center", marginTop: 16 }}>
<Button size="small" onClick={loadAccounts}>
<RedoOutlined />
</Button>
</div>
</div>
)}
{/* 操作日志 */}
{tab === "logs" && (
<div
style={{
background: "#fff",
borderRadius: 12,
padding: 16,
boxShadow: "0 1px 4px #eee",
}}
>
{logsLoading ? (
<div
style={{ textAlign: "center", color: "#888", padding: 32 }}
>
<SpinLoading />
</div>
) : logs.length === 0 ? (
<div
style={{ textAlign: "center", color: "#aaa", padding: 32 }}
>
</div>
) : (
<div
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
{logs.map((log) => (
<div
key={log.id}
style={{
display: "flex",
flexDirection: "column",
gap: 2,
background: "#f7f8fa",
borderRadius: 8,
padding: 10,
}}
>
<div style={{ fontWeight: 500 }}>{log.content}</div>
<div style={{ fontSize: 12, color: "#888" }}>
: {log.username} · {log.createTime}
</div>
</div>
))}
</div>
)}
<div style={{ textAlign: "center", marginTop: 16 }}>
<Button size="small" onClick={loadLogs}>
<RedoOutlined />
</Button>
</div>
</div>
)}
</div>
)}
</Layout>
);
};
export default DeviceDetail;

View File

@@ -1,286 +1,294 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Switch, Input, message, Dropdown, Menu } from "antd";
import { NavBar, Button } from "antd-mobile";
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 (
<Layout
header={
<>
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div className="nav-title">
<ArrowLeftOutlined
twoToneColor="#1677ff"
onClick={() => navigate("/workspace")}
/>
</div>
}
right={
<Button
size="small"
color="primary"
onClick={() => navigate("/workspace/moments-sync/new")}
>
<PlusOutlined />
</Button>
}
>
<span className="nav-title"></span>
</NavBar>
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索任务名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={fetchTasks}
loading={loading}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</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>
);
};
export default MomentsSync;
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Switch, Input, message, Dropdown, Menu } from "antd";
import { NavBar, Button } from "antd-mobile";
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 (
<Layout
header={
<>
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div className="nav-title">
<ArrowLeftOutlined
twoToneColor="#1677ff"
onClick={() => navigate("/workspace")}
/>
</div>
}
right={
<Button
size="small"
color="primary"
onClick={() => navigate("/workspace/moments-sync/new")}
>
<PlusOutlined />
</Button>
}
>
<span className="nav-title"></span>
</NavBar>
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索任务名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={fetchTasks}
loading={loading}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</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
className={style.moreBtn}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
tabIndex={0}
aria-label="更多操作"
>
<MoreOutlined />
</button>
</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>
);
};
export default MomentsSync;

View File

@@ -1,7 +1,13 @@
.formBg {
padding: 16px;
}
.formStepBtnRow{
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
padding: 12px;
}
.formSteps {
display: flex;
justify-content: center;

View File

@@ -1,8 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button, Input, Switch, message, Spin } from "antd";
import { ArrowLeftOutlined } from "@ant-design/icons";
import { NavBar } from "antd-mobile";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
@@ -15,6 +14,7 @@ import {
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import ContentLibrarySelection from "@/components/ContentLibrarySelection";
import NavCommon from "@/components/NavCommon";
const steps = [
{ id: 1, title: "基础设置", subtitle: "基础设置" },
@@ -27,10 +27,15 @@ const defaultForm = {
startTime: "06:00",
endTime: "23:59",
syncCount: 5,
accountType: "business" as "business" | "personal",
syncInterval: 30,
syncType: 1, // 1=业务号 2=人设号
accountType: "business" as "business" | "personal", // 仅UI用
enabled: true,
selectedDevices: [] as string[],
selectedLibraries: [] as string[],
selectedLibraries: [] as (string | number)[],
contentTypes: ["text", "image", "video"],
targetTags: [] as string[],
filterKeywords: [] as string[],
};
const NewMomentsSync: React.FC = () => {
@@ -53,10 +58,15 @@ const NewMomentsSync: React.FC = () => {
startTime: res.timeRange?.start || "06:00",
endTime: res.timeRange?.end || "23:59",
syncCount: res.config?.syncCount || res.syncCount || 5,
syncInterval: res.config?.syncInterval || res.syncInterval || 30,
syncType: res.accountType === 1 ? 1 : 2,
accountType: res.accountType === 1 ? "business" : "personal",
enabled: res.status === 1,
selectedDevices: res.config?.devices || [],
selectedLibraries: res.config?.contentLibraryNames || [],
contentTypes: res.config?.contentTypes || ["text", "image", "video"],
targetTags: res.config?.targetTags || [],
filterKeywords: res.config?.filterKeywords || [],
});
}
} catch {
@@ -79,6 +89,15 @@ const NewMomentsSync: React.FC = () => {
setFormData((prev) => ({ ...prev, ...data }));
};
// UI选择账号类型时同步syncType和accountType
const handleAccountTypeChange = (type: "business" | "personal") => {
setFormData((prev) => ({
...prev,
accountType: type,
syncType: type === "business" ? 1 : 2,
}));
};
// 提交
const handleSubmit = async () => {
if (!formData.taskName.trim()) {
@@ -98,13 +117,18 @@ const NewMomentsSync: React.FC = () => {
const params = {
name: formData.taskName,
devices: formData.selectedDevices,
contentLibraries: formData.selectedLibraries,
contentLibraries: formData.selectedLibraries.map(Number),
syncInterval: formData.syncInterval,
syncCount: formData.syncCount,
syncType: formData.syncType, // 账号类型真实传参
accountType: formData.accountType === "business" ? 1 : 2, // 也要传
startTime: formData.startTime,
endTime: formData.endTime,
accountType: formData.accountType === "business" ? 1 : 2,
status: formData.enabled ? 1 : 2,
contentTypes: formData.contentTypes,
targetTags: formData.targetTags,
filterKeywords: formData.filterKeywords,
type: 2,
status: formData.enabled ? 1 : 2,
};
if (isEditMode && id) {
await updateMomentsSync({ id, ...params });
@@ -122,7 +146,7 @@ const NewMomentsSync: React.FC = () => {
}
};
// 步骤内容
// 步骤内容(去除按钮)
const renderStep = () => {
if (currentStep === 0) {
return (
@@ -166,7 +190,7 @@ const NewMomentsSync: React.FC = () => {
updateForm({ syncCount: Math.max(1, formData.syncCount - 1) })
}
>
-
<MinusOutlined />
</button>
<span className={style.counterValue}>{formData.syncCount}</span>
<button
@@ -175,7 +199,7 @@ const NewMomentsSync: React.FC = () => {
updateForm({ syncCount: formData.syncCount + 1 })
}
>
+
<PlusOutlined />
</button>
<span className={style.counterUnit}></span>
</div>
@@ -186,13 +210,13 @@ const NewMomentsSync: React.FC = () => {
<div className={style.accountTypeRow}>
<button
className={`${style.accountTypeBtn} ${formData.accountType === "business" ? style.accountTypeActive : ""}`}
onClick={() => updateForm({ accountType: "business" })}
onClick={() => handleAccountTypeChange("business")}
>
</button>
<button
className={`${style.accountTypeBtn} ${formData.accountType === "personal" ? style.accountTypeActive : ""}`}
onClick={() => updateForm({ accountType: "personal" })}
onClick={() => handleAccountTypeChange("personal")}
>
</button>
@@ -209,12 +233,6 @@ const NewMomentsSync: React.FC = () => {
/>
</div>
</div>
<div className={style.formStepBtnRow}>
<Button type="primary" onClick={next} className={style.nextBtn}>
</Button>
</div>
</div>
);
}
@@ -231,14 +249,6 @@ const NewMomentsSync: React.FC = () => {
selectedListMaxHeight={200}
/>
</div>
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn}>
</Button>
<Button type="primary" onClick={next} className={style.nextBtn}>
</Button>
</div>
</div>
);
}
@@ -260,19 +270,57 @@ const NewMomentsSync: React.FC = () => {
</div>
)}
</div>
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn}>
</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={loading}
className={style.completeBtn}
>
</Button>
</div>
</div>
);
}
return null;
};
// 统一底部按钮
const renderFooter = () => {
if (loading) return null;
if (currentStep === 0) {
return (
<div className={style.formStepBtnRow}>
<Button
type="primary"
disabled={!formData.taskName.trim()}
onClick={next}
className={style.nextBtn}
block
>
</Button>
</div>
);
}
if (currentStep === 1) {
return (
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn} block>
</Button>
<Button type="primary" onClick={next} className={style.nextBtn} block>
</Button>
</div>
);
}
if (currentStep === 2) {
return (
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn} block>
</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={loading}
className={style.completeBtn}
block
>
</Button>
</div>
);
}
@@ -282,21 +330,9 @@ const NewMomentsSync: React.FC = () => {
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div className="nav-title">
<ArrowLeftOutlined
twoToneColor="#1677ff"
onClick={() => navigate(-1)}
/>
</div>
}
>
{isEditMode ? "编辑朋友圈同步" : "新建朋友圈同步"}
</NavBar>
<NavCommon title={isEditMode ? "编辑朋友圈同步" : "新建朋友圈同步"} />
}
footer={renderFooter()}
>
<div className={style.formBg}>
<StepIndicator currentStep={currentStep + 1} steps={steps} />