diff --git a/Cunkebao/src/pages/mobile/mine/setting/index.tsx b/Cunkebao/src/pages/mobile/mine/setting/index.tsx index 1ddcf2269..c002543f3 100644 --- a/Cunkebao/src/pages/mobile/mine/setting/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/setting/index.tsx @@ -8,7 +8,6 @@ import { LogoutOutlined, SettingOutlined, LockOutlined, - ReloadOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import { useUserStore } from "@/store/module/user"; @@ -16,7 +15,7 @@ import { useSettingsStore } from "@/store/module/settings"; import style from "./index.module.scss"; import NavCommon from "@/components/NavCommon"; import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp"; -import { updateChecker } from "@/utils/updateChecker"; +import { clearApplicationCache } from "@/utils/cacheCleaner"; interface SettingItem { id: string; @@ -58,13 +57,35 @@ const Setting: React.FC = () => { const handleClearCache = () => { Dialog.confirm({ content: "确定要清除缓存吗?这将清除所有本地数据。", - onConfirm: () => { - sendMessageToParent( - { - action: "clearCache", - }, - TYPE_EMUE.FUNCTION, - ); + onConfirm: async () => { + const handler = Toast.show({ + icon: "loading", + content: "正在清理缓存...", + duration: 0, + }); + try { + await clearApplicationCache(); + sendMessageToParent( + { + action: "clearCache", + }, + TYPE_EMUE.FUNCTION, + ); + handler.close(); + Toast.show({ + icon: "success", + content: "缓存清理完成", + position: "top", + }); + } catch (error) { + console.error("clear cache failed", error); + handler.close(); + Toast.show({ + icon: "fail", + content: "缓存清理失败,请稍后再试", + position: "top", + }); + } }, }); }; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 557ae0c94..7a9a1ce3f 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -1,10 +1,26 @@ import request from "@/api/request"; +import axios from "axios"; +import { useUserStore } from "@/store/module/user"; // 获取微信号详情 export function getWechatAccountDetail(id: string) { return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET"); } +// 获取微信号概览数据 +export function getWechatAccountOverview(id: string) { + return request("/v1/wechats/overview", { wechatId: id }, "GET"); +} + +// 获取微信号朋友圈列表 +export function getWechatMoments(params: { + wechatId: string; + page?: number; + limit?: number; +}) { + return request("/v1/wechats/moments", params, "GET"); +} + // 获取微信号好友列表 export function getWechatFriends(params: { wechatAccount: string; @@ -36,3 +52,68 @@ export function transferWechatFriends(params: { }) { return request("/v1/wechats/transfer-friends", params, "POST"); } + +// 导出朋友圈接口(直接下载文件) +export async function exportWechatMoments(params: { + wechatId: string; + keyword?: string; + type?: number; + startTime?: string; + endTime?: string; +}): Promise { + const { token } = useUserStore.getState(); + const baseURL = + (import.meta as any).env?.VITE_API_BASE_URL || "/api"; + + // 构建查询参数 + const queryParams = new URLSearchParams(); + queryParams.append("wechatId", params.wechatId); + if (params.keyword) { + queryParams.append("keyword", params.keyword); + } + if (params.type !== undefined) { + queryParams.append("type", params.type.toString()); + } + if (params.startTime) { + queryParams.append("startTime", params.startTime); + } + if (params.endTime) { + queryParams.append("endTime", params.endTime); + } + + try { + const response = await axios.get( + `${baseURL}/v1/wechats/moments/export?${queryParams.toString()}`, + { + responseType: "blob", + headers: { + Authorization: token ? `Bearer ${token}` : undefined, + }, + } + ); + + // 创建下载链接 + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + + // 从响应头获取文件名,如果没有则使用默认文件名 + const contentDisposition = response.headers["content-disposition"]; + let fileName = "朋友圈导出.xlsx"; + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (fileNameMatch && fileNameMatch[1]) { + fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, "")); + } + } + + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error: any) { + throw new Error(error.response?.data?.message || error.message || "导出失败"); + } +} diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts index 20d7a2c9a..9245ba9fc 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts @@ -1,3 +1,41 @@ +// 概览数据接口 +export interface WechatAccountOverview { + healthScoreAssessment: { + score: number; + dailyLimit: number; + todayAdded: number; + lastAddTime: string; + statusTag: string; + baseComposition?: Array<{ + name: string; + score: number; + formatted: string; + friendCount?: number; + }>; + dynamicRecords?: Array<{ + title?: string; + description?: string; + time?: string; + score?: number; + formatted?: string; + statusTag?: string; + }>; + }; + accountValue: { + value: number; + formatted: string; + }; + todayValueChange: { + change: number; + formatted: string; + isPositive: boolean; + }; + totalFriends: number; + todayNewFriends: number; + highValueChatrooms: number; + todayNewChatrooms: number; +} + export interface WechatAccountSummary { accountAge: string; activityLevel: { @@ -15,12 +53,51 @@ export interface WechatAccountSummary { todayAdded: number; addLimit: number; }; + healthScore?: { + score: number; + lastUpdate?: string; + lastAddTime?: string; + baseScore?: number; + verifiedScore?: number; + friendsScore?: number; + activities?: { + type: string; + time?: string; + score: number; + description?: string; + status?: string; + }[]; + }; + moments?: { + id: string; + date: string; + month: string; + day: string; + content: string; + images?: string[]; + timeAgo?: string; + hasEmoji?: boolean; + }[]; + accountValue?: { + value: number; + todayChange?: number; + }; + friendsCount?: { + total: number; + todayAdded?: number; + }; + groupsCount?: { + total: number; + todayAdded?: number; + }; restrictions: { id: number; level: number; reason: string; date: string; }[]; + // 新增概览数据 + overview?: WechatAccountOverview; } export interface Friend { @@ -39,6 +116,27 @@ export interface Friend { region: string; source: string; notes: string; + value?: number; + valueFormatted?: string; + statusTags?: string[]; +} + +export interface MomentItem { + id: string; + snsId: string; + type: number; + content: string; + resUrls: string[]; + commentList?: any[]; + likeList?: any[]; + createTime: string; + momentEntity?: { + lat?: string; + lng?: string; + location?: string; + picSize?: number; + userName?: string; + }; } export interface WechatFriendDetail { diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index c6309f70f..954921fc1 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -143,67 +143,235 @@ } .overview-content { - .info-grid { + // 健康分评估区域 + .health-score-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .health-score-info { + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + } + + // 账号统计卡片网格 + .account-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; - .info-card { - background: linear-gradient(135deg, #e6f7ff, #f0f8ff); + .stat-card { + background: #ffffff; padding: 16px; border-radius: 12px; - border: 1px solid #bae7ff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - transition: all 0.3s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - &:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); - } - - .info-header { + .stat-header { display: flex; + justify-content: space-between; align-items: center; - gap: 8px; margin-bottom: 8px; - .info-icon { - font-size: 16px; - color: #1677ff; - padding: 6px; - background: #e6f7ff; - border-radius: 8px; + .stat-title { + font-size: 14px; + color: #666; } - .info-title { - flex: 1; + .stat-icon-up { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; - .title-text { - font-size: 12px; - font-weight: 600; - color: #1677ff; - margin-bottom: 2px; + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(-45deg); + width: 8px; + height: 8px; + border-top: 2px solid #722ed1; + border-right: 2px solid #722ed1; + } + } + + .stat-icon-plus { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 2px; + background: #52c41a; } - .title-sub { - font-size: 10px; - color: #666; + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 10px; + background: #52c41a; + } + } + + .stat-icon-people { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 7px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #1677ff; + } + + &::after { + content: ''; + position: absolute; + top: 13px; + left: 5px; + width: 10px; + height: 5px; + border-radius: 10px 10px 0 0; + background: #1677ff; + } + } + + .stat-icon-chat { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: #fa8c16; } } } - .info-value { - text-align: right; - font-size: 18px; - font-weight: 700; - color: #1677ff; + .stat-value { + font-size: 20px; + font-weight: 600; + color: #333; + } - .value-unit { - font-size: 12px; - color: #666; - margin-left: 4px; - } + .stat-value-positive { + font-size: 20px; + font-weight: 600; + color: #52c41a; } } } @@ -449,6 +617,47 @@ } } + .friends-summary { + display: flex; + align-items: center; + justify-content: space-between; + background: #f5f9ff; + border: 1px solid #e0edff; + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 16px; + + .summary-item { + display: flex; + flex-direction: column; + gap: 4px; + } + + .summary-label { + font-size: 12px; + color: #666; + } + + .summary-value { + font-size: 20px; + font-weight: 600; + color: #111; + } + + .summary-value-highlight { + font-size: 20px; + font-weight: 600; + color: #fa541c; + } + + .summary-divider { + width: 1px; + height: 32px; + background: #e6e6e6; + margin: 0 12px; + } + } + .friends-list { .empty { text-align: center; @@ -467,83 +676,100 @@ } } - .friend-item { + .friend-card { display: flex; align-items: center; - padding: 12px; + padding: 14px; background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; - } + border: 1px solid #f0f0f0; + border-radius: 12px; + margin-bottom: 10px; + gap: 12px; + transition: box-shadow 0.2s, border-color 0.2s; - .friend-item-static { - display: flex; - align-items: center; - padding: 12px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; + &:hover { + border-color: #cfe2ff; + box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15); + } } .friend-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - margin-right: 12px; + width: 48px; + height: 48px; + + .adm-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + } } - .friend-info { + .friend-main { flex: 1; min-width: 0; + } - .friend-header { - display: flex; - align-items: center; - justify-content: space-between; + .friend-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .friend-name { + font-size: 15px; + font-weight: 600; + color: #111; + flex-shrink: 0; + } + + .friend-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + .friend-tag { + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + background: #f5f5f5; + color: #666; + } + + .friend-id-row { + font-size: 12px; + color: #999; + margin-bottom: 6px; + } + + .friend-status-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .friend-status-chip { + background: #f0f7ff; + color: #1677ff; + font-size: 11px; + padding: 2px 8px; + border-radius: 8px; + } + + .friend-value { + text-align: right; + + .value-label { + font-size: 11px; + color: #999; margin-bottom: 4px; - - .friend-name { - font-size: 14px; - font-weight: 500; - color: #333; - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - .friend-remark { - color: #666; - margin-left: 4px; - } - } - - .friend-arrow { - font-size: 12px; - color: #ccc; - } } - .friend-wechat-id { - font-size: 12px; - color: #666; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .friend-tags { - display: flex; - flex-wrap: wrap; - gap: 4px; - - .friend-tag { - font-size: 10px; - padding: 2px 6px; - border-radius: 6px; - } + .value-amount { + font-size: 14px; + font-weight: 600; + color: #fa541c; } } } @@ -619,6 +845,56 @@ margin-top: 20px; } + .popup-footer { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } + + .export-form { + margin-top: 20px; + + .form-item { + margin-bottom: 20px; + + label { + display: block; + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + } + + .type-selector { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .type-option { + padding: 8px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s; + background: white; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.active { + background: #1677ff; + border-color: #1677ff; + color: white; + } + } + } + } + } + .restrictions-detail { .restriction-detail-item { display: flex; @@ -769,6 +1045,511 @@ margin-right: 8px; } +.health-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + + // 健康分评估区域 + .health-score-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .health-score-info { + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + } + + .health-score-card { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + + .health-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-section-title { + font-size: 16px; + font-weight: 600; + color: #ff8800; + margin-bottom: 12px; + position: relative; + padding-left: 12px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 16px; + background: #ff8800; + border-radius: 2px; + } + } + + .health-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + + .health-item-label { + font-size: 14px; + color: #333; + display: flex; + align-items: center; + + .health-item-icon-warning { + width: 16px; + height: 16px; + border-radius: 50%; + background: #ffebeb; + margin-right: 8px; + position: relative; + + &::before { + content: '!'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #ff4d4f; + font-size: 12px; + font-weight: bold; + } + } + + .health-item-tag { + background: #fff7e6; + color: #fa8c16; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 8px; + } + } + + .health-item-value-positive { + font-size: 14px; + font-weight: 600; + color: #52c41a; + } + + .health-item-value-negative { + font-size: 14px; + font-weight: 600; + color: #ff4d4f; + } + + .health-item-value-empty { + width: 20px; + } + } + + .health-empty { + text-align: center; + color: #999; + font-size: 14px; + padding: 20px 0; + } + } +} + +.moments-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + background: #f5f5f5; + + .moments-action-bar { + display: flex; + justify-content: space-between; + padding: 0 16px 16px; + + .action-button, .action-button-dark { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 70px; + height: 40px; + border-radius: 8px; + background: #1677ff; + + .action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export { + width: 20px; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + margin-bottom: 2px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 2px; + background: white; + } + } + + .action-icon-image::after { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: white; + } + + .action-icon-video::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 8px; + border-color: transparent transparent transparent white; + } + + .action-text, .action-text-light { + font-size: 12px; + color: white; + } + } + + .action-button-dark { + background: #333; + } + } + + .moments-list { + padding: 0 16px; + + .moment-item { + display: flex; + margin-bottom: 16px; + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .moment-date { + margin-right: 12px; + text-align: center; + + .date-day { + font-size: 20px; + font-weight: 600; + color: #333; + line-height: 1; + } + + .date-month { + font-size: 12px; + color: #999; + margin-top: 2px; + } + } + + .moment-content { + flex: 1; + + .moment-text { + font-size: 14px; + line-height: 1.5; + color: #333; + margin-bottom: 8px; + white-space: pre-wrap; // 保留换行和空格,确保文本完整显示 + word-wrap: break-word; // 长单词自动换行 + + .moment-emoji { + display: inline; + font-size: 16px; + vertical-align: middle; + } + } + + .moment-images { + margin-bottom: 8px; + + .image-grid { + display: grid; + gap: 8px; + width: 100%; + + // 1张图片:宽度拉伸,高度自适应 + &.single { + grid-template-columns: 1fr; + + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + } + } + + // 2张图片:左右并列 + &.double { + grid-template-columns: 1fr 1fr; + + img { + width: 100%; + height: 120px; + object-fit: cover; + border-radius: 8px; + } + } + + // 3张图片:三张并列 + &.triple { + grid-template-columns: 1fr 1fr 1fr; + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + } + + // 4张图片:2x2网格布局 + &.quad { + grid-template-columns: repeat(2, 1fr); + + img { + width: 100%; + height: 140px; + object-fit: cover; + border-radius: 8px; + } + } + + // 5张及以上:网格布局(9宫格) + &.grid { + grid-template-columns: repeat(3, 1fr); + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + + .image-more { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + border-radius: 8px; + color: white; + font-size: 12px; + font-weight: 500; + height: 100px; + } + } + } + } + + .moment-footer { + display: flex; + justify-content: flex-end; + + .moment-time { + font-size: 12px; + color: #999; + } + } + } + } + } +} + .risk-content { padding: 16px 0; height: 500px; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 3be617b58..333eb3530 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -11,6 +11,7 @@ import { Avatar, Tag, Switch, + DatePicker, } from "antd-mobile"; import { Input, Pagination } from "antd"; import NavCommon from "@/components/NavCommon"; @@ -21,11 +22,18 @@ import { } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import style from "./detail.module.scss"; -import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api"; +import { + getWechatAccountDetail, + getWechatFriends, + transferWechatFriends, + getWechatAccountOverview, + getWechatMoments, + exportWechatMoments, +} from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; -import { WechatAccountSummary, Friend } from "./data"; +import { WechatAccountSummary, Friend, MomentItem } from "./data"; const WechatAccountDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -34,6 +42,7 @@ const WechatAccountDetail: React.FC = () => { const [accountSummary, setAccountSummary] = useState(null); const [accountInfo, setAccountInfo] = useState(null); + const [overviewData, setOverviewData] = useState(null); const [showRestrictions, setShowRestrictions] = useState(false); const [showTransferConfirm, setShowTransferConfirm] = useState(false); const [selectedDevices, setSelectedDevices] = useState([]); @@ -50,6 +59,22 @@ const WechatAccountDetail: React.FC = () => { const [isFetchingFriends, setIsFetchingFriends] = useState(false); const [hasFriendLoadError, setHasFriendLoadError] = useState(false); const [isFriendsEmpty, setIsFriendsEmpty] = useState(false); + const [moments, setMoments] = useState([]); + const [momentsPage, setMomentsPage] = useState(1); + const [momentsTotal, setMomentsTotal] = useState(0); + const [isFetchingMoments, setIsFetchingMoments] = useState(false); + const [momentsError, setMomentsError] = useState(null); + const MOMENTS_LIMIT = 10; + + // 导出相关状态 + const [showExportPopup, setShowExportPopup] = useState(false); + const [exportKeyword, setExportKeyword] = useState(""); + const [exportType, setExportType] = useState(undefined); + const [exportStartTime, setExportStartTime] = useState(null); + const [exportEndTime, setExportEndTime] = useState(null); + const [showStartTimePicker, setShowStartTimePicker] = useState(false); + const [showEndTimePicker, setShowEndTimePicker] = useState(false); + const [exportLoading, setExportLoading] = useState(false); // 获取基础信息 const fetchAccountInfo = useCallback(async () => { @@ -80,6 +105,19 @@ const WechatAccountDetail: React.FC = () => { } }, [id]); + // 获取概览数据 + const fetchOverviewData = useCallback(async () => { + if (!id) return; + try { + const response = await getWechatAccountOverview(id); + if (response) { + setOverviewData(response); + } + } catch (e) { + console.error("获取概览数据失败:", e); + } + }, [id]); + // 获取好友列表 - 封装为独立函数 const fetchFriendsList = useCallback( async (page: number = 1, keyword: string = "") => { @@ -96,26 +134,44 @@ const WechatAccountDetail: React.FC = () => { keyword: keyword, }); - const newFriends = response.list.map((friend: any) => ({ - id: friend.id.toString(), - avatar: friend.avatar || "/placeholder.svg", - nickname: friend.nickname || "未知用户", - wechatId: friend.wechatId || "", - remark: friend.memo || "", - addTime: friend.createTime || new Date().toISOString().split("T")[0], - lastInteraction: - friend.lastInteraction || new Date().toISOString().split("T")[0], - tags: friend.tags - ? friend.tags.map((tag: string, index: number) => ({ - id: `tag-${index}`, - name: tag, - color: getRandomTagColor(), - })) - : [], - region: friend.region || "未知", - source: friend.source || "未知", - notes: friend.notes || "", - })); + const newFriends = response.list.map((friend: any) => { + const memoTags = Array.isArray(friend.memo) + ? friend.memo + : friend.memo + ? String(friend.memo) + .split(/[,\s,、]+/) + .filter(Boolean) + : []; + + const tagList = Array.isArray(friend.tags) + ? friend.tags + : friend.tags + ? [friend.tags] + : []; + + return { + id: friend.id.toString(), + avatar: friend.avatar || "/placeholder.svg", + nickname: friend.nickname || "未知用户", + wechatId: friend.wechatId || "", + remark: friend.notes || "", + addTime: + friend.createTime || new Date().toISOString().split("T")[0], + lastInteraction: + friend.lastInteraction || new Date().toISOString().split("T")[0], + tags: memoTags.map((tag: string, index: number) => ({ + id: `tag-${index}`, + name: tag, + color: getRandomTagColor(), + })), + statusTags: tagList, + region: friend.region || "未知", + source: friend.source || "未知", + notes: friend.notes || "", + value: friend.value, + valueFormatted: friend.valueFormatted, + }; + }); setFriends(newFriends); setFriendsTotal(response.total); @@ -137,6 +193,46 @@ const WechatAccountDetail: React.FC = () => { [id], ); + const fetchMomentsList = useCallback( + async (page: number = 1, append: boolean = false) => { + if (!id) return; + setIsFetchingMoments(true); + setMomentsError(null); + try { + const response = await getWechatMoments({ + wechatId: id, + page, + limit: MOMENTS_LIMIT, + }); + + const list: MomentItem[] = (response.list || []).map((moment: any) => ({ + id: moment.id?.toString() || Math.random().toString(), + snsId: moment.snsId, + type: moment.type, + content: moment.content || "", + resUrls: moment.resUrls || [], + commentList: moment.commentList || [], + likeList: moment.likeList || [], + createTime: moment.createTime || "", + momentEntity: moment.momentEntity || {}, + })); + + setMoments(prev => (append ? [...prev, ...list] : list)); + setMomentsTotal(response.total || list.length); + setMomentsPage(page); + } catch (error) { + console.error("获取朋友圈数据失败:", error); + setMomentsError("获取朋友圈数据失败"); + if (!append) { + setMoments([]); + } + } finally { + setIsFetchingMoments(false); + } + }, + [id], + ); + // 搜索好友 const handleSearch = useCallback(() => { setFriendsPage(1); @@ -161,8 +257,9 @@ const WechatAccountDetail: React.FC = () => { useEffect(() => { if (id) { fetchAccountInfo(); + fetchOverviewData(); } - }, [id, fetchAccountInfo]); + }, [id, fetchAccountInfo, fetchOverviewData]); // 监听标签切换 - 只在切换到好友列表时请求一次 useEffect(() => { @@ -173,6 +270,14 @@ const WechatAccountDetail: React.FC = () => { } }, [activeTab, id, fetchFriendsList, searchQuery]); + useEffect(() => { + if (activeTab === "moments" && id) { + if (moments.length === 0) { + fetchMomentsList(1, false); + } + } + }, [activeTab, id, fetchMomentsList, moments.length]); + // 工具函数 const getRandomTagColor = (): string => { const colors = [ @@ -271,6 +376,85 @@ const WechatAccountDetail: React.FC = () => { navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`); }; + const handleLoadMoreMoments = () => { + if (isFetchingMoments) return; + if (moments.length >= momentsTotal) return; + fetchMomentsList(momentsPage + 1, true); + }; + + // 处理朋友圈导出 + const handleExportMoments = useCallback(async () => { + if (!id) { + Toast.show({ content: "微信ID不存在", position: "top" }); + return; + } + + setExportLoading(true); + try { + // 格式化时间 + const formatDate = (date: Date | null): string | undefined => { + if (!date) return undefined; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + await exportWechatMoments({ + wechatId: id, + keyword: exportKeyword || undefined, + type: exportType, + startTime: formatDate(exportStartTime), + endTime: formatDate(exportEndTime), + }); + + Toast.show({ content: "导出成功", position: "top" }); + setShowExportPopup(false); + // 重置筛选条件 + setExportKeyword(""); + setExportType(undefined); + setExportStartTime(null); + setExportEndTime(null); + } catch (error: any) { + console.error("导出失败:", error); + Toast.show({ + content: error.message || "导出失败,请重试", + position: "top", + }); + } finally { + setExportLoading(false); + } + }, [id, exportKeyword, exportType, exportStartTime, exportEndTime]); + + const formatMomentDateParts = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return { day: "--", month: "--" }; + } + const day = date.getDate().toString().padStart(2, "0"); + const month = `${date.getMonth() + 1}月`; + return { day, month }; + }; + + const formatMomentTimeAgo = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return dateString || "--"; + } + const diff = Date.now() - date.getTime(); + const minutes = Math.floor(diff / (1000 * 60)); + if (minutes < 1) return "刚刚"; + if (minutes < 60) return `${minutes}分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}小时前`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}天前`; + return date.toLocaleDateString("zh-CN", { + month: "2-digit", + day: "2-digit", + }); + }; + return ( } loading={loadingInfo}>
@@ -313,73 +497,223 @@ const WechatAccountDetail: React.FC = () => { onChange={handleTabChange} className={style["tabs"]} > - +
-
-
-
- {accountInfo?.friendShip?.totalFriend ?? "-"} + {/* 健康分评估区域 */} +
+
健康分评估
+
+
+ {overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}
-
好友数量
-
-
-
- +{accountSummary?.statistics.todayAdded ?? "-"} +
+
+
+
+ {overviewData?.healthScoreAssessment?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{overviewData?.healthScoreAssessment?.dailyLimit || 0} 人
+
+
+
今日已加
+
{overviewData?.healthScoreAssessment?.todayAdded || 0} 人
+
+
-
今日新增
-
- 今日可添加: - - {accountSummary?.statistics.todayAdded ?? 0}/ - {accountSummary?.statistics.addLimit ?? 0} - -
-
-
-
-
-
-
-
-
- {accountInfo?.friendShip?.groupNumber ?? "-"} + + {/* 账号价值和好友数量区域 */} +
+ {/* 账号价值 */} +
+
+
账号价值
+
-
群聊数量
-
-
-
- {accountInfo?.activity?.yesterdayMsgCount ?? "-"} +
+ {overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`} +
+
+ + {/* 今日价值变化 */} +
+
+
今日价值变化
+
+
+
+ {overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
-
今日消息
-
-
设备信息
-
- 设备名称: - {accountInfo?.deviceName ?? "-"} + + {/* 好友数量和今日新增好友区域 */} +
+ {/* 好友总数 */} +
+
+
好友总数
+
+
+
+ {overviewData?.totalFriends || accountInfo?.friendShip?.totalFriend || "0"} +
-
- 系统类型: - {accountInfo?.deviceType ?? "-"} + + {/* 今日新增好友 */} +
+
+
今日新增好友
+
+
+
+ +{overviewData?.todayNewFriends || accountSummary?.statistics.todayAdded || "0"} +
-
- 系统版本: - {accountInfo?.deviceVersion ?? "-"} +
+ + {/* 高价群聊区域 */} +
+ {/* 高价群聊 */} +
+
+
高价群聊
+
+
+
+ {overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || "0"} +
+ + {/* 今日新增群聊 */} +
+
+
今日新增群聊
+
+
+
+ +{overviewData?.todayNewChatrooms || "0"} +
+
+
+ + +
+ + + +
+ {/* 健康分评估区域 */} +
+
健康分评估
+
+
+ {overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"} +
+
+
+
+
+ {overviewData?.healthScoreAssessment?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{overviewData?.healthScoreAssessment?.dailyLimit || 0} 人
+
+
+
今日已加
+
{overviewData?.healthScoreAssessment?.todayAdded || 0} 人
+
+
+
+
+
+ + {/* 基础构成 */} +
+
基础构成
+ {(overviewData?.healthScoreAssessment?.baseComposition && + overviewData.healthScoreAssessment.baseComposition.length > 0 + ? overviewData.healthScoreAssessment.baseComposition + : [ + { name: "账号基础分", formatted: "+60" }, + { name: "已修改微信号", formatted: "+10" }, + { name: "好友数量加成", formatted: "+12", friendCount: 5595 }, + ] + ).map((item, index) => ( +
+
+ {item.name} + {item.friendCount ? ` (${item.friendCount})` : ""} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {item.formatted || `${item.score ?? 0}`} +
+
+ ))} +
+ + {/* 动态记录 */} +
+
动态记录
+ {overviewData?.healthScoreAssessment?.dynamicRecords && + overviewData.healthScoreAssessment.dynamicRecords.length > 0 ? ( + overviewData.healthScoreAssessment.dynamicRecords.map( + (record, index) => ( +
+
+ + {record.title || record.description || "记录"} + {record.statusTag && ( + + {record.statusTag} + + )} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {record.formatted || + (record.score && record.score > 0 + ? `+${record.score}` + : record.score || "-")} +
+
+ ), + ) + ) : ( +
暂无动态记录
+ )}
+ 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} + title={`好友${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} key="friends" >
@@ -406,6 +740,23 @@ const WechatAccountDetail: React.FC = () => {
+ {/* 好友概要 */} +
+
+
好友总数
+
+ {friendsTotal || overviewData?.totalFriends || 0} +
+
+
+
+
好友总估值
+
+ {overviewData?.accountValue?.formatted || "¥1,500,000"} +
+
+
+ {/* 好友列表 */}
{isFetchingFriends && friends.length === 0 ? ( @@ -431,36 +782,44 @@ const WechatAccountDetail: React.FC = () => { {friends.map(friend => (
handleFriendClick(friend)} > - -
-
+
+ +
+
+
- {friend.nickname} - {friend.remark && ( - - ({friend.remark}) - - )} + {friend.nickname || "未知好友"}
+
-
- {friend.wechatId} +
+ ID: {friend.wechatId || "-"}
-
- {friend.tags?.map((tag, index) => ( - + {friend.statusTags?.map((tag, idx) => ( + - {typeof tag === "string" ? tag : tag.name} - + {tag} + ))} + {friend.remark && ( + + {friend.remark} + + )} +
+
+
+
+ {friend.valueFormatted + || (typeof friend.value === "number" + ? `¥${friend.value.toLocaleString()}` + : "估值 -")}
@@ -484,45 +843,118 @@ const WechatAccountDetail: React.FC = () => {
- -
- {accountSummary?.restrictions && - accountSummary.restrictions.length > 0 ? ( -
- {accountSummary.restrictions.map(restriction => ( -
-
-
- {restriction.reason} -
-
- {restriction.date - ? formatDateTime(restriction.date) - : "暂无时间"} -
-
-
- - {restriction.level === 1 - ? "低风险" - : restriction.level === 2 - ? "中风险" - : "高风险"} - -
-
- ))} + + +
+ {/* 功能按钮栏 */} +
+
+ + 文本 +
+
+ + 图片 +
+
+ + 视频 +
+
setShowExportPopup(true)} + > + + 导出 +
+
+ + {/* 朋友圈列表 */} +
+ {isFetchingMoments && moments.length === 0 ? ( +
+ +
+ ) : momentsError ? ( +
{momentsError}
+ ) : moments.length === 0 ? ( +
暂无朋友圈内容
+ ) : ( + moments.map(moment => { + const { day, month } = formatMomentDateParts( + moment.createTime, + ); + const timeAgo = formatMomentTimeAgo(moment.createTime); + const imageCount = moment.resUrls?.length || 0; + // 根据图片数量选择对应的grid类,参考素材管理的实现 + let gridClass = ""; + if (imageCount === 1) gridClass = style["single"]; + else if (imageCount === 2) gridClass = style["double"]; + else if (imageCount === 3) gridClass = style["triple"]; + else if (imageCount === 4) gridClass = style["quad"]; + else if (imageCount > 4) gridClass = style["grid"]; + + return ( +
+
+
{day}
+
{month}
+
+
+ {moment.content && ( +
+ {moment.content} +
+ )} + {imageCount > 0 && ( +
+
+ {moment.resUrls + .slice(0, 9) + .map((url, index) => ( + 朋友圈图片 + ))} + {imageCount > 9 && ( +
+ +{imageCount - 9} +
+ )} +
+
+ )} +
+ + {timeAgo} + +
+
+
+ ); + }) + )} +
+ + {moments.length < momentsTotal && ( +
+
- ) : ( -
暂无风险记录
)}
+
@@ -644,6 +1076,153 @@ const WechatAccountDetail: React.FC = () => {
+ {/* 朋友圈导出弹窗 */} + setShowExportPopup(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

导出朋友圈

+ +
+ +
+ {/* 关键词搜索 */} +
+ + setExportKeyword(e.target.value)} + allowClear + /> +
+ + {/* 类型筛选 */} +
+ +
+
setExportType(undefined)} + > + 全部 +
+
setExportType(4)} + > + 文本 +
+
setExportType(1)} + > + 图片 +
+
setExportType(3)} + > + 视频 +
+
+
+ + {/* 开始时间 */} +
+ + setShowStartTimePicker(true)} + /> + setShowStartTimePicker(false)} + onConfirm={val => { + setExportStartTime(val); + setShowStartTimePicker(false); + }} + /> +
+ + {/* 结束时间 */} +
+ + setShowEndTimePicker(true)} + /> + setShowEndTimePicker(false)} + onConfirm={val => { + setExportEndTime(val); + setShowEndTimePicker(false); + }} + /> +
+
+ +
+ + +
+
+
+ {/* 好友详情弹窗 */} {/* Removed */} diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss index 4cfd3adc3..8ddb6b92b 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss @@ -2,6 +2,39 @@ padding: 0 12px; } +.filter-bar { + padding: 12px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + + .filter-buttons { + display: flex; + gap: 8px; + + .filter-button { + flex: 1; + height: 32px; + border-radius: 6px; + border: 1px solid #d9d9d9; + background: #fff; + color: #666; + font-size: 14px; + transition: all 0.2s; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.filter-button-active { + background: #1677ff; + border-color: #1677ff; + color: #fff; + } + } + } +} + .nav-title { font-size: 18px; font-weight: 600; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx index 4b761b60e..dbd304433 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx @@ -33,11 +33,12 @@ const WechatAccounts: React.FC = () => { const [totalAccounts, setTotalAccounts] = useState(0); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all"); // 获取路由参数 wechatStatus const wechatStatus = searchParams.get("wechatStatus"); - const fetchAccounts = async (page = 1, keyword = "") => { + const fetchAccounts = async (page = 1, keyword = "", status?: "all" | "online" | "offline") => { setIsLoading(true); try { const params: any = { @@ -46,8 +47,12 @@ const WechatAccounts: React.FC = () => { keyword, }; - // 如果有 wechatStatus 参数,添加到请求参数中 - if (wechatStatus) { + // 优先使用传入的status参数,否则使用路由参数,最后使用状态中的筛选 + const filterStatus = status || wechatStatus || statusFilter; + + if (filterStatus && filterStatus !== "all") { + params.wechatStatus = filterStatus === "online" ? "1" : "0"; + } else if (wechatStatus) { params.wechatStatus = wechatStatus; } @@ -60,7 +65,7 @@ const WechatAccounts: React.FC = () => { setTotalAccounts(0); } } catch (e) { - Toast.show({ content: "获取微信号失败", position: "top" }); + setAccounts([]); setTotalAccounts(0); } finally { @@ -69,18 +74,24 @@ const WechatAccounts: React.FC = () => { }; useEffect(() => { - fetchAccounts(currentPage, searchTerm); + fetchAccounts(currentPage, searchTerm, statusFilter); // eslint-disable-next-line - }, [currentPage]); + }, [currentPage, statusFilter]); const handleSearch = () => { setCurrentPage(1); - fetchAccounts(1, searchTerm); + fetchAccounts(1, searchTerm, statusFilter); + }; + + const handleStatusFilterChange = (status: "all" | "online" | "offline") => { + setStatusFilter(status); + setCurrentPage(1); + fetchAccounts(1, searchTerm, status); }; const handleRefresh = async () => { setIsRefreshing(true); - await fetchAccounts(currentPage, searchTerm); + await fetchAccounts(currentPage, searchTerm, statusFilter); setIsRefreshing(false); Toast.show({ content: "刷新成功", position: "top" }); }; @@ -122,6 +133,31 @@ const WechatAccounts: React.FC = () => {
+
+
+ + + +
+
} > diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index d6da5a659..59613c07e 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -369,13 +369,15 @@ const ScenarioList: React.FC = () => { backFn={() => navigate("/scenarios")} title={scenarioName || ""} right={ - + scenarioId !== "10" ? ( + + ) : null } /> @@ -424,13 +426,15 @@ const ScenarioList: React.FC = () => {
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
- + {scenarioId !== "10" && ( + + )}
) : ( <> diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 76d460c85..6c61b6da3 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -242,7 +242,9 @@ const BasicSettings: React.FC = ({
) : (
- {sceneList.map(scene => { + {sceneList + .filter(scene => scene.id !== 10) + .map(scene => { const selected = formData.scenario === scene.id; return ( + + ) + } + > +
+ + {loading ? ( +
+ 加载中... +
+ ) : messageList.length === 0 ? ( + + ) : ( + messageList.map(item => ( +
handleReadMessage(item.id)} + > +
+ + {item.friendData?.nickname?.charAt(0) || "U"} + +
+
+
+ + {item.title} + + {item.isRead === 0 && ( +
+ )} +
+
+ {item.message} +
+ {item.isRead === 0 && ( +
+ {formatTime(item.createTime)} + +
+ )} +
+
+ )) + )} +
+ ), + }, + { + key: "friendRequests", + label: "好友添加记录", + children: ( +
+ {friendRequestLoading ? ( +
+ 加载中... +
+ ) : friendRequestList.length === 0 ? ( + + ) : ( + friendRequestList.map(item => ( +
+
+ + {item.adder?.nickname?.charAt(0) || "U"} + +
+
+
+ + 添加好友: + {getAddedUserName(item)} + + + {getStatusText( + item.status?.code, + item.status?.text, + )} + +
+
+ 申请人:{getAdderName(item)} +
+
+ 验证信息:{item.other?.msgContent || "无"} +
+ +
+ {item.other?.remark && ( + + 备注:{item.other.remark} + + )} +
+
+ {formatTime(item.time?.addTime)} +
+
+
+ )) + )} +
+ ), + }, + ]} + /> +
+ + + ); +}; + +export default Notice; diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts index f814b6b94..dd665a02a 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts @@ -14,3 +14,8 @@ export const readMessage = (params: { id: number }) => { export const readAll = () => { return request(`/v1/kefu/notice/readAll`, undefined, "PUT"); }; + +// 好友添加任务列表 +export const friendRequestList = (params: { page: number; limit: number }) => { + return request(`/v1/kefu/wechatFriend/addTaskList`, params, "GET"); +}; diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx index 52d9e5df6..dce087d40 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx @@ -1,29 +1,18 @@ -import React, { useState, useEffect } from "react"; -import { - Layout, - Drawer, - Avatar, - Space, - Button, - Badge, - Dropdown, - Empty, - message, -} from "antd"; +import React, { useState } from "react"; +import { Layout, Avatar, Space, Button, Dropdown, message } from "antd"; import { BarChartOutlined, UserOutlined, - BellOutlined, LogoutOutlined, ThunderboltOutlined, SettingOutlined, - CalendarOutlined, + SendOutlined, ClearOutlined, } from "@ant-design/icons"; -import { noticeList, readMessage, readAll } from "./api"; import { useUserStore } from "@/store/module/user"; import { useNavigate, useLocation } from "react-router-dom"; import styles from "./index.module.scss"; +import Notice from "./Notice"; const { Header } = Layout; @@ -32,40 +21,12 @@ interface NavCommonProps { onMenuClick?: () => void; } -// 消息数据类型 -interface MessageItem { - id: number; - type: number; - companyId: number; - userId: number; - bindId: number; - title: string; - message: string; - isRead: number; - createTime: string; - readTime: string; - friendData: { - nickname: string; - avatar: string; - }; -} - const NavCommon: React.FC = ({ title = "触客宝" }) => { - const [messageDrawerVisible, setMessageDrawerVisible] = useState(false); - const [messageList, setMessageList] = useState([]); - const [messageCount, setMessageCount] = useState(0); - const [loading, setLoading] = useState(false); const [clearingCache, setClearingCache] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useUserStore(); - // 初始化时获取消息列表 - useEffect(() => { - fetchMessageList(); - setInterval(IntervalMessageCount, 30 * 1000); - }, []); - // 处理菜单图标点击:在两个路由之间切换 const handleMenuClick = () => { if (!location.pathname.startsWith("/pc/powerCenter")) { @@ -74,48 +35,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/weChat"); } }; - // 定时器获取消息条数 - const IntervalMessageCount = async () => { - try { - const response = await noticeList({ page: 1, limit: 20 }); - if (response && response.noRead) { - setMessageCount(response.noRead); - } - } catch (error) { - console.error("获取消息列表失败:", error); - } - }; - // 获取消息列表 - const fetchMessageList = async () => { - try { - setLoading(true); - const response = await noticeList({ page: 1, limit: 20 }); - if (response && response.list) { - setMessageList(response.list); - // 计算未读消息数量 - const unreadCount = response.list.filter( - (item: MessageItem) => item.isRead === 0, - ).length; - setMessageCount(unreadCount); - } - } catch (error) { - console.error("获取消息列表失败:", error); - } finally { - setLoading(false); - } - }; - - // 处理消息中心点击 - const handleMessageClick = () => { - setMessageDrawerVisible(true); - fetchMessageList(); - }; - - // 处理消息抽屉关闭 - const handleMessageDrawerClose = () => { - setMessageDrawerVisible(false); - }; - // 处理退出登录 const handleLogout = () => { logout(); // 清除localStorage中的token和用户状态 @@ -215,61 +134,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { } }; - // 处理消息已读 - const handleReadMessage = async (messageId: number) => { - try { - await readMessage({ id: messageId }); // 这里需要根据实际API调整参数 - // 更新本地状态 - setMessageList(prev => - prev.map(item => - item.id === messageId ? { ...item, isRead: 1 } : item, - ), - ); - // 重新计算未读数量 - const unreadCount = - messageList.filter(item => item.isRead === 0).length - 1; - setMessageCount(Math.max(0, unreadCount)); - } catch (error) { - console.error("标记消息已读失败:", error); - } - }; - - // 处理全部已读 - const handleReadAll = async () => { - try { - await readAll(); // 这里需要根据实际API调整参数 - // 更新本地状态 - setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 }))); - setMessageCount(0); - } catch (error) { - console.error("全部已读失败:", error); - } - }; - - // 格式化时间 - const formatTime = (timeStr: string) => { - const date = new Date(timeStr); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) { - return date.toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - }); - } else if (days === 1) { - return "昨天"; - } else if (days < 7) { - return `${days}天前`; - } else { - return date.toLocaleDateString("zh-CN", { - month: "2-digit", - day: "2-digit", - }); - } - }; - // 用户菜单项 const userMenuItems = [ { @@ -317,10 +181,10 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { > {title}
@@ -333,11 +197,7 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { {user?.tokens} -
- - - -
+ = ({ title = "触客宝" }) => {
- - - - - } - > -
- {loading ? ( -
- 加载中... -
- ) : messageList.length === 0 ? ( - - ) : ( - messageList.map(item => ( -
handleReadMessage(item.id)} - > -
- - {item.friendData?.nickname?.charAt(0) || "U"} - -
-
-
- {item.title} - {item.isRead === 0 && ( -
- )} -
-
{item.message}
- {item.isRead === 0 && ( -
- {formatTime(item.createTime)} - -
- )} -
-
- )) - )} -
-
); }; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx index fed9b453d..97ea24520 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx @@ -20,7 +20,7 @@ const ContentManagement: React.FC = () => { return (
, - color: "#52c41a", - tag: "内容管理", - features: [ - "多库管理与分类", - "AI调用权限配置", - "内容检索规则设置", - "手动内容上传", - ], - path: "/pc/powerCenter/content-library", - }, + // { + // id: "content-library", + // title: "AI内容库配置", + // description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量", + // icon: , + // color: "#52c41a", + // tag: "内容管理", + // features: [ + // "多库管理与分类", + // "AI调用权限配置", + // "内容检索规则设置", + // "手动内容上传", + // ], + // path: "/pc/powerCenter/content-library", + // }, { id: "message-push-assistant", title: "消息推送助手", diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx index 51652b1e7..87090063e 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx @@ -39,18 +39,6 @@ const PowerCenter: React.FC = () => { return (
{/* 页面标题区域 */} -
-
-
-
-

功能中心

-
-

- AI智能营销·一站式客户管理·高效业务增长 -

-
-
- {/* KPI统计区域(置顶,按图展示) */}
@@ -157,9 +145,11 @@ const PowerCenter: React.FC = () => { {card.features.map((feature, index) => (
  • {feature}
  • @@ -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) => (
  • {feature}
  • diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss index f7c7d1a47..0896f8402 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss @@ -32,6 +32,7 @@ .rightColumn { flex: 1; + max-width: 500px; display: flex; flex-direction: column; gap: 20px; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx index ab0eefe13..82f109e71 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx @@ -746,10 +746,6 @@ const StepSendMessage: React.FC = ({ />
    -
    - {group.messages[0]} - {group.messages.length > 1 && " ..."} -
    )) )} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt deleted file mode 100644 index df1db7a1d..000000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt +++ /dev/null @@ -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 diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts index a36270332..9b4a4abe9 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -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 => { + // 转换参数格式,确保 limit 和 page 是字符串 + const requestParams: Record = { + ...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"); }; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx index 1236b5a86..8b71fa3c7 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -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 = { + 好友消息: "#1890ff", + 群消息: "#52c41a", + 群公告: "#722ed1", + }; + return ( + + {pushType} + + ); + } + // 如果没有中文名称,根据代码映射 + const codeMap: Record = { + [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 ( {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: , }, - [PushStatus.IN_PROGRESS]: { + completed: { + text: "已完成", + color: "#52c41a", + icon: , + }, + [PushStatus.PENDING]: { + text: "进行中", + color: "#1890ff", + icon: , + }, + pending: { text: "进行中", color: "#1890ff", icon: , @@ -189,12 +229,43 @@ const PushHistory: React.FC = () => { color: "#ff4d4f", icon: , }, + failed: { + text: "失败", + color: "#ff4d4f", + icon: , + }, }; - const config = statusMap[status] || { - text: "未知", - color: "#666", - icon: null, + + // 根据状态文本匹配 + const textMap: Record< + string, + { text: string; color: string; icon: React.ReactNode } + > = { + 已完成: { + text: "已完成", + color: "#52c41a", + icon: , + }, + 进行中: { + text: "进行中", + color: "#1890ff", + icon: , + }, + 失败: { + text: "失败", + color: "#ff4d4f", + icon: , + }, }; + + const config = textMap[displayText] || + statusMap[status] || + statusMap[status.toLowerCase()] || { + text: displayText, + color: "#666", + icon: null, + }; + return ( { 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) => {text}, }, + { + title: "内容库", + dataIndex: "contentLibraryName", + key: "contentLibraryName", + width: 150, + ellipsis: true, + render: (text: string) => ( + {text || "-"} + ), + }, { 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={} > - - - + + +
    @@ -353,7 +436,7 @@ const PushHistory: React.FC = () => { columns={columns} dataSource={dataSource} loading={loading} - rowKey="id" + rowKey="workbenchId" pagination={false} className={styles.dataTable} /> diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/api.ts b/Touchkebao/src/pages/pc/ckbox/weChat/api.ts index 32bb96047..7e77433e4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/api.ts @@ -28,14 +28,30 @@ export function getTrafficPoolList() { "GET", ); } +type ListRequestOptions = { + debounceGap?: number; +}; + // 好友列表 -export function getContactList(params) { - return request("/v1/kefu/wechatFriend/list", params, "GET"); +export function getContactList(params, options?: ListRequestOptions) { + return request( + "/v1/kefu/wechatFriend/list", + params, + "GET", + undefined, + options?.debounceGap, + ); } // 群列表 -export function getGroupList(params) { - return request("/v1/kefu/wechatChatroom/list", params, "GET"); +export function getGroupList(params, options?: ListRequestOptions) { + return request( + "/v1/kefu/wechatChatroom/list", + params, + "GET", + undefined, + options?.debounceGap, + ); } // 分组列表 export function getLabelsListByGroup(params) { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss new file mode 100644 index 000000000..b8f657244 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss @@ -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; + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx new file mode 100644 index 000000000..093b30670 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -0,0 +1,1016 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Modal, Input, Button, List, message, Spin } from "antd"; +import { SearchOutlined, EnvironmentOutlined } from "@ant-design/icons"; +import { useWebSocketStore } from "@/store/module/websocket/websocket"; +import styles from "./selectMap.module.scss"; + +// 声明腾讯地图类型(新版TMap API) +declare global { + interface Window { + TMap: any; + geolocationRef: any; // 全局IP定位服务引用(TMap.service.IPLocation实例) + } +} + +interface SelectMapProps { + visible: boolean; + onClose: () => void; + contract?: any; + addMessage?: (message: any) => void; + onConfirm?: (locationXml: string) => void; +} + +interface SearchResult { + id: string; + title: string; + address: string; + location: { + lat: number; + lng: number; + }; + adcode?: string; + city?: string; + district?: string; +} + +interface LocationData { + x: string; // 纬度 + y: string; // 经度 + scale: string; // 缩放级别 + label: string; // 地址标签 + poiname: string; // POI名称 + maptype: string; // 地图类型 + poiid: string; // POI ID +} + +const SelectMap: React.FC = ({ + visible, + onClose, + contract, + addMessage, + onConfirm, +}) => { + const [searchValue, setSearchValue] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [selectedLocation, setSelectedLocation] = useState( + null, + ); + const [map, setMap] = useState(null); + const [isReverseGeocoding, setIsReverseGeocoding] = useState(false); + const [isLocating, setIsLocating] = useState(false); + const [tmapLoaded, setTmapLoaded] = useState(false); + const mapContainerRef = useRef(null); + const geocoderRef = useRef(null); + const suggestServiceRef = useRef(null); + const markerRef = useRef(null); + const { sendCommand } = useWebSocketStore.getState(); + + // XML转义函数,防止特殊字符破坏XML格式 + const escapeXml = (str: string | undefined | null): string => { + if (!str) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + }; + + // 加载腾讯地图SDK + useEffect(() => { + // 检查TMap是否已经加载 + if (window.TMap) { + // 等待 API 完全初始化 + const checkAPIReady = () => { + if (window.TMap && window.TMap.Map) { + console.log("腾讯地图SDK已加载,API 可用"); + setTmapLoaded(true); + } else { + // 如果 API 还未完全初始化,等待一段时间后重试 + setTimeout(checkAPIReady, 100); + } + }; + checkAPIReady(); + return; + } + + // 动态加载腾讯地图SDK(使用与index.html相同的密钥) + const script = document.createElement("script"); + script.src = + "https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"; + script.async = true; + script.onload = () => { + console.log("腾讯地图SDK脚本加载成功,等待 API 初始化..."); + // 等待 API 完全初始化 + const checkAPIReady = () => { + if (window.TMap && window.TMap.Map) { + console.log("腾讯地图SDK API 初始化完成"); + setTmapLoaded(true); + } else { + // 如果 API 还未完全初始化,等待一段时间后重试(最多等待 5 秒) + setTimeout(checkAPIReady, 100); + } + }; + // 延迟检查,给 API 一些初始化时间 + setTimeout(checkAPIReady, 200); + }; + script.onerror = () => { + console.error("腾讯地图SDK加载失败"); + message.error("地图加载失败,请刷新页面重试"); + }; + document.head.appendChild(script); + + return () => { + // 清理script标签 + if (document.head.contains(script)) { + document.head.removeChild(script); + } + }; + }, []); + + // 检查 TMap API 是否可用(辅助函数) + const checkTMapAPI = () => { + if (!window.TMap) { + console.error("TMap 未加载"); + return false; + } + + // 检查 MultiMarker 是否可用 + if (!window.TMap.MultiMarker) { + console.error("TMap.MultiMarker 不可用", { + TMap: window.TMap, + keys: Object.keys(window.TMap || {}), + }); + return false; + } + + // 检查 Style 是否存在(可能是构造函数、对象或命名空间) + // 注意:Style 可能不是构造函数,而是配置对象或命名空间 + const hasStyle = + window.TMap.MultiMarker.Style !== undefined || + window.TMap.MarkerStyle !== undefined; + + if (!hasStyle) { + console.warn("TMap Style API 不可用,将使用配置对象方式", { + MultiMarker: window.TMap.MultiMarker, + MultiMarkerKeys: Object.keys(window.TMap.MultiMarker || {}), + MarkerStyle: window.TMap.MarkerStyle, + }); + // 不返回 false,因为 MultiMarker 可能接受配置对象 + } + + return true; + }; + + // 创建标记样式(兼容不同的 API 版本) + const createMarkerStyle = (options: any) => { + // 检查 MultiMarker.Style 是否存在 + if (window.TMap.MultiMarker?.Style) { + // 如果 Style 是函数(构造函数),使用 new + if (typeof window.TMap.MultiMarker.Style === "function") { + try { + return new window.TMap.MultiMarker.Style(options); + } catch (error) { + console.warn( + "使用 new MultiMarker.Style 失败,尝试直接返回配置对象:", + error, + ); + // 如果构造函数调用失败,直接返回配置对象 + return options; + } + } else { + // 如果 Style 不是函数,可能是对象或命名空间,直接返回配置对象 + // MultiMarker 可能接受配置对象而不是 Style 实例 + console.log("MultiMarker.Style 不是构造函数,直接使用配置对象"); + return options; + } + } + // 尝试 MarkerStyle + if (window.TMap.MarkerStyle) { + if (typeof window.TMap.MarkerStyle === "function") { + try { + return new window.TMap.MarkerStyle(options); + } catch (error) { + console.warn( + "使用 new MarkerStyle 失败,尝试直接返回配置对象:", + error, + ); + return options; + } + } else { + return options; + } + } + // 如果都不存在,直接返回配置对象(让 MultiMarker 自己处理) + console.warn("未找到 Style API,直接使用配置对象"); + return options; + }; + + // 初始化地图 + useEffect(() => { + if (visible && mapContainerRef.current && tmapLoaded && window.TMap) { + console.log("开始初始化地图"); + console.log("TMap API 检查:", { + TMap: !!window.TMap, + MultiMarker: !!window.TMap.MultiMarker, + MultiMarkerStyle: !!window.TMap.MultiMarker?.Style, + MarkerStyle: !!window.TMap.MarkerStyle, + }); + + // 检查容器尺寸,确保容器有有效的宽高 + const checkContainerSize = () => { + if (!mapContainerRef.current) return false; + const rect = mapContainerRef.current.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }; + + let mapInstance: any = null; + let handleMapClickFn: ((evt: any) => void) | null = null; + let delayTimer: NodeJS.Timeout | null = null; + let isMounted = true; // 标记弹窗是否仍然打开 + + // 初始化地图函数(使用箭头函数避免函数声明位置问题) + const initializeMap = () => { + if (!mapContainerRef.current) return; + + try { + // 再次检查容器尺寸 + const rect = mapContainerRef.current.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) { + console.error("地图容器尺寸无效:", rect); + message.error("地图容器尺寸无效,请刷新页面重试"); + return; + } + + // 创建地图实例 + const center = new window.TMap.LatLng(39.908823, 116.39747); // 默认北京 + mapInstance = new window.TMap.Map(mapContainerRef.current, { + center: center, + zoom: 13, + rotation: 0, + pitch: 0, + }); + + setMap(mapInstance); + + // 创建地理编码服务(用于反向地理编码) + geocoderRef.current = new window.TMap.service.Geocoder(); + + // 创建IP定位服务 + window.geolocationRef = new window.TMap.service.IPLocation(); + + // 创建搜索建议服务 + suggestServiceRef.current = new window.TMap.service.Suggestion({ + pageSize: 10, + autoExtend: true, + }); + + // 地图点击事件处理函数 + handleMapClickFn = (evt: any) => { + try { + // 检查弹窗是否仍然打开,以及必要的API是否可用 + if (!isMounted || !mapInstance || !mapContainerRef.current) { + return; + } + + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.warning("地图标记功能不可用,请刷新页面重试"); + return; + } + + const lat = evt.latLng.getLat(); + const lng = evt.latLng.getLng(); + + console.log("地图点击:", lat, lng); + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + + // 创建新标记 + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: mapInstance, + styles: { + marker: markerStyle, + }, + geometries: [ + { + id: "selected-marker", + styleId: "marker", + position: new window.TMap.LatLng(lat, lng), + properties: { + title: "选中位置", + }, + }, + ], + }); + + markerRef.current = newMarker; + + // 设置基本位置信息(防止白屏) + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: `${lat}, ${lng}`, + poiname: "选中位置", + maptype: "0", + poiid: "", + }); + + // 反向地理编码获取地址 + if (!isMounted || !geocoderRef.current) { + return; + } + + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: new window.TMap.LatLng(lat, lng) }) + .then((result: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.log("反向地理编码结果:", result); + + try { + if (result && result.result) { + const resultData = result.result; + const address = resultData.address || ""; + const addressComponent = + resultData.address_component || {}; + const formattedAddresses = + resultData.formatted_addresses || {}; + + // 构建地址标签 + let addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + address; + + if (!addressLabel) { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) + parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.street) + parts.push(addressComponent.street); + if (addressComponent.street_number) + parts.push(addressComponent.street_number); + addressLabel = parts.join(""); + } + + if (!addressLabel) { + addressLabel = `${lat}, ${lng}`; + } + + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: addressLabel, + poiname: addressComponent.street || "未知位置", + maptype: "0", + poiid: resultData.poi_id || "", + }); + } else { + message.warning("获取详细地址信息失败,将使用坐标显示"); + } + } catch (error) { + console.error("解析地址信息错误:", error); + message.warning("解析地址信息失败,将使用坐标显示"); + } + }) + .catch((error: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.error("反向地理编码错误:", error); + message.warning("获取详细地址信息失败,将使用坐标显示"); + }); + } catch (error) { + console.error("地图点击处理错误:", error); + message.error("处理地图点击时出错,请重试"); + } + }; + + // 绑定地图点击事件 + mapInstance.on("click", handleMapClickFn); + + // 使用腾讯地图API初始化用户位置 + const initializeUserLocation = ( + lat: number, + lng: number, + isDefault: boolean = false, + ) => { + // 检查弹窗是否仍然打开,以及必要的API是否可用 + if (!isMounted || !mapInstance || !mapContainerRef.current) { + console.log("弹窗已关闭或地图实例无效,跳过初始化位置"); + return; + } + + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.warning("地图标记功能不可用,请刷新页面重试"); + return; + } + + // 创建位置对象 + let userLocation: any = null; + try { + console.log(isDefault ? "使用默认位置:" : "用户位置:", lat, lng); + + // 移动地图中心到位置 + userLocation = new window.TMap.LatLng(lat, lng); + mapInstance.setCenter(userLocation); + mapInstance.setZoom(16); + + // 添加标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: mapInstance, + styles: { + marker: markerStyle, + }, + geometries: [ + { + id: "user-location", + styleId: "marker", + position: userLocation, + properties: { + title: isDefault ? "默认位置" : "当前位置", + }, + }, + ], + }); + + markerRef.current = newMarker; + } catch (error) { + console.error("创建标记点失败:", error); + // 即使创建标记失败,也设置基本的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: `${lat}, ${lng}`, + poiname: isDefault ? "默认位置" : "当前位置", + maptype: "0", + poiid: "", + }); + return; + } + + // 使用腾讯地图服务获取该位置的地址信息 + if (!isMounted || !geocoderRef.current || !userLocation) { + return; + } + + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: userLocation }) + .then((result: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + if (result && result.result) { + const resultData = result.result; + const formattedAddresses = + resultData.formatted_addresses || {}; + const addressComponent = resultData.address_component || {}; + + const addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + resultData.address || + `${lat}, ${lng}`; + + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: addressLabel, + poiname: + addressComponent.street || + (isDefault ? "默认位置" : "当前位置"), + maptype: "0", + poiid: resultData.poi_id || "", + }); + } + }) + .catch((error: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.error("获取地址信息失败:", error); + // 即使获取地址失败,也设置基本的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: `${lat}, ${lng}`, + poiname: isDefault ? "默认位置" : "当前位置", + maptype: "0", + poiid: "", + }); + }); + }; + + // 使用腾讯地图IP定位获取用户位置 + setIsLocating(true); + try { + if (window.geolocationRef) { + window.geolocationRef + .locate() + .then((result: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsLocating(false); + console.log("IP定位结果:", result); + if (result && result.result && result.result.location) { + const { lat, lng } = result.result.location; + // message.info("已定位到您的大致位置"); + initializeUserLocation(lat, lng, false); + } else { + // IP定位失败:使用默认位置 + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + }) + .catch((error: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsLocating(false); + console.error("IP定位失败:", error); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + }); + } else { + // IP定位服务未初始化:使用默认位置 + setIsLocating(false); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + } catch (error) { + // 捕获任何可能的错误,防止白屏 + console.error("定位过程中发生错误:", error); + if (isMounted) { + setIsLocating(false); + message.error("定位服务出现异常,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + } + } catch (error) { + console.error("初始化地图时出错:", error); + message.error("地图加载失败,请刷新页面重试"); + setIsLocating(false); + } + }; + + // 使用 requestAnimationFrame 确保容器尺寸正确后再初始化 + const initTimer = requestAnimationFrame(() => { + // 再次检查容器尺寸 + if (!checkContainerSize()) { + console.log("容器尺寸无效,延迟初始化地图"); + delayTimer = setTimeout(() => { + if (checkContainerSize() && mapContainerRef.current) { + initializeMap(); + } else { + console.error("地图容器尺寸仍然无效"); + message.error("地图容器初始化失败,请刷新页面重试"); + } + }, 100); + return; + } + + // 容器尺寸有效,立即初始化 + initializeMap(); + }); + + // 清理函数 + return () => { + // 标记弹窗已关闭 + isMounted = false; + // 取消 requestAnimationFrame + cancelAnimationFrame(initTimer); + // 清理延迟定时器 + if (delayTimer) { + clearTimeout(delayTimer); + } + // 清理地图事件监听 + if (mapInstance && handleMapClickFn) { + try { + mapInstance.off("click", handleMapClickFn); + } catch (error) { + console.error("清理地图事件监听失败:", error); + } + } + // 清理地图实例 + if (mapInstance) { + try { + mapInstance.destroy(); + } catch (error) { + console.error("销毁地图实例失败:", error); + } + mapInstance = null; + } + // 清理标记点 + if (markerRef.current) { + try { + markerRef.current.setMap(null); + } catch (error) { + console.error("清理标记点失败:", error); + } + markerRef.current = null; + } + // 重置地图状态 + setMap(null); + }; + } + }, [visible, tmapLoaded]); + + // 搜索地址(获取搜索建议) + const handleSearch = () => { + try { + if (!searchValue.trim()) { + message.warning("请输入搜索关键词"); + return; + } + + if (!suggestServiceRef.current) { + message.error("搜索服务未初始化,请刷新页面重试"); + return; + } + + setIsSearching(true); + suggestServiceRef.current + .getSuggestions({ + keyword: searchValue, + location: map ? map.getCenter() : undefined, + }) + .then((result: any) => { + setIsSearching(false); + console.log("搜索建议结果:", result); + + if (result && result.data && result.data.length > 0) { + const searchResults = result.data.map((item: any) => ({ + id: item.id, + title: item.title || item.name || "", + address: item.address || "", + location: { + lat: item.location.lat, + lng: item.location.lng, + }, + adcode: item.adcode || "", + city: item.city || "", + district: item.district || "", + })); + setSearchResults(searchResults); + } else { + setSearchResults([]); + message.info("未找到相关地址"); + } + }) + .catch((error: any) => { + setIsSearching(false); + console.error("搜索失败:", error); + message.error("搜索失败,请重试"); + // 确保搜索状态被重置 + setSearchResults([]); + }); + } catch (error) { + setIsSearching(false); + console.error("搜索处理错误:", error); + message.error("搜索过程中出错,请重试"); + setSearchResults([]); + } + }; + + // 选择搜索结果 + const handleSelectResult = (result: SearchResult) => { + try { + if (!map) { + message.error("地图未初始化,请刷新页面重试"); + return; + } + + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.error("地图API不可用,请刷新页面重试"); + return; + } + + const lat = result.location.lat; + const lng = result.location.lng; + + console.log("选择搜索结果:", result); + + // 移动地图中心 + map.setCenter(new window.TMap.LatLng(lat, lng)); + map.setZoom(16); + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: map, + styles: { + marker: markerStyle, + }, + geometries: [ + { + id: "selected-poi", + styleId: "marker", + position: new window.TMap.LatLng(lat, lng), + properties: { + title: result.title, + }, + }, + ], + }); + + markerRef.current = newMarker; + + // 设置选中的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) + setSelectedLocation({ + x: lat.toString(), + y: lng.toString(), + scale: "16", + label: result.address || result.title, + poiname: result.title || "", + maptype: "0", + poiid: result.id || "", + }); + + // 清空搜索结果 + setSearchResults([]); + setSearchValue(""); + } catch (error) { + console.error("选择搜索结果错误:", error); + message.error("选择位置时出错,请重试"); + } + }; + + // 确认选择 + const handleConfirm = () => { + try { + if (!selectedLocation) { + message.warning("请先选择位置"); + return; + } + + // 转义XML特殊字符,确保格式正确 + // 注意:经纬度在存储时已经格式化为6位小数,直接使用即可 + const escapedLabel = escapeXml(selectedLocation.label); + const escapedPoiname = escapeXml(selectedLocation.poiname); + const scale = selectedLocation.scale || "16"; + const maptype = selectedLocation.maptype || "0"; + const poiid = escapeXml(selectedLocation.poiid || ""); + + // 生成XML格式的位置信息(格式与正确示例保持一致) + const locationXml = + ''; + + // 如果有onConfirm回调,调用它 + if (onConfirm) { + onConfirm(locationXml); + } + + // 如果有addMessage和contract,发送位置消息 + if (addMessage && contract) { + const messageId = +Date.now(); + const localMessage = { + id: messageId, + wechatAccountId: contract.wechatAccountId, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: locationXml, + msgType: 48, // 位置消息类型 + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + + addMessage(localMessage); + console.log(locationXml); + + // 发送消息到服务器 + sendCommand("CmdSendMessage", { + wechatAccountId: contract.wechatAccountId, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + msgSubType: 0, + msgType: 48, + content: locationXml, + seq: messageId, + }); + } + + // 关闭弹窗并重置状态 + handleClose(); + } catch (error) { + console.error("确认位置时出错:", error); + message.error("发送位置信息时出错,请重试"); + } + }; + + // 关闭弹窗 + const handleClose = () => { + setSearchValue(""); + setSearchResults([]); + setSelectedLocation(null); + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + setIsSearching(false); + setIsReverseGeocoding(false); + setIsLocating(false); + onClose(); + }; + + return ( + + 取消 + , + , + ]} + > +
    + {/* 搜索区域 */} +
    + setSearchValue(e.target.value)} + onPressEnter={handleSearch} + prefix={} + suffix={ + + } + className={styles.searchInput} + /> + + {/* 搜索结果列表 */} + {searchResults.length > 0 && ( +
    + ( + handleSelectResult(item)} + > + } + title={item.title} + description={item.address} + /> + + )} + /> +
    + )} +
    + + {/* 地图区域 */} +
    + +
    + +
    + + {/* 选中位置信息 */} + {selectedLocation && ( +
    +
    + 已选择位置 +
    +
    + {selectedLocation.label || selectedLocation.poiname} +
    +
    + 经度: {selectedLocation.y}, 纬度: {selectedLocation.x} +
    +
    + )} +
    + + ); +}; + +export default SelectMap; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx index 1b4d86f77..2d0e4c0e9 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx @@ -6,11 +6,14 @@ import { WechatFriendAllot, WechatFriendRebackAllot, } from "@/pages/pc/ckbox/weChat/api"; +import { dataProcessing } from "@/api/ai"; import { useCurrentContact } from "@/store/module/weChat/weChat"; import { ContactManager } from "@/utils/dbAction/contact"; import { MessageManager } from "@/utils/dbAction/message"; import { useUserStore } from "@/store/module/user"; import { useWeChatStore } from "@/store/module/weChat/weChat"; +import { useMessageStore } from "@weChatStore/message"; +import { useContactStore } from "@weChatStore/contacts"; const { TextArea } = Input; const { Option } = Select; @@ -37,6 +40,8 @@ const ToContract: React.FC = ({ const clearCurrentContact = useWeChatStore( state => state.clearCurrentContact, ); + const removeSessionById = useMessageStore(state => state.removeSessionById); + const deleteContact = useContactStore(state => state.deleteContact); const [visible, setVisible] = useState(false); const [selectedTarget, setSelectedTarget] = useState(null); const [comment, setComment] = useState(""); @@ -79,6 +84,12 @@ const ToContract: React.FC = ({ notifyReceiver: true, comment: comment.trim(), }); + dataProcessing({ + type: "CmdAllotFriend", + wechatChatroomId: currentContact.id, + toAccountId: selectedTarget as number, + wechatAccountId: currentContact.wechatAccountId, + }); } else { await WechatFriendAllot({ wechatFriendId: currentContact.id, @@ -86,6 +97,12 @@ const ToContract: React.FC = ({ notifyReceiver: true, comment: comment.trim(), }); + dataProcessing({ + type: "CmdAllotFriend", + wechatFriendId: currentContact.id, + toAccountId: selectedTarget as number, + wechatAccountId: currentContact.wechatAccountId, + }); } } @@ -97,7 +114,10 @@ const ToContract: React.FC = ({ const currentUserId = useUserStore.getState().user?.id || 0; const contactType = "chatroomId" in currentContact ? "group" : "friend"; - // 1. 从会话列表数据库删除 + // 1. 立即从Store中删除会话(更新UI) + removeSessionById(currentContact.id, contactType); + + // 2. 从会话列表数据库删除 await MessageManager.deleteSession( currentUserId, currentContact.id, @@ -105,11 +125,19 @@ const ToContract: React.FC = ({ ); console.log("✅ 已从会话列表删除"); - // 2. 从联系人数据库删除 + // 3. 从联系人数据库删除 await ContactManager.deleteContact(currentContact.id); console.log("✅ 已从联系人数据库删除"); - // 3. 清空当前选中的联系人(关闭聊天窗口) + // 4. 从联系人Store中删除(更新联系人列表UI) + try { + await deleteContact(currentContact.id); + console.log("✅ 已从联系人列表Store删除"); + } catch (error) { + console.error("从联系人Store删除失败:", error); + } + + // 5. 清空当前选中的联系人(关闭聊天窗口) clearCurrentContact(); message.success("转接成功,已清理本地数据"); @@ -151,7 +179,10 @@ const ToContract: React.FC = ({ const currentUserId = useUserStore.getState().user?.id || 0; const contactType = "chatroomId" in currentContact ? "group" : "friend"; - // 1. 从会话列表数据库删除 + // 1. 立即从Store中删除会话(更新UI) + removeSessionById(currentContact.id, contactType); + + // 2. 从会话列表数据库删除 await MessageManager.deleteSession( currentUserId, currentContact.id, @@ -159,11 +190,19 @@ const ToContract: React.FC = ({ ); console.log("✅ 已从会话列表删除"); - // 2. 从联系人数据库删除 + // 3. 从联系人数据库删除 await ContactManager.deleteContact(currentContact.id); console.log("✅ 已从联系人数据库删除"); - // 3. 清空当前选中的联系人(关闭聊天窗口) + // 4. 从联系人Store中删除(更新联系人列表UI) + try { + await deleteContact(currentContact.id); + console.log("✅ 已从联系人列表Store删除"); + } catch (error) { + console.error("从联系人Store删除失败:", error); + } + + // 5. 清空当前选中的联系人(关闭聊天窗口) clearCurrentContact(); message.success("转回成功,已清理本地数据"); @@ -209,9 +248,9 @@ const ToContract: React.FC = ({ width: "100%", }} > - + */}
    diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss new file mode 100644 index 000000000..1d8603b03 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss @@ -0,0 +1,160 @@ +// 红包消息样式 +.redPacketMessage { + background: transparent; + box-shadow: none; + max-width: 300px; +} + +.redPacketCard { + position: relative; + display: flex; + flex-direction: column; + padding: 16px 20px; + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3); + overflow: hidden; + + // 红包装饰背景 + &::before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient( + circle, + rgba(255, 215, 0, 0.15) 0%, + transparent 70% + ); + animation: shimmer 3s ease-in-out infinite; + } + + // 金色装饰边框 + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 2px solid rgba(255, 215, 0, 0.4); + border-radius: 8px; + pointer-events: none; + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4); + background: linear-gradient(135deg, #ff7b7b 0%, #ff6b7f 100%); + } + + &:active { + transform: translateY(0); + } +} + +@keyframes shimmer { + 0%, + 100% { + transform: rotate(0deg); + } + 50% { + transform: rotate(180deg); + } +} + +.redPacketHeader { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + position: relative; + z-index: 1; +} + +.redPacketIcon { + font-size: 32px; + line-height: 1; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } +} + +.redPacketTitle { + flex: 1; + font-size: 16px; + font-weight: 600; + color: #ffffff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + letter-spacing: 0.5px; + line-height: 1.4; + word-break: break-word; +} + +.redPacketFooter { + display: flex; + align-items: center; + justify-content: flex-end; + position: relative; + z-index: 1; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.3); +} + +.redPacketLabel { + font-size: 12px; + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + + &::before { + content: "💰"; + margin-right: 4px; + font-size: 14px; + } +} + +// 消息文本样式(用于错误提示) +.messageText { + line-height: 1.4; + white-space: pre-wrap; + word-break: break-word; + color: #8c8c8c; + font-size: 13px; +} + +// 响应式设计 +@media (max-width: 768px) { + .redPacketMessage { + max-width: 200px; + } + + .redPacketCard { + padding: 12px 16px; + } + + .redPacketIcon { + font-size: 28px; + } + + .redPacketTitle { + font-size: 14px; + } + + .redPacketLabel { + font-size: 11px; + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx new file mode 100644 index 000000000..75bf86ff5 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import styles from "./RedPacketMessage.module.scss"; + +interface RedPacketData { + nativeurl?: string; + paymsgid?: string; + sendertitle?: string; + [key: string]: any; +} + +interface RedPacketMessageProps { + content: string; +} + +const RedPacketMessage: React.FC = ({ content }) => { + const renderErrorMessage = (fallbackText: string) => ( +
    {fallbackText}
    + ); + + if (typeof content !== "string" || !content.trim()) { + return renderErrorMessage("[红包消息 - 无效内容]"); + } + + try { + const trimmedContent = content.trim(); + const jsonData: RedPacketData = JSON.parse(trimmedContent); + + // 验证是否为红包消息 + const isRedPacket = + jsonData.nativeurl && + typeof jsonData.nativeurl === "string" && + jsonData.nativeurl.includes( + "wxpay://c2cbizmessagehandler/hongbao/receivehongbao", + ); + + if (!isRedPacket) { + return renderErrorMessage("[红包消息 - 格式错误]"); + } + + const title = jsonData.sendertitle || "恭喜发财,大吉大利"; + const paymsgid = jsonData.paymsgid || ""; + + return ( +
    +
    +
    +
    🧧
    +
    {title}
    +
    +
    + 微信红包 +
    +
    +
    + ); + } catch (e) { + console.warn("红包消息解析失败:", e); + return renderErrorMessage("[红包消息 - 解析失败]"); + } +}; + +export default RedPacketMessage; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx index 4bfe7c513..ccd4759b4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx @@ -7,6 +7,7 @@ import VideoMessage from "./components/VideoMessage"; import ClickMenu from "./components/ClickMeau"; import LocationMessage from "./components/LocationMessage"; import SystemRecommendRemarkMessage from "./components/SystemRecommendRemarkMessage/index"; +import RedPacketMessage from "./components/RedPacketMessage"; import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { formatWechatTime } from "@/utils/common"; import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji"; @@ -14,7 +15,11 @@ import styles from "./com.module.scss"; import { useWeChatStore } from "@/store/module/weChat/weChat"; import { useContactStore } from "@/store/module/weChat/contacts"; import { useCustomerStore } from "@weChatStore/customer"; -import { fetchReCallApi, fetchVoiceToTextApi } from "./api"; +import { + fetchReCallApi, + fetchVoiceToTextApi, + getChatroomMemberList, +} from "./api"; import TransmitModal from "./components/TransmitModal"; const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i; @@ -131,6 +136,16 @@ const tryParseContentJson = (content: string): Record | null => { interface MessageRecordProps { contract: ContractData | weChatGroup; } +type GroupRenderItem = { + id: number; + identifier: string; + nickname: string; + avatar: string; + groupId: number; + chatroomId?: string; + wechatId?: string; +}; + const MessageRecord: React.FC = ({ contract }) => { const messagesEndRef = useRef(null); // 右键菜单状态 @@ -152,9 +167,6 @@ const MessageRecord: React.FC = ({ contract }) => { const loadChatMessages = useWeChatStore(state => state.loadChatMessages); const messagesLoading = useWeChatStore(state => state.messagesLoading); const isLoadingData = useWeChatStore(state => state.isLoadingData); - const currentGroupMembers = useWeChatStore( - state => state.currentGroupMembers, - ); const showCheckbox = useWeChatStore(state => state.showCheckbox); const prevMessagesRef = useRef(currentMessages); const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox); @@ -168,6 +180,7 @@ const MessageRecord: React.FC = ({ contract }) => { ); const setTransmitModal = useContactStore(state => state.setTransmitModal); + const [groupRender, setGroupRender] = useState([]); const currentContract = useWeChatStore(state => state.currentContract); const updateQuoteMessageContent = useWeChatStore( @@ -242,6 +255,7 @@ const MessageRecord: React.FC = ({ contract }) => { msg?: ChatRecord, contract?: ContractData | weChatGroup, ) => { + console.log("红包"); if (isLegacyEmojiContent(trimmedContent)) { return renderEmojiContent(rawContent); } @@ -249,6 +263,17 @@ const MessageRecord: React.FC = ({ contract }) => { const jsonData = tryParseContentJson(trimmedContent); if (jsonData && typeof jsonData === "object") { + // 判断是否为红包消息 + if ( + jsonData.nativeurl && + typeof jsonData.nativeurl === "string" && + jsonData.nativeurl.includes( + "wxpay://c2cbizmessagehandler/hongbao/receivehongbao", + ) + ) { + return ; + } + if (jsonData.type === "file" && msg && contract) { return ( = ({ contract }) => { ); }; + useEffect(() => { + const fetchGroupMembers = async () => { + if (!contract.chatroomId) { + setGroupRender([]); + return; + } + try { + const res = await getChatroomMemberList({ groupId: contract.id }); + setGroupRender(res?.list || []); + } catch (error) { + console.error("获取群成员失败", error); + setGroupRender([]); + } + }; + fetchGroupMembers(); + }, [contract.id, contract.chatroomId]); + + const renderGroupUser = (msg: ChatRecord) => { + if (!msg) { + return { avatar: "", nickname: "" }; + } + + const member = groupRender.find( + user => user?.identifier === msg?.senderWechatId, + ); + console.log(member, "member"); + + return { + avatar: member?.avatar || msg?.avatar, + nickname: member?.nickname || msg?.senderNickname, + }; + }; + useEffect(() => { const prevMessages = prevMessagesRef.current; + const prevLength = prevMessages.length; const hasVideoStateChange = currentMessages.some((msg, index) => { // 首先检查消息对象本身是否为null或undefined @@ -384,8 +443,9 @@ const MessageRecord: React.FC = ({ contract }) => { } }); - // 只有在没有视频状态变化时才自动滚动到底部 - if (!hasVideoStateChange && isLoadingData) { + if (currentMessages.length > prevLength && !hasVideoStateChange) { + scrollToBottom(); + } else if (isLoadingData && !hasVideoStateChange) { scrollToBottom(); } @@ -496,13 +556,6 @@ const MessageRecord: React.FC = ({ contract }) => { }; // 获取群成员头像 - const groupMemberAvatar = (msg: ChatRecord) => { - const groupMembers = currentGroupMembers.find( - v => v?.wechatId == msg?.sender?.wechatId, - ); - return groupMembers?.avatar; - }; - // 清理微信ID前缀 const clearWechatidInContent = (sender: any, content: string) => { try { @@ -577,6 +630,7 @@ const MessageRecord: React.FC = ({ contract }) => { const isOwn = msg?.isSend; const isGroup = !!contract.chatroomId; + return (
    = ({ contract }) => { {/* 如果是群聊 */} {isGroup && !isOwn && ( <> - {/* Checkbox 显示控制 */} + {/* 群聊场景下根据消息发送者匹配头像与昵称 */} {showCheckbox && (
    = ({ contract }) => { )} } className={styles.messageAvatar} />
    {!isOwn && (
    - {msg?.sender?.nickname} + {renderGroupUser(msg)?.nickname}
    )} <> diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx index 4966a8631..a986f5cb4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx @@ -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 = ({ useState>(value); const [changedKeys, setChangedKeys] = useState([]); + // 使用 useRef 存储上一次的 value,用于深度比较 + const prevValueRef = useRef>(value); + + // 深度比较函数:比较两个对象的值是否真的变化了 + const isValueChanged = useCallback( + (prev: Record, next: Record) => { + 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 = ({ newEditingFields[field.key] = false; }); setEditingFields(newEditingFields); - }, [value, fields]); + + // 更新 ref + prevValueRef.current = value; + }, [value, fields, isValueChanged]); const handleFieldChange = useCallback( (fieldKey: string, nextVal: string) => { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 6f40e175e..951b5bcc9 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -210,14 +210,34 @@ const Person: React.FC = ({ 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 diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx index 8b1315a9d..7c812bb8f 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx @@ -62,7 +62,7 @@ export interface QuickWordsProps { const QuickWords: React.FC = ({ onInsert }) => { const [activeTab, setActiveTab] = useState( - QuickWordsType.PUBLIC, + QuickWordsType.PERSONAL, ); const [keyword, setKeyword] = useState(""); const [loading, setLoading] = useState(false); diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss index 9cc2941eb..3decb640c 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss @@ -42,11 +42,25 @@ &.active { .userAvatar { - border: 4px solid #1890ff; + border-color: #1890ff; + } + } - .active & { - border-color: #1890ff; - } + .avatarWrapper { + position: relative; + } + + .userAvatar { + border: 4px solid transparent; + border-radius: 50%; + transition: + filter 0.2s ease, + opacity 0.2s ease, + border-color 0.2s ease; + + &.offline { + filter: grayscale(100%); + opacity: 0.75; } } .allUser { @@ -76,20 +90,13 @@ .onlineIndicator { position: absolute; - bottom: 10px; - right: 10px; - width: 8px; - height: 8px; + bottom: 4px; + right: 4px; + width: 12px; + height: 12px; border-radius: 50%; - border: 1px solid #2e2e2e; - - &.online { - background-color: #52c41a; // 绿色表示在线 - } - - &.offline { - background-color: #8c8c8c; // 灰色表示离线 - } + border: 2px solid #ffffff; + background-color: #52c41a; } // 骨架屏样式 diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx index 9ce88a1f3..6aa58c2bb 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx @@ -89,7 +89,6 @@ const CustomerList: React.FC = () => { >
    全部
    -
    {customerList.map(customer => (
    { overflowCount={99} className={styles.messageBadge} > - - {!customer.avatar && customer.name.charAt(0)} - +
    + + {!customer.avatar && customer.name.charAt(0)} + + {customer.isOnline && ( + + )} +
    -
    ))} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss index 77c9db244..5d0fb1f1d 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss @@ -188,6 +188,37 @@ text-align: center; } +// 加载容器样式 +.loadingContainer { + height: 100%; + display: flex; + align-items: flex-start; + justify-content: center; + min-height: 400px; + position: relative; + padding-top: 40px; + + :global(.ant-spin-container) { + width: 100%; + } + + :global(.ant-spin-spinning) { + position: relative; + } + + :global(.ant-spin-text) { + color: #1890ff; + font-size: 14px; + margin-top: 12px; + } +} + +.loadingContent { + width: 100%; + height: 100%; + opacity: 0.6; +} + // 骨架屏样式 .skeletonContainer { padding: 10px; @@ -211,3 +242,41 @@ align-items: center; margin-bottom: 8px; } + +// 同步状态提示栏样式 +.syncStatusBar { + height: 50px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 1px solid #f0f0f0; + background-color: #fafafa; + padding: 0 16px; + flex-shrink: 0; +} + +.syncStatusContent { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + justify-content: space-between; + padding: 0px 20px; +} + +.syncStatusText { + font-size: 14px; + color: #666; +} + +.syncButton { + color: green; + cursor: pointer; + font-size: 14px; + &:hover { + color: green; + } + &:active { + transform: scale(0.98); + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index 2625b5815..1e57ed746 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useState, useRef } from "react"; -import { List, Avatar, Badge, Modal, Input, message, Skeleton } from "antd"; +import { List, Avatar, Badge, Modal, Input, message } from "antd"; import { UserOutlined, TeamOutlined, PushpinOutlined, DeleteOutlined, EditOutlined, + LoadingOutlined, + CheckCircleOutlined, } from "@ant-design/icons"; import styles from "./com.module.scss"; import { @@ -38,14 +40,13 @@ const MessageList: React.FC = () => { // Store状态 const { - loading, hasLoadedOnce, - setLoading, setHasLoadedOnce, sessions, setSessions: setSessionState, } = useMessageStore(); const [filteredSessions, setFilteredSessions] = useState([]); + const [syncing, setSyncing] = useState(false); // 同步状态 // 右键菜单相关状态 const [contextMenu, setContextMenu] = useState<{ @@ -74,6 +75,7 @@ const MessageList: React.FC = () => { const contextMenuRef = useRef(null); const previousUserIdRef = useRef(null); const loadRequestRef = useRef(0); + const autoClickRef = useRef(false); // 右键菜单事件处理 const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => { @@ -296,70 +298,104 @@ const MessageList: React.FC = () => { // ==================== 数据加载 ==================== - // 与服务器同步数据 + // 与服务器同步数据(优化版:逐页同步,立即更新UI) const syncWithServer = async () => { if (!currentUserId) return; + setSyncing(true); // 开始同步,显示同步状态栏 + try { - // 获取会话列表数据(分页获取所有数据) - let allMessages: any[] = []; let page = 1; const limit = 500; let hasMore = true; + let totalProcessed = 0; + let successCount = 0; + let failCount = 0; - // 分页获取会话列表 + // 分页获取会话列表,每页成功后立即同步 while (hasMore) { - const result: any = await getMessageList({ - page, - limit, - }); + try { + const result: any = await getMessageList({ + page, + limit, + }); - if (!result || !Array.isArray(result) || result.length === 0) { - hasMore = false; - break; - } + if (!result || !Array.isArray(result) || result.length === 0) { + hasMore = false; + break; + } - allMessages = [...allMessages, ...result]; + // 立即处理这一页的数据 + const friends = result.filter( + (msg: any) => msg.dataType === "friend" || !msg.chatroomId, + ); + const groups = result + .filter((msg: any) => msg.dataType === "group" || msg.chatroomId) + .map((msg: any) => ({ + ...msg, + chatroomAvatar: msg.chatroomAvatar || msg.avatar || "", + })); - if (result.length < limit) { - hasMore = false; - } else { + // 立即同步这一页到数据库(会触发UI更新) + // 分页同步时跳过删除检查,避免误删其他页的会话 + await MessageManager.syncSessions( + currentUserId, + { + friends, + groups, + }, + { skipDelete: true }, + ); + + totalProcessed += result.length; + successCount++; + + // 判断是否还有下一页 + if (result.length < limit) { + hasMore = false; + } else { + page++; + } + } catch (error) { + // 忽略单页失败,继续处理下一页 + console.error(`第${page}页同步失败:`, error); + failCount++; + + // 如果连续失败太多,停止同步 + if (failCount >= 3) { + console.warn("连续失败次数过多,停止同步"); + break; + } + + // 继续下一页 page++; + if (page > 100) { + // 防止无限循环 + hasMore = false; + } } } - // 分离好友和群聊数据 - const friends = allMessages.filter( - (msg: any) => msg.dataType === "friend" || !msg.chatroomId, - ); - const groups = allMessages - .filter((msg: any) => msg.dataType === "group" || msg.chatroomId) - .map((msg: any) => { - // 确保群聊数据包含正确的头像字段 - // 如果接口返回的是 avatar 字段,需要映射到 chatroomAvatar - return { - ...msg, - chatroomAvatar: msg.chatroomAvatar || msg.avatar || "", - }; - }); - - // 执行增量同步 - const syncResult = await MessageManager.syncSessions(currentUserId, { - friends, - groups, - }); - - // 同步后验证数据 - const verifySession = await MessageManager.getUserSessions(currentUserId); - console.log("同步后的会话数据示例:", verifySession[0]); - console.log( - `会话同步完成: 新增${syncResult.added}, 更新${syncResult.updated}, 删除${syncResult.deleted}`, + `会话同步完成: 成功${successCount}页, 失败${failCount}页, 共处理${totalProcessed}条数据`, ); - - // 会话管理器会在有变更时触发订阅回调 } catch (error) { console.error("同步服务器数据失败:", error); + } finally { + setSyncing(false); // 同步完成,更新状态栏 + } + }; + + // 手动触发同步的函数 + const handleManualSync = async () => { + if (syncing) return; // 如果正在同步,不重复触发 + setSyncing(true); + try { + await syncWithServer(); + } catch (error) { + console.error("手动同步失败:", error); + } finally { + setSyncing(false); } }; @@ -370,6 +406,7 @@ const MessageList: React.FC = () => { previousUserIdRef.current = currentUserId; setHasLoadedOnce(false); setSessionState([]); + autoClickRef.current = false; // 重置自动点击标记 }, [currentUserId, setHasLoadedOnce, setSessionState]); // 初始化加载会话列表 @@ -383,8 +420,6 @@ const MessageList: React.FC = () => { const requestId = ++loadRequestRef.current; const initializeSessions = async () => { - setLoading(true); - try { const cachedSessions = await MessageManager.getUserSessions(currentUserId); @@ -393,6 +428,7 @@ const MessageList: React.FC = () => { return; } + // 有缓存数据立即显示 if (cachedSessions.length > 0) { setSessionState(cachedSessions); } @@ -400,12 +436,18 @@ const MessageList: React.FC = () => { const needsFullSync = cachedSessions.length === 0 || !hasLoadedOnce; if (needsFullSync) { - await syncWithServer(); - if (isCancelled || loadRequestRef.current !== requestId) { - return; - } - setHasLoadedOnce(true); + // 不等待同步完成,让它在后台进行,第一页数据同步后会立即更新UI + syncWithServer() + .then(() => { + if (!isCancelled && loadRequestRef.current === requestId) { + setHasLoadedOnce(true); + } + }) + .catch(error => { + console.error("同步失败:", error); + }); } else { + // 后台同步 syncWithServer().catch(error => { console.error("后台同步失败:", error); }); @@ -414,10 +456,6 @@ const MessageList: React.FC = () => { if (!isCancelled) { console.error("初始化会话列表失败:", error); } - } finally { - if (!isCancelled && loadRequestRef.current === requestId) { - setLoading(false); - } } }; @@ -447,25 +485,97 @@ const MessageList: React.FC = () => { // 根据客服和搜索关键词筛选会话 useEffect(() => { - let filtered = [...sessions]; + const filterSessions = async () => { + let filtered = [...sessions]; - // 根据当前选中的客服筛选 - if (currentCustomer && currentCustomer.id !== 0) { - filtered = filtered.filter(v => v.wechatAccountId === currentCustomer.id); + // 根据当前选中的客服筛选 + if (currentCustomer && currentCustomer.id !== 0) { + filtered = filtered.filter( + v => v.wechatAccountId === currentCustomer.id, + ); + } + + // 根据搜索关键词进行模糊匹配(支持搜索昵称、备注名、微信号) + if (searchKeyword.trim()) { + const keyword = searchKeyword.toLowerCase(); + + // 如果搜索关键词可能是微信号,需要从联系人表补充 wechatId + const sessionsNeedingWechatId = filtered.filter( + v => !v.wechatId && v.type === "friend", + ); + + // 批量从联系人表获取 wechatId + if (sessionsNeedingWechatId.length > 0) { + const contactPromises = sessionsNeedingWechatId.map(session => + ContactManager.getContactByIdAndType( + currentUserId, + session.id, + session.type, + ), + ); + const contacts = await Promise.all(contactPromises); + + // 补充 wechatId 到会话数据 + contacts.forEach((contact, index) => { + if (contact && contact.wechatId) { + const session = sessionsNeedingWechatId[index]; + const sessionIndex = filtered.findIndex( + s => s.id === session.id && s.type === session.type, + ); + if (sessionIndex !== -1) { + filtered[sessionIndex] = { + ...filtered[sessionIndex], + wechatId: contact.wechatId, + }; + } + } + }); + } + + filtered = filtered.filter(v => { + const nickname = (v.nickname || "").toLowerCase(); + const conRemark = (v.conRemark || "").toLowerCase(); + const wechatId = (v.wechatId || "").toLowerCase(); + return ( + nickname.includes(keyword) || + conRemark.includes(keyword) || + wechatId.includes(keyword) + ); + }); + } + + setFilteredSessions(filtered); + }; + + filterSessions(); + }, [sessions, currentCustomer, searchKeyword, currentUserId]); + + // 渲染完毕后自动点击第一个聊天记录 + useEffect(() => { + // 只在以下条件满足时自动点击: + // 1. 有过滤后的会话列表 + // 2. 当前没有选中的联系人 + // 3. 还没有自动点击过 + // 4. 不在搜索状态(避免搜索时自动切换) + if ( + filteredSessions.length > 0 && + !currentContract && + !autoClickRef.current && + !searchKeyword.trim() + ) { + // 延迟一点时间确保DOM已渲染 + const timer = setTimeout(() => { + const firstSession = filteredSessions[0]; + if (firstSession) { + autoClickRef.current = true; + onContactClick(firstSession); + } + }, 100); + + return () => clearTimeout(timer); } - - // 根据搜索关键词进行模糊匹配 - if (searchKeyword.trim()) { - const keyword = searchKeyword.toLowerCase(); - filtered = filtered.filter(v => { - const nickname = (v.nickname || "").toLowerCase(); - const conRemark = (v.conRemark || "").toLowerCase(); - return nickname.includes(keyword) || conRemark.includes(keyword); - }); - } - - setFilteredSessions(filtered); - }, [sessions, currentCustomer, searchKeyword]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filteredSessions, currentContract, searchKeyword]); // ==================== WebSocket消息处理 ==================== @@ -711,147 +821,146 @@ const MessageList: React.FC = () => { } }; - // 渲染骨架屏 - const renderSkeleton = () => ( -
    - {Array(8) - .fill(null) - .map((_, index) => ( -
    - -
    -
    - - -
    - -
    -
    - ))} + // 渲染同步状态提示栏 + const renderSyncStatusBar = () => ( +
    + {syncing ? ( +
    + + 同步中... + +
    + ) : ( +
    + + + 同步完成 + + + 同步 + +
    + )}
    ); return (
    - {loading ? ( - // 加载状态:显示骨架屏 - renderSkeleton() - ) : ( - <> - ( - onContactClick(session)} - onContextMenu={e => handleContextMenu(e, session)} - > -
    - - - ) : ( - - ) - } - /> - -
    -
    -
    - {session.conRemark || - session.nickname || - session.wechatId} -
    -
    - {formatWechatTime(session?.lastUpdateTime)} -
    -
    -
    - {messageFilter(session.content)} -
    + {/* 同步状态提示栏 */} + {renderSyncStatusBar()} + + ( + onContactClick(session)} + onContextMenu={e => handleContextMenu(e, session)} + > +
    + + + ) : ( + + ) + } + /> + +
    +
    +
    + {session.conRemark || session.nickname || session.wechatId} +
    +
    + {formatWechatTime(session?.lastUpdateTime)}
    - - )} - /> - - {/* 右键菜单 */} - {contextMenu.visible && contextMenu.session && ( -
    -
    handleTogglePin(contextMenu.session!)} - > - - {(contextMenu.session.config as any)?.top ? "取消置顶" : "置顶"} -
    -
    handleEditRemark(contextMenu.session!)} - > - - 修改备注 -
    -
    handleDelete(contextMenu.session!)} - > - - 删除 +
    + {messageFilter(session.content)} +
    - )} + + )} + locale={{ + emptyText: + filteredSessions.length === 0 && !syncing ? "暂无会话" : null, + }} + /> - {/* 修改备注Modal */} - - setEditRemarkModal({ - visible: false, - session: null, - remark: "", - }) - } - okText="保存" - cancelText="取消" + {/* 右键菜单 */} + {contextMenu.visible && contextMenu.session && ( +
    +
    handleTogglePin(contextMenu.session!)} > - - setEditRemarkModal(prev => ({ - ...prev, - remark: e.target.value, - })) - } - placeholder="请输入备注" - maxLength={20} - /> - - + + {(contextMenu.session.config as any)?.top ? "取消置顶" : "置顶"} +
    +
    handleEditRemark(contextMenu.session!)} + > + + 修改备注 +
    +
    handleDelete(contextMenu.session!)} + > + + 删除 +
    +
    )} + + {/* 修改备注Modal */} + + setEditRemarkModal({ + visible: false, + session: null, + remark: "", + }) + } + okText="保存" + cancelText="取消" + > + + setEditRemarkModal(prev => ({ + ...prev, + remark: e.target.value, + })) + } + placeholder="请输入备注" + maxLength={20} + /> +
    ); }; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss index 20d9a3d38..0c34dac0e 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss @@ -48,6 +48,43 @@ font-weight: 500; font-size: 14px; color: #262626; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.headerActions { + display: flex; + gap: 8px; + + :global(.ant-btn-sm) { + padding: 0 8px; + } +} + +.actionButton { + color: #fff !important; + border: none !important; + transition: background-color 0.2s ease; + + &:hover, + &:focus { + color: #fff; + opacity: 0.9; + } +} + +.currentPageButton { + background-color: #1677ff !important; +} + +.allSelectButton { + background-color: #13c2c2 !important; +} + +.deselectState { + background-color: #ff7875 !important; } .listContent { @@ -57,6 +94,14 @@ min-height: 0; } +.selectedList { + .listContent { + flex: unset; + height: 600px; + overflow-y: auto; + } +} + .contactItem, .selectedItem { display: flex; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx index 68a99575c..7505b815b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Modal, Input, @@ -32,6 +32,7 @@ const PopChatRoom: React.FC = ({ visible, onCancel }) => { const [showNameModal, setShowNameModal] = useState(false); const [chatroomName, setChatroomName] = useState(""); const pageSize = 10; + const MAX_SELECT_COUNT = 50; // 最多选择联系人数量 const { sendCommand } = useWebSocketStore(); const currentUserId = useUserStore(state => state.user?.id) || 0; const currentCustomer = useCustomerStore(state => state.currentCustomer); @@ -91,12 +92,115 @@ const PopChatRoom: React.FC = ({ visible, onCancel }) => { return filteredContacts.slice(start, end); }, [filteredContacts, page]); + const isContactSelected = useCallback( + (contactId: number) => { + return selectedContacts.some(contact => contact.id === contactId); + }, + [selectedContacts], + ); + + const addContactsToSelection = (contacts: Contact[]) => { + setSelectedContacts(prev => { + const existingIds = new Set(prev.map(contact => contact.id)); + const additions = contacts.filter( + contact => !existingIds.has(contact.id), + ); + if (additions.length === 0) return prev; + return [...prev, ...additions]; + }); + }; + + const removeContactsFromSelection = (contacts: Contact[]) => { + if (contacts.length === 0) return; + const removalIds = new Set(contacts.map(contact => contact.id)); + setSelectedContacts(prev => + prev.filter(contact => !removalIds.has(contact.id)), + ); + }; + + const isCurrentPageFullySelected = useMemo(() => { + return ( + paginatedContacts.length > 0 && + paginatedContacts.every(contact => isContactSelected(contact.id)) + ); + }, [isContactSelected, paginatedContacts]); + + const isAllContactsFullySelected = useMemo(() => { + return ( + filteredContacts.length > 0 && + filteredContacts.every(contact => isContactSelected(contact.id)) + ); + }, [filteredContacts, isContactSelected]); + + const handleToggleCurrentPageSelection = () => { + if (isCurrentPageFullySelected) { + removeContactsFromSelection(paginatedContacts); + } else { + const currentSelectedCount = selectedContacts.length; + const remainingSlots = MAX_SELECT_COUNT - currentSelectedCount; + + if (remainingSlots <= 0) { + message.warning(`最多只能选择${MAX_SELECT_COUNT}个联系人`); + return; + } + + // 获取当前页未选中的联系人 + const unselectedContacts = paginatedContacts.filter( + contact => !isContactSelected(contact.id), + ); + + if (unselectedContacts.length > remainingSlots) { + // 只选择前 remainingSlots 个未选中的联系人 + const contactsToAdd = unselectedContacts.slice(0, remainingSlots); + addContactsToSelection(contactsToAdd); + message.warning( + `最多只能选择${MAX_SELECT_COUNT}个联系人,已选择前${remainingSlots}个`, + ); + } else { + addContactsToSelection(unselectedContacts); + } + } + }; + + const handleToggleAllContactsSelection = () => { + if (isAllContactsFullySelected) { + removeContactsFromSelection(filteredContacts); + } else { + const currentSelectedCount = selectedContacts.length; + const remainingSlots = MAX_SELECT_COUNT - currentSelectedCount; + + if (remainingSlots <= 0) { + message.warning(`最多只能选择${MAX_SELECT_COUNT}个联系人`); + return; + } + + if (filteredContacts.length > remainingSlots) { + // 只选择前 remainingSlots 个未选中的联系人 + const unselectedContacts = filteredContacts.filter( + contact => !isContactSelected(contact.id), + ); + const contactsToAdd = unselectedContacts.slice(0, remainingSlots); + addContactsToSelection(contactsToAdd); + message.warning( + `最多只能选择${MAX_SELECT_COUNT}个联系人,已选择前${remainingSlots}个`, + ); + } else { + addContactsToSelection(filteredContacts); + } + } + }; + // 处理联系人选择 const handleContactSelect = (contact: Contact) => { setSelectedContacts(prev => { if (isContactSelected(contact.id)) { return prev.filter(item => item.id !== contact.id); } + // 检查是否超过50个限制 + if (prev.length >= MAX_SELECT_COUNT) { + message.warning(`最多只能选择${MAX_SELECT_COUNT}个联系人`); + return prev; + } return [...prev, contact]; }); }; @@ -108,11 +212,6 @@ const PopChatRoom: React.FC = ({ visible, onCancel }) => { ); }; - // 检查联系人是否已选择 - const isContactSelected = (contactId: number) => { - return selectedContacts.some(contact => contact.id === contactId); - }; - // 处理取消 const handleCancel = () => { setSearchValue(""); @@ -219,6 +318,31 @@ const PopChatRoom: React.FC = ({ visible, onCancel }) => {
    联系人 ({filteredContacts.length}) +
    + + + (全选最多50个) +
    {loading ? ( diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts index cf2b81174..fff567c7b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts @@ -18,7 +18,7 @@ export const getAllFriends = async () => { let hasMore = true; while (hasMore) { - const result = await getContactList({ page, limit }); + const result = await getContactList({ page, limit }, { debounceGap: 0 }); const friendList = result?.list || []; if ( @@ -56,7 +56,7 @@ export const getAllGroups = async () => { let hasMore = true; while (hasMore) { - const result = await getGroupList({ page, limit }); + const result = await getGroupList({ page, limit }, { debounceGap: 0 }); const groupList = result?.list || []; if (!groupList || !Array.isArray(groupList) || groupList.length === 0) { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx index 0bb8510cd..f898c82c4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx @@ -54,11 +54,7 @@ const CkboxPage: React.FC = () => {
    ) : (
    -
    - -

    欢迎使用触客宝

    -

    选择一个联系人开始聊天

    -
    +
    )} diff --git a/Touchkebao/src/store/module/weChat/weChat.data.ts b/Touchkebao/src/store/module/weChat/weChat.data.ts index dc9aa163e..1b0c1e94c 100644 --- a/Touchkebao/src/store/module/weChat/weChat.data.ts +++ b/Touchkebao/src/store/module/weChat/weChat.data.ts @@ -46,6 +46,8 @@ export interface WeChatState { currentMessagesPageSize: number; /** 是否还有更多历史消息 */ currentMessagesHasMore: boolean; + /** 当前消息请求ID,用于防止跨联系人数据串联 */ + currentMessagesRequestId: number; /** 添加新消息 */ addMessage: (message: ChatRecord) => void; /** 更新指定消息 */ diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts index 89a001a95..bfedd6a13 100644 --- a/Touchkebao/src/store/module/weChat/weChat.ts +++ b/Touchkebao/src/store/module/weChat/weChat.ts @@ -18,6 +18,7 @@ import { getFriendInjectConfig, } from "@/pages/pc/ckbox/api"; import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; +import { useWebSocketStore } from "@/store/module/websocket/websocket"; /** * AI请求防抖管理 @@ -392,13 +393,73 @@ export const manualTriggerAi = async () => { return false; } - // 更新AI回复内容 - state.updateQuoteMessageContent(messageContent?.content || ""); - state.updateIsLoadingAiChat(false); - console.log( - `✅ 手动AI回复成功 [${generationId}]:`, - messageContent?.content, - ); + // 获取当前接待类型 + const aiType = (currentContract as any)?.aiType || 0; // 0=人工, 1=AI辅助, 2=AI接管 + const aiResponseContent = messageContent?.content || ""; + const isWechatGroup = !!(currentContract as any)?.chatroomId; + + // 根据接待类型处理AI回复 + if (aiType === 2 && aiResponseContent) { + // AI接管模式:直接发送消息,不经过MessageEnter组件 + const messageId = +Date.now(); + + // 构造本地消息对象 + const localMessage: ChatRecord = { + id: messageId, + wechatAccountId: currentContract.wechatAccountId, + wechatFriendId: isWechatGroup ? 0 : currentContract.id, + wechatChatroomId: isWechatGroup ? currentContract.id : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: aiResponseContent, + msgType: 1, + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + + // 添加到消息列表 + state.addMessage(localMessage); + + // 直接发送消息 + const { sendCommand } = useWebSocketStore.getState(); + sendCommand("CmdSendMessage", { + wechatAccountId: currentContract.wechatAccountId, + wechatChatroomId: isWechatGroup ? currentContract.id : 0, + wechatFriendId: isWechatGroup ? 0 : currentContract.id, + msgSubType: 0, + msgType: 1, + content: aiResponseContent, + seq: messageId, + }); + + state.updateIsLoadingAiChat(false); + console.log( + `✅ 手动AI接管模式:直接发送消息 [${generationId}]:`, + aiResponseContent, + ); + } else if (aiType === 1) { + // AI辅助模式:设置quoteMessageContent,让MessageEnter组件填充输入框 + state.updateQuoteMessageContent(aiResponseContent); + state.updateIsLoadingAiChat(false); + console.log( + `✅ 手动AI辅助模式:填充输入框 [${generationId}]:`, + aiResponseContent, + ); + } else { + // 其他情况 + state.updateIsLoadingAiChat(false); + } // 清除当前生成ID currentAiGenerationId = null; @@ -455,6 +516,7 @@ export const useWeChatStore = create()( currentMessagesPage: 1, currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, currentMessagesHasMore: true, + currentMessagesRequestId: 0, // ==================== 聊天消息管理方法 ==================== /** 添加新消息到当前聊天 */ @@ -542,6 +604,7 @@ export const useWeChatStore = create()( currentMessagesPage: 1, currentMessagesHasMore: true, currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, + currentMessagesRequestId: 0, }); }, /** 设置当前联系人并加载相关数据 */ @@ -555,6 +618,7 @@ export const useWeChatStore = create()( pendingMessages = []; const state = useWeChatStore.getState(); + const newRequestId = Date.now(); // 切换联系人时清空当前消息,等待重新加载 set({ currentMessages: [], @@ -562,6 +626,7 @@ export const useWeChatStore = create()( currentMessagesHasMore: true, currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, isLoadingAiChat: false, + currentMessagesRequestId: newRequestId, }); const params: any = {}; @@ -582,7 +647,10 @@ export const useWeChatStore = create()( set({ aiQuoteMessageContent: result }); }); // 注意:会话列表的未读数清零在MessageList组件的onContactClick中处理 - set({ currentContract: contract }); + set({ + currentContract: contract, + currentMessagesRequestId: newRequestId, + }); updateConfig({ id: contract.id, config: { chat: true }, @@ -595,8 +663,10 @@ export const useWeChatStore = create()( loadChatMessages: async (Init: boolean, pageOverride?: number) => { const state = useWeChatStore.getState(); const contact = state.currentContract; + const requestIdAtStart = state.currentMessagesRequestId; + const requestedContactId = contact?.id; - if (!contact) { + if (!contact || !requestedContactId) { return; } @@ -655,22 +725,42 @@ export const useWeChatStore = create()( }); } - set(current => ({ - currentMessages: Init - ? sortedMessages - : [...sortedMessages, ...current.currentMessages], - currentGroupMembers: - Init && isGroup ? nextGroupMembers : current.currentGroupMembers, - currentMessagesPage: paginationMeta.page, - currentMessagesPageSize: paginationMeta.limit, - currentMessagesHasMore: paginationMeta.hasMore, - })); + set(current => { + if ( + current.currentMessagesRequestId !== requestIdAtStart || + !current.currentContract || + current.currentContract.id !== requestedContactId + ) { + return {}; + } + return { + currentMessages: Init + ? sortedMessages + : [...sortedMessages, ...current.currentMessages], + currentGroupMembers: + Init && isGroup + ? nextGroupMembers + : current.currentGroupMembers, + currentMessagesPage: paginationMeta.page, + currentMessagesPageSize: paginationMeta.limit, + currentMessagesHasMore: paginationMeta.hasMore, + }; + }); } catch (error) { console.error("获取聊天消息失败:", error); } finally { - set({ - messagesLoading: false, - isLoadingData: false, + set(current => { + if ( + current.currentMessagesRequestId !== requestIdAtStart || + !current.currentContract || + current.currentContract.id !== requestedContactId + ) { + return {}; + } + return { + messagesLoading: false, + isLoadingData: false, + }; }); } }, @@ -755,9 +845,10 @@ export const useWeChatStore = create()( currentMessages: [...state.currentMessages, message], })); - // 只有文字消息才触发AI(msgType === 1) + // 只有文字消息才触发AI(msgType === 1),且必须是对方发送的消息(isSend !== true) if ( message.msgType === 1 && + !message.isSend && [1, 2].includes((currentContract as any).aiType || 0) ) { console.log("📨 收到新消息,准备触发AI"); @@ -837,15 +928,85 @@ export const useWeChatStore = create()( return; } - // 附加生成ID到回复内容 - set(() => ({ - quoteMessageContent: messageContent?.content || "", - isLoadingAiChat: false, - })); - console.log( - `✅ AI回复成功 [${generationId}]:`, - messageContent?.content, - ); + // 获取当前接待类型 + const aiType = (currentContract as any)?.aiType || 0; // 0=人工, 1=AI辅助, 2=AI接管 + const aiResponseContent = messageContent?.content || ""; + + // 根据接待类型处理AI回复 + if (aiType === 2 && aiResponseContent) { + // AI接管模式:直接发送消息,不经过MessageEnter组件 + const messageId = +Date.now(); + + // 构造本地消息对象 + const localMessage: ChatRecord = { + id: messageId, + wechatAccountId: currentContract.wechatAccountId, + wechatFriendId: isWechatGroup ? 0 : currentContract.id, + wechatChatroomId: isWechatGroup + ? currentContract.id + : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: aiResponseContent, + msgType: 1, + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + + // 添加到消息列表 + set(state => ({ + currentMessages: [ + ...state.currentMessages, + localMessage, + ], + isLoadingAiChat: false, + })); + + // 直接发送消息 + const { sendCommand } = useWebSocketStore.getState(); + sendCommand("CmdSendMessage", { + wechatAccountId: currentContract.wechatAccountId, + wechatChatroomId: isWechatGroup + ? currentContract.id + : 0, + wechatFriendId: isWechatGroup ? 0 : currentContract.id, + msgSubType: 0, + msgType: 1, + content: aiResponseContent, + seq: messageId, + }); + + console.log( + `✅ AI接管模式:直接发送消息 [${generationId}]:`, + aiResponseContent, + ); + } else if (aiType === 1) { + // AI辅助模式:设置quoteMessageContent,让MessageEnter组件填充输入框 + set(() => ({ + quoteMessageContent: aiResponseContent, + isLoadingAiChat: false, + })); + console.log( + `✅ AI辅助模式:填充输入框 [${generationId}]:`, + aiResponseContent, + ); + } else { + // 其他情况 + set(() => ({ + isLoadingAiChat: false, + })); + } // 清除当前生成ID currentAiGenerationId = null; diff --git a/Touchkebao/src/store/module/websocket/msgManage.ts b/Touchkebao/src/store/module/websocket/msgManage.ts index 2fcaceb33..a7b11732c 100644 --- a/Touchkebao/src/store/module/websocket/msgManage.ts +++ b/Touchkebao/src/store/module/websocket/msgManage.ts @@ -6,19 +6,26 @@ import { Messages } from "./msg.data"; import { db } from "@/utils/db"; import { Modal } from "antd"; import { useCustomerStore, updateCustomerList } from "../weChat/customer"; +import { dataProcessing } from "@/api/ai"; // 消息处理器类型定义 type MessageHandler = (message: WebSocketMessage) => void; -const addMessage = useWeChatStore.getState().addMessage; -const recallMessage = useWeChatStore.getState().recallMessage; -const receivedMsg = useWeChatStore.getState().receivedMsg; -const findMessageBySeq = useWeChatStore.getState().findMessageBySeq; -const findMessageById = useWeChatStore.getState().findMessageById; -const updateMessage = useWeChatStore.getState().updateMessage; -const updateMomentCommonLoading = - useWeChatStore.getState().updateMomentCommonLoading; -const addMomentCommon = useWeChatStore.getState().addMomentCommon; -const setFileDownloadUrl = useWeChatStore.getState().setFileDownloadUrl; -const setFileDownloading = useWeChatStore.getState().setFileDownloading; + +// 延迟获取 store 方法,避免循环依赖问题 +const getWeChatStoreMethods = () => { + const state = useWeChatStore.getState(); + return { + addMessage: state.addMessage, + recallMessage: state.recallMessage, + receivedMsg: state.receivedMsg, + findMessageBySeq: state.findMessageBySeq, + findMessageById: state.findMessageById, + updateMessage: state.updateMessage, + updateMomentCommonLoading: state.updateMomentCommonLoading, + addMomentCommon: state.addMomentCommon, + setFileDownloadUrl: state.setFileDownloadUrl, + setFileDownloading: state.setFileDownloading, + }; +}; // 消息处理器映射 const messageHandlers: Record = { // 微信账号存活状态响应 @@ -45,7 +52,9 @@ const messageHandlers: Record = { updateCustomerList(updatedCustomerList); }, // 发送消息响应 - CmdSendMessageResp: message => { + CmdSendMessageResp: (message: Messages) => { + const { findMessageBySeq, updateMessage } = getWeChatStoreMethods(); + const msg = findMessageBySeq(message.seq); if (msg) { updateMessage(message.seq, { @@ -53,11 +62,23 @@ const messageHandlers: Record = { id: message.friendMessage?.id || message.chatroomMessage?.id, }); } + //异步传新消息给数据库 + goAsyncServiceData(message); }, CmdSendMessageResult: message => { + const { updateMessage } = getWeChatStoreMethods(); updateMessage(message.friendMessageId || message.chatroomMessageId, { sendStatus: 0, }); + // 最终消息同步处理 + dataProcessing({ + chatroomMessageId: message.chatroomMessageId, + friendMessageId: message.friendMessageId, + sendStatus: message.sendStatus, + type: "CmdSendMessageResult", + wechatAccountId: 1, + wechatTime: message.wechatTime, + }); }, // 接收消息响应 CmdReceiveMessageResp: message => { @@ -68,8 +89,10 @@ const messageHandlers: Record = { //收到消息 CmdNewMessage: (message: Messages) => { // 处理消息本身 + const { receivedMsg } = getWeChatStoreMethods(); receivedMsg(message.friendMessage || message.chatroomMessage); - + //异步传新消息给数据库 + goAsyncServiceData(message); // 触发会话列表更新事件 const msgData = message.friendMessage || message.chatroomMessage; if (msgData) { @@ -107,6 +130,7 @@ const messageHandlers: Record = { // setVideoUrl(message.friendMessageId, message.url); }, CmdDownloadFileResult: message => { + const { setFileDownloadUrl, setFileDownloading } = getWeChatStoreMethods(); const messageId = message.friendMessageId || message.chatroomMessageId; if (!messageId) { @@ -124,6 +148,8 @@ const messageHandlers: Record = { }, CmdFetchMomentResult: message => { + const { addMomentCommon, updateMomentCommonLoading } = + getWeChatStoreMethods(); addMomentCommon(message.result); updateMomentCommonLoading(false); }, @@ -131,7 +157,7 @@ const messageHandlers: Record = { CmdNotify: async (message: WebSocketMessage) => { console.log("通知消息", message); // 在这里添加具体的处理逻辑 - if (message.notify == "Auth failed") { + if (["Auth failed", "Kicked out"].includes(message.notify)) { // 避免重复弹窗 if ((window as any).__CKB_AUTH_FAILED_SHOWN__) { return; @@ -162,16 +188,18 @@ const messageHandlers: Record = { //撤回消息 CmdMessageRecalled: message => { + const { recallMessage } = getWeChatStoreMethods(); const MessageId = message.friendMessageId || message.chatroomMessageId; recallMessage(MessageId); }, CmdVoiceToTextResult: message => { + const { findMessageById, updateMessage } = getWeChatStoreMethods(); const msg = findMessageById( message.friendMessageId || message.chatroomMessageId, ); - const content = JSON.parse(msg.content); if (msg) { + const content = JSON.parse(msg.content); updateMessage(msg.id, { content: JSON.stringify({ ...content, @@ -180,8 +208,21 @@ const messageHandlers: Record = { }); } }, - - // 可以继续添加更多处理器... +}; +//消息异步同步 +const goAsyncServiceData = (message: Messages) => { + const chatroomMessages = message.chatroomMessage + ? [message.chatroomMessage] + : null; + const friendMessages = message.friendMessage ? [message.friendMessage] : null; + dataProcessing({ + chatroomMessage: chatroomMessages, + friendMessage: friendMessages, + type: "CmdNewMessage", + wechatAccountId: + message.friendMessage?.wechatAccountId || + message.chatroomMessage?.wechatAccountId, + }); }; // 默认处理器 diff --git a/Touchkebao/src/store/module/websocket/websocket.ts b/Touchkebao/src/store/module/websocket/websocket.ts index 57ed7f15e..912daf48b 100644 --- a/Touchkebao/src/store/module/websocket/websocket.ts +++ b/Touchkebao/src/store/module/websocket/websocket.ts @@ -2,7 +2,6 @@ import { createPersistStore } from "@/store/createPersistStore"; import { useUserStore } from "../user"; import { useCkChatStore } from "@/store/module/ckchat/ckchat"; import { useCustomerStore } from "@/store/module/weChat/customer"; -const { getAccountId } = useCkChatStore.getState(); import { msgManageCore } from "./msgManage"; // WebSocket消息类型 export interface WebSocketMessage { @@ -141,6 +140,7 @@ export const useWebSocketStore = createPersistStore( } // 构建WebSocket URL + const { getAccountId } = useCkChatStore.getState(); const params = new URLSearchParams({ client: fullConfig.client.toString(), accountId: getAccountId().toString(), @@ -330,6 +330,7 @@ export const useWebSocketStore = createPersistStore( // console.log("WebSocket连接成功"); const { token2 } = useUserStore.getState(); + const { getAccountId } = useCkChatStore.getState(); // 发送登录命令 if (currentState.config) { currentState.sendCommand("CmdSignIn", { @@ -350,36 +351,6 @@ export const useWebSocketStore = createPersistStore( _handleMessage: (event: MessageEvent) => { try { const data = JSON.parse(event.data); - // console.log("收到WebSocket消息:", data); - - // 处理特定的通知消息 - if (data.cmdType === "CmdNotify") { - // 处理Auth failed通知 - if (data.notify === "Auth failed" || data.notify === "Kicked out") { - // console.error(`WebSocket ${data.notify},断开连接`); - // Toast.show({ - // content: `WebSocket ${data.notify},断开连接`, - // position: "top", - // }); - - // 禁用自动重连 - if (get().config) { - set({ - config: { - ...get().config!, - autoReconnect: false, - }, - }); - } - - // 停止客服状态查询定时器 - get()._stopAliveStatusTimer(); - - // 断开连接 - get().disconnect(); - return; - } - } const currentState = get(); const newMessage: WebSocketMessage = { diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts index abbb7fdb7..77371a2b6 100644 --- a/Touchkebao/src/utils/dbAction/contact.ts +++ b/Touchkebao/src/utils/dbAction/contact.ts @@ -40,6 +40,7 @@ export class ContactManager { /** * 搜索联系人 + * 支持搜索昵称、备注名、微信号 */ static async searchContacts( userId: number, @@ -52,8 +53,11 @@ export class ContactManager { return contacts.filter(contact => { const nickname = (contact.nickname || "").toLowerCase(); const conRemark = (contact.conRemark || "").toLowerCase(); + const wechatId = (contact.wechatId || "").toLowerCase(); return ( - nickname.includes(lowerKeyword) || conRemark.includes(lowerKeyword) + nickname.includes(lowerKeyword) || + conRemark.includes(lowerKeyword) || + wechatId.includes(lowerKeyword) ); }); } catch (error) { @@ -353,13 +357,13 @@ export class ContactManager { exclude: boolean = false, ): Promise { 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 +398,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) { diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts index e34041b98..53610a63f 100644 --- a/Touchkebao/src/utils/dbAction/message.ts +++ b/Touchkebao/src/utils/dbAction/message.ts @@ -231,6 +231,8 @@ export class MessageManager { "phone", "region", "extendFields", + "wechatId", // 添加wechatId比较 + "alias", // 添加alias比较 ]; for (const field of fieldsToCompare) { @@ -257,6 +259,8 @@ export class MessageManager { * 增量同步会话数据 * @param userId 用户ID * @param serverData 服务器数据 + * @param options 同步选项 + * @param options.skipDelete 是否跳过删除检查(用于分页增量同步) * @returns 同步结果统计 */ static async syncSessions( @@ -265,6 +269,9 @@ export class MessageManager { friends?: ContractData[]; groups?: weChatGroup[]; }, + options?: { + skipDelete?: boolean; // 是否跳过删除检查(用于分页增量同步) + }, ): Promise<{ added: number; updated: number; @@ -321,10 +328,12 @@ export class MessageManager { } } - // 检查删除 - for (const localSession of localSessions) { - if (!serverSessionMap.has(localSession.serverId)) { - toDelete.push(localSession.serverId); + // 检查删除(仅在非增量同步模式下执行) + if (!options?.skipDelete) { + for (const localSession of localSessions) { + if (!serverSessionMap.has(localSession.serverId)) { + toDelete.push(localSession.serverId); + } } } @@ -650,7 +659,7 @@ export class MessageManager { updatedSession.sortKey = this.generateSortKey(updatedSession); await chatSessionService.update(serverId, updatedSession); - console.log(`会话时间已更新: ${serverId} -> ${newTime}`); + await this.triggerCallbacks(userId); } } catch (error) { console.error("更新会话时间失败:", error); @@ -821,7 +830,7 @@ export class MessageManager { }; await chatSessionService.create(sessionWithSortKey); - console.log(`创建新会话: ${session.nickname || session.wechatId}`); + await this.triggerCallbacks(userId); } catch (error) { console.error("创建会话失败:", error); throw error; diff --git a/Touchkebao/src/utils/filter.ts b/Touchkebao/src/utils/filter.ts index 5e2d37547..22887a8bd 100644 --- a/Touchkebao/src/utils/filter.ts +++ b/Touchkebao/src/utils/filter.ts @@ -58,6 +58,11 @@ export const messageFilter = (message: string) => { return "[图片]"; } + // XML 格式的位置消息:包含 ]/i.test(message)) { + return "[位置]"; + } + // 其他情况直接返回原始消息 return message; } diff --git a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx b/Touchkebao/消息功能规划.md similarity index 100% rename from Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx rename to Touchkebao/消息功能规划.md