From 2c299e3add0bd64292c30341ec691d4827fc853a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 12:29:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E8=AE=BE=E5=A4=87=E3=80=81=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/DeviceSelection/index.tsx | 208 +++++++++++ .../components/DeviceSelection/module.scss | 156 ++++++++ .../DeviceSelectionDialog/index.tsx | 216 ++++++++++++ .../DeviceSelectionDialog/module.scss | 170 +++++++++ .../src/components/FriendSelection/index.tsx | 332 ++++++++++++++++++ .../components/FriendSelection/module.scss | 223 ++++++++++++ .../src/components/GroupSelection/index.tsx | 317 +++++++++++++++++ .../src/components/GroupSelection/module.scss | 223 ++++++++++++ .../src/pages/workspace/auto-like/list/api.ts | 2 +- .../pages/workspace/auto-like/list/index.tsx | 2 +- .../src/pages/workspace/auto-like/new/api.ts | 2 +- .../pages/workspace/auto-like/new/index.tsx | 2 +- .../workspace/auto-like/record/api.ts} | 0 .../workspace/auto-like/record/data.ts} | 4 +- .../workspace/auto-like/record/index.tsx | 4 +- .../workspace/group-push/detail}/groupPush.ts | 2 +- .../workspace/group-push/detail/index.tsx | 5 +- .../pages/workspace/group-push/form/index.tsx | 2 +- .../pages/workspace/group-push/list/index.tsx | 2 +- 19 files changed, 1860 insertions(+), 12 deletions(-) create mode 100644 nkebao/src/components/DeviceSelection/index.tsx create mode 100644 nkebao/src/components/DeviceSelection/module.scss create mode 100644 nkebao/src/components/DeviceSelectionDialog/index.tsx create mode 100644 nkebao/src/components/DeviceSelectionDialog/module.scss create mode 100644 nkebao/src/components/FriendSelection/index.tsx create mode 100644 nkebao/src/components/FriendSelection/module.scss create mode 100644 nkebao/src/components/GroupSelection/index.tsx create mode 100644 nkebao/src/components/GroupSelection/module.scss rename nkebao/src/{types/auto-like.ts => pages/workspace/auto-like/record/api.ts} (100%) rename nkebao/src/{api/autoLike.ts => pages/workspace/auto-like/record/data.ts} (93%) rename nkebao/src/{api => pages/workspace/group-push/detail}/groupPush.ts (94%) diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx new file mode 100644 index 00000000..67d5072c --- /dev/null +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; +import request from "@/api/request"; +import style from "./module.scss"; + +// 设备选择项接口 +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; +} + +// 组件属性接口 +interface DeviceSelectionProps { + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + placeholder?: string; + className?: string; +} + +export default function DeviceSelection({ + selectedDevices, + onSelect, + placeholder = "选择设备", + className = "", +}: DeviceSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [devices, setDevices] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + + // 获取设备列表,支持keyword + const fetchDevices = async (keyword: string = "") => { + setLoading(true); + try { + const res = await request( + "/v1/device/list", + { + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }, + "GET" + ); + + 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", + })) + ); + } + } catch (error) { + console.error("获取设备列表失败:", error); + Toast.show({ content: "获取设备列表失败", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 打开弹窗时获取设备列表 + const openPopup = () => { + setSearchQuery(""); + setPopupVisible(true); + fetchDevices(""); + }; + + // 搜索防抖 + useEffect(() => { + if (!popupVisible) return; + const timer = setTimeout(() => { + fetchDevices(searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, popupVisible]); + + // 过滤设备(只保留状态过滤) + const filteredDevices = devices.filter((device) => { + const matchesStatus = + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); + return matchesStatus; + }); + + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedDevices.length === 0) return ""; + return `已选择 ${selectedDevices.length} 个设备`; + }; + + return ( + <> + {/* 输入框 */} +
+ + +
+ + {/* 设备选择弹窗 */} + setPopupVisible(false)} + position="bottom" + bodyStyle={{ height: "80vh" }} + > +
+
+
选择设备
+
+
+
+ + setSearchQuery(val)} + className={style.popupSearchInput} + /> +
+ +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ + ); +} diff --git a/nkebao/src/components/DeviceSelection/module.scss b/nkebao/src/components/DeviceSelection/module.scss new file mode 100644 index 00000000..34510d81 --- /dev/null +++ b/nkebao/src/components/DeviceSelection/module.scss @@ -0,0 +1,156 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + z-index: 10; + font-size: 18px; +} +.input { + padding-left: 38px !important; + height: 56px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} +.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: 16px; + padding: 16px; +} +.popupSearchInputWrap { + position: relative; + flex: 1; +} +.popupSearchInput { + padding-left: 36px !important; + border-radius: 12px !important; + height: 44px; + font-size: 15px; + border: 1px solid #e5e6eb !important; + background: #f8f9fa; +} +.statusSelect { + width: 120px; + height: 40px; + border-radius: 8px; + border: 1px solid #e5e6eb; + font-size: 15px; + padding: 0 10px; + background: #fff; +} +.deviceList { + flex: 1; + overflow-y: auto; +} +.deviceListInner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} +.deviceItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + background: #fff; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.deviceCheckbox { + margin-top: 4px; +} +.deviceInfo { + flex: 1; +} +.deviceInfoRow { + display: flex; + align-items: center; + justify-content: space-between; +} +.deviceName { + font-weight: 500; + font-size: 16px; + color: #222; +} +.statusOnline { + width: 56px; + height: 24px; + border-radius: 12px; + background: #52c41a; + color: #fff; + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; +} +.statusOffline { + width: 56px; + height: 24px; + border-radius: 12px; + background: #e5e6eb; + color: #888; + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; +} +.deviceInfoDetail { + font-size: 13px; + color: #888; + margin-top: 4px; +} +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.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; +} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx new file mode 100644 index 00000000..4e741e15 --- /dev/null +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -0,0 +1,216 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; +import request from "@/api/request"; +import style from "./module.scss"; + +interface Device { + id: string; + name: string; + imei: string; + wxid: string; + status: "online" | "offline"; + usedInPlans: number; + nickname: string; +} + +interface DeviceSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; +} + +export function DeviceSelectionDialog({ + open, + onOpenChange, + selectedDevices, + onSelect, +}: DeviceSelectionDialogProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + const [devices, setDevices] = useState([]); + + // 获取设备列表,支持keyword + const fetchDevices = useCallback(async (keyword: string = "") => { + setLoading(true); + try { + const response = await request( + "/v1/device/list", + { + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }, + "GET" + ); + + if (response && Array.isArray(response.list)) { + // 转换服务端数据格式为组件需要的格式 + const convertedDevices: Device[] = response.list.map( + (serverDevice: any) => ({ + id: serverDevice.id.toString(), + name: serverDevice.memo || `设备 ${serverDevice.id}`, + imei: serverDevice.imei, + wxid: serverDevice.wechatId || "", + status: serverDevice.alive === 1 ? "online" : "offline", + usedInPlans: 0, // 这个字段需要从其他API获取 + nickname: serverDevice.nickname || "", + }) + ); + setDevices(convertedDevices); + } + } catch (error) { + console.error("获取设备列表失败:", error); + Toast.show({ + content: "获取设备列表失败,请检查网络连接", + position: "top", + }); + } finally { + setLoading(false); + } + }, []); + + // 打开弹窗时获取设备列表 + useEffect(() => { + if (open) { + fetchDevices(""); + } + }, [open, fetchDevices]); + + // 搜索防抖 + useEffect(() => { + if (!open) return; + const timer = setTimeout(() => { + fetchDevices(searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, open, fetchDevices]); + + // 过滤设备(只保留状态过滤) + const filteredDevices = devices.filter((device) => { + const matchesStatus = + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); + return matchesStatus; + }); + + const handleDeviceSelect = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + onOpenChange(false)} + position="bottom" + bodyStyle={{ height: "80vh" }} + > +
+
+
选择设备
+
+
+
+ + setSearchQuery(val)} + className={style.popupSearchInput} + /> +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : filteredDevices.length === 0 ? ( +
+
暂无数据
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +} diff --git a/nkebao/src/components/DeviceSelectionDialog/module.scss b/nkebao/src/components/DeviceSelectionDialog/module.scss new file mode 100644 index 00000000..2a9cd2de --- /dev/null +++ b/nkebao/src/components/DeviceSelectionDialog/module.scss @@ -0,0 +1,170 @@ +.popupContainer { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} +.popupHeader { + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} +.popupTitle { + font-size: 18px; + font-weight: 600; + text-align: center; +} +.popupSearchRow { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; +} +.popupSearchInputWrap { + position: relative; + flex: 1; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + z-index: 10; + font-size: 16px; +} +.popupSearchInput { + padding-left: 36px !important; + border-radius: 12px !important; + height: 44px; + font-size: 15px; + border: 1px solid #e5e6eb !important; + background: #f8f9fa; +} +.statusSelect { + width: 128px; + height: 40px; + border-radius: 8px; + border: 1px solid #e5e6eb; + font-size: 14px; + padding: 0 12px; + background: #fff; +} +.refreshBtn { + min-width: 40px; + height: 40px; + border-radius: 8px; +} +.loadingIcon { + animation: spin 1s linear infinite; + font-size: 16px; +} +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +.deviceList { + flex: 1; + overflow-y: auto; +} +.deviceListInner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} +.deviceItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + background: #fff; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.deviceCheckbox { + margin-top: 4px; +} +.deviceInfo { + flex: 1; +} +.deviceInfoRow { + display: flex; + align-items: center; + justify-content: space-between; +} +.deviceName { + font-weight: 500; + font-size: 16px; + color: #222; +} +.statusOnline { + padding: 4px 8px; + border-radius: 12px; + background: #52c41a; + color: #fff; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.statusOffline { + padding: 4px 8px; + border-radius: 12px; + background: #e5e6eb; + color: #888; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.deviceInfoDetail { + font-size: 13px; + color: #888; + margin-top: 4px; +} +.usedInPlans { + font-size: 13px; + color: #fa8c16; + margin-top: 4px; +} +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} +.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; +} diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx new file mode 100644 index 00000000..43f37ce5 --- /dev/null +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -0,0 +1,332 @@ +import React, { useState, useEffect } from "react"; +import { + SearchOutlined, + CloseOutlined, + LeftOutlined, + RightOutlined, +} from "@ant-design/icons"; +import { Input, Button, Popup, Toast } from "antd-mobile"; +import request from "@/api/request"; +import style from "./module.scss"; + +// 微信好友接口类型 +interface WechatFriend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + customer: string; +} + +// 组件属性接口 +interface FriendSelectionProps { + selectedFriends: string[]; + onSelect: (friends: string[]) => void; + onSelectDetail?: (friends: WechatFriend[]) => void; + deviceIds?: string[]; + enableDeviceFilter?: boolean; + placeholder?: string; + className?: string; +} + +export default function FriendSelection({ + selectedFriends, + onSelect, + onSelectDetail, + deviceIds = [], + enableDeviceFilter = true, + placeholder = "选择微信好友", + className = "", +}: FriendSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [friends, setFriends] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalFriends, setTotalFriends] = useState(0); + const [loading, setLoading] = useState(false); + + // 打开弹窗并请求第一页好友 + const openPopup = () => { + setCurrentPage(1); + setSearchQuery(""); + setPopupVisible(true); + fetchFriends(1, ""); + }; + + // 当页码变化时,拉取对应页数据(弹窗已打开时) + useEffect(() => { + if (popupVisible && currentPage !== 1) { + fetchFriends(currentPage, searchQuery); + } + }, [currentPage, popupVisible, searchQuery]); + + // 搜索防抖 + useEffect(() => { + if (!popupVisible) return; + + const timer = setTimeout(() => { + setCurrentPage(1); + fetchFriends(1, searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery, popupVisible]); + + // 获取好友列表API - 添加 keyword 参数 + const fetchFriends = async (page: number, keyword: string = "") => { + setLoading(true); + try { + let params: any = { + page, + limit: 20, + }; + + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + if (enableDeviceFilter) { + if (deviceIds.length === 0) { + setFriends([]); + setTotalFriends(0); + setTotalPages(1); + setLoading(false); + return; + } + params.deviceIds = deviceIds.join(","); + } + + const res = await request("/v1/friend", params, "GET"); + + 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)); + } + } catch (error) { + console.error("获取好友列表失败:", error); + Toast.show({ content: "获取好友列表失败", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 处理好友选择 + const handleFriendToggle = (friendId: string) => { + let newIds: string[]; + if (selectedFriends.includes(friendId)) { + newIds = selectedFriends.filter((id) => id !== friendId); + } else { + newIds = [...selectedFriends, friendId]; + } + onSelect(newIds); + if (onSelectDetail) { + const selectedObjs = friends.filter((f) => newIds.includes(f.id)); + onSelectDetail(selectedObjs); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedFriends.length === 0) return ""; + return `已选择 ${selectedFriends.length} 个好友`; + }; + + const handleConfirm = () => { + setPopupVisible(false); + }; + + // 清空搜索 + const handleClearSearch = () => { + setSearchQuery(""); + setCurrentPage(1); + fetchFriends(1, ""); + }; + + return ( + <> + {/* 输入框 */} +
+ + + + + + +
+ + {/* 微信好友选择弹窗 */} + setPopupVisible(false)} + position="bottom" + bodyStyle={{ height: "80vh" }} + > +
+
+
选择微信好友
+
+ setSearchQuery(val)} + className={style.searchInput} + /> + + {searchQuery && ( + + )} +
+
+ +
+ {loading ? ( +
+
加载中...
+
+ ) : friends.length > 0 ? ( +
+ {friends.map((friend) => ( + + ))} +
+ ) : ( +
+
+ {deviceIds.length === 0 + ? "请先选择设备" + : searchQuery + ? `没有找到包含"${searchQuery}"的好友` + : "没有找到好友"} +
+
+ )} +
+ +
+
总计 {totalFriends} 个好友
+
+ + + {currentPage} / {totalPages} + + +
+
+ +
+ + +
+
+
+ + ); +} diff --git a/nkebao/src/components/FriendSelection/module.scss b/nkebao/src/components/FriendSelection/module.scss new file mode 100644 index 00000000..86535501 --- /dev/null +++ b/nkebao/src/components/FriendSelection/module.scss @@ -0,0 +1,223 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; +} +.input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} +.popupHeader { + padding: 24px; +} +.popupTitle { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; +} +.searchWrapper { + position: relative; + margin-bottom: 16px; +} +.searchInput { + padding-left: 40px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + border-radius: 24px !important; + border: 1px solid #e5e6eb !important; + font-size: 15px; + background: #f8f9fa; +} +.searchIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 16px; +} +.clearBtn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + height: 24px; + width: 24px; + border-radius: 50%; + min-width: 24px; +} + +.friendList { + flex: 1; + overflow-y: auto; +} +.friendListInner { + border-top: 1px solid #f0f0f0; +} +.friendItem { + display: flex; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.radioWrapper { + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.radioSelected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1890ff; + display: flex; + align-items: center; + justify-content: center; +} +.radioUnselected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #e5e6eb; + display: flex; + align-items: center; + justify-content: center; +} +.radioDot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #1890ff; +} +.friendInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} +.friendAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 500; + overflow: hidden; +} +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} +.friendDetail { + flex: 1; +} +.friendName { + font-weight: 500; + font-size: 16px; + color: #222; + margin-bottom: 2px; +} +.friendId { + font-size: 13px; + color: #888; + margin-bottom: 2px; +} +.friendCustomer { + font-size: 13px; + color: #bdbdbd; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} + +.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; +} +.pageInfo { + font-size: 14px; + color: #222; +} + +.popupFooter { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.cancelBtn { + padding: 0 24px; + border-radius: 24px; + border: 1px solid #e5e6eb; +} +.confirmBtn { + padding: 0 24px; + border-radius: 24px; +} diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx new file mode 100644 index 00000000..5e871e86 --- /dev/null +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -0,0 +1,317 @@ +import React, { useState, useEffect } from "react"; +import { + SearchOutlined, + CloseOutlined, + LeftOutlined, + RightOutlined, +} from "@ant-design/icons"; +import { Input, Button, Popup, Toast } from "antd-mobile"; +import request from "@/api/request"; +import style from "./module.scss"; + +// 群组接口类型 +interface WechatGroup { + id: string; + chatroomId: string; + name: string; + avatar: string; + ownerWechatId: string; + ownerNickname: string; + ownerAvatar: string; +} + +// 组件属性接口 +interface GroupSelectionProps { + selectedGroups: string[]; + onSelect: (groups: string[]) => void; + onSelectDetail?: (groups: WechatGroup[]) => void; + placeholder?: string; + className?: string; +} + +export default function GroupSelection({ + selectedGroups, + onSelect, + onSelectDetail, + placeholder = "选择群聊", + className = "", +}: GroupSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [groups, setGroups] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalGroups, setTotalGroups] = useState(0); + const [loading, setLoading] = useState(false); + + // 打开弹窗并请求第一页群组 + const openPopup = () => { + setCurrentPage(1); + setSearchQuery(""); + setPopupVisible(true); + fetchGroups(1, ""); + }; + + // 当页码变化时,拉取对应页数据(弹窗已打开时) + useEffect(() => { + if (popupVisible && currentPage !== 1) { + fetchGroups(currentPage, searchQuery); + } + }, [currentPage, popupVisible, searchQuery]); + + // 搜索防抖 + useEffect(() => { + if (!popupVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchGroups(1, searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, popupVisible]); + + // 获取群组列表API - 支持keyword + const fetchGroups = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: any = { + page, + limit: 20, + }; + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + const res = await request("/v1/chatroom", params, "GET"); + + 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)); + } + } catch (error) { + console.error("获取群组列表失败:", error); + Toast.show({ content: "获取群组列表失败", position: "top" }); + } 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 (onSelectDetail) { + const selectedObjs = groups.filter((g) => newIds.includes(g.id)); + onSelectDetail(selectedObjs); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedGroups.length === 0) return ""; + return `已选择 ${selectedGroups.length} 个群聊`; + }; + + const handleConfirm = () => { + setPopupVisible(false); + }; + + // 清空搜索 + const handleClearSearch = () => { + setSearchQuery(""); + setCurrentPage(1); + fetchGroups(1, ""); + }; + + return ( + <> + {/* 输入框 */} +
+ + + + + + +
+ + {/* 群组选择弹窗 */} + setPopupVisible(false)} + position="bottom" + bodyStyle={{ height: "80vh" }} + > +
+
+
选择群聊
+
+ setSearchQuery(val)} + className={style.searchInput} + /> + + {searchQuery && ( + + )} +
+
+ +
+ {loading ? ( +
+
加载中...
+
+ ) : groups.length > 0 ? ( +
+ {groups.map((group) => ( + + ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的群聊` + : "没有找到群聊"} +
+
+ )} +
+ +
+
+ 总计 {totalGroups} 个群聊 + {searchQuery && ` (搜索: "${searchQuery}")`} +
+
+ + + {currentPage} / {totalPages} + + +
+
+ +
+ + +
+
+
+ + ); +} diff --git a/nkebao/src/components/GroupSelection/module.scss b/nkebao/src/components/GroupSelection/module.scss new file mode 100644 index 00000000..75dbd174 --- /dev/null +++ b/nkebao/src/components/GroupSelection/module.scss @@ -0,0 +1,223 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; +} +.input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} +.popupHeader { + padding: 24px; +} +.popupTitle { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; +} +.searchWrapper { + position: relative; + margin-bottom: 16px; +} +.searchInput { + padding-left: 40px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + border-radius: 24px !important; + border: 1px solid #e5e6eb !important; + font-size: 15px; + background: #f8f9fa; +} +.searchIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 16px; +} +.clearBtn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + height: 24px; + width: 24px; + border-radius: 50%; + min-width: 24px; +} + +.groupList { + flex: 1; + overflow-y: auto; +} +.groupListInner { + border-top: 1px solid #f0f0f0; +} +.groupItem { + display: flex; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.radioWrapper { + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.radioSelected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1890ff; + display: flex; + align-items: center; + justify-content: center; +} +.radioUnselected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #e5e6eb; + display: flex; + align-items: center; + justify-content: center; +} +.radioDot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #1890ff; +} +.groupInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} +.groupAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 500; + overflow: hidden; +} +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} +.groupDetail { + flex: 1; +} +.groupName { + font-weight: 500; + font-size: 16px; + color: #222; + margin-bottom: 2px; +} +.groupId { + font-size: 13px; + color: #888; + margin-bottom: 2px; +} +.groupOwner { + font-size: 13px; + color: #bdbdbd; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} + +.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; +} +.pageInfo { + font-size: 14px; + color: #222; +} + +.popupFooter { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.cancelBtn { + padding: 0 24px; + border-radius: 24px; + border: 1px solid #e5e6eb; +} +.confirmBtn { + padding: 0 24px; + border-radius: 24px; +} diff --git a/nkebao/src/pages/workspace/auto-like/list/api.ts b/nkebao/src/pages/workspace/auto-like/list/api.ts index 930902b4..34d4424a 100644 --- a/nkebao/src/pages/workspace/auto-like/list/api.ts +++ b/nkebao/src/pages/workspace/auto-like/list/api.ts @@ -5,7 +5,7 @@ import { UpdateLikeTaskData, LikeRecord, PaginatedResponse, -} from "@/types/auto-like"; +} from "@/pages/workspace/auto-like/record/api"; // 获取自动点赞任务列表 export function fetchAutoLikeTasks( diff --git a/nkebao/src/pages/workspace/auto-like/list/index.tsx b/nkebao/src/pages/workspace/auto-like/list/index.tsx index b3244fab..f9a70a18 100644 --- a/nkebao/src/pages/workspace/auto-like/list/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/list/index.tsx @@ -22,7 +22,7 @@ import { toggleAutoLikeTask, copyAutoLikeTask, } from "./api"; -import { LikeTask } from "@/types/auto-like"; +import { LikeTask } from "@/pages/workspace/auto-like/record/api"; import style from "./index.module.scss"; // 卡片菜单组件 diff --git a/nkebao/src/pages/workspace/auto-like/new/api.ts b/nkebao/src/pages/workspace/auto-like/new/api.ts index 6ca2944c..703d7bb6 100644 --- a/nkebao/src/pages/workspace/auto-like/new/api.ts +++ b/nkebao/src/pages/workspace/auto-like/new/api.ts @@ -3,7 +3,7 @@ import { CreateLikeTaskData, UpdateLikeTaskData, LikeTask, -} from "@/types/auto-like"; +} from "@/pages/workspace/auto-like/record/api"; // 获取自动点赞任务详情 export function fetchAutoLikeTaskDetail(id: string): Promise { diff --git a/nkebao/src/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx index b0a13181..1b8da372 100644 --- a/nkebao/src/pages/workspace/auto-like/new/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -17,7 +17,7 @@ import { CreateLikeTaskData, UpdateLikeTaskData, ContentType, -} from "@/types/auto-like"; +} from "@/pages/workspace/auto-like/record/api"; import style from "./new.module.scss"; const contentTypeLabels: Record = { diff --git a/nkebao/src/types/auto-like.ts b/nkebao/src/pages/workspace/auto-like/record/api.ts similarity index 100% rename from nkebao/src/types/auto-like.ts rename to nkebao/src/pages/workspace/auto-like/record/api.ts diff --git a/nkebao/src/api/autoLike.ts b/nkebao/src/pages/workspace/auto-like/record/data.ts similarity index 93% rename from nkebao/src/api/autoLike.ts rename to nkebao/src/pages/workspace/auto-like/record/data.ts index 8ee3f4ba..122a093d 100644 --- a/nkebao/src/api/autoLike.ts +++ b/nkebao/src/pages/workspace/auto-like/record/data.ts @@ -1,4 +1,4 @@ -import { request } from "./request"; +import { request } from "../../../../api/request"; import { LikeTask, CreateLikeTaskData, @@ -6,7 +6,7 @@ import { LikeRecord, ApiResponse, PaginatedResponse, -} from "@/types/auto-like"; +} from "@/pages/workspace/auto-like/record/api"; // 获取自动点赞任务列表 export async function fetchAutoLikeTasks(): Promise { diff --git a/nkebao/src/pages/workspace/auto-like/record/index.tsx b/nkebao/src/pages/workspace/auto-like/record/index.tsx index 4d52913a..6eb12b60 100644 --- a/nkebao/src/pages/workspace/auto-like/record/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/record/index.tsx @@ -12,8 +12,8 @@ import { import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; -import { fetchLikeRecords, fetchAutoLikeTaskDetail } from "@/api/autoLike"; -import { LikeRecord, LikeTask } from "@/types/auto-like"; +import { fetchLikeRecords, fetchAutoLikeTaskDetail } from "./data"; +import { LikeRecord, LikeTask } from "./api"; import style from "./record.module.scss"; // 格式化日期 diff --git a/nkebao/src/api/groupPush.ts b/nkebao/src/pages/workspace/group-push/detail/groupPush.ts similarity index 94% rename from nkebao/src/api/groupPush.ts rename to nkebao/src/pages/workspace/group-push/detail/groupPush.ts index 00ab3620..c1b06848 100644 --- a/nkebao/src/api/groupPush.ts +++ b/nkebao/src/pages/workspace/group-push/detail/groupPush.ts @@ -1,4 +1,4 @@ -import request from "./request"; +import request from "../../../../api/request"; export interface GroupPushTask { id: string; diff --git a/nkebao/src/pages/workspace/group-push/detail/index.tsx b/nkebao/src/pages/workspace/group-push/detail/index.tsx index 5e316ff7..e7806476 100644 --- a/nkebao/src/pages/workspace/group-push/detail/index.tsx +++ b/nkebao/src/pages/workspace/group-push/detail/index.tsx @@ -10,7 +10,10 @@ import { } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; -import { getGroupPushTaskDetail, GroupPushTask } from "@/api/groupPush"; +import { + getGroupPushTaskDetail, + GroupPushTask, +} from "@/pages/workspace/group-push/detail/groupPush"; import styles from "./index.module.scss"; const Detail: React.FC = () => { diff --git a/nkebao/src/pages/workspace/group-push/form/index.tsx b/nkebao/src/pages/workspace/group-push/form/index.tsx index e6b4be35..8a57544b 100644 --- a/nkebao/src/pages/workspace/group-push/form/index.tsx +++ b/nkebao/src/pages/workspace/group-push/form/index.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { Button } from "antd-mobile"; import { NavBar } from "antd-mobile"; import { LeftOutline } from "antd-mobile-icons"; -import { createGroupPushTask } from "@/api/groupPush"; +import { createGroupPushTask } from "@/pages/workspace/group-push/detail/groupPush"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import StepIndicator from "./components/StepIndicator"; diff --git a/nkebao/src/pages/workspace/group-push/list/index.tsx b/nkebao/src/pages/workspace/group-push/list/index.tsx index 2fe91634..6eaa35f1 100644 --- a/nkebao/src/pages/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/workspace/group-push/list/index.tsx @@ -38,7 +38,7 @@ import { toggleGroupPushTask, copyGroupPushTask, GroupPushTask, -} from "@/api/groupPush"; +} from "@/pages/workspace/group-push/detail/groupPush"; import styles from "./index.module.scss"; const { Search } = Input;