diff --git a/nkebao/.env.development b/nkebao/.env.development index fde60295..da6a111b 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,5 +1,4 @@ # 基础环境变量示例 VITE_API_BASE_URL=http://www.yishi.com -# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/components/DeviceSelection/data.ts b/nkebao/src/components/DeviceSelection/data.ts new file mode 100644 index 00000000..612e9c61 --- /dev/null +++ b/nkebao/src/components/DeviceSelection/data.ts @@ -0,0 +1,26 @@ +// 设备选择项接口 +export interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +// 组件属性接口 +export interface DeviceSelectionProps { + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + placeholder?: string; + className?: string; + mode?: "input" | "dialog"; // 新增,默认input + open?: boolean; // 仅mode=dialog时生效 + onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效 + selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 + showInput?: boolean; // 新增 + showSelectedList?: boolean; // 新增 + readonly?: boolean; // 新增 +} diff --git a/nkebao/src/components/DeviceSelection/index.module.scss b/nkebao/src/components/DeviceSelection/index.module.scss index d457fa24..c209c95f 100644 --- a/nkebao/src/components/DeviceSelection/index.module.scss +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -19,12 +19,7 @@ background: #f8f9fa; } -.popupContainer { - display: flex; - flex-direction: column; - height: 100vh; - background: #fff; -} + .popupHeader { padding: 16px; border-bottom: 1px solid #f0f0f0; diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index a62625d2..9e7f1ffe 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,39 +1,10 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Checkbox, Popup, Toast } from "antd-mobile"; +import React, { useState } from "react"; +import { SearchOutlined } from "@ant-design/icons"; import { Input, Button } from "antd"; -import { getDeviceList } from "./api"; -import style from "./index.module.scss"; import { DeleteOutlined } from "@ant-design/icons"; - -// 设备选择项接口 -interface DeviceSelectionItem { - id: string; - name: string; - imei: string; - wechatId: string; - status: "online" | "offline"; - wxid?: string; - nickname?: string; - usedInPlans?: number; -} - -// 组件属性接口 -interface DeviceSelectionProps { - selectedDevices: string[]; - onSelect: (devices: string[]) => void; - placeholder?: string; - className?: string; - mode?: "input" | "dialog"; // 新增,默认input - open?: boolean; // 仅mode=dialog时生效 - onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效 - selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 - showInput?: boolean; // 新增 - showSelectedList?: boolean; // 新增 - readonly?: boolean; // 新增 -} - -const PAGE_SIZE = 20; +import { DeviceSelectionProps } from "./data"; +import SelectionPopup from "./selectionPopup"; +import style from "./index.module.scss"; const DeviceSelection: React.FC = ({ selectedDevices, @@ -57,92 +28,10 @@ const DeviceSelection: React.FC = ({ if (!isDialog) setPopupVisible(v); }; - // 设备数据 - const [devices, setDevices] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [loading, setLoading] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [total, setTotal] = useState(0); - - // 获取设备列表,支持keyword和分页 - const fetchDevices = useCallback( - async (keyword: string = "", page: number = 1) => { - setLoading(true); - try { - const res = await getDeviceList({ - page, - limit: PAGE_SIZE, - keyword: keyword.trim() || undefined, - }); - if (res && Array.isArray(res.list)) { - setDevices( - res.list.map((d: any) => ({ - id: d.id?.toString() || "", - name: d.memo || d.imei || "", - imei: d.imei || "", - wechatId: d.wechatId || "", - status: d.alive === 1 ? "online" : "offline", - wxid: d.wechatId || "", - nickname: d.nickname || "", - usedInPlans: d.usedInPlans || 0, - })) - ); - setTotal(res.total || 0); - } - } catch (error) { - console.error("获取设备列表失败:", error); - } finally { - setLoading(false); - } - }, - [] - ); - - // 打开弹窗时获取第一页 + // 打开弹窗 const openPopup = () => { if (readonly) return; - setSearchQuery(""); - setCurrentPage(1); setRealVisible(true); - fetchDevices("", 1); - }; - - // 搜索防抖 - useEffect(() => { - if (!realVisible) return; - const timer = setTimeout(() => { - setCurrentPage(1); - fetchDevices(searchQuery, 1); - }, 500); - return () => clearTimeout(timer); - }, [searchQuery, realVisible, fetchDevices]); - - // 翻页时重新请求 - useEffect(() => { - if (!realVisible) return; - fetchDevices(searchQuery, currentPage); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - - // 过滤设备(只保留状态过滤) - const filteredDevices = devices.filter((device) => { - const matchesStatus = - statusFilter === "all" || - (statusFilter === "online" && device.status === "online") || - (statusFilter === "offline" && device.status === "offline"); - return matchesStatus; - }); - - const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); - - // 处理设备选择 - const handleDeviceToggle = (deviceId: string) => { - if (selectedDevices.includes(deviceId)) { - onSelect(selectedDevices.filter((id) => id !== deviceId)); - } else { - onSelect([...selectedDevices, deviceId]); - } }; // 获取显示文本 @@ -151,138 +40,12 @@ const DeviceSelection: React.FC = ({ return `已选择 ${selectedDevices.length} 个设备`; }; - // 获取已选设备详细信息 - const selectedDeviceObjs = selectedDevices - .map((id) => devices.find((d) => d.id === id)) - .filter(Boolean) as DeviceSelectionItem[]; - // 删除已选设备 const handleRemoveDevice = (id: string) => { if (readonly) return; onSelect(selectedDevices.filter((d) => d !== id)); }; - // 弹窗内容 - const popupContent = ( -
-
-
选择设备
-
-
-
- - setSearchQuery(val)} - className={style.popupSearchInput} - /> -
- - -
-
- {loading ? ( -
-
加载中...
-
- ) : ( -
- {filteredDevices.map((device) => ( - - ))} -
- )} -
- {/* 分页栏 */} -
-
总计 {total} 个设备
-
- - - {currentPage} / {totalPages} - - -
-
-
-
- 已选择 {selectedDevices.length} 个设备 -
-
- - -
-
-
- ); - return ( <> {/* mode=input 显示输入框,mode=dialog不显示 */} @@ -304,75 +67,71 @@ const DeviceSelection: React.FC = ({ )} {/* 已选设备列表窗口 */} - {mode === "input" && - showSelectedList && - selectedDeviceObjs.length > 0 && ( -
- {selectedDeviceObjs.map((device) => ( + {mode === "input" && showSelectedList && selectedDevices.length > 0 && ( +
+ {selectedDevices.map((deviceId) => ( +
-
- {device.name} -
- {!readonly && ( -
- ))} -
- )} + {!readonly && ( +
+ ))} +
+ )} {/* 弹窗 */} - setRealVisible(false)} - position="bottom" - bodyStyle={{ height: "100vh" }} - > - {popupContent} - + onClose={() => setRealVisible(false)} + selectedDevices={selectedDevices} + onSelect={onSelect} + /> ); }; diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx index 8f77eabc..c846e3cc 100644 --- a/nkebao/src/components/DeviceSelection/selectionPopup.tsx +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -1,7 +1,10 @@ -import React from "react"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup } from "antd-mobile"; +import React, { useState, useEffect, useCallback } from "react"; +import { Checkbox, Popup } from "antd-mobile"; +import { getDeviceList } from "./api"; import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; interface DeviceSelectionItem { id: string; @@ -19,42 +22,95 @@ interface SelectionPopupProps { onClose: () => void; selectedDevices: string[]; onSelect: (devices: string[]) => void; - devices: DeviceSelectionItem[]; - loading: boolean; - searchQuery: string; - setSearchQuery: (v: string) => void; - statusFilter: string; - setStatusFilter: (v: string) => void; - onRefresh: () => void; - filteredDevices: DeviceSelectionItem[]; - total: number; - currentPage: number; - totalPages: number; - setCurrentPage: (v: number) => void; - onCancel: () => void; - onConfirm: () => void; } +const PAGE_SIZE = 20; + const SelectionPopup: React.FC = ({ visible, onClose, selectedDevices, onSelect, - devices, - loading, - searchQuery, - setSearchQuery, - statusFilter, - setStatusFilter, - onRefresh, - filteredDevices, - total, - currentPage, - totalPages, - setCurrentPage, - onCancel, - onConfirm, }) => { + // 设备数据 + const [devices, setDevices] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const res = await getDeviceList({ + page, + limit: PAGE_SIZE, + keyword: keyword.trim() || undefined, + }); + if (res && Array.isArray(res.list)) { + setDevices( + res.list.map((d: any) => ({ + id: d.id?.toString() || "", + name: d.memo || d.imei || "", + imei: d.imei || "", + wechatId: d.wechatId || "", + status: d.alive === 1 ? "online" : "offline", + wxid: d.wechatId || "", + nickname: d.nickname || "", + usedInPlans: d.usedInPlans || 0, + })) + ); + setTotal(res.total || 0); + } + } catch (error) { + console.error("获取设备列表失败:", error); + } finally { + setLoading(false); + } + }, + [] + ); + + // 打开弹窗时获取第一页 + useEffect(() => { + if (visible) { + setSearchQuery(""); + setCurrentPage(1); + fetchDevices("", 1); + } + }, [visible, fetchDevices]); + + // 搜索防抖 + useEffect(() => { + if (!visible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchDevices(searchQuery, 1); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, visible, fetchDevices]); + + // 翻页时重新请求 + useEffect(() => { + if (!visible) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + + // 过滤设备(只保留状态过滤) + const filteredDevices = devices.filter((device) => { + const matchesStatus = + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); + return matchesStatus; + }); + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + // 处理设备选择 const handleDeviceToggle = (deviceId: string) => { if (selectedDevices.includes(deviceId)) { @@ -67,49 +123,45 @@ const SelectionPopup: React.FC = ({ return ( {}} + onMaskClick={onClose} position="bottom" bodyStyle={{ height: "100vh" }} closeOnMaskClick={false} > -
-
-
选择设备
-
-
-
- - -
- - -
+ fetchDevices(searchQuery, currentPage)} + showTabs={true} + tabsConfig={{ + activeKey: statusFilter, + onChange: setStatusFilter, + tabs: [ + { title: "全部", key: "all" }, + { title: "在线", key: "online" }, + { title: "离线", key: "offline" }, + ], + }} + /> + } + footer={ + + } + >
{loading ? (
@@ -147,49 +199,7 @@ const SelectionPopup: React.FC = ({
)}
- {/* 分页栏 */} -
-
总计 {total} 个设备
-
- - - {currentPage} / {totalPages} - - -
-
-
-
- 已选择 {selectedDevices.length} 个设备 -
-
- - -
-
-
+
); }; diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index 51598c53..bb5c69a9 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -1,9 +1,12 @@ import React, { useState, useEffect } from "react"; import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; -import { Popup, Toast } from "antd-mobile"; +import { Popup } from "antd-mobile"; import { Button, Input } from "antd"; import { getFriendList } from "./api"; import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; // 微信好友接口类型 interface WechatFriend { @@ -104,35 +107,18 @@ export default function FriendSelection({ params.keyword = keyword.trim(); } - if (enableDeviceFilter) { - if (deviceIds.length === 0) { - setFriends([]); - setTotalFriends(0); - setTotalPages(1); - setLoading(false); - return; - } - params.deviceIds = deviceIds.join(","); + if (enableDeviceFilter && deviceIds.length > 0) { + params.deviceIds = deviceIds; } - const res = await getFriendList(params); - - if (res && Array.isArray(res.list)) { - setFriends( - res.list.map((friend: any) => ({ - id: friend.id?.toString() || "", - nickname: friend.nickname || "", - wechatId: friend.wechatId || "", - avatar: friend.avatar || "", - customer: friend.customer || "", - })) - ); - setTotalFriends(res.total || 0); - setTotalPages(Math.ceil((res.total || 0) / 20)); + const response = await getFriendList(params); + if (response && response.list) { + setFriends(response.list); + setTotalFriends(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); } } catch (error) { console.error("获取好友列表失败:", error); - Toast.show({ content: "获取好友列表失败", position: "top" }); } finally { setLoading(false); } @@ -140,16 +126,20 @@ export default function FriendSelection({ // 处理好友选择 const handleFriendToggle = (friendId: string) => { - let newIds: string[]; - if (selectedFriends.includes(friendId)) { - newIds = selectedFriends.filter((id) => id !== friendId); - } else { - newIds = [...selectedFriends, friendId]; - } - onSelect(newIds); + if (readonly) return; + + const newSelectedFriends = selectedFriends.includes(friendId) + ? selectedFriends.filter((id) => id !== friendId) + : [...selectedFriends, friendId]; + + onSelect(newSelectedFriends); + + // 如果有 onSelectDetail 回调,传递完整的好友对象 if (onSelectDetail) { - const selectedObjs = friends.filter((f) => newIds.includes(f.id)); - onSelectDetail(selectedObjs); + const selectedFriendObjs = friends.filter((friend) => + newSelectedFriends.includes(friend.id) + ); + onSelectDetail(selectedFriendObjs); } }; @@ -160,29 +150,22 @@ export default function FriendSelection({ }; // 获取已选好友详细信息 - const selectedFriendObjs = selectedFriends - .map((id) => friends.find((f) => f.id === id)) - .filter(Boolean) as WechatFriend[]; + const selectedFriendObjs = friends.filter((friend) => + selectedFriends.includes(friend.id) + ); // 删除已选好友 const handleRemoveFriend = (id: string) => { if (readonly) return; - onSelect(selectedFriends.filter((f) => f !== id)); + onSelect(selectedFriends.filter((d) => d !== id)); }; - // 确认按钮逻辑 + // 确认选择 const handleConfirm = () => { - setRealVisible(false); if (onConfirm) { onConfirm(selectedFriends, selectedFriendObjs); } - }; - - // 清空搜索 - const handleClearSearch = () => { - setSearchQuery(""); - setCurrentPage(1); - fetchFriends(1, ""); + setRealVisible(false); }; return ( @@ -271,40 +254,30 @@ export default function FriendSelection({ position="bottom" bodyStyle={{ height: "100vh" }} > -
-
-
选择微信好友
-
- setSearchQuery(e.target.value)} - disabled={readonly} - prefix={} - allowClear - size="large" - /> - {searchQuery && !readonly && ( -
-
+ fetchFriends(currentPage, searchQuery)} + /> + } + footer={ + setRealVisible(false)} + onConfirm={handleConfirm} + /> + } + >
{loading ? (
@@ -372,50 +345,7 @@ export default function FriendSelection({
)}
- {/* 分页栏 */} -
-
总计 {totalFriends} 个好友
-
- - - {currentPage} / {totalPages} - - -
-
- {/* 底部按钮栏 */} -
-
- 已选择 {selectedFriends.length} 个好友 -
-
- - -
-
-
+ ); diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx index 15d2222a..2fb925b5 100644 --- a/nkebao/src/components/GroupSelection/index.tsx +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -1,15 +1,12 @@ import React, { useState, useEffect } from "react"; -import { - SearchOutlined, - CloseOutlined, - LeftOutlined, - RightOutlined, - DeleteOutlined, -} from "@ant-design/icons"; -import { Button as AntdButton, Input as AntdInput } from "antd"; -import { Popup, Toast } from "antd-mobile"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Button, Input } from "antd"; +import { Popup } from "antd-mobile"; import { getGroupList } from "./api"; import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; // 群组接口类型 interface WechatGroup { @@ -61,9 +58,9 @@ export default function GroupSelection({ const [loading, setLoading] = useState(false); // 获取已选群聊详细信息 - const selectedGroupObjs = selectedGroups - .map((id) => groups.find((g) => g.id === id)) - .filter(Boolean) as WechatGroup[]; + const selectedGroupObjs = groups.filter((group) => + selectedGroups.includes(group.id) + ); // 删除已选群聊 const handleRemoveGroup = (id: string) => { @@ -101,58 +98,52 @@ export default function GroupSelection({ setCurrentPage(1); fetchGroups(1, searchQuery); }, 500); + return () => clearTimeout(timer); }, [searchQuery, realVisible]); - // 获取群组列表API - 支持keyword + // 获取群聊列表API const fetchGroups = async (page: number, keyword: string = "") => { setLoading(true); try { - const params: any = { + let params: any = { page, limit: 20, }; + if (keyword.trim()) { params.keyword = keyword.trim(); } - const res = await getGroupList(params); - - if (res && Array.isArray(res.list)) { - setGroups( - res.list.map((group: any) => ({ - id: group.id?.toString() || "", - chatroomId: group.chatroomId || "", - name: group.name || "", - avatar: group.avatar || "", - ownerWechatId: group.ownerWechatId || "", - ownerNickname: group.ownerNickname || "", - ownerAvatar: group.ownerAvatar || "", - })) - ); - setTotalGroups(res.total || 0); - setTotalPages(Math.ceil((res.total || 0) / 20)); + const response = await getGroupList(params); + if (response && response.list) { + setGroups(response.list); + setTotalGroups(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); } } catch (error) { - console.error("获取群组列表失败:", error); - Toast.show({ content: "获取群组列表失败", position: "top" }); + console.error("获取群聊列表失败:", error); } finally { setLoading(false); } }; - // 处理群组选择 + // 处理群聊选择 const handleGroupToggle = (groupId: string) => { - let newIds: string[]; - if (selectedGroups.includes(groupId)) { - newIds = selectedGroups.filter((id) => id !== groupId); - } else { - newIds = [...selectedGroups, groupId]; - } - onSelect(newIds); + if (readonly) return; + + const newSelectedGroups = selectedGroups.includes(groupId) + ? selectedGroups.filter((id) => id !== groupId) + : [...selectedGroups, groupId]; + + onSelect(newSelectedGroups); + + // 如果有 onSelectDetail 回调,传递完整的群聊对象 if (onSelectDetail) { - const selectedObjs = groups.filter((g) => newIds.includes(g.id)); - onSelectDetail(selectedObjs); + const selectedGroupObjs = groups.filter((group) => + newSelectedGroups.includes(group.id) + ); + onSelectDetail(selectedGroupObjs); } }; @@ -162,19 +153,12 @@ export default function GroupSelection({ return `已选择 ${selectedGroups.length} 个群聊`; }; - // 确认按钮逻辑 + // 确认选择 const handleConfirm = () => { - setRealVisible(false); if (onConfirm) { onConfirm(selectedGroups, selectedGroupObjs); } - }; - - // 清空搜索 - const handleClearSearch = () => { - setSearchQuery(""); - setCurrentPage(1); - fetchGroups(1, ""); + setRealVisible(false); }; return ( @@ -182,7 +166,7 @@ export default function GroupSelection({ {/* 输入框 */} {showInput && (
- {!readonly && ( - } size="small" @@ -263,41 +247,30 @@ export default function GroupSelection({ position="bottom" bodyStyle={{ height: "100vh" }} > -
-
-
选择群聊
-
- setSearchQuery(e.target.value)} - disabled={readonly} - prefix={} - allowClear - size="large" - /> - - {searchQuery && !readonly && ( - } - size="small" - className={style.clearBtn} - onClick={handleClearSearch} - style={{ - color: "#ff4d4f", - border: "none", - background: "none", - minWidth: 24, - height: 24, - display: "flex", - alignItems: "center", - justifyContent: "center", - }} - /> - )} -
-
+ fetchGroups(currentPage, searchQuery)} + /> + } + footer={ + setRealVisible(false)} + onConfirm={handleConfirm} + /> + } + >
{loading ? (
@@ -361,52 +334,7 @@ export default function GroupSelection({
)}
- {/* 分页栏 */} -
-
总计 {totalGroups} 个群聊
-
- setCurrentPage(Math.max(1, currentPage - 1))} - disabled={currentPage === 1 || loading} - className={style.pageBtn} - style={{ borderRadius: 16 }} - > - - - - {currentPage} / {totalPages} - - - setCurrentPage(Math.min(totalPages, currentPage + 1)) - } - disabled={currentPage === totalPages || loading} - className={style.pageBtn} - style={{ borderRadius: 16 }} - > - - -
-
- {/* 底部按钮栏 */} -
-
- 已选择 {selectedGroups.length} 个群聊 -
-
- setRealVisible(false)}> - 取消 - - - 确定 - -
-
-
+ ); diff --git a/nkebao/src/components/NavCommon/index.tsx b/nkebao/src/components/NavCommon/index.tsx new file mode 100644 index 00000000..d047afb7 --- /dev/null +++ b/nkebao/src/components/NavCommon/index.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { NavBar } from "antd-mobile"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; + +interface NavCommonProps { + title: string; + backFn?: () => void; + right?: React.ReactNode; +} + +const NavCommon: React.FC = ({ title, backFn, right }) => { + const navigate = useNavigate(); + return ( + + { + if (backFn) { + backFn(); + } else { + navigate(-1); + } + }} + /> +
+ } + right={right} + > + + {title} + + + ); +}; + +export default NavCommon; diff --git a/nkebao/src/components/PopuLayout/footer.module.scss b/nkebao/src/components/PopuLayout/footer.module.scss new file mode 100644 index 00000000..ae36d681 --- /dev/null +++ b/nkebao/src/components/PopuLayout/footer.module.scss @@ -0,0 +1,71 @@ +.popupFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fff; +} + +.selectedCount { + font-size: 14px; + color: #888; +} + +.footerBtnGroup { + display: flex; + gap: 12px; +} + +.paginationRow { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} + +.totalCount { + font-size: 14px; + color: #888; +} + +.paginationControls { + display: flex; + align-items: center; + gap: 8px; +} + +.pageBtn { + padding: 0 8px; + height: 32px; + min-width: 32px; + border-radius: 16px; + border: 1px solid #d9d9d9; + color: #333; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + + &:hover:not(:disabled) { + border-color: #1677ff; + color: #1677ff; + } + + &:disabled { + background: #f5f5f5; + color: #ccc; + cursor: not-allowed; + } +} + +.pageInfo { + font-size: 14px; + color: #222; + margin: 0 8px; + min-width: 60px; + text-align: center; +} \ No newline at end of file diff --git a/nkebao/src/components/PopuLayout/footer.tsx b/nkebao/src/components/PopuLayout/footer.tsx new file mode 100644 index 00000000..0bea8d16 --- /dev/null +++ b/nkebao/src/components/PopuLayout/footer.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { Button } from "antd"; +import style from "./footer.module.scss"; +import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons"; + +interface PopupFooterProps { + total: number; + currentPage: number; + totalPages: number; + loading: boolean; + selectedCount: number; + onPageChange: (page: number) => void; + onCancel: () => void; + onConfirm: () => void; +} + +const PopupFooter: React.FC = ({ + total, + currentPage, + totalPages, + loading, + selectedCount, + onPageChange, + onCancel, + onConfirm, +}) => { + return ( + <> + {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
已选择 {selectedCount} 个设备
+
+ + +
+
+ + ); +}; + +export default PopupFooter; diff --git a/nkebao/src/components/PopuLayout/header.module.scss b/nkebao/src/components/PopuLayout/header.module.scss new file mode 100644 index 00000000..b98ca843 --- /dev/null +++ b/nkebao/src/components/PopuLayout/header.module.scss @@ -0,0 +1,52 @@ +.popupHeader { + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.popupTitle { + font-size: 20px; + font-weight: 600; + text-align: center; +} + +.popupSearchRow { + display: flex; + align-items: center; + gap: 5px; + padding: 16px; +} + +.popupSearchInputWrap { + position: relative; + flex: 1; +} + +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + z-index: 10; + font-size: 18px; +} + + +.refreshBtn { + width: 36px; + height: 36px; +} + +.loadingIcon { + animation: spin 1s linear infinite; + font-size: 16px; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/nkebao/src/components/PopuLayout/header.tsx b/nkebao/src/components/PopuLayout/header.tsx new file mode 100644 index 00000000..17db0c63 --- /dev/null +++ b/nkebao/src/components/PopuLayout/header.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button } from "antd"; +import { Tabs } from "antd-mobile"; +import style from "./header.module.scss"; + +interface PopupHeaderProps { + title: string; + searchQuery: string; + setSearchQuery: (value: string) => void; + searchPlaceholder?: string; + loading?: boolean; + onRefresh?: () => void; + showRefresh?: boolean; + showSearch?: boolean; + showTabs?: boolean; + tabsConfig?: { + activeKey: string; + onChange: (key: string) => void; + tabs: Array<{ title: string; key: string }>; + }; +} + +const PopupHeader: React.FC = ({ + title, + searchQuery, + setSearchQuery, + searchPlaceholder = "搜索...", + loading = false, + onRefresh, + showRefresh = true, + showSearch = true, + showTabs = false, + tabsConfig, +}) => { + return ( + <> +
+
{title}
+
+ + {showSearch && ( +
+
+ setSearchQuery(e.target.value)} + prefix={} + size="large" + /> +
+ + {showRefresh && onRefresh && ( + + )} +
+ )} + + {showTabs && tabsConfig && ( + + {tabsConfig.tabs.map((tab) => ( + + ))} + + )} + + ); +}; + +export default PopupHeader; diff --git a/nkebao/src/components/StepIndicator/index.tsx b/nkebao/src/components/StepIndicator/index.tsx index 5702c77f..38cb7509 100644 --- a/nkebao/src/components/StepIndicator/index.tsx +++ b/nkebao/src/components/StepIndicator/index.tsx @@ -11,7 +11,7 @@ const StepIndicator: React.FC = ({ steps, }) => { return ( -
+
{steps.map((step, idx) => ( { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState("devices"); + + // 设备选择状态 + const [selectedDevices, setSelectedDevices] = useState([]); + + // 好友选择状态 + const [selectedFriends, setSelectedFriends] = useState([]); + + // 群组选择状态 + const [selectedGroups, setSelectedGroups] = useState([]); + + return ( + }> +
+ + +
+

DeviceSelection 组件测试

+ +
+ 已选设备: {selectedDevices.length} 个 +
+ 设备ID: {selectedDevices.join(", ") || "无"} +
+
+
+ + +
+

FriendSelection 组件测试

+ +
+ 已选好友: {selectedFriends.length} 个 +
+ 好友ID: {selectedFriends.join(", ") || "无"} +
+
+
+ + +
+

GroupSelection 组件测试

+ +
+ 已选群组: {selectedGroups.length} 个 +
+ 群组ID: {selectedGroups.join(", ") || "无"} +
+
+
+
+
+
+ ); +}; + +export default ComponentTest; diff --git a/nkebao/src/pages/scenarios/plan/new/index.tsx b/nkebao/src/pages/scenarios/plan/new/index.tsx index a3987023..96bae635 100644 --- a/nkebao/src/pages/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/scenarios/plan/new/index.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { message } from "antd"; -import { NavBar } from "antd-mobile"; -import { ArrowLeftOutlined } from "@ant-design/icons"; +import NavCommon from "@/components/NavCommon"; import BasicSettings from "./steps/BasicSettings"; import FriendRequestSettings from "./steps/FriendRequestSettings"; import MessageSettings from "./steps/MessageSettings"; @@ -205,25 +204,8 @@ export default function NewPlan() { - - router(-1)} - /> -
- } - > - - {isEdit ? "编辑朋友圈同步" : "新建朋友圈同步"} - - -
- -
+ + } > diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx index 1f989532..dde074fc 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,17 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; -import { - Form, - Input, - Button, - Tag, - Switch, - // Upload, - Modal, - Alert, - Row, - Col, - message, -} from "antd"; +import { Form, Input, Button, Tag, Switch, Modal } from "antd"; +import { Button as ButtonMobile } from "antd-mobile"; import { PlusOutlined, EyeOutlined, @@ -21,6 +10,8 @@ import { CheckOutlined, } from "@ant-design/icons"; import { uploadFile } from "@/api/common"; +import Layout from "@/components/Layout/Layout"; +import styles from "./base.module.scss"; interface BasicSettingsProps { isEdit: boolean; @@ -424,31 +415,31 @@ const BasicSettings: React.FC = ({ formData.scenario !== 1 ? { display: "none" } : { display: "block" }; return ( -
+
{/* 场景选择区块 */} - {sceneLoading ? ( -
加载中...
- ) : ( - - {sceneList.map((scene) => ( - - - - ))} - - )} - + + ); + })} +
+
{/* 计划名称输入区 */} -
计划名称
-
+
计划名称
+
onChange({ ...formData, name: String(e.target.value) }) @@ -456,17 +447,16 @@ const BasicSettings: React.FC = ({ placeholder="请输入计划名称" />
- -
获客标签(可多选)
+
获客标签(可多选)
{/* 标签选择区块 */} {formData.scenario && ( -
+
{(currentScene?.scenarioTags || []).map((tag: string) => ( handleScenarioTagToggle(tag)} - style={{ marginBottom: 4 }} + className={styles["basic-tag-item"]} > {tag} @@ -477,9 +467,9 @@ const BasicSettings: React.FC = ({ key={tag.id} color={selectedScenarioTags.includes(tag.id) ? "blue" : "default"} onClick={() => handleScenarioTagToggle(tag.id)} - style={{ marginBottom: 4 }} closable onClose={() => handleRemoveCustomTag(tag.id)} + className={styles["basic-tag-item"]} > {tag.name} @@ -487,49 +477,33 @@ const BasicSettings: React.FC = ({
)} {/* 自定义标签输入区 */} -
-
- setCustomTagInput(e.target.value)} - placeholder="添加自定义标签" - className="w-full" - /> -
-
- -
+
+ setCustomTagInput(e.target.value)} + placeholder="添加自定义标签" + /> +
- {/* 输入获客成功提示 */} -
-
- { - setTips(e.target.value); - onChange({ ...formData, tips: e.target.value }); - }} - placeholder="请输入获客成功提示" - className="w-full" - /> -
-
- - {/* 选素材 */} -
-
选择海报
-
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); }} - > + placeholder="请输入获客成功提示" + /> +
+ {/* 选素材 */} +
+
选择海报
+
{[...materials, ...customPosters].map((material) => { const isSelected = selectedMaterials.some( (m) => m.id === material.id @@ -538,35 +512,15 @@ const BasicSettings: React.FC = ({ return (
handleMaterialSelect(material)} > {/* 预览按钮:自定义海报在左上,内置海报在右上 */} - + {/* 删除自定义海报按钮 */} {isCustom && (
-
+
支持 CSV、Excel 格式,上传后将文件保存到服务器
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */} {formData.scenario === 5 && ( -
-
-
- 电话获客设置 +
+
+ 电话获客设置 +
+
+
+ 自动加好友 + + setPhoneSettings((s) => ({ ...s, autoAdd: v })) + } + />
-
-
- 自动加好友 - - setPhoneSettings((s) => ({ ...s, autoAdd: v })) - } - /> -
-
- 语音转文字 - - setPhoneSettings((s) => ({ ...s, speechToText: v })) - } - /> -
-
- 问题提取 - - setPhoneSettings((s) => ({ ...s, questionExtraction: v })) - } - /> -
+
+ 语音转文字 + + setPhoneSettings((s) => ({ ...s, speechToText: v })) + } + /> +
+
+ 问题提取 + + setPhoneSettings((s) => ({ ...s, questionExtraction: v })) + } + />
)} {/* 微信群设置区块,仅在选择微信群场景时显示 */} {formData.scenario === 7 && ( -
+
= ({
)} - -
+
是否启用 onChange({ ...formData, enabled: value })} />
- + +
+ + 下一步 + +
); }; diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx index f76e0fb3..16e458d2 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -13,6 +13,7 @@ import { } from "antd"; import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons"; import DeviceSelection from "@/components/DeviceSelection"; +import Layout from "@/components/Layout/Layout"; interface FriendRequestSettingsProps { formData: any; @@ -97,143 +98,151 @@ const FriendRequestSettings: React.FC = ({ return ( <> -
-
- 选择设备 -
- d.id)} - onSelect={(deviceIds) => { - const newSelectedDevices = deviceIds.map((id) => ({ - id, - name: `设备 ${id}`, - status: "online", - })); - setSelectedDevices(newSelectedDevices); - onChange({ ...formData, device: deviceIds }); - }} - placeholder="选择设备" - /> + +
+ + +
+
+ } + > +
+
+ 选择设备 +
+ d.id)} + onSelect={(deviceIds) => { + const newSelectedDevices = deviceIds.map((id) => ({ + id, + name: `设备 ${id}`, + status: "online", + })); + setSelectedDevices(newSelectedDevices); + onChange({ ...formData, device: deviceIds }); + }} + placeholder="选择设备" + /> +
-
-
-
- 好友备注 - setShowRemarkTip(true)} - onMouseLeave={() => setShowRemarkTip(false)} - onClick={() => setShowRemarkTip((v) => !v)} - > - ? - - {showRemarkTip && ( -
-
设置添加好友时的备注格式
-
备注格式预览:
-
- {formData.remarkType === "phone" && - `138****1234+${getScenarioTitle()}`} - {formData.remarkType === "nickname" && - `小红书用户2851+${getScenarioTitle()}`} - {formData.remarkType === "source" && - `抖音直播+${getScenarioTitle()}`} +
+
+ 好友备注 + setShowRemarkTip(true)} + onMouseLeave={() => setShowRemarkTip(false)} + onClick={() => setShowRemarkTip((v) => !v)} + > + ? + + {showRemarkTip && ( +
+
设置添加好友时的备注格式
+
+ 备注格式预览: +
+
+ {formData.remarkType === "phone" && + `138****1234+${getScenarioTitle()}`} + {formData.remarkType === "nickname" && + `小红书用户2851+${getScenarioTitle()}`} + {formData.remarkType === "source" && + `抖音直播+${getScenarioTitle()}`} +
-
- )} -
- -
- -
-
- 招呼语 -
+
- - onChange({ ...formData, greeting: e.target.value }) - } - placeholder="请输入招呼语" - className="mt-2" - /> -
-
- 添加间隔 -
+
+
+ 招呼语 + +
- onChange({ - ...formData, - addFriendInterval: Number(e.target.value), - }) + onChange({ ...formData, greeting: e.target.value }) } - /> -
分钟
-
-
- -
- 允许加人的时间段 -
- - onChange({ ...formData, addFriendTimeStart: e.target.value }) - } - className="w-32" - /> - - - onChange({ ...formData, addFriendTimeEnd: e.target.value }) - } - className="w-32" + placeholder="请输入招呼语" + className="mt-2" />
-
- {hasWarnings && ( - - )} +
+ 添加间隔 +
+ + onChange({ + ...formData, + addFriendInterval: Number(e.target.value), + }) + } + /> +
分钟
+
+
-
- - +
+ 允许加人的时间段 +
+ + onChange({ ...formData, addFriendTimeStart: e.target.value }) + } + className="w-32" + /> + + + onChange({ ...formData, addFriendTimeEnd: e.target.value }) + } + className="w-32" + /> +
+
+ + {hasWarnings && ( + + )}
-
+ = ({ return ( <> -
-
-

消息设置

- -
+ +
+ + +
+
+ } + > +
+
+

消息设置

+ +
- - -
- - +
-
+ {/* 添加天数计划弹窗 */} { - return ( - window.history.back()} - > -
- 微信号详情 -
- - } - footer={} - > -
-

微信号详情页面

-

此页面正在开发中...

-
-
- ); -}; - -export default WechatAccountDetail; diff --git a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx b/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx deleted file mode 100644 index 57b00863..00000000 --- a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx +++ /dev/null @@ -1,37 +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 WechatAccounts: React.FC = () => { - return ( - - 微信号管理 -
- } - right={ - - } - /> - } - footer={} - > -
-

