重构关键词管理模块:更新关键词和敏感词的请求参数类型为数字,增强类型安全性;优化关键词管理界面,改进状态切换和删除功能,增加确认删除的弹窗,提升用户体验。

This commit is contained in:
超级老白兔
2025-09-28 17:41:01 +08:00
parent 1bd3883b18
commit d8074502e0
7 changed files with 387 additions and 326 deletions

View File

@@ -132,9 +132,9 @@ export interface KeywordAddRequest {
title: string;
keywords: string;
content: string;
matchType: string; // 匹配类型:模糊匹配、精确匹配
priority: string; // 优先级
replyType: string; // 回复类型:文本回复、模板回复
type: number; // 匹配类型:模糊匹配、精确匹配
level: number; // 优先级
replyType: number; // 回复类型:文本回复、模板回复
status: string;
}
@@ -143,7 +143,7 @@ export interface KeywordUpdateRequest extends KeywordAddRequest {
}
export interface KeywordSetStatusRequest {
id: string;
id: number;
}
// 关键词回复-列表
@@ -157,12 +157,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");
}

View File

@@ -4,14 +4,14 @@ import React, {
forwardRef,
useImperativeHandle,
} from "react";
import { Button, Input, Tag, Switch, message } from "antd";
import { Button, Input, Tag, Switch, message, Popconfirm } 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 +23,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;
materialId: string;
level: number;
}
const KeywordManagement = forwardRef<any, Record<string, never>>(
@@ -40,31 +40,52 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingKeywordId, setEditingKeywordId] = useState<string | null>(
const [editingKeywordId, setEditingKeywordId] = useState<number | null>(
null,
);
//匹配类型
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";
}
};
@@ -94,26 +115,24 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
}));
// 关键词管理相关函数
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,15 +142,11 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
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("删除失败");
@@ -169,7 +184,7 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
<Button icon={<FilterOutlined />}></Button>
{/* <Button icon={<FilterOutlined />}>筛选</Button> */}
</div>
<div className={styles.keywordList}>
@@ -181,41 +196,48 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
filteredKeywords.map(item => (
<div key={item.id} className={styles.keywordItem}>
<div className={styles.itemContent}>
<div className={styles.title}>{item.title}</div>
<div className={styles.tags}>
<Tag className={styles.matchTag}>{item.matchType}</Tag>
<Tag className={styles.priorityTag}>
{item.priority}
</Tag>
<div className={styles.leftSection}>
<div className={styles.titleRow}>
<div className={styles.title}>{item.title}</div>
<Tag color="default">{getMatchTypeText(item.type)}</Tag>
<Tag color="default">{getPriorityText(item.level)}</Tag>
</div>
<div className={styles.description}>{item.content}</div>
<div className={styles.footer}>
<Tag color={getReplyTypeColor(item.replyType)}>
{getReplyTypeText(item.replyType)}
</Tag>
</div>
</div>
<div className={styles.rightSection}>
<Switch
checked={item.status === 1}
onChange={() => handleToggleKeyword(item.id)}
className={styles.toggleSwitch}
/>
<Button
type="text"
size="small"
icon={<FormOutlined className={styles.editIcon} />}
onClick={() => handleEditKeyword(item.id)}
className={styles.actionBtn}
/>
<Popconfirm
title="确认删除"
description="确定要删除这个关键词吗?删除后无法恢复。"
onConfirm={() => handleDeleteKeyword(item.id)}
okText="确定"
cancelText="取消"
okType="danger"
>
<Button
type="text"
size="small"
icon={<DeleteOutlined className={styles.deleteIcon} />}
className={styles.actionBtn}
/>
</Popconfirm>
</div>
<div className={styles.description}>{item.content}</div>
<Tag
color={getReplyTypeColor(item.replyType)}
className={styles.replyTypeTag}
>
{getReplyTypeText(item.replyType)}
</Tag>
</div>
<div className={styles.itemActions}>
<Switch
checked={item.enabled}
onChange={() => handleToggleKeyword(item.id)}
className={styles.toggleSwitch}
/>
<Button
type="text"
size="small"
icon={<FormOutlined className={styles.editIcon} />}
onClick={() => handleEditKeyword(item.id)}
className={styles.actionBtn}
/>
<Button
type="text"
size="small"
icon={<DeleteOutlined className={styles.deleteIcon} />}
onClick={() => handleDeleteKeyword(item.id)}
className={styles.actionBtn}
/>
</div>
</div>
))

View File

