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 && ( +
+
+ ))} +
+ )} + {/* 弹窗 */} + + + ); +} 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} + ) : ( + 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} + > + 全选当前页 + + )}