group-chat-summary.component.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { WxworkCorp } from 'fmode-ng/core';
  4. import { FmodeObject } from 'fmode-ng/parse';
  5. // 群聊消息
  6. interface ChatMessage {
  7. id: string;
  8. sender: string;
  9. senderName: string;
  10. content: string;
  11. time: Date;
  12. isCustomer: boolean;
  13. needsReply: boolean; // 是否需要回复
  14. replyTime?: Date; // 回复时间
  15. }
  16. /**
  17. * 群聊信息汇总组件
  18. *
  19. * 功能:
  20. * 1. 展示群聊中客户的历史消息
  21. * 2. 一键筛选客户消息
  22. * 3. 未回复消息提示(超过10分钟)
  23. * 4. 群聊介绍文案自动发送
  24. */
  25. @Component({
  26. selector: 'app-group-chat-summary',
  27. standalone: true,
  28. imports: [CommonModule],
  29. templateUrl: './group-chat-summary.component.html',
  30. styleUrls: ['./group-chat-summary.component.scss']
  31. })
  32. export class GroupChatSummaryComponent implements OnInit, OnChanges {
  33. @Input() groupChat: FmodeObject | null = null;
  34. @Input() contact: FmodeObject | null = null;
  35. @Input() cid: string = '';
  36. // 群聊消息
  37. messages: ChatMessage[] = [];
  38. customerMessages: ChatMessage[] = []; // 客户消息
  39. unreadMessages: ChatMessage[] = []; // 未回复消息
  40. // UI状态
  41. loading: boolean = false;
  42. showOnlyCustomer: boolean = false; // 只看客户消息
  43. showOnlyUnread: boolean = false; // 只看未回复
  44. collapsed: boolean = true; // 折叠状态
  45. // 统计
  46. totalMessages: number = 0;
  47. customerMessageCount: number = 0;
  48. unreadCount: number = 0;
  49. // 群聊介绍
  50. groupIntro: string = '';
  51. introSent: boolean = false;
  52. // 企微SDK
  53. wecorp: WxworkCorp | null = null;
  54. constructor() {}
  55. ngOnInit() {
  56. this.initWxwork();
  57. this.loadGroupIntro();
  58. }
  59. ngOnChanges(changes: SimpleChanges) {
  60. if (changes['groupChat'] && this.groupChat) {
  61. this.loadChatMessages();
  62. }
  63. }
  64. /**
  65. * 初始化企微SDK
  66. */
  67. async initWxwork() {
  68. if (this.cid) {
  69. try {
  70. // @ts-ignore
  71. this.wecorp = new WxworkCorp(this.cid);
  72. } catch (err) {
  73. console.error('初始化企微SDK失败:', err);
  74. }
  75. }
  76. }
  77. /**
  78. * 加载群介绍文案
  79. */
  80. loadGroupIntro() {
  81. // 默认群介绍文案
  82. this.groupIntro = `
  83. 欢迎加入映三色设计服务群!👋
  84. 我是您的专属设计顾问,很高兴为您服务。
  85. 📋 服务流程:
  86. 1️⃣ 需求沟通 - 了解您的设计需求
  87. 2️⃣ 方案设计 - 提供专业设计方案
  88. 3️⃣ 方案优化 - 根据您的反馈调整
  89. 4️⃣ 交付执行 - 完成设计并交付
  90. ⏰ 服务时间:工作日 9:00-18:00
  91. 📞 紧急联系:请直接拨打客服电话
  92. 有任何问题随时在群里@我,我会及时回复您!💙
  93. `.trim();
  94. // 检查是否已发送
  95. this.checkIntroSent();
  96. }
  97. /**
  98. * 检查群介绍是否已发送
  99. */
  100. async checkIntroSent() {
  101. if (!this.groupChat) return;
  102. const data = this.groupChat.get('data') || {};
  103. this.introSent = data.introSent || false;
  104. }
  105. /**
  106. * 发送群介绍
  107. */
  108. async sendGroupIntro() {
  109. if (!this.groupChat || !this.wecorp) {
  110. window?.fmode?.alert('群聊信息不完整,无法发送');
  111. return;
  112. }
  113. try {
  114. const chatId = this.groupChat.get('chat_id');
  115. if (!chatId) {
  116. window?.fmode?.alert('群聊ID不存在');
  117. return;
  118. }
  119. // 调用企微API发送消息
  120. await this.wecorp.appchat.send({
  121. chatid: chatId,
  122. msgtype: 'text',
  123. text: {
  124. content: this.groupIntro
  125. }
  126. });
  127. // 标记已发送
  128. const data = this.groupChat.get('data') || {};
  129. data.introSent = true;
  130. data.introSentAt = new Date();
  131. this.groupChat.set('data', data);
  132. await this.groupChat.save();
  133. this.introSent = true;
  134. window?.fmode?.alert('群介绍已发送!');
  135. } catch (err: any) {
  136. console.error('发送群介绍失败:', err);
  137. window?.fmode?.alert('发送失败: ' + (err.message || '未知错误'));
  138. }
  139. }
  140. /**
  141. * 加载群聊消息
  142. */
  143. async loadChatMessages() {
  144. if (!this.groupChat) return;
  145. this.loading = true;
  146. try {
  147. // 从 groupChat.data 获取聊天记录
  148. const data = this.groupChat.get('data') || {};
  149. const chatHistory = data.chatHistory || [];
  150. // 转换为消息对象
  151. this.messages = chatHistory.map((msg: any) => ({
  152. id: msg.msgid || Math.random().toString(36).substr(2, 9),
  153. sender: msg.from,
  154. senderName: msg.fromName || msg.from,
  155. content: this.extractMessageContent(msg),
  156. time: new Date(msg.msgtime * 1000),
  157. isCustomer: this.isCustomerMessage(msg.from),
  158. needsReply: this.checkNeedsReply(msg),
  159. replyTime: msg.replyTime ? new Date(msg.replyTime) : undefined
  160. })).filter((msg: ChatMessage) => msg.content); // 过滤空消息
  161. // 统计
  162. this.totalMessages = this.messages.length;
  163. this.customerMessages = this.messages.filter(m => m.isCustomer);
  164. this.customerMessageCount = this.customerMessages.length;
  165. // 检查未回复消息
  166. this.checkUnreadMessages();
  167. console.log(`✅ 加载了 ${this.totalMessages} 条消息,客户消息 ${this.customerMessageCount} 条`);
  168. } catch (err) {
  169. console.error('加载群聊消息失败:', err);
  170. } finally {
  171. this.loading = false;
  172. }
  173. }
  174. /**
  175. * 提取消息内容
  176. */
  177. extractMessageContent(msg: any): string {
  178. if (msg.msgtype === 'text' && msg.text) {
  179. return msg.text.content || '';
  180. } else if (msg.msgtype === 'image') {
  181. return '[图片]';
  182. } else if (msg.msgtype === 'file') {
  183. return '[文件]';
  184. } else if (msg.msgtype === 'voice') {
  185. return '[语音]';
  186. } else if (msg.msgtype === 'video') {
  187. return '[视频]';
  188. }
  189. return '';
  190. }
  191. /**
  192. * 判断是否为客户消息
  193. */
  194. isCustomerMessage(sender: string): boolean {
  195. // 企微外部联系人ID通常以 wm 或 wo 开头
  196. return sender.startsWith('wm') || sender.startsWith('wo');
  197. }
  198. /**
  199. * 检查是否需要回复
  200. */
  201. checkNeedsReply(msg: any): boolean {
  202. // 如果是客户消息且没有回复时间
  203. if (this.isCustomerMessage(msg.from) && !msg.replyTime) {
  204. const msgTime = new Date(msg.msgtime * 1000);
  205. const now = new Date();
  206. const diff = now.getTime() - msgTime.getTime();
  207. // 超过10分钟未回复
  208. return diff > 10 * 60 * 1000;
  209. }
  210. return false;
  211. }
  212. /**
  213. * 检查未回复消息
  214. */
  215. checkUnreadMessages() {
  216. this.unreadMessages = this.customerMessages.filter(msg => {
  217. const now = new Date();
  218. const diff = now.getTime() - msg.time.getTime();
  219. return diff > 10 * 60 * 1000 && !msg.replyTime;
  220. });
  221. this.unreadCount = this.unreadMessages.length;
  222. // 如果有超过10分钟未回复的消息,发送通知
  223. if (this.unreadCount > 0) {
  224. this.sendUnreadNotification();
  225. }
  226. }
  227. /**
  228. * 发送未回复通知
  229. */
  230. async sendUnreadNotification() {
  231. // 这里可以调用企微API发送应用消息通知
  232. console.log(`⚠️ 有 ${this.unreadCount} 条消息超过10分钟未回复`);
  233. // TODO: 实现企微应用消息推送
  234. // 需要后端配合实现推送到技术人员手机
  235. }
  236. /**
  237. * 切换折叠状态
  238. */
  239. toggleCollapse() {
  240. this.collapsed = !this.collapsed;
  241. }
  242. /**
  243. * 只看客户消息
  244. */
  245. toggleCustomerFilter() {
  246. this.showOnlyCustomer = !this.showOnlyCustomer;
  247. this.showOnlyUnread = false;
  248. }
  249. /**
  250. * 只看未回复
  251. */
  252. toggleUnreadFilter() {
  253. this.showOnlyUnread = !this.showOnlyUnread;
  254. this.showOnlyCustomer = false;
  255. }
  256. /**
  257. * 获取显示的消息列表
  258. */
  259. getDisplayMessages(): ChatMessage[] {
  260. if (this.showOnlyUnread) {
  261. return this.unreadMessages;
  262. } else if (this.showOnlyCustomer) {
  263. return this.customerMessages;
  264. }
  265. return this.messages;
  266. }
  267. /**
  268. * 打开企微群聊
  269. */
  270. async openGroupChat() {
  271. if (!this.groupChat) return;
  272. try {
  273. const chatId = this.groupChat.get('chat_id');
  274. if (chatId && this.wecorp) {
  275. // 使用企微SDK打开群聊
  276. // @ts-ignore - WxworkCorp API
  277. await this.wecorp.openChat?.(chatId);
  278. } else {
  279. window?.fmode?.alert('群聊ID不存在');
  280. }
  281. } catch (err) {
  282. console.error('打开群聊失败:', err);
  283. window?.fmode?.alert('打开群聊失败');
  284. }
  285. }
  286. /**
  287. * 格式化时间
  288. */
  289. formatTime(date: Date): string {
  290. if (!date) return '';
  291. const now = new Date();
  292. const diff = now.getTime() - date.getTime();
  293. // 小于1分钟
  294. if (diff < 60 * 1000) {
  295. return '刚刚';
  296. }
  297. // 小于1小时
  298. if (diff < 60 * 60 * 1000) {
  299. const minutes = Math.floor(diff / (60 * 1000));
  300. return `${minutes}分钟前`;
  301. }
  302. // 小于24小时
  303. if (diff < 24 * 60 * 60 * 1000) {
  304. const hours = Math.floor(diff / (60 * 60 * 1000));
  305. return `${hours}小时前`;
  306. }
  307. // 超过24小时
  308. const month = date.getMonth() + 1;
  309. const day = date.getDate();
  310. const hour = date.getHours();
  311. const minute = date.getMinutes();
  312. return `${month}/${day} ${hour}:${minute.toString().padStart(2, '0')}`;
  313. }
  314. /**
  315. * 获取未回复时长
  316. */
  317. getUnreadDuration(date: Date): string {
  318. const now = new Date();
  319. const diff = now.getTime() - date.getTime();
  320. const minutes = Math.floor(diff / (60 * 1000));
  321. if (minutes < 60) {
  322. return `${minutes}分钟`;
  323. } else {
  324. const hours = Math.floor(minutes / 60);
  325. return `${hours}小时`;
  326. }
  327. }
  328. /**
  329. * 获取当前时间戳
  330. */
  331. getCurrentTime(): number {
  332. return Date.now();
  333. }
  334. }