feat: 本次提交更新内容如下
内容库做好了
This commit is contained in:
5
nkebao/src/components/ContentLibrarySelection/api.ts
Normal file
5
nkebao/src/components/ContentLibrarySelection/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
export function getContentLibraryList(params: any) {
|
||||
return request("/v1/content/library/list", params, "GET");
|
||||
}
|
||||
117
nkebao/src/components/ContentLibrarySelection/index.module.scss
Normal file
117
nkebao/src/components/ContentLibrarySelection/index.module.scss
Normal 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;
|
||||
}
|
||||
343
nkebao/src/components/ContentLibrarySelection/index.tsx
Normal file
343
nkebao/src/components/ContentLibrarySelection/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user