@@ -27,7 +27,7 @@ interface SensitiveWordItem {
title: string;
keywords: string;
content: string;
operation: string;
operation: number;
status: string;
enabled: boolean;
}
@@ -185,12 +185,6 @@ const SensitiveWordManagement = forwardRef<any, Record<string, never>>(
<div key={item.id} className={styles.sensitiveItem}>
<div className={styles.itemContent}>
<div className={styles.categoryName}>{item.title}</div>
<Tag
color={getTagColor(item.keywords)}
className={styles.sensitiveTag}
>
{item.keywords}
</Tag>
<div className={styles.actionText}>
{getOperationText(item.operation)}
</div>

View File

@@ -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;
flex-direction: column;
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;
}
}
}
}
}

View File

@@ -14,7 +14,7 @@ const { Option } = Select;
interface KeywordModalProps {
visible: boolean;
mode: "add" | "edit";
keywordId?: string | null;
keywordId?: number | null;
onCancel: () => void;
onSuccess: () => void;
}
@@ -31,7 +31,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
// 获取关键词详情
const fetchKeywordDetails = useCallback(
async (id: string) => {
async (id: number) => {
try {
const response = await getKeywordDetails(id);
if (response) {
@@ -40,8 +40,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
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,
});
@@ -76,8 +76,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
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",
};
@@ -97,8 +97,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
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,
};
@@ -141,10 +141,10 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
layout="vertical"
onFinish={handleSubmit}
initialValues={{
status: "1",
matchType: "模糊匹配",
priority: "1",
replyType: "text",
status: 1,
type: "模糊匹配",
level: 1,
replyType: 0,
}}
>
<Form.Item
@@ -172,27 +172,25 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
</Form.Item>
<Form.Item
name="matchType"
name="type"
label="匹配类型"
rules={[{ required: true, message: "请选择匹配类型" }]}
>
<Select placeholder="请选择匹配类型">
<Option value="模糊匹配"></Option>
<Option value="精确匹配"></Option>
<Option value={0}></Option>
<Option value={1}></Option>
</Select>
</Form.Item>
<Form.Item
name="priority"
name="level"
label="优先级"
rules={[{ required: true, message: "请选择优先级" }]}
>
<Select placeholder="请选择优先级">
<Option value="1">1</Option>
<Option value="2">2</Option>
<Option value="3">3</Option>
<Option value="4">4</Option>
<Option value="5">5</Option>
<Option value={0}></Option>
<Option value={1}></Option>
<Option value={2}></Option>
</Select>
</Form.Item>
@@ -202,8 +200,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
rules={[{ required: true, message: "请选择回复类型" }]}
>
<Select placeholder="请选择回复类型">
<Option value="text"></Option>
<Option value="template"></Option>
<Option value={0}></Option>
<Option value={1}></Option>
</Select>
</Form.Item>
@@ -213,8 +211,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
<Option value={1}></Option>
<Option value={0}></Option>
</Select>
</Form.Item>

View File

@@ -166,11 +166,11 @@ const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
rules={[{ required: true, message: "请选择操作类型" }]}
>
<Select placeholder="请选择操作类型">
<Option value="0"></Option>
<Option value="1"></Option>
<Option value="2"></Option>
<Option value="3"></Option>
<Option value="4"></Option>
<Option value={0}></Option>
<Option value={1}></Option>
<Option value={2}></Option>
<Option value={3}></Option>
<Option value={4}></Option>
</Select>
</Form.Item>
@@ -180,8 +180,8 @@ const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
<Option value={1}></Option>
<Option value={0}></Option>
</Select>
</Form.Item>

View File

@@ -291,133 +291,6 @@
}
}
// 关键词管理样式
.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 {
display: flex;
justify-content: space-between;
align-items: flex-start;
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;
flex-direction: column;
gap: 8px;
flex: 1;
.title {
font-size: 16px;
font-weight: 500;
color: #262626;
margin: 0;
}
.tags {
display: flex;
gap: 8px;
margin-bottom: 4px;
.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: 4px;
border: none;
align-self: flex-start;
}
}
.itemActions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
.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;
}
}
}
}
}
// 弹窗中的图片上传组件样式
:global(.material-cover-upload) {
.uploadContainer {
@@ -525,82 +398,4 @@
}
}
}
.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 {
flex-direction: column;
align-items: stretch;
gap: 12px;
.itemContent {
.tags {
flex-wrap: wrap;
}
}
.itemActions {
justify-content: flex-end;
margin-left: 0;
}
}
}
}