重构内容管理模块,移除不再使用的管理组件和模态框,优化样式布局,新增发布和计划功能组件,提升用户体验和界面一致性。
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Input, Select, Button, message } from "antd";
|
||||
import { listData, updateMoment } from "./api";
|
||||
import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload";
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
|
||||
interface EditMomentModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
momentData?: listData;
|
||||
}
|
||||
|
||||
const EditMomentModal: React.FC<EditMomentModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
momentData,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [contentType, setContentType] = useState<number>(1);
|
||||
const [resUrls, setResUrls] = useState<string[]>([]);
|
||||
const [linkData, setLinkData] = useState({
|
||||
desc: "",
|
||||
image: "",
|
||||
url: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && momentData) {
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
content: momentData.text,
|
||||
type: momentData.momentContentType.toString(),
|
||||
sendTime: momentData.sendTime
|
||||
? new Date(momentData.sendTime * 1000).toISOString().slice(0, 16)
|
||||
: "",
|
||||
});
|
||||
|
||||
setContentType(momentData.momentContentType);
|
||||
setResUrls(momentData.picUrlList || []);
|
||||
|
||||
// 处理链接数据
|
||||
if (momentData.link && momentData.link.length > 0) {
|
||||
setLinkData({
|
||||
desc: momentData.link[0] || "",
|
||||
image: "",
|
||||
url: momentData.link[0] || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [visible, momentData, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
const updateData: any = {
|
||||
id: momentData?.id,
|
||||
content: values.content,
|
||||
type: values.type,
|
||||
"wechatIds[]": [momentData?.accountCount || 1], // 这里需要根据实际情况调整
|
||||
};
|
||||
|
||||
// 根据内容类型添加相应字段
|
||||
switch (parseInt(values.type)) {
|
||||
case 1: // 文本
|
||||
break;
|
||||
case 2: // 图文
|
||||
if (resUrls.length > 0) {
|
||||
updateData["picUrlList[]"] = resUrls;
|
||||
}
|
||||
break;
|
||||
case 3: // 视频
|
||||
if (resUrls[0]) {
|
||||
updateData.videoUrl = resUrls[0];
|
||||
}
|
||||
break;
|
||||
case 4: // 链接
|
||||
if (linkData.url) {
|
||||
updateData["link[url]"] = [linkData.url];
|
||||
if (linkData.desc) {
|
||||
updateData["link[desc]"] = [linkData.desc];
|
||||
}
|
||||
if (linkData.image) {
|
||||
updateData["link[image]"] = [linkData.image];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 添加定时发布时间
|
||||
if (values.sendTime) {
|
||||
updateData.timingTime = values.sendTime;
|
||||
}
|
||||
|
||||
const success = await updateMoment(updateData);
|
||||
|
||||
if (success) {
|
||||
message.success("更新成功!");
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error("更新失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("更新失败:", error);
|
||||
message.error("更新失败,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setResUrls([]);
|
||||
setLinkData({ desc: "", image: "", url: "" });
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑朋友圈"
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="content"
|
||||
label="内容"
|
||||
rules={[{ required: true, message: "请输入内容" }]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请输入内容" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="type" label="类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder="请选择类型"
|
||||
onChange={value => setContentType(parseInt(value))}
|
||||
>
|
||||
<Select.Option value="1">文本</Select.Option>
|
||||
<Select.Option value="2">图文</Select.Option>
|
||||
<Select.Option value="3">视频</Select.Option>
|
||||
<Select.Option value="4">链接</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sendTime" label="发布时间">
|
||||
<Input type="datetime-local" />
|
||||
</Form.Item>
|
||||
|
||||
{/* 图文类型 */}
|
||||
{contentType === 2 && (
|
||||
<Form.Item label="图片">
|
||||
<UploadComponent value={resUrls} onChange={setResUrls} count={9} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 视频类型 */}
|
||||
{contentType === 3 && (
|
||||
<Form.Item label="视频">
|
||||
<VideoUpload
|
||||
value={resUrls[0] || ""}
|
||||
onChange={url => setResUrls([url as string])}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 链接类型 */}
|
||||
{contentType === 4 && (
|
||||
<>
|
||||
<Form.Item label="链接地址">
|
||||
<Input
|
||||
value={linkData.url}
|
||||
onChange={e =>
|
||||
setLinkData(prev => ({ ...prev, url: e.target.value }))
|
||||
}
|
||||
placeholder="请输入链接地址"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="链接描述">
|
||||
<Input
|
||||
value={linkData.desc}
|
||||
onChange={e =>
|
||||
setLinkData(prev => ({ ...prev, desc: e.target.value }))
|
||||
}
|
||||
placeholder="请输入链接描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="链接封面">
|
||||
<UploadComponent
|
||||
value={linkData.image ? [linkData.image] : []}
|
||||
onChange={urls =>
|
||||
setLinkData(prev => ({ ...prev, image: urls[0] || "" }))
|
||||
}
|
||||
count={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditMomentModal;
|
||||
@@ -0,0 +1,200 @@
|
||||
.momentPublish {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0 0 24px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.accountSection {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.accountList {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.accountCard {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
border-color: #d9d9d9;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
text-align: center;
|
||||
|
||||
.accountName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.usageBadge {
|
||||
:global(.ant-badge-count) {
|
||||
font-size: 12px;
|
||||
min-width: 32px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.limitText {
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentSection {
|
||||
margin-bottom: 24px;
|
||||
.formItem {
|
||||
margin-bottom: 24px;
|
||||
.formSelect {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.contentInput {
|
||||
:global(.ant-input) {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-input::placeholder) {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.imageSection {
|
||||
margin-bottom: 24px;
|
||||
|
||||
:global(.ant-upload-list-picture-card) {
|
||||
.ant-upload-list-item {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.publishSection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.publishButton {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #69c0ff 100%);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.momentPublish {
|
||||
padding: 16px;
|
||||
|
||||
.accountSection {
|
||||
.accountList {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.accountCard {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.publishSection {
|
||||
.publishButton {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button, Input, Select, message, Card, Badge } from "antd";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { addMoment } from "./api";
|
||||
import styles from "./MomentPublish.module.scss";
|
||||
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||
import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload";
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
|
||||
interface MomentPublishProps {
|
||||
onPublishSuccess?: () => void;
|
||||
}
|
||||
|
||||
const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||
const [content, setContent] = useState<string>("");
|
||||
// 发布类型:1文本 2图文 3视频 4链接
|
||||
const [contentType, setContentType] = useState<number>(1);
|
||||
const [sendTime, setSendTime] = useState<string>("");
|
||||
const [resUrls, setResUrls] = useState<string[]>([]); // 图片/视频等资源
|
||||
const [comment, setComment] = useState<string>("");
|
||||
// 链接
|
||||
const [linkDesc, setLinkDesc] = useState<string>("");
|
||||
const [linkImage, setLinkImage] = useState<string>("");
|
||||
const [linkUrl, setLinkUrl] = useState<string>("");
|
||||
const [accounts, setAccounts] = useState<KfUserListData[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
// 从store获取客服列表
|
||||
const kfUserList = useCkChatStore(state => state.kfUserList);
|
||||
|
||||
// 获取账号使用情况
|
||||
const fetchAccountUsage = useCallback(async () => {
|
||||
if (kfUserList.length === 0) return;
|
||||
|
||||
// 直接使用客服列表数据,不需要额外的API调用
|
||||
const accountData = kfUserList.map((kf, index) => ({
|
||||
...kf,
|
||||
name: kf.nickname || `客服${index + 1}`,
|
||||
isSelected: selectedAccounts.includes(kf.id.toString()),
|
||||
isDisabled: kf.momentsNum >= kf.momentsMax,
|
||||
}));
|
||||
|
||||
setAccounts(accountData);
|
||||
}, [kfUserList, selectedAccounts]);
|
||||
|
||||
// 如果没有选中的账号且有可用账号,自动选择第一个可用账号
|
||||
useEffect(() => {
|
||||
if (selectedAccounts.length === 0 && accounts.length > 0) {
|
||||
const firstAvailable = accounts.find(acc => !acc.isDisabled);
|
||||
if (firstAvailable) {
|
||||
setSelectedAccounts([firstAvailable.id.toString()]);
|
||||
}
|
||||
}
|
||||
}, [accounts, selectedAccounts.length]);
|
||||
|
||||
// 当客服列表变化时,重新获取使用情况
|
||||
useEffect(() => {
|
||||
fetchAccountUsage();
|
||||
}, [kfUserList, selectedAccounts, fetchAccountUsage]);
|
||||
|
||||
const handleAccountSelect = (accountId: string) => {
|
||||
const account = accounts.find(acc => acc.id.toString() === accountId);
|
||||
if (!account || account.isDisabled) return;
|
||||
|
||||
setSelectedAccounts(prev =>
|
||||
prev.includes(accountId)
|
||||
? prev.filter(id => id !== accountId)
|
||||
: [...prev, accountId],
|
||||
);
|
||||
};
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value);
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!content.trim()) {
|
||||
message.warning("请输入朋友圈内容");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccounts.length === 0) {
|
||||
message.warning("请选择发布账号");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 根据API要求构造数据
|
||||
const publishData: any = {
|
||||
content: content.trim(),
|
||||
type: contentType.toString(),
|
||||
wechatIds: selectedAccounts,
|
||||
};
|
||||
|
||||
// 根据内容类型添加相应字段
|
||||
switch (contentType) {
|
||||
case 1: // 文本
|
||||
// 文本类型只需要content
|
||||
break;
|
||||
case 2: // 图文
|
||||
if (resUrls.length > 0) {
|
||||
publishData["picUrlList[]"] = resUrls;
|
||||
}
|
||||
break;
|
||||
case 3: // 视频
|
||||
if (resUrls[0]) {
|
||||
publishData.videoUrl = resUrls[0];
|
||||
}
|
||||
break;
|
||||
case 4: // 链接
|
||||
if (linkUrl) {
|
||||
publishData["link[url]"] = [linkUrl];
|
||||
if (linkDesc) {
|
||||
publishData["link[desc]"] = [linkDesc];
|
||||
}
|
||||
if (linkImage) {
|
||||
publishData["link[image]"] = [linkImage];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 添加定时发布时间
|
||||
if (sendTime) {
|
||||
publishData.timingTime = sendTime;
|
||||
}
|
||||
console.log(publishData);
|
||||
|
||||
await addMoment(publishData);
|
||||
|
||||
message.success("发布成功!");
|
||||
// 重置表单
|
||||
setContent("");
|
||||
setResUrls([]);
|
||||
setLinkDesc("");
|
||||
setLinkImage("");
|
||||
setLinkUrl("");
|
||||
setSendTime("");
|
||||
// 重新获取账号使用情况
|
||||
await fetchAccountUsage();
|
||||
// 触发父组件的刷新回调
|
||||
onPublishSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("发布失败:", error);
|
||||
message.error("发布失败,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.momentPublish}>
|
||||
<h3 className={styles.title}>发布朋友圈</h3>
|
||||
|
||||
{/* 选择发布账号 */}
|
||||
<div className={styles.accountSection}>
|
||||
<h4 className={styles.sectionTitle}>选择发布账号</h4>
|
||||
<div className={styles.accountList}>
|
||||
{accounts.map(account => (
|
||||
<Card
|
||||
key={account.id}
|
||||
className={`${styles.accountCard} ${
|
||||
selectedAccounts.includes(account.id.toString())
|
||||
? styles.selected
|
||||
: ""
|
||||
} ${account.isDisabled ? styles.disabled : ""}`}
|
||||
onClick={() => handleAccountSelect(account.id.toString())}
|
||||
>
|
||||
<div className={styles.accountInfo}>
|
||||
<div className={styles.accountName}>{account.name}</div>
|
||||
<Badge
|
||||
count={`${account.momentsNum}/${account.momentsMax}`}
|
||||
className={styles.usageBadge}
|
||||
color={
|
||||
account.momentsNum >= account.momentsMax ? "red" : "blue"
|
||||
}
|
||||
/>
|
||||
{account.isDisabled && (
|
||||
<div className={styles.limitText}>今日已达上限</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 朋友圈内容 */}
|
||||
<div className={styles.contentSection}>
|
||||
{/* 发布时间 */}
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>发布时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={sendTime}
|
||||
onChange={e => setSendTime((e.target as HTMLInputElement).value)}
|
||||
placeholder="请选择发布时间"
|
||||
className={styles.contentInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 类型选择 */}
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>类型</label>
|
||||
<div>
|
||||
<Select
|
||||
value={contentType}
|
||||
onChange={value => setContentType(value)}
|
||||
className={styles.formSelect}
|
||||
>
|
||||
<Select.Option value={1}>文本</Select.Option>
|
||||
<Select.Option value={2}>图文</Select.Option>
|
||||
<Select.Option value={3}>视频</Select.Option>
|
||||
<Select.Option value={4}>链接</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文本内容 */}
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>内容</label>
|
||||
<Input.TextArea
|
||||
value={content}
|
||||
onChange={handleContentChange}
|
||||
placeholder="请输入内容"
|
||||
className={styles.contentInput}
|
||||
rows={6}
|
||||
showCount
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 链接类型 */}
|
||||
{contentType === 2 && (
|
||||
<>
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>描述</label>
|
||||
<Input
|
||||
value={linkDesc}
|
||||
onChange={e =>
|
||||
setLinkDesc((e.target as HTMLInputElement).value)
|
||||
}
|
||||
placeholder="请输入描述"
|
||||
className={styles.contentInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>封面图</label>
|
||||
<UploadComponent
|
||||
value={linkImage ? [linkImage] : []}
|
||||
onChange={urls => setLinkImage(urls[0] || "")}
|
||||
count={1}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>链接地址</label>
|
||||
<Input
|
||||
value={linkUrl}
|
||||
onChange={e => setLinkUrl((e.target as HTMLInputElement).value)}
|
||||
placeholder="请输入链接地址"
|
||||
className={styles.contentInput}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 视频类型 */}
|
||||
{contentType === 3 && (
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>视频上传</label>
|
||||
<div style={{ width: "40%" }}>
|
||||
<VideoUpload
|
||||
value={resUrls[0] || ""}
|
||||
onChange={url => setResUrls([url as string])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图片/小程序 素材上传 */}
|
||||
{contentType === 2 && (
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>图片上传 (最多9张)</label>
|
||||
<UploadComponent value={resUrls} onChange={setResUrls} count={9} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formLabel}>备注</label>
|
||||
<Input.TextArea
|
||||
value={comment}
|
||||
onChange={e => setComment((e.target as HTMLTextAreaElement).value)}
|
||||
placeholder="请输入备注"
|
||||
className={styles.contentInput}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 发布按钮 */}
|
||||
<div className={styles.publishSection}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handlePublish}
|
||||
loading={loading}
|
||||
disabled={selectedAccounts.length === 0 || !content.trim()}
|
||||
className={styles.publishButton}
|
||||
>
|
||||
{loading ? "发布中..." : "发布朋友圈"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MomentPublish;
|
||||
@@ -0,0 +1,285 @@
|
||||
import React from "react";
|
||||
import { Modal, Card, Tag, Badge } from "antd";
|
||||
import {
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
LinkOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { listData } from "./api";
|
||||
|
||||
interface PreviewMomentModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
momentData?: listData;
|
||||
}
|
||||
|
||||
const PreviewMomentModal: React.FC<PreviewMomentModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
momentData,
|
||||
}) => {
|
||||
if (!momentData) return null;
|
||||
|
||||
// 获取内容类型信息
|
||||
const getContentTypeInfo = (type: number) => {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return { icon: <FileTextOutlined />, label: "文本", color: "blue" };
|
||||
case 2:
|
||||
return { icon: <PictureOutlined />, label: "图文", color: "green" };
|
||||
case 3:
|
||||
return {
|
||||
icon: <VideoCameraOutlined />,
|
||||
label: "视频",
|
||||
color: "purple",
|
||||
};
|
||||
case 4:
|
||||
return { icon: <LinkOutlined />, label: "链接", color: "orange" };
|
||||
default:
|
||||
return { icon: <FileTextOutlined />, label: "未知", color: "default" };
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return "未设置";
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = (isSend: number) => {
|
||||
switch (isSend) {
|
||||
case 0:
|
||||
return { status: "processing" as const, text: "待发布" };
|
||||
case 1:
|
||||
return { status: "success" as const, text: "已发布" };
|
||||
case 2:
|
||||
return { status: "error" as const, text: "发布失败" };
|
||||
default:
|
||||
return { status: "default" as const, text: "未知" };
|
||||
}
|
||||
};
|
||||
|
||||
const contentTypeInfo = getContentTypeInfo(momentData.momentContentType);
|
||||
const statusInfo = getStatusInfo(momentData.isSend);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="朋友圈预览"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
width={500}
|
||||
footer={null}
|
||||
>
|
||||
<Card className="preview-card">
|
||||
<div className="preview-header">
|
||||
<div className="preview-status">
|
||||
<Badge status={statusInfo.status} text={statusInfo.text} />
|
||||
<Tag
|
||||
color={contentTypeInfo.color}
|
||||
icon={contentTypeInfo.icon}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{contentTypeInfo.label}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="preview-content">
|
||||
<div className="preview-text">{momentData.text || "无文本内容"}</div>
|
||||
|
||||
{/* 图片预览 */}
|
||||
{momentData.picUrlList && momentData.picUrlList.length > 0 && (
|
||||
<div className="preview-images">
|
||||
{momentData.picUrlList.map((image, index) => (
|
||||
<div key={index} className="preview-image-item">
|
||||
<img
|
||||
src={image}
|
||||
alt={`图片${index + 1}`}
|
||||
className="preview-image"
|
||||
onError={e => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频预览 */}
|
||||
{momentData.videoUrl && (
|
||||
<div className="preview-video">
|
||||
<div className="preview-video-icon">
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
<span className="preview-video-text">视频内容</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 链接预览 */}
|
||||
{momentData.link && momentData.link.length > 0 && (
|
||||
<div className="preview-link">
|
||||
<LinkOutlined className="preview-link-icon" />
|
||||
<span className="preview-link-text">
|
||||
{momentData.link.length} 个链接
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="preview-details">
|
||||
<div className="preview-detail-item">
|
||||
<span className="preview-detail-label">发布时间:</span>
|
||||
<span className="preview-detail-value">
|
||||
{formatTime(momentData.sendTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="preview-detail-item">
|
||||
<span className="preview-detail-label">账号数量:</span>
|
||||
<span className="preview-detail-value">
|
||||
{momentData.accountCount} 个账号
|
||||
</span>
|
||||
</div>
|
||||
<div className="preview-detail-item">
|
||||
<span className="preview-detail-label">创建时间:</span>
|
||||
<span className="preview-detail-value">
|
||||
{formatTime(momentData.createTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style jsx>{`
|
||||
.preview-card {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.preview-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #262626;
|
||||
margin-bottom: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preview-images {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-image-item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f0f8ff;
|
||||
border: 1px solid #d6e4ff;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-video-icon {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-video-text {
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-link-icon {
|
||||
color: #fa8c16;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-link-text {
|
||||
color: #fa8c16;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-details {
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.preview-detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.preview-detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preview-detail-label {
|
||||
color: #8c8c8c;
|
||||
min-width: 80px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.preview-detail-value {
|
||||
color: #595959;
|
||||
font-weight: 500;
|
||||
}
|
||||
`}</style>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewMomentModal;
|
||||
@@ -0,0 +1,352 @@
|
||||
.publishSchedule {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #8c8c8c;
|
||||
|
||||
.loadingText {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduleList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scheduleCard {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.statusBadge {
|
||||
:global(.ant-badge) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(.ant-badge-status-text) {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.typeTag {
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.actionButton {
|
||||
color: #1890ff;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
color: #ff4d4f;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
.postContent {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #262626;
|
||||
margin-bottom: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.postImages {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.imagePlaceholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.imagePreview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.imageIcon {
|
||||
font-size: 20px;
|
||||
opacity: 0.6;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreImages {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.videoPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 8px 12px;
|
||||
background: #f0f8ff;
|
||||
border: 1px solid #d6e4ff;
|
||||
border-radius: 6px;
|
||||
|
||||
.videoIcon {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.videoText {
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.linkPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 8px 12px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 6px;
|
||||
|
||||
.linkIcon {
|
||||
color: #fa8c16;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.linkText {
|
||||
color: #fa8c16;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.postDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #1890ff;
|
||||
|
||||
.detailItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
|
||||
.detailIcon {
|
||||
color: #8c8c8c;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
color: #8c8c8c;
|
||||
min-width: 60px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
color: #595959;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emptySubText {
|
||||
font-size: 14px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.paginationInfo {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
|
||||
.paginationText {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.publishSchedule {
|
||||
padding: 16px;
|
||||
|
||||
.scheduleList {
|
||||
max-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.scheduleCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
.postDetails {
|
||||
.detailItem {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
.detailLabel {
|
||||
min-width: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {
|
||||
Card,
|
||||
Badge,
|
||||
Button,
|
||||
message,
|
||||
Popconfirm,
|
||||
Empty,
|
||||
Spin,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserOutlined,
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
LinkOutlined,
|
||||
FileTextOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { getMomentList, deleteMoment, listData } from "./api";
|
||||
import EditMomentModal from "./EditMomentModal";
|
||||
import PreviewMomentModal from "./PreviewMomentModal";
|
||||
import styles from "./PublishSchedule.module.scss";
|
||||
|
||||
// 定义组件暴露的方法接口
|
||||
export interface PublishScheduleRef {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PublishSchedule = forwardRef<PublishScheduleRef>((props, ref) => {
|
||||
const [scheduledPosts, setScheduledPosts] = useState<listData[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
});
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||
const [selectedMoment, setSelectedMoment] = useState<listData | undefined>();
|
||||
|
||||
// 获取内容类型图标和标签
|
||||
const getContentTypeInfo = (type: number) => {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return { icon: <FileTextOutlined />, label: "文本", color: "blue" };
|
||||
case 2:
|
||||
return { icon: <PictureOutlined />, label: "图文", color: "green" };
|
||||
case 3:
|
||||
return {
|
||||
icon: <VideoCameraOutlined />,
|
||||
label: "视频",
|
||||
color: "purple",
|
||||
};
|
||||
case 4:
|
||||
return { icon: <LinkOutlined />, label: "链接", color: "orange" };
|
||||
case 5:
|
||||
return { icon: <AppstoreOutlined />, label: "小程序", color: "cyan" };
|
||||
default:
|
||||
return { icon: <FileTextOutlined />, label: "未知", color: "default" };
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return "未设置";
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// 获取发布计划列表
|
||||
const fetchScheduledMoments = useCallback(
|
||||
async (page = 1) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMomentList({ page, limit: pagination.limit });
|
||||
setScheduledPosts(response.list);
|
||||
setPagination(prev => ({ ...prev, page, total: response.total }));
|
||||
} catch (error) {
|
||||
console.error("获取发布计划失败:", error);
|
||||
message.error("获取发布计划失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[pagination.limit],
|
||||
);
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh: () => {
|
||||
fetchScheduledMoments(pagination.page);
|
||||
},
|
||||
}));
|
||||
|
||||
// 组件挂载时获取数据
|
||||
useEffect(() => {
|
||||
fetchScheduledMoments();
|
||||
}, [fetchScheduledMoments]);
|
||||
|
||||
const handleDeletePost = async (postId: number) => {
|
||||
try {
|
||||
const success = await deleteMoment({ id: postId });
|
||||
if (success) {
|
||||
setScheduledPosts(prev => prev.filter(post => post.id !== postId));
|
||||
message.success("删除成功");
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (scheduledPosts.length === 1 && pagination.page > 1) {
|
||||
fetchScheduledMoments(pagination.page - 1);
|
||||
}
|
||||
} else {
|
||||
message.error("删除失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除发布计划失败:", error);
|
||||
message.error("删除失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditPost = (post: listData) => {
|
||||
setSelectedMoment(post);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleViewPost = (post: listData) => {
|
||||
setSelectedMoment(post);
|
||||
setPreviewModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
// 编辑成功后刷新列表
|
||||
fetchScheduledMoments(pagination.page);
|
||||
};
|
||||
|
||||
const getStatusBadge = (isSend: number) => {
|
||||
switch (isSend) {
|
||||
case 0:
|
||||
return <Badge status="processing" text="待发布" />;
|
||||
case 1:
|
||||
return <Badge status="success" text="已发布" />;
|
||||
case 2:
|
||||
return <Badge status="error" text="发布失败" />;
|
||||
default:
|
||||
return <Badge status="default" text="未知" />;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchScheduledMoments(pagination.page);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.publishSchedule}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>发布计划</h3>
|
||||
<div className={styles.headerActions}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ClockCircleOutlined />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.scheduleList}>
|
||||
{loading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Spin size="large" />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : scheduledPosts.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<div>
|
||||
<div className={styles.emptyText}>暂无发布计划</div>
|
||||
<div className={styles.emptySubText}>
|
||||
创建朋友圈内容后可以设置定时发布
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
scheduledPosts.map(post => {
|
||||
const contentTypeInfo = getContentTypeInfo(post.momentContentType);
|
||||
return (
|
||||
<Card key={post.id} className={styles.scheduleCard}>
|
||||
<div className={styles.cardHeader}>
|
||||
<div className={styles.headerLeft}>
|
||||
<div className={styles.statusBadge}>
|
||||
{getStatusBadge(post.isSend)}
|
||||
</div>
|
||||
<Tag
|
||||
color={contentTypeInfo.color}
|
||||
icon={contentTypeInfo.icon}
|
||||
className={styles.typeTag}
|
||||
>
|
||||
{contentTypeInfo.label}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<Tooltip title="预览">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewPost(post)}
|
||||
size="small"
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEditPost(post)}
|
||||
size="small"
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这条发布计划吗?"
|
||||
onConfirm={() => handleDeletePost(post.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
className={styles.deleteButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.postContent}>
|
||||
{post.text || "无文本内容"}
|
||||
</div>
|
||||
|
||||
{/* 图片展示 */}
|
||||
{post.picUrlList && post.picUrlList.length > 0 && (
|
||||
<div className={styles.postImages}>
|
||||
{post.picUrlList.slice(0, 3).map((image, index) => (
|
||||
<div key={index} className={styles.imagePlaceholder}>
|
||||
<img
|
||||
src={image}
|
||||
alt={`图片${index + 1}`}
|
||||
className={styles.imagePreview}
|
||||
onError={e => {
|
||||
(e.target as HTMLImageElement).style.display =
|
||||
"none";
|
||||
(
|
||||
e.target as HTMLImageElement
|
||||
).nextElementSibling?.classList.add(styles.show);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.imageIcon}>
|
||||
<PictureOutlined />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{post.picUrlList.length > 3 && (
|
||||
<div className={styles.moreImages}>
|
||||
+{post.picUrlList.length - 3}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频展示 */}
|
||||
{post.videoUrl && (
|
||||
<div className={styles.videoPreview}>
|
||||
<div className={styles.videoIcon}>
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
<span className={styles.videoText}>视频内容</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 链接展示 */}
|
||||
{post.link && post.link.length > 0 && (
|
||||
<div className={styles.linkPreview}>
|
||||
<LinkOutlined className={styles.linkIcon} />
|
||||
<span className={styles.linkText}>
|
||||
{post.link.length} 个链接
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.postDetails}>
|
||||
<div className={styles.detailItem}>
|
||||
<ClockCircleOutlined className={styles.detailIcon} />
|
||||
<span className={styles.detailLabel}>发布时间:</span>
|
||||
<span className={styles.detailValue}>
|
||||
{formatTime(post.sendTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<UserOutlined className={styles.detailIcon} />
|
||||
<span className={styles.detailLabel}>账号数量:</span>
|
||||
<span className={styles.detailValue}>
|
||||
{post.accountCount} 个账号
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>创建时间:</span>
|
||||
<span className={styles.detailValue}>
|
||||
{formatTime(post.createTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页信息 */}
|
||||
{scheduledPosts.length > 0 && (
|
||||
<div className={styles.paginationInfo}>
|
||||
<span className={styles.paginationText}>
|
||||
共 {pagination.total} 条记录,当前第 {pagination.page} 页
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
<EditMomentModal
|
||||
visible={editModalVisible}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
onSuccess={handleEditSuccess}
|
||||
momentData={selectedMoment}
|
||||
/>
|
||||
|
||||
{/* 预览弹窗 */}
|
||||
<PreviewMomentModal
|
||||
visible={previewModalVisible}
|
||||
onCancel={() => setPreviewModalVisible(false)}
|
||||
momentData={selectedMoment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PublishSchedule.displayName = "PublishSchedule";
|
||||
|
||||
export default PublishSchedule;
|
||||
@@ -0,0 +1,86 @@
|
||||
import request from "@/api/request";
|
||||
export interface listData {
|
||||
id: number;
|
||||
text: "";
|
||||
momentContentType: number;
|
||||
picUrlList: string[];
|
||||
videoUrl: string;
|
||||
link: string[];
|
||||
publicMode: number;
|
||||
isSend: number;
|
||||
createTime: number;
|
||||
sendTime: number;
|
||||
accountCount: number;
|
||||
}
|
||||
|
||||
interface listResponse {
|
||||
list: listData[];
|
||||
total: number;
|
||||
}
|
||||
// 朋友圈定时发布 - 列表
|
||||
export const getMomentList = (data: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}): Promise<listResponse> => {
|
||||
return request("/v1/kefu/moments/list", data, "GET");
|
||||
};
|
||||
|
||||
export interface MomentRequest {
|
||||
id?: number;
|
||||
/**
|
||||
* 朋友圈内容
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* 标签列表
|
||||
*/
|
||||
"labels[]"?: string[];
|
||||
/**
|
||||
* 链接信息-描述 type4有效
|
||||
*/
|
||||
"link[desc]"?: string[];
|
||||
/**
|
||||
* 链接信息-图标 type4有效
|
||||
*/
|
||||
"link[image]"?: string[];
|
||||
/**
|
||||
* 链接信息-链接 type4有效
|
||||
*/
|
||||
"link[url]"?: string[];
|
||||
/**
|
||||
* 图片列表 type2有效
|
||||
*/
|
||||
"picUrlList[]"?: string[];
|
||||
/**
|
||||
* 定时发布时间
|
||||
*/
|
||||
timingTime?: string;
|
||||
/**
|
||||
* 内容类型 1文本 2图文 3视频 4链接
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 视频链接 type3有效
|
||||
*/
|
||||
videoUrl?: string;
|
||||
/**
|
||||
* 微信账号ID列表
|
||||
*/
|
||||
"wechatIds[]"?: string[];
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
// 朋友圈定时发布 - 添加
|
||||
export const addMoment = (data: MomentRequest) => {
|
||||
return request("/v1/kefu/moments/add", data, "POST");
|
||||
};
|
||||
|
||||
// 朋友圈定时发布 - 编辑
|
||||
export const updateMoment = (data: MomentRequest) => {
|
||||
return request("/v1/kefu/moments/update", data, "POST");
|
||||
};
|
||||
|
||||
// 朋友圈定时发布 - 删除
|
||||
export const deleteMoment = (data: { id: number }) => {
|
||||
return request("/v1/kefu/moments/delete", data, "DELETE");
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
// 管理组件导出
|
||||
export { default as MaterialManagement } from "./management/MaterialManagement";
|
||||
export { default as SensitiveWordManagement } from "./management/SensitiveWordManagement";
|
||||
export { default as KeywordManagement } from "./management/KeywordManagement";
|
||||
|
||||
// 模态框组件导出
|
||||
export { default as MaterialModal } from "./modals/MaterialModal";
|
||||
export { default as SensitiveWordModal } from "./modals/SensitiveWordModal";
|
||||
export { default as KeywordModal } from "./modals/KeywordModal";
|
||||
export { default as MomentPublish } from "./MomentPublish";
|
||||
export { default as PublishSchedule } from "./PublishSchedule";
|
||||
export { default as EditMomentModal } from "./EditMomentModal";
|
||||
export { default as PreviewMomentModal } from "./PreviewMomentModal";
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Tag,
|
||||
Switch,
|
||||
message,
|
||||
Popconfirm,
|
||||
Pagination,
|
||||
} from "antd";
|
||||
import {
|
||||
SearchOutlined,
|
||||
FormOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./index.module.scss";
|
||||
import {
|
||||
getKeywordList,
|
||||
deleteKeyword,
|
||||
setKeywordStatus,
|
||||
type KeywordListParams,
|
||||
} from "../../api";
|
||||
import KeywordModal from "../modals/KeywordModal";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
interface KeywordItem {
|
||||
id?: number;
|
||||
type: number;
|
||||
replyType: number;
|
||||
title: string;
|
||||
keywords: string;
|
||||
status: number;
|
||||
content: string;
|
||||
metailGroupsOptions: { title: string; id: number }[];
|
||||
level: number;
|
||||
}
|
||||
|
||||
const KeywordManagement = forwardRef<any, Record<string, never>>(
|
||||
(props, ref) => {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||
const [editingKeywordId, setEditingKeywordId] = useState<number | 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: number) => {
|
||||
switch (replyType) {
|
||||
case 0:
|
||||
return "素材回复";
|
||||
case 1:
|
||||
return "自定义";
|
||||
default:
|
||||
return "未知类型";
|
||||
}
|
||||
};
|
||||
|
||||
// 回复类型颜色
|
||||
const getReplyTypeColor = (replyType: number) => {
|
||||
switch (replyType) {
|
||||
case 0:
|
||||
return "blue";
|
||||
case 1:
|
||||
return "purple";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
// 获取关键词列表(服务端搜索)
|
||||
const fetchKeywords = async (params?: KeywordListParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const requestParams = {
|
||||
page: pagination.current.toString(),
|
||||
limit: pagination.pageSize.toString(),
|
||||
keyword: keywordQuery || undefined,
|
||||
...params,
|
||||
} as KeywordListParams;
|
||||
const response = await getKeywordList(requestParams);
|
||||
if (response) {
|
||||
setKeywordsList(response.list || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total || 0,
|
||||
}));
|
||||
} else {
|
||||
setKeywordsList([]);
|
||||
message.error(response?.message || "获取关键词列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取关键词列表失败:", error);
|
||||
setKeywordsList([]);
|
||||
message.error("获取关键词列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchKeywords,
|
||||
}));
|
||||
|
||||
// 关键词管理相关函数
|
||||
const handleToggleKeyword = async (id: number) => {
|
||||
try {
|
||||
await setKeywordStatus({ id });
|
||||
setKeywordsList(prev =>
|
||||
prev.map(item =>
|
||||
item.id === id
|
||||
? { ...item, status: item.status === 1 ? 0 : 1 }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
message.success("状态更新成功");
|
||||
} catch (error) {
|
||||
console.error("状态更新失败:", error);
|
||||
message.error("状态更新失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditKeyword = (id: number) => {
|
||||
setEditingKeywordId(id);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
// 编辑弹窗成功回调
|
||||
const handleEditSuccess = () => {
|
||||
fetchKeywords(); // 重新获取数据
|
||||
};
|
||||
|
||||
const handleDeleteKeyword = async (id: number) => {
|
||||
try {
|
||||
await deleteKeyword(id);
|
||||
setKeywordsList(prev => prev.filter(item => item.id !== id));
|
||||
message.success("删除成功");
|
||||
} catch (error) {
|
||||
console.error("删除失败:", error);
|
||||
message.error("删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 移除本地筛选,改为服务端搜索,列表直接使用 keywordsList
|
||||
|
||||
// 搜索处理函数
|
||||
const handleSearch = (value: string) => {
|
||||
setKeywordQuery(value || "");
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
};
|
||||
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number, pageSize?: number) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || prev.pageSize,
|
||||
}));
|
||||
};
|
||||
|
||||
// 初始化与依赖变化时获取数据(依赖分页与搜索关键字)
|
||||
useEffect(() => {
|
||||
fetchKeywords();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pagination.current, pagination.pageSize, keywordQuery]);
|
||||
|
||||
return (
|
||||
<div className={styles.keywordContent}>
|
||||
<div className={styles.searchSection}>
|
||||
<Search
|
||||
placeholder="搜索关键词..."
|
||||
value={searchValue}
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 300 }}
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.keywordList}>
|
||||
{loading ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : keywordsList.length === 0 ? (
|
||||
<div className={styles.empty}>暂无关键词数据</div>
|
||||
) : (
|
||||
keywordsList.map(item => (
|
||||
<div key={item.id} className={styles.keywordItem}>
|
||||
<div className={styles.itemContent}>
|
||||
<div className={styles.leftSection}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.title}>{item.title}</div>
|
||||
<Tag color="default">{getMatchTypeText(item.type)}</Tag>
|
||||
<Tag color="default">{getPriorityText(item.level)}</Tag>
|
||||
</div>
|
||||
{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>
|
||||
))
|
||||
)}
|
||||
</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
|
||||
visible={editModalVisible}
|
||||
mode="edit"
|
||||
keywordId={editingKeywordId}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
setEditingKeywordId(null);
|
||||
}}
|
||||
onSuccess={handleEditSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
KeywordManagement.displayName = "KeywordManagement";
|
||||
|
||||
export default KeywordManagement;
|
||||
@@ -1,320 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Button, Input, Card, message, Popconfirm, Pagination } from "antd";
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
FormOutlined,
|
||||
FileTextOutlined,
|
||||
FileImageOutlined,
|
||||
PlayCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "../../index.module.scss";
|
||||
import {
|
||||
getMaterialList,
|
||||
deleteMaterial,
|
||||
type MaterialListParams,
|
||||
} from "../../api";
|
||||
import MaterialModal from "../modals/MaterialModal";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
interface MaterialItem {
|
||||
id: number;
|
||||
companyId: number;
|
||||
userId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
cover: string;
|
||||
status: number;
|
||||
type: string; // 素材类型:文本、图片、视频
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
isDel: number;
|
||||
delTime: string | null;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
const MaterialManagement = forwardRef<any, Record<string, never>>(
|
||||
(props, ref) => {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 使用 ref 来存储最新的分页状态
|
||||
const paginationRef = useRef(pagination);
|
||||
paginationRef.current = pagination;
|
||||
|
||||
// 获取类型图标
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "文本":
|
||||
return <FileTextOutlined className={styles.typeIcon} />;
|
||||
case "图片":
|
||||
return <FileImageOutlined className={styles.typeIcon} />;
|
||||
case "视频":
|
||||
return <PlayCircleOutlined className={styles.typeIcon} />;
|
||||
default:
|
||||
return <FileTextOutlined className={styles.typeIcon} />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取素材列表
|
||||
const fetchMaterials = async (params?: MaterialListParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const currentPagination = paginationRef.current;
|
||||
const requestParams = {
|
||||
page: currentPagination.current.toString(),
|
||||
limit: currentPagination.pageSize.toString(),
|
||||
...params,
|
||||
};
|
||||
const response = await getMaterialList(requestParams);
|
||||
if (response) {
|
||||
setMaterialsList(response.list || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total || 0,
|
||||
}));
|
||||
} else {
|
||||
setMaterialsList([]);
|
||||
message.error(response?.message || "获取素材列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取素材列表失败:", error);
|
||||
setMaterialsList([]);
|
||||
message.error("获取素材列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchMaterials,
|
||||
}));
|
||||
|
||||
// 素材管理相关函数
|
||||
const handleDeleteMaterial = async (id: number) => {
|
||||
try {
|
||||
await deleteMaterial(id.toString());
|
||||
setMaterialsList(prev => prev.filter(item => item.id !== id));
|
||||
message.success("删除成功");
|
||||
} catch (error) {
|
||||
message.error("删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑素材
|
||||
const handleEditMaterial = (id: number) => {
|
||||
setEditingMaterialId(id);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
// 编辑弹窗成功回调
|
||||
const handleEditSuccess = () => {
|
||||
fetchMaterials(); // 重新获取数据
|
||||
};
|
||||
|
||||
// 搜索处理函数
|
||||
const handleSearch = (value: string) => {
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchMaterials({ keyword: value });
|
||||
};
|
||||
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number, pageSize?: number) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || prev.pageSize,
|
||||
}));
|
||||
// 分页变化后立即获取数据
|
||||
setTimeout(() => {
|
||||
fetchMaterials();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 组件挂载时获取数据
|
||||
useEffect(() => {
|
||||
fetchMaterials();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.materialContent}>
|
||||
<div className={styles.searchSection}>
|
||||
<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.materialGrid}>
|
||||
{loading ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : materialsList.length === 0 ? (
|
||||
<div className={styles.empty}>暂无素材数据</div>
|
||||
) : (
|
||||
materialsList.map(item => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className={styles.materialCard}
|
||||
hoverable
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
icon={<FormOutlined />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleEditMaterial(item.id);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确认删除"
|
||||
description="确定要删除这个素材吗?删除后无法恢复。"
|
||||
onConfirm={() => handleDeleteMaterial(item.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okType="danger"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<div
|
||||
className={styles.thumbnail}
|
||||
onClick={() => handleEditMaterial(item.id)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.cover ? (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={item.cover}
|
||||
alt={item.title}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
right: "4px",
|
||||
background: "rgba(0, 0, 0, 0.6)",
|
||||
color: "white",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
fontSize: "10px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{item.type}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#f5f5f5",
|
||||
color: "#999",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{getTypeIcon(item.type)}
|
||||
<span style={{ marginTop: "8px", fontSize: "12px" }}>
|
||||
{item.type}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.title}>{item.title}</div>
|
||||
<div className={styles.meta}>
|
||||
<div>创建人: {item.userName}</div>
|
||||
<div>{item.createTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</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
|
||||
visible={editModalVisible}
|
||||
mode="edit"
|
||||
materialId={editingMaterialId}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
setEditingMaterialId(null);
|
||||
}}
|
||||
onSuccess={handleEditSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MaterialManagement.displayName = "MaterialManagement";
|
||||
|
||||
export default MaterialManagement;
|
||||
@@ -1,293 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Tag,
|
||||
Switch,
|
||||
message,
|
||||
Popconfirm,
|
||||
Pagination,
|
||||
} from "antd";
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
FormOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "../../index.module.scss";
|
||||
import {
|
||||
getSensitiveWordList,
|
||||
deleteSensitiveWord,
|
||||
setSensitiveWordStatus,
|
||||
type SensitiveWordListParams,
|
||||
} from "../../api";
|
||||
import SensitiveWordModal from "../modals/SensitiveWordModal";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
interface SensitiveWordItem {
|
||||
id: string;
|
||||
title: string;
|
||||
keywords: string;
|
||||
content: string;
|
||||
operation: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const SensitiveWordManagement = forwardRef<any, Record<string, never>>(
|
||||
(props, ref) => {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [sensitiveWordsList, setSensitiveWordsList] = useState<
|
||||
SensitiveWordItem[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||
const [editingSensitiveWordId, setEditingSensitiveWordId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
switch (tag) {
|
||||
case "政治":
|
||||
return "#ff4d4f";
|
||||
case "色情":
|
||||
return "#ff4d4f";
|
||||
case "暴力":
|
||||
return "#ff4d4f";
|
||||
default:
|
||||
return "#ff4d4f";
|
||||
}
|
||||
};
|
||||
|
||||
// 操作类型映射
|
||||
const getOperationText = (operation: number) => {
|
||||
switch (operation) {
|
||||
case 0:
|
||||
return "不操作";
|
||||
case 1:
|
||||
return "替换";
|
||||
case 2:
|
||||
return "删除";
|
||||
case 3:
|
||||
return "警告";
|
||||
case 4:
|
||||
return "禁止发送";
|
||||
default:
|
||||
return "未知操作";
|
||||
}
|
||||
};
|
||||
|
||||
// 获取敏感词列表
|
||||
const fetchSensitiveWords = async (params?: SensitiveWordListParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const requestParams = {
|
||||
page: pagination.current.toString(),
|
||||
limit: pagination.pageSize.toString(),
|
||||
...params,
|
||||
};
|
||||
const response = await getSensitiveWordList(requestParams);
|
||||
if (response) {
|
||||
setSensitiveWordsList(response.list || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total || 0,
|
||||
}));
|
||||
} else {
|
||||
setSensitiveWordsList([]);
|
||||
message.error(response?.message || "获取敏感词列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取敏感词列表失败:", error);
|
||||
setSensitiveWordsList([]);
|
||||
message.error("获取敏感词列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchSensitiveWords,
|
||||
}));
|
||||
|
||||
// 敏感词管理相关函数
|
||||
const handleToggleSensitiveWord = async (id: string) => {
|
||||
try {
|
||||
const response = await setSensitiveWordStatus({ id });
|
||||
if (response) {
|
||||
setSensitiveWordsList(prev =>
|
||||
prev.map(item =>
|
||||
item.id === id
|
||||
? { ...item, status: item.status === 1 ? 0 : 1 }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
message.success("状态更新成功");
|
||||
} else {
|
||||
message.error(response?.message || "状态更新失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("状态更新失败:", error);
|
||||
message.error("状态更新失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSensitiveWord = (id: string) => {
|
||||
setEditingSensitiveWordId(id);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
// 编辑弹窗成功回调
|
||||
const handleEditSuccess = () => {
|
||||
fetchSensitiveWords(); // 重新获取数据
|
||||
};
|
||||
|
||||
const handleDeleteSensitiveWord = async (id: string) => {
|
||||
try {
|
||||
await deleteSensitiveWord(id);
|
||||
setSensitiveWordsList(prev => prev.filter(item => item.id !== id));
|
||||
message.success("删除成功");
|
||||
} 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]);
|
||||
|
||||
return (
|
||||
<div className={styles.sensitiveContent}>
|
||||
<div className={styles.searchSection}>
|
||||
<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}>
|
||||
{loading ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : filteredSensitiveWords.length === 0 ? (
|
||||
<div className={styles.empty}>暂无敏感词数据</div>
|
||||
) : (
|
||||
filteredSensitiveWords.map(item => (
|
||||
<div key={item.id} className={styles.sensitiveItem}>
|
||||
<div className={styles.itemContent}>
|
||||
<div className={styles.categoryName}>{item.title}</div>
|
||||
<div className={styles.actionText}>
|
||||
{getOperationText(item.operation)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemActions}>
|
||||
<Switch
|
||||
checked={item.status == 1}
|
||||
onChange={() => handleToggleSensitiveWord(item.id)}
|
||||
className={styles.toggleSwitch}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<FormOutlined className={styles.editIcon} />}
|
||||
onClick={() => handleEditSensitiveWord(item.id)}
|
||||
className={styles.actionBtn}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description="确定要删除这个敏感词吗?删除后无法恢复。"
|
||||
onConfirm={() => handleDeleteSensitiveWord(item.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okType="danger"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined className={styles.deleteIcon} />}
|
||||
className={styles.actionBtn}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</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>
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
<SensitiveWordModal
|
||||
visible={editModalVisible}
|
||||
mode="edit"
|
||||
sensitiveWordId={editingSensitiveWordId}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
setEditingSensitiveWordId(null);
|
||||
}}
|
||||
onSuccess={handleEditSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SensitiveWordManagement.displayName = "SensitiveWordManagement";
|
||||
|
||||
export default SensitiveWordManagement;
|
||||
@@ -1,252 +0,0 @@
|
||||
// 关键词管理样式
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// 管理组件统一导出
|
||||
export { default as MaterialManagement } from "./MaterialManagement";
|
||||
export { default as SensitiveWordManagement } from "./SensitiveWordManagement";
|
||||
export { default as KeywordManagement } from "./KeywordManagement";
|
||||
@@ -1,276 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Input, Select } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
FileTextOutlined,
|
||||
FileImageOutlined,
|
||||
PlayCircleOutlined,
|
||||
FileOutlined,
|
||||
SoundOutlined,
|
||||
LinkOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
import FileUpload from "@/components/Upload/FileUpload";
|
||||
import AudioUpload from "@/components/Upload/AudioUpload";
|
||||
import type { ContentItem, LinkData } from "../../api";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface ContentManagerProps {
|
||||
value?: ContentItem[];
|
||||
onChange?: (content: ContentItem[]) => void;
|
||||
}
|
||||
|
||||
const ContentManager: React.FC<ContentManagerProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
}) => {
|
||||
const [contentItems, setContentItems] = useState<ContentItem[]>(value);
|
||||
|
||||
// 内容类型配置
|
||||
const contentTypes = [
|
||||
{ value: "text", label: "文本", icon: <FileTextOutlined /> },
|
||||
{ value: "image", label: "图片", icon: <FileImageOutlined /> },
|
||||
{ value: "video", label: "视频", icon: <PlayCircleOutlined /> },
|
||||
{ value: "file", label: "文件", icon: <FileOutlined /> },
|
||||
{ value: "audio", label: "音频", icon: <SoundOutlined /> },
|
||||
{ value: "link", label: "链接", icon: <LinkOutlined /> },
|
||||
];
|
||||
|
||||
// 同步外部value到内部state
|
||||
useEffect(() => {
|
||||
setContentItems(value);
|
||||
}, [value]);
|
||||
|
||||
// 初始化时添加默认文本内容项
|
||||
useEffect(() => {
|
||||
if (contentItems.length === 0) {
|
||||
const defaultTextItem: ContentItem = {
|
||||
type: "text",
|
||||
data: "",
|
||||
};
|
||||
setContentItems([defaultTextItem]);
|
||||
onChange?.([defaultTextItem]);
|
||||
}
|
||||
}, [contentItems.length, onChange]);
|
||||
|
||||
// 更新内容项
|
||||
const updateContentItems = (newItems: ContentItem[]) => {
|
||||
setContentItems(newItems);
|
||||
onChange?.(newItems);
|
||||
};
|
||||
|
||||
// 添加新内容项
|
||||
const handleAddItem = () => {
|
||||
const newItem: ContentItem = {
|
||||
type: "text",
|
||||
data: "",
|
||||
};
|
||||
|
||||
const newItems = [...contentItems, newItem];
|
||||
updateContentItems(newItems);
|
||||
};
|
||||
|
||||
// 删除内容项
|
||||
const handleDeleteItem = (index: number) => {
|
||||
const newItems = contentItems.filter((_, i) => i !== index);
|
||||
updateContentItems(newItems);
|
||||
};
|
||||
|
||||
// 更新内容项数据
|
||||
const updateItemData = (index: number, data: any) => {
|
||||
const newItems = [...contentItems];
|
||||
newItems[index] = { ...newItems[index], data };
|
||||
updateContentItems(newItems);
|
||||
};
|
||||
|
||||
// 更新内容项类型
|
||||
const updateItemType = (index: number, newType: string) => {
|
||||
const newItems = [...contentItems];
|
||||
|
||||
// 根据新类型重置数据
|
||||
let newData: any;
|
||||
if (newType === "link") {
|
||||
newData = { title: "", url: "", cover: "" };
|
||||
} else {
|
||||
newData = "";
|
||||
}
|
||||
|
||||
newItems[index] = {
|
||||
type: newType as any,
|
||||
data: newData,
|
||||
};
|
||||
updateContentItems(newItems);
|
||||
};
|
||||
|
||||
// 渲染内容项
|
||||
const renderContentItem = (item: ContentItem, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
border: "1px solid #d9d9d9",
|
||||
borderRadius: "6px",
|
||||
padding: "12px",
|
||||
marginBottom: "8px",
|
||||
backgroundColor: "#fafafa",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Select
|
||||
value={item.type}
|
||||
onChange={newType => updateItemType(index, newType)}
|
||||
style={{ width: 120 }}
|
||||
size="small"
|
||||
>
|
||||
{contentTypes.map(type => (
|
||||
<Option key={type.value} value={type.value}>
|
||||
{type.icon} {type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{index !== 0 && (
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteItem(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderContentInput(item, index)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染内容输入
|
||||
const renderContentInput = (item: ContentItem, index: number) => {
|
||||
switch (item.type) {
|
||||
case "text":
|
||||
return (
|
||||
<TextArea
|
||||
value={item.data as string}
|
||||
onChange={e => updateItemData(index, e.target.value)}
|
||||
placeholder="请输入文本内容"
|
||||
rows={3}
|
||||
/>
|
||||
);
|
||||
case "image":
|
||||
return (
|
||||
<ImageUpload
|
||||
count={1}
|
||||
accept="image/*"
|
||||
value={item.data ? [item.data as string] : []}
|
||||
onChange={urls => updateItemData(index, urls[0] || "")}
|
||||
/>
|
||||
);
|
||||
case "video":
|
||||
return (
|
||||
<VideoUpload
|
||||
value={item.data as string}
|
||||
onChange={url => {
|
||||
const videoUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||
updateItemData(index, videoUrl);
|
||||
}}
|
||||
maxSize={50}
|
||||
showPreview={true}
|
||||
/>
|
||||
);
|
||||
case "file":
|
||||
return (
|
||||
<FileUpload
|
||||
value={item.data as string}
|
||||
onChange={url => {
|
||||
const fileUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||
updateItemData(index, fileUrl);
|
||||
}}
|
||||
maxSize={10}
|
||||
showPreview={true}
|
||||
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
|
||||
/>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<AudioUpload
|
||||
value={item.data as string}
|
||||
onChange={url => {
|
||||
const audioUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||
updateItemData(index, audioUrl);
|
||||
}}
|
||||
maxSize={50}
|
||||
showPreview={true}
|
||||
/>
|
||||
);
|
||||
case "link": {
|
||||
const linkData = (item.data as LinkData) || {
|
||||
title: "",
|
||||
url: "",
|
||||
cover: "",
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={linkData.title}
|
||||
onChange={e =>
|
||||
updateItemData(index, { ...linkData, title: e.target.value })
|
||||
}
|
||||
placeholder="链接标题"
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<Input
|
||||
value={linkData.url}
|
||||
onChange={e =>
|
||||
updateItemData(index, { ...linkData, url: e.target.value })
|
||||
}
|
||||
placeholder="链接URL"
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<ImageUpload
|
||||
count={1}
|
||||
accept="image/*"
|
||||
value={linkData.cover ? [linkData.cover] : []}
|
||||
onChange={urls =>
|
||||
updateItemData(index, { ...linkData, cover: urls[0] || "" })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 内容列表 */}
|
||||
{contentItems.map((item, index) => renderContentItem(item, index))}
|
||||
|
||||
{/* 添加内容区域 */}
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
添加内容
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentManager;
|
||||
@@ -1,288 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
||||
import {
|
||||
addKeyword,
|
||||
updateKeyword,
|
||||
getKeywordDetails,
|
||||
type KeywordAddRequest,
|
||||
type KeywordUpdateRequest,
|
||||
} from "../../api";
|
||||
import MetailSelection from "@/components/MetailSelection";
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface KeywordModalProps {
|
||||
visible: boolean;
|
||||
mode: "add" | "edit";
|
||||
keywordId?: number | null;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const KeywordModal: React.FC<KeywordModalProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
keywordId,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
|
||||
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
||||
|
||||
// 获取关键词详情
|
||||
const fetchKeywordDetails = useCallback(
|
||||
async (id: number) => {
|
||||
try {
|
||||
const response = await getKeywordDetails(id);
|
||||
if (response) {
|
||||
const keyword = response;
|
||||
form.setFieldsValue({
|
||||
title: keyword.title,
|
||||
keywords: keyword.keywords,
|
||||
content: keyword.content,
|
||||
type: keyword.type,
|
||||
level: keyword.level,
|
||||
replyType: keyword.replyType,
|
||||
status: keyword.status,
|
||||
metailGroups: keyword.metailGroups,
|
||||
});
|
||||
setSelectedOptions(keyword.metailGroupsOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取关键词详情失败:", error);
|
||||
message.error("获取关键词详情失败");
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
// 当弹窗打开时处理数据
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (mode === "edit" && keywordId) {
|
||||
// 编辑模式:获取详情
|
||||
fetchKeywordDetails(keywordId);
|
||||
} else if (mode === "add") {
|
||||
// 添加模式:重置表单
|
||||
form.resetFields();
|
||||
setSelectedOptions([]);
|
||||
}
|
||||
}
|
||||
}, [visible, mode, keywordId, fetchKeywordDetails, form]);
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (mode === "add") {
|
||||
const data: KeywordAddRequest = {
|
||||
title: values.title,
|
||||
keywords: values.keywords,
|
||||
content: values.content,
|
||||
type: values.type,
|
||||
level: values.level,
|
||||
replyType: values.replyType,
|
||||
status: values.status || "1",
|
||||
metailGroups: values.metailGroups,
|
||||
};
|
||||
|
||||
const response = await addKeyword(data);
|
||||
if (response) {
|
||||
message.success("添加关键词成功");
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "添加关键词失败");
|
||||
}
|
||||
} else {
|
||||
const data: KeywordUpdateRequest = {
|
||||
id: keywordId,
|
||||
title: values.title,
|
||||
keywords: values.keywords,
|
||||
content: values.content,
|
||||
type: values.type,
|
||||
level: values.level,
|
||||
replyType: values.replyType,
|
||||
status: values.status,
|
||||
metailGroups: values.metailGroups,
|
||||
};
|
||||
|
||||
const response = await updateKeyword(data);
|
||||
if (response) {
|
||||
message.success("更新关键词回复成功");
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "更新关键词回复失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${mode === "add" ? "添加" : "更新"}关键词失败:`, error);
|
||||
message.error(`${mode === "add" ? "添加" : "更新"}关键词失败`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setSelectedOptions([]);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={handleFormValuesChange}
|
||||
initialValues={{
|
||||
status: 1,
|
||||
type: "模糊匹配",
|
||||
level: 1,
|
||||
replyType: 0,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="关键词标题"
|
||||
rules={[{ required: true, message: "请输入关键词标题" }]}
|
||||
>
|
||||
<Input placeholder="请输入关键词标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keywords"
|
||||
label="关键词"
|
||||
rules={[{ required: true, message: "请输入关键词" }]}
|
||||
>
|
||||
<Input placeholder="请输入关键词" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="replyType"
|
||||
label="回复类型"
|
||||
rules={[{ required: true, message: "请选择回复类型" }]}
|
||||
>
|
||||
<Select placeholder="请选择回复类型">
|
||||
<Option value={0}>素材回复</Option>
|
||||
<Option value={1}>自定义</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{form.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>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: "请选择状态" }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value={1}>启用</Option>
|
||||
<Option value={0}>禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
||||
>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeywordModal;
|
||||
@@ -1,197 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
||||
import {
|
||||
addMaterial,
|
||||
updateMaterial,
|
||||
getMaterialDetails,
|
||||
type MaterialAddRequest,
|
||||
type MaterialUpdateRequest,
|
||||
type ContentItem,
|
||||
} from "../../api";
|
||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
||||
import ContentManager from "./ContentManager";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface MaterialModalProps {
|
||||
visible: boolean;
|
||||
mode: "add" | "edit";
|
||||
materialId?: number | null;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const MaterialModal: React.FC<MaterialModalProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
materialId,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [contentItems, setContentItems] = useState<ContentItem[]>([]);
|
||||
|
||||
// 获取素材详情
|
||||
const fetchMaterialDetails = useCallback(
|
||||
async (id: number) => {
|
||||
try {
|
||||
const response = await getMaterialDetails(id.toString());
|
||||
if (response) {
|
||||
const material = response;
|
||||
form.setFieldsValue({
|
||||
title: material.title,
|
||||
cover: material.cover ? [material.cover] : [],
|
||||
status: material.status,
|
||||
});
|
||||
// 设置内容项
|
||||
setContentItems(material.content || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取素材详情失败:", error);
|
||||
message.error("获取素材详情失败");
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
// 当弹窗打开时处理数据
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (mode === "edit" && materialId) {
|
||||
// 编辑模式:获取详情
|
||||
fetchMaterialDetails(materialId);
|
||||
} else if (mode === "add") {
|
||||
// 添加模式:重置表单
|
||||
form.resetFields();
|
||||
setContentItems([]);
|
||||
}
|
||||
}
|
||||
}, [visible, mode, materialId, fetchMaterialDetails, form]);
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 验证内容项
|
||||
if (contentItems.length === 0) {
|
||||
message.warning("请至少添加一个内容项");
|
||||
return;
|
||||
}
|
||||
|
||||
const coverValue = Array.isArray(values.cover)
|
||||
? values.cover[0] || ""
|
||||
: values.cover || "";
|
||||
|
||||
const data: MaterialAddRequest = {
|
||||
title: values.title,
|
||||
status: values.status || 1,
|
||||
content: contentItems,
|
||||
...(coverValue && { cover: coverValue }),
|
||||
};
|
||||
|
||||
if (mode === "add") {
|
||||
const response = await addMaterial(data);
|
||||
if (response) {
|
||||
message.success("添加素材成功");
|
||||
form.resetFields();
|
||||
setContentItems([]);
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "添加素材失败");
|
||||
}
|
||||
} else {
|
||||
const updateData: MaterialUpdateRequest = {
|
||||
...data,
|
||||
id: materialId?.toString(),
|
||||
};
|
||||
|
||||
const response = await updateMaterial(updateData);
|
||||
if (response) {
|
||||
message.success("更新素材成功");
|
||||
form.resetFields();
|
||||
setContentItems([]);
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "更新素材失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${mode === "add" ? "添加" : "更新"}素材失败:`, error);
|
||||
message.error(`${mode === "add" ? "添加" : "更新"}素材失败`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setContentItems([]);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const title = mode === "add" ? "添加素材" : "编辑素材";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{ status: 1 }}
|
||||
>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="素材标题"
|
||||
rules={[{ required: true, message: "请输入素材标题" }]}
|
||||
>
|
||||
<Input placeholder="请输入素材标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="素材内容">
|
||||
<ContentManager value={contentItems} onChange={setContentItems} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="cover" label="封面图片">
|
||||
<ImageUpload
|
||||
count={1}
|
||||
accept="image/*"
|
||||
className="material-cover-upload"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: "请选择状态" }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value={1}>启用</Option>
|
||||
<Option value={0}>禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
||||
>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialModal;
|
||||
@@ -1,203 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
||||
import {
|
||||
addSensitiveWord,
|
||||
updateSensitiveWord,
|
||||
getSensitiveWordDetails,
|
||||
type SensitiveWordAddRequest,
|
||||
type SensitiveWordUpdateRequest,
|
||||
} from "../../api";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface SensitiveWordModalProps {
|
||||
visible: boolean;
|
||||
mode: "add" | "edit";
|
||||
sensitiveWordId?: string | null;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
sensitiveWordId,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取敏感词详情
|
||||
const fetchSensitiveWordDetails = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
const response = await getSensitiveWordDetails(id);
|
||||
if (response) {
|
||||
const sensitiveWord = response;
|
||||
form.setFieldsValue({
|
||||
title: sensitiveWord.title,
|
||||
keywords: sensitiveWord.keywords,
|
||||
content: sensitiveWord.content,
|
||||
operation: sensitiveWord.operation,
|
||||
status: sensitiveWord.status,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取敏感词详情失败:", error);
|
||||
message.error("获取敏感词详情失败");
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
// 当弹窗打开时处理数据
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (mode === "edit" && sensitiveWordId) {
|
||||
// 编辑模式:获取详情
|
||||
fetchSensitiveWordDetails(sensitiveWordId);
|
||||
} else if (mode === "add") {
|
||||
// 添加模式:重置表单
|
||||
form.resetFields();
|
||||
}
|
||||
}
|
||||
}, [visible, mode, sensitiveWordId, fetchSensitiveWordDetails, form]);
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (mode === "add") {
|
||||
const data: SensitiveWordAddRequest = {
|
||||
title: values.title,
|
||||
keywords: values.keywords,
|
||||
content: values.content,
|
||||
operation: values.operation,
|
||||
status: values.status || "1",
|
||||
};
|
||||
|
||||
const response = await addSensitiveWord(data);
|
||||
if (response) {
|
||||
message.success("添加敏感词成功");
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "添加敏感词失败");
|
||||
}
|
||||
} else {
|
||||
const data: SensitiveWordUpdateRequest = {
|
||||
id: sensitiveWordId,
|
||||
title: values.title,
|
||||
keywords: values.keywords,
|
||||
content: values.content,
|
||||
operation: values.operation,
|
||||
status: values.status,
|
||||
};
|
||||
|
||||
const response = await updateSensitiveWord(data);
|
||||
if (response) {
|
||||
message.success("更新敏感词成功");
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(response?.message || "更新敏感词失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${mode === "add" ? "添加" : "更新"}敏感词失败:`, error);
|
||||
message.error(`${mode === "add" ? "添加" : "更新"}敏感词失败`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const title = mode === "add" ? "添加敏感词" : "编辑敏感词";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{ status: "1", operation: "1" }}
|
||||
>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="敏感词标题"
|
||||
rules={[{ required: true, message: "请输入敏感词标题" }]}
|
||||
>
|
||||
<Input placeholder="请输入敏感词标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keywords"
|
||||
label="关键词"
|
||||
rules={[{ required: true, message: "请输入关键词" }]}
|
||||
>
|
||||
<Input placeholder="请输入关键词" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="content"
|
||||
label="敏感词内容"
|
||||
rules={[{ required: true, message: "请输入敏感词内容" }]}
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入敏感词内容" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="operation"
|
||||
label="操作类型"
|
||||
rules={[{ required: true, message: "请选择操作类型" }]}
|
||||
>
|
||||
<Select placeholder="请选择操作类型">
|
||||
<Option value={0}>不操作</Option>
|
||||
<Option value={1}>替换</Option>
|
||||
<Option value={2}>删除</Option>
|
||||
<Option value={3}>警告</Option>
|
||||
<Option value={4}>禁止发送</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: "请选择状态" }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value={1}>启用</Option>
|
||||
<Option value={0}>禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
||||
>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensitiveWordModal;
|
||||
@@ -1,5 +0,0 @@
|
||||
// 模态框组件统一导出
|
||||
export { default as MaterialModal } from "./MaterialModal";
|
||||
export { default as SensitiveWordModal } from "./SensitiveWordModal";
|
||||
export { default as KeywordModal } from "./KeywordModal";
|
||||
export { default as ContentManager } from "./ContentManager";
|
||||
@@ -53,10 +53,27 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 24px;
|
||||
min-height: 400px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.mainLayout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
margin: 24px 0px;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.materialContent {
|
||||
@@ -385,6 +402,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mainLayout {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.leftSection,
|
||||
.rightSection {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,76 +1,19 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Button } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import PowerNavigation from "@/components/PowerNavtion";
|
||||
import MomentPublish from "./components/MomentPublish";
|
||||
import PublishSchedule from "./components/PublishSchedule";
|
||||
import styles from "./index.module.scss";
|
||||
import {
|
||||
MaterialManagement,
|
||||
SensitiveWordManagement,
|
||||
KeywordManagement,
|
||||
MaterialModal,
|
||||
SensitiveWordModal,
|
||||
KeywordModal,
|
||||
} from "./components";
|
||||
|
||||
const ContentManagement: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>("material");
|
||||
const [materialModalVisible, setMaterialModalVisible] = useState(false);
|
||||
const [sensitiveWordModalVisible, setSensitiveWordModalVisible] =
|
||||
useState(false);
|
||||
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
|
||||
// 用于触发 PublishSchedule 刷新的引用
|
||||
const publishScheduleRef = useRef<{ refresh: () => void }>(null);
|
||||
|
||||
// 引用管理组件
|
||||
const materialManagementRef = useRef<any>(null);
|
||||
const keywordManagementRef = useRef<any>(null);
|
||||
const sensitiveWordManagementRef = useRef<any>(null);
|
||||
|
||||
const tabs = [
|
||||
{ key: "material", label: "素材资源库" },
|
||||
{ key: "sensitive", label: "敏感词管理" },
|
||||
{ key: "keyword", label: "关键词回复" },
|
||||
];
|
||||
|
||||
// 弹窗成功回调
|
||||
const handleModalSuccess = () => {
|
||||
console.log("handleModalSuccess");
|
||||
// 刷新素材列表
|
||||
if (materialManagementRef.current?.fetchMaterials) {
|
||||
console.log("刷新素材列表");
|
||||
materialManagementRef.current.fetchMaterials();
|
||||
}
|
||||
// 刷新敏感词列表
|
||||
if (sensitiveWordManagementRef.current?.fetchSensitiveWords) {
|
||||
console.log("刷新敏感词列表");
|
||||
sensitiveWordManagementRef.current.fetchSensitiveWords();
|
||||
}
|
||||
// 刷新关键词列表
|
||||
if (keywordManagementRef.current?.fetchKeywords) {
|
||||
console.log("刷新关键词列表");
|
||||
keywordManagementRef.current.fetchKeywords();
|
||||
}
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case "material":
|
||||
return (
|
||||
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
||||
);
|
||||
case "sensitive":
|
||||
return (
|
||||
<SensitiveWordManagement
|
||||
ref={sensitiveWordManagementRef}
|
||||
{...({} as any)}
|
||||
/>
|
||||
);
|
||||
case "keyword":
|
||||
return (
|
||||
<KeywordManagement ref={keywordManagementRef} {...({} as any)} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
||||
);
|
||||
// 发布成功后的回调函数
|
||||
const handlePublishSuccess = () => {
|
||||
// 触发 PublishSchedule 组件刷新
|
||||
if (publishScheduleRef.current) {
|
||||
publishScheduleRef.current.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,47 +27,19 @@ const ContentManagement: React.FC = () => {
|
||||
rightContent={<div className={styles.headerActions}></div>}
|
||||
/>
|
||||
|
||||
<div className={styles.tabsSection}>
|
||||
<br />
|
||||
<div className={styles.tabs}>
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={`${styles.tab} ${
|
||||
activeTab === tab.key ? styles.tabActive : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.mainLayout}>
|
||||
{/* 左侧:发布朋友圈 */}
|
||||
<div className={styles.leftSection}>
|
||||
<MomentPublish onPublishSuccess={handlePublishSuccess} />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>{renderTabContent()}</div>
|
||||
|
||||
{/* 弹窗组件 */}
|
||||
<MaterialModal
|
||||
visible={materialModalVisible}
|
||||
mode="add"
|
||||
onCancel={() => setMaterialModalVisible(false)}
|
||||
onSuccess={handleModalSuccess}
|
||||
/>
|
||||
|
||||
<SensitiveWordModal
|
||||
visible={sensitiveWordModalVisible}
|
||||
mode="add"
|
||||
onCancel={() => setSensitiveWordModalVisible(false)}
|
||||
onSuccess={handleModalSuccess}
|
||||
/>
|
||||
|
||||
<KeywordModal
|
||||
visible={keywordModalVisible}
|
||||
mode="add"
|
||||
keywordId={null}
|
||||
onCancel={() => setKeywordModalVisible(false)}
|
||||
onSuccess={handleModalSuccess}
|
||||
/>
|
||||
{/* 右侧:发布计划 */}
|
||||
<div className={styles.rightSection}>
|
||||
<PublishSchedule ref={publishScheduleRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -671,7 +671,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
console.warn("Invalid createTime format:", firstMessage.createTime);
|
||||
return;
|
||||
}
|
||||
timestamp = date.getTime() - 20000;
|
||||
timestamp = date.getTime() - 24 * 36000 * 1000;
|
||||
} catch (error) {
|
||||
console.error("Error parsing createTime:", error);
|
||||
return;
|
||||
|
||||
@@ -4,3 +4,28 @@ import request from "@/api/request";
|
||||
export const getContactList = (params: { prevId: string; count: number }) => {
|
||||
return request("/api/wechatFriend/list", params, "GET");
|
||||
};
|
||||
|
||||
export interface dataProcessingPost {
|
||||
/**
|
||||
* CmdModifyFriendLabel专属
|
||||
*/
|
||||
"labels[]"?: string[];
|
||||
/**
|
||||
* CmdModifyFriendLabel专属
|
||||
*/
|
||||
newRemark?: string;
|
||||
/**
|
||||
* 类型 CmdModifyFriendRemark修改备注 CmdModifyFriendLabel好友修改标签
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* 公共
|
||||
*/
|
||||
wechatAccountId?: number;
|
||||
wechatFriendId?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export const dataProcessing = (params: dataProcessingPost) => {
|
||||
return request("/v1/kefu/dataProcessing", params, "POST");
|
||||
};
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
toggleChatSessionPin,
|
||||
deleteChatSession,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { updateConfig } from "@/pages/pc/ckbox/api";
|
||||
import { dataProcessing } from "./api";
|
||||
import styles from "./MessageList.module.scss";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
interface MessageListProps {}
|
||||
@@ -23,6 +25,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
const { setCurrentContact, currentContract } = useWeChatStore();
|
||||
const getChatSessions = useCkChatStore(state => state.chatSessions);
|
||||
const kfSelected = useCkChatStore(state => state.kfSelected);
|
||||
const { sendCommand } = useWebSocketStore();
|
||||
const onContactClick = (session: ContractData | weChatGroup) => {
|
||||
console.log(session);
|
||||
|
||||
@@ -139,12 +142,56 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
};
|
||||
|
||||
// 保存备注
|
||||
const handleSaveRemark = () => {
|
||||
const handleSaveRemark = async () => {
|
||||
if (!editRemarkModal.session) return;
|
||||
|
||||
// TODO: 调用API更新备注
|
||||
console.log("更新备注", editRemarkModal.session, editRemarkModal.remark);
|
||||
const session = editRemarkModal.session;
|
||||
const isGroup = "chatroomId" in session;
|
||||
|
||||
try {
|
||||
if (isGroup) {
|
||||
// 群聊备注修改
|
||||
sendCommand("CmdModifyGroupRemark", {
|
||||
wechatAccountId: session.wechatAccountId,
|
||||
chatroomId: session.chatroomId,
|
||||
newRemark: editRemarkModal.remark,
|
||||
});
|
||||
await dataProcessing({
|
||||
type: "CmdModifyGroupRemark",
|
||||
wechatAccountId: session.wechatAccountId,
|
||||
wechatFriendId: session.id,
|
||||
newRemark: editRemarkModal.remark,
|
||||
});
|
||||
} else {
|
||||
// 好友备注修改
|
||||
sendCommand("CmdModifyFriendRemark", {
|
||||
wechatAccountId: session.wechatAccountId,
|
||||
wechatFriendId: session.id,
|
||||
newRemark: editRemarkModal.remark,
|
||||
});
|
||||
await dataProcessing({
|
||||
type: "CmdModifyFriendRemark",
|
||||
wechatAccountId: session.wechatAccountId,
|
||||
wechatFriendId: session.id,
|
||||
newRemark: editRemarkModal.remark,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改备注后会更新当前的session的conRemark,然后使用updateChatSession方法更新会话列表
|
||||
const updatedSession = {
|
||||
...session,
|
||||
conRemark: editRemarkModal.remark,
|
||||
};
|
||||
|
||||
// 更新会话列表中的备注
|
||||
const { updateChatSession } = useCkChatStore.getState();
|
||||
updateChatSession(updatedSession);
|
||||
|
||||
message.success("备注更新成功");
|
||||
} catch (error) {
|
||||
console.error("更新备注失败:", error);
|
||||
message.error("备注更新失败,请重试");
|
||||
}
|
||||
|
||||
setEditRemarkModal({
|
||||
visible: false,
|
||||
|
||||
@@ -189,7 +189,7 @@ export const getAllContactList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let page = 1;
|
||||
const limit = 1000;
|
||||
const limit = 500;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
@@ -257,7 +257,7 @@ export const getAllGroupList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let page = 1;
|
||||
const limit = 1000;
|
||||
const limit = 500;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
|
||||
@@ -201,7 +201,7 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
wechatAccountId: contact.wechatAccountId,
|
||||
From: 1,
|
||||
To: To || +new Date(),
|
||||
Count: 5,
|
||||
Count: 20,
|
||||
olderData: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WebSocketMessage } from "./websocket";
|
||||
import { deepCopy } from "@/utils/common";
|
||||
import { Messages } from "./msg.data";
|
||||
import { db } from "@/utils/db";
|
||||
import { Modal } from "antd";
|
||||
// 消息处理器类型定义
|
||||
type MessageHandler = (message: WebSocketMessage) => void;
|
||||
const addMessage = useWeChatStore.getState().addMessage;
|
||||
@@ -82,16 +83,33 @@ const messageHandlers: Record<string, MessageHandler> = {
|
||||
console.log("通知消息", message);
|
||||
// 在这里添加具体的处理逻辑
|
||||
if (message.notify == "Auth failed") {
|
||||
// 避免重复弹窗
|
||||
if ((window as any).__CKB_AUTH_FAILED_SHOWN__) {
|
||||
return;
|
||||
}
|
||||
(window as any).__CKB_AUTH_FAILED_SHOWN__ = true;
|
||||
|
||||
Modal.warning({
|
||||
title: "登录失效",
|
||||
content: "认证已失效或账号在其他设备登录,请重新登录。",
|
||||
okText: "重新登录",
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 被踢出时删除所有缓存数据
|
||||
localStorage.clear();
|
||||
// 删除
|
||||
await db.kfUsers.clear();
|
||||
await db.weChatGroup.clear();
|
||||
await db.contracts.clear();
|
||||
await db.newContractList.clear();
|
||||
} finally {
|
||||
(window as any).__CKB_AUTH_FAILED_SHOWN__ = false;
|
||||
window.location.href = "/login";
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
//撤回消息
|
||||
CmdMessageRecalled: message => {
|
||||
|
||||
Reference in New Issue
Block a user