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

内容库做好了
This commit is contained in:
笔记本里的永平
2025-07-23 14:51:14 +08:00
parent 7c33e8fa62
commit f73c0c279f
4 changed files with 497 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import request from "@/api/request";
export function getContentLibraryList(params: any) {
return request("/v1/content/library/list", params, "GET");
}

View File

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

View File

@@ -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<ContentLibraryItem[]>([]);
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 && (
<div className={`${style.inputWrapper} ${className}`}>
<Input
placeholder={placeholder}
value={getDisplayText()}
onClick={openPopup}
prefix={<SearchOutlined />}
allowClear={!readonly}
size="large"
readOnly={readonly}
disabled={readonly}
style={
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
}
/>
</div>
)}
{/* 已选内容库列表窗口 */}
{showSelectedList && selectedLibraryObjs.length > 0 && (
<div
className={style.selectedListWindow}
style={{
maxHeight: selectedListMaxHeight,
overflowY: "auto",
marginTop: 8,
border: "1px solid #e5e6eb",
borderRadius: 8,
background: "#fff",
}}
>
{selectedLibraryObjs.map((item) => (
<div
key={item.id}
className={style.selectedListRow}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
borderBottom: "1px solid #f0f0f0",
fontSize: 14,
}}
>
<div
style={{
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item.name || item.id}
</div>
{!readonly && (
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
style={{
marginLeft: 4,
color: "#ff4d4f",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveLibrary(item.id)}
/>
)}
</div>
))}
</div>
)}
{/* 弹窗 */}
<Popup
visible={realVisible && !readonly}
onMaskClick={() => setRealVisible(false)}
position="bottom"
bodyStyle={{ height: "100vh" }}
>
<Layout
header={
<PopupHeader
title="选择内容库"
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchPlaceholder="搜索内容库"
loading={loading}
onRefresh={() => fetchLibraries(currentPage, searchQuery)}
/>
}
footer={
<PopupFooter
total={totalLibraries}
currentPage={currentPage}
totalPages={totalPages}
loading={loading}
selectedCount={selectedLibraries.length}
onPageChange={setCurrentPage}
onCancel={() => setRealVisible(false)}
onConfirm={handleConfirm}
/>
}
>
<div className={style.libraryList}>
{loading ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : libraries.length > 0 ? (
<div className={style.libraryListInner}>
{libraries.map((item) => (
<label key={item.id} className={style.libraryItem}>
<Checkbox
checked={selectedLibraries.includes(item.id)}
onChange={() => !readonly && handleLibraryToggle(item.id)}
disabled={readonly}
className={style.checkboxWrapper}
/>
<div className={style.libraryInfo}>
<div className={style.libraryHeader}>
<span className={style.libraryName}>{item.name}</span>
<span className={style.typeTag}>
{getTypeText(item.sourceType)}
</span>
</div>
<div className={style.libraryMeta}>
<div>: {item.creatorName || "-"}</div>
<div>: {formatDate(item.updateTime)}</div>
</div>
{item.description && (
<div className={style.libraryDesc}>
{item.description}
</div>
)}
</div>
</label>
))}
</div>
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}>
{searchQuery
? `没有找到包含"${searchQuery}"的内容库`
: "没有找到内容库"}
</div>
</div>
)}
</div>
</Layout>
</Popup>
</>
);
}