clientSop.html 22 KB

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