diff --git a/Touchkebao/src/components/MetailSelection/api.ts b/Touchkebao/src/components/MetailSelection/api.ts
new file mode 100644
index 00000000..2df6bc32
--- /dev/null
+++ b/Touchkebao/src/components/MetailSelection/api.ts
@@ -0,0 +1,10 @@
+import request from "@/api/request";
+
+// 获取群组列表
+export function getGroupList(params: {
+ page: number;
+ limit: number;
+ keyword?: string;
+}) {
+ return request("/v1/kefu/content/material/list", params, "GET");
+}
diff --git a/Touchkebao/src/components/MetailSelection/data.ts b/Touchkebao/src/components/MetailSelection/data.ts
new file mode 100644
index 00000000..3061247a
--- /dev/null
+++ b/Touchkebao/src/components/MetailSelection/data.ts
@@ -0,0 +1,27 @@
+export interface GroupSelectionItem {
+ id: string;
+ title: string;
+ cover?: string;
+ status: number;
+ [key: string]: any;
+}
+
+// 组件属性接口
+export interface GroupSelectionProps {
+ selectedOptions: GroupSelectionItem[];
+ onSelect: (groups: GroupSelectionItem[]) => void;
+ onSelectDetail?: (groups: GroupSelectionItem[]) => void;
+ placeholder?: string;
+ className?: string;
+ visible?: boolean;
+ onVisibleChange?: (visible: boolean) => void;
+ selectedListMaxHeight?: number;
+ showInput?: boolean;
+ showSelectedList?: boolean;
+ readonly?: boolean;
+ selectionMode?: "multiple" | "single"; // 新增:选择模式,默认为多选
+ onConfirm?: (
+ selectedIds: string[],
+ selectedItems: GroupSelectionItem[],
+ ) => void;
+}
diff --git a/Touchkebao/src/components/MetailSelection/index.module.scss b/Touchkebao/src/components/MetailSelection/index.module.scss
new file mode 100644
index 00000000..bedba3ef
--- /dev/null
+++ b/Touchkebao/src/components/MetailSelection/index.module.scss
@@ -0,0 +1,206 @@
+.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;
+}
+.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;
+}
+
+.popupContainer {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ 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;
+ transition: background 0.2s;
+ &:hover {
+ background: #f5f6fa;
+ }
+}
+.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 {
+ 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/Touchkebao/src/components/MetailSelection/index.tsx b/Touchkebao/src/components/MetailSelection/index.tsx
new file mode 100644
index 00000000..f7600027
--- /dev/null
+++ b/Touchkebao/src/components/MetailSelection/index.tsx
@@ -0,0 +1,138 @@
+import React, { useState } from "react";
+import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
+import { Button, Input } from "antd";
+import { Avatar } from "antd-mobile";
+import style from "./index.module.scss";
+import SelectionPopup from "./selectionPopup";
+import { GroupSelectionProps } from "./data";
+export default function GroupSelection({
+ selectedOptions,
+ onSelect,
+ onSelectDetail,
+ placeholder = "选择素材",
+ className = "",
+ visible,
+ onVisibleChange,
+ selectedListMaxHeight = 300,
+ showInput = true,
+ showSelectedList = true,
+ readonly = false,
+ selectionMode = "single", // 默认为多选模式
+ onConfirm,
+}: GroupSelectionProps) {
+ const [popupVisible, setPopupVisible] = useState(false);
+
+ // 删除已选素材
+ const handleRemoveGroup = (id: string) => {
+ if (readonly) return;
+ onSelect(selectedOptions.filter(g => g.id !== id));
+ };
+
+ // 受控弹窗逻辑
+ const realVisible = visible !== undefined ? visible : popupVisible;
+ const setRealVisible = (v: boolean) => {
+ if (onVisibleChange) onVisibleChange(v);
+ if (visible === undefined) setPopupVisible(v);
+ };
+
+ // 打开弹窗
+ const openPopup = () => {
+ if (readonly) return;
+ setRealVisible(true);
+ };
+
+ // 清空已选择的素材
+ const handleClear = () => {
+ if (readonly) return;
+ onSelect([]);
+ };
+
+ // 获取显示文本
+ const getDisplayText = () => {
+ if (selectedOptions.length === 0) return "";
+ if (selectionMode === "single") {
+ return selectedOptions[0]?.title || "已选择素材";
+ }
+ return `已选择 ${selectedOptions.length} 个素材`;
+ };
+
+ return (
+ <>
+ {/* 输入框 */}
+ {showInput && (
+
+ }
+ allowClear={!readonly && selectedOptions.length > 0}
+ onClear={handleClear}
+ size="large"
+ readOnly={readonly}
+ disabled={readonly}
+ style={
+ readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
+ }
+ />
+
+ )}
+ {/* 已选素材列表窗口 */}
+ {showSelectedList && selectedOptions.length > 0 && (
+
+ {selectedOptions.map(group => (
+
+
+
+
+
{group.title}
+
ID: {group.id}
+
+ {!readonly && (
+
}
+ size="small"
+ style={{
+ marginLeft: 4,
+ color: "#ff4d4f",
+ border: "none",
+ background: "none",
+ minWidth: 24,
+ height: 24,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ }}
+ onClick={() => handleRemoveGroup(group.id)}
+ />
+ )}
+
+
+ ))}
+
+ )}
+ {/* 弹窗 */}
+
+ >
+ );
+}
diff --git a/Touchkebao/src/components/MetailSelection/selectionPopup.tsx b/Touchkebao/src/components/MetailSelection/selectionPopup.tsx
new file mode 100644
index 00000000..e318faf0
--- /dev/null
+++ b/Touchkebao/src/components/MetailSelection/selectionPopup.tsx
@@ -0,0 +1,257 @@
+import React, { useState, useEffect } from "react";
+import { Popup, Checkbox, Radio } from "antd-mobile";
+
+import { getGroupList } from "./api";
+import style from "./index.module.scss";
+import Layout from "@/components/Layout/Layout";
+import PopupHeader from "@/components/PopuLayout/header";
+import PopupFooter from "@/components/PopuLayout/footer";
+import { GroupSelectionItem } from "./data";
+
+// 弹窗属性接口
+interface SelectionPopupProps {
+ visible: boolean;
+ onVisibleChange: (visible: boolean) => void;
+ selectedOptions: GroupSelectionItem[];
+ onSelect: (groups: GroupSelectionItem[]) => void;
+ onSelectDetail?: (groups: GroupSelectionItem[]) => void;
+ readonly?: boolean;
+ selectionMode?: "multiple" | "single"; // 新增:选择模式,默认为多选
+ onConfirm?: (
+ selectedIds: string[],
+ selectedItems: GroupSelectionItem[],
+ ) => void;
+}
+
+export default function SelectionPopup({
+ visible,
+ onVisibleChange,
+ selectedOptions,
+ onSelect,
+ onSelectDetail,
+ readonly = false,
+ selectionMode = "multiple", // 默认为多选模式
+ onConfirm,
+}: SelectionPopupProps) {
+ 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 [tempSelectedOptions, setTempSelectedOptions] = useState<
+ GroupSelectionItem[]
+ >([]);
+
+ // 获取素材列表API
+ const fetchGroups = async (page: number, keyword: string = "") => {
+ setLoading(true);
+ try {
+ const params: any = {
+ page,
+ limit: 20,
+ };
+
+ if (keyword.trim()) {
+ params.keyword = keyword.trim();
+ }
+
+ const response = await getGroupList(params);
+ if (response && response.list) {
+ setGroups(response.list);
+ setTotalGroups(response.total || 0);
+ setTotalPages(Math.ceil((response.total || 0) / 20));
+ }
+ } catch (error) {
+ console.error("获取素材列表失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 处理素材选择
+ const handleGroupToggle = (group: GroupSelectionItem) => {
+ if (readonly) return;
+
+ if (selectionMode === "single") {
+ // 单选模式:直接设置为当前选中的项
+ setTempSelectedOptions([group]);
+ } else {
+ // 多选模式:切换选中状态
+ const newSelectedGroups = tempSelectedOptions.some(g => g.id === group.id)
+ ? tempSelectedOptions.filter(g => g.id !== group.id)
+ : tempSelectedOptions.concat(group);
+
+ setTempSelectedOptions(newSelectedGroups);
+ }
+ };
+
+ // 全选当前页(仅在多选模式下有效)
+ const handleSelectAllCurrentPage = (checked: boolean) => {
+ if (readonly || selectionMode === "single") return;
+
+ if (checked) {
+ // 全选:添加当前页面所有未选中的素材
+ const currentPageGroups = groups.filter(
+ group => !tempSelectedOptions.some(g => g.id === group.id),
+ );
+ setTempSelectedOptions(prev => [...prev, ...currentPageGroups]);
+ } else {
+ // 取消全选:移除当前页面的所有素材
+ const currentPageGroupIds = groups.map(g => g.id);
+ setTempSelectedOptions(prev =>
+ prev.filter(g => !currentPageGroupIds.includes(g.id)),
+ );
+ }
+ };
+
+ // 检查当前页是否全选(仅在多选模式下有效)
+ const isCurrentPageAllSelected =
+ selectionMode === "multiple" &&
+ groups.length > 0 &&
+ groups.every(group => tempSelectedOptions.some(g => g.id === group.id));
+
+ // 确认选择
+ const handleConfirm = () => {
+ // 用户点击确认时,才更新实际的selectedOptions
+ onSelect(tempSelectedOptions);
+
+ // 如果有 onSelectDetail 回调,传递完整的素材对象
+ if (onSelectDetail) {
+ const selectedGroupObjs = groups.filter(group =>
+ tempSelectedOptions.some(g => g.id === group.id),
+ );
+ onSelectDetail(selectedGroupObjs);
+ }
+
+ if (onConfirm) {
+ onConfirm(
+ tempSelectedOptions.map(g => g.id),
+ tempSelectedOptions,
+ );
+ }
+ onVisibleChange(false);
+ };
+
+ // 弹窗打开时初始化数据(只执行一次)
+ useEffect(() => {
+ if (visible) {
+ setCurrentPage(1);
+ setSearchQuery("");
+ // 复制一份selectedOptions到临时变量
+ setTempSelectedOptions([...selectedOptions]);
+ fetchGroups(1, "");
+ }
+ }, [visible]);
+
+ // 搜索防抖(只在弹窗打开且搜索词变化时执行)
+ useEffect(() => {
+ if (!visible || searchQuery === "") return;
+
+ const timer = setTimeout(() => {
+ setCurrentPage(1);
+ fetchGroups(1, searchQuery);
+ }, 500);
+
+ return () => clearTimeout(timer);
+ }, [searchQuery, visible]);
+
+ // 页码变化时请求数据(只在弹窗打开且页码不是1时执行)
+ useEffect(() => {
+ if (!visible) return;
+ fetchGroups(currentPage, searchQuery);
+ }, [currentPage, visible, searchQuery]);
+
+ return (
+ onVisibleChange(false)}
+ position="bottom"
+ bodyStyle={{ height: "100vh" }}
+ >
+ fetchGroups(currentPage, searchQuery)}
+ />
+ }
+ footer={
+ onVisibleChange(false)}
+ onConfirm={handleConfirm}
+ isAllSelected={isCurrentPageAllSelected}
+ onSelectAll={handleSelectAllCurrentPage}
+ showSelectAll={selectionMode === "multiple"} // 只在多选模式下显示全选功能
+ />
+ }
+ >
+
+ {loading ? (
+
+ ) : groups.length > 0 ? (
+
+ {groups.map(group => (
+
+ {selectionMode === "single" ? (
+
g.id === group.id)}
+ onChange={() => !readonly && handleGroupToggle(group)}
+ disabled={readonly}
+ style={{ marginRight: 12 }}
+ />
+ ) : (
+ g.id === group.id)}
+ onChange={() => !readonly && handleGroupToggle(group)}
+ disabled={readonly}
+ style={{ marginRight: 12 }}
+ />
+ )}
+
+
+ {group.cover ? (
+

+ ) : (
+ group.title.charAt(0)
+ )}
+
+
+
{group.title}
+
+ 创建人: {group.userName}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+ {searchQuery
+ ? `没有找到包含"${searchQuery}"的素材`
+ : "没有找到素材"}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/Touchkebao/src/components/PopuLayout/footer.tsx b/Touchkebao/src/components/PopuLayout/footer.tsx
index 60e562be..a4dbe0cd 100644
--- a/Touchkebao/src/components/PopuLayout/footer.tsx
+++ b/Touchkebao/src/components/PopuLayout/footer.tsx
@@ -14,6 +14,7 @@ interface PopupFooterProps {
// 全选功能相关
isAllSelected?: boolean;
onSelectAll?: (checked: boolean) => void;
+ showSelectAll?: boolean; // 新增:控制全选功能显示,默认为true
}
const PopupFooter: React.FC = ({
@@ -26,19 +27,22 @@ const PopupFooter: React.FC = ({
onConfirm,
isAllSelected = false,
onSelectAll,
+ showSelectAll = true, // 默认为true,显示全选功能
}) => {
return (
<>
{/* 分页栏 */}
- onSelectAll(e.target.checked)}
- className={style.selectAllCheckbox}
- >
- 全选当前页
-
+ {showSelectAll && (
+ onSelectAll?.(e.target.checked)}
+ className={style.selectAllCheckbox}
+ >
+ 全选当前页
+
+ )}