Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop

This commit is contained in:
wong
2025-11-24 17:19:06 +08:00
29 changed files with 1570 additions and 648 deletions

View File

@@ -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;
}
}

View File

@@ -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%;
}
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -20,7 +20,7 @@ const ContentManagement: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="内容管理"
title="发朋友圈"
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
showBackButton={true}
backButtonText="返回功能中心"

View File

@@ -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: "消息推送助手",

View File

@@ -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>

View File

@@ -32,6 +32,7 @@
.rightColumn {
flex: 1;
max-width: 500px;
display: flex;
flex-direction: column;
gap: 20px;

View File

@@ -746,10 +746,6 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
/>
</div>
</div>
<div className={styles.scriptGroupContent}>
{group.messages[0]}
{group.messages.length > 1 && " ..."}
</div>
</div>
))
)}

View File

@@ -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

View File

@@ -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");
};

View File

@@ -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}
/>

View File

@@ -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;
}
}

View File

@@ -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}
/>
</>
);
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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) => {

View File

@@ -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

View File

@@ -47,6 +47,11 @@
.active & {
border-color: #1890ff;
}
&.offline {
filter: grayscale(100%);
opacity: 0.6;
}
}
}
.allUser {

View File

@@ -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>
))}
</>

View File

@@ -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);
}
}
};

View File

@@ -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) {

View File

@@ -58,6 +58,11 @@ export const messageFilter = (message: string) => {
return "[图片]";
}
// XML 格式的位置消息:包含 <location 标签
if (/<location[\s>]/i.test(message)) {
return "[位置]";
}
// 其他情况直接返回原始消息
return message;
}