refactor(ContentSelection): 将弹窗组件抽离为独立文件并优化逻辑
将内容选择弹窗组件抽离为独立的selectionPopup.tsx文件,优化代码结构 添加加载状态处理,改进搜索和分页逻辑,增强组件复用性
This commit is contained in:
2
Cunkebao/dist/.vite/manifest.json
vendored
2
Cunkebao/dist/.vite/manifest.json
vendored
@@ -33,7 +33,7 @@
|
||||
"name": "vendor"
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-C4WYi_u0.js",
|
||||
"file": "assets/index-BQZfedpY.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
|
||||
2
Cunkebao/dist/index.html
vendored
2
Cunkebao/dist/index.html
vendored
@@ -11,7 +11,7 @@
|
||||
</style>
|
||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
||||
<script type="text/javascript" src="/websdk.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C4WYi_u0.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BQZfedpY.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-DUe_gloh.js">
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState } 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";
|
||||
import { ContentItem, ContentSelectionProps } from "./data";
|
||||
import SelectionPopup from "./selectionPopup";
|
||||
|
||||
// 类型标签文本
|
||||
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")}`;
|
||||
};
|
||||
|
||||
export default function ContentSelection({
|
||||
const ContentSelection: React.FC<ContentSelectionProps> = ({
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
placeholder = "选择内容库",
|
||||
@@ -45,25 +17,9 @@ export default function ContentSelection({
|
||||
showSelectedList = true,
|
||||
readonly = false,
|
||||
onConfirm,
|
||||
}: ContentSelectionProps) {
|
||||
}) => {
|
||||
// 弹窗控制
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [libraries, setLibraries] = useState<ContentItem[]>([]);
|
||||
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 [tempSelectedOptions, setTempSelectedOptions] = useState<ContentItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
// 删除已选内容库
|
||||
const handleRemoveLibrary = (id: number) => {
|
||||
if (readonly) return;
|
||||
onSelect(selectedOptions.filter(c => c.id !== id));
|
||||
};
|
||||
|
||||
// 受控弹窗逻辑
|
||||
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||
const setRealVisible = (v: boolean) => {
|
||||
if (onVisibleChange) onVisibleChange(v);
|
||||
@@ -73,62 +29,7 @@ export default function ContentSelection({
|
||||
// 打开弹窗
|
||||
const openPopup = () => {
|
||||
if (readonly) return;
|
||||
setCurrentPage(1);
|
||||
setSearchQuery("");
|
||||
// 复制一份selectedOptions到临时变量
|
||||
setTempSelectedOptions([...selectedOptions]);
|
||||
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 {
|
||||
const 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 = (library: ContentItem) => {
|
||||
if (readonly) return;
|
||||
const newSelected = tempSelectedOptions.some(c => c.id === library.id)
|
||||
? tempSelectedOptions.filter(c => c.id !== library.id)
|
||||
: [...tempSelectedOptions, library];
|
||||
setTempSelectedOptions(newSelected);
|
||||
};
|
||||
|
||||
// 获取显示文本
|
||||
@@ -137,14 +38,16 @@ export default function ContentSelection({
|
||||
return `已选择 ${selectedOptions.length} 个内容库`;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
// 用户点击确认时,才更新实际的selectedOptions
|
||||
onSelect(tempSelectedOptions);
|
||||
if (onConfirm) {
|
||||
onConfirm(tempSelectedOptions);
|
||||
}
|
||||
setRealVisible(false);
|
||||
// 删除已选内容库
|
||||
const handleRemoveLibrary = (id: number) => {
|
||||
if (readonly) return;
|
||||
onSelect(selectedOptions.filter(c => c.id !== id));
|
||||
};
|
||||
|
||||
// 清除所有已选内容库
|
||||
const handleClearAll = () => {
|
||||
if (readonly) return;
|
||||
onSelect([]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -158,6 +61,7 @@ export default function ContentSelection({
|
||||
onClick={openPopup}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear={!readonly}
|
||||
onClear={handleClearAll}
|
||||
size="large"
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
@@ -227,85 +131,15 @@ export default function ContentSelection({
|
||||
</div>
|
||||
)}
|
||||
{/* 弹窗 */}
|
||||
<Popup
|
||||
<SelectionPopup
|
||||
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={tempSelectedOptions.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={tempSelectedOptions
|
||||
.map(c => c.id)
|
||||
.includes(item.id)}
|
||||
onChange={() => !readonly && handleLibraryToggle(item)}
|
||||
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>
|
||||
onClose={() => setRealVisible(false)}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onSelect}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ContentSelection;
|
||||
|
||||
233
Cunkebao/src/components/ContentSelection/selectionPopup.tsx
Normal file
233
Cunkebao/src/components/ContentSelection/selectionPopup.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Checkbox, Popup } from "antd-mobile";
|
||||
import { getContentLibraryList } 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 { ContentItem } from "./data";
|
||||
|
||||
interface SelectionPopupProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
selectedOptions: ContentItem[];
|
||||
onSelect: (libraries: ContentItem[]) => void;
|
||||
onConfirm?: (libraries: ContentItem[]) => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
// 类型标签文本
|
||||
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")}`;
|
||||
};
|
||||
|
||||
const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
onConfirm,
|
||||
}) => {
|
||||
// 内容库数据
|
||||
const [libraries, setLibraries] = useState<ContentItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(true); // 默认设置为加载中状态
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalLibraries, setTotalLibraries] = useState(0);
|
||||
const [tempSelectedOptions, setTempSelectedOptions] = useState<ContentItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
// 获取内容库列表,支持keyword和分页
|
||||
const fetchLibraries = useCallback(
|
||||
async (page: number, keyword: string = "") => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
};
|
||||
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) / PAGE_SIZE));
|
||||
} else {
|
||||
// 如果没有返回列表数据,设置为空数组
|
||||
setLibraries([]);
|
||||
setTotalLibraries(0);
|
||||
setTotalPages(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取内容库列表失败:", error);
|
||||
// 请求失败时,设置为空数组
|
||||
setLibraries([]);
|
||||
setTotalLibraries(0);
|
||||
setTotalPages(1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 打开弹窗时获取第一页
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setSearchQuery("");
|
||||
setCurrentPage(1);
|
||||
// 复制一份selectedOptions到临时变量
|
||||
setTempSelectedOptions([...selectedOptions]);
|
||||
// 设置loading状态,避免显示空内容
|
||||
setLoading(true);
|
||||
fetchLibraries(1, "");
|
||||
} else {
|
||||
// 关闭弹窗时重置加载状态,确保下次打开时显示加载中
|
||||
setLoading(true);
|
||||
}
|
||||
}, [visible, fetchLibraries, selectedOptions]);
|
||||
|
||||
// 搜索防抖
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentPage(1);
|
||||
fetchLibraries(1, searchQuery);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery, visible, fetchLibraries]);
|
||||
|
||||
// 翻页时重新请求
|
||||
useEffect(() => {
|
||||
if (!visible || currentPage === 1) return;
|
||||
fetchLibraries(currentPage, searchQuery);
|
||||
}, [currentPage, visible, fetchLibraries, searchQuery]);
|
||||
|
||||
// 处理内容库选择
|
||||
const handleLibraryToggle = (library: ContentItem) => {
|
||||
const newSelected = tempSelectedOptions.some(c => c.id === library.id)
|
||||
? tempSelectedOptions.filter(c => c.id !== library.id)
|
||||
: [...tempSelectedOptions, library];
|
||||
setTempSelectedOptions(newSelected);
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
// 用户点击确认时,才更新实际的selectedOptions
|
||||
onSelect(tempSelectedOptions);
|
||||
if (onConfirm) {
|
||||
onConfirm(tempSelectedOptions);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
// 渲染内容库列表或空状态提示
|
||||
const OptionsList = () => {
|
||||
return libraries.length > 0 ? (
|
||||
<div className={style.libraryListInner}>
|
||||
{libraries.map(item => (
|
||||
<label key={item.id} className={style.libraryItem}>
|
||||
<Checkbox
|
||||
checked={tempSelectedOptions.map(c => c.id).includes(item.id)}
|
||||
onChange={() => handleLibraryToggle(item)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
onMaskClick={onClose}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "100vh" }}
|
||||
closeOnMaskClick={false}
|
||||
>
|
||||
<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={tempSelectedOptions.length}
|
||||
onPageChange={setCurrentPage}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.libraryList}>
|
||||
{loading ? (
|
||||
<div className={style.loadingBox}>
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : (
|
||||
OptionsList()
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectionPopup;
|
||||
Reference in New Issue
Block a user