更新关键词管理模块:新增素材选择功能,优化回复类型切换逻辑,提升用户体验和代码可读性。
This commit is contained in:
10
Touchkebao/src/components/MetailSelection/api.ts
Normal file
10
Touchkebao/src/components/MetailSelection/api.ts
Normal file
@@ -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");
|
||||||
|
}
|
||||||
27
Touchkebao/src/components/MetailSelection/data.ts
Normal file
27
Touchkebao/src/components/MetailSelection/data.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
206
Touchkebao/src/components/MetailSelection/index.module.scss
Normal file
206
Touchkebao/src/components/MetailSelection/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
138
Touchkebao/src/components/MetailSelection/index.tsx
Normal file
138
Touchkebao/src/components/MetailSelection/index.tsx
Normal file
@@ -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 && (
|
||||||
|
<div className={`${style.inputWrapper} ${className}`}>
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={getDisplayText()}
|
||||||
|
onClick={openPopup}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear={!readonly && selectedOptions.length > 0}
|
||||||
|
onClear={handleClear}
|
||||||
|
size="large"
|
||||||
|
readOnly={readonly}
|
||||||
|
disabled={readonly}
|
||||||
|
style={
|
||||||
|
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 已选素材列表窗口 */}
|
||||||
|
{showSelectedList && selectedOptions.length > 0 && (
|
||||||
|
<div
|
||||||
|
className={style.selectedListWindow}
|
||||||
|
style={{
|
||||||
|
maxHeight: selectedListMaxHeight,
|
||||||
|
overflowY: "auto",
|
||||||
|
marginTop: 8,
|
||||||
|
border: "1px solid #e5e6eb",
|
||||||
|
borderRadius: 8,
|
||||||
|
background: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedOptions.map(group => (
|
||||||
|
<div key={group.id} className={style.selectedListRow}>
|
||||||
|
<div className={style.selectedListRowContent}>
|
||||||
|
<Avatar src={group.cover} />
|
||||||
|
<div className={style.selectedListRowContentText}>
|
||||||
|
<div>{group.title}</div>
|
||||||
|
<div>ID: {group.id}</div>
|
||||||
|
</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={() => handleRemoveGroup(group.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 弹窗 */}
|
||||||
|
<SelectionPopup
|
||||||
|
visible={realVisible}
|
||||||
|
onVisibleChange={setRealVisible}
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onSelectDetail={onSelectDetail}
|
||||||
|
readonly={readonly}
|
||||||
|
selectionMode={selectionMode}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
257
Touchkebao/src/components/MetailSelection/selectionPopup.tsx
Normal file
257
Touchkebao/src/components/MetailSelection/selectionPopup.tsx
Normal file
@@ -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<GroupSelectionItem[]>([]);
|
||||||
|
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 (
|
||||||
|
<Popup
|
||||||
|
visible={visible && !readonly}
|
||||||
|
onMaskClick={() => onVisibleChange(false)}
|
||||||
|
position="bottom"
|
||||||
|
bodyStyle={{ height: "100vh" }}
|
||||||
|
>
|
||||||
|
<Layout
|
||||||
|
header={
|
||||||
|
<PopupHeader
|
||||||
|
title="选择素材"
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
searchPlaceholder="搜索素材"
|
||||||
|
loading={loading}
|
||||||
|
onRefresh={() => fetchGroups(currentPage, searchQuery)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<PopupFooter
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
loading={loading}
|
||||||
|
selectedCount={tempSelectedOptions.length}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
onCancel={() => onVisibleChange(false)}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
isAllSelected={isCurrentPageAllSelected}
|
||||||
|
onSelectAll={handleSelectAllCurrentPage}
|
||||||
|
showSelectAll={selectionMode === "multiple"} // 只在多选模式下显示全选功能
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={style.groupList}>
|
||||||
|
{loading ? (
|
||||||
|
<div className={style.loadingBox}>
|
||||||
|
<div className={style.loadingText}>加载中...</div>
|
||||||
|
</div>
|
||||||
|
) : groups.length > 0 ? (
|
||||||
|
<div className={style.groupListInner}>
|
||||||
|
{groups.map(group => (
|
||||||
|
<div key={group.id} className={style.groupItem}>
|
||||||
|
{selectionMode === "single" ? (
|
||||||
|
<Radio
|
||||||
|
checked={tempSelectedOptions.some(g => g.id === group.id)}
|
||||||
|
onChange={() => !readonly && handleGroupToggle(group)}
|
||||||
|
disabled={readonly}
|
||||||
|
style={{ marginRight: 12 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Checkbox
|
||||||
|
checked={tempSelectedOptions.some(g => g.id === group.id)}
|
||||||
|
onChange={() => !readonly && handleGroupToggle(group)}
|
||||||
|
disabled={readonly}
|
||||||
|
style={{ marginRight: 12 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={style.groupInfo}>
|
||||||
|
<div className={style.groupAvatar}>
|
||||||
|
{group.cover ? (
|
||||||
|
<img
|
||||||
|
src={group.cover}
|
||||||
|
alt={group.title}
|
||||||
|
className={style.avatarImg}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
group.title.charAt(0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={style.groupDetail}>
|
||||||
|
<div className={style.groupName}>{group.title}</div>
|
||||||
|
<div className={style.groupOwner}>
|
||||||
|
创建人: {group.userName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={style.emptyBox}>
|
||||||
|
<div className={style.emptyText}>
|
||||||
|
{searchQuery
|
||||||
|
? `没有找到包含"${searchQuery}"的素材`
|
||||||
|
: "没有找到素材"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ interface PopupFooterProps {
|
|||||||
// 全选功能相关
|
// 全选功能相关
|
||||||
isAllSelected?: boolean;
|
isAllSelected?: boolean;
|
||||||
onSelectAll?: (checked: boolean) => void;
|
onSelectAll?: (checked: boolean) => void;
|
||||||
|
showSelectAll?: boolean; // 新增:控制全选功能显示,默认为true
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupFooter: React.FC<PopupFooterProps> = ({
|
const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||||
@@ -26,19 +27,22 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
isAllSelected = false,
|
isAllSelected = false,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
|
showSelectAll = true, // 默认为true,显示全选功能
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 分页栏 */}
|
{/* 分页栏 */}
|
||||||
<div className={style.paginationRow}>
|
<div className={style.paginationRow}>
|
||||||
<div className={style.totalCount}>
|
<div className={style.totalCount}>
|
||||||
<Checkbox
|
{showSelectAll && (
|
||||||
checked={isAllSelected}
|
<Checkbox
|
||||||
onChange={e => onSelectAll(e.target.checked)}
|
checked={isAllSelected}
|
||||||
className={style.selectAllCheckbox}
|
onChange={e => onSelectAll?.(e.target.checked)}
|
||||||
>
|
className={style.selectAllCheckbox}
|
||||||
全选当前页
|
>
|
||||||
</Checkbox>
|
全选当前页
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={style.paginationControls}>
|
<div className={style.paginationControls}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ export interface KeywordAddRequest {
|
|||||||
level: number; // 优先级
|
level: number; // 优先级
|
||||||
replyType: number; // 回复类型:文本回复、模板回复
|
replyType: number; // 回复类型:文本回复、模板回复
|
||||||
status: string;
|
status: string;
|
||||||
|
materialId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeywordUpdateRequest extends KeywordAddRequest {
|
export interface KeywordUpdateRequest extends KeywordAddRequest {
|
||||||
id?: string;
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeywordSetStatusRequest {
|
export interface KeywordSetStatusRequest {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type KeywordAddRequest,
|
type KeywordAddRequest,
|
||||||
type KeywordUpdateRequest,
|
type KeywordUpdateRequest,
|
||||||
} from "../../api";
|
} from "../../api";
|
||||||
|
import MetailSelection from "@/components/MetailSelection";
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
level: keyword.level,
|
level: keyword.level,
|
||||||
replyType: keyword.replyType,
|
replyType: keyword.replyType,
|
||||||
status: keyword.status,
|
status: keyword.status,
|
||||||
|
materialId: keyword.materialId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -63,6 +64,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
} else if (mode === "add") {
|
} else if (mode === "add") {
|
||||||
// 添加模式:重置表单
|
// 添加模式:重置表单
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedOptions([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [visible, mode, keywordId, fetchKeywordDetails, form]);
|
}, [visible, mode, keywordId, fetchKeywordDetails, form]);
|
||||||
@@ -80,6 +82,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
level: values.level,
|
level: values.level,
|
||||||
replyType: values.replyType,
|
replyType: values.replyType,
|
||||||
status: values.status || "1",
|
status: values.status || "1",
|
||||||
|
materialId: values.materialId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await addKeyword(data);
|
const response = await addKeyword(data);
|
||||||
@@ -101,6 +104,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
level: values.level,
|
level: values.level,
|
||||||
replyType: values.replyType,
|
replyType: values.replyType,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
|
materialId: values.materialId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await updateKeyword(data);
|
const response = await updateKeyword(data);
|
||||||
@@ -123,11 +127,44 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedOptions([]);
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
|
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
||||||
|
const handSelectMaterial = (options: any[]) => {
|
||||||
|
if (options.length === 0) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
materialId: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 在单选模式下,只取第一个选项的ID
|
||||||
|
form.setFieldsValue({
|
||||||
|
materialId: options[0].id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedOptions(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听表单值变化
|
||||||
|
const handleFormValuesChange = (changedValues: any) => {
|
||||||
|
// 当回复类型切换时,清空素材选择
|
||||||
|
if (changedValues.replyType !== undefined) {
|
||||||
|
setSelectedOptions([]);
|
||||||
|
if (changedValues.replyType === 1) {
|
||||||
|
// 切换到自定义回复时,清空materialId
|
||||||
|
form.setFieldsValue({
|
||||||
|
materialId: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 切换到素材回复时,清空content
|
||||||
|
form.setFieldsValue({
|
||||||
|
content: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={title}
|
title={title}
|
||||||
@@ -140,6 +177,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleSubmit}
|
onFinish={handleSubmit}
|
||||||
|
onValuesChange={handleFormValuesChange}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
status: 1,
|
status: 1,
|
||||||
type: "模糊匹配",
|
type: "模糊匹配",
|
||||||
@@ -164,13 +202,39 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="content"
|
name="replyType"
|
||||||
label="回复内容"
|
label="回复类型"
|
||||||
rules={[{ required: true, message: "请输入回复内容" }]}
|
rules={[{ required: true, message: "请选择回复类型" }]}
|
||||||
>
|
>
|
||||||
<TextArea rows={4} placeholder="请输入回复内容" />
|
<Select placeholder="请选择回复类型">
|
||||||
|
<Option value={0}>素材回复</Option>
|
||||||
|
<Option value={1}>自定义</Option>
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{form.getFieldValue("replyType") === 1 ? (
|
||||||
|
<Form.Item
|
||||||
|
name="content"
|
||||||
|
label="回复内容"
|
||||||
|
rules={[{ required: true, message: "请输入回复内容" }]}
|
||||||
|
>
|
||||||
|
<TextArea rows={4} placeholder="请输入回复内容" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name="materialId"
|
||||||
|
label="回复内容"
|
||||||
|
rules={[{ required: true, message: "请输入回复内容" }]}
|
||||||
|
>
|
||||||
|
<MetailSelection
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
onSelect={handSelectMaterial}
|
||||||
|
selectionMode="single"
|
||||||
|
placeholder="选择素材"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="type"
|
name="type"
|
||||||
label="匹配类型"
|
label="匹配类型"
|
||||||
@@ -194,17 +258,6 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="replyType"
|
|
||||||
label="回复类型"
|
|
||||||
rules={[{ required: true, message: "请选择回复类型" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择回复类型">
|
|
||||||
<Option value={0}>素材回复</Option>
|
|
||||||
<Option value={1}>自定义</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="status"
|
name="status"
|
||||||
label="状态"
|
label="状态"
|
||||||
|
|||||||
Reference in New Issue
Block a user