clientSop.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>客户SOP</title>
  8. <!--引入 element-ui 的样式,-->
  9. <link rel="stylesheet"
  10. href="https://wl-1306604067.cos.ap-guangzhou.myqcloud.com/production/ct/103548289110001/1742018383195/element-ui.css">
  11. <!-- 必须先引入vue, 后使用element-ui -->
  12. <script
  13. src="https://wl-1306604067.cos.ap-guangzhou.myqcloud.com/production/ct/103548289110001/1742017957144/vue.js"></script>
  14. <!-- 引入element 的组件库-->
  15. <script
  16. src="https://wl-1306604067.cos.ap-guangzhou.myqcloud.com/production/ct/103548289110001/1742017747738/element-ui.js"></script>
  17. <script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
  18. <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
  19. <script src="https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js"></script>
  20. <!-- <script>
  21. var vConsole = new window.VConsole();
  22. </script> -->
  23. </head>
  24. <style>
  25. body {
  26. margin: 0;
  27. padding: 0;
  28. }
  29. .box {
  30. width: 100vw;
  31. height: 100vh;
  32. box-sizing: border-box;
  33. background: #FAFAFA;
  34. }
  35. .page4 {
  36. width: 100vw;
  37. height: 100vh;
  38. box-sizing: border-box;
  39. }
  40. .top_tip {
  41. background: #afdefd;
  42. display: flex;
  43. justify-content: center;
  44. color: #FFF;
  45. font-size: 14px;
  46. padding: 6px 0;
  47. }
  48. .logo {
  49. width: 20px;
  50. height: 20px;
  51. margin-right: 10px;
  52. }
  53. .tag_list {
  54. display: flex;
  55. align-items: center;
  56. padding: 10px 20px;
  57. overflow-x: auto;
  58. }
  59. .content {
  60. padding: 20px;
  61. margin-bottom: 10px;
  62. background: #FFF;
  63. }
  64. .icon_logo {
  65. width: 20px;
  66. vertical-align: bottom;
  67. }
  68. .sop_content {
  69. background: #f1f9ff;
  70. padding: 10px 20px;
  71. border-radius: 10px;
  72. font-size: 14px;
  73. }
  74. .font {
  75. color: #1890ff;
  76. }
  77. .sop_list {
  78. padding: 10px 20px;
  79. }
  80. .sop_time {
  81. font-weight: 600;
  82. }
  83. .dot {
  84. width: 10px;
  85. height: 10px;
  86. color: #1890ff;
  87. }
  88. .sop_item {
  89. background: #FFF;
  90. padding: 20px;
  91. margin: 10px 0;
  92. font-size: 14px;
  93. }
  94. .sop_body {
  95. margin: 10px 0;
  96. }
  97. .sop_c {
  98. background: #FAFAFA;
  99. padding: 10px;
  100. }
  101. .sop_foot {
  102. display: flex;
  103. justify-content: flex-end;
  104. align-items: center;
  105. margin: 10px 0;
  106. }
  107. .sop_btn {
  108. background: #1890ff;
  109. color: #FFFFFF;
  110. padding: 5px 20px;
  111. border-radius: 4px;
  112. font-size: 12px;
  113. }
  114. </style>
  115. <body>
  116. <div id="box" class="box">
  117. <!-- 数据查看 -->
  118. <div class="page4">
  119. <div class="top_tip">
  120. <img class="logo" src="./img/wefan.png"></img>
  121. 微分机器人提供技术支持
  122. </div>
  123. <el-tabs v-model="activeName" stretch @tab-click="handleClickTab">
  124. <el-tab-pane label="执行中" name="playing">
  125. <div class="tag_list">
  126. <el-tag style="margin-right: 10px;" :type="item.type" effect="plain" v-for="(item, index) in executeData"
  127. :key="index" @click="getSop(item)">{{item.sopName}}</el-tag>
  128. </div>
  129. <div class="content">
  130. <div class="sop_content">
  131. <img class="icon_logo" :src="sopItem.clientAvatar || './img/avatar.png'"></img>
  132. <span>「{{sopItem.clientName}}」在「{{timeFormat(sopItem.triggerTime, 'yyyy-MM-dd hh:mm:ss')}}」</span>
  133. <span class="font" v-if="sopItem.triggerType">添加好友后触发客户SOP任务,</span>
  134. <span class="font" v-else>打上标签后触发客户SOP任务,</span>
  135. <span>快去跟进吧</span>
  136. </div>
  137. </div>
  138. <div class="sop_list" v-for="(item, index) in sopPlans" :key="index">
  139. <div class="sop_time">
  140. <span class="dot">·</span>
  141. {{timeFormat(item.executeTime, 'yyyy-MM-dd')}}
  142. </div>
  143. <div class="sop_item">
  144. <div><span style="color: #1890ff;">{{timeFormat(item.executeTime, 'hh:mm:ss')}}</span>发送<span
  145. style="color: #999;">{{item.isSend ? '(已发送)' : ''}}</span></div>
  146. <div class="sop_body" v-if="item.ctOthers.length > 0">
  147. <div v-for="(c, cIndex) in item.ctOthers" :key="cIndex">
  148. <div class="sop_c" v-if="c.matterType === 0">
  149. <span>{{ c.matterText }}</span>
  150. </div>
  151. <div class="sop_c" v-if="c.matterType === 1">
  152. <img style="width: 100px;" :src="c.imgUrl" />
  153. </div>
  154. <div class="sop_c" v-if="c.matterType === 5">
  155. <span>{{ c.linkTitle }}</span>
  156. </div>
  157. <div class="sop_c" v-if="c.matterType === 2">
  158. <span>自定义视频</span>
  159. </div>
  160. <div class="sop_c" v-if="c.matterType === 4">
  161. <span>{{ c.fileName }}</span>
  162. </div>
  163. <div class="sop_foot">
  164. <div class="sop_btn" @click="handleSend(item.id, c)">发送</div>
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. </div>
  170. </el-tab-pane>
  171. <el-tab-pane label="已完成" name="ending">
  172. <div class="tag_list">
  173. <el-tag style="margin-right: 10px;" :type="item.type" effect="plain" v-for="(item, index) in executeData"
  174. :key="index" @click="getSop(item)">{{item.sopName}}</el-tag>
  175. </div>
  176. <div class="content">
  177. <div class="sop_content">
  178. <img class="icon_logo" :src="sopItem.clientAvatar || './img/avatar.png'"></img>
  179. <span>「{{sopItem.clientName}}」在「{{timeFormat(sopItem.triggerTime, 'yyyy-MM-dd hh:mm:ss')}}」</span>
  180. <span class="font" v-if="sopItem.triggerType">添加好友后触发客户SOP任务,</span>
  181. <span class="font" v-else>打上标签后触发客户SOP任务,</span>
  182. <span>快去跟进吧</span>
  183. </div>
  184. </div>
  185. <div class="sop_list" v-for="(item, index) in sopPlans" :key="index">
  186. <div class="sop_time">
  187. <span class="dot">·</span>
  188. {{timeFormat(item.executeTime, 'yyyy-MM-dd')}}
  189. </div>
  190. <div class="sop_item">
  191. <div><span style="color: #1890ff;">{{timeFormat(item.executeTime, 'hh:mm:ss')}}</span>发送(<span
  192. style="color: #999;">{{item.isSend ? '已发送' : ''}}</span>)</div>
  193. <div class="sop_body" v-if="item.ctOthers.length > 0">
  194. <div v-for="(c, cIndex) in item.ctOthers" :key="cIndex">
  195. <div class="sop_c" v-if="c.matterType === 0">
  196. <span>{{ c.matterText }}</span>
  197. </div>
  198. <div class="sop_c" v-if="c.matterType === 1">
  199. <img style="width: 100px;" :src="c.imgUrl" />
  200. </div>
  201. <div class="sop_c" v-if="c.matterType === 5">
  202. <span>{{ c.linkTitle }}</span>
  203. </div>
  204. <div class="sop_c" v-if="c.matterType === 2">
  205. <span>自定义视频</span>
  206. </div>
  207. <div class="sop_c" v-if="c.matterType === 4">
  208. <span>{{ c.fileName }}</span>
  209. </div>
  210. <div class="sop_foot">
  211. <div class="sop_btn" @click="handleSend(item.id, c)">发送</div>
  212. </div>
  213. </div>
  214. </div>
  215. </div>
  216. </div>
  217. </el-tab-pane>
  218. </el-tabs>
  219. </div>
  220. </div>
  221. </body>
  222. <script>
  223. new Vue({
  224. el: '#box',
  225. data () {
  226. return {
  227. httpUrl: '',
  228. bId: null,
  229. env: '',
  230. memberId: null,
  231. activeName: 'playing',
  232. executeData: [],
  233. sopItem: {},
  234. sopPlans: [],
  235. historyId: null,
  236. externalUserId: null,
  237. }
  238. },
  239. created () {
  240. this.bId = this.getQueryParam('bId')
  241. this.env = this.getQueryParam('env')
  242. if (!this.env || this.env === 'prod') {
  243. this.httpUrl = 'https://wlapi.wefanbot.com'
  244. } else {
  245. this.httpUrl = 'http://test.wefanbot.com:18993'
  246. }
  247. if (this.getQueryParam('memberId')) {
  248. // 已授权
  249. this.memberId = this.getQueryParam('memberId')
  250. this.getQyWxSign()
  251. } else {
  252. // 授权
  253. this.getAuth()
  254. }
  255. // this.memberId = "woU17nDAAAnoSca19vZVKiNEKdc9tyYQ"
  256. // this.externalUserId = "wmU17nDAAAGT2PF6G8PUHdSx2DY1ljpg"
  257. // this.executeSopList()
  258. },
  259. methods: {
  260. getAuth () {
  261. fetch(this.httpUrl + `/p/insuite/p/getRedirectUri?env=${this.env}&bId=${this.bId}`)
  262. .then(res => {
  263. return res.json()
  264. }).then(result => {
  265. let { data, code, msg } = result
  266. if (code === 1) {
  267. window.location.replace(data)
  268. } else {
  269. this.$message({
  270. message: msg,
  271. type: 'warning'
  272. })
  273. }
  274. })
  275. },
  276. getQyWxSign () {
  277. fetch(this.httpUrl + '/scrm/v1/wxcp-corp/p/getAgentConfig', {
  278. method: 'post',
  279. body: JSON.stringify({
  280. bId: this.bId,
  281. url: window.location.href,
  282. }),
  283. headers: {
  284. 'Content-Type': 'application/json'
  285. }
  286. }).then(res => {
  287. return res.json()
  288. }).then(result => {
  289. let { data, code, msg } = result
  290. if (code === 1) {
  291. let that = this
  292. wx.agentConfig({
  293. corpid: data.corpid,
  294. agentid: data.agentId,
  295. timestamp: data.timestamp, // 必填,生成签名的时间戳
  296. nonceStr: data.nonceStr, // 必填,生成签名的随机串
  297. signature: data.agentSignature, // 必填,签名,见附录1
  298. jsApiList: ['getCurExternalContact'], // 必填,需要使用的JS接口列表
  299. success: function(res) {
  300. // 回调
  301. // 此处直接在注入回调中调用了获取当前外部联系人userId的接口,注意此接口是从聊天框的工具栏进入才能获取
  302. wx.invoke('getCurExternalContact', {
  303. }, function(res) {
  304. if (res.err_msg == "getCurExternalContact:ok") {
  305. console.log('invoke成功:', res.userId);
  306. that.externalUserId = res.userId
  307. that.executeSopList()
  308. } else {
  309. //错误处理
  310. console.log('invoke失败:', res)
  311. }
  312. });
  313. },
  314. fail: function(res) {
  315. if (res.errMsg.indexOf('function not exist') > -1) {
  316. alert('版本过低请升级');
  317. }
  318. },
  319. complete: function(res) {
  320. // 回调
  321. console.log('complete1', res);
  322. }
  323. });
  324. } else {
  325. this.$message({
  326. message: msg,
  327. type: 'warning'
  328. })
  329. }
  330. })
  331. },
  332. handleClickTab (tab) {
  333. this.executeData = []
  334. this.sopItem = {}
  335. this.sopPlans = []
  336. this.historyId = null
  337. if (tab.name === 'playing') {
  338. this.executeSopList(0)
  339. } else {
  340. this.executeSopList(1)
  341. }
  342. },
  343. //获取执行中的sop计划
  344. executeSopList (status) {
  345. fetch(this.httpUrl + `/scrm/v1/wxcp-sop/p/executeSopList?externalUserId=${this.externalUserId}&memberId=${this.memberId}&bId=${this.bId}&status=${status || 0}`)
  346. .then(res => {
  347. return res.json()
  348. }).then(result => {
  349. let { data, code, msg } = result
  350. if (code === 1) {
  351. this.executeData = data
  352. if (data.length > 0) {
  353. this.executeData.forEach(i => {
  354. i.type = ''
  355. })
  356. this.getSop(data[0], status)
  357. }
  358. } else {
  359. this.$message({
  360. message: msg,
  361. type: 'warning'
  362. })
  363. }
  364. })
  365. },
  366. getSop (item, status) {
  367. this.sopItem = item
  368. this.executeData.forEach(i => {
  369. if (i.sopId !== item.sopId) {
  370. i.type = ''
  371. } else {
  372. i.type = 'success'
  373. }
  374. })
  375. this.getSopHistoryList(item, status)
  376. },
  377. // 获取sop内容
  378. getSopHistoryList (item, status) {
  379. fetch(this.httpUrl + `/scrm/v1/wxcp-sop/p/executeSopHistoryList?clientId=${item.clientId}&memberId=${this.memberId}&sopId=${item.sopId}&status=${status || 0}`)
  380. .then(res => {
  381. return res.json()
  382. }).then(result => {
  383. let { data, code, msg } = result
  384. if (code === 1) {
  385. this.sopPlans = data
  386. } else {
  387. this.$message({
  388. message: msg,
  389. type: 'warning'
  390. })
  391. }
  392. })
  393. },
  394. handleSend (historyId, c) {
  395. this.historyId = historyId
  396. if (c.matterType === 1 || c.matterType === 2 || c.matterType === 4) {
  397. fetch(this.httpUrl + `/scrm/v1/wxcp-sop/p/getMedia?planContentId=${c.id}`)
  398. .then(res => {
  399. return res.json()
  400. }).then(result => {
  401. let { data, code, msg } = result
  402. if (code === 1) {
  403. this.sendData(c, data.mediaId)
  404. } else {
  405. this.$message({
  406. message: msg,
  407. type: 'warning'
  408. })
  409. }
  410. })
  411. } else {
  412. this.sendData(c)
  413. }
  414. },
  415. sendData (c, mediaId) {
  416. fetch(this.httpUrl + '/scrm/v1/wxcp-corp/p/getAgentConfig', {
  417. method: 'post',
  418. body: JSON.stringify({
  419. bId: this.bId,
  420. url: window.location.href,
  421. }),
  422. headers: {
  423. 'Content-Type': 'application/json'
  424. }
  425. }).then(res => {
  426. return res.json()
  427. }).then(result => {
  428. let { data, code, msg } = result
  429. if (code === 1) {
  430. let that = this
  431. wx.agentConfig({
  432. corpid: data.corpid,
  433. agentid: data.agentId,
  434. timestamp: data.timestamp, // 必填,生成签名的时间戳
  435. nonceStr: data.nonceStr, // 必填,生成签名的随机串
  436. signature: data.agentSignature, // 必填,签名,见附录1
  437. jsApiList: ['sendChatMessage'], // 必填,需要使用的JS接口列表
  438. success: function(res) {
  439. // 回调
  440. if (c.matterType === 0) {
  441. wx.invoke('sendChatMessage', {
  442. msgtype: "text", //消息类型,必填
  443. enterChat: false, //为true时表示发送完成之后顺便进入会话,仅移动端3.1.10及以上版本支持该字段
  444. text: {
  445. content: c.matterText, //文本内容
  446. },
  447. }, function(res) {
  448. if (res.err_msg == 'sendChatMessage:ok') {
  449. that.updateSop()
  450. //发送成功
  451. that.$message({
  452. message: '发送成功',
  453. type: 'success'
  454. })
  455. }
  456. })
  457. } else if (c.matterType === 1) {
  458. wx.invoke('sendChatMessage', {
  459. msgtype: "image", //消息类型,必填
  460. enterChat: false, //为true时表示发送完成之后顺便进入会话,仅移动端3.1.10及以上版本支持该字段
  461. image:
  462. {
  463. mediaid: mediaId, //图片的素材id
  464. },
  465. }, function(res) {
  466. if (res.err_msg == 'sendChatMessage:ok') {
  467. that.updateSop()
  468. //发送成功
  469. that.$message({
  470. message: '发送成功',
  471. type: 'success'
  472. })
  473. }
  474. })
  475. } else if (c.matterType === 2) {
  476. wx.invoke('sendChatMessage', {
  477. msgtype: "video", //消息类型,必填
  478. enterChat: false, //为true时表示发送完成之后顺便进入会话,仅移动端3.1.10及以上版本支持该字段
  479. video:
  480. {
  481. mediaid: mediaId, //视频的素材id
  482. },
  483. }, function(res) {
  484. if (res.err_msg == 'sendChatMessage:ok') {
  485. that.updateSop()
  486. //发送成功
  487. that.$message({
  488. message: '发送成功',
  489. type: 'success'
  490. })
  491. }
  492. })
  493. } else if (c.matterType === 4) {
  494. wx.invoke('sendChatMessage', {
  495. msgtype: "file", //消息类型,必填
  496. enterChat: false, //为true时表示发送完成之后顺便进入会话,仅移动端3.1.10及以上版本支持该字段
  497. file:
  498. {
  499. mediaid: mediaId, //文件的素材id
  500. },
  501. }, function(res) {
  502. if (res.err_msg == 'sendChatMessage:ok') {
  503. that.updateSop()
  504. //发送成功
  505. that.$message({
  506. message: '发送成功',
  507. type: 'success'
  508. })
  509. }
  510. })
  511. } else if (c.matterType === 5) {
  512. wx.invoke('sendChatMessage', {
  513. msgtype: "news", //消息类型,必填
  514. enterChat: false, //为true时表示发送完成之后顺便进入会话,仅移动端3.1.10及以上版本支持该字段
  515. news:
  516. {
  517. link: c.linkUrl, //H5消息页面url 必填
  518. title: c.linkTitle, //H5消息标题
  519. desc: c.linkDescription, //H5消息摘要
  520. imgUrl: c.linkThumbUrl, //H5消息封面图片URL
  521. }
  522. }, function(res) {
  523. if (res.err_msg == 'sendChatMessage:ok') {
  524. that.updateSop()
  525. //发送成功
  526. that.$message({
  527. message: '发送成功',
  528. type: 'success'
  529. })
  530. }
  531. })
  532. }
  533. },
  534. fail: function(res) {
  535. if (res.errMsg.indexOf('function not exist') > -1) {
  536. alert('版本过低请升级');
  537. }
  538. },
  539. complete: function(res) {
  540. // 回调
  541. console.log('complete', res);
  542. }
  543. });
  544. } else {
  545. this.$message({
  546. message: msg,
  547. type: 'warning'
  548. })
  549. }
  550. })
  551. },
  552. // 发送成功后刷新数据
  553. updateSop () {
  554. fetch(this.httpUrl + `/scrm/v1/wxcp-sop/p/updateHistorySendStatus?historyId=${this.historyId}`)
  555. .then(res => {
  556. return res.json()
  557. }).then(result => {
  558. let { data, code, msg } = result
  559. if (code === 1) {
  560. this.getSopHistoryList(this.sopItem)
  561. } else {
  562. this.$message({
  563. message: msg,
  564. type: 'warning'
  565. })
  566. }
  567. }).finally(() => {
  568. this.historyId = null
  569. })
  570. },
  571. // 截取url中的数据
  572. getQueryParam (paramName) {
  573. // 获取当前URL的查询字符串部分
  574. const queryString = window.location.search;
  575. // 创建一个URLSearchParams对象
  576. const urlParams = new URLSearchParams(queryString);
  577. // 返回指定参数的值,如果不存在则返回null
  578. return urlParams.get(paramName);
  579. },
  580. timeFormat (time, format = 'yyyy-MM-dd hh:mm:ss') {
  581. if (time === undefined || time === '' || time === null) {
  582. return '/';
  583. }
  584. const date = new Date(time);
  585. const o = {
  586. 'M+': date.getMonth() + 1, // 月份
  587. 'd+': date.getDate(), // 日
  588. 'h+': date.getHours(), // 小时
  589. 'm+': date.getMinutes(), // 分钟
  590. 's+': date.getSeconds(), // 秒
  591. 'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
  592. 'S': date.getMilliseconds() // 毫秒
  593. };
  594. // 处理年份
  595. if (/(y+)/.test(format)) {
  596. format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  597. }
  598. // 处理日期和时间部分
  599. for (let k in o) {
  600. if (new RegExp('(' + k + ')').test(format)) {
  601. let value = o[k];
  602. let padding = RegExp.$1.length === 1 ? '' : '00'; // 根据格式字符串中的长度决定是否补零
  603. format = format.replace(RegExp.$1, ('' + value).padStart(padding.length + value.toString().length - value.toString().length, '0'));
  604. }
  605. }
  606. // 如果格式只包含时间部分,移除日期部分可能的占位符
  607. if (!/(M+|d+)/.test(format)) {
  608. // 移除任何可能存在的日期占位符(如:'yyyy-MM-dd ')
  609. format = format.replace(/(\s*-\s*){2}/g, ''); // 移除两个'-'之间的任何内容
  610. format = format.replace(/^\s*yyyy-\s*/, ''); // 移除开头的'yyyy-'
  611. }
  612. // 如果格式只包含日期部分,移除时间部分可能的占位符
  613. if (!/(h+|m+|s+)/.test(format)) {
  614. // 移除任何可能存在的时间占位符(如:' hh:mm:ss')
  615. format = format.replace(/(\s*:\s*){2}/g, ''); // 移除两个':'之间的任何内容
  616. format = format.replace(/\s*hh:\s*$/, ''); // 移除结尾的' hh:'
  617. }
  618. return format;
  619. }
  620. }
  621. })
  622. </script>
  623. </html>