feat: 本次提交更新内容如下

通用设备、微信组件迁移
This commit is contained in:
2025-07-22 12:29:16 +08:00
parent 28059d7e2b
commit 2c299e3add
19 changed files with 1860 additions and 12 deletions

View File

@@ -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<WechatGroup[]>([]);
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 (
<>
{/* 输入框 */}
<div className={`${style.inputWrapper} ${className}`}>
<span className={style.inputIcon}>
<svg
width="20"
height="20"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</span>
<Input
placeholder={placeholder}
className={style.input}
readOnly
onClick={openPopup}
value={getDisplayText()}
/>
</div>
{/* 群组选择弹窗 */}
<Popup
visible={popupVisible}
onMaskClick={() => setPopupVisible(false)}
position="bottom"
bodyStyle={{ height: "80vh" }}
>
<div className={style.popupContainer}>
<div className={style.popupHeader}>
<div className={style.popupTitle}></div>
<div className={style.searchWrapper}>
<Input
placeholder="搜索群聊"
value={searchQuery}
onChange={(val) => setSearchQuery(val)}
className={style.searchInput}
/>
<SearchOutlined className={style.searchIcon} />
{searchQuery && (
<Button
fill="none"
size="mini"
className={style.clearBtn}
onClick={handleClearSearch}
>
<CloseOutlined />
</Button>
)}
</div>
</div>
<div className={style.groupList}>
{loading ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : groups.length > 0 ? (
<div className={style.groupListInner}>
{groups.map((group) => (
<label
key={group.id}
className={style.groupItem}
onClick={() => handleGroupToggle(group.id)}
>
<div className={style.radioWrapper}>
<div
className={
selectedGroups.includes(group.id)
? style.radioSelected
: style.radioUnselected
}
>
{selectedGroups.includes(group.id) && (
<div className={style.radioDot}></div>
)}
</div>
</div>
<div className={style.groupInfo}>
<div className={style.groupAvatar}>
{group.avatar ? (
<img
src={group.avatar}
alt={group.name}
className={style.avatarImg}
/>
) : (
group.name.charAt(0)
)}
</div>
<div className={style.groupDetail}>
<div className={style.groupName}>{group.name}</div>
<div className={style.groupId}>
ID: {group.chatroomId}
</div>
{group.ownerNickname && (
<div className={style.groupOwner}>
: {group.ownerNickname}
</div>
)}
</div>
</div>
</label>
))}
</div>
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}>
{searchQuery
? `没有找到包含"${searchQuery}"的群聊`
: "没有找到群聊"}
</div>
</div>
)}
</div>
<div className={style.paginationRow}>
<div className={style.totalCount}>
{totalGroups}
{searchQuery && ` (搜索: "${searchQuery}")`}
</div>
<div className={style.paginationControls}>
<Button
fill="none"
size="mini"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1 || loading}
className={style.pageBtn}
>
<LeftOutlined />
</Button>
<span className={style.pageInfo}>
{currentPage} / {totalPages}
</span>
<Button
fill="none"
size="mini"
onClick={() =>
setCurrentPage(Math.min(totalPages, currentPage + 1))
}
disabled={currentPage === totalPages || loading}
className={style.pageBtn}
>
<RightOutlined />
</Button>
</div>
</div>
<div className={style.popupFooter}>
<Button
fill="outline"
onClick={() => setPopupVisible(false)}
className={style.cancelBtn}
>
</Button>
<Button
color="primary"
onClick={handleConfirm}
className={style.confirmBtn}
>
({selectedGroups.length})
</Button>
</div>
</div>
</Popup>
</>
);
}

View File

@@ -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;
}