diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx index b6d8d467..aa15519d 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx @@ -31,7 +31,7 @@ const defaultForm = { syncCount: 5, syncInterval: 30, syncType: 1, // 1=业务号 2=人设号 - accountType: "business" as "business" | "personal", // 仅UI用 + accountType: 1, // 仅UI用 enabled: true, deviceGroups: [] as any[], contentGroups: [] as any[], // 存完整内容库对象数组 @@ -67,8 +67,8 @@ const NewMomentsSync: React.FC = () => { endTime: res.timeRange?.end || "23:59", syncCount: res.config?.syncCount || res.syncCount || 5, syncInterval: res.config?.syncInterval || res.syncInterval || 30, - syncType: res.accountType === 1 ? 1 : 2, - accountType: res.accountType === 1 ? "business" : "personal", + syncType: res.config?.syncType, + accountType: res.config?.accountType, enabled: res.status === 1, deviceGroups: res.config?.deviceGroups || [], // 关键:用id字符串数组回填 @@ -101,11 +101,11 @@ const NewMomentsSync: React.FC = () => { }; // UI选择账号类型时同步syncType和accountType - const handleAccountTypeChange = (type: "business" | "personal") => { + const handleAccountTypeChange = (type: number) => { setFormData(prev => ({ ...prev, accountType: type, - syncType: type === "business" ? 1 : 2, + syncType: type, })); }; const handleDevicesChange = (devices: DeviceSelectionItem[]) => { @@ -135,11 +135,11 @@ const NewMomentsSync: React.FC = () => { const params = { name: formData.taskName, deviceGroups: formData.deviceGroups, - contentGroups: formData.contentGroups.map((lib: any) => lib.id), + contentGroups: contentGroupsOptions.map((lib: any) => lib.id), syncInterval: formData.syncInterval, syncCount: formData.syncCount, syncType: formData.syncType, // 账号类型真实传参 - accountType: formData.accountType === "business" ? 1 : 2, // 也要传 + accountType: formData.accountType, // 也要传 startTime: formData.startTime, endTime: formData.endTime, contentTypes: formData.contentTypes, @@ -227,14 +227,14 @@ const NewMomentsSync: React.FC = () => {
账号类型
diff --git a/Touchkebao/src/components/MetailSelection/api.ts b/Touchkebao/src/components/MetailSelection/api.ts new file mode 100644 index 00000000..2df6bc32 --- /dev/null +++ b/Touchkebao/src/components/MetailSelection/api.ts @@ -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"); +} diff --git a/Touchkebao/src/components/MetailSelection/data.ts b/Touchkebao/src/components/MetailSelection/data.ts new file mode 100644 index 00000000..3061247a --- /dev/null +++ b/Touchkebao/src/components/MetailSelection/data.ts @@ -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; +} diff --git a/Touchkebao/src/components/MetailSelection/index.module.scss b/Touchkebao/src/components/MetailSelection/index.module.scss new file mode 100644 index 00000000..bedba3ef --- /dev/null +++ b/Touchkebao/src/components/MetailSelection/index.module.scss @@ -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; +} diff --git a/Touchkebao/src/components/MetailSelection/index.tsx b/Touchkebao/src/components/MetailSelection/index.tsx new file mode 100644 index 00000000..f7600027 --- /dev/null +++ b/Touchkebao/src/components/MetailSelection/index.tsx @@ -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 && ( +
+ } + allowClear={!readonly && selectedOptions.length > 0} + onClear={handleClear} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选素材列表窗口 */} + {showSelectedList && selectedOptions.length > 0 && ( +
+ {selectedOptions.map(group => ( +
+
+ +
+
{group.title}
+
ID: {group.id}
+
+ {!readonly && ( +
+
+ ))} +
+ )} + {/* 弹窗 */} + + + ); +} diff --git a/Touchkebao/src/components/MetailSelection/selectionPopup.tsx b/Touchkebao/src/components/MetailSelection/selectionPopup.tsx new file mode 100644 index 00000000..e318faf0 --- /dev/null +++ b/Touchkebao/src/components/MetailSelection/selectionPopup.tsx @@ -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([]); + 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 ( + onVisibleChange(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchGroups(currentPage, searchQuery)} + /> + } + footer={ + onVisibleChange(false)} + onConfirm={handleConfirm} + isAllSelected={isCurrentPageAllSelected} + onSelectAll={handleSelectAllCurrentPage} + showSelectAll={selectionMode === "multiple"} // 只在多选模式下显示全选功能 + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : groups.length > 0 ? ( +
+ {groups.map(group => ( +
+ {selectionMode === "single" ? ( + g.id === group.id)} + onChange={() => !readonly && handleGroupToggle(group)} + disabled={readonly} + style={{ marginRight: 12 }} + /> + ) : ( + g.id === group.id)} + onChange={() => !readonly && handleGroupToggle(group)} + disabled={readonly} + style={{ marginRight: 12 }} + /> + )} +
+
+ {group.cover ? ( + {group.title} + ) : ( + group.title.charAt(0) + )} +
+
+
{group.title}
+
+ 创建人: {group.userName} +
+
+
+
+ ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的素材` + : "没有找到素材"} +
+
+ )} +
+
+
+ ); +} diff --git a/Touchkebao/src/components/PopuLayout/footer.tsx b/Touchkebao/src/components/PopuLayout/footer.tsx index 60e562be..a4dbe0cd 100644 --- a/Touchkebao/src/components/PopuLayout/footer.tsx +++ b/Touchkebao/src/components/PopuLayout/footer.tsx @@ -14,6 +14,7 @@ interface PopupFooterProps { // 全选功能相关 isAllSelected?: boolean; onSelectAll?: (checked: boolean) => void; + showSelectAll?: boolean; // 新增:控制全选功能显示,默认为true } const PopupFooter: React.FC = ({ @@ -26,19 +27,22 @@ const PopupFooter: React.FC = ({ onConfirm, isAllSelected = false, onSelectAll, + showSelectAll = true, // 默认为true,显示全选功能 }) => { return ( <> {/* 分页栏 */}
- onSelectAll(e.target.checked)} - className={style.selectAllCheckbox} - > - 全选当前页 - + {showSelectAll && ( + onSelectAll?.(e.target.checked)} + className={style.selectAllCheckbox} + > + 全选当前页 + + )}
@@ -120,14 +240,7 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => {
- + = ({ title = "触客宝" }) => { className={styles.messageDrawer} extra={ - } >
-
-
- - 林 - + {loading ? ( +
+ 加载中...
-
-
- 新消息 -
+ ) : messageList.length === 0 ? ( + + ) : ( + messageList.map(item => ( +
handleReadMessage(item.id)} + > +
+ + {item.friendData?.nickname?.charAt(0) || "U"} + +
+
+
+ {item.title} + {item.isRead === 0 && ( +
+ )} +
+
{item.message}
+ {item.isRead === 0 && ( +
+ {formatTime(item.createTime)} + +
+ )} +
-
- 林秀杰:关于自由主时间特惠,原价1999元,现在只需1699元 -
-
- 03-05 - -
-
-
- -
-
- - E - -
-
-
- 群提醒 -
-
-
- Eric在「云归营私域银行项目群」中@了您 -
-
03-05
-
-
- -
-
- - 李 - -
-
-
- 好友申请 -
-
李翔想请求添加您为好友
-
03-04
-
- - -
-
-
+ )) + )}
diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/api.ts index 62d156ce..8b90daa4 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/api.ts @@ -118,7 +118,7 @@ export function updateSensitiveWord(data: SensitiveWordUpdateRequest) { // 违禁词管理-修改状态 export function setSensitiveWordStatus(data: SensitiveWordSetStatusRequest) { - return request("/v1/kefu/content/sensitiveWord/setStatus", data, "POST"); + return request("/v1/kefu/content/sensitiveWord/setStatus", data, "GET"); } // 关键词回复管理相关接口 @@ -132,18 +132,19 @@ export interface KeywordAddRequest { title: string; keywords: string; content: string; - matchType: string; // 匹配类型:模糊匹配、精确匹配 - priority: string; // 优先级 - replyType: string; // 回复类型:文本回复、模板回复 + type: number; // 匹配类型:模糊匹配、精确匹配 + level: number; // 优先级 + replyType: number; // 回复类型:文本回复、模板回复 status: string; + metailGroups: any[]; } export interface KeywordUpdateRequest extends KeywordAddRequest { - id?: string; + id?: number; } export interface KeywordSetStatusRequest { - id: string; + id: number; } // 关键词回复-列表 @@ -157,12 +158,12 @@ export function addKeyword(data: KeywordAddRequest) { } // 关键词回复-详情 -export function getKeywordDetails(id: string) { +export function getKeywordDetails(id: number) { return request("/v1/kefu/content/keywords/details", { id }, "GET"); } // 关键词回复-删除 -export function deleteKeyword(id: string) { +export function deleteKeyword(id: number) { return request("/v1/kefu/content/keywords/del", { id }, "DELETE"); } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx index 99be1aaa..e3b94ad8 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx @@ -4,14 +4,21 @@ import React, { forwardRef, useImperativeHandle, } from "react"; -import { Button, Input, Tag, Switch, message } from "antd"; +import { + Button, + Input, + Tag, + Switch, + message, + Popconfirm, + Pagination, +} from "antd"; import { SearchOutlined, - FilterOutlined, FormOutlined, DeleteOutlined, } from "@ant-design/icons"; -import styles from "../../index.module.scss"; +import styles from "./index.module.scss"; import { getKeywordList, deleteKeyword, @@ -23,15 +30,15 @@ import KeywordModal from "../modals/KeywordModal"; const { Search } = Input; interface KeywordItem { - id: string; + id?: number; + type: number; + replyType: number; title: string; keywords: string; + status: number; content: string; - matchType: string; - priority: string; - replyType: string; - status: string; - enabled: boolean; + metailGroupsOptions: { title: string; id: number }[]; + level: number; } const KeywordManagement = forwardRef>( @@ -40,41 +47,79 @@ const KeywordManagement = forwardRef>( const [keywordsList, setKeywordsList] = useState([]); const [loading, setLoading] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); - const [editingKeywordId, setEditingKeywordId] = useState( + const [editingKeywordId, setEditingKeywordId] = useState( null, ); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + total: 0, + }); + // 已提交的搜索关键词(仅在点击搜索时更新,用于服务端查询) + const [keywordQuery, setKeywordQuery] = useState(""); + //匹配类型 + const getMatchTypeText = (type: number) => { + switch (type) { + case 0: + return "模糊匹配"; + case 1: + return "精确匹配"; + } + }; + + //匹配优先级 + const getPriorityText = (level: number) => { + switch (level) { + case 0: + return "低优先级"; + case 1: + return "中优先级"; + case 2: + return "高优先级"; + } + }; // 回复类型映射 - const getReplyTypeText = (replyType: string) => { + const getReplyTypeText = (replyType: number) => { switch (replyType) { - case "text": - return "文本回复"; - case "template": - return "模板回复"; + case 0: + return "素材回复"; + case 1: + return "自定义"; default: return "未知类型"; } }; // 回复类型颜色 - const getReplyTypeColor = (replyType: string) => { + const getReplyTypeColor = (replyType: number) => { switch (replyType) { - case "text": - return "#1890ff"; - case "template": - return "#722ed1"; + case 0: + return "blue"; + case 1: + return "purple"; default: - return "#8c8c8c"; + return "gray"; } }; - // 获取关键词列表 + // 获取关键词列表(服务端搜索) const fetchKeywords = async (params?: KeywordListParams) => { try { setLoading(true); - const response = await getKeywordList(params || {}); + const requestParams = { + page: pagination.current.toString(), + limit: pagination.pageSize.toString(), + keyword: keywordQuery || undefined, + ...params, + } as KeywordListParams; + const response = await getKeywordList(requestParams); if (response) { setKeywordsList(response.list || []); + setPagination(prev => ({ + ...prev, + total: response.total || 0, + })); } else { setKeywordsList([]); message.error(response?.message || "获取关键词列表失败"); @@ -94,26 +139,24 @@ const KeywordManagement = forwardRef>( })); // 关键词管理相关函数 - const handleToggleKeyword = async (id: string) => { + const handleToggleKeyword = async (id: number) => { try { - const response = await setKeywordStatus({ id }); - if (response) { - setKeywordsList(prev => - prev.map(item => - item.id === id ? { ...item, enabled: !item.enabled } : item, - ), - ); - message.success("状态更新成功"); - } else { - message.error(response?.message || "状态更新失败"); - } + await setKeywordStatus({ id }); + setKeywordsList(prev => + prev.map(item => + item.id === id + ? { ...item, status: item.status === 1 ? 0 : 1 } + : item, + ), + ); + message.success("状态更新成功"); } catch (error) { console.error("状态更新失败:", error); message.error("状态更新失败"); } }; - const handleEditKeyword = (id: string) => { + const handleEditKeyword = (id: number) => { setEditingKeywordId(id); setEditModalVisible(true); }; @@ -123,40 +166,39 @@ const KeywordManagement = forwardRef>( fetchKeywords(); // 重新获取数据 }; - const handleDeleteKeyword = async (id: string) => { + const handleDeleteKeyword = async (id: number) => { try { - const response = await deleteKeyword(id); - if (response) { - setKeywordsList(prev => prev.filter(item => item.id !== id)); - message.success("删除成功"); - } else { - message.error(response?.message || "删除失败"); - } + await deleteKeyword(id); + setKeywordsList(prev => prev.filter(item => item.id !== id)); + message.success("删除成功"); } catch (error) { console.error("删除失败:", error); message.error("删除失败"); } }; - // 搜索和筛选功能 - const filteredKeywords = keywordsList.filter(item => { - if (!searchValue) return true; - return ( - item.title.toLowerCase().includes(searchValue.toLowerCase()) || - item.keywords.toLowerCase().includes(searchValue.toLowerCase()) || - item.content.toLowerCase().includes(searchValue.toLowerCase()) - ); - }); + // 移除本地筛选,改为服务端搜索,列表直接使用 keywordsList // 搜索处理函数 const handleSearch = (value: string) => { - fetchKeywords({ keyword: value }); + setKeywordQuery(value || ""); + setPagination(prev => ({ ...prev, current: 1 })); }; - // 组件挂载时获取数据 + // 分页处理函数 + const handlePageChange = (page: number, pageSize?: number) => { + setPagination(prev => ({ + ...prev, + current: page, + pageSize: pageSize || prev.pageSize, + })); + }; + + // 初始化与依赖变化时获取数据(依赖分页与搜索关键字) useEffect(() => { fetchKeywords(); - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pagination.current, pagination.pageSize, keywordQuery]); return (
@@ -169,59 +211,92 @@ const KeywordManagement = forwardRef>( style={{ width: 300 }} prefix={} /> -
{loading ? (
加载中...
- ) : filteredKeywords.length === 0 ? ( + ) : keywordsList.length === 0 ? (
暂无关键词数据
) : ( - filteredKeywords.map(item => ( + keywordsList.map(item => (
-
{item.title}
-
- {item.matchType} - - 优先级{item.priority} - +
+
+
{item.title}
+ {getMatchTypeText(item.type)} + {getPriorityText(item.level)} +
+ {item.content.length ? ( +
{item.content}
+ ) : ( +
+ 已选素材: + {item.metailGroupsOptions.map(v => ( + + {v.title} + + ))} +
+ )} +
+ + {getReplyTypeText(item.replyType)} + +
+
+
+ handleToggleKeyword(item.id)} + className={styles.toggleSwitch} + /> +
-
{item.content}
- - {getReplyTypeText(item.replyType)} - -
-
- handleToggleKeyword(item.id)} - className={styles.toggleSwitch} - /> -
)) )}
+ {/* 分页组件 */} +
+ + `第 ${range[0]}-${range[1]} 条/共 ${total} 条` + } + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+ {/* 编辑弹窗 */} >( const [editingMaterialId, setEditingMaterialId] = useState( null, ); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + total: 0, + }); + + // 使用 ref 来存储最新的分页状态 + const paginationRef = useRef(pagination); + paginationRef.current = pagination; // 获取类型图标 const getTypeIcon = (type: string) => { @@ -68,9 +78,19 @@ const MaterialManagement = forwardRef>( const fetchMaterials = async (params?: MaterialListParams) => { try { setLoading(true); - const response = await getMaterialList(params || {}); + const currentPagination = paginationRef.current; + const requestParams = { + page: currentPagination.current.toString(), + limit: currentPagination.pageSize.toString(), + ...params, + }; + const response = await getMaterialList(requestParams); if (response) { setMaterialsList(response.list || []); + setPagination(prev => ({ + ...prev, + total: response.total || 0, + })); } else { setMaterialsList([]); message.error(response?.message || "获取素材列表失败"); @@ -91,22 +111,13 @@ const MaterialManagement = forwardRef>( // 素材管理相关函数 const handleDeleteMaterial = async (id: number) => { - Modal.confirm({ - title: "确认删除", - content: "确定要删除这个素材吗?删除后无法恢复。", - okText: "确定", - cancelText: "取消", - okType: "danger", - onOk: async () => { - try { - await deleteMaterial(id.toString()); - setMaterialsList(prev => prev.filter(item => item.id !== id)); - message.success("删除成功"); - } catch (error) { - message.error("删除失败"); - } - }, - }); + try { + await deleteMaterial(id.toString()); + setMaterialsList(prev => prev.filter(item => item.id !== id)); + message.success("删除成功"); + } catch (error) { + message.error("删除失败"); + } }; // 编辑素材 @@ -122,9 +133,23 @@ const MaterialManagement = forwardRef>( // 搜索处理函数 const handleSearch = (value: string) => { + setPagination(prev => ({ ...prev, current: 1 })); fetchMaterials({ keyword: value }); }; + // 分页处理函数 + const handlePageChange = (page: number, pageSize?: number) => { + setPagination(prev => ({ + ...prev, + current: page, + pageSize: pageSize || prev.pageSize, + })); + // 分页变化后立即获取数据 + setTimeout(() => { + fetchMaterials(); + }, 0); + }; + // 组件挂载时获取数据 useEffect(() => { fetchMaterials(); @@ -167,18 +192,24 @@ const MaterialManagement = forwardRef>( > 编辑 , - , + + , ]} >
>( )}
+ {/* 分页组件 */} +
+ + `第 ${range[0]}-${range[1]} 条/共 ${total} 条` + } + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+ {/* 编辑弹窗 */} { - const [searchValue, setSearchValue] = useState(""); - const [sensitiveWordsList, setSensitiveWordsList] = useState< - SensitiveWordItem[] - >([]); - const [loading, setLoading] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - const [editingSensitiveWordId, setEditingSensitiveWordId] = useState< - string | null - >(null); +const SensitiveWordManagement = forwardRef>( + (props, ref) => { + const [searchValue, setSearchValue] = useState(""); + const [sensitiveWordsList, setSensitiveWordsList] = useState< + SensitiveWordItem[] + >([]); + const [loading, setLoading] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editingSensitiveWordId, setEditingSensitiveWordId] = useState< + string | null + >(null); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + total: 0, + }); - const getTagColor = (tag: string) => { - switch (tag) { - case "政治": - return "#ff4d4f"; - case "色情": - return "#ff4d4f"; - case "暴力": - return "#ff4d4f"; - default: - return "#ff4d4f"; - } - }; + const getTagColor = (tag: string) => { + switch (tag) { + case "政治": + return "#ff4d4f"; + case "色情": + return "#ff4d4f"; + case "暴力": + return "#ff4d4f"; + default: + return "#ff4d4f"; + } + }; - // 操作类型映射 - const getOperationText = (operation: string) => { - switch (operation) { - case "0": - return "不操作"; - case "1": - return "替换"; - case "2": - return "删除"; - case "3": - return "警告"; - case "4": - return "禁止发送"; - default: - return "未知操作"; - } - }; + // 操作类型映射 + const getOperationText = (operation: number) => { + switch (operation) { + case 0: + return "不操作"; + case 1: + return "替换"; + case 2: + return "删除"; + case 3: + return "警告"; + case 4: + return "禁止发送"; + default: + return "未知操作"; + } + }; - // 获取敏感词列表 - const fetchSensitiveWords = async (params?: SensitiveWordListParams) => { - try { - setLoading(true); - const response = await getSensitiveWordList(params || {}); - if (response) { - setSensitiveWordsList(response.list || []); - } else { + // 获取敏感词列表 + const fetchSensitiveWords = async (params?: SensitiveWordListParams) => { + try { + setLoading(true); + const requestParams = { + page: pagination.current.toString(), + limit: pagination.pageSize.toString(), + ...params, + }; + const response = await getSensitiveWordList(requestParams); + if (response) { + setSensitiveWordsList(response.list || []); + setPagination(prev => ({ + ...prev, + total: response.total || 0, + })); + } else { + setSensitiveWordsList([]); + message.error(response?.message || "获取敏感词列表失败"); + } + } catch (error) { + console.error("获取敏感词列表失败:", error); setSensitiveWordsList([]); - message.error(response?.message || "获取敏感词列表失败"); + message.error("获取敏感词列表失败"); + } finally { + setLoading(false); } - } catch (error) { - console.error("获取敏感词列表失败:", error); - setSensitiveWordsList([]); - message.error("获取敏感词列表失败"); - } finally { - setLoading(false); - } - }; + }; - // 敏感词管理相关函数 - const handleToggleSensitiveWord = async (id: string) => { - try { - const response = await setSensitiveWordStatus({ id }); - if (response) { - setSensitiveWordsList(prev => - prev.map(item => - item.id === id ? { ...item, enabled: !item.enabled } : item, - ), - ); - message.success("状态更新成功"); - } else { - message.error(response?.message || "状态更新失败"); + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + fetchSensitiveWords, + })); + + // 敏感词管理相关函数 + const handleToggleSensitiveWord = async (id: string) => { + try { + const response = await setSensitiveWordStatus({ id }); + if (response) { + setSensitiveWordsList(prev => + prev.map(item => + item.id === id + ? { ...item, status: item.status === 1 ? 0 : 1 } + : item, + ), + ); + message.success("状态更新成功"); + } else { + message.error(response?.message || "状态更新失败"); + } + } catch (error) { + console.error("状态更新失败:", error); + message.error("状态更新失败"); } - } catch (error) { - console.error("状态更新失败:", error); - message.error("状态更新失败"); - } - }; + }; - const handleEditSensitiveWord = (id: string) => { - setEditingSensitiveWordId(id); - setEditModalVisible(true); - }; + const handleEditSensitiveWord = (id: string) => { + setEditingSensitiveWordId(id); + setEditModalVisible(true); + }; - // 编辑弹窗成功回调 - const handleEditSuccess = () => { - fetchSensitiveWords(); // 重新获取数据 - }; + // 编辑弹窗成功回调 + const handleEditSuccess = () => { + fetchSensitiveWords(); // 重新获取数据 + }; - const handleDeleteSensitiveWord = async (id: string) => { - try { - const response = await deleteSensitiveWord(id); - if (response) { + const handleDeleteSensitiveWord = async (id: string) => { + try { + await deleteSensitiveWord(id); setSensitiveWordsList(prev => prev.filter(item => item.id !== id)); message.success("删除成功"); - } else { - message.error(response?.message || "删除失败"); + } catch (error) { + console.error("删除失败:", error); + message.error("删除失败"); } - } catch (error) { - console.error("删除失败:", error); - message.error("删除失败"); - } - }; + }; + + // 搜索和筛选功能 + const filteredSensitiveWords = sensitiveWordsList.filter(item => { + if (!searchValue) return true; + return ( + item.title.toLowerCase().includes(searchValue.toLowerCase()) || + item.keywords.toLowerCase().includes(searchValue.toLowerCase()) || + item.content.toLowerCase().includes(searchValue.toLowerCase()) + ); + }); + + // 搜索处理函数 + const handleSearch = (value: string) => { + setPagination(prev => ({ ...prev, current: 1 })); + fetchSensitiveWords({ keyword: value }); + }; + + // 分页处理函数 + const handlePageChange = (page: number, pageSize?: number) => { + setPagination(prev => ({ + ...prev, + current: page, + pageSize: pageSize || prev.pageSize, + })); + }; + + // 组件挂载和分页变化时获取数据 + useEffect(() => { + fetchSensitiveWords(); + }, [pagination.current, pagination.pageSize]); - // 搜索和筛选功能 - const filteredSensitiveWords = sensitiveWordsList.filter(item => { - if (!searchValue) return true; return ( - item.title.toLowerCase().includes(searchValue.toLowerCase()) || - item.keywords.toLowerCase().includes(searchValue.toLowerCase()) || - item.content.toLowerCase().includes(searchValue.toLowerCase()) - ); - }); +
+
+ setSearchValue(e.target.value)} + onSearch={handleSearch} + style={{ width: 300 }} + prefix={} + /> + +
- // 搜索处理函数 - const handleSearch = (value: string) => { - fetchSensitiveWords({ keyword: value }); - }; - - // 组件挂载时获取数据 - useEffect(() => { - fetchSensitiveWords(); - }, []); - - return ( -
-
- setSearchValue(e.target.value)} - onSearch={handleSearch} - style={{ width: 300 }} - prefix={} - /> - -
- -
- {loading ? ( -
加载中...
- ) : filteredSensitiveWords.length === 0 ? ( -
暂无敏感词数据
- ) : ( - filteredSensitiveWords.map(item => ( -
-
-
{item.title}
- - {item.keywords} - -
- {getOperationText(item.operation)} +
+ {loading ? ( +
加载中...
+ ) : filteredSensitiveWords.length === 0 ? ( +
暂无敏感词数据
+ ) : ( + filteredSensitiveWords.map(item => ( +
+
+
{item.title}
+
+ {getOperationText(item.operation)} +
+
+
+ handleToggleSensitiveWord(item.id)} + className={styles.toggleSwitch} + /> +
-
- handleToggleSensitiveWord(item.id)} - className={styles.toggleSwitch} - /> -
-
- )) - )} -
+ )) + )} +
- {/* 编辑弹窗 */} - { - setEditModalVisible(false); - setEditingSensitiveWordId(null); - }} - onSuccess={handleEditSuccess} - /> -
- ); -}; + {/* 分页组件 */} +
+ + `第 ${range[0]}-${range[1]} 条/共 ${total} 条` + } + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+ + {/* 编辑弹窗 */} + { + setEditModalVisible(false); + setEditingSensitiveWordId(null); + }} + onSuccess={handleEditSuccess} + /> +
+ ); + }, +); + +SensitiveWordManagement.displayName = "SensitiveWordManagement"; export default SensitiveWordManagement; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss new file mode 100644 index 00000000..52943078 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss @@ -0,0 +1,252 @@ +// 关键词管理样式 +.keywordContent { + .searchSection { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + + :global(.ant-input-search) { + width: 300px; + } + + :global(.ant-btn) { + height: 32px; + border-radius: 6px; + } + } + + .keywordList { + display: flex; + flex-direction: column; + gap: 12px; + } + + .keywordItem { + padding: 16px 20px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 8px; + transition: all 0.3s; + + &:hover { + border-color: #d9d9d9; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .itemContent { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + + .leftSection { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + + .titleRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 4px; + + .title { + font-size: 16px; + font-weight: 500; + color: #262626; + margin: 0; + } + + .tags { + display: flex; + gap: 8px; + + .matchTag, + .priorityTag { + font-size: 12px; + padding: 2px 8px; + border-radius: 12px; + background: #f0f0f0; + color: #666; + border: none; + } + } + } + + .description { + font-size: 14px; + color: #666; + line-height: 1.5; + margin-bottom: 8px; + } + + .replyTypeTag { + font-size: 12px; + padding: 2px 8px; + border-radius: 20px; + background: #fff; + color: #fff; + } + } + + .rightSection { + display: flex; + + align-items: center; + gap: 8px; + + .toggleSwitch { + :global(.ant-switch) { + background-color: #d9d9d9; + } + + :global(.ant-switch-checked) { + background-color: #1890ff; + } + } + + .actionBtn { + width: 28px; + height: 28px; + padding: 0; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: #f5f5f5; + } + + .editIcon { + font-size: 14px; + color: #1890ff; + } + + .deleteIcon { + font-size: 14px; + color: #ff4d4f; + } + } + } + } + } +} + +// 响应式设计 +@media (max-width: 768px) { + .container { + padding: 16px; + } + + .headerActions { + flex-direction: column; + align-items: stretch; + + :global(.ant-btn) { + width: 100%; + } + } + + .tabs { + flex-direction: column; + padding: 0; + + .tab { + border-bottom: 1px solid #e8e8e8; + border-radius: 0; + + &:last-child { + border-bottom: none; + } + } + } + + .materialContent { + .searchSection { + flex-direction: column; + gap: 12px; + align-items: stretch; + + :global(.ant-input-search) { + width: 100%; + } + } + + .materialGrid { + grid-template-columns: 1fr; + gap: 16px; + } + } + + .sensitiveContent { + .searchSection { + flex-direction: column; + gap: 12px; + align-items: stretch; + + :global(.ant-input-search) { + width: 100%; + } + } + + .sensitiveItem { + flex-direction: column; + align-items: stretch; + gap: 12px; + + .itemContent { + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .categoryName { + min-width: auto; + } + } + + .itemActions { + justify-content: flex-end; + } + } + } + + .keywordContent { + .searchSection { + flex-direction: column; + gap: 12px; + align-items: stretch; + + :global(.ant-input-search) { + width: 100%; + } + } + + .keywordItem { + .itemContent { + flex-direction: column; + gap: 12px; + + .leftSection { + .titleRow { + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .tags { + flex-wrap: wrap; + } + } + } + + .rightSection { + flex-direction: row; + justify-content: flex-end; + align-items: center; + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/KeywordModal.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/KeywordModal.tsx index 209977fb..6dccd250 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/KeywordModal.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/KeywordModal.tsx @@ -7,14 +7,14 @@ import { type KeywordAddRequest, type KeywordUpdateRequest, } from "../../api"; - +import MetailSelection from "@/components/MetailSelection"; const { TextArea } = Input; const { Option } = Select; interface KeywordModalProps { visible: boolean; mode: "add" | "edit"; - keywordId?: string | null; + keywordId?: number | null; onCancel: () => void; onSuccess: () => void; } @@ -28,10 +28,12 @@ const KeywordModal: React.FC = ({ }) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); + const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复"; + const [selectedOptions, setSelectedOptions] = useState([]); // 获取关键词详情 const fetchKeywordDetails = useCallback( - async (id: string) => { + async (id: number) => { try { const response = await getKeywordDetails(id); if (response) { @@ -40,11 +42,13 @@ const KeywordModal: React.FC = ({ title: keyword.title, keywords: keyword.keywords, content: keyword.content, - matchType: keyword.matchType, - priority: keyword.priority, + type: keyword.type, + level: keyword.level, replyType: keyword.replyType, status: keyword.status, + metailGroups: keyword.metailGroups, }); + setSelectedOptions(keyword.metailGroupsOptions); } } catch (error) { console.error("获取关键词详情失败:", error); @@ -63,6 +67,7 @@ const KeywordModal: React.FC = ({ } else if (mode === "add") { // 添加模式:重置表单 form.resetFields(); + setSelectedOptions([]); } } }, [visible, mode, keywordId, fetchKeywordDetails, form]); @@ -76,10 +81,11 @@ const KeywordModal: React.FC = ({ title: values.title, keywords: values.keywords, content: values.content, - matchType: values.matchType, - priority: values.priority, + type: values.type, + level: values.level, replyType: values.replyType, status: values.status || "1", + metailGroups: values.metailGroups, }; const response = await addKeyword(data); @@ -97,10 +103,11 @@ const KeywordModal: React.FC = ({ title: values.title, keywords: values.keywords, content: values.content, - matchType: values.matchType, - priority: values.priority, + type: values.type, + level: values.level, replyType: values.replyType, status: values.status, + metailGroups: values.metailGroups, }; const response = await updateKeyword(data); @@ -123,11 +130,42 @@ const KeywordModal: React.FC = ({ const handleCancel = () => { form.resetFields(); + setSelectedOptions([]); onCancel(); }; - const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复"; + const handSelectMaterial = (options: any[]) => { + if (options.length === 0) { + form.setFieldsValue({ + metailGroups: [], + }); + } else { + // 在单选模式下,只取第一个选项的ID + form.setFieldsValue({ + metailGroups: options.map(v => v.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 ( = ({ form={form} layout="vertical" onFinish={handleSubmit} + onValuesChange={handleFormValuesChange} initialValues={{ - status: "1", - matchType: "模糊匹配", - priority: "1", - replyType: "text", + status: 1, + type: "模糊匹配", + level: 1, + replyType: 0, }} > = ({ - -