diff --git a/nkebao/src/components/ContentLibrarySelection/api.ts b/nkebao/src/components/ContentLibrarySelection/api.ts new file mode 100644 index 00000000..3b6dcd7b --- /dev/null +++ b/nkebao/src/components/ContentLibrarySelection/api.ts @@ -0,0 +1,5 @@ +import request from "@/api/request"; + +export function getContentLibraryList(params: any) { + return request("/v1/content/library/list", params, "GET"); +} diff --git a/nkebao/src/components/ContentLibrarySelection/index.module.scss b/nkebao/src/components/ContentLibrarySelection/index.module.scss new file mode 100644 index 00000000..8fb2fca5 --- /dev/null +++ b/nkebao/src/components/ContentLibrarySelection/index.module.scss @@ -0,0 +1,117 @@ +.inputWrapper { + position: relative; +} +.selectedListWindow { + margin-top: 8px; + border: 1px solid #e5e6eb; + border-radius: 8px; + background: #fff; +} +.selectedListRow { + display: flex; + align-items: center; + padding: 4px 8px; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; +} +.libraryList { + flex: 1; + overflow-y: auto; +} +.libraryListInner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} +.libraryItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + background: #fff; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.checkboxWrapper { + margin-top: 4px; +} +.checkboxSelected { + width: 20px; + height: 20px; + border-radius: 4px; + background: #1677ff; + display: flex; + align-items: center; + justify-content: center; +} +.checkboxUnselected { + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid #e5e6eb; + background: #fff; +} +.checkboxDot { + width: 12px; + height: 12px; + border-radius: 2px; + background: #fff; +} +.libraryInfo { + flex: 1; +} +.libraryHeader { + display: flex; + align-items: center; + justify-content: space-between; +} +.libraryName { + font-weight: 500; + font-size: 16px; + color: #222; +} +.typeTag { + font-size: 12px; + color: #1677ff; + border: 1px solid #1677ff; + border-radius: 12px; + padding: 2px 10px; + margin-left: 8px; + background: #f4f8ff; + font-weight: 500; +} +.libraryMeta { + font-size: 12px; + color: #888; +} +.libraryDesc { + font-size: 13px; + color: #888; + margin-top: 4px; +} +.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: 100px; +} +.emptyText { + color: #888; + font-size: 15px; +} \ No newline at end of file diff --git a/nkebao/src/components/ContentLibrarySelection/index.tsx b/nkebao/src/components/ContentLibrarySelection/index.tsx new file mode 100644 index 00000000..cd301467 --- /dev/null +++ b/nkebao/src/components/ContentLibrarySelection/index.tsx @@ -0,0 +1,343 @@ +import React, { useState, useEffect } from "react"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Button, Input } from "antd"; +import { Popup, Checkbox } 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 { getContentLibraryList } from "./api"; + +// 内容库接口类型 +interface ContentLibraryItem { + id: string; + name: string; + description?: string; + sourceType?: number; // 1=文本 2=图片 3=视频 + creatorName?: string; + updateTime?: string; + [key: string]: any; +} + +// 类型标签文本 +const getTypeText = (type?: number) => { + if (type === 1) return "文本"; + if (type === 2) return "图片"; + if (type === 3) return "视频"; + return "未知"; +}; + +// 时间格式化 +const formatDate = (dateStr?: string) => { + if (!dateStr) return "-"; + const d = new Date(dateStr); + if (isNaN(d.getTime())) return "-"; + return `${d.getFullYear()}/${(d.getMonth() + 1) + .toString() + .padStart(2, "0")}/${d.getDate().toString().padStart(2, "0")} ${d + .getHours() + .toString() + .padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d + .getSeconds() + .toString() + .padStart(2, "0")}`; +}; + +// 组件属性接口 +interface ContentLibrarySelectionProps { + selectedLibraries: string[]; + onSelect: (libraries: string[]) => void; + onSelectDetail?: (libraries: ContentLibraryItem[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: ContentLibraryItem[] + ) => void; +} + +export default function ContentLibrarySelection({ + selectedLibraries, + onSelect, + onSelectDetail, + placeholder = "选择内容库", + className = "", + visible, + onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, + onConfirm, +}: ContentLibrarySelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [libraries, setLibraries] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalLibraries, setTotalLibraries] = useState(0); + const [loading, setLoading] = useState(false); + + // 获取已选内容库详细信息 + const selectedLibraryObjs = libraries.filter((item) => + selectedLibraries.includes(item.id) + ); + + // 删除已选内容库 + const handleRemoveLibrary = (id: string) => { + if (readonly) return; + onSelect(selectedLibraries.filter((g) => g !== 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; + setCurrentPage(1); + setSearchQuery(""); + setRealVisible(true); + fetchLibraries(1, ""); + }; + + // 当页码变化时,拉取对应页数据(弹窗已打开时) + useEffect(() => { + if (realVisible && currentPage !== 1) { + fetchLibraries(currentPage, searchQuery); + } + }, [currentPage, realVisible, searchQuery]); + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchLibraries(1, searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, realVisible]); + + // 获取内容库列表API + const fetchLibraries = async (page: number, keyword: string = "") => { + setLoading(true); + try { + let params: any = { + page, + limit: 20, + }; + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + const response = await getContentLibraryList(params); + if (response && response.list) { + setLibraries(response.list); + setTotalLibraries(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); + } + } catch (error) { + console.error("获取内容库列表失败:", error); + } finally { + setLoading(false); + } + }; + + // 处理内容库选择 + const handleLibraryToggle = (libraryId: string) => { + if (readonly) return; + const newSelected = selectedLibraries.includes(libraryId) + ? selectedLibraries.filter((id) => id !== libraryId) + : [...selectedLibraries, libraryId]; + onSelect(newSelected); + if (onSelectDetail) { + const selectedObjs = libraries.filter((item) => + newSelected.includes(item.id) + ); + onSelectDetail(selectedObjs); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedLibraries.length === 0) return ""; + return `已选择 ${selectedLibraries.length} 个内容库`; + }; + + // 确认选择 + const handleConfirm = () => { + if (onConfirm) { + onConfirm(selectedLibraries, selectedLibraryObjs); + } + setRealVisible(false); + }; + + return ( + <> + {/* 输入框 */} + {showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选内容库列表窗口 */} + {showSelectedList && selectedLibraryObjs.length > 0 && ( +
+ {selectedLibraryObjs.map((item) => ( +
+
+ {item.name || item.id} +
+ {!readonly && ( +
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchLibraries(currentPage, searchQuery)} + /> + } + footer={ + setRealVisible(false)} + onConfirm={handleConfirm} + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : libraries.length > 0 ? ( +
+ {libraries.map((item) => ( + + ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的内容库` + : "没有找到内容库"} +
+
+ )} +
+
+
+ + ); +} diff --git a/nkebao/src/pages/component-test/index.tsx b/nkebao/src/pages/component-test/index.tsx index 8162c835..fa7f273e 100644 --- a/nkebao/src/pages/component-test/index.tsx +++ b/nkebao/src/pages/component-test/index.tsx @@ -7,6 +7,7 @@ import NavCommon from "@/components/NavCommon"; import DeviceSelection from "@/components/DeviceSelection"; import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; +import ContentLibrarySelection from "@/components/ContentLibrarySelection"; const ComponentTest: React.FC = () => { const navigate = useNavigate(); @@ -21,6 +22,9 @@ const ComponentTest: React.FC = () => { // 群组选择状态 const [selectedGroups, setSelectedGroups] = useState([]); + // 内容库选择状态 + const [selectedLibraries, setSelectedLibraries] = useState([]); + return ( }>
@@ -99,6 +103,34 @@ const ComponentTest: React.FC = () => {
+ + +
+

+ ContentLibrarySelection 组件测试 +

+ +
+ 已选内容库: {selectedLibraries.length} 个 +
+ 内容库ID:{" "} + {selectedLibraries.join(", ") || "无"} +
+
+