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

View File

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

View File

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

View File

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