微信号管理页面

-

此页面正在开发中...

-
- - ); -}; - -export default WechatAccounts; diff --git a/nkebao/src/pages/wechat-accounts/detail/api.ts b/nkebao/src/pages/wechat-accounts/detail/api.ts new file mode 100644 index 00000000..40757d27 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/api.ts @@ -0,0 +1,26 @@ +import request from "@/api/request"; + +// 获取微信号详情 +export function getWechatAccountDetail(id: string) { + return request("/WechatAccount/detail", { id }, "GET"); +} + +// 获取微信号summary +export function getWechatAccountSummary(id: string) { + return request(`/v1/wechats/${id}/summary`, {}, "GET"); +} + +// 获取微信号好友列表 +export function getWechatFriends(params: { + wechatAccountKeyword: string; + pageIndex: number; + pageSize: number; + friendKeyword?: string; +}) { + return request("/WechatFriend/friendlistData", params, "POST"); +} + +// 获取微信好友详情 +export function getWechatFriendDetail(id: string) { + return request("/v1/WechatFriend/detail", { id }, "GET"); +} diff --git a/nkebao/src/pages/wechat-accounts/detail/detail.module.scss b/nkebao/src/pages/wechat-accounts/detail/detail.module.scss new file mode 100644 index 00000000..bd0ca84c --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/detail.module.scss @@ -0,0 +1,720 @@ +.wechat-account-detail-page { + padding: 16px; + background: linear-gradient(to bottom, #f0f8ff, #ffffff); + min-height: 100vh; + + .loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + } + + .account-card { + margin-bottom: 16px; + border-radius: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e8f4fd; + + .account-info { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 20px; + + .avatar-section { + position: relative; + + .avatar { + width: 64px; + height: 64px; + border-radius: 50%; + border: 4px solid #e8f4fd; + } + + .status-dot { + position: absolute; + bottom: 2px; + right: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid #fff; + + &.status-normal { + background: #52c41a; + } + + &.status-abnormal { + background: #ff4d4f; + } + } + } + + .info-section { + flex: 1; + min-width: 0; + + .name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + + .nickname { + font-size: 20px; + font-weight: 600; + color: #1a1a1a; + margin: 0; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .status-tag { + font-size: 12px; + padding: 2px 8px; + border-radius: 12px; + } + } + + .wechat-id { + font-size: 14px; + color: #666; + margin: 0 0 12px 0; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .action-btn { + font-size: 12px; + padding: 6px 12px; + border-radius: 8px; + border: 1px solid #d9d9d9; + background: #fff; + color: #666; + transition: all 0.2s; + + &:hover { + background: #f5f5f5; + border-color: #bfbfbf; + } + } + } + } + } + } + + .tabs-card { + border-radius: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e8f4fd; + + .tabs { + .adm-tabs-header { + border-bottom: 1px solid #e8e8e8; + background: #fff; + border-radius: 16px 16px 0 0; + + .adm-tabs-tab { + font-size: 14px; + font-weight: 500; + color: #666; + transition: all 0.2s; + + &.adm-tabs-tab-active { + color: #1677ff; + font-weight: 600; + } + } + + .adm-tabs-tab-line { + background: #1677ff; + height: 2px; + } + } + + .adm-tabs-content { + padding: 16px; + } + } + } + + .overview-content { + .info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 16px; + + .info-card { + background: linear-gradient(135deg, #e6f7ff, #f0f8ff); + padding: 16px; + border-radius: 12px; + border: 1px solid #bae7ff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.3s; + + &:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); + } + + .info-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + .info-icon { + font-size: 16px; + color: #1677ff; + padding: 6px; + background: #e6f7ff; + border-radius: 8px; + } + + .info-title { + flex: 1; + + .title-text { + font-size: 12px; + font-weight: 600; + color: #1677ff; + margin-bottom: 2px; + } + + .title-sub { + font-size: 10px; + color: #666; + } + } + } + + .info-value { + text-align: right; + font-size: 18px; + font-weight: 700; + color: #1677ff; + + .value-unit { + font-size: 12px; + color: #666; + margin-left: 4px; + } + } + } + } + + .weight-card { + background: linear-gradient(135deg, #fff7e6, #fff2d9); + padding: 20px; + border-radius: 12px; + border: 1px solid #ffd591; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 16px; + transition: all 0.3s; + + &:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); + } + + .weight-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + + .weight-icon { + font-size: 16px; + color: #fa8c16; + margin-right: 8px; + } + + .weight-title { + flex: 1; + font-size: 14px; + font-weight: 600; + color: #fa8c16; + } + + .weight-score { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border-radius: 16px; + font-weight: 600; + + &.text-green-600 { + background: #f6ffed; + color: #52c41a; + } + + &.text-yellow-600 { + background: #fffbe6; + color: #fa8c16; + } + + &.text-red-600 { + background: #fff2f0; + color: #ff4d4f; + } + + .score-value { + font-size: 20px; + font-weight: 700; + } + + .score-unit { + font-size: 12px; + } + } + } + + .weight-description { + font-size: 12px; + color: #fa8c16; + background: #fff7e6; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid #ffd591; + margin-bottom: 16px; + } + + .weight-items { + .weight-item { + display: flex; + align-items: center; + margin-bottom: 12px; + + .item-label { + flex-shrink: 0; + width: 64px; + font-size: 12px; + font-weight: 500; + color: #fa8c16; + } + + .progress-bar { + flex: 1; + margin: 0 12px; + height: 8px; + background: #ffd591; + border-radius: 4px; + overflow: hidden; + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #fa8c16, #ffa940); + border-radius: 4px; + transition: width 0.5s ease; + } + } + + .item-value { + flex-shrink: 0; + width: 40px; + font-size: 12px; + font-weight: 500; + color: #fa8c16; + text-align: right; + } + } + } + } + + .restrictions-card { + background: linear-gradient(135deg, #fff2f0, #fff1f0); + padding: 16px; + border-radius: 12px; + border: 1px solid #ffccc7; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + + .restrictions-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + + .restrictions-icon { + font-size: 16px; + color: #ff4d4f; + margin-right: 8px; + } + + .restrictions-title { + flex: 1; + font-size: 14px; + font-weight: 600; + color: #ff4d4f; + } + + .restrictions-btn { + font-size: 12px; + padding: 4px 8px; + border-radius: 6px; + } + } + + .restrictions-list { + .restriction-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #ffccc7; + + &:last-child { + border-bottom: none; + } + + .restriction-info { + flex: 1; + + .restriction-reason { + display: block; + font-size: 12px; + color: #333; + margin-bottom: 2px; + } + + .restriction-date { + font-size: 10px; + color: #666; + } + } + + .restriction-level { + font-size: 10px; + padding: 2px 6px; + border-radius: 8px; + font-weight: 500; + + &.text-red-600 { + background: #fff2f0; + color: #ff4d4f; + } + + &.text-yellow-600 { + background: #fffbe6; + color: #fa8c16; + } + + &.text-gray-600 { + background: #f5f5f5; + color: #666; + } + } + } + } + } + } + + .friends-content { + .search-bar { + display: flex; + gap: 8px; + margin-bottom: 16px; + + .search-input-wrapper { + flex: 1; + + .adm-input { + border-radius: 8px; + border: 1px solid #d9d9d9; + } + } + + .search-btn { + padding: 8px 12px; + border-radius: 8px; + } + } + + .friends-list { + .empty { + text-align: center; + color: #999; + padding: 40px 0; + font-size: 14px; + } + + .error { + text-align: center; + color: #ff4d4f; + padding: 40px 0; + + p { + margin-bottom: 12px; + } + } + + .friend-item { + display: flex; + align-items: center; + padding: 12px; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 8px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #f5f5f5; + border-color: #d9d9d9; + } + + .friend-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 12px; + } + + .friend-info { + flex: 1; + min-width: 0; + + .friend-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; + + .friend-name { + font-size: 14px; + font-weight: 500; + color: #333; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .friend-remark { + color: #666; + margin-left: 4px; + } + } + + .friend-arrow { + font-size: 12px; + color: #ccc; + } + } + + .friend-wechat-id { + font-size: 12px; + color: #666; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .friend-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + + .friend-tag { + font-size: 10px; + padding: 2px 6px; + border-radius: 6px; + } + } + } + } + + .loading-more { + display: flex; + justify-content: center; + padding: 16px 0; + } + } + } +} + +.popup-content { + padding: 20px; + max-height: 80vh; + overflow-y: auto; + + .popup-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + + h3 { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; + } + } + + .popup-description { + font-size: 14px; + color: #666; + margin-bottom: 16px; + line-height: 1.5; + } + + .popup-actions { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 20px; + } + + .restrictions-detail { + .restriction-detail-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } + + .restriction-detail-info { + flex: 1; + + .restriction-detail-reason { + font-size: 14px; + color: #333; + margin-bottom: 4px; + } + + .restriction-detail-date { + font-size: 12px; + color: #666; + } + } + + .restriction-detail-level { + font-size: 12px; + padding: 4px 8px; + border-radius: 8px; + font-weight: 500; + + &.text-red-600 { + background: #fff2f0; + color: #ff4d4f; + } + + &.text-yellow-600 { + background: #fffbe6; + color: #fa8c16; + } + + &.text-gray-600 { + background: #f5f5f5; + color: #666; + } + } + } + } + + .loading-detail { + display: flex; + justify-content: center; + align-items: center; + padding: 40px 0; + } + + .error-detail { + text-align: center; + color: #ff4d4f; + padding: 40px 0; + + p { + margin-bottom: 12px; + } + } + + .friend-detail-content { + .friend-detail-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; + + .friend-detail-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + } + + .friend-detail-info { + .friend-detail-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin: 0 0 4px 0; + } + + .friend-detail-wechat-id { + font-size: 12px; + color: #666; + margin: 0; + } + } + } + + .friend-detail-items { + .detail-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } + + .detail-label { + font-size: 14px; + color: #666; + flex-shrink: 0; + width: 80px; + } + + .detail-value { + font-size: 14px; + color: #333; + text-align: right; + flex: 1; + margin-left: 16px; + } + + .detail-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + justify-content: flex-end; + flex: 1; + margin-left: 16px; + + .detail-tag { + font-size: 10px; + padding: 2px 6px; + border-radius: 6px; + } + } + } + } + } +} diff --git a/nkebao/src/pages/wechat-accounts/detail/index.tsx b/nkebao/src/pages/wechat-accounts/detail/index.tsx new file mode 100644 index 00000000..fe8dd32e --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/index.tsx @@ -0,0 +1,940 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + NavBar, + Card, + Tabs, + Button, + SpinLoading, + Popup, + Toast, + Input, + Avatar, + Tag, +} from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; +import { + SearchOutlined, + ReloadOutlined, + UserOutlined, + ClockCircleOutlined, + MessageOutlined, + StarOutlined, + ExclamationCircleOutlined, + RightOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import style from "./detail.module.scss"; +import { + getWechatAccountDetail, + getWechatAccountSummary, + getWechatFriends, + getWechatFriendDetail, +} from "./api"; + +interface WechatAccountSummary { + accountAge: string; + activityLevel: { + allTimes: number; + dayTimes: number; + }; + accountWeight: { + scope: number; + ageWeight: number; + activityWeigth: number; + restrictWeight: number; + realNameWeight: number; + }; + statistics: { + todayAdded: number; + addLimit: number; + }; + restrictions: { + id: number; + level: string; + reason: string; + date: string; + }[]; +} + +interface Friend { + id: string; + avatar: string; + nickname: string; + wechatId: string; + remark: string; + addTime: string; + lastInteraction: string; + tags: Array<{ + id: string; + name: string; + color: string; + }>; + region: string; + source: string; + notes: string; +} + +interface WechatFriendDetail { + id: number; + avatar: string; + nickname: string; + region: string; + wechatId: string; + addDate: string; + tags: string[]; + memo: string; + source: string; +} + +const WechatAccountDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [accountSummary, setAccountSummary] = + useState(null); + const [accountInfo, setAccountInfo] = useState(null); + const [showRestrictions, setShowRestrictions] = useState(false); + const [showTransferConfirm, setShowTransferConfirm] = useState(false); + const [showFriendDetail, setShowFriendDetail] = useState(false); + const [selectedFriend, setSelectedFriend] = useState(null); + const [friendDetail, setFriendDetail] = useState( + null + ); + const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false); + const [friendDetailError, setFriendDetailError] = useState( + null + ); + const [searchQuery, setSearchQuery] = useState(""); + const [activeTab, setActiveTab] = useState("overview"); + const [isLoading, setIsLoading] = useState(false); + const [loadingInfo, setLoadingInfo] = useState(true); + const [loadingSummary, setLoadingSummary] = useState(true); + + // 好友列表相关状态 + const [friends, setFriends] = useState([]); + const [friendsPage, setFriendsPage] = useState(1); + const [friendsTotal, setFriendsTotal] = useState(0); + const [hasMoreFriends, setHasMoreFriends] = useState(true); + const [isFetchingFriends, setIsFetchingFriends] = useState(false); + const [hasFriendLoadError, setHasFriendLoadError] = useState(false); + const [isFriendsEmpty, setIsFriendsEmpty] = useState(false); + const friendsObserver = useRef(null); + const friendsLoadingRef = useRef(null); + + // 获取基础信息 + const fetchAccountInfo = useCallback(async () => { + if (!id) return; + setLoadingInfo(true); + try { + const response = await getWechatAccountDetail(id); + if (response && response.data) { + setAccountInfo(response.data); + } else { + Toast.show({ + content: response?.msg || "获取账号信息失败", + position: "top", + }); + } + } catch (e) { + Toast.show({ content: "获取账号信息失败", position: "top" }); + } finally { + setLoadingInfo(false); + } + }, [id]); + + // 获取summary + const fetchAccountSummary = useCallback(async () => { + if (!id) return; + setLoadingSummary(true); + try { + const response = await getWechatAccountSummary(id); + if (response && response.data) { + setAccountSummary(response.data); + } else { + Toast.show({ + content: response?.msg || "获取账号概览失败", + position: "top", + }); + } + } catch (e) { + Toast.show({ content: "获取账号概览失败", position: "top" }); + } finally { + setLoadingSummary(false); + } + }, [id]); + + // 获取好友列表 + const fetchFriends = useCallback( + async (page: number = 1, isNewSearch: boolean = false) => { + if (!id || isFetchingFriends) return; + + try { + setIsFetchingFriends(true); + setHasFriendLoadError(false); + const response = await getWechatFriends({ + wechatAccountKeyword: id, + pageIndex: page, + pageSize: 20, + friendKeyword: searchQuery, + }); + + if (response && response.data) { + const newFriends = response.data.list.map((friend: any) => ({ + id: friend.id.toString(), + avatar: friend.avatar || "/placeholder.svg", + nickname: friend.nickname || "未知用户", + wechatId: friend.wechatId || "", + remark: friend.memo || "", + addTime: + friend.createTime || new Date().toISOString().split("T")[0], + lastInteraction: + friend.lastInteraction || new Date().toISOString().split("T")[0], + tags: friend.tags + ? friend.tags.map((tag: string, index: number) => ({ + id: `tag-${index}`, + name: tag, + color: getRandomTagColor(), + })) + : [], + region: friend.region || "未知", + source: friend.source || "未知", + notes: friend.notes || "", + })); + + if (isNewSearch) { + setFriends(newFriends); + if (newFriends.length === 0) { + setIsFriendsEmpty(true); + setHasMoreFriends(false); + } else { + setIsFriendsEmpty(false); + setHasMoreFriends(newFriends.length === 20); + } + } else { + setFriends((prev) => [...prev, ...newFriends]); + setHasMoreFriends(newFriends.length === 20); + } + + setFriendsTotal(response.data.total); + setFriendsPage(page); + } else { + setHasFriendLoadError(true); + if (isNewSearch) { + setFriends([]); + setIsFriendsEmpty(true); + setHasMoreFriends(false); + } + Toast.show({ + content: response?.msg || "获取好友列表失败", + position: "top", + }); + } + } catch (error) { + console.error("获取好友列表失败:", error); + setHasFriendLoadError(true); + if (isNewSearch) { + setFriends([]); + setIsFriendsEmpty(true); + setHasMoreFriends(false); + } + Toast.show({ + content: "获取好友列表失败,请检查网络连接", + position: "top", + }); + } finally { + setIsFetchingFriends(false); + } + }, + [id, searchQuery, isFetchingFriends] + ); + + // 初始化数据 + useEffect(() => { + if (id) { + fetchAccountInfo(); + fetchAccountSummary(); + if (activeTab === "friends") { + fetchFriends(1, true); + } + } + // eslint-disable-next-line + }, [id]); + + // 监听标签切换 + useEffect(() => { + if (activeTab === "friends" && id) { + setIsFriendsEmpty(false); + setHasFriendLoadError(false); + fetchFriends(1, true); + } + }, [activeTab, id, fetchFriends]); + + // 无限滚动加载好友 + useEffect(() => { + if ( + !friendsLoadingRef.current || + !hasMoreFriends || + isFetchingFriends || + isFriendsEmpty + ) + return; + + friendsObserver.current = new IntersectionObserver( + (entries) => { + if ( + entries[0].isIntersecting && + hasMoreFriends && + !isFetchingFriends && + !isFriendsEmpty + ) { + fetchFriends(friendsPage + 1, false); + } + }, + { threshold: 0.1 } + ); + + friendsObserver.current.observe(friendsLoadingRef.current); + + return () => { + if (friendsObserver.current) { + friendsObserver.current.disconnect(); + } + }; + }, [ + hasMoreFriends, + isFetchingFriends, + friendsPage, + fetchFriends, + isFriendsEmpty, + ]); + + // 工具函数 + const getRandomTagColor = (): string => { + const colors = [ + "bg-blue-100 text-blue-800", + "bg-green-100 text-green-800", + "bg-red-100 text-red-800", + "bg-pink-100 text-pink-800", + "bg-emerald-100 text-emerald-800", + "bg-amber-100 text-amber-800", + ]; + return colors[Math.floor(Math.random() * colors.length)]; + }; + + const calculateAccountAge = (registerTime: string) => { + const registerDate = new Date(registerTime); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - registerDate.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const years = Math.floor(diffDays / 365); + const months = Math.floor((diffDays % 365) / 30); + return { years, months }; + }; + + const formatAccountAge = (age: { years: number; months: number }) => { + if (age.years > 0) { + return `${age.years}年${age.months}个月`; + } + return `${age.months}个月`; + }; + + const getWeightColor = (weight: number) => { + if (weight >= 80) return "text-green-600"; + if (weight >= 60) return "text-yellow-600"; + return "text-red-600"; + }; + + const getWeightDescription = (weight: number) => { + if (weight >= 80) return "账号质量优秀,可以正常使用"; + if (weight >= 60) return "账号质量良好,需要注意使用频率"; + return "账号质量较差,建议谨慎使用"; + }; + + const handleTransferFriends = () => { + setShowTransferConfirm(true); + }; + + const confirmTransferFriends = () => { + Toast.show({ + content: "好友转移计划已创建,请在场景获客中查看详情", + position: "top", + }); + setShowTransferConfirm(false); + navigate("/scenarios"); + }; + + const handleFriendClick = async (friend: Friend) => { + setSelectedFriend(friend); + setShowFriendDetail(true); + setIsLoadingFriendDetail(true); + setFriendDetailError(null); + + try { + const response = await getWechatFriendDetail(friend.id); + if (response && response.data) { + setFriendDetail(response.data); + } else { + setFriendDetailError(response?.msg || "获取好友详情失败"); + } + } catch (error) { + console.error("获取好友详情失败:", error); + setFriendDetailError("网络错误,请稍后重试"); + } finally { + setIsLoadingFriendDetail(false); + } + }; + + const getRestrictionLevelColor = (level: string) => { + switch (level) { + case "high": + return "text-red-600"; + case "medium": + return "text-yellow-600"; + default: + return "text-gray-600"; + } + }; + + const formatDateTime = (dateString: string) => { + const date = new Date(dateString); + return date + .toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + .replace(/\//g, "-"); + }; + + const handleSearch = () => { + setIsFriendsEmpty(false); + setHasFriendLoadError(false); + fetchFriends(1, true); + }; + + const handleTabChange = (value: string) => { + setActiveTab(value); + }; + + if (loadingInfo || loadingSummary) { + return ( + + 微信号详情 + + } + > +
+ +
+
+ ); + } + + return ( + }> +
+ {/* 账号基本信息卡片 */} + +
+
+ +
+
+
+
+

