Merge branch 'yongpxu-dev' into develop
This commit is contained in:
@@ -31,7 +31,7 @@ const defaultForm = {
|
|||||||
syncCount: 5,
|
syncCount: 5,
|
||||||
syncInterval: 30,
|
syncInterval: 30,
|
||||||
syncType: 1, // 1=业务号 2=人设号
|
syncType: 1, // 1=业务号 2=人设号
|
||||||
accountType: "business" as "business" | "personal", // 仅UI用
|
accountType: 1, // 仅UI用
|
||||||
enabled: true,
|
enabled: true,
|
||||||
deviceGroups: [] as any[],
|
deviceGroups: [] as any[],
|
||||||
contentGroups: [] as any[], // 存完整内容库对象数组
|
contentGroups: [] as any[], // 存完整内容库对象数组
|
||||||
@@ -67,8 +67,8 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
endTime: res.timeRange?.end || "23:59",
|
endTime: res.timeRange?.end || "23:59",
|
||||||
syncCount: res.config?.syncCount || res.syncCount || 5,
|
syncCount: res.config?.syncCount || res.syncCount || 5,
|
||||||
syncInterval: res.config?.syncInterval || res.syncInterval || 30,
|
syncInterval: res.config?.syncInterval || res.syncInterval || 30,
|
||||||
syncType: res.accountType === 1 ? 1 : 2,
|
syncType: res.config?.syncType,
|
||||||
accountType: res.accountType === 1 ? "business" : "personal",
|
accountType: res.config?.accountType,
|
||||||
enabled: res.status === 1,
|
enabled: res.status === 1,
|
||||||
deviceGroups: res.config?.deviceGroups || [],
|
deviceGroups: res.config?.deviceGroups || [],
|
||||||
// 关键:用id字符串数组回填
|
// 关键:用id字符串数组回填
|
||||||
@@ -101,11 +101,11 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// UI选择账号类型时同步syncType和accountType
|
// UI选择账号类型时同步syncType和accountType
|
||||||
const handleAccountTypeChange = (type: "business" | "personal") => {
|
const handleAccountTypeChange = (type: number) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
accountType: type,
|
accountType: type,
|
||||||
syncType: type === "business" ? 1 : 2,
|
syncType: type,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
const handleDevicesChange = (devices: DeviceSelectionItem[]) => {
|
const handleDevicesChange = (devices: DeviceSelectionItem[]) => {
|
||||||
@@ -135,11 +135,11 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
const params = {
|
const params = {
|
||||||
name: formData.taskName,
|
name: formData.taskName,
|
||||||
deviceGroups: formData.deviceGroups,
|
deviceGroups: formData.deviceGroups,
|
||||||
contentGroups: formData.contentGroups.map((lib: any) => lib.id),
|
contentGroups: contentGroupsOptions.map((lib: any) => lib.id),
|
||||||
syncInterval: formData.syncInterval,
|
syncInterval: formData.syncInterval,
|
||||||
syncCount: formData.syncCount,
|
syncCount: formData.syncCount,
|
||||||
syncType: formData.syncType, // 账号类型真实传参
|
syncType: formData.syncType, // 账号类型真实传参
|
||||||
accountType: formData.accountType === "business" ? 1 : 2, // 也要传
|
accountType: formData.accountType, // 也要传
|
||||||
startTime: formData.startTime,
|
startTime: formData.startTime,
|
||||||
endTime: formData.endTime,
|
endTime: formData.endTime,
|
||||||
contentTypes: formData.contentTypes,
|
contentTypes: formData.contentTypes,
|
||||||
@@ -227,14 +227,14 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
<div className={style.formLabel}>账号类型</div>
|
<div className={style.formLabel}>账号类型</div>
|
||||||
<div className={style.accountTypeRow}>
|
<div className={style.accountTypeRow}>
|
||||||
<button
|
<button
|
||||||
className={`${style.accountTypeBtn} ${formData.accountType === "business" ? style.accountTypeActive : ""}`}
|
className={`${style.accountTypeBtn} ${formData.accountType === 1 ? style.accountTypeActive : ""}`}
|
||||||
onClick={() => handleAccountTypeChange("business")}
|
onClick={() => handleAccountTypeChange(1)}
|
||||||
>
|
>
|
||||||
业务号
|
业务号
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${style.accountTypeBtn} ${formData.accountType === "personal" ? style.accountTypeActive : ""}`}
|
className={`${style.accountTypeBtn} ${formData.accountType === 2 ? style.accountTypeActive : ""}`}
|
||||||
onClick={() => handleAccountTypeChange("personal")}
|
onClick={() => handleAccountTypeChange(2)}
|
||||||
>
|
>
|
||||||
人设号
|
人设号
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
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
|
||||||
|
|||||||
16
Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts
Normal file
16
Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 消息列表
|
||||||
|
export const noticeList = (params: { page: number; limit: number }) => {
|
||||||
|
return request(`/v1/kefu/notice/list`, params, "GET");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 消息列表
|
||||||
|
export const readMessage = (params: { id: number }) => {
|
||||||
|
return request(`/v1/kefu/notice/readMessage`, params, "PUT");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 消息列表
|
||||||
|
export const readAll = () => {
|
||||||
|
return request(`/v1/kefu/notice/readAll`, undefined, "PUT");
|
||||||
|
};
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Layout, Drawer, Avatar, Space, Button, Badge, Dropdown } from "antd";
|
import {
|
||||||
|
Layout,
|
||||||
|
Drawer,
|
||||||
|
Avatar,
|
||||||
|
Space,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
Dropdown,
|
||||||
|
Empty,
|
||||||
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
UserSwitchOutlined,
|
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
WechatOutlined,
|
WechatOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { noticeList, readMessage, readAll } from "./api";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
@@ -22,17 +30,42 @@ interface NavCommonProps {
|
|||||||
onMenuClick?: () => void;
|
onMenuClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 消息数据类型
|
||||||
|
interface MessageItem {
|
||||||
|
id: number;
|
||||||
|
type: number;
|
||||||
|
companyId: number;
|
||||||
|
userId: number;
|
||||||
|
bindId: number;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
isRead: number;
|
||||||
|
createTime: string;
|
||||||
|
readTime: string;
|
||||||
|
friendData: {
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||||
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
|
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
|
||||||
const [messageCount] = useState(3); // 模拟消息数量
|
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
||||||
|
const [messageCount, setMessageCount] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout } = useUserStore();
|
const { user, logout } = useUserStore();
|
||||||
|
|
||||||
|
// 初始化时获取消息列表
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMessageList();
|
||||||
|
setInterval(IntervalMessageCount, 30 * 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 处理菜单图标点击:在两个路由之间切换
|
// 处理菜单图标点击:在两个路由之间切换
|
||||||
const handleMenuClick = () => {
|
const handleMenuClick = (index: number) => {
|
||||||
const current = location.pathname;
|
if (index === 0) {
|
||||||
if (current.startsWith("/pc/weChat")) {
|
|
||||||
navigate("/pc/powerCenter");
|
navigate("/pc/powerCenter");
|
||||||
} else {
|
} else {
|
||||||
navigate("/pc/weChat");
|
navigate("/pc/weChat");
|
||||||
@@ -43,9 +76,41 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
return location.pathname.startsWith("/pc/weChat");
|
return location.pathname.startsWith("/pc/weChat");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 定时器获取消息条数
|
||||||
|
const IntervalMessageCount = async () => {
|
||||||
|
try {
|
||||||
|
const response = await noticeList({ page: 1, limit: 20 });
|
||||||
|
if (response && response.noRead) {
|
||||||
|
setMessageCount(response.noRead);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取消息列表失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 获取消息列表
|
||||||
|
const fetchMessageList = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await noticeList({ page: 1, limit: 20 });
|
||||||
|
if (response && response.list) {
|
||||||
|
setMessageList(response.list);
|
||||||
|
// 计算未读消息数量
|
||||||
|
const unreadCount = response.list.filter(
|
||||||
|
(item: MessageItem) => item.isRead === 0,
|
||||||
|
).length;
|
||||||
|
setMessageCount(unreadCount);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取消息列表失败:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理消息中心点击
|
// 处理消息中心点击
|
||||||
const handleMessageClick = () => {
|
const handleMessageClick = () => {
|
||||||
setMessageDrawerVisible(true);
|
setMessageDrawerVisible(true);
|
||||||
|
fetchMessageList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理消息抽屉关闭
|
// 处理消息抽屉关闭
|
||||||
@@ -59,23 +124,78 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
navigate("/login"); // 跳转到登录页面
|
navigate("/login"); // 跳转到登录页面
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理消息已读
|
||||||
|
const handleReadMessage = async (messageId: number) => {
|
||||||
|
try {
|
||||||
|
await readMessage({ id: messageId }); // 这里需要根据实际API调整参数
|
||||||
|
// 更新本地状态
|
||||||
|
setMessageList(prev =>
|
||||||
|
prev.map(item =>
|
||||||
|
item.id === messageId ? { ...item, isRead: 1 } : item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// 重新计算未读数量
|
||||||
|
const unreadCount =
|
||||||
|
messageList.filter(item => item.isRead === 0).length - 1;
|
||||||
|
setMessageCount(Math.max(0, unreadCount));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("标记消息已读失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理全部已读
|
||||||
|
const handleReadAll = async () => {
|
||||||
|
try {
|
||||||
|
await readAll(); // 这里需要根据实际API调整参数
|
||||||
|
// 更新本地状态
|
||||||
|
setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 })));
|
||||||
|
setMessageCount(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("全部已读失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timeStr: string) => {
|
||||||
|
const date = new Date(timeStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (days === 0) {
|
||||||
|
return date.toLocaleTimeString("zh-CN", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
} else if (days === 1) {
|
||||||
|
return "昨天";
|
||||||
|
} else if (days < 7) {
|
||||||
|
return `${days}天前`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString("zh-CN", {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 用户菜单项
|
// 用户菜单项
|
||||||
const userMenuItems = [
|
const userMenuItems = [
|
||||||
{
|
{
|
||||||
key: "userInfo",
|
key: "userInfo",
|
||||||
label: (
|
label: (
|
||||||
<div style={{ fontWeight: "bold", color: "#188eee" }}>
|
<div style={{ fontWeight: "bold", color: "#188eee" }}>
|
||||||
{user.username}({user.account})
|
{user.account}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "profile",
|
key: "settings",
|
||||||
icon: <UserSwitchOutlined style={{ fontSize: 16 }} />,
|
icon: <SettingOutlined style={{ fontSize: 16 }} />,
|
||||||
label: "个人资料",
|
label: "全局配置",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
console.log("个人资料点击");
|
navigate("/pc/commonConfig");
|
||||||
// TODO: 跳转到个人资料页面
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -93,14 +213,14 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={<BarChartOutlined />}
|
icon={<BarChartOutlined />}
|
||||||
type={!isWeChat() ? "primary" : "default"}
|
type={!isWeChat() ? "primary" : "default"}
|
||||||
onClick={handleMenuClick}
|
onClick={() => handleMenuClick(0)}
|
||||||
>
|
>
|
||||||
功能中心
|
功能中心
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<WechatOutlined />}
|
icon={<WechatOutlined />}
|
||||||
type={isWeChat() ? "primary" : "default"}
|
type={isWeChat() ? "primary" : "default"}
|
||||||
onClick={handleMenuClick}
|
onClick={() => handleMenuClick(1)}
|
||||||
>
|
>
|
||||||
Ai智能客服
|
Ai智能客服
|
||||||
</Button>
|
</Button>
|
||||||
@@ -120,14 +240,7 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
<BellOutlined style={{ fontSize: 20 }} />
|
<BellOutlined style={{ fontSize: 20 }} />
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
navigate("/pc/commonConfig");
|
|
||||||
}}
|
|
||||||
icon={<SettingOutlined />}
|
|
||||||
>
|
|
||||||
全局配置
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{ items: userMenuItems }}
|
menu={{ items: userMenuItems }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
@@ -160,74 +273,57 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
className={styles.messageDrawer}
|
className={styles.messageDrawer}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="text" size="small">
|
<Button type="text" size="small" onClick={handleReadAll}>
|
||||||
全部已读
|
全部已读
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.messageContent}>
|
<div className={styles.messageContent}>
|
||||||
<div className={styles.messageItem}>
|
{loading ? (
|
||||||
<div className={styles.messageAvatar}>
|
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||||
<Avatar size={40} style={{ backgroundColor: "#87d068" }}>
|
加载中...
|
||||||
林
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.messageInfo}>
|
) : messageList.length === 0 ? (
|
||||||
<div className={styles.messageTitle}>
|
<Empty description="暂无消息" />
|
||||||
<span className={styles.messageType}>新消息</span>
|
) : (
|
||||||
<div className={styles.messageStatus}></div>
|
messageList.map(item => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`${styles.messageItem} ${
|
||||||
|
item.isRead === 0 ? styles.unread : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleReadMessage(item.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.messageAvatar}>
|
||||||
|
<Avatar
|
||||||
|
size={40}
|
||||||
|
src={item.friendData?.avatar}
|
||||||
|
style={{ backgroundColor: "#87d068" }}
|
||||||
|
>
|
||||||
|
{item.friendData?.nickname?.charAt(0) || "U"}
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className={styles.messageInfo}>
|
||||||
|
<div className={styles.messageTitle}>
|
||||||
|
<span className={styles.messageType}>{item.title}</span>
|
||||||
|
{item.isRead === 0 && (
|
||||||
|
<div className={styles.messageStatus}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.messageText}>{item.message}</div>
|
||||||
|
{item.isRead === 0 && (
|
||||||
|
<div className={styles.messageTime}>
|
||||||
|
{formatTime(item.createTime)}
|
||||||
|
<Button type="link" size="small">
|
||||||
|
标记已读
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.messageText}>
|
))
|
||||||
林秀杰:关于自由主时间特惠,原价1999元,现在只需1699元
|
)}
|
||||||
</div>
|
|
||||||
<div className={styles.messageTime}>
|
|
||||||
03-05
|
|
||||||
<Button type="link" size="small">
|
|
||||||
回复
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.messageItem}>
|
|
||||||
<div className={styles.messageAvatar}>
|
|
||||||
<Avatar size={40} style={{ backgroundColor: "#f56a00" }}>
|
|
||||||
E
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
<div className={styles.messageInfo}>
|
|
||||||
<div className={styles.messageTitle}>
|
|
||||||
<span className={styles.messageType}>群提醒</span>
|
|
||||||
<div className={styles.messageStatus}></div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.messageText}>
|
|
||||||
Eric在「云归营私域银行项目群」中@了您
|
|
||||||
</div>
|
|
||||||
<div className={styles.messageTime}>03-05</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.messageItem}>
|
|
||||||
<div className={styles.messageAvatar}>
|
|
||||||
<Avatar size={40} style={{ backgroundColor: "#1890ff" }}>
|
|
||||||
李
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
<div className={styles.messageInfo}>
|
|
||||||
<div className={styles.messageTitle}>
|
|
||||||
<span className={styles.messageType}>好友申请</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.messageText}>李翔想请求添加您为好友</div>
|
|
||||||
<div className={styles.messageTime}>03-04</div>
|
|
||||||
<div className={styles.messageActions}>
|
|
||||||
<Button type="primary" size="small">
|
|
||||||
接受
|
|
||||||
</Button>
|
|
||||||
<Button size="small">拒绝</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function updateSensitiveWord(data: SensitiveWordUpdateRequest) {
|
|||||||
|
|
||||||
// 违禁词管理-修改状态
|
// 违禁词管理-修改状态
|
||||||
export function setSensitiveWordStatus(data: SensitiveWordSetStatusRequest) {
|
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;
|
title: string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
content: string;
|
content: string;
|
||||||
matchType: string; // 匹配类型:模糊匹配、精确匹配
|
type: number; // 匹配类型:模糊匹配、精确匹配
|
||||||
priority: string; // 优先级
|
level: number; // 优先级
|
||||||
replyType: string; // 回复类型:文本回复、模板回复
|
replyType: number; // 回复类型:文本回复、模板回复
|
||||||
status: string;
|
status: string;
|
||||||
|
metailGroups: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeywordUpdateRequest extends KeywordAddRequest {
|
export interface KeywordUpdateRequest extends KeywordAddRequest {
|
||||||
id?: string;
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeywordSetStatusRequest {
|
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");
|
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");
|
return request("/v1/kefu/content/keywords/del", { id }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import React, {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Button, Input, Tag, Switch, message } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Tag,
|
||||||
|
Switch,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Pagination,
|
||||||
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FilterOutlined,
|
|
||||||
FormOutlined,
|
FormOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import styles from "../../index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import {
|
import {
|
||||||
getKeywordList,
|
getKeywordList,
|
||||||
deleteKeyword,
|
deleteKeyword,
|
||||||
@@ -23,15 +30,15 @@ import KeywordModal from "../modals/KeywordModal";
|
|||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
interface KeywordItem {
|
interface KeywordItem {
|
||||||
id: string;
|
id?: number;
|
||||||
|
type: number;
|
||||||
|
replyType: number;
|
||||||
title: string;
|
title: string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
|
status: number;
|
||||||
content: string;
|
content: string;
|
||||||
matchType: string;
|
metailGroupsOptions: { title: string; id: number }[];
|
||||||
priority: string;
|
level: number;
|
||||||
replyType: string;
|
|
||||||
status: string;
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeywordManagement = forwardRef<any, Record<string, never>>(
|
const KeywordManagement = forwardRef<any, Record<string, never>>(
|
||||||
@@ -40,41 +47,79 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
|
|||||||
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
|
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||||
const [editingKeywordId, setEditingKeywordId] = useState<string | null>(
|
const [editingKeywordId, setEditingKeywordId] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
// 已提交的搜索关键词(仅在点击搜索时更新,用于服务端查询)
|
||||||
|
const [keywordQuery, setKeywordQuery] = useState<string>("");
|
||||||
|
|
||||||
|
//匹配类型
|
||||||
|
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) {
|
switch (replyType) {
|
||||||
case "text":
|
case 0:
|
||||||
return "文本回复";
|
return "素材回复";
|
||||||
case "template":
|
case 1:
|
||||||
return "模板回复";
|
return "自定义";
|
||||||
default:
|
default:
|
||||||
return "未知类型";
|
return "未知类型";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 回复类型颜色
|
// 回复类型颜色
|
||||||
const getReplyTypeColor = (replyType: string) => {
|
const getReplyTypeColor = (replyType: number) => {
|
||||||
switch (replyType) {
|
switch (replyType) {
|
||||||
case "text":
|
case 0:
|
||||||
return "#1890ff";
|
return "blue";
|
||||||
case "template":
|
case 1:
|
||||||
return "#722ed1";
|
return "purple";
|
||||||
default:
|
default:
|
||||||
return "#8c8c8c";
|
return "gray";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取关键词列表
|
// 获取关键词列表(服务端搜索)
|
||||||
const fetchKeywords = async (params?: KeywordListParams) => {
|
const fetchKeywords = async (params?: KeywordListParams) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) {
|
if (response) {
|
||||||
setKeywordsList(response.list || []);
|
setKeywordsList(response.list || []);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
total: response.total || 0,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
setKeywordsList([]);
|
setKeywordsList([]);
|
||||||
message.error(response?.message || "获取关键词列表失败");
|
message.error(response?.message || "获取关键词列表失败");
|
||||||
@@ -94,26 +139,24 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// 关键词管理相关函数
|
// 关键词管理相关函数
|
||||||
const handleToggleKeyword = async (id: string) => {
|
const handleToggleKeyword = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await setKeywordStatus({ id });
|
await setKeywordStatus({ id });
|
||||||
if (response) {
|
setKeywordsList(prev =>
|
||||||
setKeywordsList(prev =>
|
prev.map(item =>
|
||||||
prev.map(item =>
|
item.id === id
|
||||||
item.id === id ? { ...item, enabled: !item.enabled } : item,
|
? { ...item, status: item.status === 1 ? 0 : 1 }
|
||||||
),
|
: item,
|
||||||
);
|
),
|
||||||
message.success("状态更新成功");
|
);
|
||||||
} else {
|
message.success("状态更新成功");
|
||||||
message.error(response?.message || "状态更新失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("状态更新失败:", error);
|
console.error("状态更新失败:", error);
|
||||||
message.error("状态更新失败");
|
message.error("状态更新失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditKeyword = (id: string) => {
|
const handleEditKeyword = (id: number) => {
|
||||||
setEditingKeywordId(id);
|
setEditingKeywordId(id);
|
||||||
setEditModalVisible(true);
|
setEditModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -123,40 +166,39 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
|
|||||||
fetchKeywords(); // 重新获取数据
|
fetchKeywords(); // 重新获取数据
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteKeyword = async (id: string) => {
|
const handleDeleteKeyword = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await deleteKeyword(id);
|
await deleteKeyword(id);
|
||||||
if (response) {
|
setKeywordsList(prev => prev.filter(item => item.id !== id));
|
||||||
setKeywordsList(prev => prev.filter(item => item.id !== id));
|
message.success("删除成功");
|
||||||
message.success("删除成功");
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "删除失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("删除失败:", error);
|
console.error("删除失败:", error);
|
||||||
message.error("删除失败");
|
message.error("删除失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索和筛选功能
|
// 移除本地筛选,改为服务端搜索,列表直接使用 keywordsList
|
||||||
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())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 搜索处理函数
|
// 搜索处理函数
|
||||||
const handleSearch = (value: string) => {
|
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(() => {
|
useEffect(() => {
|
||||||
fetchKeywords();
|
fetchKeywords();
|
||||||
}, []);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pagination.current, pagination.pageSize, keywordQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.keywordContent}>
|
<div className={styles.keywordContent}>
|
||||||
@@ -169,59 +211,92 @@ const KeywordManagement = forwardRef<any, Record<string, never>>(
|
|||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
<Button icon={<FilterOutlined />}>筛选</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.keywordList}>
|
<div className={styles.keywordList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={styles.loading}>加载中...</div>
|
<div className={styles.loading}>加载中...</div>
|
||||||
) : filteredKeywords.length === 0 ? (
|
) : keywordsList.length === 0 ? (
|
||||||
<div className={styles.empty}>暂无关键词数据</div>
|
<div className={styles.empty}>暂无关键词数据</div>
|
||||||
) : (
|
) : (
|
||||||
filteredKeywords.map(item => (
|
keywordsList.map(item => (
|
||||||
<div key={item.id} className={styles.keywordItem}>
|
<div key={item.id} className={styles.keywordItem}>
|
||||||
<div className={styles.itemContent}>
|
<div className={styles.itemContent}>
|
||||||
<div className={styles.title}>{item.title}</div>
|
<div className={styles.leftSection}>
|
||||||
<div className={styles.tags}>
|
<div className={styles.titleRow}>
|
||||||
<Tag className={styles.matchTag}>{item.matchType}</Tag>
|
<div className={styles.title}>{item.title}</div>
|
||||||
<Tag className={styles.priorityTag}>
|
<Tag color="default">{getMatchTypeText(item.type)}</Tag>
|
||||||
优先级{item.priority}
|
<Tag color="default">{getPriorityText(item.level)}</Tag>
|
||||||
</Tag>
|
</div>
|
||||||
|
{item.content.length ? (
|
||||||
|
<div className={styles.description}>{item.content}</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.description}>
|
||||||
|
已选素材:
|
||||||
|
{item.metailGroupsOptions.map(v => (
|
||||||
|
<Tag color="success" key={v.id}>
|
||||||
|
{v.title}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
<div style={{ marginTop: 16, textAlign: "right" }}>
|
||||||
|
<Pagination
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
total={pagination.total}
|
||||||
|
showSizeChanger
|
||||||
|
showQuickJumper
|
||||||
|
showTotal={(total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
||||||
|
}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
onShowSizeChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 编辑弹窗 */}
|
||||||
<KeywordModal
|
<KeywordModal
|
||||||
visible={editModalVisible}
|
visible={editModalVisible}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Button, Input, Card, message, Modal } from "antd";
|
import { Button, Input, Card, message, Popconfirm, Pagination } from "antd";
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FilterOutlined,
|
FilterOutlined,
|
||||||
@@ -49,6 +50,15 @@ const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|||||||
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 ref 来存储最新的分页状态
|
||||||
|
const paginationRef = useRef(pagination);
|
||||||
|
paginationRef.current = pagination;
|
||||||
|
|
||||||
// 获取类型图标
|
// 获取类型图标
|
||||||
const getTypeIcon = (type: string) => {
|
const getTypeIcon = (type: string) => {
|
||||||
@@ -68,9 +78,19 @@ const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|||||||
const fetchMaterials = async (params?: MaterialListParams) => {
|
const fetchMaterials = async (params?: MaterialListParams) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) {
|
if (response) {
|
||||||
setMaterialsList(response.list || []);
|
setMaterialsList(response.list || []);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
total: response.total || 0,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
setMaterialsList([]);
|
setMaterialsList([]);
|
||||||
message.error(response?.message || "获取素材列表失败");
|
message.error(response?.message || "获取素材列表失败");
|
||||||
@@ -91,22 +111,13 @@ const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|||||||
|
|
||||||
// 素材管理相关函数
|
// 素材管理相关函数
|
||||||
const handleDeleteMaterial = async (id: number) => {
|
const handleDeleteMaterial = async (id: number) => {
|
||||||
Modal.confirm({
|
try {
|
||||||
title: "确认删除",
|
await deleteMaterial(id.toString());
|
||||||
content: "确定要删除这个素材吗?删除后无法恢复。",
|
setMaterialsList(prev => prev.filter(item => item.id !== id));
|
||||||
okText: "确定",
|
message.success("删除成功");
|
||||||
cancelText: "取消",
|
} catch (error) {
|
||||||
okType: "danger",
|
message.error("删除失败");
|
||||||
onOk: async () => {
|
}
|
||||||
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<any, Record<string, never>>(
|
|||||||
|
|
||||||
// 搜索处理函数
|
// 搜索处理函数
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
fetchMaterials({ keyword: value });
|
fetchMaterials({ keyword: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 分页处理函数
|
||||||
|
const handlePageChange = (page: number, pageSize?: number) => {
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize || prev.pageSize,
|
||||||
|
}));
|
||||||
|
// 分页变化后立即获取数据
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchMaterials();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 组件挂载时获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMaterials();
|
fetchMaterials();
|
||||||
@@ -167,18 +192,24 @@ const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Popconfirm
|
||||||
key="delete"
|
key="delete"
|
||||||
type="text"
|
title="确认删除"
|
||||||
danger
|
description="确定要删除这个素材吗?删除后无法恢复。"
|
||||||
icon={<DeleteOutlined />}
|
onConfirm={() => handleDeleteMaterial(item.id)}
|
||||||
onClick={e => {
|
okText="确定"
|
||||||
e.stopPropagation();
|
cancelText="取消"
|
||||||
handleDeleteMaterial(item.id);
|
okType="danger"
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>,
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -252,6 +283,22 @@ const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
<div style={{ marginTop: 16, textAlign: "right" }}>
|
||||||
|
<Pagination
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
total={pagination.total}
|
||||||
|
showSizeChanger
|
||||||
|
showQuickJumper
|
||||||
|
showTotal={(total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
||||||
|
}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
onShowSizeChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 编辑弹窗 */}
|
||||||
<MaterialModal
|
<MaterialModal
|
||||||
visible={editModalVisible}
|
visible={editModalVisible}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, {
|
||||||
import { Button, Input, Tag, Switch, message } from "antd";
|
useState,
|
||||||
|
useEffect,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Tag,
|
||||||
|
Switch,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Pagination,
|
||||||
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FilterOutlined,
|
FilterOutlined,
|
||||||
@@ -22,211 +35,259 @@ interface SensitiveWordItem {
|
|||||||
title: string;
|
title: string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
content: string;
|
content: string;
|
||||||
operation: string;
|
operation: number;
|
||||||
status: string;
|
status: number;
|
||||||
enabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SensitiveWordManagement: React.FC = () => {
|
const SensitiveWordManagement = forwardRef<any, Record<string, never>>(
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
(props, ref) => {
|
||||||
const [sensitiveWordsList, setSensitiveWordsList] = useState<
|
const [searchValue, setSearchValue] = useState<string>("");
|
||||||
SensitiveWordItem[]
|
const [sensitiveWordsList, setSensitiveWordsList] = useState<
|
||||||
>([]);
|
SensitiveWordItem[]
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
>([]);
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [editingSensitiveWordId, setEditingSensitiveWordId] = useState<
|
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||||
string | null
|
const [editingSensitiveWordId, setEditingSensitiveWordId] = useState<
|
||||||
>(null);
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const getTagColor = (tag: string) => {
|
const getTagColor = (tag: string) => {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case "政治":
|
case "政治":
|
||||||
return "#ff4d4f";
|
return "#ff4d4f";
|
||||||
case "色情":
|
case "色情":
|
||||||
return "#ff4d4f";
|
return "#ff4d4f";
|
||||||
case "暴力":
|
case "暴力":
|
||||||
return "#ff4d4f";
|
return "#ff4d4f";
|
||||||
default:
|
default:
|
||||||
return "#ff4d4f";
|
return "#ff4d4f";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 操作类型映射
|
// 操作类型映射
|
||||||
const getOperationText = (operation: string) => {
|
const getOperationText = (operation: number) => {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case "0":
|
case 0:
|
||||||
return "不操作";
|
return "不操作";
|
||||||
case "1":
|
case 1:
|
||||||
return "替换";
|
return "替换";
|
||||||
case "2":
|
case 2:
|
||||||
return "删除";
|
return "删除";
|
||||||
case "3":
|
case 3:
|
||||||
return "警告";
|
return "警告";
|
||||||
case "4":
|
case 4:
|
||||||
return "禁止发送";
|
return "禁止发送";
|
||||||
default:
|
default:
|
||||||
return "未知操作";
|
return "未知操作";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取敏感词列表
|
// 获取敏感词列表
|
||||||
const fetchSensitiveWords = async (params?: SensitiveWordListParams) => {
|
const fetchSensitiveWords = async (params?: SensitiveWordListParams) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getSensitiveWordList(params || {});
|
const requestParams = {
|
||||||
if (response) {
|
page: pagination.current.toString(),
|
||||||
setSensitiveWordsList(response.list || []);
|
limit: pagination.pageSize.toString(),
|
||||||
} else {
|
...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([]);
|
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) => {
|
useImperativeHandle(ref, () => ({
|
||||||
try {
|
fetchSensitiveWords,
|
||||||
const response = await setSensitiveWordStatus({ id });
|
}));
|
||||||
if (response) {
|
|
||||||
setSensitiveWordsList(prev =>
|
// 敏感词管理相关函数
|
||||||
prev.map(item =>
|
const handleToggleSensitiveWord = async (id: string) => {
|
||||||
item.id === id ? { ...item, enabled: !item.enabled } : item,
|
try {
|
||||||
),
|
const response = await setSensitiveWordStatus({ id });
|
||||||
);
|
if (response) {
|
||||||
message.success("状态更新成功");
|
setSensitiveWordsList(prev =>
|
||||||
} else {
|
prev.map(item =>
|
||||||
message.error(response?.message || "状态更新失败");
|
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) => {
|
const handleEditSensitiveWord = (id: string) => {
|
||||||
setEditingSensitiveWordId(id);
|
setEditingSensitiveWordId(id);
|
||||||
setEditModalVisible(true);
|
setEditModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑弹窗成功回调
|
// 编辑弹窗成功回调
|
||||||
const handleEditSuccess = () => {
|
const handleEditSuccess = () => {
|
||||||
fetchSensitiveWords(); // 重新获取数据
|
fetchSensitiveWords(); // 重新获取数据
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteSensitiveWord = async (id: string) => {
|
const handleDeleteSensitiveWord = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await deleteSensitiveWord(id);
|
await deleteSensitiveWord(id);
|
||||||
if (response) {
|
|
||||||
setSensitiveWordsList(prev => prev.filter(item => item.id !== id));
|
setSensitiveWordsList(prev => prev.filter(item => item.id !== id));
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
} else {
|
} catch (error) {
|
||||||
message.error(response?.message || "删除失败");
|
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 (
|
return (
|
||||||
item.title.toLowerCase().includes(searchValue.toLowerCase()) ||
|
<div className={styles.sensitiveContent}>
|
||||||
item.keywords.toLowerCase().includes(searchValue.toLowerCase()) ||
|
<div className={styles.searchSection}>
|
||||||
item.content.toLowerCase().includes(searchValue.toLowerCase())
|
<Search
|
||||||
);
|
placeholder="搜索敏感词..."
|
||||||
});
|
value={searchValue}
|
||||||
|
onChange={e => setSearchValue(e.target.value)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
/>
|
||||||
|
<Button icon={<FilterOutlined />}>筛选</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
// 搜索处理函数
|
<div className={styles.sensitiveList}>
|
||||||
const handleSearch = (value: string) => {
|
{loading ? (
|
||||||
fetchSensitiveWords({ keyword: value });
|
<div className={styles.loading}>加载中...</div>
|
||||||
};
|
) : filteredSensitiveWords.length === 0 ? (
|
||||||
|
<div className={styles.empty}>暂无敏感词数据</div>
|
||||||
// 组件挂载时获取数据
|
) : (
|
||||||
useEffect(() => {
|
filteredSensitiveWords.map(item => (
|
||||||
fetchSensitiveWords();
|
<div key={item.id} className={styles.sensitiveItem}>
|
||||||
}, []);
|
<div className={styles.itemContent}>
|
||||||
|
<div className={styles.categoryName}>{item.title}</div>
|
||||||
return (
|
<div className={styles.actionText}>
|
||||||
<div className={styles.sensitiveContent}>
|
{getOperationText(item.operation)}
|
||||||
<div className={styles.searchSection}>
|
</div>
|
||||||
<Search
|
</div>
|
||||||
placeholder="搜索敏感词..."
|
<div className={styles.itemActions}>
|
||||||
value={searchValue}
|
<Switch
|
||||||
onChange={e => setSearchValue(e.target.value)}
|
checked={item.status == 1}
|
||||||
onSearch={handleSearch}
|
onChange={() => handleToggleSensitiveWord(item.id)}
|
||||||
style={{ width: 300 }}
|
className={styles.toggleSwitch}
|
||||||
prefix={<SearchOutlined />}
|
/>
|
||||||
/>
|
<Button
|
||||||
<Button icon={<FilterOutlined />}>筛选</Button>
|
type="text"
|
||||||
</div>
|
size="small"
|
||||||
|
icon={<FormOutlined className={styles.editIcon} />}
|
||||||
<div className={styles.sensitiveList}>
|
onClick={() => handleEditSensitiveWord(item.id)}
|
||||||
{loading ? (
|
className={styles.actionBtn}
|
||||||
<div className={styles.loading}>加载中...</div>
|
/>
|
||||||
) : filteredSensitiveWords.length === 0 ? (
|
<Popconfirm
|
||||||
<div className={styles.empty}>暂无敏感词数据</div>
|
title="确认删除"
|
||||||
) : (
|
description="确定要删除这个敏感词吗?删除后无法恢复。"
|
||||||
filteredSensitiveWords.map(item => (
|
onConfirm={() => handleDeleteSensitiveWord(item.id)}
|
||||||
<div key={item.id} className={styles.sensitiveItem}>
|
okText="确定"
|
||||||
<div className={styles.itemContent}>
|
cancelText="取消"
|
||||||
<div className={styles.categoryName}>{item.title}</div>
|
okType="danger"
|
||||||
<Tag
|
>
|
||||||
color={getTagColor(item.keywords)}
|
<Button
|
||||||
className={styles.sensitiveTag}
|
type="text"
|
||||||
>
|
size="small"
|
||||||
{item.keywords}
|
icon={<DeleteOutlined className={styles.deleteIcon} />}
|
||||||
</Tag>
|
className={styles.actionBtn}
|
||||||
<div className={styles.actionText}>
|
/>
|
||||||
{getOperationText(item.operation)}
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.itemActions}>
|
))
|
||||||
<Switch
|
)}
|
||||||
checked={item.enabled}
|
</div>
|
||||||
onChange={() => handleToggleSensitiveWord(item.id)}
|
|
||||||
className={styles.toggleSwitch}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<FormOutlined className={styles.editIcon} />}
|
|
||||||
onClick={() => handleEditSensitiveWord(item.id)}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined className={styles.deleteIcon} />}
|
|
||||||
onClick={() => handleDeleteSensitiveWord(item.id)}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 分页组件 */}
|
||||||
<SensitiveWordModal
|
<div style={{ marginTop: 16, textAlign: "right" }}>
|
||||||
visible={editModalVisible}
|
<Pagination
|
||||||
mode="edit"
|
current={pagination.current}
|
||||||
sensitiveWordId={editingSensitiveWordId}
|
pageSize={pagination.pageSize}
|
||||||
onCancel={() => {
|
total={pagination.total}
|
||||||
setEditModalVisible(false);
|
showSizeChanger
|
||||||
setEditingSensitiveWordId(null);
|
showQuickJumper
|
||||||
}}
|
showTotal={(total, range) =>
|
||||||
onSuccess={handleEditSuccess}
|
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
||||||
/>
|
}
|
||||||
</div>
|
onChange={handlePageChange}
|
||||||
);
|
onShowSizeChange={handlePageChange}
|
||||||
};
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 编辑弹窗 */}
|
||||||
|
<SensitiveWordModal
|
||||||
|
visible={editModalVisible}
|
||||||
|
mode="edit"
|
||||||
|
sensitiveWordId={editingSensitiveWordId}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
setEditingSensitiveWordId(null);
|
||||||
|
}}
|
||||||
|
onSuccess={handleEditSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
SensitiveWordManagement.displayName = "SensitiveWordManagement";
|
||||||
|
|
||||||
export default SensitiveWordManagement;
|
export default SensitiveWordManagement;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,14 @@ 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;
|
||||||
|
|
||||||
interface KeywordModalProps {
|
interface KeywordModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
mode: "add" | "edit";
|
mode: "add" | "edit";
|
||||||
keywordId?: string | null;
|
keywordId?: number | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,12 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
||||||
|
|
||||||
// 获取关键词详情
|
// 获取关键词详情
|
||||||
const fetchKeywordDetails = useCallback(
|
const fetchKeywordDetails = useCallback(
|
||||||
async (id: string) => {
|
async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await getKeywordDetails(id);
|
const response = await getKeywordDetails(id);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -40,11 +42,13 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
title: keyword.title,
|
title: keyword.title,
|
||||||
keywords: keyword.keywords,
|
keywords: keyword.keywords,
|
||||||
content: keyword.content,
|
content: keyword.content,
|
||||||
matchType: keyword.matchType,
|
type: keyword.type,
|
||||||
priority: keyword.priority,
|
level: keyword.level,
|
||||||
replyType: keyword.replyType,
|
replyType: keyword.replyType,
|
||||||
status: keyword.status,
|
status: keyword.status,
|
||||||
|
metailGroups: keyword.metailGroups,
|
||||||
});
|
});
|
||||||
|
setSelectedOptions(keyword.metailGroupsOptions);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取关键词详情失败:", error);
|
console.error("获取关键词详情失败:", error);
|
||||||
@@ -63,6 +67,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]);
|
||||||
@@ -76,10 +81,11 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
title: values.title,
|
title: values.title,
|
||||||
keywords: values.keywords,
|
keywords: values.keywords,
|
||||||
content: values.content,
|
content: values.content,
|
||||||
matchType: values.matchType,
|
type: values.type,
|
||||||
priority: values.priority,
|
level: values.level,
|
||||||
replyType: values.replyType,
|
replyType: values.replyType,
|
||||||
status: values.status || "1",
|
status: values.status || "1",
|
||||||
|
metailGroups: values.metailGroups,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await addKeyword(data);
|
const response = await addKeyword(data);
|
||||||
@@ -97,10 +103,11 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
title: values.title,
|
title: values.title,
|
||||||
keywords: values.keywords,
|
keywords: values.keywords,
|
||||||
content: values.content,
|
content: values.content,
|
||||||
matchType: values.matchType,
|
type: values.type,
|
||||||
priority: values.priority,
|
level: values.level,
|
||||||
replyType: values.replyType,
|
replyType: values.replyType,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
|
metailGroups: values.metailGroups,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await updateKeyword(data);
|
const response = await updateKeyword(data);
|
||||||
@@ -123,11 +130,42 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedOptions([]);
|
||||||
onCancel();
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={title}
|
title={title}
|
||||||
@@ -140,11 +178,12 @@ 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,
|
||||||
matchType: "模糊匹配",
|
type: "模糊匹配",
|
||||||
priority: "1",
|
level: 1,
|
||||||
replyType: "text",
|
replyType: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -163,47 +202,60 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
<Input placeholder="请输入关键词" />
|
<Input placeholder="请输入关键词" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="回复内容"
|
|
||||||
rules={[{ required: true, message: "请输入回复内容" }]}
|
|
||||||
>
|
|
||||||
<TextArea rows={4} placeholder="请输入回复内容" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="matchType"
|
|
||||||
label="匹配类型"
|
|
||||||
rules={[{ required: true, message: "请选择匹配类型" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择匹配类型">
|
|
||||||
<Option value="模糊匹配">模糊匹配</Option>
|
|
||||||
<Option value="精确匹配">精确匹配</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="priority"
|
|
||||||
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>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="replyType"
|
name="replyType"
|
||||||
label="回复类型"
|
label="回复类型"
|
||||||
rules={[{ required: true, message: "请选择回复类型" }]}
|
rules={[{ required: true, message: "请选择回复类型" }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择回复类型">
|
<Select placeholder="请选择回复类型">
|
||||||
<Option value="text">文本回复</Option>
|
<Option value={0}>素材回复</Option>
|
||||||
<Option value="template">模板回复</Option>
|
<Option value={1}>自定义</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{form.getFieldValue("replyType") === 1 ? (
|
||||||
|
<Form.Item
|
||||||
|
name="content"
|
||||||
|
label="回复内容"
|
||||||
|
rules={[{ required: true, message: "请输入回复内容" }]}
|
||||||
|
>
|
||||||
|
<TextArea rows={4} placeholder="请输入回复内容" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name="metailGroups"
|
||||||
|
label="回复内容"
|
||||||
|
rules={[{ required: true, message: "请输入回复内容" }]}
|
||||||
|
>
|
||||||
|
<MetailSelection
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
onSelect={handSelectMaterial}
|
||||||
|
selectionMode="single"
|
||||||
|
placeholder="选择素材"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="type"
|
||||||
|
label="匹配类型"
|
||||||
|
rules={[{ required: true, message: "请选择匹配类型" }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择匹配类型">
|
||||||
|
<Option value={0}>模糊匹配</Option>
|
||||||
|
<Option value={1}>精确匹配</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="level"
|
||||||
|
label="优先级"
|
||||||
|
rules={[{ required: true, message: "请选择优先级" }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择优先级">
|
||||||
|
<Option value={0}>低优先级</Option>
|
||||||
|
<Option value={1}>中优先级</Option>
|
||||||
|
<Option value={2}>高优先级</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -213,8 +265,8 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||||||
rules={[{ required: true, message: "请选择状态" }]}
|
rules={[{ required: true, message: "请选择状态" }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择状态">
|
<Select placeholder="请选择状态">
|
||||||
<Option value="1">启用</Option>
|
<Option value={1}>启用</Option>
|
||||||
<Option value="0">禁用</Option>
|
<Option value={0}>禁用</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
import { Modal, Form, Input, Button, message, Select } from "antd";
|
||||||
import {
|
import {
|
||||||
addSensitiveWord,
|
addSensitiveWord,
|
||||||
@@ -30,34 +30,40 @@ const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// 获取敏感词详情
|
// 获取敏感词详情
|
||||||
const fetchSensitiveWordDetails = async (id: string) => {
|
const fetchSensitiveWordDetails = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
const response = await getSensitiveWordDetails(id);
|
try {
|
||||||
if (response) {
|
const response = await getSensitiveWordDetails(id);
|
||||||
const sensitiveWord = response;
|
if (response) {
|
||||||
form.setFieldsValue({
|
const sensitiveWord = response;
|
||||||
title: sensitiveWord.title,
|
form.setFieldsValue({
|
||||||
keywords: sensitiveWord.keywords,
|
title: sensitiveWord.title,
|
||||||
content: sensitiveWord.content,
|
keywords: sensitiveWord.keywords,
|
||||||
operation: sensitiveWord.operation,
|
content: sensitiveWord.content,
|
||||||
status: sensitiveWord.status,
|
operation: sensitiveWord.operation,
|
||||||
});
|
status: sensitiveWord.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取敏感词详情失败:", error);
|
||||||
|
message.error("获取敏感词详情失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error("获取敏感词详情失败:", error);
|
[form],
|
||||||
message.error("获取敏感词详情失败");
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当弹窗打开且为编辑模式时,获取详情
|
// 当弹窗打开时处理数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && mode === "edit" && sensitiveWordId) {
|
if (visible) {
|
||||||
fetchSensitiveWordDetails(sensitiveWordId);
|
if (mode === "edit" && sensitiveWordId) {
|
||||||
} else if (visible && mode === "add") {
|
// 编辑模式:获取详情
|
||||||
// 添加模式时重置表单
|
fetchSensitiveWordDetails(sensitiveWordId);
|
||||||
form.resetFields();
|
} else if (mode === "add") {
|
||||||
|
// 添加模式:重置表单
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [visible, mode, sensitiveWordId]);
|
}, [visible, mode, sensitiveWordId, fetchSensitiveWordDetails, form]);
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
const handleSubmit = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -160,11 +166,11 @@ const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
|
|||||||
rules={[{ required: true, message: "请选择操作类型" }]}
|
rules={[{ required: true, message: "请选择操作类型" }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择操作类型">
|
<Select placeholder="请选择操作类型">
|
||||||
<Option value="0">不操作</Option>
|
<Option value={0}>不操作</Option>
|
||||||
<Option value="1">替换</Option>
|
<Option value={1}>替换</Option>
|
||||||
<Option value="2">删除</Option>
|
<Option value={2}>删除</Option>
|
||||||
<Option value="3">警告</Option>
|
<Option value={3}>警告</Option>
|
||||||
<Option value="4">禁止发送</Option>
|
<Option value={4}>禁止发送</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -174,8 +180,8 @@ const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
|
|||||||
rules={[{ required: true, message: "请选择状态" }]}
|
rules={[{ required: true, message: "请选择状态" }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择状态">
|
<Select placeholder="请选择状态">
|
||||||
<Option value="1">启用</Option>
|
<Option value={1}>启用</Option>
|
||||||
<Option value="0">禁用</Option>
|
<Option value={0}>禁用</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
:global(.material-cover-upload) {
|
||||||
.uploadContainer {
|
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const ContentManagement: React.FC = () => {
|
|||||||
// 引用管理组件
|
// 引用管理组件
|
||||||
const materialManagementRef = useRef<any>(null);
|
const materialManagementRef = useRef<any>(null);
|
||||||
const keywordManagementRef = useRef<any>(null);
|
const keywordManagementRef = useRef<any>(null);
|
||||||
|
const sensitiveWordManagementRef = useRef<any>(null);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: "material", label: "素材资源库" },
|
{ key: "material", label: "素材资源库" },
|
||||||
@@ -44,12 +45,20 @@ const ContentManagement: React.FC = () => {
|
|||||||
|
|
||||||
// 弹窗成功回调
|
// 弹窗成功回调
|
||||||
const handleModalSuccess = () => {
|
const handleModalSuccess = () => {
|
||||||
|
console.log("handleModalSuccess");
|
||||||
// 刷新素材列表
|
// 刷新素材列表
|
||||||
if (materialManagementRef.current?.fetchMaterials) {
|
if (materialManagementRef.current?.fetchMaterials) {
|
||||||
|
console.log("刷新素材列表");
|
||||||
materialManagementRef.current.fetchMaterials();
|
materialManagementRef.current.fetchMaterials();
|
||||||
}
|
}
|
||||||
|
// 刷新敏感词列表
|
||||||
|
if (sensitiveWordManagementRef.current?.fetchSensitiveWords) {
|
||||||
|
console.log("刷新敏感词列表");
|
||||||
|
sensitiveWordManagementRef.current.fetchSensitiveWords();
|
||||||
|
}
|
||||||
// 刷新关键词列表
|
// 刷新关键词列表
|
||||||
if (keywordManagementRef.current?.fetchKeywords) {
|
if (keywordManagementRef.current?.fetchKeywords) {
|
||||||
|
console.log("刷新关键词列表");
|
||||||
keywordManagementRef.current.fetchKeywords();
|
keywordManagementRef.current.fetchKeywords();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -61,7 +70,12 @@ const ContentManagement: React.FC = () => {
|
|||||||
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
||||||
);
|
);
|
||||||
case "sensitive":
|
case "sensitive":
|
||||||
return <SensitiveWordManagement />;
|
return (
|
||||||
|
<SensitiveWordManagement
|
||||||
|
ref={sensitiveWordManagementRef}
|
||||||
|
{...({} as any)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "keyword":
|
case "keyword":
|
||||||
return (
|
return (
|
||||||
<KeywordManagement ref={keywordManagementRef} {...({} as any)} />
|
<KeywordManagement ref={keywordManagementRef} {...({} as any)} />
|
||||||
@@ -89,12 +103,12 @@ const ContentManagement: React.FC = () => {
|
|||||||
>
|
>
|
||||||
添加素材
|
添加素材
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddKeyword}>
|
|
||||||
添加关键词回复
|
|
||||||
</Button>
|
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddSensitiveWord}>
|
<Button icon={<PlusOutlined />} onClick={handleAddSensitiveWord}>
|
||||||
添加敏感词
|
添加敏感词
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddKeyword}>
|
||||||
|
添加关键词回复
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -136,6 +150,7 @@ const ContentManagement: React.FC = () => {
|
|||||||
<KeywordModal
|
<KeywordModal
|
||||||
visible={keywordModalVisible}
|
visible={keywordModalVisible}
|
||||||
mode="add"
|
mode="add"
|
||||||
|
keywordId={null}
|
||||||
onCancel={() => setKeywordModalVisible(false)}
|
onCancel={() => setKeywordModalVisible(false)}
|
||||||
onSuccess={handleModalSuccess}
|
onSuccess={handleModalSuccess}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -114,9 +114,9 @@
|
|||||||
|
|
||||||
// 标签页
|
// 标签页
|
||||||
.tabs {
|
.tabs {
|
||||||
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
gap: 10px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
@@ -127,15 +127,6 @@
|
|||||||
color: #8c8c8c;
|
color: #8c8c8c;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.activeTab {
|
|
||||||
color: #1890ff;
|
|
||||||
border-bottom-color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import PowerNavigation from "@/components/PowerNavtion";
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import {
|
import { SearchOutlined, FilterOutlined } from "@ant-design/icons";
|
||||||
SearchOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
PhoneOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { Button, Input, Row, Col, Pagination, Spin, message } from "antd";
|
import { Button, Input, Table, message } from "antd";
|
||||||
import { getContactList } from "@/pages/pc/ckbox/weChat/api";
|
import { getContactList } from "@/pages/pc/ckbox/weChat/api";
|
||||||
import { ContractData } from "@/pages/pc/ckbox/data";
|
import { ContractData } from "@/pages/pc/ckbox/data";
|
||||||
import Layout from "@/components/Layout/LayoutFiexd";
|
import Layout from "@/components/Layout/LayoutFiexd";
|
||||||
@@ -196,132 +191,129 @@ const CustomerManagement: React.FC = () => {
|
|||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* 标签页 */}
|
{/* 标签按钮组 */}
|
||||||
<div className={styles.tabs}>
|
<div className={styles.tabs}>
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button
|
<Button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={`${styles.tab} ${activeTab === tab.key ? styles.activeTab : ""}`}
|
type={activeTab === tab.key ? "primary" : "default"}
|
||||||
onClick={() => setActiveTab(tab.key)}
|
onClick={() => setActiveTab(tab.key)}
|
||||||
>
|
>
|
||||||
{tab.label} ({tab.count})
|
{tab.label}
|
||||||
</button>
|
<span style={{ marginLeft: 6, opacity: 0.85 }}>
|
||||||
|
{tab.count}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
footer={
|
footer={null}
|
||||||
<div className="pagination-wrapper">
|
|
||||||
<Pagination
|
|
||||||
current={pagination.current}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
total={pagination.total}
|
|
||||||
showSizeChanger
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total, range) =>
|
|
||||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
|
||||||
}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
loadContacts(page, pageSize || pagination.pageSize);
|
|
||||||
}}
|
|
||||||
onShowSizeChange={(current, size) => {
|
|
||||||
loadContacts(1, size);
|
|
||||||
}}
|
|
||||||
pageSizeOptions={["6", "12", "24", "48"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 联系人卡片列表 */}
|
{/* 联系人表格 */}
|
||||||
<div className={styles.contactsList}>
|
<Table
|
||||||
{loading ? (
|
rowKey={(record: any) => record.id || record.serverId}
|
||||||
<div style={{ textAlign: "center", padding: "50px" }}>
|
loading={loading}
|
||||||
<Spin size="large" />
|
dataSource={filteredContacts as any}
|
||||||
<p style={{ marginTop: "16px", color: "#666" }}>
|
columns={[
|
||||||
正在加载联系人数据...
|
{
|
||||||
</p>
|
title: "客户姓名",
|
||||||
</div>
|
key: "name",
|
||||||
) : filteredContacts.length === 0 ? (
|
render: (_: any, record: any) => {
|
||||||
<div style={{ textAlign: "center", padding: "50px" }}>
|
const displayName =
|
||||||
<p style={{ color: "#999" }}>暂无联系人数据</p>
|
record.conRemark ||
|
||||||
</div>
|
record.nickname ||
|
||||||
) : (
|
record.alias ||
|
||||||
<>
|
"未知用户";
|
||||||
<Row gutter={[16, 16]}>
|
return (
|
||||||
{filteredContacts.map(contact => (
|
<div className={styles.contactInfo}>
|
||||||
<Col span={8} key={contact.id || contact.serverId}>
|
<Avatar
|
||||||
<div className={styles.contactCard}>
|
name={displayName}
|
||||||
<div className={styles.cardHeader}>
|
avatar={record.avatar}
|
||||||
<div className={styles.contactInfo}>
|
size={40}
|
||||||
<Avatar
|
/>
|
||||||
name={
|
<div className={styles.nameSection}>
|
||||||
contact.conRemark ||
|
<h3 className={styles.contactName}>{displayName}</h3>
|
||||||
contact.nickname ||
|
<p className={styles.roleCompany}>
|
||||||
contact.alias ||
|
客户 · {record.desc || "未设置公司"}
|
||||||
"未知用户"
|
</p>
|
||||||
}
|
|
||||||
avatar={contact.avatar}
|
|
||||||
size={48}
|
|
||||||
/>
|
|
||||||
<div className={styles.nameSection}>
|
|
||||||
<h3 className={styles.contactName}>
|
|
||||||
{contact.conRemark ||
|
|
||||||
contact.nickname ||
|
|
||||||
contact.alias ||
|
|
||||||
"未知用户"}
|
|
||||||
</h3>
|
|
||||||
<p className={styles.roleCompany}>
|
|
||||||
客户 {"·"} {contact.desc || "未设置公司"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.contactDetails}>
|
|
||||||
<div className={styles.contactInfo}>
|
|
||||||
<p className={styles.contactItem}>
|
|
||||||
<span className={styles.label}>电话:</span>{" "}
|
|
||||||
{contact.phone || "未设置电话"}
|
|
||||||
</p>
|
|
||||||
<p className={styles.contactItem}>
|
|
||||||
<span className={styles.label}>地区:</span>{" "}
|
|
||||||
{contact.region || contact.city || "未设置地区"}
|
|
||||||
</p>
|
|
||||||
<p className={styles.contactItem}>
|
|
||||||
<span className={styles.label}>微信ID:</span>{" "}
|
|
||||||
{contact.wechatId}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.tagsSection}>
|
|
||||||
<div className={styles.tags}>
|
|
||||||
{contact?.labels?.map(
|
|
||||||
(tag: string, index: number) => (
|
|
||||||
<span key={index} className={styles.tag}>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button type="primary" block>
|
|
||||||
<MessageOutlined />
|
|
||||||
聊天
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</div>
|
||||||
))}
|
);
|
||||||
</Row>
|
},
|
||||||
</>
|
},
|
||||||
)}
|
{
|
||||||
</div>
|
title: "RFM评分",
|
||||||
|
dataIndex: "rfmScore",
|
||||||
|
key: "rfmScore",
|
||||||
|
width: 100,
|
||||||
|
render: (val: any) => val ?? "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "电话",
|
||||||
|
dataIndex: "phone",
|
||||||
|
key: "phone",
|
||||||
|
width: 180,
|
||||||
|
render: (val: string) => val || "未设置电话",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "微信号",
|
||||||
|
dataIndex: "wechatId",
|
||||||
|
key: "wechatId",
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "地址",
|
||||||
|
key: "address",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_: any, record: any) =>
|
||||||
|
record.region || record.city || "未设置地区",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "标签",
|
||||||
|
key: "labels",
|
||||||
|
render: (_: any, record: any) => (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{(record?.labels || []).map(
|
||||||
|
(tag: string, index: number) => (
|
||||||
|
<span key={index} className={styles.tag}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
width: 120,
|
||||||
|
render: () => (
|
||||||
|
<Button type="primary" size="small">
|
||||||
|
聊天
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pagination={{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: pagination.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number, range: [number, number]) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
|
pageSizeOptions: ["6", "12", "24", "48"],
|
||||||
|
}}
|
||||||
|
onChange={(pager: any) => {
|
||||||
|
const nextCurrent = pager.current || 1;
|
||||||
|
const nextSize = pager.pageSize || pagination.pageSize;
|
||||||
|
loadContacts(nextCurrent, nextSize);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { TeamOutlined, CommentOutlined, BookOutlined } from "@ant-design/icons";
|
||||||
AimOutlined,
|
|
||||||
ThunderboltOutlined,
|
|
||||||
RiseOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
CommentOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
SoundOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
|
|
||||||
// 数据类型定义
|
// 数据类型定义
|
||||||
export interface FeatureCard {
|
export interface FeatureCard {
|
||||||
@@ -16,89 +7,97 @@ export interface FeatureCard {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
color: string;
|
color: string;
|
||||||
|
tag: string;
|
||||||
|
features: string[];
|
||||||
path?: string;
|
path?: string;
|
||||||
isNew?: boolean;
|
|
||||||
isHot?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeatureCategory {
|
export interface KPIData {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
value: string;
|
||||||
icon: React.ReactNode;
|
label: string;
|
||||||
color: string;
|
trend?: {
|
||||||
count: number;
|
icon: string;
|
||||||
features: FeatureCard[];
|
text: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 功能数据
|
// 功能数据 - 匹配图片中的三个核心模块
|
||||||
export const featureCategories: FeatureCategory[] = [
|
export const featureCategories: FeatureCard[] = [
|
||||||
{
|
{
|
||||||
id: "core",
|
id: "customer-management",
|
||||||
title: "核心功能",
|
title: "客户好友管理",
|
||||||
icon: <AimOutlined style={{ fontSize: "24px" }} />,
|
description: "管理客户关系,维护好友信息,查看沟通记录,提升客户满意度",
|
||||||
|
icon: <TeamOutlined style={{ fontSize: "32px", color: "#1890ff" }} />,
|
||||||
color: "#1890ff",
|
color: "#1890ff",
|
||||||
count: 2,
|
tag: "核心功能",
|
||||||
features: [
|
features: [
|
||||||
{
|
"RFM价值评分系统",
|
||||||
id: "customer-management",
|
"多维度精准筛选",
|
||||||
title: "客户好友管理",
|
"完整聊天记录查看",
|
||||||
description: "管理客户关系,维护好友信息,提升客户满意度",
|
"客户详情页面",
|
||||||
icon: <TeamOutlined style={{ fontSize: "24px" }} />,
|
|
||||||
color: "#1890ff",
|
|
||||||
path: "/pc/powerCenter/customer-management",
|
|
||||||
isHot: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "communication-record",
|
|
||||||
title: "沟通记录",
|
|
||||||
description: "记录和分析所有客户沟通历史,优化服务质量",
|
|
||||||
icon: <CommentOutlined style={{ fontSize: "24px" }} />,
|
|
||||||
color: "#52c41a",
|
|
||||||
path: "/pc/powerCenter/communication-record",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
path: "/pc/powerCenter/customer-management",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ai",
|
id: "ai-reception",
|
||||||
title: "AI智能功能",
|
title: "AI接待设置",
|
||||||
icon: <ThunderboltOutlined style={{ fontSize: "24px" }} />,
|
description: "配置AI自动回复,智能推送策略,提升接待效率和客户体验",
|
||||||
|
icon: <CommentOutlined style={{ fontSize: "32px", color: "#722ed1" }} />,
|
||||||
color: "#722ed1",
|
color: "#722ed1",
|
||||||
count: 2,
|
tag: "AI智能",
|
||||||
features: [
|
features: [
|
||||||
{
|
"自动欢迎语设置",
|
||||||
id: "ai-training",
|
"AI智能推送策略",
|
||||||
title: "AI模型训练",
|
"标签化精准推送",
|
||||||
description: "训练专属AI模型,提升智能服务能力",
|
"接待模式切换",
|
||||||
icon: <FileTextOutlined style={{ fontSize: "24px" }} />,
|
|
||||||
color: "#fa8c16",
|
|
||||||
path: "/pc/powerCenter/ai-training",
|
|
||||||
isNew: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "auto-greeting",
|
|
||||||
title: "自动问候",
|
|
||||||
description: "设置智能问候规则,自动化客户接待流程",
|
|
||||||
icon: <SoundOutlined style={{ fontSize: "24px" }} />,
|
|
||||||
color: "#722ed1",
|
|
||||||
path: "/pc/powerCenter/auto-greeting",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
path: "/pc/powerCenter/ai-reception",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "marketing",
|
id: "content-library",
|
||||||
title: "营销管理",
|
title: "AI内容库配置",
|
||||||
icon: <RiseOutlined style={{ fontSize: "24px" }} />,
|
description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量",
|
||||||
|
icon: <BookOutlined style={{ fontSize: "32px", color: "#52c41a" }} />,
|
||||||
color: "#52c41a",
|
color: "#52c41a",
|
||||||
count: 1,
|
tag: "内容管理",
|
||||||
features: [
|
features: [
|
||||||
{
|
"多库管理与分类",
|
||||||
id: "content-management",
|
"AI调用权限配置",
|
||||||
title: "内容管理",
|
"内容检索规则设置",
|
||||||
description: "管理营销内容,素材库,提升内容创作效率",
|
"手动内容上传",
|
||||||
icon: <EditOutlined style={{ fontSize: "24px" }} />,
|
|
||||||
color: "#722ed1",
|
|
||||||
path: "/pc/powerCenter/content-management",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
path: "/pc/powerCenter/content-library",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// KPI统计数据
|
||||||
|
export const kpiData: KPIData[] = [
|
||||||
|
{
|
||||||
|
id: "total-customers",
|
||||||
|
value: "1,234",
|
||||||
|
label: "总客户数",
|
||||||
|
trend: {
|
||||||
|
icon: "↑",
|
||||||
|
text: "12% 本月",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "active-customers",
|
||||||
|
value: "856",
|
||||||
|
label: "活跃客户",
|
||||||
|
trend: {
|
||||||
|
icon: "↑",
|
||||||
|
text: "8% 本月",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "assigned-users",
|
||||||
|
value: "342",
|
||||||
|
label: "当前客服分配用户数",
|
||||||
|
trend: {
|
||||||
|
icon: "",
|
||||||
|
text: "当前登录客服",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,160 +1,188 @@
|
|||||||
.powerCenter {
|
.powerCenter {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
background: #ffffff;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
// 功能分类区域
|
// 页面标题区域
|
||||||
.categorySection {
|
.pageHeader {
|
||||||
margin-bottom: 48px;
|
text-align: center;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
|
||||||
.categoryHeader {
|
.titleSection {
|
||||||
display: flex;
|
.mainTitle {
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
.categoryIcon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 16px;
|
gap: 16px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
margin-bottom: 16px;
|
||||||
|
|
||||||
.anticon {
|
.titleIcon {
|
||||||
font-size: 24px;
|
font-size: 32px;
|
||||||
color: white;
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryInfo {
|
.subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心功能模块
|
||||||
|
.coreFeatures {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
|
||||||
|
.featureCard {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 400px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 32px;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 10px;
|
|
||||||
.categoryTitle {
|
.cardHeader {
|
||||||
font-size: 24px;
|
display: flex;
|
||||||
font-weight: 600;
|
justify-content: space-between;
|
||||||
color: #1a1a1a;
|
align-items: flex-start;
|
||||||
margin: 0 0 4px 0;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.cardIcon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTag {
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryCount {
|
.cardInfo {
|
||||||
font-size: 12px;
|
flex: 1;
|
||||||
color: #666;
|
display: flex;
|
||||||
background: #f0f0f0;
|
flex-direction: column;
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #e0e0e0;
|
.cardTitle {
|
||||||
height: 24px;
|
font-size: 20px;
|
||||||
line-height: 20px;
|
font-weight: 600;
|
||||||
padding: 0 10px;
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardDescription {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "•";
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.featureCards {
|
// KPI统计区域
|
||||||
.featureCard {
|
.kpiSection {
|
||||||
border-radius: 16px;
|
margin-bottom: 40px;
|
||||||
border: none;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
.kpiCard {
|
||||||
transform: translateY(-2px);
|
background: white;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpiValue {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpiLabel {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpiTrend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.trendIcon {
|
||||||
|
color: #52c41a;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContent {
|
.trendText {
|
||||||
.cardHeader {
|
font-size: 12px;
|
||||||
position: relative;
|
color: #52c41a;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
.cardIcon {
|
|
||||||
color: #ffffff;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
.anticon {
|
|
||||||
font-size: 24px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
background: #ff6b35;
|
|
||||||
color: white;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0 1px 4px rgba(255, 107, 53, 0.3);
|
|
||||||
height: 24px;
|
|
||||||
// 新功能标签样式
|
|
||||||
&[data-type="new"] {
|
|
||||||
background: #52c41a;
|
|
||||||
box-shadow: 0 1px 4px rgba(82, 196, 26, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardInfo {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a1a;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDescription {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardAction {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: #979797;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 14px;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .arrow {
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,63 +208,65 @@
|
|||||||
.powerCenter {
|
.powerCenter {
|
||||||
padding: 32px 24px;
|
padding: 32px 24px;
|
||||||
|
|
||||||
.categorySection {
|
.pageHeader {
|
||||||
.categoryHeader {
|
margin-bottom: 40px;
|
||||||
.categoryIcon {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
|
|
||||||
.anticon {
|
.titleSection {
|
||||||
font-size: 22px;
|
.mainTitle {
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleIcon {
|
||||||
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryInfo {
|
.subtitle {
|
||||||
.categoryTitle {
|
font-size: 16px;
|
||||||
font-size: 22px;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coreFeatures {
|
||||||
|
.featureCard {
|
||||||
|
height: 360px;
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
.cardIcon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardInfo {
|
||||||
|
.cardTitle {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardDescription {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureList {
|
||||||
|
li {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.featureCards {
|
.kpiSection {
|
||||||
.featureCard {
|
.kpiCard {
|
||||||
height: 180px;
|
padding: 20px;
|
||||||
width: 260px;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.cardContent {
|
.kpiValue {
|
||||||
.cardIcon {
|
font-size: 28px;
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
margin: 18px auto 14px;
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardInfo {
|
|
||||||
padding: 0 14px 14px;
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
font-size: 15px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDescription {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardAction {
|
|
||||||
font-size: 11px;
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,76 +277,89 @@
|
|||||||
.powerCenter {
|
.powerCenter {
|
||||||
padding: 24px 16px;
|
padding: 24px 16px;
|
||||||
|
|
||||||
.categorySection {
|
.pageHeader {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
.categoryHeader {
|
.titleSection {
|
||||||
.categoryIcon {
|
.mainTitle {
|
||||||
width: 40px;
|
flex-direction: column;
|
||||||
height: 40px;
|
gap: 8px;
|
||||||
|
|
||||||
.anticon {
|
h1 {
|
||||||
font-size: 20px;
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleIcon {
|
||||||
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryInfo {
|
.subtitle {
|
||||||
.categoryTitle {
|
font-size: 14px;
|
||||||
font-size: 20px;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coreFeatures {
|
||||||
|
.featureCard {
|
||||||
|
height: 320px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.cardIcon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryCount {
|
.cardInfo {
|
||||||
font-size: 12px;
|
.cardTitle {
|
||||||
padding: 3px 10px;
|
font-size: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardDescription {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureList {
|
||||||
|
li {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.featureCards {
|
.kpiSection {
|
||||||
.featureCard {
|
.kpiCard {
|
||||||
height: 160px;
|
padding: 16px;
|
||||||
width: 240px;
|
margin-bottom: 12px;
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.cardContent {
|
.kpiValue {
|
||||||
.badge {
|
font-size: 24px;
|
||||||
top: 10px;
|
}
|
||||||
right: 10px;
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardIcon {
|
.kpiLabel {
|
||||||
width: 40px;
|
font-size: 12px;
|
||||||
height: 40px;
|
}
|
||||||
margin: 16px auto 12px;
|
|
||||||
|
|
||||||
.anticon {
|
.kpiTrend {
|
||||||
font-size: 20px;
|
.trendText {
|
||||||
}
|
font-size: 11px;
|
||||||
}
|
|
||||||
|
|
||||||
.cardInfo {
|
|
||||||
padding: 0 12px 12px;
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDescription {
|
|
||||||
font-size: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardAction {
|
|
||||||
font-size: 10px;
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { FeatureCard, featureCategories } from "./index.data.tsx";
|
import { FeatureCard, featureCategories, kpiData } from "./index.data";
|
||||||
import { Col, Row } from "antd";
|
import { Col, Row } from "antd";
|
||||||
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
const PowerCenter: React.FC = () => {
|
const PowerCenter: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const getKpiBg = (id: string) => {
|
||||||
|
if (id === "total-customers") return "#1890ff";
|
||||||
|
if (id === "active-customers") return "#52c41a";
|
||||||
|
return "#722ed1";
|
||||||
|
};
|
||||||
|
|
||||||
const handleCardClick = (card: FeatureCard) => {
|
const handleCardClick = (card: FeatureCard) => {
|
||||||
if (card.path) {
|
if (card.path) {
|
||||||
@@ -14,79 +25,127 @@ const PowerCenter: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.powerCenter}>
|
<div className={styles.powerCenter}>
|
||||||
{/* 功能分类展示 */}
|
{/* 页面标题区域 */}
|
||||||
{featureCategories.map(category => (
|
<div className={styles.pageHeader}>
|
||||||
<div key={category.id} className={styles.categorySection}>
|
<div className={styles.titleSection}>
|
||||||
{/* 分类标题 */}
|
<div className={styles.mainTitle}>
|
||||||
<div className={styles.categoryHeader}>
|
<div className={styles.titleIcon}>⭐</div>
|
||||||
<div
|
<h1>功能中心</h1>
|
||||||
className={styles.categoryIcon}
|
|
||||||
style={{ backgroundColor: category.color }}
|
|
||||||
>
|
|
||||||
{category.icon}
|
|
||||||
</div>
|
|
||||||
<div className={styles.categoryInfo}>
|
|
||||||
<h2 className={styles.categoryTitle}>{category.title}</h2>
|
|
||||||
<span
|
|
||||||
className={styles.categoryCount}
|
|
||||||
style={{ backgroundColor: category.color, color: "#ffffff" }}
|
|
||||||
>
|
|
||||||
{category.count}个功能
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
AI智能营销·一站式客户管理·高效业务增长
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 功能卡片 */}
|
{/* KPI统计区域(置顶,按图展示) */}
|
||||||
<div className={styles.featureCards}>
|
<div className={styles.kpiSection}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
{category.features.map(card => (
|
{kpiData.map(kpi => (
|
||||||
<Col span={8} key={card.id}>
|
<Col span={8} key={kpi.id}>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={styles.kpiLabel}
|
||||||
|
style={{ textAlign: "left", marginBottom: 6 }}
|
||||||
|
>
|
||||||
|
{kpi.label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.kpiValue}
|
||||||
|
style={{ textAlign: "left", marginBottom: 6 }}
|
||||||
|
>
|
||||||
|
{kpi.value}
|
||||||
|
</div>
|
||||||
|
{kpi.trend && (
|
||||||
|
<div
|
||||||
|
className={styles.kpiTrend}
|
||||||
|
style={{ justifyContent: "flex-start" }}
|
||||||
|
>
|
||||||
|
<span className={styles.trendIcon}>
|
||||||
|
{kpi.trend.icon}
|
||||||
|
</span>
|
||||||
|
<span className={styles.trendText}>
|
||||||
|
{kpi.trend.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
aria-hidden
|
||||||
className={styles.featureCard}
|
style={{
|
||||||
onClick={() => handleCardClick(card)}
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 12,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: getKpiBg(kpi.id),
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 6px 14px rgba(0,0,0,0.18)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.cardContent}>
|
{kpi.id === "total-customers" && (
|
||||||
<div className={styles.cardHeader}>
|
<UserOutlined style={{ fontSize: 22 }} />
|
||||||
<div
|
)}
|
||||||
className={styles.cardIcon}
|
{kpi.id === "active-customers" && (
|
||||||
style={{ backgroundColor: card.color }}
|
<TeamOutlined style={{ fontSize: 22 }} />
|
||||||
>
|
)}
|
||||||
{card.icon}
|
{kpi.id !== "total-customers" &&
|
||||||
</div>
|
kpi.id !== "active-customers" && (
|
||||||
{/* 热门/新功能标签 */}
|
<UsergroupAddOutlined style={{ fontSize: 22 }} />
|
||||||
{(card.isHot || card.isNew) && (
|
)}
|
||||||
<div
|
</div>
|
||||||
className={styles.badge}
|
</div>
|
||||||
data-type={card.isNew ? "new" : "hot"}
|
</div>
|
||||||
>
|
</Col>
|
||||||
{card.isHot ? "热门" : "新功能"}
|
))}
|
||||||
</div>
|
</Row>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* 功能图标 */}
|
{/* 核心功能模块 */}
|
||||||
</div>
|
<div className={styles.coreFeatures}>
|
||||||
|
<Row gutter={24}>
|
||||||
{/* 功能信息 */}
|
{featureCategories.map(card => (
|
||||||
<div className={styles.cardInfo}>
|
<Col span={8} key={card.id}>
|
||||||
<h3 className={styles.cardTitle}>{card.title}</h3>
|
<div
|
||||||
<p className={styles.cardDescription}>
|
className={styles.featureCard}
|
||||||
{card.description}
|
onClick={() => handleCardClick(card)}
|
||||||
</p>
|
>
|
||||||
<div className={styles.cardAction}>
|
<div className={styles.cardContent}>
|
||||||
<span>点击进入功能</span>
|
<div className={styles.cardHeader}>
|
||||||
<span className={styles.arrow}>→</span>
|
<div className={styles.cardIcon}>{card.icon}</div>
|
||||||
</div>
|
<div
|
||||||
</div>
|
className={styles.cardTag}
|
||||||
|
style={{ backgroundColor: card.color }}
|
||||||
|
>
|
||||||
|
{card.tag}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
|
<div className={styles.cardInfo}>
|
||||||
|
<h3 className={styles.cardTitle}>{card.title}</h3>
|
||||||
|
<p className={styles.cardDescription}>{card.description}</p>
|
||||||
|
|
||||||
|
<ul className={styles.featureList}>
|
||||||
|
{card.features.map((feature, index) => (
|
||||||
|
<li key={index}>{feature}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
{/* 页面底部 */}
|
{/* 页面底部 */}
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<p>触客宝 AI私域营销系统 - 让每一次沟通都更有价值</p>
|
<p>触客宝 AI私域营销系统 - 让每一次沟通都更有价值</p>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.chatRecordSearch {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
.searchContentContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
.timeRange {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.timeRangeTitle {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.searchContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.searchContentTitle {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button, Input, DatePicker, message } from "antd";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { CloseOutlined } from "@ant-design/icons";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
const ChatRecordSearch: React.FC = () => {
|
||||||
|
const [searchContent, setSearchContent] = useState<string>("");
|
||||||
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const SearchMessage = useWeChatStore(state => state.SearchMessage);
|
||||||
|
const updateShowChatRecordModel = useWeChatStore(
|
||||||
|
state => state.updateShowChatRecordModel,
|
||||||
|
);
|
||||||
|
// 执行查找
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!dateRange) {
|
||||||
|
message.warning("请选择时间范围");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [From, To] = dateRange;
|
||||||
|
const searchData = {
|
||||||
|
From: From.unix() * 1000,
|
||||||
|
To: To.unix() * 1000,
|
||||||
|
keyword: searchContent.trim(),
|
||||||
|
};
|
||||||
|
await SearchMessage(searchData);
|
||||||
|
message.success("查找完成");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查找失败:", error);
|
||||||
|
message.error("查找失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setSearchContent("");
|
||||||
|
setDateRange(null);
|
||||||
|
setLoading(false);
|
||||||
|
handleSearch();
|
||||||
|
updateShowChatRecordModel(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chatRecordSearch}>
|
||||||
|
<div className={styles.searchContentContainer}>
|
||||||
|
{/* 时间范围选择 */}
|
||||||
|
<div className={styles.timeRange}>
|
||||||
|
<div className={styles.timeRangeTitle}>时间范围</div>
|
||||||
|
<RangePicker
|
||||||
|
value={dateRange}
|
||||||
|
onChange={setDateRange}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder={["开始日期", "结束日期"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 查找内容输入 */}
|
||||||
|
<div className={styles.searchContent}>
|
||||||
|
<div className={styles.searchContentTitle}>查找内容</div>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入要查找的关键词或内容"
|
||||||
|
value={searchContent}
|
||||||
|
onChange={e => setSearchContent(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Button type="primary" loading={loading} onClick={handleSearch}>
|
||||||
|
查找
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CloseOutlined style={{ fontSize: 20 }} onClick={handleCancel} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatRecordSearch;
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Button, Modal, Input, DatePicker, message } from "antd";
|
|
||||||
import { MessageOutlined } from "@ant-design/icons";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
|
|
||||||
interface ChatRecordProps {
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatRecord: React.FC<ChatRecordProps> = ({
|
|
||||||
className,
|
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [searchContent, setSearchContent] = useState<string>("");
|
|
||||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const SearchMessage = useWeChatStore(state => state.SearchMessage);
|
|
||||||
|
|
||||||
// 打开弹窗
|
|
||||||
const openModal = () => {
|
|
||||||
setVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭弹窗并重置状态
|
|
||||||
const closeModal = () => {
|
|
||||||
setVisible(false);
|
|
||||||
setSearchContent("");
|
|
||||||
setDateRange(null);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行查找
|
|
||||||
const handleSearch = async () => {
|
|
||||||
if (!dateRange) {
|
|
||||||
message.warning("请选择时间范围");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const [From, To] = dateRange;
|
|
||||||
const searchData = {
|
|
||||||
From: From.unix() * 1000,
|
|
||||||
To: To.unix() * 1000,
|
|
||||||
keyword: searchContent.trim(),
|
|
||||||
};
|
|
||||||
await SearchMessage(searchData);
|
|
||||||
|
|
||||||
message.success("查找完成");
|
|
||||||
closeModal();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("查找失败:", error);
|
|
||||||
message.error("查找失败,请重试");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
onClick={openModal}
|
|
||||||
style={{
|
|
||||||
cursor: disabled ? "not-allowed" : "pointer",
|
|
||||||
opacity: disabled ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MessageOutlined />
|
|
||||||
聊天记录
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="查找聊天记录"
|
|
||||||
open={visible}
|
|
||||||
onCancel={closeModal}
|
|
||||||
width={450}
|
|
||||||
centered
|
|
||||||
maskClosable={!loading}
|
|
||||||
footer={[
|
|
||||||
<div
|
|
||||||
key="footer"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={closeModal}
|
|
||||||
disabled={loading}
|
|
||||||
style={{ marginRight: "8px" }}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" loading={loading} onClick={handleSearch}>
|
|
||||||
查找
|
|
||||||
</Button>
|
|
||||||
</div>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div style={{ padding: "20px 0" }}>
|
|
||||||
{/* 时间范围选择 */}
|
|
||||||
<div style={{ marginBottom: "20px" }}>
|
|
||||||
<div
|
|
||||||
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
|
|
||||||
>
|
|
||||||
时间范围
|
|
||||||
</div>
|
|
||||||
<RangePicker
|
|
||||||
value={dateRange}
|
|
||||||
onChange={setDateRange}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
size="large"
|
|
||||||
disabled={loading}
|
|
||||||
placeholder={["开始日期", "结束日期"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 查找内容输入 */}
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
|
|
||||||
>
|
|
||||||
查找内容
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入要查找的关键词或内容"
|
|
||||||
value={searchContent}
|
|
||||||
onChange={e => setSearchContent(e.target.value)}
|
|
||||||
size="large"
|
|
||||||
maxLength={100}
|
|
||||||
showCount
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatRecord;
|
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PictureOutlined,
|
PictureOutlined,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
|
MessageOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
@@ -14,7 +15,6 @@ import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
|
|||||||
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
|
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
|
||||||
import AudioRecorder from "@/components/Upload/AudioRecorder";
|
import AudioRecorder from "@/components/Upload/AudioRecorder";
|
||||||
import ToContract from "./components/toContract";
|
import ToContract from "./components/toContract";
|
||||||
import ChatRecord from "./components/chatRecord";
|
|
||||||
import styles from "./MessageEnter.module.scss";
|
import styles from "./MessageEnter.module.scss";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
const { Footer } = Layout;
|
const { Footer } = Layout;
|
||||||
@@ -35,6 +35,12 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
const updateTransmitModal = useWeChatStore(
|
const updateTransmitModal = useWeChatStore(
|
||||||
state => state.updateTransmitModal,
|
state => state.updateTransmitModal,
|
||||||
);
|
);
|
||||||
|
const showChatRecordModel = useWeChatStore(
|
||||||
|
state => state.showChatRecordModel,
|
||||||
|
);
|
||||||
|
const updateShowChatRecordModel = useWeChatStore(
|
||||||
|
state => state.updateShowChatRecordModel,
|
||||||
|
);
|
||||||
|
|
||||||
const quoteMessageContent = useWeChatStore(
|
const quoteMessageContent = useWeChatStore(
|
||||||
state => state.quoteMessageContent,
|
state => state.quoteMessageContent,
|
||||||
@@ -154,6 +160,9 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
const handTurnRignt = () => {
|
const handTurnRignt = () => {
|
||||||
updateTransmitModal(true);
|
updateTransmitModal(true);
|
||||||
};
|
};
|
||||||
|
const openChatRecordModel = () => {
|
||||||
|
updateShowChatRecordModel(!showChatRecordModel);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,7 +211,17 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.rightTool}>
|
<div className={styles.rightTool}>
|
||||||
<ToContract className={styles.rightToolItem} />
|
<ToContract className={styles.rightToolItem} />
|
||||||
<ChatRecord className={styles.rightToolItem} />
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#666",
|
||||||
|
}}
|
||||||
|
onClick={openChatRecordModel}
|
||||||
|
>
|
||||||
|
<MessageOutlined />
|
||||||
|
聊天记录
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputArea}>
|
<div className={styles.inputArea}>
|
||||||
|
|||||||
@@ -121,6 +121,11 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表情包消息
|
// 表情包消息
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
// 通用消息文本样式
|
||||||
|
.messageText {
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 位置消息基础样式
|
||||||
|
.locationMessage {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationCard {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 位置消息头部
|
||||||
|
.locationHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px 8px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
margin-right: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 位置消息内容
|
||||||
|
.locationContent {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiName {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationAddress {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiCategory,
|
||||||
|
.poiPhone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
.categoryIcon,
|
||||||
|
.phoneIcon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
|
||||||
|
.coordLabel {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordValue {
|
||||||
|
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 位置消息操作区域
|
||||||
|
.locationAction {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
.actionText {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.locationMessage {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationCard {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationHeader {
|
||||||
|
padding: 10px 14px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationIcon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationContent {
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiName {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationAddress {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationDetails {
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiCategory,
|
||||||
|
.poiPhone {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinates {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationAction {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 深色模式支持(如果需要)
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.locationCard {
|
||||||
|
background: #1f1f1f;
|
||||||
|
border-color: #333;
|
||||||
|
color: #e6e6e6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationHeader {
|
||||||
|
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationTitle {
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiName {
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationAddress {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poiCategory,
|
||||||
|
.poiPhone {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinates {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #333;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationAction {
|
||||||
|
background: linear-gradient(135deg, #1a2332 0%, #0d1419 100%);
|
||||||
|
border-top-color: #333;
|
||||||
|
color: #40a9ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #0d1419 0%, #1a2332 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./LocationMessage.module.scss";
|
||||||
|
|
||||||
|
interface LocationMessageProps {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocationData {
|
||||||
|
x: string; // 经度
|
||||||
|
y: string; // 纬度
|
||||||
|
scale: string; // 缩放级别
|
||||||
|
label: string; // 地址标签
|
||||||
|
maptype: string; // 地图类型
|
||||||
|
poiname: string; // POI名称
|
||||||
|
poiid: string; // POI ID
|
||||||
|
buildingId: string; // 建筑ID
|
||||||
|
floorName: string; // 楼层名称
|
||||||
|
poiCategoryTips: string; // POI分类提示
|
||||||
|
poiBusinessHour: string; // 营业时间
|
||||||
|
poiPhone: string; // 电话
|
||||||
|
poiPriceTips: string; // 价格提示
|
||||||
|
isFromPoiList: string; // 是否来自POI列表
|
||||||
|
adcode: string; // 行政区划代码
|
||||||
|
cityname: string; // 城市名称
|
||||||
|
fromusername: string; // 发送者用户名
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationMessage: React.FC<LocationMessageProps> = ({ content }) => {
|
||||||
|
// 统一的错误消息渲染函数
|
||||||
|
const renderErrorMessage = (fallbackText: string) => (
|
||||||
|
<div className={styles.messageText}>{fallbackText}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
|
return renderErrorMessage("[位置消息 - 无效内容]");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析位置消息内容
|
||||||
|
const parseLocationContent = (content: string): LocationData | null => {
|
||||||
|
try {
|
||||||
|
// 提取XML中的location标签内容
|
||||||
|
const locationMatch = content.match(/<location[^>]*>/);
|
||||||
|
if (!locationMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationTag = locationMatch[0];
|
||||||
|
|
||||||
|
// 提取所有属性
|
||||||
|
const extractAttribute = (tag: string, attrName: string): string => {
|
||||||
|
const regex = new RegExp(`${attrName}="([^"]*)"`);
|
||||||
|
const match = tag.match(regex);
|
||||||
|
return match ? match[1] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: extractAttribute(locationTag, "x"),
|
||||||
|
y: extractAttribute(locationTag, "y"),
|
||||||
|
scale: extractAttribute(locationTag, "scale"),
|
||||||
|
label: extractAttribute(locationTag, "label"),
|
||||||
|
maptype: extractAttribute(locationTag, "maptype"),
|
||||||
|
poiname: extractAttribute(locationTag, "poiname"),
|
||||||
|
poiid: extractAttribute(locationTag, "poiid"),
|
||||||
|
buildingId: extractAttribute(locationTag, "buildingId"),
|
||||||
|
floorName: extractAttribute(locationTag, "floorName"),
|
||||||
|
poiCategoryTips: extractAttribute(locationTag, "poiCategoryTips"),
|
||||||
|
poiBusinessHour: extractAttribute(locationTag, "poiBusinessHour"),
|
||||||
|
poiPhone: extractAttribute(locationTag, "poiPhone"),
|
||||||
|
poiPriceTips: extractAttribute(locationTag, "poiPriceTips"),
|
||||||
|
isFromPoiList: extractAttribute(locationTag, "isFromPoiList"),
|
||||||
|
adcode: extractAttribute(locationTag, "adcode"),
|
||||||
|
cityname: extractAttribute(locationTag, "cityname"),
|
||||||
|
fromusername: extractAttribute(locationTag, "fromusername"),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析位置消息失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const locationData = parseLocationContent(content);
|
||||||
|
|
||||||
|
if (!locationData) {
|
||||||
|
return renderErrorMessage("[位置消息 - 解析失败]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成地图链接
|
||||||
|
const generateMapUrl = (lat: string, lng: string, label: string) => {
|
||||||
|
// 使用腾讯地图链接
|
||||||
|
return `https://apis.map.qq.com/uri/v1/marker?marker=coord:${lat},${lng};title:${encodeURIComponent(label)}&referer=wechat`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapUrl = generateMapUrl(
|
||||||
|
locationData.y,
|
||||||
|
locationData.x,
|
||||||
|
locationData.label,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理POI信息
|
||||||
|
const poiName = locationData.poiname || locationData.label;
|
||||||
|
const poiCategory = locationData.poiCategoryTips
|
||||||
|
? locationData.poiCategoryTips.split(":")[0]
|
||||||
|
: "";
|
||||||
|
const poiPhone = locationData.poiPhone || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.locationMessage}>
|
||||||
|
<div
|
||||||
|
className={styles.locationCard}
|
||||||
|
onClick={() => window.open(mapUrl, "_blank")}
|
||||||
|
>
|
||||||
|
{/* 位置详情 */}
|
||||||
|
<div className={styles.locationContent}>
|
||||||
|
{/* POI名称 */}
|
||||||
|
{poiName && <div className={styles.poiName}>{poiName}</div>}
|
||||||
|
|
||||||
|
{/* 详细地址 */}
|
||||||
|
<div className={styles.locationAddress}>{locationData.label}</div>
|
||||||
|
|
||||||
|
{/* POI分类和电话 */}
|
||||||
|
<div className={styles.locationDetails}>
|
||||||
|
{poiCategory && (
|
||||||
|
<div className={styles.poiCategory}>
|
||||||
|
<span className={styles.categoryIcon}>🏷️</span>
|
||||||
|
{poiCategory}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{poiPhone && (
|
||||||
|
<div className={styles.poiPhone}>
|
||||||
|
<span className={styles.phoneIcon}>📞</span>
|
||||||
|
{poiPhone}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("位置消息渲染失败:", error);
|
||||||
|
return renderErrorMessage("[位置消息 - 渲染失败]");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationMessage;
|
||||||
@@ -5,6 +5,7 @@ import AudioMessage from "./components/AudioMessage/AudioMessage";
|
|||||||
import SmallProgramMessage from "./components/SmallProgramMessage";
|
import SmallProgramMessage from "./components/SmallProgramMessage";
|
||||||
import VideoMessage from "./components/VideoMessage";
|
import VideoMessage from "./components/VideoMessage";
|
||||||
import ClickMenu from "./components/ClickMeau";
|
import ClickMenu from "./components/ClickMeau";
|
||||||
|
import LocationMessage from "./components/LocationMessage";
|
||||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import { formatWechatTime } from "@/utils/common";
|
import { formatWechatTime } from "@/utils/common";
|
||||||
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
||||||
@@ -270,6 +271,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
}
|
}
|
||||||
return renderErrorMessage("[表情包]");
|
return renderErrorMessage("[表情包]");
|
||||||
|
|
||||||
|
case 48: // 定位消息
|
||||||
|
return <LocationMessage content={content || ""} />;
|
||||||
|
|
||||||
case 49: // 小程序/文章/其他:图文、文件
|
case 49: // 小程序/文章/其他:图文、文件
|
||||||
return <SmallProgramMessage content={content || ""} />;
|
return <SmallProgramMessage content={content || ""} />;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
// 快捷回复项接口
|
||||||
|
export interface QuickWordsReply {
|
||||||
|
id: number;
|
||||||
|
groupId: number;
|
||||||
|
userId: number;
|
||||||
|
title: string;
|
||||||
|
msgType: number;
|
||||||
|
content: string;
|
||||||
|
createTime: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
sortIndex: string;
|
||||||
|
updateTime: string | null;
|
||||||
|
isDel: number;
|
||||||
|
delTime: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷回复组接口
|
||||||
|
export interface QuickWordsItem {
|
||||||
|
id: number;
|
||||||
|
groupName: string;
|
||||||
|
sortIndex: string;
|
||||||
|
parentId: number;
|
||||||
|
replyType: string;
|
||||||
|
replys: any | null;
|
||||||
|
companyId: number;
|
||||||
|
userId: number;
|
||||||
|
replies: QuickWordsReply[];
|
||||||
|
children: QuickWordsItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
//好友接待配置
|
||||||
|
export function setFriendInjectConfig(params: any): Promise<QuickWordsItem[]> {
|
||||||
|
return request("/v1/kefu/reply/list", params, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddReplyRequest {
|
||||||
|
id?: string;
|
||||||
|
content?: string;
|
||||||
|
groupId?: string;
|
||||||
|
/**
|
||||||
|
* 1文本 3图片 43视频 49链接 等
|
||||||
|
*/
|
||||||
|
msgType?: string[];
|
||||||
|
/**
|
||||||
|
* 默认50
|
||||||
|
*/
|
||||||
|
sortIndex?: string;
|
||||||
|
title?: string;
|
||||||
|
[property: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加快捷回复
|
||||||
|
export function addReply(params: AddReplyRequest): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/addReply", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新快捷回复
|
||||||
|
export function updateReply(params: AddReplyRequest): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/updateReply", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除快捷回复
|
||||||
|
export function deleteReply(params: { id: string }): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/deleteReply", params, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddGroupRequest {
|
||||||
|
id?: string;
|
||||||
|
groupName?: string;
|
||||||
|
parentId?: string;
|
||||||
|
/**
|
||||||
|
* 0 公共 1私有 2部门
|
||||||
|
*/
|
||||||
|
replyType?: string[];
|
||||||
|
/**
|
||||||
|
* 默认50
|
||||||
|
*/
|
||||||
|
sortIndex?: string;
|
||||||
|
[property: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加快捷回复组
|
||||||
|
export function addGroup(params: AddGroupRequest): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/addGroup", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新快捷回复组
|
||||||
|
export function updateGroup(params: AddGroupRequest): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/updateGroup", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除快捷回复组
|
||||||
|
export function deleteGroup(params: { id: string }): Promise<any> {
|
||||||
|
return request("/v1/kefu/reply/deleteGroup", params, "DELETE");
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Modal, Form, Input, Space, Button } from "antd";
|
||||||
|
import { AddGroupRequest } from "../api";
|
||||||
|
|
||||||
|
export interface GroupModalProps {
|
||||||
|
open: boolean;
|
||||||
|
mode: "add" | "edit";
|
||||||
|
initialValues?: Partial<AddGroupRequest>;
|
||||||
|
onSubmit: (values: AddGroupRequest) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupModal: React.FC<GroupModalProps> = ({
|
||||||
|
open,
|
||||||
|
mode,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm<AddGroupRequest>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={mode === "add" ? "新增分组" : "编辑分组"}
|
||||||
|
open={open}
|
||||||
|
onCancel={() => {
|
||||||
|
onCancel();
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={values => onSubmit(values)}
|
||||||
|
initialValues={initialValues}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="groupName"
|
||||||
|
label="分组名称"
|
||||||
|
rules={[{ required: true, message: "请输入分组名称" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入分组名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onCancel();
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupModal;
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Modal, Form, Input, Select, Space, Button } from "antd";
|
||||||
|
import {
|
||||||
|
PictureOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
|
||||||
|
// 简化版不再使用样式与解析组件
|
||||||
|
import { AddReplyRequest } from "../api";
|
||||||
|
|
||||||
|
export interface QuickReplyModalProps {
|
||||||
|
open: boolean;
|
||||||
|
mode: "add" | "edit";
|
||||||
|
initialValues?: Partial<AddReplyRequest>;
|
||||||
|
onSubmit: (values: AddReplyRequest) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
groupOptions?: { label: string; value: string }[];
|
||||||
|
defaultGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuickReplyModal: React.FC<QuickReplyModalProps> = ({
|
||||||
|
open,
|
||||||
|
mode,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
groupOptions,
|
||||||
|
defaultGroupId,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm<AddReplyRequest>();
|
||||||
|
|
||||||
|
const mergedInitialValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
groupId: defaultGroupId,
|
||||||
|
msgType: initialValues?.msgType || ["1"],
|
||||||
|
...initialValues,
|
||||||
|
} as Partial<AddReplyRequest>;
|
||||||
|
}, [initialValues, defaultGroupId]);
|
||||||
|
|
||||||
|
// 监听类型变化
|
||||||
|
const msgTypeWatch = Form.useWatch("msgType", form);
|
||||||
|
const selectedMsgType = useMemo(() => {
|
||||||
|
const value = msgTypeWatch;
|
||||||
|
const raw = Array.isArray(value) ? value[0] : value;
|
||||||
|
return Number(raw || "1");
|
||||||
|
}, [msgTypeWatch]);
|
||||||
|
|
||||||
|
// 根据文件格式判断消息类型
|
||||||
|
const getMsgTypeByFileFormat = (filePath: string): number => {
|
||||||
|
const extension = filePath.toLowerCase().split(".").pop() || "";
|
||||||
|
const imageFormats = [
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"gif",
|
||||||
|
"bmp",
|
||||||
|
"webp",
|
||||||
|
"svg",
|
||||||
|
"ico",
|
||||||
|
];
|
||||||
|
if (imageFormats.includes(extension)) return 3;
|
||||||
|
const videoFormats = [
|
||||||
|
"mp4",
|
||||||
|
"avi",
|
||||||
|
"mov",
|
||||||
|
"wmv",
|
||||||
|
"flv",
|
||||||
|
"mkv",
|
||||||
|
"webm",
|
||||||
|
"3gp",
|
||||||
|
"rmvb",
|
||||||
|
];
|
||||||
|
if (videoFormats.includes(extension)) return 43;
|
||||||
|
return 49;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileType = {
|
||||||
|
TEXT: 1,
|
||||||
|
IMAGE: 2,
|
||||||
|
VIDEO: 3,
|
||||||
|
AUDIO: 4,
|
||||||
|
FILE: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const handleFileUploaded = (
|
||||||
|
filePath: string | { url: string; durationMs: number },
|
||||||
|
fileType: number,
|
||||||
|
) => {
|
||||||
|
let msgType = 1;
|
||||||
|
if (([FileType.TEXT] as number[]).includes(fileType)) {
|
||||||
|
msgType = getMsgTypeByFileFormat(filePath as string);
|
||||||
|
} else if (([FileType.IMAGE] as number[]).includes(fileType)) {
|
||||||
|
msgType = 3;
|
||||||
|
} else if (([FileType.VIDEO] as number[]).includes(fileType)) {
|
||||||
|
msgType = 43;
|
||||||
|
} else if (([FileType.AUDIO] as number[]).includes(fileType)) {
|
||||||
|
msgType = 34;
|
||||||
|
} else if (([FileType.FILE] as number[]).includes(fileType)) {
|
||||||
|
msgType = 49;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setFieldsValue({
|
||||||
|
msgType: [String(msgType)],
|
||||||
|
content: ([FileType.AUDIO] as number[]).includes(fileType)
|
||||||
|
? JSON.stringify(filePath)
|
||||||
|
: (filePath as string),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化后不再有预览解析
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={mode === "add" ? "添加快捷回复" : "编辑快捷回复"}
|
||||||
|
open={open}
|
||||||
|
onCancel={() => {
|
||||||
|
onCancel();
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={values => {
|
||||||
|
const normalized = {
|
||||||
|
...values,
|
||||||
|
msgType: Array.isArray(values.msgType)
|
||||||
|
? values.msgType
|
||||||
|
: [String(values.msgType)],
|
||||||
|
} as AddReplyRequest;
|
||||||
|
onSubmit(normalized);
|
||||||
|
}}
|
||||||
|
initialValues={mergedInitialValues}
|
||||||
|
>
|
||||||
|
<Space style={{ width: "100%" }} size={24}>
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="标题"
|
||||||
|
rules={[{ required: true, message: "请输入标题" }]}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入快捷语标题" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="groupId"
|
||||||
|
label="选择分组"
|
||||||
|
style={{ width: 260 }}
|
||||||
|
rules={[{ required: true, message: "请选择分组" }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择分组"
|
||||||
|
options={groupOptions}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="msgType"
|
||||||
|
label="消息类型"
|
||||||
|
rules={[{ required: true, message: "请选择消息类型" }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择消息类型">
|
||||||
|
<Select.Option value="1">文本</Select.Option>
|
||||||
|
<Select.Option value="3">图片</Select.Option>
|
||||||
|
<Select.Option value="43">视频</Select.Option>
|
||||||
|
<Select.Option value="49">链接</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="content"
|
||||||
|
label="内容"
|
||||||
|
rules={[{ required: true, message: "请输入/上传内容" }]}
|
||||||
|
>
|
||||||
|
{selectedMsgType === 1 && (
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="请输入文本内容"
|
||||||
|
value={form.getFieldValue("content")}
|
||||||
|
onChange={e => form.setFieldsValue({ content: e.target.value })}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedMsgType === 3 && (
|
||||||
|
<SimpleFileUpload
|
||||||
|
onFileUploaded={filePath =>
|
||||||
|
handleFileUploaded(filePath, FileType.IMAGE)
|
||||||
|
}
|
||||||
|
maxSize={1}
|
||||||
|
type={1}
|
||||||
|
slot={<Button icon={<PictureOutlined />}>上传图片</Button>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedMsgType === 43 && (
|
||||||
|
<SimpleFileUpload
|
||||||
|
onFileUploaded={filePath =>
|
||||||
|
handleFileUploaded(filePath, FileType.VIDEO)
|
||||||
|
}
|
||||||
|
maxSize={1}
|
||||||
|
type={4}
|
||||||
|
slot={<Button icon={<VideoCameraOutlined />}>上传视频</Button>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedMsgType === 49 && (
|
||||||
|
<Input
|
||||||
|
placeholder="请输入链接地址"
|
||||||
|
prefix={<LinkOutlined />}
|
||||||
|
value={form.getFieldValue("content")}
|
||||||
|
onChange={e => form.setFieldsValue({ content: e.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onCancel();
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickReplyModal;
|
||||||
@@ -1,136 +1,583 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState, useEffect, useCallback } from "react";
|
||||||
import { Card, Input, Button, Space, List, Tag } from "antd";
|
import {
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Tabs,
|
||||||
|
Tree,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
message,
|
||||||
|
Tooltip,
|
||||||
|
Spin,
|
||||||
|
Dropdown,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
PictureOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
QuickWordsItem,
|
||||||
|
QuickWordsReply,
|
||||||
|
setFriendInjectConfig,
|
||||||
|
addReply,
|
||||||
|
updateReply,
|
||||||
|
deleteReply,
|
||||||
|
updateGroup,
|
||||||
|
deleteGroup,
|
||||||
|
AddReplyRequest,
|
||||||
|
AddGroupRequest,
|
||||||
|
addGroup,
|
||||||
|
} from "./api";
|
||||||
|
import Layout from "@/components/Layout/LayoutFiexd";
|
||||||
|
import QuickReplyModal from "./components/QuickReplyModal";
|
||||||
|
import GroupModal from "./components/GroupModal";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
|
|
||||||
export interface QuickWordItem {
|
// 消息类型枚举
|
||||||
id: string | number;
|
export enum MessageType {
|
||||||
text?: string; // 兼容旧结构
|
TEXT = 1,
|
||||||
title?: string;
|
IMAGE = 3,
|
||||||
content?: string;
|
VIDEO = 43,
|
||||||
tag?: string; // 分类/标签
|
LINK = 49,
|
||||||
usageCount?: number;
|
}
|
||||||
|
|
||||||
|
// 快捷语类型枚举
|
||||||
|
export enum QuickWordsType {
|
||||||
|
PERSONAL = 1, // 个人
|
||||||
|
PUBLIC = 0, // 公共
|
||||||
|
DEPARTMENT = 2, // 部门
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuickWordsProps {
|
export interface QuickWordsProps {
|
||||||
title?: string;
|
onInsert?: (reply: QuickWordsReply) => void;
|
||||||
words: QuickWordItem[];
|
|
||||||
onInsert?: (text: string) => void;
|
|
||||||
onAdd?: (text: string) => void;
|
|
||||||
onRemove?: (id: string | number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuickWords: React.FC<QuickWordsProps> = ({
|
const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
|
||||||
title = "快捷语录",
|
const [activeTab, setActiveTab] = useState<QuickWordsType>(
|
||||||
words,
|
QuickWordsType.PUBLIC,
|
||||||
onInsert,
|
|
||||||
|
|
||||||
onRemove,
|
|
||||||
}) => {
|
|
||||||
const [keyword, setKeyword] = useState("");
|
|
||||||
const sorted = useMemo(
|
|
||||||
() =>
|
|
||||||
[...(words || [])].sort((a, b) =>
|
|
||||||
String(a.id).localeCompare(String(b.id)),
|
|
||||||
),
|
|
||||||
[words],
|
|
||||||
);
|
);
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [quickWordsData, setQuickWordsData] = useState<QuickWordsItem[]>([]);
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const [addModalVisible, setAddModalVisible] = useState(false);
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
const [groupModalVisible, setGroupModalVisible] = useState(false);
|
||||||
|
const [editingItem, setEditingItem] = useState<QuickWordsReply | null>(null);
|
||||||
|
const [editingGroup, setEditingGroup] = useState<QuickWordsItem | null>(null);
|
||||||
|
const [isAddingGroup, setIsAddingGroup] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [groupForm] = Form.useForm();
|
||||||
|
const updateQuoteMessageContent = useWeChatStore(
|
||||||
|
state => state.updateQuoteMessageContent,
|
||||||
|
);
|
||||||
|
const currentContract = useWeChatStore(state => state.currentContract);
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
|
||||||
|
const sendQuickReplyNow = (reply: QuickWordsReply) => {
|
||||||
|
if (!currentContract) return;
|
||||||
|
const params = {
|
||||||
|
wechatAccountId: currentContract.wechatAccountId,
|
||||||
|
wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0,
|
||||||
|
wechatFriendId: currentContract?.chatroomId ? 0 : currentContract.id,
|
||||||
|
msgSubType: 0,
|
||||||
|
msgType: reply.msgType,
|
||||||
|
content: reply.content,
|
||||||
|
} as any;
|
||||||
|
sendCommand("CmdSendMessage", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewAndConfirmSend = (reply: QuickWordsReply) => {
|
||||||
|
let previewNode: React.ReactNode = null;
|
||||||
|
if (reply.msgType === MessageType.IMAGE) {
|
||||||
|
previewNode = (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<img
|
||||||
|
src={reply.content}
|
||||||
|
alt="预览"
|
||||||
|
style={{ maxWidth: 360, maxHeight: 320, borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (reply.msgType === MessageType.VIDEO) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(reply.content || "{}");
|
||||||
|
const cover = json.previewImage || json.thumbPath || "";
|
||||||
|
previewNode = (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
{cover ? (
|
||||||
|
<img
|
||||||
|
src={String(cover)}
|
||||||
|
alt="视频预览"
|
||||||
|
style={{ maxWidth: 360, maxHeight: 320, borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>视频消息</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
previewNode = <div>视频消息</div>;
|
||||||
|
}
|
||||||
|
} else if (reply.msgType === MessageType.LINK) {
|
||||||
|
previewNode = (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, marginBottom: 8 }}>{reply.title}</div>
|
||||||
|
<div style={{ color: "#1677ff" }}>{reply.content}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认发送该快捷语?",
|
||||||
|
content: previewNode,
|
||||||
|
okText: "发送",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: () => {
|
||||||
|
sendQuickReplyNow(reply);
|
||||||
|
message.success("已发送");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取快捷语数据
|
||||||
|
const fetchQuickWords = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await setFriendInjectConfig({ replyType: activeTab });
|
||||||
|
setQuickWordsData(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error("获取快捷语数据失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchQuickWords();
|
||||||
|
}, [fetchQuickWords]);
|
||||||
|
|
||||||
|
// 获取消息类型图标
|
||||||
|
const getMessageTypeIcon = (msgType: number) => {
|
||||||
|
switch (msgType) {
|
||||||
|
case MessageType.TEXT:
|
||||||
|
return <FileTextOutlined style={{ color: "#1890ff" }} />;
|
||||||
|
case MessageType.IMAGE:
|
||||||
|
return <PictureOutlined style={{ color: "#52c41a" }} />;
|
||||||
|
case MessageType.VIDEO:
|
||||||
|
return <PlayCircleOutlined style={{ color: "#fa8c16" }} />;
|
||||||
|
default:
|
||||||
|
return <FileTextOutlined style={{ color: "#8c8c8c" }} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将数据转换为Tree组件需要的格式
|
||||||
|
const convertToTreeData = (data: QuickWordsItem[]): any[] => {
|
||||||
|
return data.map(item => ({
|
||||||
|
key: `group-${item.id}`,
|
||||||
|
title: (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{item.groupName}</span>
|
||||||
|
<div style={{ display: "flex", gap: 4 }}>
|
||||||
|
<Tooltip title="编辑分组">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEditGroup(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除分组">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteGroup(item.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
...item.replies.map(reply => ({
|
||||||
|
key: `reply-${reply.id}`,
|
||||||
|
title: (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// 将快捷语内容写入输入框(仅文本或可直接粘贴的内容)
|
||||||
|
try {
|
||||||
|
if ([MessageType.TEXT].includes(reply.msgType)) {
|
||||||
|
updateQuoteMessageContent(reply.content || "");
|
||||||
|
} else if ([MessageType.LINK].includes(reply.msgType)) {
|
||||||
|
previewAndConfirmSend(reply);
|
||||||
|
} else {
|
||||||
|
// 图片/视频等类型:弹出预览确认后直接发送
|
||||||
|
previewAndConfirmSend(reply);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
{getMessageTypeIcon(reply.msgType)}
|
||||||
|
<span>{reply.title}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 4 }}>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEditReply(reply);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteReply(reply.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
isLeaf: true,
|
||||||
|
})),
|
||||||
|
...convertToTreeData(item.children || []),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理添加快捷回复
|
||||||
|
const handleAddReply = async (values: AddReplyRequest) => {
|
||||||
|
try {
|
||||||
|
const fallbackGroupId =
|
||||||
|
selectedKeys[0]?.toString().replace("group-", "") ||
|
||||||
|
groupOptions[0]?.value ||
|
||||||
|
"";
|
||||||
|
await addReply({
|
||||||
|
...values,
|
||||||
|
groupId: values.groupId || fallbackGroupId,
|
||||||
|
replyType: [activeTab.toString()],
|
||||||
|
});
|
||||||
|
message.success("添加快捷回复成功");
|
||||||
|
setAddModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("添加快捷回复失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑快捷回复
|
||||||
|
const handleEditReply = (reply: QuickWordsReply) => {
|
||||||
|
setEditingItem(reply);
|
||||||
|
setEditModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理更新快捷回复
|
||||||
|
const handleUpdateReply = async (values: AddReplyRequest) => {
|
||||||
|
if (!editingItem) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateReply({
|
||||||
|
...values,
|
||||||
|
id: editingItem.id.toString(),
|
||||||
|
});
|
||||||
|
message.success("更新快捷回复成功");
|
||||||
|
setEditModalVisible(false);
|
||||||
|
setEditingItem(null);
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("更新快捷回复失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除快捷回复
|
||||||
|
const handleDeleteReply = async (id: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认删除",
|
||||||
|
content: "确定要删除这个快捷回复吗?",
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteReply({ id: id.toString() });
|
||||||
|
message.success("删除成功");
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑分组
|
||||||
|
const handleEditGroup = (group: QuickWordsItem) => {
|
||||||
|
setIsAddingGroup(false);
|
||||||
|
setEditingGroup(group);
|
||||||
|
setGroupModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开新增分组
|
||||||
|
const handleOpenAddGroup = () => {
|
||||||
|
setIsAddingGroup(true);
|
||||||
|
setEditingGroup(null);
|
||||||
|
groupForm.resetFields();
|
||||||
|
setGroupModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理更新分组
|
||||||
|
const handleUpdateGroup = async (values: AddGroupRequest) => {
|
||||||
|
if (!editingGroup) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateGroup({
|
||||||
|
...values,
|
||||||
|
id: editingGroup.id.toString(),
|
||||||
|
});
|
||||||
|
message.success("更新分组成功");
|
||||||
|
setGroupModalVisible(false);
|
||||||
|
setEditingGroup(null);
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("更新分组失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理新增分组
|
||||||
|
const handleAddGroup = async (values: AddGroupRequest) => {
|
||||||
|
try {
|
||||||
|
await addGroup({
|
||||||
|
...values,
|
||||||
|
parentId: selectedKeys[0]?.toString().startsWith("group-")
|
||||||
|
? selectedKeys[0]?.toString().replace("group-", "")
|
||||||
|
: "0",
|
||||||
|
replyType: [activeTab.toString()],
|
||||||
|
});
|
||||||
|
message.success("新增分组成功");
|
||||||
|
setGroupModalVisible(false);
|
||||||
|
setIsAddingGroup(false);
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("新增分组失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除分组
|
||||||
|
const handleDeleteGroup = async (id: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认删除",
|
||||||
|
content: "确定要删除这个分组吗?删除后该分组下的所有快捷回复也会被删除。",
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteGroup({ id: id.toString() });
|
||||||
|
message.success("删除成功");
|
||||||
|
fetchQuickWords();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤数据
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!keyword.trim()) return quickWordsData;
|
||||||
|
|
||||||
|
const filterData = (data: QuickWordsItem[]): QuickWordsItem[] => {
|
||||||
|
return data
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
replies: item.replies.filter(
|
||||||
|
reply =>
|
||||||
|
reply.title.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
|
reply.content.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
|
),
|
||||||
|
children: filterData(item.children || []),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.replies.length > 0 ||
|
||||||
|
item.children.length > 0 ||
|
||||||
|
item.groupName.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return filterData(quickWordsData);
|
||||||
|
}, [quickWordsData, keyword]);
|
||||||
|
|
||||||
|
const treeData = convertToTreeData(filteredData);
|
||||||
|
|
||||||
|
// 供新增/编辑快捷语使用的分组下拉数据
|
||||||
|
const groupOptions = useMemo(() => {
|
||||||
|
const flat: { label: string; value: string }[] = [];
|
||||||
|
const walk = (items: QuickWordsItem[]) => {
|
||||||
|
items.forEach(it => {
|
||||||
|
flat.push({ label: it.groupName, value: it.id.toString() });
|
||||||
|
if (it.children && it.children.length) walk(it.children);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(quickWordsData);
|
||||||
|
return flat;
|
||||||
|
}, [quickWordsData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={title} style={{ marginTop: 12 }}>
|
<Layout
|
||||||
<Space direction="vertical" style={{ width: "100%" }}>
|
header={
|
||||||
<Input.Search
|
<div style={{ padding: "0 16px" }}>
|
||||||
placeholder="搜索快捷语录..."
|
<Tabs
|
||||||
allowClear
|
activeKey={activeTab.toString()}
|
||||||
value={keyword}
|
onChange={key => setActiveTab(Number(key) as QuickWordsType)}
|
||||||
onChange={e => setKeyword(e.target.value)}
|
items={[
|
||||||
onSearch={v => setKeyword(v)}
|
{
|
||||||
/>
|
key: QuickWordsType.PERSONAL.toString(),
|
||||||
|
label: "个人快捷语",
|
||||||
|
},
|
||||||
|
|
||||||
<List
|
{
|
||||||
itemLayout="vertical"
|
key: QuickWordsType.DEPARTMENT.toString(),
|
||||||
split={false}
|
label: "公司快捷语",
|
||||||
dataSource={sorted.filter(item => {
|
},
|
||||||
const text = `${item.title || ""}${item.content || ""}${item.text || ""}`;
|
]}
|
||||||
return text.toLowerCase().includes(keyword.trim().toLowerCase());
|
/>
|
||||||
})}
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
renderItem={item => {
|
<Input.Search
|
||||||
const displayTitle = item.title || item.text || "未命名";
|
placeholder="输入关键字过滤"
|
||||||
const displayContent = item.content || item.text || "";
|
allowClear
|
||||||
return (
|
value={keyword}
|
||||||
<List.Item
|
onChange={e => setKeyword(e.target.value)}
|
||||||
style={{
|
style={{ flex: 1 }}
|
||||||
padding: "12px 8px",
|
/>
|
||||||
border: "1px solid #f0f0f0",
|
<Dropdown
|
||||||
borderRadius: 8,
|
menu={{
|
||||||
marginBottom: 12,
|
items: [
|
||||||
background: "#fff",
|
{ key: "add-group", label: "添加新分组" },
|
||||||
}}
|
{ key: "add-reply", label: "新增快捷语" },
|
||||||
>
|
{ key: "import-reply", label: "导入快捷语" },
|
||||||
<div
|
],
|
||||||
style={{
|
onClick: ({ key }) => {
|
||||||
display: "flex",
|
if (key === "add-group") return handleOpenAddGroup();
|
||||||
justifyContent: "space-between",
|
if (key === "add-reply") return setAddModalVisible(true);
|
||||||
gap: 12,
|
if (key === "import-reply")
|
||||||
}}
|
return message.info("导入快捷语功能开发中");
|
||||||
>
|
},
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
}}
|
||||||
<div
|
placement="bottomRight"
|
||||||
style={{
|
trigger={["click"]}
|
||||||
display: "flex",
|
>
|
||||||
alignItems: "center",
|
<Tooltip title="添加">
|
||||||
gap: 8,
|
<Button type="primary" icon={<PlusOutlined />} />
|
||||||
marginBottom: 6,
|
</Tooltip>
|
||||||
}}
|
</Dropdown>
|
||||||
>
|
<Tooltip title="刷新">
|
||||||
{item.tag && <Tag color="blue">{item.tag}</Tag>}
|
<Button icon={<ReloadOutlined />} onClick={fetchQuickWords} />
|
||||||
<span style={{ fontWeight: 600, color: "#262626" }}>
|
</Tooltip>
|
||||||
{displayTitle}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
}
|
||||||
<div
|
>
|
||||||
style={{
|
<Space direction="vertical" style={{ width: "100%", padding: 16 }}>
|
||||||
color: "#8c8c8c",
|
<Spin spinning={loading}>
|
||||||
fontSize: 13,
|
<Tree
|
||||||
lineHeight: 1.6,
|
showLine
|
||||||
whiteSpace: "pre-wrap",
|
showIcon
|
||||||
}}
|
expandedKeys={expandedKeys}
|
||||||
>
|
selectedKeys={selectedKeys}
|
||||||
{displayContent}
|
onExpand={setExpandedKeys}
|
||||||
</div>
|
onSelect={setSelectedKeys}
|
||||||
{typeof item.usageCount === "number" && (
|
treeData={treeData}
|
||||||
<div
|
/>
|
||||||
style={{ color: "#bfbfbf", fontSize: 12, marginTop: 6 }}
|
</Spin>
|
||||||
>
|
|
||||||
使用 {item.usageCount} 次
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
|
||||||
>
|
|
||||||
{onRemove && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
onClick={() => onRemove(item.id)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => onInsert?.(displayContent || displayTitle)}
|
|
||||||
>
|
|
||||||
使用
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
|
||||||
|
<QuickReplyModal
|
||||||
|
open={addModalVisible}
|
||||||
|
mode="add"
|
||||||
|
groupOptions={groupOptions}
|
||||||
|
defaultGroupId={
|
||||||
|
selectedKeys[0]?.toString().replace("group-", "") ||
|
||||||
|
groupOptions[0]?.value
|
||||||
|
}
|
||||||
|
onSubmit={handleAddReply}
|
||||||
|
onCancel={() => setAddModalVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QuickReplyModal
|
||||||
|
open={editModalVisible}
|
||||||
|
mode="edit"
|
||||||
|
groupOptions={groupOptions}
|
||||||
|
defaultGroupId={selectedKeys[0]?.toString().replace("group-", "")}
|
||||||
|
initialValues={
|
||||||
|
editingItem
|
||||||
|
? {
|
||||||
|
title: editingItem.title,
|
||||||
|
content: editingItem.content,
|
||||||
|
msgType: [editingItem.msgType.toString()],
|
||||||
|
groupId:
|
||||||
|
editingItem.groupId?.toString?.() ||
|
||||||
|
selectedKeys[0]?.toString().replace("group-", ""),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSubmit={handleUpdateReply}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
setEditingItem(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GroupModal
|
||||||
|
open={groupModalVisible}
|
||||||
|
mode={isAddingGroup ? "add" : "edit"}
|
||||||
|
initialValues={
|
||||||
|
editingGroup ? { groupName: editingGroup.groupName } : undefined
|
||||||
|
}
|
||||||
|
onSubmit={isAddingGroup ? handleAddGroup : handleUpdateGroup}
|
||||||
|
onCancel={() => {
|
||||||
|
setGroupModalVisible(false);
|
||||||
|
setEditingGroup(null);
|
||||||
|
setIsAddingGroup(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import MessageEnter from "./components/MessageEnter";
|
|||||||
import MessageRecord from "./components/MessageRecord";
|
import MessageRecord from "./components/MessageRecord";
|
||||||
import FollowupReminderModal from "./components/FollowupReminderModal";
|
import FollowupReminderModal from "./components/FollowupReminderModal";
|
||||||
import TodoListModal from "./components/TodoListModal";
|
import TodoListModal from "./components/TodoListModal";
|
||||||
|
import ChatRecordSearch from "./components/ChatRecordSearch";
|
||||||
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
const { Header, Content } = Layout;
|
const { Header, Content } = Layout;
|
||||||
@@ -37,6 +38,9 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
const aiQuoteMessageContent = useWeChatStore(
|
const aiQuoteMessageContent = useWeChatStore(
|
||||||
state => state.aiQuoteMessageContent,
|
state => state.aiQuoteMessageContent,
|
||||||
);
|
);
|
||||||
|
const showChatRecordModel = useWeChatStore(
|
||||||
|
state => state.showChatRecordModel,
|
||||||
|
);
|
||||||
const [showProfile, setShowProfile] = useState(true);
|
const [showProfile, setShowProfile] = useState(true);
|
||||||
const [followupModalVisible, setFollowupModalVisible] = useState(false);
|
const [followupModalVisible, setFollowupModalVisible] = useState(false);
|
||||||
const [todoModalVisible, setTodoModalVisible] = useState(false);
|
const [todoModalVisible, setTodoModalVisible] = useState(false);
|
||||||
@@ -136,12 +140,18 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
<div className={styles.extend}>
|
<div className={styles.extend}>
|
||||||
<Button icon={<BellOutlined />} onClick={handleFollowupClick}>
|
{showChatRecordModel ? (
|
||||||
跟进提醒
|
<ChatRecordSearch />
|
||||||
</Button>
|
) : (
|
||||||
<Button icon={<CheckSquareOutlined />} onClick={handleTodoClick}>
|
<>
|
||||||
待办事项
|
<Button icon={<BellOutlined />} onClick={handleFollowupClick}>
|
||||||
</Button>
|
跟进提醒
|
||||||
|
</Button>
|
||||||
|
<Button icon={<CheckSquareOutlined />} onClick={handleTodoClick}>
|
||||||
|
待办事项
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 聊天内容 */}
|
{/* 聊天内容 */}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
* 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
|
* 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
|
||||||
*/
|
*/
|
||||||
export interface WeChatState {
|
export interface WeChatState {
|
||||||
|
showChatRecordModel: boolean;
|
||||||
|
updateShowChatRecordModel: (show: boolean) => void;
|
||||||
aiQuoteMessageContent: number;
|
aiQuoteMessageContent: number;
|
||||||
updateAiQuoteMessageContent: (message: number) => void;
|
updateAiQuoteMessageContent: (message: number) => void;
|
||||||
quoteMessageContent: string;
|
quoteMessageContent: string;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ import {
|
|||||||
export const useWeChatStore = create<WeChatState>()(
|
export const useWeChatStore = create<WeChatState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
showChatRecordModel: false,
|
||||||
|
updateShowChatRecordModel: (show: boolean) => {
|
||||||
|
set({ showChatRecordModel: show });
|
||||||
|
},
|
||||||
//当前用户的ai接管状态
|
//当前用户的ai接管状态
|
||||||
aiQuoteMessageContent: 0,
|
aiQuoteMessageContent: 0,
|
||||||
updateAiQuoteMessageContent: (message: number) => {
|
updateAiQuoteMessageContent: (message: number) => {
|
||||||
@@ -159,7 +163,8 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
} else {
|
} else {
|
||||||
params.wechatChatroomId = contract.id;
|
params.wechatChatroomId = contract.id;
|
||||||
}
|
}
|
||||||
|
//重置动作
|
||||||
|
set({ showChatRecordModel: false });
|
||||||
clearUnreadCount1(params);
|
clearUnreadCount1(params);
|
||||||
clearUnreadCount2([contract.id]);
|
clearUnreadCount2([contract.id]);
|
||||||
getFriendInjectConfig({
|
getFriendInjectConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user