diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx new file mode 100644 index 00000000..c332a14e --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx @@ -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 = ({ + visible, + onCancel, + onSuccess, + momentData, +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [contentType, setContentType] = useState(1); + const [resUrls, setResUrls] = useState([]); + 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 ( + + 取消 + , + , + ]} + > +
+ + + + + + + + + + + + + {/* 图文类型 */} + {contentType === 2 && ( + + + + )} + + {/* 视频类型 */} + {contentType === 3 && ( + + setResUrls([url as string])} + /> + + )} + + {/* 链接类型 */} + {contentType === 4 && ( + <> + + + setLinkData(prev => ({ ...prev, url: e.target.value })) + } + placeholder="请输入链接地址" + /> + + + + setLinkData(prev => ({ ...prev, desc: e.target.value })) + } + placeholder="请输入链接描述" + /> + + + + setLinkData(prev => ({ ...prev, image: urls[0] || "" })) + } + count={1} + /> + + + )} +
+
+ ); +}; + +export default EditMomentModal; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss new file mode 100644 index 00000000..53c6ff14 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss @@ -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%; + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx new file mode 100644 index 00000000..1a740746 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx @@ -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 = ({ onPublishSuccess }) => { + const [selectedAccounts, setSelectedAccounts] = useState([]); + const [content, setContent] = useState(""); + // 发布类型:1文本 2图文 3视频 4链接 + const [contentType, setContentType] = useState(1); + const [sendTime, setSendTime] = useState(""); + const [resUrls, setResUrls] = useState([]); // 图片/视频等资源 + const [comment, setComment] = useState(""); + // 链接 + const [linkDesc, setLinkDesc] = useState(""); + const [linkImage, setLinkImage] = useState(""); + const [linkUrl, setLinkUrl] = useState(""); + const [accounts, setAccounts] = useState([]); + const [loading, setLoading] = useState(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) => { + 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 ( +
+

发布朋友圈

+ + {/* 选择发布账号 */} +
+

选择发布账号

+
+ {accounts.map(account => ( + handleAccountSelect(account.id.toString())} + > +
+
{account.name}
+ = account.momentsMax ? "red" : "blue" + } + /> + {account.isDisabled && ( +
今日已达上限
+ )} +
+
+ ))} +
+
+ + {/* 朋友圈内容 */} +
+ {/* 发布时间 */} +
+ + setSendTime((e.target as HTMLInputElement).value)} + placeholder="请选择发布时间" + className={styles.contentInput} + /> +
+ + {/* 类型选择 */} +
+ +
+ +
+
+ + {/* 文本内容 */} +
+ + +
+ + {/* 链接类型 */} + {contentType === 2 && ( + <> +
+ + + setLinkDesc((e.target as HTMLInputElement).value) + } + placeholder="请输入描述" + className={styles.contentInput} + /> +
+
+ + setLinkImage(urls[0] || "")} + count={1} + /> +
+
+ + setLinkUrl((e.target as HTMLInputElement).value)} + placeholder="请输入链接地址" + className={styles.contentInput} + /> +
+ + )} + + {/* 视频类型 */} + {contentType === 3 && ( +
+ +
+ setResUrls([url as string])} + /> +
+
+ )} + + {/* 图片/小程序 素材上传 */} + {contentType === 2 && ( +
+ + +
+ )} + + {/* 备注 */} +
+ + setComment((e.target as HTMLTextAreaElement).value)} + placeholder="请输入备注" + className={styles.contentInput} + rows={4} + /> +
+
+ + {/* 发布按钮 */} +
+ +
+
+ ); +}; + +export default MomentPublish; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx new file mode 100644 index 00000000..8f72a505 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx @@ -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 = ({ + visible, + onCancel, + momentData, +}) => { + if (!momentData) return null; + + // 获取内容类型信息 + const getContentTypeInfo = (type: number) => { + switch (type) { + case 1: + return { icon: , label: "文本", color: "blue" }; + case 2: + return { icon: , label: "图文", color: "green" }; + case 3: + return { + icon: , + label: "视频", + color: "purple", + }; + case 4: + return { icon: , label: "链接", color: "orange" }; + default: + return { icon: , 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 ( + + +
+
+ + + {contentTypeInfo.label} + +
+
+ +
+
{momentData.text || "无文本内容"}
+ + {/* 图片预览 */} + {momentData.picUrlList && momentData.picUrlList.length > 0 && ( +
+ {momentData.picUrlList.map((image, index) => ( +
+ {`图片${index { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
+ ))} +
+ )} + + {/* 视频预览 */} + {momentData.videoUrl && ( +
+
+ +
+ 视频内容 +
+ )} + + {/* 链接预览 */} + {momentData.link && momentData.link.length > 0 && ( +
+ + + {momentData.link.length} 个链接 + +
+ )} +
+ +
+
+ 发布时间: + + {formatTime(momentData.sendTime)} + +
+
+ 账号数量: + + {momentData.accountCount} 个账号 + +
+
+ 创建时间: + + {formatTime(momentData.createTime)} + +
+
+
+ + +
+ ); +}; + +export default PreviewMomentModal; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss new file mode 100644 index 00000000..20a384e9 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss @@ -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; + } + } + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx new file mode 100644 index 00000000..4bec521f --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx @@ -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((props, ref) => { + const [scheduledPosts, setScheduledPosts] = useState([]); + const [loading, setLoading] = useState(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(); + + // 获取内容类型图标和标签 + const getContentTypeInfo = (type: number) => { + switch (type) { + case 1: + return { icon: , label: "文本", color: "blue" }; + case 2: + return { icon: , label: "图文", color: "green" }; + case 3: + return { + icon: , + label: "视频", + color: "purple", + }; + case 4: + return { icon: , label: "链接", color: "orange" }; + case 5: + return { icon: , label: "小程序", color: "cyan" }; + default: + return { icon: , 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 ; + case 1: + return ; + case 2: + return ; + default: + return ; + } + }; + + const handleRefresh = () => { + fetchScheduledMoments(pagination.page); + }; + + return ( +
+
+

发布计划

+
+ +
+
+ +
+ {loading ? ( +
+ +
加载中...
+
+ ) : scheduledPosts.length === 0 ? ( + +
暂无发布计划
+
+ 创建朋友圈内容后可以设置定时发布 +
+
+ } + /> + ) : ( + scheduledPosts.map(post => { + const contentTypeInfo = getContentTypeInfo(post.momentContentType); + return ( + +
+
+
+ {getStatusBadge(post.isSend)} +
+ + {contentTypeInfo.label} + +
+
+ +
+
+ +
+
+ {post.text || "无文本内容"} +
+ + {/* 图片展示 */} + {post.picUrlList && post.picUrlList.length > 0 && ( +
+ {post.picUrlList.slice(0, 3).map((image, index) => ( +
+ {`图片${index { + (e.target as HTMLImageElement).style.display = + "none"; + ( + e.target as HTMLImageElement + ).nextElementSibling?.classList.add(styles.show); + }} + /> +
+ +
+
+ ))} + {post.picUrlList.length > 3 && ( +
+ +{post.picUrlList.length - 3} +
+ )} +
+ )} + + {/* 视频展示 */} + {post.videoUrl && ( +
+
+ +
+ 视频内容 +
+ )} + + {/* 链接展示 */} + {post.link && post.link.length > 0 && ( +
+ + + {post.link.length} 个链接 + +
+ )} + +
+
+ + 发布时间: + + {formatTime(post.sendTime)} + +
+
+ + 账号数量: + + {post.accountCount} 个账号 + +
+
+ 创建时间: + + {formatTime(post.createTime)} + +
+
+
+
+ ); + }) + )} +
+ + {/* 分页信息 */} + {scheduledPosts.length > 0 && ( +
+ + 共 {pagination.total} 条记录,当前第 {pagination.page} 页 + +
+ )} + + {/* 编辑弹窗 */} + setEditModalVisible(false)} + onSuccess={handleEditSuccess} + momentData={selectedMoment} + /> + + {/* 预览弹窗 */} + setPreviewModalVisible(false)} + momentData={selectedMoment} + /> + + ); +}); + +PublishSchedule.displayName = "PublishSchedule"; + +export default PublishSchedule; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts new file mode 100644 index 00000000..23041afd --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts @@ -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 => { + 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"); +}; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts index 8acd8cb1..2e65eec3 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts @@ -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"; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx deleted file mode 100644 index e3b94ad8..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/KeywordManagement.tsx +++ /dev/null @@ -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>( - (props, ref) => { - const [searchValue, setSearchValue] = useState(""); - const [keywordsList, setKeywordsList] = useState([]); - const [loading, setLoading] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - const [editingKeywordId, setEditingKeywordId] = useState( - null, - ); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - // 已提交的搜索关键词(仅在点击搜索时更新,用于服务端查询) - const [keywordQuery, setKeywordQuery] = useState(""); - - //匹配类型 - const getMatchTypeText = (type: number) => { - switch (type) { - case 0: - return "模糊匹配"; - case 1: - return "精确匹配"; - } - }; - - //匹配优先级 - const getPriorityText = (level: number) => { - switch (level) { - case 0: - return "低优先级"; - case 1: - return "中优先级"; - case 2: - return "高优先级"; - } - }; - // 回复类型映射 - const getReplyTypeText = (replyType: 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 ( -
-
- setSearchValue(e.target.value)} - onSearch={handleSearch} - style={{ width: 300 }} - prefix={} - /> -
- -
- {loading ? ( -
加载中...
- ) : keywordsList.length === 0 ? ( -
暂无关键词数据
- ) : ( - keywordsList.map(item => ( -
-
-
-
-
{item.title}
- {getMatchTypeText(item.type)} - {getPriorityText(item.level)} -
- {item.content.length ? ( -
{item.content}
- ) : ( -
- 已选素材: - {item.metailGroupsOptions.map(v => ( - - {v.title} - - ))} -
- )} -
- - {getReplyTypeText(item.replyType)} - -
-
-
- handleToggleKeyword(item.id)} - className={styles.toggleSwitch} - /> -
-
-
- )) - )} -
- - {/* 分页组件 */} -
- - `第 ${range[0]}-${range[1]} 条/共 ${total} 条` - } - onChange={handlePageChange} - onShowSizeChange={handlePageChange} - /> -
- - {/* 编辑弹窗 */} - { - setEditModalVisible(false); - setEditingKeywordId(null); - }} - onSuccess={handleEditSuccess} - /> -
- ); - }, -); - -KeywordManagement.displayName = "KeywordManagement"; - -export default KeywordManagement; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/MaterialManagement.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/MaterialManagement.tsx deleted file mode 100644 index 65411fd6..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/MaterialManagement.tsx +++ /dev/null @@ -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>( - (props, ref) => { - const [searchValue, setSearchValue] = useState(""); - const [materialsList, setMaterialsList] = useState([]); - const [loading, setLoading] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - const [editingMaterialId, setEditingMaterialId] = useState( - null, - ); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - // 使用 ref 来存储最新的分页状态 - const paginationRef = useRef(pagination); - paginationRef.current = pagination; - - // 获取类型图标 - const getTypeIcon = (type: string) => { - switch (type) { - case "文本": - return ; - case "图片": - return ; - case "视频": - return ; - default: - return ; - } - }; - - // 获取素材列表 - 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 ( -
-
- setSearchValue(e.target.value)} - onSearch={handleSearch} - style={{ width: 300 }} - prefix={} - /> - -
- -
- {loading ? ( -
加载中...
- ) : materialsList.length === 0 ? ( -
暂无素材数据
- ) : ( - materialsList.map(item => ( - } - onClick={e => { - e.stopPropagation(); - handleEditMaterial(item.id); - }} - > - 编辑 - , - handleDeleteMaterial(item.id)} - okText="确定" - cancelText="取消" - okType="danger" - > - - , - ]} - > -
handleEditMaterial(item.id)} - style={{ cursor: "pointer" }} - > - {item.cover ? ( -
- {item.title} -
- {item.type} -
-
- ) : ( -
- {getTypeIcon(item.type)} - - {item.type} - -
- )} -
- -
-
{item.title}
-
-
创建人: {item.userName}
-
{item.createTime}
-
-
-
- )) - )} -
- - {/* 分页组件 */} -
- - `第 ${range[0]}-${range[1]} 条/共 ${total} 条` - } - onChange={handlePageChange} - onShowSizeChange={handlePageChange} - /> -
- - {/* 编辑弹窗 */} - { - setEditModalVisible(false); - setEditingMaterialId(null); - }} - onSuccess={handleEditSuccess} - /> -
- ); - }, -); - -MaterialManagement.displayName = "MaterialManagement"; - -export default MaterialManagement; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/SensitiveWordManagement.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/SensitiveWordManagement.tsx deleted file mode 100644 index 7979be4c..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/SensitiveWordManagement.tsx +++ /dev/null @@ -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>( - (props, ref) => { - const [searchValue, setSearchValue] = useState(""); - const [sensitiveWordsList, setSensitiveWordsList] = useState< - SensitiveWordItem[] - >([]); - const [loading, setLoading] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - const [editingSensitiveWordId, setEditingSensitiveWordId] = useState< - string | null - >(null); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - const getTagColor = (tag: string) => { - switch (tag) { - case "政治": - return "#ff4d4f"; - case "色情": - return "#ff4d4f"; - case "暴力": - return "#ff4d4f"; - default: - return "#ff4d4f"; - } - }; - - // 操作类型映射 - const 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 ( -
-
- setSearchValue(e.target.value)} - onSearch={handleSearch} - style={{ width: 300 }} - prefix={} - /> - -
- -
- {loading ? ( -
加载中...
- ) : filteredSensitiveWords.length === 0 ? ( -
暂无敏感词数据
- ) : ( - filteredSensitiveWords.map(item => ( -
-
-
{item.title}
-
- {getOperationText(item.operation)} -
-
-
- handleToggleSensitiveWord(item.id)} - className={styles.toggleSwitch} - /> -
-
- )) - )} -
- - {/* 分页组件 */} -
- - `第 ${range[0]}-${range[1]} 条/共 ${total} 条` - } - onChange={handlePageChange} - onShowSizeChange={handlePageChange} - /> -
- - {/* 编辑弹窗 */} - { - setEditModalVisible(false); - setEditingSensitiveWordId(null); - }} - onSuccess={handleEditSuccess} - /> -
- ); - }, -); - -SensitiveWordManagement.displayName = "SensitiveWordManagement"; - -export default SensitiveWordManagement; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss deleted file mode 100644 index 52943078..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.module.scss +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.ts deleted file mode 100644 index deff774a..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/management/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 管理组件统一导出 -export { default as MaterialManagement } from "./MaterialManagement"; -export { default as SensitiveWordManagement } from "./SensitiveWordManagement"; -export { default as KeywordManagement } from "./KeywordManagement"; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/ContentManager.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/ContentManager.tsx deleted file mode 100644 index 915f2de6..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/components/modals/ContentManager.tsx +++ /dev/null @@ -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 = ({ - value = [], - onChange, -}) => { - const [contentItems, setContentItems] = useState(value); - - // 内容类型配置 - const contentTypes = [ - { value: "text", label: "文本", icon: }, - { value: "image", label: "图片", icon: }, - { value: "video", label: "视频", icon: }, - { value: "file", label: "文件", icon: }, - { value: "audio", label: "音频", icon: }, - { value: "link", label: "链接", icon: }, - ]; - - // 同步外部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 ( -
-
-
- -
- {index !== 0 && ( -
- - {renderContentInput(item, index)} -
- ); - }; - - // 渲染内容输入 - const renderContentInput = (item: ContentItem, index: number) => { - switch (item.type) { - case "text": - return ( -