chat-aggregate.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>数字员工 - 聚合聊天</title>
  7. <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  8. <style>
  9. *{margin:0;padding:0;box-sizing:border-box}
  10. body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a1a;color:#e0e0e0;height:100vh;overflow:hidden}
  11. .main{display:flex;height:100vh}
  12. /* 左侧账户列表 */
  13. .account-panel{width:80px;background:#0d0d1f;border-right:1px solid #1a1a3e;display:flex;flex-direction:column;padding:12px 0}
  14. .account-item{width:56px;height:56px;border-radius:50%;margin:0 auto 8px;cursor:pointer;position:relative;border:2px solid transparent;transition:.2s;display:flex;align-items:center;justify-content:center;font-size:24px}
  15. .account-item:hover{border-color:#e94560;transform:scale(1.05)}
  16. .account-item.active{border-color:#e94560;background:#e9456022}
  17. .account-item .badge{position:absolute;top:-2px;right:-2px;background:#e94560;color:#fff;border-radius:10px;padding:1px 5px;font-size:10px}
  18. .account-item.disabled{opacity:.4;cursor:not-allowed}
  19. /* 会话列表 */
  20. .session-panel{width:280px;background:#1a1a2e;border-right:1px solid #2a2a4a;display:flex;flex-direction:column}
  21. .session-header{padding:12px 16px;border-bottom:1px solid #2a2a4a}
  22. .session-header h3{font-size:14px;color:#e94560}
  23. .session-header .search{margin-top:8px}
  24. .session-header input{width:100%;padding:6px 10px;background:#0a0a1a;border:1px solid #2a2a4a;border-radius:4px;color:#e0e0e0;font-size:12px}
  25. .session-list{flex:1;overflow-y:auto;padding:8px}
  26. .session-item{display:flex;padding:10px;cursor:pointer;border-radius:6px;margin-bottom:4px;transition:.2s}
  27. .session-item:hover{background:#2a2a4a}
  28. .session-item.active{background:#0f3460}
  29. .session-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#e94560,#f59e0b);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
  30. .session-info{flex:1;min-width:0;margin-left:10px}
  31. .session-info .name{font-size:13px;color:#e0e0e0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  32. .session-info .msg{font-size:12px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px}
  33. .session-time{font-size:11px;color:#666;text-align:right}
  34. .session-item.unread .name{font-weight:600;color:#fff}
  35. .session-item.unread .badge{background:#e94560;color:#fff;border-radius:10px;padding:1px 5px;font-size:10px}
  36. /* 聊天区域 */
  37. .chat-panel{flex:1;display:flex;flex-direction:column;background:#0a0a1a}
  38. .chat-header{padding:12px 20px;background:#1a1a2e;border-bottom:1px solid #2a2a4a;display:flex;align-items:center;justify-content:space-between}
  39. .chat-title .name{font-size:15px;color:#e0e0e0}
  40. .chat-title .type{font-size:11px;color:#888;margin-left:8px}
  41. .chat-actions{display:flex;gap:8px}
  42. .chat-actions button{padding:6px 12px;background:#0f3460;border:none;border-radius:4px;color:#e0e0e0;font-size:12px;cursor:pointer}
  43. .chat-actions button:hover{background:#1a4a80}
  44. /* 消息列表 */
  45. .message-list{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column}
  46. .message-item{display:flex;margin-bottom:12px;max-width:80%}
  47. .message-item.sent{align-self:flex-end}
  48. .message-item.sent .msg-bubble{background:#e94560;color:#fff;border-radius:12px 12px 0 12px}
  49. .message-item.received{align-self:flex-start}
  50. .message-item.received .msg-bubble{background:#1a1a2e;color:#e0e0e0;border-radius:12px 12px 12px 0;border:1px solid #2a2a4a}
  51. .msg-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#22c55e);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0}
  52. .message-item.sent .msg-avatar{order:2;margin-left:8px}
  53. .message-item.received .msg-avatar{order:1;margin-right:8px}
  54. .msg-content{display:flex;flex-direction:column}
  55. .message-item.sent .msg-content{order:1}
  56. .message-item.received .msg-content{order:2}
  57. .msg-bubble{padding:10px 14px;max-width:max-content}
  58. .msg-text{font-size:13px;line-height:1.5}
  59. .msg-time{font-size:10px;color:#666;margin-top:4px;text-align:right}
  60. .message-item.sent .msg-time{color:#fff8}
  61. /* 输入区域 */
  62. .chat-input{padding:12px 20px;background:#1a1a2e;border-top:1px solid #2a2a4a}
  63. .input-row{display:flex;gap:10px}
  64. .input-row textarea{flex:1;padding:10px 14px;background:#0a0a1a;border:1px solid #2a2a4a;border-radius:8px;color:#e0e0e0;font-size:13px;resize:none;min-height:44px;max-height:120px;font-family:inherit}
  65. .input-row textarea:focus{outline:none;border-color:#e94560}
  66. .input-row button{padding:10px 24px;background:#e94560;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;flex-shrink:0}
  67. .input-row button:hover{background:#d63850}
  68. .input-row button:disabled{opacity:.5;cursor:not-allowed}
  69. /* 客户信息面板 */
  70. .customer-panel{width:280px;background:#1a1a2e;border-left:1px solid #2a2a4a;display:flex;flex-direction:column}
  71. .customer-header{padding:16px;border-bottom:1px solid #2a2a4a;text-align:center}
  72. .customer-avatar{width:64px;height:64px;border-radius:50%;background:linear-gradient(135deg,#e94560,#f59e0b);display:flex;align-items:center;justify-content:center;font-size:24px;margin:0 auto}
  73. .customer-name{font-size:15px;color:#e0e0e0;margin-top:8px}
  74. .customer-id{font-size:11px;color:#666}
  75. .customer-tabs{display:flex;border-bottom:1px solid #2a2a4a}
  76. .customer-tabs .tab{flex:1;padding:8px;text-align:center;font-size:12px;color:#888;cursor:pointer;border-bottom:2px solid transparent}
  77. .customer-tabs .tab.active{border-color:#e94560;color:#e94560}
  78. .customer-detail{flex:1;overflow-y:auto;padding:12px}
  79. .detail-section{margin-bottom:16px}
  80. .detail-section h4{font-size:12px;color:#888;margin-bottom:8px}
  81. .detail-row{display:flex;justify-content:space-between;padding:4px 0;font-size:12px}
  82. .detail-row .label{color:#888}
  83. .detail-row .value{color:#e0e0e0}
  84. .tag-list{display:flex;flex-wrap:gap;gap:4px}
  85. .tag{display:inline-block;padding:3px 8px;background:#0f3460;color:#ccc;border-radius:4px;font-size:11px}
  86. .record-item{padding:8px;border-bottom:1px solid #1a1a3e}
  87. .record-item .time{font-size:11px;color:#666}
  88. .record-item .desc{font-size:12px;color:#e0e0e0;margin-top:2px}
  89. /* 渠道图标 */
  90. .channel-qw{background:linear-gradient(135deg,#1890ff,#096dd9)}
  91. .channel-wx{background:linear-gradient(135deg,#07c160,#10b981)}
  92. .channel-im{background:linear-gradient(135deg,#6366f1,#8b5cf6)}
  93. .channel-whatsapp{background:linear-gradient(135deg,#25d366,#10b981)}
  94. .channel-other{background:linear-gradient(135deg,#6b7280,#9ca3af)}
  95. /* 滚动条 */
  96. ::-webkit-scrollbar{width:6px}
  97. ::-webkit-scrollbar-track{background:#0a0a1a}
  98. ::-webkit-scrollbar-thumb{background:#2a2a4a;border-radius:3px}
  99. ::-webkit-scrollbar-thumb:hover{background:#3a3a5a}
  100. </style>
  101. </head>
  102. <body>
  103. <div id="app" class="main">
  104. <!-- 左侧账户列表 -->
  105. <div class="account-panel">
  106. <div v-for="acc in accounts" :key="acc.id"
  107. :class="['account-item', acc.active ? 'active' : '', acc.connected ? '' : 'disabled']"
  108. @click="selectAccount(acc)" :title="acc.name">
  109. <span>{{acc.icon}}</span>
  110. <span v-if="acc.unread>0" class="badge">{{acc.unread}}</span>
  111. </div>
  112. </div>
  113. <!-- 会话列表 -->
  114. <div class="session-panel">
  115. <div class="session-header">
  116. <h3>🗨️ 聊天列表</h3>
  117. <div class="search">
  118. <input v-model="searchKey" placeholder="搜索联系人..." @keyup="filterSessions">
  119. </div>
  120. </div>
  121. <div class="session-list">
  122. <div v-for="sess in filteredSessions" :key="sess.sessionId"
  123. :class="['session-item', sess.sessionId === currentSession?.sessionId ? 'active' : '', sess.unread > 0 ? 'unread' : '']"
  124. @click="selectSession(sess)">
  125. <div class="session-avatar">{{sess.avatar||'?'}}</div>
  126. <div class="session-info">
  127. <div class="name">{{sess.name}}</div>
  128. <div class="msg">{{sess.lastMsg||'暂无消息'}}</div>
  129. </div>
  130. <div style="text-align:right;margin-left:8px">
  131. <div class="session-time">{{formatLobsterDateTime(sess.lastTime)}}</div>
  132. <span v-if="sess.unread>0" class="badge">{{sess.unread}}</span>
  133. </div>
  134. </div>
  135. <div v-if="filteredSessions.length===0" style="text-align:center;padding:40px;color:#666;font-size:13px">
  136. 暂无会话记录
  137. </div>
  138. </div>
  139. </div>
  140. <!-- 聊天区域 -->
  141. <div class="chat-panel">
  142. <div v-if="currentSession" class="chat-header">
  143. <div class="chat-title">
  144. <span class="name">{{currentSession.name}}</span>
  145. <span class="type">{{channelName(currentSession.channelType)}}</span>
  146. </div>
  147. <div class="chat-actions">
  148. <button @click="toggleControlMode">{{currentSession.controlMode === 'ai' ? '🤖 AI接管中' : '👤 人工接管'}}</button>
  149. <button @click="showCustomerInfo=true">👤 客户信息</button>
  150. </div>
  151. </div>
  152. <div v-else class="chat-header">
  153. <div class="chat-title">
  154. <span class="name">请选择一个会话</span>
  155. </div>
  156. </div>
  157. <div class="message-list" ref="messageList">
  158. <div v-for="(msg, idx) in messages" :key="idx" :class="['message-item', msg.sendType === 1 ? 'received' : 'sent']">
  159. <div class="msg-avatar">{{msg.sendType === 1 ? '👤' : '🤖'}}</div>
  160. <div class="msg-content">
  161. <div class="msg-bubble">
  162. <div class="msg-text">{{msg.content}}</div>
  163. </div>
  164. <div class="msg-time">{{formatLobsterDateTime(msg.time)}}</div>
  165. </div>
  166. </div>
  167. <div v-if="messages.length===0" style="text-align:center;padding:60px;color:#666">
  168. <div style="font-size:48px;margin-bottom:12px">💬</div>
  169. <p>开始与客户聊天</p>
  170. </div>
  171. </div>
  172. <div class="chat-input">
  173. <div class="input-row">
  174. <textarea v-model="inputMsg" placeholder="输入消息..." @keyup.enter="sendMessage"></textarea>
  175. <button @click="sendMessage" :disabled="!inputMsg.trim()">发送</button>
  176. </div>
  177. </div>
  178. </div>
  179. <!-- 客户信息面板 -->
  180. <div class="customer-panel" v-if="showCustomerInfo">
  181. <div class="customer-header">
  182. <div class="customer-avatar">{{currentSession?.avatar||'?'}}</div>
  183. <div class="customer-name">{{currentSession?.name||'-'}}</div>
  184. <div class="customer-id">{{currentSession?.channelSourceId||'-'}}</div>
  185. </div>
  186. <div class="customer-tabs">
  187. <div :class="['tab', customerTab==='basic'?'active':'']" @click="customerTab='basic'">基本信息</div>
  188. <div :class="['tab', customerTab==='tags'?'active':'']" @click="customerTab='tags'">标签</div>
  189. <div :class="['tab', customerTab==='records'?'active':'']" @click="customerTab='records'">访问记录</div>
  190. </div>
  191. <div class="customer-detail">
  192. <div v-if="customerTab==='basic'" class="detail-section">
  193. <h4>📋 基本信息</h4>
  194. <div class="detail-row"><span class="label">渠道</span><span class="value">{{channelName(currentSession?.channelType)}}</span></div>
  195. <div class="detail-row"><span class="label">来源ID</span><span class="value">{{currentSession?.channelSourceId||'-'}}</span></div>
  196. <div class="detail-row"><span class="label">联系人ID</span><span class="value">{{currentSession?.contactId||'-'}}</span></div>
  197. <div class="detail-row"><span class="label">会话ID</span><span class="value">{{currentSession?.sessionId||'-'}}</span></div>
  198. <div class="detail-row"><span class="label">创建时间</span><span class="value">{{formatLobsterDateTime(currentSession?.createTime)}}</span></div>
  199. </div>
  200. <div v-if="customerTab==='tags'" class="detail-section">
  201. <h4>🏷️ 客户标签</h4>
  202. <div class="tag-list">
  203. <span v-for="tag in customerTags" :key="tag" class="tag">{{tag}}</span>
  204. </div>
  205. <div v-if="customerTags.length===0" style="color:#666;font-size:12px">暂无标签</div>
  206. </div>
  207. <div v-if="customerTab==='records'" class="detail-section">
  208. <h4>📊 访问记录</h4>
  209. <div v-for="record in visitRecords" :key="record.time" class="record-item">
  210. <div class="time">{{formatLobsterDateTime(record.time)}}</div>
  211. <div class="desc">{{record.desc}}</div>
  212. </div>
  213. <div v-if="visitRecords.length===0" style="color:#666;font-size:12px">暂无访问记录</div>
  214. </div>
  215. </div>
  216. </div>
  217. </div>
  218. <script>
  219. const {createApp, ref, computed, watch, nextTick, onMounted} = Vue;
  220. createApp({
  221. setup(){
  222. const searchKey = ref('');
  223. const inputMsg = ref('');
  224. const showCustomerInfo = ref(true);
  225. const customerTab = ref('basic');
  226. const messageList = ref(null);
  227. const loading = ref(false);
  228. // ====== API配置 ======
  229. // 从URL参数获取配置(iframe嵌入时由父窗口传递)
  230. const getUrlParam = (name) => {
  231. const params = new URLSearchParams(window.location.search);
  232. return params.get(name) || '';
  233. };
  234. // 获取API基路径:URL参数 > 父窗口webpack环境变量 > 默认值
  235. const getBaseApi = () => {
  236. const fromUrl = getUrlParam('baseApi');
  237. if (fromUrl) return fromUrl;
  238. try {
  239. if (window.parent && window.parent.process && window.parent.process.env) {
  240. return window.parent.process.env.VUE_APP_BASE_API || '/dev-api';
  241. }
  242. } catch(e) {}
  243. return '/dev-api';
  244. };
  245. const BASE_API = getBaseApi();
  246. // 获取前端类型:URL参数 > 默认值
  247. const getFrontendType = () => {
  248. return getUrlParam('frontendType') || 'company';
  249. };
  250. const FRONTEND_TYPE = getFrontendType();
  251. // 从Cookie获取Token
  252. const getToken = () => {
  253. const match = document.cookie.match(/(?:^|;\s*)Web-Token=([^;]*)/);
  254. return match ? match[1] : null;
  255. };
  256. // 获取租户编码:URL参数 > localStorage
  257. const getTenantCode = () => {
  258. const fromUrl = getUrlParam('tenantCode');
  259. if (fromUrl) return fromUrl;
  260. try {
  261. return localStorage.getItem('tenantCode') || '';
  262. } catch(e) { return ''; }
  263. };
  264. // 认证请求工具
  265. const request = async (url, options = {}) => {
  266. const token = getToken();
  267. const tenantCode = getTenantCode();
  268. const headers = {
  269. 'Content-Type': 'application/json',
  270. 'X-Frontend-Type': FRONTEND_TYPE,
  271. };
  272. if (token) {
  273. headers['Authorization'] = 'Bearer ' + token;
  274. }
  275. if (tenantCode) {
  276. headers['tenant-code'] = tenantCode;
  277. }
  278. // 合并自定义headers
  279. if (options.headers) {
  280. Object.assign(headers, options.headers);
  281. }
  282. const fullUrl = url.startsWith('http') ? url : (BASE_API + url);
  283. const response = await fetch(fullUrl, {
  284. ...options,
  285. headers,
  286. credentials: 'include',
  287. });
  288. if (!response.ok) {
  289. const text = await response.text().catch(() => '');
  290. throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
  291. }
  292. const data = await response.json();
  293. if (data.code === 401) {
  294. // Token过期,通知父窗口刷新
  295. if (window.parent && window.parent.location) {
  296. window.parent.location.reload();
  297. }
  298. throw new Error('登录已过期');
  299. }
  300. return data;
  301. };
  302. const formatLobsterDateTime = (time) => {
  303. if (time === null || time === undefined || time === '') return '—';
  304. const date = time instanceof Date ? time : new Date(time);
  305. if (isNaN(date.getTime())) return '—';
  306. const y = date.getFullYear();
  307. const m = String(date.getMonth() + 1).padStart(2, '0');
  308. const d = String(date.getDate()).padStart(2, '0');
  309. const h = String(date.getHours()).padStart(2, '0');
  310. const mi = String(date.getMinutes()).padStart(2, '0');
  311. const s = String(date.getSeconds()).padStart(2, '0');
  312. return `${y}-${m}-${d} ${h}:${mi}:${s}`;
  313. };
  314. // 账户列表
  315. const accounts = ref([]);
  316. // 会话列表
  317. const sessions = ref([]);
  318. // 当前会话
  319. const currentSession = ref(null);
  320. // 消息列表
  321. const messages = ref([]);
  322. // 客户标签
  323. const customerTags = ref([]);
  324. // 访问记录
  325. const visitRecords = ref([]);
  326. // 当前登录用户的企微账户
  327. const currentAccount = ref(null);
  328. // 过滤后的会话
  329. const filteredSessions = computed(()=>{
  330. if(!searchKey.value) return sessions.value;
  331. const key = searchKey.value.toLowerCase();
  332. return sessions.value.filter(s=>{
  333. const name = s.nickName || s.name || '';
  334. return name.toLowerCase().includes(key);
  335. });
  336. });
  337. // 加载账户列表
  338. const loadAccounts = async () => {
  339. try {
  340. // 先获取当前登录用户绑定的企微账户
  341. const res = await request('/qw/user/getMyQwUserList');
  342. if (res.code === 200 || res.code === 0) {
  343. const qwAccounts = (res.data || []).map((acc, idx) => ({
  344. id: acc.id || `qw_${idx}`,
  345. name: acc.qwUserName || '企微账户',
  346. icon: '💼',
  347. active: idx === 0,
  348. connected: true,
  349. unread: 0,
  350. type: 'QW',
  351. corpId: acc.corpId,
  352. qwUserId: acc.qwUserId
  353. }));
  354. // 添加个微账户占位(后续扩展)
  355. const wxAccounts = []; // 暂时为空,后续实现个微账户绑定
  356. accounts.value = [...qwAccounts, ...wxAccounts];
  357. if (accounts.value.length > 0) {
  358. currentAccount.value = accounts.value[0];
  359. }
  360. }
  361. } catch (error) {
  362. console.error('加载账户失败:', error);
  363. // 降级显示模拟账户
  364. accounts.value = [
  365. {id:'qw', name:'企业微信', icon:'💼', active:true, connected:true, unread:0, type:'QW'},
  366. {id:'wx', name:'个人微信', icon:'💬', active:false, connected:false, unread:0, type:'WX'},
  367. ];
  368. currentAccount.value = accounts.value[0];
  369. }
  370. };
  371. // 加载会话列表
  372. const loadSessions = async () => {
  373. try {
  374. // 先尝试加载chat会话
  375. const chatRes = await request('/chat/chatSession/list?pageNum=1&pageSize=100');
  376. if (chatRes.code === 200 || chatRes.rows) {
  377. const chatSessions = (chatRes.rows || []).map(s => ({
  378. sessionId: s.sessionId,
  379. name: s.nickName || s.userName || '客户',
  380. avatar: (s.nickName || s.userName || '客').charAt(0),
  381. channelType: 'CHAT',
  382. channelSourceId: s.userId,
  383. contactId: s.userId,
  384. lastMsg: '',
  385. lastTime: s.createTime || '',
  386. unread: 0,
  387. createTime: s.createTime,
  388. status: s.status
  389. }));
  390. // 再尝试加载企微外部联系人作为会话
  391. const qwRes = await request('/qw/externalContact/list?pageNum=1&pageSize=100');
  392. if (qwRes.code === 200 || qwRes.rows) {
  393. const qwSessions = (qwRes.rows || []).map(c => ({
  394. sessionId: `qw_${c.id}`,
  395. name: c.name || c.remark || '企微客户',
  396. avatar: (c.name || c.remark || '客').charAt(0),
  397. channelType: 'QW',
  398. channelSourceId: c.externalUserId,
  399. contactId: c.id,
  400. lastMsg: '',
  401. lastTime: c.createTime || '',
  402. unread: 0,
  403. createTime: c.createTime,
  404. tagIds: c.tagIds,
  405. customerId: c.customerId,
  406. remarkMobiles: c.remarkMobiles
  407. }));
  408. sessions.value = [...qwSessions, ...chatSessions];
  409. } else {
  410. sessions.value = chatSessions;
  411. }
  412. }
  413. } catch (error) {
  414. console.error('加载会话失败:', error);
  415. sessions.value = [];
  416. }
  417. if (sessions.value.length > 0 && !currentSession.value) {
  418. selectSession(sessions.value[0]);
  419. }
  420. };
  421. // 选择账户
  422. const selectAccount = (acc)=>{
  423. if(!acc.connected) return;
  424. accounts.value.forEach(a=>a.active = false);
  425. acc.active = true;
  426. currentAccount.value = acc;
  427. loadSessions();
  428. };
  429. // 选择会话
  430. const selectSession = async (sess)=>{
  431. currentSession.value = sess;
  432. // 清除未读
  433. sess.unread = 0;
  434. // 加载消息
  435. await loadMessages(sess);
  436. // 加载客户标签和信息
  437. await loadCustomerInfo(sess);
  438. };
  439. // 加载消息
  440. const loadMessages = async (session) => {
  441. messages.value = [];
  442. try {
  443. if (session.channelType === 'CHAT') {
  444. // 加载chat会话消息
  445. const chatDetailRes = await request(`/chat/chatSession/${session.sessionId}`);
  446. if (chatDetailRes.code === 200 || chatDetailRes.data) {
  447. const msgRes = await request(`/chat/chatMsg/list?sessionId=${session.sessionId}&pageNum=1&pageSize=100`);
  448. if (msgRes.rows) {
  449. messages.value = msgRes.rows.map(m => ({
  450. content: m.content,
  451. sendType: m.sendType,
  452. time: m.createTime || new Date()
  453. }));
  454. }
  455. }
  456. } else if (session.channelType === 'QW') {
  457. // TODO: 加载企微聊天记录
  458. messages.value = [];
  459. }
  460. } catch (error) {
  461. console.error('加载消息失败:', error);
  462. messages.value = [];
  463. }
  464. nextTick(()=>{
  465. if(messageList.value){
  466. messageList.value.scrollTop = messageList.value.scrollHeight;
  467. }
  468. });
  469. };
  470. // 加载客户信息
  471. const loadCustomerInfo = async (session) => {
  472. customerTags.value = [];
  473. visitRecords.value = [];
  474. try {
  475. if (session.tagIds && session.tagIds !== '[]') {
  476. // 解析标签ID并加载标签名称
  477. const tagIds = JSON.parse(session.tagIds);
  478. if (tagIds.length > 0) {
  479. const tagRes = await request(`/qw/tag/list?tagIds=${tagIds.join(',')}`);
  480. if (tagRes.rows) {
  481. customerTags.value = tagRes.rows.map(t => t.tagName || t.name);
  482. }
  483. }
  484. }
  485. // 加载CRM客户信息
  486. if (session.customerId) {
  487. const crmRes = await request(`/crm/customer/${session.customerId}`);
  488. if (crmRes.data) {
  489. // 补充客户基本信息
  490. if (crmRes.data.phone) {
  491. visitRecords.value.push({
  492. time: new Date(),
  493. desc: `手机号: ${crmRes.data.phone}`
  494. });
  495. }
  496. if (crmRes.data.email) {
  497. visitRecords.value.push({
  498. time: new Date(),
  499. desc: `邮箱: ${crmRes.data.email}`
  500. });
  501. }
  502. }
  503. }
  504. // 加载fastGpt聊天摘要
  505. try {
  506. const chatRes = await request('/fastGpt/fastGptChatSession/list?pageNum=1&pageSize=10');
  507. if (chatRes.rows) {
  508. const relatedChat = chatRes.rows.find(c =>
  509. c.externalUserId === session.channelSourceId ||
  510. c.userId === session.contactId
  511. );
  512. if (relatedChat) {
  513. visitRecords.value.push({
  514. time: relatedChat.createTime || new Date(),
  515. desc: `最近聊天: ${relatedChat.lastMsg || '暂无消息'}`
  516. });
  517. }
  518. }
  519. } catch (err) {
  520. console.error('加载聊天摘要失败:', err);
  521. }
  522. } catch (error) {
  523. console.error('加载客户信息失败:', error);
  524. }
  525. };
  526. // 发送消息
  527. const sendMessage = async ()=>{
  528. if(!inputMsg.value.trim() || !currentSession.value) return;
  529. const msg = {
  530. content: inputMsg.value,
  531. sendType: 2,
  532. time: new Date()
  533. };
  534. messages.value.push(msg);
  535. const contentToSend = inputMsg.value;
  536. inputMsg.value = '';
  537. nextTick(()=>{
  538. if(messageList.value){
  539. messageList.value.scrollTop = messageList.value.scrollHeight;
  540. }
  541. });
  542. // 调用发送消息接口
  543. try {
  544. if (currentSession.value.channelType === 'QW') {
  545. // 企微发送消息
  546. await request('/qw/msg/send', {
  547. method: 'POST',
  548. body: JSON.stringify({
  549. externalUserId: currentSession.value.channelSourceId,
  550. content: contentToSend,
  551. qwUserId: currentAccount.value?.qwUserId
  552. })
  553. });
  554. } else if (currentSession.value.channelType === 'CHAT') {
  555. // Chat会话发送消息
  556. await request('/chat/chatMsg', {
  557. method: 'POST',
  558. body: JSON.stringify({
  559. sessionId: currentSession.value.sessionId,
  560. content: contentToSend,
  561. sendType: 2
  562. })
  563. });
  564. }
  565. } catch (error) {
  566. console.error('发送消息失败:', error);
  567. }
  568. };
  569. // 切换控制模式
  570. const toggleControlMode = ()=>{
  571. if(currentSession.value){
  572. currentSession.value.controlMode = currentSession.value.controlMode === 'ai' ? 'human' : 'ai';
  573. }
  574. };
  575. // 渠道名称
  576. const channelName = (type)=>{
  577. const names = {QW:'企业微信', WX:'个人微信', IM:'系统IM', WHATSAPP:'WhatsApp', OTHER:'其他渠道', CHAT:'在线咨询'};
  578. return names[type] || type;
  579. };
  580. // 初始化
  581. onMounted(async () => {
  582. await loadAccounts();
  583. await loadSessions();
  584. });
  585. return {
  586. searchKey, inputMsg, showCustomerInfo, customerTab, messageList, loading,
  587. accounts, sessions, currentSession, messages, customerTags, visitRecords,
  588. filteredSessions,
  589. selectAccount, selectSession, sendMessage, toggleControlMode, channelName, formatLobsterDateTime
  590. };
  591. }
  592. }).mount('#app');
  593. </script>
  594. </body>
  595. </html>