Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop
This commit is contained in:
@@ -1,147 +0,0 @@
|
||||
.chatFooter {
|
||||
background: #f7f7f7;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: #e6e6e6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
min-width: 60px;
|
||||
font-size: 13px;
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
|
||||
&:hover {
|
||||
background: #06ad56;
|
||||
border-color: #06ad56;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #059748;
|
||||
border-color: #059748;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #b3b3b3;
|
||||
border-color: #b3b3b3;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hintButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inputToolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
.stepContent {
|
||||
.stepHeader {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
|
||||
.leftColumn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 400px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
border: 2px dashed #52c41a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f6ffed;
|
||||
|
||||
.previewTitle {
|
||||
font-size: 14px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.messageBubble {
|
||||
min-height: 60px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
.currentEditingLabel {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.savedScriptGroups {
|
||||
.scriptGroupTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scriptGroupItem {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
|
||||
.scriptGroupHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.scriptGroupLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
:global(.ant-radio) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.scriptGroupName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.actionButton {
|
||||
padding: 4px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupContent {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageInputArea {
|
||||
.messageInput {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.attachmentButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aiRewriteSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageHint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsPanel {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
|
||||
.settingsTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settingItem {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settingLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.settingControl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tagSection {
|
||||
.settingLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pushPreview {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f0f7ff;
|
||||
|
||||
.previewTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.step3Content {
|
||||
.rightColumn {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.step3Content {
|
||||
flex-direction: column;
|
||||
|
||||
.leftColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import ContentSelection from "@/components/ContentSelection";
|
||||
import { ContentItem } from "@/components/ContentSelection/data";
|
||||
import InputMessage from "./InputMessage/InputMessage";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface StepSendMessageProps {
|
||||
@@ -11,6 +11,10 @@
|
||||
</style>
|
||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
||||
<script type="text/javascript" src="/websdk.js"></script>
|
||||
<script
|
||||
charset="utf-8"
|
||||
src="https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
LogoutOutlined,
|
||||
ThunderboltOutlined,
|
||||
SettingOutlined,
|
||||
CalendarOutlined,
|
||||
SendOutlined,
|
||||
ClearOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { noticeList, readMessage, readAll } from "./api";
|
||||
@@ -317,10 +317,10 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
></Button>
|
||||
|
||||
<Button
|
||||
icon={<CalendarOutlined />}
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleContentManagementClick}
|
||||
>
|
||||
内容管理
|
||||
发朋友圈
|
||||
</Button>
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ const ContentManagement: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PowerNavigation
|
||||
title="内容管理"
|
||||
title="发朋友圈"
|
||||
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
|
||||
showBackButton={true}
|
||||
backButtonText="返回功能中心"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { TeamOutlined, CommentOutlined, BookOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
TeamOutlined,
|
||||
CommentOutlined,
|
||||
BookOutlined,
|
||||
SendOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
// 数据类型定义
|
||||
export interface FeatureCard {
|
||||
@@ -56,21 +61,21 @@ export const featureCategories: FeatureCard[] = [
|
||||
],
|
||||
path: "/pc/powerCenter/ai-reception",
|
||||
},
|
||||
{
|
||||
id: "content-library",
|
||||
title: "AI内容库配置",
|
||||
description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量",
|
||||
icon: <BookOutlined style={{ fontSize: "32px", color: "#52c41a" }} />,
|
||||
color: "#52c41a",
|
||||
tag: "内容管理",
|
||||
features: [
|
||||
"多库管理与分类",
|
||||
"AI调用权限配置",
|
||||
"内容检索规则设置",
|
||||
"手动内容上传",
|
||||
],
|
||||
path: "/pc/powerCenter/content-library",
|
||||
},
|
||||
// {
|
||||
// id: "content-library",
|
||||
// title: "AI内容库配置",
|
||||
// description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量",
|
||||
// icon: <BookOutlined style={{ fontSize: "32px", color: "#52c41a" }} />,
|
||||
// color: "#52c41a",
|
||||
// tag: "内容管理",
|
||||
// features: [
|
||||
// "多库管理与分类",
|
||||
// "AI调用权限配置",
|
||||
// "内容检索规则设置",
|
||||
// "手动内容上传",
|
||||
// ],
|
||||
// path: "/pc/powerCenter/content-library",
|
||||
// },
|
||||
{
|
||||
id: "message-push-assistant",
|
||||
title: "消息推送助手",
|
||||
|
||||
@@ -39,18 +39,6 @@ const PowerCenter: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.powerCenter}>
|
||||
{/* 页面标题区域 */}
|
||||
<div className={styles.pageHeader}>
|
||||
<div className={styles.titleSection}>
|
||||
<div className={styles.mainTitle}>
|
||||
<div className={styles.titleIcon}>⭐</div>
|
||||
<h1>功能中心</h1>
|
||||
</div>
|
||||
<p className={styles.subtitle}>
|
||||
AI智能营销·一站式客户管理·高效业务增长
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI统计区域(置顶,按图展示) */}
|
||||
<div className={styles.kpiSection}>
|
||||
<Row gutter={16}>
|
||||
@@ -157,9 +145,11 @@ const PowerCenter: React.FC = () => {
|
||||
{card.features.map((feature, index) => (
|
||||
<li
|
||||
key={index}
|
||||
style={{
|
||||
"--dot-color": card.color,
|
||||
} as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
"--dot-color": card.color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{feature}
|
||||
</li>
|
||||
@@ -186,7 +176,7 @@ const PowerCenter: React.FC = () => {
|
||||
className={styles.cardIcon}
|
||||
style={{
|
||||
backgroundColor: getIconBgColor(
|
||||
featureCategories[3].color
|
||||
featureCategories[3].color,
|
||||
),
|
||||
}}
|
||||
>
|
||||
@@ -212,9 +202,11 @@ const PowerCenter: React.FC = () => {
|
||||
{featureCategories[3].features.map((feature, index) => (
|
||||
<li
|
||||
key={index}
|
||||
style={{
|
||||
"--dot-color": featureCategories[3].color,
|
||||
} as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
"--dot-color": featureCategories[3].color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{feature}
|
||||
</li>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
.rightColumn {
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
@@ -746,10 +746,6 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.scriptGroupContent}>
|
||||
{group.messages[0]}
|
||||
{group.messages.length > 1 && " ..."}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
帮我对接数据,以下是传参实例,三种模式都是同一界面的。
|
||||
|
||||
群发助手传参实例
|
||||
{
|
||||
"name": "群群发-新品宣传", // 任务名称
|
||||
"type": 3, // 工作台类型:3=群消息推送
|
||||
"autoStart": 1, // 保存后自动启动
|
||||
"status": 1, // 是否启用
|
||||
"pushType": 0, // 推送方式:0=定时,1=立即
|
||||
"targetType": 1, // 目标类型:1=群推送
|
||||
"groupPushSubType": 1, // 群推送子类型:1=群群发,2=群公告
|
||||
"startTime": "09:00", // 推送起始时间
|
||||
"endTime": "20:00", // 推送结束时间
|
||||
"maxPerDay": 200, // 每日最大推送群数
|
||||
"pushOrder": 1, // 推送顺序:1=最早优先,2=最新优先
|
||||
"wechatGroups": [102, 205, 318], // 选择的微信群 ID 列表
|
||||
"contentGroups": [11, 12], // 关联内容库 ID 列表
|
||||
"friendIntervalMin": 10, // 群间最小间隔(秒)
|
||||
"friendIntervalMax": 25, // 群间最大间隔(秒)
|
||||
"messageIntervalMin": 2, // 同一群消息间最小间隔(秒)
|
||||
"messageIntervalMax": 6, // 同一群消息间最大间隔(秒)
|
||||
"isRandomTemplate": 1, // 是否随机选择话术模板
|
||||
"postPushTags": [301, 302], // 推送完成后打的标签
|
||||
ownerWechatIds:[123123,1231231] //客服id
|
||||
}
|
||||
|
||||
//群公告传参实例
|
||||
{
|
||||
"name": "群公告-双11活动", // 任务名称
|
||||
"type": 3, // 群消息推送
|
||||
"autoStart": 0, // 不自动启动
|
||||
"status": 1, // 启用
|
||||
"pushType": 1, // 立即推送
|
||||
"targetType": 1, // 群推送
|
||||
"groupPushSubType": 2, // 群公告
|
||||
"startTime": "08:30", // 开始时间
|
||||
"endTime": "18:30", // 结束时间
|
||||
"maxPerDay": 80, // 每日最大公告数
|
||||
"pushOrder": 2, // 最新优先
|
||||
"wechatGroups": [5021, 5026], // 公告目标群
|
||||
"announcementContent": "…", // 公告正文
|
||||
"enableAiRewrite": 1, // 启用 AI 改写
|
||||
"aiRewritePrompt": "保持活泼口吻…", // AI 改写提示词
|
||||
"contentGroups": [21], // 关联内容库
|
||||
"friendIntervalMin": 15, // 群间最小间隔
|
||||
"friendIntervalMax": 30, // 群间最大间隔
|
||||
"messageIntervalMin": 3, // 消息间最小间隔
|
||||
"messageIntervalMax": 9, // 消息间最大间隔
|
||||
"isRandomTemplate": 0, // 不随机模板
|
||||
"postPushTags": [], // 推送后标签
|
||||
ownerWechatIds:[123123,1231231] //客服id
|
||||
}
|
||||
|
||||
//好友传参实例
|
||||
{
|
||||
"name": "好友私聊-新客转化", // 任务名称
|
||||
"type": 3, // 群消息推送
|
||||
"autoStart": 1, // 自动启动
|
||||
"status": 1, // 启用
|
||||
"pushType": 0, // 定时推送
|
||||
"targetType": 2, // 目标类型:2=好友推送
|
||||
"groupPushSubType": 1, // 固定为群群发(好友推送不支持公告)
|
||||
"startTime": "10:00", // 开始时间
|
||||
"endTime": "22:00", // 结束时间
|
||||
"maxPerDay": 150, // 每日最大推送好友数
|
||||
"pushOrder": 1, // 最早优先
|
||||
"wechatFriends": ["12312"], // 指定好友列表(可为空数组)
|
||||
"deviceGroups": [9001, 9002], // 必选:推送设备分组 ID
|
||||
"contentGroups": [41, 42], // 话术内容库
|
||||
"friendIntervalMin": 12, // 好友间最小间隔
|
||||
"friendIntervalMax": 28, // 好友间最大间隔
|
||||
"messageIntervalMin": 4, // 消息间最小间隔
|
||||
"messageIntervalMax": 10, // 消息间最大间隔
|
||||
"isRandomTemplate": 1, // 随机话术
|
||||
"postPushTags": [501], // 推送后标签
|
||||
ownerWechatIds:[123123,1231231] //客服id
|
||||
}
|
||||
|
||||
请求接口是 queryWorkbenchCreate
|
||||
@@ -6,8 +6,10 @@ export interface GetPushHistoryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
pushType?: string;
|
||||
status?: string;
|
||||
pushTypeCode?: string; // 推送类型代码:friend, group, announcement
|
||||
status?: string; // 状态:pending, completed, failed
|
||||
workbenchId?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
// 获取推送历史接口响应
|
||||
@@ -27,11 +29,30 @@ export interface GetPushHistoryResponse {
|
||||
*/
|
||||
export interface GetGroupPushHistoryParams {
|
||||
keyword?: string;
|
||||
limit: string;
|
||||
page: string;
|
||||
limit?: string | number;
|
||||
page?: string | number;
|
||||
pageSize?: string | number;
|
||||
pushTypeCode?: string;
|
||||
status?: string;
|
||||
workbenchId?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
export const getPushHistory = async (params: GetGroupPushHistoryParams) => {
|
||||
return request("/v1/workbench/group-push-history", params, "GET");
|
||||
|
||||
export const getPushHistory = async (
|
||||
params: GetGroupPushHistoryParams,
|
||||
): Promise<GetPushHistoryResponse> => {
|
||||
// 转换参数格式,确保 limit 和 page 是字符串
|
||||
const requestParams: Record<string, any> = {
|
||||
...params,
|
||||
};
|
||||
|
||||
if (params.page !== undefined) {
|
||||
requestParams.page = String(params.page);
|
||||
}
|
||||
|
||||
if (params.pageSize !== undefined) {
|
||||
requestParams.limit = String(params.pageSize);
|
||||
}
|
||||
|
||||
return request("/v1/workbench/group-push-history", requestParams, "GET");
|
||||
};
|
||||
|
||||
@@ -15,30 +15,33 @@ import styles from "./index.module.scss";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// 推送类型枚举
|
||||
export enum PushType {
|
||||
FRIEND_MESSAGE = "friend-message", // 好友消息
|
||||
GROUP_MESSAGE = "group-message", // 群消息
|
||||
GROUP_ANNOUNCEMENT = "group-announcement", // 群公告
|
||||
// 推送类型代码枚举
|
||||
export enum PushTypeCode {
|
||||
FRIEND = "friend", // 好友消息
|
||||
GROUP = "group", // 群消息
|
||||
ANNOUNCEMENT = "announcement", // 群公告
|
||||
}
|
||||
|
||||
// 推送状态枚举
|
||||
export enum PushStatus {
|
||||
PENDING = "pending", // 进行中
|
||||
COMPLETED = "completed", // 已完成
|
||||
IN_PROGRESS = "in-progress", // 进行中
|
||||
FAILED = "failed", // 失败
|
||||
}
|
||||
|
||||
// 推送历史记录接口
|
||||
export interface PushHistoryRecord {
|
||||
id: string;
|
||||
pushType: PushType;
|
||||
pushContent: string;
|
||||
workbenchId: number;
|
||||
taskName: string;
|
||||
pushType: string; // 推送类型中文名称,如 "好友消息"
|
||||
pushTypeCode: string; // 推送类型代码,如 "friend"
|
||||
targetCount: number;
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
status: PushStatus;
|
||||
failCount: number;
|
||||
status: string; // 状态代码,如 "pending"
|
||||
statusText: string; // 状态中文名称,如 "进行中"
|
||||
createTime: string;
|
||||
contentLibraryName: string; // 内容库名称
|
||||
}
|
||||
|
||||
const PushHistory: React.FC = () => {
|
||||
@@ -59,8 +62,8 @@ const PushHistory: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: any = {
|
||||
page,
|
||||
pageSize: pagination.pageSize,
|
||||
page: String(page),
|
||||
limit: String(pagination.pageSize),
|
||||
};
|
||||
|
||||
if (searchValue.trim()) {
|
||||
@@ -68,7 +71,7 @@ const PushHistory: React.FC = () => {
|
||||
}
|
||||
|
||||
if (typeFilter !== "all") {
|
||||
params.pushType = typeFilter;
|
||||
params.pushTypeCode = typeFilter;
|
||||
}
|
||||
|
||||
if (statusFilter !== "all") {
|
||||
@@ -157,13 +160,33 @@ const PushHistory: React.FC = () => {
|
||||
};
|
||||
|
||||
// 获取推送类型标签
|
||||
const getPushTypeTag = (type: PushType) => {
|
||||
const typeMap = {
|
||||
[PushType.FRIEND_MESSAGE]: { text: "好友消息", color: "#666" },
|
||||
[PushType.GROUP_MESSAGE]: { text: "群消息", color: "#666" },
|
||||
[PushType.GROUP_ANNOUNCEMENT]: { text: "群公告", color: "#666" },
|
||||
const getPushTypeTag = (pushType: string, pushTypeCode?: string) => {
|
||||
// 优先使用中文名称,如果没有则根据代码映射
|
||||
if (pushType) {
|
||||
const colorMap: Record<string, string> = {
|
||||
好友消息: "#1890ff",
|
||||
群消息: "#52c41a",
|
||||
群公告: "#722ed1",
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
color={colorMap[pushType] || "#666"}
|
||||
style={{ borderRadius: "12px" }}
|
||||
>
|
||||
{pushType}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
// 如果没有中文名称,根据代码映射
|
||||
const codeMap: Record<string, { text: string; color: string }> = {
|
||||
[PushTypeCode.FRIEND]: { text: "好友消息", color: "#1890ff" },
|
||||
[PushTypeCode.GROUP]: { text: "群消息", color: "#52c41a" },
|
||||
[PushTypeCode.ANNOUNCEMENT]: { text: "群公告", color: "#722ed1" },
|
||||
};
|
||||
const config = typeMap[type] || { text: "未知", color: "#666" };
|
||||
const config =
|
||||
pushTypeCode && codeMap[pushTypeCode]
|
||||
? codeMap[pushTypeCode]
|
||||
: { text: pushType || "未知", color: "#666" };
|
||||
return (
|
||||
<Tag color={config.color} style={{ borderRadius: "12px" }}>
|
||||
{config.text}
|
||||
@@ -172,14 +195,31 @@ const PushHistory: React.FC = () => {
|
||||
};
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status: PushStatus) => {
|
||||
const statusMap = {
|
||||
const getStatusTag = (status: string, statusText?: string) => {
|
||||
// 优先使用中文状态文本
|
||||
const displayText = statusText || status;
|
||||
|
||||
// 根据状态代码或文本匹配
|
||||
const statusMap: Record<
|
||||
string,
|
||||
{ text: string; color: string; icon: React.ReactNode }
|
||||
> = {
|
||||
[PushStatus.COMPLETED]: {
|
||||
text: "已完成",
|
||||
color: "#52c41a",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[PushStatus.IN_PROGRESS]: {
|
||||
completed: {
|
||||
text: "已完成",
|
||||
color: "#52c41a",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[PushStatus.PENDING]: {
|
||||
text: "进行中",
|
||||
color: "#1890ff",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
pending: {
|
||||
text: "进行中",
|
||||
color: "#1890ff",
|
||||
icon: <ClockCircleOutlined />,
|
||||
@@ -189,12 +229,43 @@ const PushHistory: React.FC = () => {
|
||||
color: "#ff4d4f",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
failed: {
|
||||
text: "失败",
|
||||
color: "#ff4d4f",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
const config = statusMap[status] || {
|
||||
text: "未知",
|
||||
color: "#666",
|
||||
icon: null,
|
||||
|
||||
// 根据状态文本匹配
|
||||
const textMap: Record<
|
||||
string,
|
||||
{ text: string; color: string; icon: React.ReactNode }
|
||||
> = {
|
||||
已完成: {
|
||||
text: "已完成",
|
||||
color: "#52c41a",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
进行中: {
|
||||
text: "进行中",
|
||||
color: "#1890ff",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
失败: {
|
||||
text: "失败",
|
||||
color: "#ff4d4f",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
const config = textMap[displayText] ||
|
||||
statusMap[status] ||
|
||||
statusMap[status.toLowerCase()] || {
|
||||
text: displayText,
|
||||
color: "#666",
|
||||
icon: null,
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={config.color}
|
||||
@@ -217,15 +288,26 @@ const PushHistory: React.FC = () => {
|
||||
dataIndex: "pushType",
|
||||
key: "pushType",
|
||||
width: 120,
|
||||
render: (type: PushType) => getPushTypeTag(type),
|
||||
render: (pushType: string, record: PushHistoryRecord) =>
|
||||
getPushTypeTag(pushType, record.pushTypeCode),
|
||||
},
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "pushContent",
|
||||
key: "pushContent",
|
||||
dataIndex: "taskName",
|
||||
key: "taskName",
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: "#333" }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "内容库",
|
||||
dataIndex: "contentLibraryName",
|
||||
key: "contentLibraryName",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: "#666", fontSize: "13px" }}>{text || "-"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "目标数量",
|
||||
dataIndex: "targetCount",
|
||||
@@ -246,8 +328,8 @@ const PushHistory: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: "失败数",
|
||||
dataIndex: "failureCount",
|
||||
key: "failureCount",
|
||||
dataIndex: "failCount",
|
||||
key: "failCount",
|
||||
width: 100,
|
||||
align: "center" as const,
|
||||
render: (count: number) => (
|
||||
@@ -260,7 +342,8 @@ const PushHistory: React.FC = () => {
|
||||
key: "status",
|
||||
width: 120,
|
||||
align: "center" as const,
|
||||
render: (status: PushStatus) => getStatusTag(status),
|
||||
render: (status: string, record: PushHistoryRecord) =>
|
||||
getStatusTag(status, record.statusText),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
@@ -329,9 +412,9 @@ const PushHistory: React.FC = () => {
|
||||
suffixIcon={<span>▼</span>}
|
||||
>
|
||||
<Option value="all">全部类型</Option>
|
||||
<Option value={PushType.FRIEND_MESSAGE}>好友消息</Option>
|
||||
<Option value={PushType.GROUP_MESSAGE}>群消息</Option>
|
||||
<Option value={PushType.GROUP_ANNOUNCEMENT}>群公告</Option>
|
||||
<Option value={PushTypeCode.FRIEND}>好友消息</Option>
|
||||
<Option value={PushTypeCode.GROUP}>群消息</Option>
|
||||
<Option value={PushTypeCode.ANNOUNCEMENT}>群公告</Option>
|
||||
</Select>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
@@ -340,8 +423,8 @@ const PushHistory: React.FC = () => {
|
||||
suffixIcon={<span>▼</span>}
|
||||
>
|
||||
<Option value="all">全部状态</Option>
|
||||
<Option value={PushStatus.PENDING}>进行中</Option>
|
||||
<Option value={PushStatus.COMPLETED}>已完成</Option>
|
||||
<Option value={PushStatus.IN_PROGRESS}>进行中</Option>
|
||||
<Option value={PushStatus.FAILED}>失败</Option>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -353,7 +436,7 @@ const PushHistory: React.FC = () => {
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
rowKey="workbenchId"
|
||||
pagination={false}
|
||||
className={styles.dataTable}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
.selectMapContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 600px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.searchArea {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10001;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
margin-top: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
pointer-events: auto;
|
||||
|
||||
:global(.ant-list-item) {
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mapArea {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.loadingOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.locationInfo {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.locationLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.locationText {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.locationCoords {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
}
|
||||
|
||||
.resultItem {
|
||||
:global(.ant-list-item-meta-title) {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:global(.ant-list-item-meta-description) {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Layout, Input, Button, Modal, message, Tooltip } from "antd";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
Layout,
|
||||
Input,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Tooltip,
|
||||
AutoComplete,
|
||||
Input as AntInput,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import {
|
||||
SendOutlined,
|
||||
FolderOutlined,
|
||||
@@ -8,6 +18,7 @@ import {
|
||||
CloseOutlined,
|
||||
MessageOutlined,
|
||||
ReloadOutlined,
|
||||
EnvironmentOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
@@ -23,6 +34,7 @@ import {
|
||||
manualTriggerAi,
|
||||
} from "@/store/module/weChat/weChat";
|
||||
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||
import SelectMap from "./components/selectMap";
|
||||
const { Footer } = Layout;
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -326,6 +338,8 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
updateShowChatRecordModel(!showChatRecordModel);
|
||||
};
|
||||
|
||||
const [mapVisible, setMapVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 聊天输入 */}
|
||||
@@ -423,6 +437,12 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
<Button
|
||||
className={styles.toolbarButton}
|
||||
type="text"
|
||||
icon={<EnvironmentOutlined />}
|
||||
onClick={() => setMapVisible(true)}
|
||||
/>
|
||||
|
||||
{/* AI模式下显示重新生成按钮 */}
|
||||
{(isAiAssist || isAiTakeover) && (
|
||||
@@ -502,7 +522,12 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
</>
|
||||
)}
|
||||
</Footer>
|
||||
、
|
||||
<SelectMap
|
||||
visible={mapVisible}
|
||||
onClose={() => setMapVisible(false)}
|
||||
contract={contract}
|
||||
addMessage={addMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
// 位置消息基础样式
|
||||
.locationMessage {
|
||||
max-width: 420px;
|
||||
width: 420px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
@@ -33,6 +35,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 地图预览区域
|
||||
.mapPreview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mapImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mapPlaceholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
|
||||
span:first-child {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
// 位置消息头部
|
||||
.locationHeader {
|
||||
display: flex;
|
||||
@@ -70,6 +111,21 @@
|
||||
// 位置消息内容
|
||||
.locationContent {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.roadName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.poiName {
|
||||
@@ -89,9 +145,8 @@
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -175,13 +230,17 @@
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.locationMessage {
|
||||
max-width: 280px;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.locationCard {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mapPreview {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.locationHeader {
|
||||
padding: 10px 14px 6px;
|
||||
}
|
||||
@@ -253,6 +312,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mapPreview {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.mapPlaceholder {
|
||||
background: #2a2a2a;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.locationHeader {
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
|
||||
border-bottom-color: #333;
|
||||
|
||||
@@ -85,10 +85,36 @@ const LocationMessage: React.FC<LocationMessageProps> = ({ content }) => {
|
||||
return renderErrorMessage("[位置消息 - 解析失败]");
|
||||
}
|
||||
|
||||
// 生成地图链接
|
||||
// 格式化经纬度为6位小数
|
||||
const formatCoordinate = (coord: string): string => {
|
||||
const num = parseFloat(coord);
|
||||
if (isNaN(num)) {
|
||||
return coord; // 如果无法解析,返回原值
|
||||
}
|
||||
return num.toFixed(6);
|
||||
};
|
||||
|
||||
// 生成地图链接(用于点击跳转)
|
||||
const generateMapUrl = (lat: string, lng: string, label: string) => {
|
||||
const formattedLat = formatCoordinate(lat);
|
||||
const formattedLng = formatCoordinate(lng);
|
||||
// 使用腾讯地图链接
|
||||
return `https://apis.map.qq.com/uri/v1/marker?marker=coord:${lat},${lng};title:${encodeURIComponent(label)}&referer=wechat`;
|
||||
return `https://apis.map.qq.com/uri/v1/marker?marker=coord:${formattedLng},${formattedLat};title:${encodeURIComponent(label)}&referer=wechat`;
|
||||
};
|
||||
|
||||
// 生成静态地图预览图URL
|
||||
const generateStaticMapUrl = (
|
||||
lat: string,
|
||||
lng: string,
|
||||
width: number = 420,
|
||||
height: number = 200,
|
||||
) => {
|
||||
const formattedLat = formatCoordinate(lat);
|
||||
const formattedLng = formatCoordinate(lng);
|
||||
const key = "7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX";
|
||||
const zoom = locationData.scale || "15";
|
||||
// 腾讯地图静态地图API
|
||||
return `https://apis.map.qq.com/ws/staticmap/v2/?center=${formattedLng},${formattedLat}&zoom=${zoom}&size=${width}x${height}&markers=${formattedLng},${formattedLat}&key=${key}`;
|
||||
};
|
||||
|
||||
const mapUrl = generateMapUrl(
|
||||
@@ -97,12 +123,18 @@ const LocationMessage: React.FC<LocationMessageProps> = ({ content }) => {
|
||||
locationData.label,
|
||||
);
|
||||
|
||||
const staticMapUrl = generateStaticMapUrl(
|
||||
locationData.y,
|
||||
locationData.x,
|
||||
420,
|
||||
200,
|
||||
);
|
||||
|
||||
// 处理POI信息
|
||||
const poiName = locationData.poiname || locationData.label;
|
||||
const poiCategory = locationData.poiCategoryTips
|
||||
? locationData.poiCategoryTips.split(":")[0]
|
||||
: "";
|
||||
const poiPhone = locationData.poiPhone || "";
|
||||
// 提取道路名称(如果有的话,从label中提取)
|
||||
const roadName =
|
||||
locationData.poiname.split(/[((]/)[0] || locationData.label;
|
||||
const detailAddress = locationData.label;
|
||||
|
||||
return (
|
||||
<div className={styles.locationMessage}>
|
||||
@@ -110,29 +142,35 @@ const LocationMessage: React.FC<LocationMessageProps> = ({ content }) => {
|
||||
className={styles.locationCard}
|
||||
onClick={() => window.open(mapUrl, "_blank")}
|
||||
>
|
||||
{/* 位置详情 */}
|
||||
{/* 地图预览图 */}
|
||||
<div className={styles.mapPreview}>
|
||||
<img
|
||||
src={staticMapUrl}
|
||||
alt={locationData.label}
|
||||
className={styles.mapImage}
|
||||
onError={e => {
|
||||
// 如果图片加载失败,显示占位符
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
const placeholder = target.nextElementSibling as HTMLElement;
|
||||
if (placeholder) {
|
||||
placeholder.style.display = "flex";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={styles.mapPlaceholder}>
|
||||
<span>📍</span>
|
||||
<span>地图加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 位置信息 */}
|
||||
<div className={styles.locationContent}>
|
||||
{/* POI名称 */}
|
||||
{poiName && <div className={styles.poiName}>{poiName}</div>}
|
||||
{/* 道路名称 */}
|
||||
{roadName && <div className={styles.roadName}>{roadName}</div>}
|
||||
|
||||
{/* 详细地址 */}
|
||||
<div className={styles.locationAddress}>{locationData.label}</div>
|
||||
|
||||
{/* POI分类和电话 */}
|
||||
<div className={styles.locationDetails}>
|
||||
{poiCategory && (
|
||||
<div className={styles.poiCategory}>
|
||||
<span className={styles.categoryIcon}>🏷️</span>
|
||||
{poiCategory}
|
||||
</div>
|
||||
)}
|
||||
{poiPhone && (
|
||||
<div className={styles.poiPhone}>
|
||||
<span className={styles.phoneIcon}>📞</span>
|
||||
{poiPhone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.locationAddress}>{detailAddress}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -347,6 +347,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const prevMessages = prevMessagesRef.current;
|
||||
const prevLength = prevMessages.length;
|
||||
|
||||
const hasVideoStateChange = currentMessages.some((msg, index) => {
|
||||
// 首先检查消息对象本身是否为null或undefined
|
||||
@@ -384,8 +385,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 只有在没有视频状态变化时才自动滚动到底部
|
||||
if (!hasVideoStateChange && isLoadingData) {
|
||||
if (currentMessages.length > prevLength && !hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
} else if (isLoadingData && !hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import React, { useCallback, useState, useEffect, useRef } from "react";
|
||||
import { Input, message } from "antd";
|
||||
import { Button } from "antd-mobile";
|
||||
import { EditOutlined } from "@ant-design/icons";
|
||||
@@ -56,8 +56,32 @@ const DetailValue: React.FC<DetailValueProps> = ({
|
||||
useState<Record<string, string>>(value);
|
||||
const [changedKeys, setChangedKeys] = useState<string[]>([]);
|
||||
|
||||
// 使用 useRef 存储上一次的 value,用于深度比较
|
||||
const prevValueRef = useRef<Record<string, string>>(value);
|
||||
|
||||
// 深度比较函数:比较两个对象的值是否真的变化了
|
||||
const isValueChanged = useCallback(
|
||||
(prev: Record<string, string>, next: Record<string, string>) => {
|
||||
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
||||
for (const key of allKeys) {
|
||||
if (prev[key] !== next[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 当外部value变化时,更新内部状态
|
||||
// 优化:只有当值真正变化时才重置编辑状态,避免因对象引用变化导致编辑状态丢失
|
||||
useEffect(() => {
|
||||
// 深度比较,只有当值真正变化时才更新
|
||||
if (!isValueChanged(prevValueRef.current, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有在值真正变化时才更新状态
|
||||
setFieldValues(value);
|
||||
setOriginalValues(value);
|
||||
setChangedKeys([]);
|
||||
@@ -67,7 +91,10 @@ const DetailValue: React.FC<DetailValueProps> = ({
|
||||
newEditingFields[field.key] = false;
|
||||
});
|
||||
setEditingFields(newEditingFields);
|
||||
}, [value, fields]);
|
||||
|
||||
// 更新 ref
|
||||
prevValueRef.current = value;
|
||||
}, [value, fields, isValueChanged]);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(fieldKey: string, nextVal: string) => {
|
||||
|
||||
@@ -210,14 +210,34 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
|
||||
|
||||
// 构建联系人或群聊详细信息
|
||||
|
||||
const customerList = useCustomerStore(state => state.customerList);
|
||||
const kfSelectedUser = useMemo(() => {
|
||||
if (!contract.wechatAccountId) return null;
|
||||
const matchedCustomer = customerList.find(
|
||||
customer => customer.id === contract.wechatAccountId,
|
||||
);
|
||||
return matchedCustomer || null;
|
||||
}, [customerList, contract.wechatAccountId]);
|
||||
// 优化:使用选择器函数直接订阅匹配的客服对象,避免订阅整个 customerList
|
||||
// 添加相等性比较,只有当匹配的客服对象或其 labels 真正变化时才触发重新渲染
|
||||
const kfSelectedUser = useCustomerStore(
|
||||
state => {
|
||||
if (!contract.wechatAccountId) return null;
|
||||
return (
|
||||
state.customerList.find(
|
||||
customer => customer.id === contract.wechatAccountId,
|
||||
) || null
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
// 如果都是 null,认为相等
|
||||
if (!prev && !next) return true;
|
||||
// 如果一个是 null 另一个不是,认为不相等
|
||||
if (!prev || !next) return false;
|
||||
// 比较关键字段:id 和 labels(因为 useEffect 中使用了 labels)
|
||||
if (prev.id !== next.id) return false;
|
||||
// 比较 labels 数组是否真的变化了
|
||||
const prevLabels = prev.labels || [];
|
||||
const nextLabels = next.labels || [];
|
||||
if (prevLabels.length !== nextLabels.length) return false;
|
||||
// 深度比较 labels 数组内容(先复制再排序,避免修改原数组)
|
||||
const prevLabelsStr = JSON.stringify([...prevLabels].sort());
|
||||
const nextLabelsStr = JSON.stringify([...nextLabels].sort());
|
||||
return prevLabelsStr === nextLabelsStr;
|
||||
},
|
||||
);
|
||||
|
||||
// 不再需要从useContactStore获取getContactsByCustomer
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
.active & {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.allUser {
|
||||
|
||||
@@ -89,7 +89,6 @@ const CustomerList: React.FC = () => {
|
||||
>
|
||||
<div className={styles.allUser}>全部</div>
|
||||
</Badge>
|
||||
<div className={`${styles.onlineIndicator} ${styles.online}`} />
|
||||
</div>
|
||||
{customerList.map(customer => (
|
||||
<div
|
||||
@@ -105,7 +104,7 @@ const CustomerList: React.FC = () => {
|
||||
<Avatar
|
||||
src={customer.avatar}
|
||||
size={50}
|
||||
className={styles.userAvatar}
|
||||
className={`${styles.userAvatar} ${!customer.isOnline ? styles.offline : ""}`}
|
||||
style={{
|
||||
backgroundColor: !customer.avatar ? "#1890ff" : undefined,
|
||||
}}
|
||||
@@ -113,9 +112,6 @@ const CustomerList: React.FC = () => {
|
||||
{!customer.avatar && customer.name.charAt(0)}
|
||||
</Avatar>
|
||||
</Badge>
|
||||
<div
|
||||
className={`${styles.onlineIndicator} ${customer.isOnline ? styles.online : styles.offline}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -383,7 +383,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
const requestId = ++loadRequestRef.current;
|
||||
|
||||
const initializeSessions = async () => {
|
||||
setLoading(true);
|
||||
// setLoading(true);
|
||||
|
||||
try {
|
||||
const cachedSessions =
|
||||
@@ -416,7 +416,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
}
|
||||
} finally {
|
||||
if (!isCancelled && loadRequestRef.current === requestId) {
|
||||
setLoading(false);
|
||||
// setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -353,13 +353,13 @@ export class ContactManager {
|
||||
exclude: boolean = false,
|
||||
): Promise<number> {
|
||||
try {
|
||||
console.log("getContactCount 调用参数:", {
|
||||
userId,
|
||||
type,
|
||||
customerId,
|
||||
groupIds,
|
||||
exclude,
|
||||
});
|
||||
// console.log("getContactCount 调用参数:", {
|
||||
// userId,
|
||||
// type,
|
||||
// customerId,
|
||||
// groupIds,
|
||||
// exclude,
|
||||
// });
|
||||
|
||||
const conditions: any[] = [
|
||||
{ field: "userId", operator: "equals", value: userId },
|
||||
@@ -394,14 +394,14 @@ export class ContactManager {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("查询条件:", conditions);
|
||||
// console.log("查询条件:", conditions);
|
||||
|
||||
const contacts =
|
||||
await contactUnifiedService.findWhereMultiple(conditions);
|
||||
|
||||
console.log(
|
||||
`查询结果数量: ${contacts.length}, type: ${type}, groupIds: ${groupIds}`,
|
||||
);
|
||||
// console.log(
|
||||
// `查询结果数量: ${contacts.length}, type: ${type}, groupIds: ${groupIds}`,
|
||||
// );
|
||||
|
||||
return contacts.length;
|
||||
} catch (error) {
|
||||
|
||||
@@ -58,6 +58,11 @@ export const messageFilter = (message: string) => {
|
||||
return "[图片]";
|
||||
}
|
||||
|
||||
// XML 格式的位置消息:包含 <location 标签
|
||||
if (/<location[\s>]/i.test(message)) {
|
||||
return "[位置]";
|
||||
}
|
||||
|
||||
// 其他情况直接返回原始消息
|
||||
return message;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user