Merge branch 'yongpxu-dev' into yongpxu-dev2

# Conflicts:
#	nkebao/src/pages/mobile/test/select.tsx   resolved by yongpxu-dev version
This commit is contained in:
超级老白兔
2025-08-08 15:04:39 +08:00
7 changed files with 314 additions and 277 deletions

View File

@@ -1,6 +1,6 @@
// 设备选择项接口
export interface DeviceSelectionItem {
id: string;
id: number;
name: string;
imei: string;
wechatId: string;
@@ -12,8 +12,8 @@ export interface DeviceSelectionItem {
// 组件属性接口
export interface DeviceSelectionProps {
selectedOptions: string[];
onSelect: (devices: string[]) => void;
selectedOptions: DeviceSelectionItem[];
onSelect: (devices: DeviceSelectionItem[]) => void;
placeholder?: string;
className?: string;
mode?: "input" | "dialog"; // 新增默认input

View File

@@ -41,9 +41,9 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
};
// 删除已选设备
const handleRemoveDevice = (id: string) => {
const handleRemoveDevice = (id: number) => {
if (readonly) return;
onSelect(selectedOptions.filter(d => d !== id));
onSelect(selectedOptions.filter(v => v.id !== id));
};
return (
@@ -79,9 +79,9 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
background: "#fff",
}}
>
{selectedOptions.map(deviceId => (
{selectedOptions.map(device => (
<div
key={deviceId}
key={device.id}
className={style.selectedListRow}
style={{
display: "flex",
@@ -100,7 +100,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
textOverflow: "ellipsis",
}}
>
{deviceId}
{device.name} - {device.wechatId}
</div>
{!readonly && (
<Button
@@ -118,7 +118,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveDevice(deviceId)}
onClick={() => handleRemoveDevice(device.id)}
/>
)}
</div>

View File

@@ -5,23 +5,13 @@ 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;
name: string;
imei: string;
wechatId: string;
status: "online" | "offline";
wxid?: string;
nickname?: string;
usedInPlans?: number;
}
import { DeviceSelectionItem } from "./data";
interface SelectionPopupProps {
visible: boolean;
onClose: () => void;
selectedOptions: string[];
onSelect: (devices: string[]) => void;
selectedOptions: DeviceSelectionItem[];
onSelect: (devices: DeviceSelectionItem[]) => void;
}
const PAGE_SIZE = 20;
@@ -112,11 +102,12 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
// 处理设备选择
const handleDeviceToggle = (deviceId: string) => {
if (selectedOptions.includes(deviceId)) {
onSelect(selectedOptions.filter(id => id !== deviceId));
const handleDeviceToggle = (device: DeviceSelectionItem) => {
if (selectedOptions.some(v => v.id === device.id)) {
onSelect(selectedOptions.filter(v => v.id !== device.id));
} else {
onSelect([...selectedOptions, deviceId]);
const newSelectedOptions = [...selectedOptions, device];
onSelect(newSelectedOptions);
}
};
@@ -172,8 +163,8 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
{filteredDevices.map(device => (
<label key={device.id} className={style.deviceItem}>
<Checkbox
checked={selectedOptions.includes(device.id)}
onChange={() => handleDeviceToggle(device.id)}
checked={selectedOptions.some(v => v.id === device.id)}
onChange={() => handleDeviceToggle(device)}
className={style.deviceCheckbox}
/>
<div className={style.deviceInfo}>

View File

@@ -1,6 +1,21 @@
.inputWrapper {
position: relative;
}
.selectedListRow {
padding: 8px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.selectedListRowContent {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.selectedListRowContentText {
flex: 1;
}
.inputIcon {
position: absolute;
left: 12px;

View File

@@ -1,13 +1,10 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
import { Popup } from "antd-mobile";
import { Button, Input } from "antd";
import { getFriendList } from "./api";
import { Avatar } from "antd-mobile";
import style from "./index.module.scss";
import Layout from "@/components/Layout/Layout";
import PopupHeader from "@/components/PopuLayout/header";
import PopupFooter from "@/components/PopuLayout/footer";
import { FriendSelectionProps, FriendSelectionItem } from "./data";
import { FriendSelectionProps } from "./data";
import SelectionPopup from "./selectionPopup";
export default function FriendSelection({
selectedOptions,
@@ -25,12 +22,7 @@ export default function FriendSelection({
onConfirm,
}: FriendSelectionProps) {
const [popupVisible, setPopupVisible] = useState(false);
const [friends, setFriends] = useState<FriendSelectionItem[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalFriends, setTotalFriends] = useState(0);
const [loading, setLoading] = useState(false);
// 内部弹窗交给 selectionPopup 处理
// 受控弹窗逻辑
const realVisible = visible !== undefined ? visible : popupVisible;
@@ -39,75 +31,10 @@ export default function FriendSelection({
if (visible === undefined) setPopupVisible(v);
};
// 打开弹窗并请求第一页好友
// 打开弹窗
const openPopup = () => {
if (readonly) return;
setCurrentPage(1);
setSearchQuery("");
setRealVisible(true);
fetchFriends(1, "");
};
// 当页码变化时,拉取对应页数据(弹窗已打开时)
useEffect(() => {
if (realVisible && currentPage !== 1) {
fetchFriends(currentPage, searchQuery);
}
}, [currentPage, realVisible, searchQuery]);
// 搜索防抖
useEffect(() => {
if (!realVisible) return;
const timer = setTimeout(() => {
setCurrentPage(1);
fetchFriends(1, searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery, realVisible]);
// 获取好友列表API - 添加 keyword 参数
const fetchFriends = async (page: number, keyword: string = "") => {
setLoading(true);
try {
const params: any = {
page,
limit: 20,
};
if (keyword.trim()) {
params.keyword = keyword.trim();
}
if (enableDeviceFilter && deviceIds.length > 0) {
params.deviceIds = deviceIds.join(",");
}
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);
} finally {
setLoading(false);
}
};
// 处理好友选择
const handleFriendToggle = (friend: FriendSelectionItem) => {
if (readonly) return;
const newSelectedFriends = selectedOptions
.map(v => v.id)
.includes(friend.id)
? selectedOptions.filter(v => v.id !== friend.id)
: selectedOptions.concat(friend);
onSelect(newSelectedFriends);
};
// 获取显示文本
@@ -122,14 +49,13 @@ export default function FriendSelection({
onSelect(selectedOptions.filter(v => v.id !== id));
};
// 确认选择
const handleConfirm = () => {
if (onConfirm) {
onConfirm(
selectedOptions.map(v => v.id),
selectedOptions,
);
}
// 弹窗确认回调
const handleConfirm = (
selectedIds: number[],
selectedItems: typeof selectedOptions,
) => {
onSelect(selectedItems);
if (onConfirm) onConfirm(selectedIds, selectedItems);
setRealVisible(false);
};
@@ -167,151 +93,48 @@ export default function FriendSelection({
}}
>
{selectedOptions.map(friend => (
<div
key={friend.id}
className={style.selectedListRow}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
borderBottom: "1px solid #f0f0f0",
fontSize: 14,
}}
>
<div
style={{
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{friend.nickname || friend.wechatId || friend.id}
<div key={friend.id} className={style.selectedListRow}>
<div className={style.selectedListRowContent}>
<Avatar src={friend.avatar} />
<div className={style.selectedListRowContentText}>
<div>{friend.nickname}</div>
<div>{friend.wechatId}</div>
</div>
{!readonly && (
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
style={{
marginLeft: 4,
color: "#ff4d4f",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveFriend(friend.id)}
/>
)}
</div>
{!readonly && (
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
style={{
marginLeft: 4,
color: "#ff4d4f",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveFriend(friend.id)}
/>
)}
</div>
))}
</div>
)}
{/* 弹窗 */}
<Popup
<SelectionPopup
visible={realVisible && !readonly}
onMaskClick={() => setRealVisible(false)}
position="bottom"
bodyStyle={{ height: "100vh" }}
>
<Layout
header={
<PopupHeader
title="选择微信好友"
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchPlaceholder="搜索好友"
loading={loading}
onRefresh={() => fetchFriends(currentPage, searchQuery)}
/>
}
footer={
<PopupFooter
total={totalFriends}
currentPage={currentPage}
totalPages={totalPages}
loading={loading}
selectedCount={selectedOptions.length}
onPageChange={setCurrentPage}
onCancel={() => setRealVisible(false)}
onConfirm={handleConfirm}
/>
}
>
<div className={style.friendList}>
{loading ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : friends.length > 0 ? (
<div className={style.friendListInner}>
{friends.map(friend => (
<label
key={friend.id}
className={style.friendItem}
onClick={() => !readonly && handleFriendToggle(friend)}
>
<div className={style.radioWrapper}>
<div
className={
selectedOptions.map(v => v.id).includes(friend.id)
? style.radioSelected
: style.radioUnselected
}
>
{selectedOptions.map(v => v.id).includes(friend.id) && (
<div className={style.radioDot}></div>
)}
</div>
</div>
<div className={style.friendInfo}>
<div className={style.friendAvatar}>
{friend.avatar ? (
<img
src={friend.avatar}
alt={friend.nickname}
className={style.avatarImg}
/>
) : (
friend.nickname.charAt(0)
)}
</div>
<div className={style.friendDetail}>
<div className={style.friendName}>
{friend.nickname}
</div>
<div className={style.friendId}>
ID: {friend.wechatId}
</div>
{friend.customer && (
<div className={style.friendCustomer}>
: {friend.customer}
</div>
)}
</div>
</div>
</label>
))}
</div>
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}>
{deviceIds.length === 0
? "请先选择设备"
: searchQuery
? `没有找到包含"${searchQuery}"的好友`
: "没有找到好友"}
</div>
</div>
)}
</div>
</Layout>
</Popup>
onVisibleChange={setRealVisible}
selectedOptions={selectedOptions}
onSelect={onSelect}
deviceIds={deviceIds}
enableDeviceFilter={enableDeviceFilter}
readonly={readonly}
onConfirm={handleConfirm}
/>
</>
);
}

View File

@@ -0,0 +1,213 @@
import React, { useCallback, useEffect, useState } from "react";
import { Popup, Checkbox } from "antd-mobile";
import Layout from "@/components/Layout/Layout";
import PopupHeader from "@/components/PopuLayout/header";
import PopupFooter from "@/components/PopuLayout/footer";
import { getFriendList } from "./api";
import style from "./index.module.scss";
import type { FriendSelectionItem } from "./data";
interface SelectionPopupProps {
visible: boolean;
onVisibleChange: (visible: boolean) => void;
selectedOptions: FriendSelectionItem[];
onSelect: (friends: FriendSelectionItem[]) => void;
deviceIds?: string[];
enableDeviceFilter?: boolean;
readonly?: boolean;
onConfirm?: (
selectedIds: number[],
selectedItems: FriendSelectionItem[],
) => void;
}
const SelectionPopup: React.FC<SelectionPopupProps> = ({
visible,
onVisibleChange,
selectedOptions,
onSelect,
deviceIds = [],
enableDeviceFilter = true,
readonly = false,
onConfirm,
}) => {
const [friends, setFriends] = useState<FriendSelectionItem[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalFriends, setTotalFriends] = useState(0);
const [loading, setLoading] = useState(false);
// 获取好友列表API
const fetchFriends = useCallback(
async (page: number, keyword: string = "") => {
setLoading(true);
try {
const params: any = {
page,
limit: 20,
};
if (keyword.trim()) {
params.keyword = keyword.trim();
}
if (enableDeviceFilter && deviceIds.length > 0) {
params.deviceIds = deviceIds.join(",");
}
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);
} finally {
setLoading(false);
}
},
[deviceIds, enableDeviceFilter],
);
// 处理好友选择
const handleFriendToggle = (friend: FriendSelectionItem) => {
if (readonly) return;
const newSelectedFriends = selectedOptions.some(f => f.id === friend.id)
? selectedOptions.filter(f => f.id !== friend.id)
: selectedOptions.concat(friend);
onSelect(newSelectedFriends);
};
// 确认选择
const handleConfirm = () => {
if (onConfirm) {
onConfirm(
selectedOptions.map(v => v.id),
selectedOptions,
);
}
onVisibleChange(false);
};
// 弹窗打开时初始化
useEffect(() => {
if (visible) {
setCurrentPage(1);
setSearchQuery("");
fetchFriends(1, "");
}
}, [visible]); // 只在弹窗开启时请求
// 搜索防抖(只在弹窗打开且搜索词变化时执行)
useEffect(() => {
if (!visible || searchQuery === "") return; // 弹窗关闭或搜索词为空时不请求
const timer = setTimeout(() => {
setCurrentPage(1);
fetchFriends(1, searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery, visible]);
// 页码变化时请求数据只在弹窗打开且页码不是1时执行
useEffect(() => {
if (!visible || currentPage === 1) return; // 弹窗关闭或第一页时不请求
fetchFriends(currentPage, searchQuery);
}, [currentPage, visible, searchQuery]);
return (
<Popup
visible={visible && !readonly}
onMaskClick={() => onVisibleChange(false)}
position="bottom"
bodyStyle={{ height: "100vh" }}
>
<Layout
header={
<PopupHeader
title="选择微信好友"
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchPlaceholder="搜索好友"
loading={loading}
onRefresh={() => fetchFriends(currentPage, searchQuery)}
/>
}
footer={
<PopupFooter
total={totalFriends}
currentPage={currentPage}
totalPages={totalPages}
loading={loading}
selectedCount={selectedOptions.length}
onPageChange={setCurrentPage}
onCancel={() => onVisibleChange(false)}
onConfirm={handleConfirm}
/>
}
>
<div className={style.friendList}>
{loading ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : friends.length > 0 ? (
<div className={style.friendListInner}>
{friends.map(friend => (
<div key={friend.id} className={style.friendItem}>
<Checkbox
checked={selectedOptions.some(f => f.id === friend.id)}
onChange={() => !readonly && handleFriendToggle(friend)}
disabled={readonly}
style={{ marginRight: 12 }}
/>
<div className={style.friendInfo}>
<div className={style.friendAvatar}>
{friend.avatar ? (
<img
src={friend.avatar}
alt={friend.nickname}
className={style.avatarImg}
/>
) : (
friend.nickname.charAt(0)
)}
</div>
<div className={style.friendDetail}>
<div className={style.friendName}>{friend.nickname}</div>
<div className={style.friendId}>
ID: {friend.wechatId}
</div>
{friend.customer && (
<div className={style.friendCustomer}>
: {friend.customer}
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}>
{deviceIds.length === 0
? "请先选择设备"
: searchQuery
? `没有找到包含"${searchQuery}"的好友`
: "没有找到好友"}
</div>
</div>
)}
</div>
</Layout>
</Popup>
);
};
export default SelectionPopup;

View File

@@ -11,12 +11,14 @@ import { isDevelopment } from "@/utils/env";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { ContentItem } from "@/components/ContentSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
import { AccountItem } from "@/components/AccountSelection/data";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
const ComponentTest: React.FC = () => {
const [activeTab, setActiveTab] = useState("accounts");
const [activeTab, setActiveTab] = useState("devices");
// 设备选择状态
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
[],
);
// 群组选择状态
const [selectedGroups, setSelectedGroups] = useState<GroupSelectionItem[]>(
@@ -26,7 +28,8 @@ const ComponentTest: React.FC = () => {
// 内容库选择状态
const [selectedContent, setSelectedContent] = useState<ContentItem[]>([]);
const [selectedAccounts, setSelectedAccounts] = useState<AccountItem[]>([]);
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
// 好友选择状态
const [selectedFriendsOptions, setSelectedFriendsOptions] = useState<
FriendSelectionItem[]
>([]);
@@ -58,7 +61,7 @@ const ComponentTest: React.FC = () => {
<div style={{ padding: "16px 0" }}>
<h3 style={{ marginBottom: 16 }}>ContentSelection </h3>
<ContentSelection
selectedOptions={selectedContent}
selectedContent={selectedContent}
onSelect={setSelectedContent}
placeholder="请选择内容库"
showSelectedList={true}
@@ -84,7 +87,7 @@ const ComponentTest: React.FC = () => {
<div style={{ padding: "16px 0" }}>
<h3 style={{ marginBottom: 16 }}>GroupSelection </h3>
<GroupSelection
selectedOptions={selectedGroups}
selectedGroups={selectedGroups}
onSelect={setSelectedGroups}
placeholder="请选择微信群组"
showSelectedList={true}
@@ -126,7 +129,8 @@ const ComponentTest: React.FC = () => {
>
<strong>:</strong> {selectedDevices.length}
<br />
<strong>ID:</strong> {selectedDevices.join(", ") || "无"}
<strong>ID:</strong>
{selectedDevices.map(d => d.id).join(", ") || "无"}
</div>
</div>
</Tabs.Tab>
@@ -135,24 +139,15 @@ const ComponentTest: React.FC = () => {
<div style={{ padding: "16px 0" }}>
<h3 style={{ marginBottom: 16 }}>AccountSelection </h3>
<AccountSelection
selectedOptions={selectedAccounts}
onSelect={setSelectedAccounts}
placeholder="请选择账号"
showSelectedList={true}
selectedListMaxHeight={300}
value={selectedAccounts}
onChange={setSelectedAccounts}
// 可根据实际API和props补充其它参数
/>
<div
style={{
marginTop: 16,
padding: 12,
background: "#f5f5f5",
borderRadius: 8,
}}
>
<strong>:</strong> {selectedAccounts.length}
<br />
<strong>ID:</strong>{" "}
{selectedAccounts.map(a => a.id).join(", ") || "无"}
<div style={{ marginTop: 16 }}>
<strong></strong>
{selectedAccounts.length > 0
? selectedAccounts.join(", ")
: "无"}
</div>
</div>
</Tabs.Tab>