feat: 本次提交更新内容如下
功能、和样式修复
This commit is contained in:
@@ -1,22 +1,27 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios';
|
import axios, {
|
||||||
import { Toast } from 'antd-mobile';
|
AxiosInstance,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
Method,
|
||||||
|
AxiosResponse,
|
||||||
|
} from "axios";
|
||||||
|
import { Toast } from "antd-mobile";
|
||||||
|
|
||||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||||
const debounceMap = new Map<string, number>();
|
const debounceMap = new Map<string, number>();
|
||||||
|
|
||||||
const instance: AxiosInstance = axios.create({
|
const instance: AxiosInstance = axios.create({
|
||||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api',
|
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.interceptors.request.use(config => {
|
instance.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers = config.headers || {};
|
config.headers = config.headers || {};
|
||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
config.headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
@@ -27,20 +32,20 @@ instance.interceptors.response.use(
|
|||||||
if (code === 200 || success) {
|
if (code === 200 || success) {
|
||||||
return res.data.data ?? res.data;
|
return res.data.data ?? res.data;
|
||||||
}
|
}
|
||||||
Toast.show({ content: msg || '接口错误', position: 'top' });
|
Toast.show({ content: msg || "接口错误", position: "top" });
|
||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem("token");
|
||||||
const currentPath = window.location.pathname + window.location.search;
|
const currentPath = window.location.pathname + window.location.search;
|
||||||
if (currentPath === '/login') {
|
if (currentPath === "/login") {
|
||||||
window.location.href = '/login';
|
window.location.href = "/login";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(msg || '接口错误');
|
return Promise.reject(msg || "接口错误");
|
||||||
},
|
},
|
||||||
err => {
|
(err) => {
|
||||||
Toast.show({ content: err.message || '网络异常', position: 'top' });
|
Toast.show({ content: err.message || "网络异常", position: "top" });
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -48,17 +53,18 @@ instance.interceptors.response.use(
|
|||||||
export function request(
|
export function request(
|
||||||
url: string,
|
url: string,
|
||||||
data?: any,
|
data?: any,
|
||||||
method: Method = 'GET',
|
method: Method = "GET",
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
debounceGap?: number
|
debounceGap?: number
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
const gap =
|
||||||
|
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const last = debounceMap.get(key) || 0;
|
const last = debounceMap.get(key) || 0;
|
||||||
if (gap > 0 && now - last < gap) {
|
if (gap > 0 && now - last < gap) {
|
||||||
Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||||
return Promise.reject('请求过于频繁,请稍后再试');
|
return Promise.reject("请求过于频繁,请稍后再试");
|
||||||
}
|
}
|
||||||
debounceMap.set(key, now);
|
debounceMap.set(key, now);
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ export function request(
|
|||||||
method,
|
method,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
if (method.toUpperCase() === 'GET') {
|
if (method.toUpperCase() === "GET") {
|
||||||
axiosConfig.params = data;
|
axiosConfig.params = data;
|
||||||
} else {
|
} else {
|
||||||
axiosConfig.data = data;
|
axiosConfig.data = data;
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ export function getDeviceList(params: {
|
|||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/device/list", params, "GET");
|
return request("/v1/devices", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile";
|
import { Checkbox, Popup, Toast } from "antd-mobile";
|
||||||
|
import { Input, Button } from "antd";
|
||||||
import { getDeviceList } from "./api";
|
import { getDeviceList } from "./api";
|
||||||
import style from "./module.scss";
|
import style from "./index.module.scss";
|
||||||
|
|
||||||
// 设备选择项接口
|
// 设备选择项接口
|
||||||
interface DeviceSelectionItem {
|
interface DeviceSelectionItem {
|
||||||
@@ -56,7 +57,6 @@ export default function DeviceSelection({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取设备列表失败:", error);
|
console.error("获取设备列表失败:", error);
|
||||||
Toast.show({ content: "获取设备列表失败", position: "top" });
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -106,13 +106,13 @@ export default function DeviceSelection({
|
|||||||
<>
|
<>
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
<div className={`${style.inputWrapper} ${className}`}>
|
<div className={`${style.inputWrapper} ${className}`}>
|
||||||
<SearchOutlined className={style.inputIcon} />
|
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={style.input}
|
|
||||||
readOnly
|
|
||||||
onClick={openPopup}
|
|
||||||
value={getDisplayText()}
|
value={getDisplayText()}
|
||||||
|
onClick={openPopup}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export default function DeviceSelection({
|
|||||||
visible={popupVisible}
|
visible={popupVisible}
|
||||||
onMaskClick={() => setPopupVisible(false)}
|
onMaskClick={() => setPopupVisible(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "80vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<div className={style.popupContainer}>
|
||||||
<div className={style.popupHeader}>
|
<div className={style.popupHeader}>
|
||||||
@@ -129,12 +129,13 @@ export default function DeviceSelection({
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.popupSearchRow}>
|
<div className={style.popupSearchRow}>
|
||||||
<div className={style.popupSearchInputWrap}>
|
<div className={style.popupSearchInputWrap}>
|
||||||
<SearchOutlined className={style.inputIcon} />
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索设备IMEI/备注/微信号"
|
placeholder="搜索设备IMEI/备注/微信号"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(val) => setSearchQuery(val)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className={style.popupSearchInput}
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@@ -146,6 +147,19 @@ export default function DeviceSelection({
|
|||||||
<option value="online">在线</option>
|
<option value="online">在线</option>
|
||||||
<option value="offline">离线</option>
|
<option value="offline">离线</option>
|
||||||
</select>
|
</select>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={() => fetchDevices(searchQuery)}
|
||||||
|
disabled={loading}
|
||||||
|
className={style.refreshBtn}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className={style.loadingIcon}>⟳</div>
|
||||||
|
) : (
|
||||||
|
<ReloadOutlined />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.deviceList}>
|
<div className={style.deviceList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ export function getDeviceList(params: {
|
|||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/device/list", params, "GET");
|
return request("/v1/devices", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
@@ -49,11 +49,6 @@
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.refreshBtn {
|
|
||||||
min-width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.loadingIcon {
|
.loadingIcon {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile";
|
import { Checkbox, Popup, Toast } from "antd-mobile";
|
||||||
|
import { Input, Button } from "antd";
|
||||||
import { getDeviceList } from "./api";
|
import { getDeviceList } from "./api";
|
||||||
import style from "./module.scss";
|
import style from "./index.module.scss";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -106,7 +107,7 @@ export function DeviceSelectionDialog({
|
|||||||
visible={open}
|
visible={open}
|
||||||
onMaskClick={() => onOpenChange(false)}
|
onMaskClick={() => onOpenChange(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "80vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<div className={style.popupContainer}>
|
||||||
<div className={style.popupHeader}>
|
<div className={style.popupHeader}>
|
||||||
@@ -114,12 +115,13 @@ export function DeviceSelectionDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.popupSearchRow}>
|
<div className={style.popupSearchRow}>
|
||||||
<div className={style.popupSearchInputWrap}>
|
<div className={style.popupSearchInputWrap}>
|
||||||
<SearchOutlined className={style.inputIcon} />
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索设备IMEI/备注"
|
placeholder="搜索设备IMEI/备注/微信号"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(val) => setSearchQuery(val)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className={style.popupSearchInput}
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@@ -132,8 +134,8 @@ export function DeviceSelectionDialog({
|
|||||||
<option value="offline">离线</option>
|
<option value="offline">离线</option>
|
||||||
</select>
|
</select>
|
||||||
<Button
|
<Button
|
||||||
fill="outline"
|
type="primary"
|
||||||
size="mini"
|
size="large"
|
||||||
onClick={() => fetchDevices(searchQuery)}
|
onClick={() => fetchDevices(searchQuery)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={style.refreshBtn}
|
className={style.refreshBtn}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import request from "@/api/request";
|
|||||||
export function getFriendList(params: {
|
export function getFriendList(params: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
deviceIds?: string;
|
deviceIds?: string; // 逗号分隔
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/friend", params, "GET");
|
return request("/v1/friend", params, "GET");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Input, Button, Popup, Toast } from "antd-mobile";
|
import { Input, Button, Popup, Toast } from "antd-mobile";
|
||||||
import { getFriendList } from "./api";
|
import { getFriendList } from "./api";
|
||||||
import style from "./module.scss";
|
import style from "./index.module.scss";
|
||||||
|
|
||||||
// 微信好友接口类型
|
// 微信好友接口类型
|
||||||
interface WechatFriend {
|
interface WechatFriend {
|
||||||
@@ -27,6 +27,8 @@ interface FriendSelectionProps {
|
|||||||
enableDeviceFilter?: boolean;
|
enableDeviceFilter?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
visible?: boolean; // 新增
|
||||||
|
onVisibleChange?: (visible: boolean) => void; // 新增
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FriendSelection({
|
export default function FriendSelection({
|
||||||
@@ -37,6 +39,8 @@ export default function FriendSelection({
|
|||||||
enableDeviceFilter = true,
|
enableDeviceFilter = true,
|
||||||
placeholder = "选择微信好友",
|
placeholder = "选择微信好友",
|
||||||
className = "",
|
className = "",
|
||||||
|
visible,
|
||||||
|
onVisibleChange,
|
||||||
}: FriendSelectionProps) {
|
}: FriendSelectionProps) {
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [friends, setFriends] = useState<WechatFriend[]>([]);
|
const [friends, setFriends] = useState<WechatFriend[]>([]);
|
||||||
@@ -46,24 +50,31 @@ export default function FriendSelection({
|
|||||||
const [totalFriends, setTotalFriends] = useState(0);
|
const [totalFriends, setTotalFriends] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 受控弹窗逻辑
|
||||||
|
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||||
|
const setRealVisible = (v: boolean) => {
|
||||||
|
if (onVisibleChange) onVisibleChange(v);
|
||||||
|
if (visible === undefined) setPopupVisible(v);
|
||||||
|
};
|
||||||
|
|
||||||
// 打开弹窗并请求第一页好友
|
// 打开弹窗并请求第一页好友
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setPopupVisible(true);
|
setRealVisible(true);
|
||||||
fetchFriends(1, "");
|
fetchFriends(1, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (popupVisible && currentPage !== 1) {
|
if (realVisible && currentPage !== 1) {
|
||||||
fetchFriends(currentPage, searchQuery);
|
fetchFriends(currentPage, searchQuery);
|
||||||
}
|
}
|
||||||
}, [currentPage, popupVisible, searchQuery]);
|
}, [currentPage, realVisible, searchQuery]);
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!popupVisible) return;
|
if (!realVisible) return;
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
@@ -71,7 +82,7 @@ export default function FriendSelection({
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, popupVisible]);
|
}, [searchQuery, realVisible]);
|
||||||
|
|
||||||
// 获取好友列表API - 添加 keyword 参数
|
// 获取好友列表API - 添加 keyword 参数
|
||||||
const fetchFriends = async (page: number, keyword: string = "") => {
|
const fetchFriends = async (page: number, keyword: string = "") => {
|
||||||
@@ -183,10 +194,10 @@ export default function FriendSelection({
|
|||||||
|
|
||||||
{/* 微信好友选择弹窗 */}
|
{/* 微信好友选择弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={realVisible}
|
||||||
onMaskClick={() => setPopupVisible(false)}
|
onMaskClick={() => setRealVisible(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "80vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<div className={style.popupContainer}>
|
||||||
<div className={style.popupHeader}>
|
<div className={style.popupHeader}>
|
||||||
@@ -312,14 +323,17 @@ export default function FriendSelection({
|
|||||||
<div className={style.popupFooter}>
|
<div className={style.popupFooter}>
|
||||||
<Button
|
<Button
|
||||||
fill="outline"
|
fill="outline"
|
||||||
onClick={() => setPopupVisible(false)}
|
onClick={() => setRealVisible(false)}
|
||||||
className={style.cancelBtn}
|
className={style.cancelBtn}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleConfirm}
|
onClick={() => {
|
||||||
|
setRealVisible(false);
|
||||||
|
handleConfirm();
|
||||||
|
}}
|
||||||
className={style.confirmBtn}
|
className={style.confirmBtn}
|
||||||
>
|
>
|
||||||
确定 ({selectedFriends.length})
|
确定 ({selectedFriends.length})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Input, Button, Popup, Toast } from "antd-mobile";
|
import { Input, Button, Popup, Toast } from "antd-mobile";
|
||||||
import { getGroupList } from "./api";
|
import { getGroupList } from "./api";
|
||||||
import style from "./module.scss";
|
import style from "./index.module.scss";
|
||||||
|
|
||||||
// 群组接口类型
|
// 群组接口类型
|
||||||
interface WechatGroup {
|
interface WechatGroup {
|
||||||
@@ -27,6 +27,8 @@ interface GroupSelectionProps {
|
|||||||
onSelectDetail?: (groups: WechatGroup[]) => void;
|
onSelectDetail?: (groups: WechatGroup[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
visible?: boolean; // 新增
|
||||||
|
onVisibleChange?: (visible: boolean) => void; // 新增
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GroupSelection({
|
export default function GroupSelection({
|
||||||
@@ -35,6 +37,8 @@ export default function GroupSelection({
|
|||||||
onSelectDetail,
|
onSelectDetail,
|
||||||
placeholder = "选择群聊",
|
placeholder = "选择群聊",
|
||||||
className = "",
|
className = "",
|
||||||
|
visible,
|
||||||
|
onVisibleChange,
|
||||||
}: GroupSelectionProps) {
|
}: GroupSelectionProps) {
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [groups, setGroups] = useState<WechatGroup[]>([]);
|
const [groups, setGroups] = useState<WechatGroup[]>([]);
|
||||||
@@ -44,30 +48,37 @@ export default function GroupSelection({
|
|||||||
const [totalGroups, setTotalGroups] = useState(0);
|
const [totalGroups, setTotalGroups] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 受控弹窗逻辑
|
||||||
|
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||||
|
const setRealVisible = (v: boolean) => {
|
||||||
|
if (onVisibleChange) onVisibleChange(v);
|
||||||
|
if (visible === undefined) setPopupVisible(v);
|
||||||
|
};
|
||||||
|
|
||||||
// 打开弹窗并请求第一页群组
|
// 打开弹窗并请求第一页群组
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setPopupVisible(true);
|
setRealVisible(true);
|
||||||
fetchGroups(1, "");
|
fetchGroups(1, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (popupVisible && currentPage !== 1) {
|
if (realVisible && currentPage !== 1) {
|
||||||
fetchGroups(currentPage, searchQuery);
|
fetchGroups(currentPage, searchQuery);
|
||||||
}
|
}
|
||||||
}, [currentPage, popupVisible, searchQuery]);
|
}, [currentPage, realVisible, searchQuery]);
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!popupVisible) return;
|
if (!realVisible) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchGroups(1, searchQuery);
|
fetchGroups(1, searchQuery);
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, popupVisible]);
|
}, [searchQuery, realVisible]);
|
||||||
|
|
||||||
// 获取群组列表API - 支持keyword
|
// 获取群组列表API - 支持keyword
|
||||||
const fetchGroups = async (page: number, keyword: string = "") => {
|
const fetchGroups = async (page: number, keyword: string = "") => {
|
||||||
@@ -128,7 +139,7 @@ export default function GroupSelection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
setPopupVisible(false);
|
setRealVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空搜索
|
// 清空搜索
|
||||||
@@ -169,10 +180,10 @@ export default function GroupSelection({
|
|||||||
|
|
||||||
{/* 群组选择弹窗 */}
|
{/* 群组选择弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={realVisible}
|
||||||
onMaskClick={() => setPopupVisible(false)}
|
onMaskClick={() => setRealVisible(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "80vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<div className={style.popupContainer}>
|
||||||
<div className={style.popupHeader}>
|
<div className={style.popupHeader}>
|
||||||
@@ -297,14 +308,17 @@ export default function GroupSelection({
|
|||||||
<div className={style.popupFooter}>
|
<div className={style.popupFooter}>
|
||||||
<Button
|
<Button
|
||||||
fill="outline"
|
fill="outline"
|
||||||
onClick={() => setPopupVisible(false)}
|
onClick={() => setRealVisible(false)}
|
||||||
className={style.cancelBtn}
|
className={style.cancelBtn}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleConfirm}
|
onClick={() => {
|
||||||
|
setRealVisible(false);
|
||||||
|
handleConfirm();
|
||||||
|
}}
|
||||||
className={style.confirmBtn}
|
className={style.confirmBtn}
|
||||||
>
|
>
|
||||||
确定 ({selectedGroups.length})
|
确定 ({selectedGroups.length})
|
||||||
|
|||||||
80
nkebao/src/components/SelectionTest.tsx
Normal file
80
nkebao/src/components/SelectionTest.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import DeviceSelection from "./DeviceSelection";
|
||||||
|
import { DeviceSelectionDialog } from "./DeviceSelectionDialog";
|
||||||
|
import FriendSelection from "./FriendSelection";
|
||||||
|
import GroupSelection from "./GroupSelection";
|
||||||
|
import { Button, Space } from "antd-mobile";
|
||||||
|
|
||||||
|
export default function SelectionTest() {
|
||||||
|
// 设备选择
|
||||||
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
|
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 好友选择
|
||||||
|
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||||
|
const [friendDialogOpen, setFriendDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 群组选择
|
||||||
|
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||||
|
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<h2>选择弹窗测试</h2>
|
||||||
|
<Space direction="vertical" block>
|
||||||
|
<div>
|
||||||
|
<b>DeviceSelection(内嵌输入框+弹窗)</b>
|
||||||
|
<DeviceSelection
|
||||||
|
selectedDevices={selectedDevices}
|
||||||
|
onSelect={setSelectedDevices}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>DeviceSelectionDialog(纯弹窗)</b>
|
||||||
|
<Button color="primary" onClick={() => setDeviceDialogOpen(true)}>
|
||||||
|
打开设备选择弹窗
|
||||||
|
</Button>
|
||||||
|
<DeviceSelectionDialog
|
||||||
|
open={deviceDialogOpen}
|
||||||
|
onOpenChange={setDeviceDialogOpen}
|
||||||
|
selectedDevices={selectedDevices}
|
||||||
|
onSelect={setSelectedDevices}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>FriendSelection</b>
|
||||||
|
<Button color="primary" onClick={() => setFriendDialogOpen(true)}>
|
||||||
|
打开好友选择弹窗
|
||||||
|
</Button>
|
||||||
|
<FriendSelection
|
||||||
|
selectedFriends={selectedFriends}
|
||||||
|
onSelect={setSelectedFriends}
|
||||||
|
placeholder="请选择微信好友"
|
||||||
|
className=""
|
||||||
|
visible={friendDialogOpen}
|
||||||
|
onVisibleChange={setFriendDialogOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>GroupSelection</b>
|
||||||
|
<Button color="primary" onClick={() => setGroupDialogOpen(true)}>
|
||||||
|
打开群组选择弹窗
|
||||||
|
</Button>
|
||||||
|
<GroupSelection
|
||||||
|
selectedGroups={selectedGroups}
|
||||||
|
onSelect={setSelectedGroups}
|
||||||
|
placeholder="请选择群聊"
|
||||||
|
className=""
|
||||||
|
visible={groupDialogOpen}
|
||||||
|
onVisibleChange={setGroupDialogOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
<div style={{ marginTop: 32 }}>
|
||||||
|
<div>已选设备ID: {selectedDevices.join(", ")}</div>
|
||||||
|
<div>已选好友ID: {selectedFriends.join(", ")}</div>
|
||||||
|
<div>已选群组ID: {selectedGroups.join(", ")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import Plans from "@/pages/plans/Plans";
|
|||||||
import PlanDetail from "@/pages/plans/PlanDetail";
|
import PlanDetail from "@/pages/plans/PlanDetail";
|
||||||
import Orders from "@/pages/orders/Orders";
|
import Orders from "@/pages/orders/Orders";
|
||||||
import ContactImport from "@/pages/contact-import/ContactImport";
|
import ContactImport from "@/pages/contact-import/ContactImport";
|
||||||
|
import SelectionTest from "@/components/SelectionTest";
|
||||||
|
|
||||||
const otherRoutes = [
|
const otherRoutes = [
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,11 @@ const otherRoutes = [
|
|||||||
element: <ContactImport />,
|
element: <ContactImport />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/selection-test",
|
||||||
|
element: <SelectionTest />,
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default otherRoutes;
|
export default otherRoutes;
|
||||||
|
|||||||
Reference in New Issue
Block a user