|
|
@@ -0,0 +1,1278 @@
|
|
|
+import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { Router, ActivatedRoute } from '@angular/router';
|
|
|
+import { IonicModule } from '@ionic/angular';
|
|
|
+import { addIcons } from 'ionicons';
|
|
|
+import {
|
|
|
+ chevronDownCircleOutline,
|
|
|
+ refreshCircle,
|
|
|
+ arrowBack,
|
|
|
+ qrCode,
|
|
|
+ link,
|
|
|
+ personAdd,
|
|
|
+ send,
|
|
|
+ checkmarkCircle,
|
|
|
+ close
|
|
|
+} from 'ionicons/icons';
|
|
|
+import { WxworkSDK, WxworkCorp, WxworkAuth } from 'fmode-ng/core';
|
|
|
+import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
|
|
|
+import { ChatMessageAIService } from '../../services/chat-message-ai.service';
|
|
|
+
|
|
|
+// 注册 Ionicons
|
|
|
+addIcons({
|
|
|
+ 'chevron-down-circle-outline': chevronDownCircleOutline,
|
|
|
+ 'refresh-circle': refreshCircle,
|
|
|
+ 'arrow-back': arrowBack,
|
|
|
+ 'qr-code': qrCode,
|
|
|
+ 'link': link,
|
|
|
+ 'person-add': personAdd,
|
|
|
+ 'send': send,
|
|
|
+ 'checkmark-circle': checkmarkCircle,
|
|
|
+ 'close': close
|
|
|
+});
|
|
|
+
|
|
|
+const Parse = FmodeParse.with('nova');
|
|
|
+
|
|
|
+interface ChatMessage {
|
|
|
+ id: string;
|
|
|
+ senderName: string;
|
|
|
+ senderUserId: string;
|
|
|
+ content: string;
|
|
|
+ time: Date;
|
|
|
+ isCustomer: boolean;
|
|
|
+ needsReply: boolean;
|
|
|
+ msgType: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface SuggestedReply {
|
|
|
+ id: string;
|
|
|
+ text: string;
|
|
|
+ icon: string;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 会话激活页面组件
|
|
|
+ *
|
|
|
+ * 功能:
|
|
|
+ * 1. 用户源聊天记录筛选
|
|
|
+ * 2. 三种入群方式(二维码/链接/手动拉群)
|
|
|
+ * 3. 入群自动化文案
|
|
|
+ * 4. 超时未回复提醒(10分钟)
|
|
|
+ * 5. 辅助回复功能(AI生成3-5条备选)
|
|
|
+ */
|
|
|
+@Component({
|
|
|
+ selector: 'app-chat-activation',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule, IonicModule],
|
|
|
+ templateUrl: './chat-activation.component.html',
|
|
|
+ styleUrls: ['./chat-activation.component.scss']
|
|
|
+})
|
|
|
+export class ChatActivationComponent implements OnInit, OnDestroy {
|
|
|
+ // 路由参数
|
|
|
+ cid: string = '';
|
|
|
+ projectId: string = '';
|
|
|
+ chatId: string = '';
|
|
|
+
|
|
|
+ // 企微SDK
|
|
|
+ wxwork: WxworkSDK | null = null;
|
|
|
+ wecorp: WxworkCorp | null = null;
|
|
|
+ wxAuth: WxworkAuth | null = null;
|
|
|
+
|
|
|
+ // 当前用户和项目
|
|
|
+ currentUser: FmodeObject | null = null;
|
|
|
+ project: FmodeObject | null = null;
|
|
|
+ groupChat: FmodeObject | null = null;
|
|
|
+ contact: FmodeObject | null = null;
|
|
|
+
|
|
|
+ // 加载状态
|
|
|
+ loading: boolean = true;
|
|
|
+ loadingMessages: boolean = false;
|
|
|
+ sendingIntro: boolean = false;
|
|
|
+
|
|
|
+ // 聊天消息
|
|
|
+ messages: ChatMessage[] = [];
|
|
|
+ filteredMessages: ChatMessage[] = [];
|
|
|
+
|
|
|
+ // 筛选状态
|
|
|
+ showOnlyCustomer: boolean = false;
|
|
|
+ showOnlyUnread: boolean = false;
|
|
|
+
|
|
|
+ // 统计数据
|
|
|
+ totalMessages: number = 0;
|
|
|
+ customerMessageCount: number = 0;
|
|
|
+ unreadCount: number = 0;
|
|
|
+
|
|
|
+ // 入群方式
|
|
|
+ joinMethods = {
|
|
|
+ qrCode: '',
|
|
|
+ link: '',
|
|
|
+ manualAdd: true
|
|
|
+ };
|
|
|
+
|
|
|
+ // 入群方式URL(用于API操作)
|
|
|
+ qrCodeUrl: string = '';
|
|
|
+ joinLink: string = '';
|
|
|
+
|
|
|
+ // 自动化文案
|
|
|
+ introSent: boolean = false;
|
|
|
+ introTemplate: string = '';
|
|
|
+
|
|
|
+ // 辅助回复
|
|
|
+ suggestedReplies: SuggestedReply[] = [];
|
|
|
+ showSuggestions: boolean = false;
|
|
|
+ selectedMessage: ChatMessage | null = null;
|
|
|
+
|
|
|
+ // 二维码显示
|
|
|
+ showQRCode: boolean = false;
|
|
|
+
|
|
|
+ // 定时器
|
|
|
+ private checkTimer: any = null;
|
|
|
+
|
|
|
+ // 刷新状态
|
|
|
+ refreshing: boolean = false;
|
|
|
+
|
|
|
+ // AI生成状态
|
|
|
+ generatingAI: boolean = false;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private router: Router,
|
|
|
+ private route: ActivatedRoute,
|
|
|
+ private cdr: ChangeDetectorRef,
|
|
|
+ private chatAI: ChatMessageAIService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ async ngOnInit() {
|
|
|
+ // 获取路由参数
|
|
|
+ this.cid = this.route.snapshot.paramMap.get('cid') || localStorage.getItem('company') || '';
|
|
|
+ this.chatId = this.route.snapshot.paramMap.get('chatId') || '';
|
|
|
+
|
|
|
+ console.log('🔍 初始化参数:', { cid: this.cid, chatId: this.chatId });
|
|
|
+
|
|
|
+ // 开发模式:设置测试数据
|
|
|
+ if (!this.cid) {
|
|
|
+ console.log('🧪 开发模式:使用测试公司ID');
|
|
|
+ this.cid = 'cDL6R1hgSi';
|
|
|
+ localStorage.setItem('company', this.cid);
|
|
|
+
|
|
|
+ // 设置测试用户信息
|
|
|
+ const testUserInfo = {
|
|
|
+ userid: 'woAs2qCQAAGQckyg7AQBxhMEoSwnlTvg',
|
|
|
+ errcode: 0,
|
|
|
+ errmsg: 'ok',
|
|
|
+ cid: 'cDL6R1hgSi'
|
|
|
+ };
|
|
|
+ localStorage.setItem(`${this.cid}/USERINFO`, JSON.stringify(testUserInfo));
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.initializeSDK();
|
|
|
+ await this.loadData();
|
|
|
+
|
|
|
+ // 启动定时检查(每分钟检查一次未回复消息)
|
|
|
+ this.startUnreadCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy() {
|
|
|
+ if (this.checkTimer) {
|
|
|
+ clearInterval(this.checkTimer);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化企微SDK
|
|
|
+ */
|
|
|
+ async initializeSDK() {
|
|
|
+ try {
|
|
|
+ if (!this.cid) {
|
|
|
+ console.error('❌ 缺少公司ID (cid)');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('✅ 初始化企微SDK, cid:', this.cid);
|
|
|
+
|
|
|
+ // 使用 fmode-ng 的正确方式初始化SDK
|
|
|
+ // @ts-ignore - fmode-ng type issue
|
|
|
+ this.wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
|
|
|
+ // @ts-ignore - fmode-ng type issue
|
|
|
+ this.wecorp = new WxworkCorp(this.cid);
|
|
|
+
|
|
|
+ console.log('✅ 企微SDK初始化成功');
|
|
|
+
|
|
|
+ // 尝试获取当前用户(可选,不阻塞页面)
|
|
|
+ try {
|
|
|
+ // @ts-ignore - fmode-ng type issue
|
|
|
+ this.wxAuth = new WxworkAuth({ cid: this.cid, appId: 'crm' });
|
|
|
+ this.currentUser = await this.wxwork.getCurrentUser();
|
|
|
+
|
|
|
+ if (this.currentUser) {
|
|
|
+ console.log('✅ 当前用户:', this.currentUser.get('name'));
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 未获取到用户信息,使用本地缓存');
|
|
|
+ }
|
|
|
+ } catch (authError) {
|
|
|
+ console.warn('⚠️ 企微授权失败,使用本地模式:', authError);
|
|
|
+ // 继续执行,不阻塞页面
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 初始化SDK失败:', error);
|
|
|
+ // 不抛出错误,允许页面继续加载
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载数据
|
|
|
+ */
|
|
|
+ async loadData() {
|
|
|
+ try {
|
|
|
+ this.loading = true;
|
|
|
+
|
|
|
+ console.log('📥 开始加载数据...');
|
|
|
+ console.log('📋 参数:', { cid: this.cid, chatId: this.chatId });
|
|
|
+
|
|
|
+ // 1. 加载群聊 - 参考project-detail的查询方式
|
|
|
+ if (this.chatId) {
|
|
|
+ console.log('🔍 查询群聊, chatId:', this.chatId, 'cid:', this.cid);
|
|
|
+
|
|
|
+ // 方式1:先尝试通过 objectId 查询(Parse数据库ID)
|
|
|
+ try {
|
|
|
+ console.log('方式1: 通过objectId查询...');
|
|
|
+ let gcQuery = new Parse.Query('GroupChat');
|
|
|
+ this.groupChat = await gcQuery.get(this.chatId);
|
|
|
+ console.log('✅ 通过objectId找到群聊');
|
|
|
+ } catch (objectIdError) {
|
|
|
+ console.log('⚠️ objectId查询失败,尝试chat_id查询');
|
|
|
+ this.groupChat = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方式2:如果没找到,尝试通过企微 chat_id 查询(参考project-detail)
|
|
|
+ if (!this.groupChat) {
|
|
|
+ console.log('方式2: 通过chat_id查询...');
|
|
|
+ const gcQuery2 = new Parse.Query('GroupChat');
|
|
|
+ gcQuery2.equalTo('chat_id', this.chatId);
|
|
|
+ gcQuery2.equalTo('company', this.cid); // 直接传cid字符串,不用Pointer
|
|
|
+
|
|
|
+ this.groupChat = await gcQuery2.first();
|
|
|
+
|
|
|
+ if (this.groupChat) {
|
|
|
+ console.log('✅ 通过chat_id找到群聊');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.groupChat) {
|
|
|
+ console.log('✅ 找到群聊:', this.groupChat.get('name'));
|
|
|
+ console.log('📊 群聊信息:', {
|
|
|
+ objectId: this.groupChat.id,
|
|
|
+ chat_id: this.groupChat.get('chat_id'),
|
|
|
+ name: this.groupChat.get('name'),
|
|
|
+ memberCount: (this.groupChat.get('member_list') || []).length
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取关联的项目(使用include)
|
|
|
+ const projectPointer = this.groupChat.get('project');
|
|
|
+ if (projectPointer) {
|
|
|
+ try {
|
|
|
+ const pQuery = new Parse.Query('Project');
|
|
|
+ pQuery.include('contact');
|
|
|
+ pQuery.include('assignee');
|
|
|
+ pQuery.include('department');
|
|
|
+ pQuery.include('department.leader');
|
|
|
+ this.project = await pQuery.get(projectPointer.id);
|
|
|
+
|
|
|
+ if (this.project) {
|
|
|
+ console.log('✅ 找到项目:', this.project.get('title'));
|
|
|
+ this.contact = this.project.get('contact');
|
|
|
+ this.projectId = this.project.id;
|
|
|
+ }
|
|
|
+ } catch (projectError) {
|
|
|
+ console.warn('⚠️ 加载项目失败:', projectError);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 群聊未关联项目');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已发送群介绍
|
|
|
+ this.introSent = this.groupChat.get('introSent') || false;
|
|
|
+
|
|
|
+ // 从企微API同步最新信息(不阻塞)
|
|
|
+ try {
|
|
|
+ await this.syncFromWxwork();
|
|
|
+ } catch (syncError) {
|
|
|
+ console.warn('⚠️ 企微同步失败:', syncError);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成入群方式(不阻塞)
|
|
|
+ try {
|
|
|
+ await this.loadJoinMethods();
|
|
|
+ } catch (joinError) {
|
|
|
+ console.warn('⚠️ 生成入群方式失败:', joinError);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.error('❌ 未找到群聊');
|
|
|
+ console.error('查询条件:', {
|
|
|
+ chatId: this.chatId,
|
|
|
+ cid: this.cid
|
|
|
+ });
|
|
|
+
|
|
|
+ // 显示错误提示
|
|
|
+ window?.fmode?.alert(`未找到群聊记录\n\n群聊ID: ${this.chatId}\n\n请使用测试工具创建群聊记录`);
|
|
|
+
|
|
|
+ // 尝试从企微API直接获取并创建(可选)
|
|
|
+ // await this.createFromWxwork();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.error('❌ 缺少 chatId 参数');
|
|
|
+ window?.fmode?.alert('缺少群聊ID参数');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 加载聊天消息
|
|
|
+ await this.loadChatMessages();
|
|
|
+
|
|
|
+ // 3. 生成介绍文案模板
|
|
|
+ this.generateIntroTemplate();
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ 加载数据失败:', error);
|
|
|
+ console.error('错误堆栈:', error.stack);
|
|
|
+
|
|
|
+ // 显示友好的错误提示
|
|
|
+ const errorMsg = `加载失败: ${error.message || error}\n\n请检查:\n1. Parse Server是否运行\n2. 群聊ID是否正确\n3. 网络连接是否正常`;
|
|
|
+ window?.fmode?.alert(errorMsg);
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ console.log('✅ 数据加载流程结束,loading:', this.loading);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从企微API同步群聊信息 - 使用 fmode-ng 标准方法
|
|
|
+ */
|
|
|
+ async syncFromWxwork() {
|
|
|
+ if (!this.wecorp || !this.groupChat) {
|
|
|
+ console.log('⏭️ 跳过企微同步:SDK未初始化或群聊不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const chatIdValue = this.groupChat.get('chat_id');
|
|
|
+ if (!chatIdValue) {
|
|
|
+ console.warn('⚠️ 群聊没有chat_id,跳过企微同步');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('🔄 从企微API同步群聊信息, chat_id:', chatIdValue);
|
|
|
+
|
|
|
+ // 设置超时保护
|
|
|
+ const timeout = new Promise<any>((_, reject) =>
|
|
|
+ setTimeout(() => reject(new Error('企微API调用超时')), 8000)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 使用 fmode-ng 的 WxworkCorp.externalContact.groupChat.get() 方法
|
|
|
+ const apiCall = this.wecorp.externalContact?.groupChat?.get(chatIdValue);
|
|
|
+
|
|
|
+ if (!apiCall) {
|
|
|
+ console.warn('⚠️ 企微API方法不可用');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const chatInfo: any = await Promise.race([apiCall, timeout]);
|
|
|
+
|
|
|
+ console.log('📥 企微API原始返回:', chatInfo);
|
|
|
+
|
|
|
+ // 处理企微API返回的数据结构
|
|
|
+ const groupChatData = chatInfo?.group_chat || chatInfo;
|
|
|
+
|
|
|
+ if (groupChatData) {
|
|
|
+ console.log('✅ 企微API返回群聊数据:', {
|
|
|
+ name: groupChatData.name,
|
|
|
+ chat_id: groupChatData.chat_id,
|
|
|
+ memberCount: groupChatData.member_list?.length
|
|
|
+ });
|
|
|
+
|
|
|
+ let needSave = false;
|
|
|
+
|
|
|
+ // 更新群聊名称
|
|
|
+ if (groupChatData.name && groupChatData.name !== this.groupChat.get('name')) {
|
|
|
+ this.groupChat.set('name', groupChatData.name);
|
|
|
+ needSave = true;
|
|
|
+ console.log('📝 更新群聊名称:', groupChatData.name);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新成员列表
|
|
|
+ if (groupChatData.member_list && Array.isArray(groupChatData.member_list)) {
|
|
|
+ this.groupChat.set('member_list', groupChatData.member_list);
|
|
|
+ if (groupChatData.member_version) {
|
|
|
+ this.groupChat.set('member_version', groupChatData.member_version);
|
|
|
+ }
|
|
|
+ needSave = true;
|
|
|
+ console.log('👥 更新成员列表,成员数:', groupChatData.member_list.length);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新群主
|
|
|
+ if (groupChatData.owner) {
|
|
|
+ this.groupChat.set('owner', groupChatData.owner);
|
|
|
+ needSave = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新群公告
|
|
|
+ if (groupChatData.notice) {
|
|
|
+ this.groupChat.set('notice', groupChatData.notice);
|
|
|
+ needSave = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存所有更新到Parse
|
|
|
+ if (needSave) {
|
|
|
+ await this.groupChat.save();
|
|
|
+ console.log('✅ 群聊信息已更新到Parse数据库');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ } else {
|
|
|
+ console.log('ℹ️ 群聊信息无变化,无需更新');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 企微API未返回有效的群聊数据');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.warn('⚠️ 企微API同步失败,使用Parse缓存数据:', error.message || error);
|
|
|
+ console.log('💾 继续使用Parse数据库中的缓存数据');
|
|
|
+ // 不抛出错误,优雅降级到Parse数据
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从企微API创建群聊记录 - 使用 fmode-ng 标准方法
|
|
|
+ */
|
|
|
+ async createFromWxwork() {
|
|
|
+ if (!this.wecorp) {
|
|
|
+ console.error('❌ 企微SDK未初始化');
|
|
|
+ window?.fmode?.alert('未找到群聊记录\n\n请在Parse数据库中创建群聊记录,或联系管理员');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('🔄 尝试从企微API获取群聊信息, chat_id:', this.chatId);
|
|
|
+
|
|
|
+ // 设置超时保护
|
|
|
+ const timeout = new Promise<any>((_, reject) =>
|
|
|
+ setTimeout(() => reject(new Error('企微API调用超时')), 10000)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 使用 fmode-ng 的标准API
|
|
|
+ const apiCall = this.wecorp.externalContact?.groupChat?.get(this.chatId);
|
|
|
+
|
|
|
+ if (!apiCall) {
|
|
|
+ throw new Error('企微API方法不可用');
|
|
|
+ }
|
|
|
+
|
|
|
+ const chatInfo: any = await Promise.race([apiCall, timeout]);
|
|
|
+
|
|
|
+ console.log('📥 企微API返回:', chatInfo);
|
|
|
+
|
|
|
+ // 处理返回的数据结构
|
|
|
+ const groupChatData = chatInfo?.group_chat || chatInfo;
|
|
|
+
|
|
|
+ if (groupChatData && (groupChatData.chat_id || groupChatData.name)) {
|
|
|
+ console.log('✅ 从企微获取到群聊信息,正在创建Parse记录...');
|
|
|
+
|
|
|
+ // 创建GroupChat记录
|
|
|
+ const GroupChat = Parse.Object.extend('GroupChat');
|
|
|
+ const newGroupChat = new GroupChat();
|
|
|
+
|
|
|
+ newGroupChat.set('chat_id', this.chatId);
|
|
|
+ newGroupChat.set('name', groupChatData.name || '未命名群聊');
|
|
|
+ newGroupChat.set('company', { __type: 'Pointer', className: 'Company', objectId: this.cid });
|
|
|
+ newGroupChat.set('member_list', groupChatData.member_list || []);
|
|
|
+
|
|
|
+ if (groupChatData.member_version) {
|
|
|
+ newGroupChat.set('member_version', groupChatData.member_version);
|
|
|
+ }
|
|
|
+ if (groupChatData.owner) {
|
|
|
+ newGroupChat.set('owner', groupChatData.owner);
|
|
|
+ }
|
|
|
+ if (groupChatData.notice) {
|
|
|
+ newGroupChat.set('notice', groupChatData.notice);
|
|
|
+ }
|
|
|
+
|
|
|
+ newGroupChat.set('data', {
|
|
|
+ createdFrom: 'chat-activation',
|
|
|
+ createdAt: new Date(),
|
|
|
+ syncFromWxwork: true
|
|
|
+ });
|
|
|
+
|
|
|
+ this.groupChat = await newGroupChat.save();
|
|
|
+ console.log('✅ 群聊记录已创建, objectId:', this.groupChat.id);
|
|
|
+
|
|
|
+ window?.fmode?.alert('已从企微同步群聊信息!');
|
|
|
+
|
|
|
+ // 生成入群方式
|
|
|
+ await this.loadJoinMethods();
|
|
|
+
|
|
|
+ // 重新加载消息
|
|
|
+ await this.loadChatMessages();
|
|
|
+
|
|
|
+ // 更新UI
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ } else {
|
|
|
+ throw new Error('企微API未返回有效的群聊信息');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ 从企微创建群聊失败:', error.message || error);
|
|
|
+ console.log('💡 提示:请使用测试脚本在Parse中创建群聊记录');
|
|
|
+
|
|
|
+ // 显示友好的错误提示
|
|
|
+ const errorMsg = `未找到群聊记录\n\n群聊ID: ${this.chatId}\n\n可能的原因:\n1. Parse数据库中没有该群聊记录\n2. 企微API权限不足(需要"客户联系"权限)\n3. 群聊ID不正确\n\n解决方法:\n请双击打开 CREATE-TEST-GROUPCHAT.html 创建测试记录`;
|
|
|
+
|
|
|
+ window?.fmode?.alert(errorMsg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载入群方式 - 使用 fmode-ng 标准方法
|
|
|
+ * 参考: yss-project/src/modules/project/components/get-group-joinway.ts
|
|
|
+ */
|
|
|
+ async loadJoinMethods() {
|
|
|
+ try {
|
|
|
+ if (!this.groupChat || !this.wecorp) {
|
|
|
+ console.warn('⚠️ 群聊对象或企微SDK未初始化');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const chatId = this.groupChat.get('chat_id');
|
|
|
+ if (!chatId) {
|
|
|
+ console.warn('⚠️ 群聊没有chat_id,无法生成入群方式');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('🔗 生成入群方式, chat_id:', chatId);
|
|
|
+
|
|
|
+ let needSave = false;
|
|
|
+
|
|
|
+ // 1. 生成二维码入群 (scene: 2)
|
|
|
+ if (!this.groupChat.get('joinQrcode')) {
|
|
|
+ try {
|
|
|
+ console.log('📱 生成二维码入群方式...');
|
|
|
+
|
|
|
+ // 调用企微API添加入群方式
|
|
|
+ const qrConfigResult = await this.wecorp.externalContact?.groupChat?.addJoinWay({
|
|
|
+ scene: 2, // 2=群二维码
|
|
|
+ chat_id_list: [chatId]
|
|
|
+ });
|
|
|
+
|
|
|
+ const config_id = qrConfigResult?.config_id;
|
|
|
+ console.log('✅ 二维码config_id:', config_id);
|
|
|
+
|
|
|
+ if (config_id) {
|
|
|
+ // 获取入群方式详情
|
|
|
+ const joinWayResult = await this.wecorp.externalContact?.groupChat?.getJoinWay(config_id);
|
|
|
+ const qrCodeUrl = joinWayResult?.join_way?.qr_code || (typeof joinWayResult?.join_way === 'string' ? joinWayResult.join_way : '');
|
|
|
+
|
|
|
+ if (qrCodeUrl) {
|
|
|
+ this.groupChat.set('joinQrcode', qrCodeUrl);
|
|
|
+ this.qrCodeUrl = qrCodeUrl;
|
|
|
+ this.joinMethods.qrCode = qrCodeUrl;
|
|
|
+ needSave = true;
|
|
|
+ console.log('✅ 二维码入群方式已生成');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.warn('⚠️ 生成二维码入群失败:', error.message || error);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const existingQrCode = this.groupChat.get('joinQrcode');
|
|
|
+ this.qrCodeUrl = typeof existingQrCode === 'string' ? existingQrCode : (existingQrCode?.qr_code || '');
|
|
|
+ this.joinMethods.qrCode = this.qrCodeUrl;
|
|
|
+ console.log('ℹ️ 使用已有的二维码入群方式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 生成链接入群 (scene: 1)
|
|
|
+ if (!this.groupChat.get('joinUrl')) {
|
|
|
+ try {
|
|
|
+ console.log('🔗 生成链接入群方式...');
|
|
|
+
|
|
|
+ // 调用企微API添加入群方式
|
|
|
+ const linkConfigResult = await this.wecorp.externalContact?.groupChat?.addJoinWay({
|
|
|
+ scene: 1, // 1=群链接
|
|
|
+ chat_id_list: [chatId]
|
|
|
+ });
|
|
|
+
|
|
|
+ const config_id = linkConfigResult?.config_id;
|
|
|
+ console.log('✅ 链接config_id:', config_id);
|
|
|
+
|
|
|
+ if (config_id) {
|
|
|
+ // 获取入群方式详情
|
|
|
+ const joinWayResult = await this.wecorp.externalContact?.groupChat?.getJoinWay(config_id);
|
|
|
+ const linkUrl = (joinWayResult?.join_way as any)?.url || (typeof joinWayResult?.join_way === 'string' ? joinWayResult.join_way : '');
|
|
|
+
|
|
|
+ if (linkUrl) {
|
|
|
+ this.groupChat.set('joinUrl', linkUrl);
|
|
|
+ this.joinLink = linkUrl;
|
|
|
+ this.joinMethods.link = linkUrl;
|
|
|
+ needSave = true;
|
|
|
+ console.log('✅ 链接入群方式已生成');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.warn('⚠️ 生成链接入群失败:', error.message || error);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const existingUrl = this.groupChat.get('joinUrl');
|
|
|
+ this.joinLink = typeof existingUrl === 'string' ? existingUrl : ((existingUrl as any)?.url || '');
|
|
|
+ this.joinMethods.link = this.joinLink;
|
|
|
+ console.log('ℹ️ 使用已有的链接入群方式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存更新到Parse
|
|
|
+ if (needSave) {
|
|
|
+ await this.groupChat.save();
|
|
|
+ console.log('✅ 入群方式已保存到Parse数据库');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新UI
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ 加载入群方式失败:', error.message || error);
|
|
|
+ console.log('💡 可能是企微API权限不足,请检查应用权限配置');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载聊天消息
|
|
|
+ */
|
|
|
+ async loadChatMessages() {
|
|
|
+ try {
|
|
|
+ this.loadingMessages = true;
|
|
|
+
|
|
|
+ if (!this.groupChat) {
|
|
|
+ this.messages = [];
|
|
|
+ this.updateStatistics();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('📥 加载聊天消息...');
|
|
|
+
|
|
|
+ // 1. 先从Parse数据库获取缓存的消息
|
|
|
+ const messagesData = this.groupChat.get('messages') || [];
|
|
|
+ const memberList = this.groupChat.get('member_list') || [];
|
|
|
+ const chatIdValue = this.groupChat.get('chat_id');
|
|
|
+
|
|
|
+ console.log('📊 Parse缓存消息数:', messagesData.length);
|
|
|
+
|
|
|
+ // 2. 尝试从企微API同步最新消息
|
|
|
+ if (this.wecorp && chatIdValue) {
|
|
|
+ try {
|
|
|
+ console.log('🔄 从企微API同步群聊信息...');
|
|
|
+
|
|
|
+ // 获取群聊详情(包含最新成员列表)
|
|
|
+ const chatInfo: any = await this.wecorp.externalContact.groupChat.get(chatIdValue);
|
|
|
+
|
|
|
+ if (chatInfo && chatInfo.group_chat) {
|
|
|
+ console.log('✅ 获取到群聊信息:', chatInfo.group_chat.name);
|
|
|
+
|
|
|
+ // 更新成员列表
|
|
|
+ if (chatInfo.group_chat.member_list) {
|
|
|
+ this.groupChat.set('member_list', chatInfo.group_chat.member_list);
|
|
|
+ this.groupChat.set('member_version', chatInfo.group_chat.member_version);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新群聊名称
|
|
|
+ if (chatInfo.group_chat.name) {
|
|
|
+ this.groupChat.set('name', chatInfo.group_chat.name);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存更新
|
|
|
+ await this.groupChat.save();
|
|
|
+ console.log('✅ 群聊信息已更新');
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (apiError) {
|
|
|
+ console.warn('⚠️ 企微API调用失败,使用缓存数据:', apiError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取客户的external_userid
|
|
|
+ const customerUserId = this.contact?.get('external_userid') || '';
|
|
|
+
|
|
|
+ // 4. 转换为ChatMessage格式
|
|
|
+ this.messages = messagesData.map((msg: any, index: number) => {
|
|
|
+ const isCustomer = msg.from === customerUserId ||
|
|
|
+ memberList.some((m: any) =>
|
|
|
+ m.type === 2 && m.userid === msg.from
|
|
|
+ );
|
|
|
+
|
|
|
+ // 判断是否需要回复(客户消息且10分钟内无回复)
|
|
|
+ const msgTime = new Date(msg.msgtime * 1000);
|
|
|
+ const needsReply = isCustomer && this.checkNeedsReply(msg, messagesData, index);
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: msg.msgid || `msg-${index}`,
|
|
|
+ senderName: this.getSenderName(msg.from, memberList),
|
|
|
+ senderUserId: msg.from,
|
|
|
+ content: this.getMessageContent(msg),
|
|
|
+ time: msgTime,
|
|
|
+ isCustomer,
|
|
|
+ needsReply,
|
|
|
+ msgType: msg.msgtype
|
|
|
+ };
|
|
|
+ }).sort((a: ChatMessage, b: ChatMessage) => b.time.getTime() - a.time.getTime());
|
|
|
+
|
|
|
+ console.log('✅ 消息加载完成,总数:', this.messages.length);
|
|
|
+
|
|
|
+ this.updateStatistics();
|
|
|
+ this.applyFilters();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 加载消息失败:', error);
|
|
|
+ } finally {
|
|
|
+ this.loadingMessages = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查消息是否需要回复
|
|
|
+ */
|
|
|
+ private checkNeedsReply(currentMsg: any, allMessages: any[], currentIndex: number): boolean {
|
|
|
+ const msgTime = new Date(currentMsg.msgtime * 1000);
|
|
|
+ const now = new Date();
|
|
|
+ const timeDiff = now.getTime() - msgTime.getTime();
|
|
|
+
|
|
|
+ // 如果消息时间超过10分钟
|
|
|
+ if (timeDiff < 10 * 60 * 1000) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查后续是否有技术人员回复
|
|
|
+ for (let i = currentIndex + 1; i < allMessages.length; i++) {
|
|
|
+ const nextMsg = allMessages[i];
|
|
|
+ const nextMsgTime = new Date(nextMsg.msgtime * 1000);
|
|
|
+
|
|
|
+ // 如果后续消息是在当前消息之后的
|
|
|
+ if (nextMsgTime > msgTime) {
|
|
|
+ // 如果是技术人员发送的消息,则认为已回复
|
|
|
+ const memberList = this.groupChat?.get('member_list') || [];
|
|
|
+ const isStaffReply = memberList.some((m: any) =>
|
|
|
+ m.type === 1 && m.userid === nextMsg.from
|
|
|
+ );
|
|
|
+
|
|
|
+ if (isStaffReply) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取发送者名称
|
|
|
+ */
|
|
|
+ private getSenderName(userId: string, memberList: any[]): string {
|
|
|
+ const member = memberList.find((m: any) => m.userid === userId);
|
|
|
+ if (member) {
|
|
|
+ return member.name || member.userid;
|
|
|
+ }
|
|
|
+ return userId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取消息内容
|
|
|
+ */
|
|
|
+ private getMessageContent(msg: any): string {
|
|
|
+ switch (msg.msgtype) {
|
|
|
+ case 'text':
|
|
|
+ return msg.text?.content || '';
|
|
|
+ case 'image':
|
|
|
+ return '[图片]';
|
|
|
+ case 'voice':
|
|
|
+ return '[语音]';
|
|
|
+ case 'video':
|
|
|
+ return '[视频]';
|
|
|
+ case 'file':
|
|
|
+ return `[文件] ${msg.file?.filename || ''}`;
|
|
|
+ case 'link':
|
|
|
+ return `[链接] ${msg.link?.title || ''}`;
|
|
|
+ default:
|
|
|
+ return '[不支持的消息类型]';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新统计数据
|
|
|
+ */
|
|
|
+ private updateStatistics() {
|
|
|
+ this.totalMessages = this.messages.length;
|
|
|
+ this.customerMessageCount = this.messages.filter(m => m.isCustomer).length;
|
|
|
+ this.unreadCount = this.messages.filter(m => m.needsReply).length;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 应用筛选
|
|
|
+ */
|
|
|
+ applyFilters() {
|
|
|
+ if (this.showOnlyCustomer) {
|
|
|
+ this.filteredMessages = this.messages.filter(m => m.isCustomer);
|
|
|
+ } else if (this.showOnlyUnread) {
|
|
|
+ this.filteredMessages = this.messages.filter(m => m.needsReply);
|
|
|
+ } else {
|
|
|
+ this.filteredMessages = [...this.messages];
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换客户消息筛选
|
|
|
+ */
|
|
|
+ toggleCustomerFilter() {
|
|
|
+ this.showOnlyCustomer = !this.showOnlyCustomer;
|
|
|
+ this.showOnlyUnread = false;
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换未回复筛选
|
|
|
+ */
|
|
|
+ toggleUnreadFilter() {
|
|
|
+ this.showOnlyUnread = !this.showOnlyUnread;
|
|
|
+ this.showOnlyCustomer = false;
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成介绍文案模板
|
|
|
+ */
|
|
|
+ generateIntroTemplate() {
|
|
|
+ if (!this.project) return;
|
|
|
+
|
|
|
+ const leader = this.project.get('department')?.get('leader');
|
|
|
+ const assignee = this.project.get('assignee');
|
|
|
+ const projectTitle = this.project.get('title') || '项目';
|
|
|
+
|
|
|
+ const leaderName = leader?.get('name') || '待定';
|
|
|
+ const techName = assignee?.get('name') || '待定';
|
|
|
+
|
|
|
+ this.introTemplate = `欢迎加入【${projectTitle}】项目群!\n\n` +
|
|
|
+ `👤 项目主管:${leaderName}\n` +
|
|
|
+ `🔧 执行技术:${techName}\n` +
|
|
|
+ `📋 项目需求:${this.project.get('description') || '详见需求文档'}\n\n` +
|
|
|
+ `我们将为您提供专业的服务,有任何问题随时沟通!`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送群介绍
|
|
|
+ */
|
|
|
+ async sendGroupIntro() {
|
|
|
+ try {
|
|
|
+ if (!this.wecorp || !this.chatId) {
|
|
|
+ window?.fmode?.alert('群聊信息不完整');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.sendingIntro = true;
|
|
|
+
|
|
|
+ // 发送文本消息到群聊
|
|
|
+ // @ts-ignore - 企微API类型定义问题
|
|
|
+ await this.wecorp.message.send({
|
|
|
+ chatid: this.chatId,
|
|
|
+ msgtype: 'text',
|
|
|
+ text: {
|
|
|
+ content: this.introTemplate
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新数据库标记
|
|
|
+ if (this.groupChat) {
|
|
|
+ this.groupChat.set('introSent', true);
|
|
|
+ this.groupChat.set('introSentAt', new Date());
|
|
|
+ await this.groupChat.save();
|
|
|
+ this.introSent = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ window?.fmode?.alert('群介绍已发送!');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送群介绍失败:', error);
|
|
|
+ window?.fmode?.alert('发送失败,请重试');
|
|
|
+ } finally {
|
|
|
+ this.sendingIntro = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成辅助回复建议(使用AI)
|
|
|
+ */
|
|
|
+ async generateSuggestedReplies(message: ChatMessage) {
|
|
|
+ try {
|
|
|
+ this.selectedMessage = message;
|
|
|
+ this.showSuggestions = true;
|
|
|
+ this.generatingAI = true;
|
|
|
+ this.suggestedReplies = [];
|
|
|
+
|
|
|
+ console.log('🤖 开始生成AI回复建议...');
|
|
|
+
|
|
|
+ // 准备项目上下文
|
|
|
+ const projectContext = this.project ? {
|
|
|
+ title: this.project.get('title'),
|
|
|
+ stage: this.project.get('stage'),
|
|
|
+ description: this.project.get('description'),
|
|
|
+ assigneeName: this.project.get('assignee')?.get('name'),
|
|
|
+ leaderName: this.project.get('department')?.get('leader')?.get('name')
|
|
|
+ } : undefined;
|
|
|
+
|
|
|
+ // 准备聊天历史(最近5条)
|
|
|
+ const chatHistory = this.messages.slice(0, 5).map(msg => ({
|
|
|
+ sender: msg.senderName,
|
|
|
+ content: msg.content,
|
|
|
+ isCustomer: msg.isCustomer
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 调用AI服务生成建议
|
|
|
+ const suggestions = await this.chatAI.generateReplySuggestions({
|
|
|
+ customerMessage: message.content,
|
|
|
+ customerName: message.senderName,
|
|
|
+ projectContext,
|
|
|
+ chatHistory,
|
|
|
+ onProgress: (progress) => {
|
|
|
+ console.log('AI生成进度:', progress);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转换为组件需要的格式
|
|
|
+ this.suggestedReplies = suggestions.map((s, index) => ({
|
|
|
+ id: `ai-${index}`,
|
|
|
+ text: s.text,
|
|
|
+ icon: s.icon
|
|
|
+ }));
|
|
|
+
|
|
|
+ console.log('✅ AI回复建议生成完成,共', this.suggestedReplies.length, '条');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 生成AI回复建议失败:', error);
|
|
|
+
|
|
|
+ // 使用默认回复作为后备
|
|
|
+ this.suggestedReplies = this.getDefaultSuggestions(message.content);
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ this.generatingAI = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取默认回复建议(后备方案)
|
|
|
+ */
|
|
|
+ private getDefaultSuggestions(content: string): SuggestedReply[] {
|
|
|
+ const suggestions: SuggestedReply[] = [];
|
|
|
+ const lowerContent = content.toLowerCase();
|
|
|
+
|
|
|
+ // 根据关键词匹配回复
|
|
|
+ if (lowerContent.includes('需求') || lowerContent.includes('要求') || lowerContent.includes('想要')) {
|
|
|
+ suggestions.push({
|
|
|
+ id: '1',
|
|
|
+ text: '您说的需求已记录,我会在1小时内反馈详细方案给您。',
|
|
|
+ icon: '📝'
|
|
|
+ });
|
|
|
+ suggestions.push({
|
|
|
+ id: '2',
|
|
|
+ text: '好的,我们会根据您的需求进行设计,稍后发送初步方案供您参考。',
|
|
|
+ icon: '✅'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lowerContent.includes('进度') || lowerContent.includes('什么时候') || lowerContent.includes('多久')) {
|
|
|
+ suggestions.push({
|
|
|
+ id: '3',
|
|
|
+ text: '目前项目进度正常,预计本周五前完成,届时会第一时间通知您。',
|
|
|
+ icon: '⏰'
|
|
|
+ });
|
|
|
+ suggestions.push({
|
|
|
+ id: '4',
|
|
|
+ text: '我们正在加紧制作中,预计2-3个工作日内完成,请您耐心等待。',
|
|
|
+ icon: '🚀'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lowerContent.includes('修改') || lowerContent.includes('调整') || lowerContent.includes('改')) {
|
|
|
+ suggestions.push({
|
|
|
+ id: '5',
|
|
|
+ text: '收到,我会马上按您的要求进行调整,调整完成后发送给您确认。',
|
|
|
+ icon: '🔧'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通用回复
|
|
|
+ if (suggestions.length < 3) {
|
|
|
+ suggestions.push({
|
|
|
+ id: '6',
|
|
|
+ text: '好的,我明白了,马上处理。',
|
|
|
+ icon: '👌'
|
|
|
+ });
|
|
|
+ suggestions.push({
|
|
|
+ id: '7',
|
|
|
+ text: '收到您的消息,我会尽快给您回复。',
|
|
|
+ icon: '✉️'
|
|
|
+ });
|
|
|
+ suggestions.push({
|
|
|
+ id: '8',
|
|
|
+ text: '感谢您的反馈,我们会认真对待并及时处理。',
|
|
|
+ icon: '🙏'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return suggestions.slice(0, 5);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送建议回复
|
|
|
+ */
|
|
|
+ async sendSuggestedReply(reply: SuggestedReply) {
|
|
|
+ try {
|
|
|
+ if (!this.wecorp || !this.chatId) {
|
|
|
+ window?.fmode?.alert('无法发送消息');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送消息到群聊
|
|
|
+ // @ts-ignore - 企微API类型定义问题
|
|
|
+ await this.wecorp.message.send({
|
|
|
+ chatid: this.chatId,
|
|
|
+ msgtype: 'text',
|
|
|
+ text: {
|
|
|
+ content: reply.text
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ window?.fmode?.alert('消息已发送!');
|
|
|
+
|
|
|
+ // 关闭建议面板
|
|
|
+ this.showSuggestions = false;
|
|
|
+ this.selectedMessage = null;
|
|
|
+
|
|
|
+ // 刷新消息列表
|
|
|
+ await this.loadChatMessages();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送消息失败:', error);
|
|
|
+ window?.fmode?.alert('发送失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动未回复检查
|
|
|
+ */
|
|
|
+ private startUnreadCheck() {
|
|
|
+ // 每分钟检查一次
|
|
|
+ this.checkTimer = setInterval(() => {
|
|
|
+ this.checkUnreadMessages();
|
|
|
+ }, 60 * 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查未回复消息并推送通知
|
|
|
+ */
|
|
|
+ private async checkUnreadMessages() {
|
|
|
+ const unreadMessages = this.messages.filter(m => m.needsReply);
|
|
|
+
|
|
|
+ for (const msg of unreadMessages) {
|
|
|
+ const timeDiff = Date.now() - msg.time.getTime();
|
|
|
+
|
|
|
+ // 10分钟未回复,发送通知
|
|
|
+ if (timeDiff >= 10 * 60 * 1000 && timeDiff < 11 * 60 * 1000) {
|
|
|
+ await this.sendUnreadNotification(msg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送未回复通知
|
|
|
+ */
|
|
|
+ private async sendUnreadNotification(message: ChatMessage) {
|
|
|
+ try {
|
|
|
+ if (!this.wecorp || !this.currentUser) return;
|
|
|
+
|
|
|
+ const groupName = this.groupChat?.get('name') || '项目群';
|
|
|
+ const notificationText = `【${groupName}】客户消息已超10分钟未回复,请及时处理!\n\n` +
|
|
|
+ `客户:${message.senderName}\n` +
|
|
|
+ `消息:${message.content}`;
|
|
|
+
|
|
|
+ // 发送通知给当前用户
|
|
|
+ const userId = this.currentUser.get('userid');
|
|
|
+ if (userId) {
|
|
|
+ // @ts-ignore - 企微API类型定义问题
|
|
|
+ await this.wecorp.message.send({
|
|
|
+ touser: userId,
|
|
|
+ agentid: '1000017', // 使用应用的agentid
|
|
|
+ msgtype: 'text',
|
|
|
+ text: {
|
|
|
+ content: notificationText
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送通知失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 复制入群链接
|
|
|
+ */
|
|
|
+ copyJoinLink() {
|
|
|
+ if (!this.joinMethods.link) {
|
|
|
+ window?.fmode?.alert('暂无入群链接');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ navigator.clipboard.writeText(this.joinMethods.link).then(() => {
|
|
|
+ window?.fmode?.alert('链接已复制!');
|
|
|
+ }).catch(() => {
|
|
|
+ window?.fmode?.alert('复制失败,请手动复制');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 打开群聊
|
|
|
+ */
|
|
|
+ async openGroupChat() {
|
|
|
+ try {
|
|
|
+ if (!this.chatId) {
|
|
|
+ window?.fmode?.alert('群聊ID不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用企微SDK打开群聊
|
|
|
+ if (this.wecorp) {
|
|
|
+ // @ts-ignore
|
|
|
+ await this.wecorp.openChat?.(this.chatId);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('打开群聊失败:', error);
|
|
|
+ window?.fmode?.alert('打开失败,请在企业微信中操作');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化时间
|
|
|
+ */
|
|
|
+ formatTime(date: Date): string {
|
|
|
+ if (!date) return '';
|
|
|
+ const now = new Date();
|
|
|
+ const diff = now.getTime() - date.getTime();
|
|
|
+ const minutes = Math.floor(diff / (1000 * 60));
|
|
|
+ const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
|
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ if (minutes < 1) return '刚刚';
|
|
|
+ if (minutes < 60) return `${minutes}分钟前`;
|
|
|
+ if (hours < 24) return `${hours}小时前`;
|
|
|
+ if (days < 7) return `${days}天前`;
|
|
|
+
|
|
|
+ return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取未回复时长
|
|
|
+ */
|
|
|
+ getUnreadDuration(time: Date): string {
|
|
|
+ const diff = Date.now() - time.getTime();
|
|
|
+ const minutes = Math.floor(diff / (1000 * 60));
|
|
|
+ const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
|
+
|
|
|
+ if (hours > 0) return `${hours}小时`;
|
|
|
+ return `${minutes}分钟`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前时间
|
|
|
+ */
|
|
|
+ getCurrentTime(): number {
|
|
|
+ return Date.now();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下拉刷新
|
|
|
+ */
|
|
|
+ async handleRefresh(event: any) {
|
|
|
+ try {
|
|
|
+ console.log('🔄 开始刷新数据...');
|
|
|
+ this.refreshing = true;
|
|
|
+
|
|
|
+ // 重新加载所有数据
|
|
|
+ await this.loadData();
|
|
|
+
|
|
|
+ console.log('✅ 刷新完成');
|
|
|
+
|
|
|
+ // 完成刷新动画
|
|
|
+ if (event && event.target) {
|
|
|
+ event.target.complete();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 刷新失败:', error);
|
|
|
+
|
|
|
+ if (event && event.target) {
|
|
|
+ event.target.complete();
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.refreshing = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动刷新消息
|
|
|
+ */
|
|
|
+ async refreshMessages() {
|
|
|
+ try {
|
|
|
+ console.log('🔄 手动刷新消息...');
|
|
|
+ this.loadingMessages = true;
|
|
|
+
|
|
|
+ await this.loadChatMessages();
|
|
|
+
|
|
|
+ window?.fmode?.alert('刷新成功!');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 刷新消息失败:', error);
|
|
|
+ window?.fmode?.alert('刷新失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取发送者头像
|
|
|
+ */
|
|
|
+ getSenderAvatar(userId: string): string {
|
|
|
+ if (!this.groupChat) return '/assets/images/default-avatar.svg';
|
|
|
+
|
|
|
+ const memberList = this.groupChat.get('member_list') || [];
|
|
|
+ const member = memberList.find((m: any) => m.userid === userId);
|
|
|
+
|
|
|
+ if (member && member.avatar) {
|
|
|
+ return member.avatar;
|
|
|
+ }
|
|
|
+
|
|
|
+ return '/assets/images/default-avatar.svg';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换二维码显示
|
|
|
+ */
|
|
|
+ toggleQRCode() {
|
|
|
+ this.showQRCode = !this.showQRCode;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 返回
|
|
|
+ */
|
|
|
+ goBack() {
|
|
|
+ if (this.projectId) {
|
|
|
+ this.router.navigate(['/wxwork', this.cid, 'project', this.projectId]);
|
|
|
+ } else {
|
|
|
+ this.router.navigate(['/wxwork', this.cid]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|