+ {accountInfo?.nickname || "未知昵称"} +

+ + {accountInfo?.wechatStatus === 1 ? "正常" : "异常"} + +
+

+ 微信号:{accountInfo?.wechatAccount || "未知"} +

+
+ + +
+
+
+ + + {/* 标签页 */} + + + +
+ {/* 账号基础信息 */} +
+
+
+ +
+
账号年龄
+ {accountSummary && ( +
+ 注册于{" "} + {new Date( + accountSummary.accountAge + ).toLocaleDateString()} +
+ )} +
+
+ {accountSummary && ( +
+ {formatAccountAge( + calculateAccountAge(accountSummary.accountAge) + )} +
+ )} +
+ +
+
+ +
+
活跃程度
+ {accountSummary && ( +
+ 总聊天{" "} + {accountSummary.activityLevel.allTimes.toLocaleString()}{" "} + 次 +
+ )} +
+
+ {accountSummary && ( +
+ {accountSummary.activityLevel.dayTimes.toLocaleString()} + 次/天 +
+ )} +
+
+ + {/* 账号权重评估 */} + {accountSummary && ( +
+
+ + + 账号权重评估 + +
+ + {accountSummary.accountWeight.scope} + + +
+
+

+ {getWeightDescription(accountSummary.accountWeight.scope)} +

+
+
+ 账号年龄 +
+
+
+ + {accountSummary.accountWeight.ageWeight}% + +
+
+ 活跃度 +
+
+
+ + {accountSummary.accountWeight.activityWeigth}% + +
+
+
+ )} + + {/* 限制记录 */} + {accountSummary && + accountSummary.restrictions && + accountSummary.restrictions.length > 0 && ( +
+
+ + + 限制记录 + + +
+
+ {accountSummary.restrictions + .slice(0, 3) + .map((restriction) => ( +
+
+ + {restriction.reason} + + + {formatDateTime(restriction.date)} + +
+ + {restriction.level === "high" + ? "高风险" + : restriction.level === "medium" + ? "中风险" + : "低风险"} + +
+ ))} +
+
+ )} +
+ + + 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} + key="friends" + > +
+ {/* 搜索栏 */} +
+
+ setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" + onPressEnter={handleSearch} + /> +
+ +
+ + {/* 好友列表 */} +
+ {isFriendsEmpty ? ( +
暂无好友数据
+ ) : hasFriendLoadError ? ( +
+

加载失败,请重试

+ +
+ ) : ( + <> + {friends.map((friend) => ( +
handleFriendClick(friend)} + > + +
+
+
+ {friend.nickname} + {friend.remark && ( + + ({friend.remark}) + + )} +
+ +
+
+ {friend.wechatId} +
+
+ {friend.tags?.map((tag, index) => ( + + {typeof tag === "string" ? tag : tag.name} + + ))} +
+
+
+ ))} + {hasMoreFriends && !isFriendsEmpty && ( +
+ +
+ )} + + )} +
+
+
+ + +
+ + {/* 限制记录详情弹窗 */} + setShowRestrictions(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

限制记录详情

+ +
+

每次限制恢复时间为24小时

+ {accountSummary && accountSummary.restrictions && ( +
+ {accountSummary.restrictions.map((restriction) => ( +
+
+
+ {restriction.reason} +
+
+ {formatDateTime(restriction.date)} +
+
+ + {restriction.level === "high" + ? "高风险" + : restriction.level === "medium" + ? "中风险" + : "低风险"} + +
+ ))} +
+ )} +
+
+ + {/* 好友转移确认弹窗 */} + setShowTransferConfirm(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

确认好友转移

+
+

+ 确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。 +

+
+ + +
+
+
+ + {/* 好友详情弹窗 */} + setShowFriendDetail(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

好友详情

+ +
+ + {isLoadingFriendDetail ? ( +
+ +
+ ) : friendDetailError ? ( +
+

{friendDetailError}

+ +
+ ) : friendDetail && selectedFriend ? ( +
+
+ +
+

+ {selectedFriend.nickname} +

+

+ 微信号:{selectedFriend.wechatId} +

+
+
+ +
+
+ 地区 + + {friendDetail.region || "未知"} + +
+
+ 添加时间 + + {friendDetail.addDate} + +
+
+ 来源 + + {friendDetail.source || "未知"} + +
+ {friendDetail.memo && ( +
+ 备注 + + {friendDetail.memo} + +
+ )} + {friendDetail.tags && friendDetail.tags.length > 0 && ( +
+ 标签 +
+ {friendDetail.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} +
+
+ ) : null} +
+
+ + ); +}; + +export default WechatAccountDetail; diff --git a/nkebao/src/pages/wechat-accounts/list/api.ts b/nkebao/src/pages/wechat-accounts/list/api.ts new file mode 100644 index 00000000..08ff193e --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/api.ts @@ -0,0 +1,30 @@ +import request from "@/api/request"; + +// 获取微信号列表 +export function getWechatAccounts(params: { + page: number; + page_size: number; + keyword?: string; +}) { + return request("v1/wechats", params, "GET"); +} + +// 获取微信号详情 +export function getWechatAccountDetail(id: string) { + return request("v1/WechatAccount/detail", { id }, "GET"); +} + +// 获取微信号好友列表 +export function getWechatFriends(params: { + wechatAccountKeyword: string; + pageIndex: number; + pageSize: number; + friendKeyword?: string; +}) { + return request("v1/WechatFriend/friendlistData", params, "POST"); +} + +// 获取微信好友详情 +export function getWechatFriendDetail(id: string) { + return request("v1/WechatFriend/detail", { id }, "GET"); +} diff --git a/nkebao/src/pages/wechat-accounts/list/index.module.scss b/nkebao/src/pages/wechat-accounts/list/index.module.scss new file mode 100644 index 00000000..3e657fb4 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/index.module.scss @@ -0,0 +1,171 @@ +.wechat-accounts-page { + padding: 0 12px; +} + +.nav-title { + font-size: 18px; + font-weight: 600; + color: #222; +} + +.card-list { + display: flex; + flex-direction: column; + gap: 14px; +} + +.account-card { + background: #fff; + border-radius: 14px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + padding: 14px 14px 10px 14px; + transition: box-shadow 0.2s; + cursor: pointer; + border: 1px solid #f0f0f0; + &:hover { + box-shadow: 0 4px 16px rgba(0,0,0,0.10); + border-color: #e6f7ff; + } +} + +.card-header { + display: flex; + align-items: center; + margin-bottom: 8px; +} +.avatar-wrapper { + position: relative; + margin-right: 12px; +} +.avatar { + width: 48px; + height: 48px; + border-radius: 50%; + border: 3px solid #e6f0fa; + box-shadow: 0 0 0 2px #1677ff33; + object-fit: cover; +} +.status-dot-normal { + position: absolute; + right: -2px; + bottom: -2px; + width: 14px; + height: 14px; + background: #52c41a; + border: 2px solid #fff; + border-radius: 50%; +} +.status-dot-abnormal { + position: absolute; + right: -2px; + bottom: -2px; + width: 14px; + height: 14px; + background: #ff4d4f; + border: 2px solid #fff; + border-radius: 50%; +} +.header-info { + flex: 1; + min-width: 0; +} +.nickname-row { + display: flex; + align-items: center; + gap: 8px; +} +.nickname { + font-weight: 600; + font-size: 16px; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.status-label-normal { + background: #e6fffb; + color: #13c2c2; + font-size: 12px; + border-radius: 8px; + padding: 2px 8px; + margin-left: 4px; +} +.status-label-abnormal { + background: #fff1f0; + color: #ff4d4f; + font-size: 12px; + border-radius: 8px; + padding: 2px 8px; + margin-left: 4px; +} +.wechat-id { + color: #888; + font-size: 13px; + margin-top: 2px; +} +.card-action { + margin-left: 8px; +} +.card-body { + margin-top: 2px; +} +.row-group { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + gap: 8px; +} +.row-item { + font-size: 13px; + color: #555; + display: flex; + align-items: center; + gap: 2px; +} +.strong { + font-weight: 600; + color: #222; +} +.strong-green { + font-weight: 600; + color: #52c41a; +} +.progress-bar { + margin: 6px 0 8px 0; +} +.progress-bg { + width: 100%; + height: 8px; + background: #f0f0f0; + border-radius: 6px; + overflow: hidden; +} +.progress-fill { + height: 8px; + background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%); + border-radius: 6px; + transition: width 0.3s; +} +.pagination { + margin: 16px 0 0 0; + display: flex; + justify-content: center; +} +.popup-content { + padding: 16px 0 8px 0; +} +.popup-content img { + box-shadow: 0 2px 8px rgba(0,0,0,0.08); +} +.loading { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} +.empty { + text-align: center; + color: #999; + padding: 48px 0 32px 0; + font-size: 15px; +} diff --git a/nkebao/src/pages/wechat-accounts/list/index.tsx b/nkebao/src/pages/wechat-accounts/list/index.tsx new file mode 100644 index 00000000..e1a306a6 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/index.tsx @@ -0,0 +1,309 @@ +import React, { useState, useEffect } from "react"; +import { + NavBar, + List, + Card, + Button, + SpinLoading, + Popup, + Toast, +} from "antd-mobile"; +import { Pagination, Input, Tooltip } from "antd"; +import { + ArrowLeftOutlined, + SearchOutlined, + ReloadOutlined, +} from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import Layout from "@/components/Layout/Layout"; +import style from "./index.module.scss"; +import { getWechatAccounts } from "./api"; + +interface WechatAccount { + id: number; + nickname: string; + avatar: string; + wechatId: string; + wechatAccount: string; + deviceId: number; + times: number; // 今日可添加 + addedCount: number; // 今日新增 + wechatStatus: number; // 1正常 0异常 + totalFriend: number; + deviceMemo: string; // 设备名 + activeTime: string; // 最后活跃 +} + +const PAGE_SIZE = 10; + +const WechatAccounts: React.FC = () => { + const navigate = useNavigate(); + const [accounts, setAccounts] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalAccounts, setTotalAccounts] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [popupVisible, setPopupVisible] = useState(false); + const [selectedAccount, setSelectedAccount] = useState( + null + ); + + const fetchAccounts = async (page = 1, keyword = "") => { + setIsLoading(true); + try { + const res = await getWechatAccounts({ + page, + page_size: PAGE_SIZE, + keyword, + }); + if (res && res.list) { + setAccounts(res.list); + setTotalAccounts(res.total || 0); + } else { + setAccounts([]); + setTotalAccounts(0); + } + } catch (e) { + Toast.show({ content: "获取微信号失败", position: "top" }); + setAccounts([]); + setTotalAccounts(0); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchAccounts(currentPage, searchTerm); + // eslint-disable-next-line + }, [currentPage]); + + const handleSearch = () => { + setCurrentPage(1); + fetchAccounts(1, searchTerm); + }; + + const handleRefresh = async () => { + setIsRefreshing(true); + await fetchAccounts(currentPage, searchTerm); + setIsRefreshing(false); + Toast.show({ content: "刷新成功", position: "top" }); + }; + + const handleAccountClick = (account: WechatAccount) => { + setSelectedAccount(account); + setPopupVisible(true); + }; + + const handleTransferFriends = (account: WechatAccount) => { + // TODO: 实现好友转移弹窗或跳转 + Toast.show({ content: `好友转移:${account.nickname}` }); + }; + + return ( + + + navigate(-1)} + /> +
+ } + > + 微信号管理 + +
+
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + onPressEnter={handleSearch} + /> +
+ +
+ + } + > +
+ {isLoading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
暂无微信账号数据
+ ) : ( +
+ {accounts.map((account) => { + const percent = + account.times > 0 + ? Math.min((account.addedCount / account.times) * 100, 100) + : 0; + return ( +
handleAccountClick(account)} + > +
+
+ {account.nickname} + +
+
+
+ + {account.nickname} + + + {account.wechatStatus === 1 ? "正常" : "异常"} + +
+
+ 微信号:{account.wechatAccount} +
+
+
+
+
+
+ 好友数量: + + {account.totalFriend} + +
+
+ 今日新增: + + +{account.addedCount} + +
+
+
+
+ 今日可添加: + {account.times} +
+
+ + 进度: + + {account.addedCount}/{account.times} + + +
+
+
+
+
+
+
+
+
+ 所属设备: + {account.deviceMemo || "-"} +
+
+ 最后活跃: + {account.activeTime} +
+
+
+
+ ); + })} +
+ )} +
+ {totalAccounts > PAGE_SIZE && ( + + )} +
+ setPopupVisible(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > + {selectedAccount && ( +
+
+ avatar +
+ {selectedAccount.nickname} +
+
+ 微信号:{selectedAccount.wechatAccount} +
+
+
+ +
+ +
+ )} +
+
+ + ); +}; + +export default WechatAccounts; diff --git a/nkebao/src/pages/workspace/moments-sync/new/api.ts b/nkebao/src/pages/workspace/moments-sync/new/api.ts new file mode 100644 index 00000000..d162f747 --- /dev/null +++ b/nkebao/src/pages/workspace/moments-sync/new/api.ts @@ -0,0 +1,32 @@ +import request from "@/api/request"; + +// 创建朋友圈同步任务 +export const createMomentsSync = (params: { + name: string; + devices: string[]; + contentLibraries: string[]; + syncCount: number; + startTime: string; + endTime: string; + accountType: number; + status: number; + type: number; +}) => request("/v1/workbench/create", params, "POST"); + +// 更新朋友圈同步任务 +export const updateMomentsSync = (params: { + id: string; + name: string; + devices: string[]; + contentLibraries: string[]; + syncCount: number; + startTime: string; + endTime: string; + accountType: number; + status: number; + type: number; +}) => request("/v1/workbench/update", params, "POST"); + +// 获取朋友圈同步任务详情 +export const getMomentsSyncDetail = (id: string) => + request("/v1/workbench/detail", { id }, "GET"); diff --git a/nkebao/src/pages/workspace/moments-sync/new/index.module.scss b/nkebao/src/pages/workspace/moments-sync/new/index.module.scss index 3cf58561..94b4005d 100644 --- a/nkebao/src/pages/workspace/moments-sync/new/index.module.scss +++ b/nkebao/src/pages/workspace/moments-sync/new/index.module.scss @@ -84,7 +84,7 @@ } .inputTime { - width: 90px; + width: 100px; height: 40px; border-radius: 8px; font-size: 15px; diff --git a/nkebao/src/pages/workspace/moments-sync/new/index.tsx b/nkebao/src/pages/workspace/moments-sync/new/index.tsx index ced744ac..707bf3bb 100644 --- a/nkebao/src/pages/workspace/moments-sync/new/index.tsx +++ b/nkebao/src/pages/workspace/moments-sync/new/index.tsx @@ -7,8 +7,19 @@ import { NavBar } from "antd-mobile"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import request from "@/api/request"; +import StepIndicator from "@/components/StepIndicator"; +import { + createMomentsSync, + updateMomentsSync, + getMomentsSyncDetail, +} from "./api"; +import DeviceSelection from "@/components/DeviceSelection"; -const steps = ["基础设置", "设备选择", "内容库选择"]; +const steps = [ + { id: 1, title: "基础设置", subtitle: "基础设置" }, + { id: 2, title: "设备选择", subtitle: "设备选择" }, + { id: 3, title: "内容库选择", subtitle: "内容库选择" }, +]; const defaultForm = { taskName: "", @@ -34,7 +45,7 @@ const NewMomentsSync: React.FC = () => { if (!id) return; setLoading(true); try { - const res = await request("/v1/workbench/detail", { id }, "GET"); + const res = await getMomentsSyncDetail(id); if (res) { setFormData({ taskName: res.name, @@ -95,11 +106,11 @@ const NewMomentsSync: React.FC = () => { type: 2, }; if (isEditMode && id) { - await request("/v1/workbench/update", { id, ...params }, "POST"); + await updateMomentsSync({ id, ...params }); message.success("更新成功"); navigate(`/workspace/moments-sync/${id}`); } else { - await request("/v1/workbench/create", params, "POST"); + await createMomentsSync(params); message.success("创建成功"); navigate("/workspace/moments-sync"); } @@ -214,18 +225,14 @@ const NewMomentsSync: React.FC = () => { return (
- Q} - className={style.searchInput} - onClick={() => message.info("这里应弹出设备选择器")} - readOnly +
选择设备
+ updateForm({ selectedDevices: devices })} + placeholder="请选择设备" + showSelectedList={true} + selectedListMaxHeight={200} /> - {formData.selectedDevices.length > 0 && ( -
- 已选设备: {formData.selectedDevices.length}个 -
- )}