edit.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. <template>
  2. <div class="send-email" style="display: flex;" v-loading="dataLoadng">
  3. <div class="header">
  4. <div class="header-btn">
  5. <el-button type="primary" @click="saveEmail" v-loading>保存</el-button>
  6. </div>
  7. </div>
  8. <div class="content" style="display: flex;">
  9. <div class="send-content">
  10. <div class="send-input">
  11. <span class="send-label">收件人</span>
  12. <el-input v-model="form.recipient" disabled></el-input>
  13. </div>
  14. <div class="send-input">
  15. <span class="send-label">抄送</span>
  16. <el-input v-model="form.recipientCc" disabled></el-input>
  17. </div>
  18. <div class="send-input">
  19. <span class="send-label">密件抄送</span>
  20. <el-input v-model="form.recipientBcc" disabled></el-input>
  21. </div>
  22. <div class="send-input">
  23. <span class="send-label">主题</span>
  24. <el-input v-model="form.subject"></el-input>
  25. </div>
  26. <div class="msg-item" style="display: flex;flex: 1">
  27. <div style="display: flex;flex-direction:column; flex: 1">
  28. <ckeditor
  29. class="send-ckeditor"
  30. placeholder="智能写信新体验,试试AI润色让文字焕发活力"
  31. @sendContnet="getContent"
  32. :content="form.content"
  33. ref="ckeditorRef"
  34. ></ckeditor>
  35. <!-- <div class="ai-button" v-if="aiShow">
  36. <el-button v-for="item in aiButton" @click="aiButtonClick(item)">
  37. <img :src="getImageUrl(item.value)" />
  38. {{ item.label }}
  39. </el-button>
  40. </div>
  41. <div class="ai-div">
  42. <p>智能写信新体验,试试AI润色让文字焕发活力</p>
  43. <el-button type="primary" @click="aiShow = !aiShow">AI润色</el-button>
  44. </div> -->
  45. </div>
  46. <div class="file-div" v-loading="fileLoading">
  47. <div class="file-header">
  48. <span>附件列表</span>
  49. <span>{{ (form.fileIds.length || 0) + (form.oldfileIds?.length || 0)}}个文件 已上传{{ fileSize }}K</span>
  50. </div>
  51. <el-upload
  52. class="file-upload"
  53. ref="uploadRef"
  54. multiple
  55. :file-list="fileList"
  56. action=""
  57. :http-request="uploadFile"
  58. :on-change="changeFiles"
  59. :on-remove="removeFile"
  60. :auto-upload="false"
  61. drag
  62. >
  63. <p style="margin-top: 50px" v-if="(form.fileIds.length + form.oldfileIds?.length) == 0">点击上传</p>
  64. </el-upload>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. <el-dialog title="AI润色" v-model="aiVisible" width="780px" append-to-body>
  71. <div
  72. v-loading="aiLoading"
  73. element-loading-text="生成中..."
  74. style="min-height: 100px;"
  75. >
  76. <p v-html="aiMsg"></p>
  77. </div>
  78. <template #footer>
  79. <el-button v-if="!aiDone" @click="aiCancel">停止生成</el-button>
  80. <div v-else style="display: flex;justify-content: space-between;">
  81. <div>
  82. <el-button @click="setAIContent('replace')">替换现有内容</el-button>
  83. <el-button @click="setAIContent('insert')">插入到正文</el-button>
  84. <el-button @click="sendSocketMessage(aiPrompt)">重新生成</el-button>
  85. </div>
  86. <div>
  87. <el-button @click="copyText">复制</el-button>
  88. <el-button @click="aiCancel">关闭</el-button>
  89. </div>
  90. </div>
  91. <p v-if="aiDone" class="ai-tips">AI产出内容仅供参考,请详细阅读后使用</p>
  92. </template>
  93. </el-dialog>
  94. </template>
  95. <script setup>
  96. import { ref, reactive, onMounted } from 'vue'
  97. import { useRouter } from 'vue-router'
  98. import { getEmailDetail, editEmail } from '@/api/marketing'
  99. import ckeditor from '@/components/ckeditor/index.vue'
  100. const getContent = (val) => {
  101. form.value.content = val
  102. }
  103. const router = useRouter();
  104. let queryData = router.currentRoute.value.query
  105. const getData = () => {
  106. getEmailDetail({ id: queryData.id }).then(res => {
  107. form.value.recipient = res.result.recipient
  108. form.value.subject = res.result.subject
  109. form.value.content = res.result.content
  110. })
  111. }
  112. const listType = ref('1')
  113. const copyFlag = ref(true)
  114. const themeFlag = ref(true)
  115. const loading = ref(false)
  116. const dataLoadng = ref(false)
  117. const emailsData = ref([])
  118. const selectedEmail = ref('')
  119. const copySelectedEmail = ref('')
  120. const fileList = ref([])
  121. const editContent = ref('')
  122. const getUserEmail = () => {
  123. // getUserEmails().then(res => {
  124. // if(res.success) {
  125. // if(res.result.length != 0) {
  126. // emailsData.value = res.result
  127. // selectedEmail.value = res.result.filter(item => item.useDefault == 1)[0].id || ''
  128. // copySelectedEmail.value = selectedEmail.value
  129. // if(queryData.id) {
  130. // dataLoadng.value = true
  131. // getEmailDetailContent({ mailId: queryData.id }).then(res => {
  132. // if(res.success) {
  133. // let result = res.result
  134. // let recipient = queryData.type == 'edit' ? result.recipient : result.from
  135. // if(queryData.type != 'forward') inputRef.value.recipientls.addMail(recipient)
  136. // // 回复全部的话,把原邮件收件人跟发件人都作为这封邮件的收件人
  137. // if(queryData.type == 'replyAll') inputRef.value.recipientls.addMail(result.recipient)
  138. // if(queryData.type == 'edit') {
  139. // editContent.value += result.content
  140. // form.value.subject = result.subject
  141. // inputRef.value.recipientCcls.addMail(result.recipientCc)
  142. // inputRef.value.recipientBccls.addMail(result.recipientBcc)
  143. // fileList.value = (result.attachments || []).map(item => {
  144. // let obj = {
  145. // name: item.fileName + '.' + item.fileExt,
  146. // url: item.filePath,
  147. // id: item.id,
  148. // fileSize: item.fileSize,
  149. // type: 'old'
  150. // }
  151. // return obj
  152. // })
  153. // changeFile(fileList.value)
  154. // if(result.folder != 'SENT') {
  155. // form.value.id = result.id
  156. // }
  157. // }else{
  158. // editContent.value = `
  159. // <p>--------------Original Message--------------</p>
  160. // <p>From:${ recipient }</p>
  161. // <p>To:${ result.recipient }</p>
  162. // <p>Cc:${ result.recipientCc || '' }</p>
  163. // <p>Subject:${ result.subject }</p>
  164. // <p>Date:${ result.recipientDate }</p>
  165. // `
  166. // if(queryData.type == 'forward') { // 转发
  167. // fileList.value = (result.attachments || []).map(item => {
  168. // let obj = {
  169. // name: item.fileName + '.' + item.fileExt,
  170. // url: item.filePath,
  171. // id: item.id,
  172. // fileSize: item.fileSize,
  173. // type: 'old'
  174. // }
  175. // return obj
  176. // })
  177. // changeFile(fileList.value)
  178. // form.value.subject = 'Fw:' + result.subject
  179. // editContent.value += ('<br />' + result.content)
  180. // }else{
  181. // form.value.replyMsgId = queryData.id
  182. // form.value.subject = 'Re:' + result.subject
  183. // if(queryData.type == 'reply' || queryData.type == 'replyAll'){ // 回复带上内容
  184. // editContent.value += ('<br />' + result.content)
  185. // }
  186. // if(queryData.type == 'replyAll'){ // 回复全部带上抄送人
  187. // inputRef.value.recipientCcls.addMail(result.recipientCc)
  188. // }
  189. // }
  190. // }
  191. // previewFileList.value = fileList.value
  192. // form.value.fileIds = []
  193. // form.value.oldfileIds = fileList.value.map(item => item.id)
  194. // setTimeout(() => {
  195. // form.value.content = editContent.value
  196. // }, 500)
  197. // }
  198. // }).finally(() => {
  199. // dataLoadng.value = false
  200. // })
  201. // }
  202. // if(queryData.email) {
  203. // addMail(queryData.email)
  204. // }
  205. // }else {
  206. // ElMessage.error('请先前往配置邮箱')
  207. // }
  208. // }
  209. // })
  210. }
  211. const ckeditorRef = ref()
  212. const personList = ref([])
  213. const personListData = ref([])
  214. const colleagueList = ref([])
  215. const isLoadAll = ref(false)
  216. const loadingData = ref(false)
  217. const custParam = reactive({
  218. scopeMenuId: null,
  219. pageSize: 50,
  220. pageIndex: 1,
  221. blurry: ''
  222. })
  223. const getUser = () => {
  224. // if(loadingData.value) {
  225. // return;
  226. // }
  227. // loadingData.value = true
  228. // getCustomerEmails(custParam).then(res => {
  229. // if(res.success) {
  230. // res.result.records.map(item => {
  231. // personList.value.push(item)
  232. // for(let i = 1; i < 4; i++){
  233. // if(item['email' + i]){
  234. // personList.value.push({
  235. // liaisonName: item.liaisonName + '(备用' + i + ')',
  236. // email: item['email' + i],
  237. // customerName: item.customerName,
  238. // country: item.country
  239. // })
  240. // }
  241. // }
  242. // })
  243. // if(custParam.pageIndex >= res.result.pages){
  244. // isLoadAll.value = true
  245. // }
  246. // custParam.pageIndex ++
  247. // }
  248. // loadingData.value = false
  249. // })
  250. // getColleagueEmails().then(res => {
  251. // if(res.success) {
  252. // colleagueList.value = res.result.map(item => {
  253. // item.show = false
  254. // return item
  255. // })
  256. // }
  257. // })
  258. }
  259. const customerName = ref()
  260. const serachCustomer = (val) =>{
  261. custParam.pageIndex = 1
  262. personList.value= []
  263. isLoadAll.value = false
  264. getUser()
  265. // getUser(val)
  266. // if(!val) {
  267. // personList.value = personListData.value
  268. // return
  269. // }
  270. // personList.value = personList.value.filter(function(item){
  271. // return item.customerName.includes(val)||item.email.includes(val)||item.liaisonName.includes(val)
  272. // })
  273. }
  274. const isDelaySend = ref(0)
  275. const form = ref({
  276. content: '',
  277. recipient: '',
  278. recipientCc: '',
  279. recipientBcc: '',
  280. subject: '',
  281. fileIds: [],
  282. replyMsgId: '',
  283. id: undefined,
  284. isReceipt: 0,
  285. isTrack: 0,
  286. readRemind: 0,
  287. delaySendTime: '',
  288. oldfileIds: undefined
  289. })
  290. const saveLoading = ref(false)
  291. const saveEmail = () => {
  292. saveLoading.value = true
  293. editEmail({ id: queryData.id, content: form.value.content, subject: form.value.subject }).then(res => {
  294. ElMessage.success('保存成功')
  295. }).catch((err) => {
  296. ElMessage.error(err.message || '保存失败')
  297. }).finally(() => {
  298. saveLoading.value = false
  299. })
  300. }
  301. // 上传附件部分
  302. // const token = getToken()
  303. const uploadRef = ref()
  304. const timer = ref()
  305. const files = ref([])
  306. const fileLoading = ref(false)
  307. const previewFileList = ref([])
  308. const changeFiles = (file) => {
  309. if(file.raw) {
  310. files.value.push(file.raw)
  311. previewFileList.value.push(file.raw)
  312. }
  313. clearTimeout(timer.value)
  314. timer.value = setTimeout(() => {
  315. uploadFile()
  316. }, 500)
  317. }
  318. const uploadFile = () => {
  319. fileLoading.value = true
  320. let formData = new FormData()
  321. files.value.map(item => {
  322. formData.append('files', item)
  323. })
  324. request({
  325. url: '/mail/file/cache_mail_file',
  326. method: 'post',
  327. data: formData,
  328. headers: {
  329. "Content-Type": "multipart/form-data",
  330. token
  331. }
  332. }).then(res => {
  333. if(res.success) {
  334. changeFile(res.result)
  335. }
  336. }).catch(() => {
  337. fileList.value = []
  338. ElMessage.error('上传失败,请稍后再试')
  339. }).finally(() => {
  340. fileLoading.value = false
  341. files.value = []
  342. })
  343. }
  344. const fileSize = ref(0)
  345. const changeFile = (files) => {
  346. files.map(item => {
  347. form.value.fileIds.push(item.id)
  348. fileSize.value = Number((fileSize.value + Number((Number(item.fileSize) / 1024))).toFixed(2))
  349. })
  350. }
  351. const removeFile = (file) => {
  352. fileSize.value = Number((fileSize.value - Number(((file.fileSize || file.size) / 1024).toFixed(2))).toFixed(2))
  353. if(file.type == 'old') {
  354. form.value.oldfileIds.splice(form.value.oldfileIds.findIndex(item => item == file.id), 1)
  355. }else {
  356. form.value.fileIds.splice(form.value.fileIds.findIndex(item => item == file.id), 1)
  357. previewFileList.value.splice(previewFileList.value.findIndex(item => item.id == file.id), 1)
  358. }
  359. }
  360. const preview = () => {
  361. // let params = JSON.parse(JSON.stringify(form.value))
  362. // params.from = emailsData.value.filter(item => item.useDefault == 1)[0].mailAccount
  363. // params.nowDate = moment().format('YYYY-MM-DD HH:mm:ss')
  364. // params.fileList = previewFileList.value.map(item => {
  365. // return { name: item.name || item.fileShowName }
  366. // })
  367. // let previewData = JSON.stringify(params)
  368. // sessionStorage.setItem('previewData', previewData)
  369. // const { href } = router.resolve({
  370. // path: '/email/preview'
  371. // })
  372. // window.open(href, '_blank')
  373. }
  374. const aiButton = ref([
  375. { label: '润色', value: 'polish' },
  376. { label: '续写', value: 'rewrite' },
  377. { label: '内容纠错', value: 'error' },
  378. // { label: '格式优化', value: 'format' },
  379. { label: '关闭润色', value: 'close' }
  380. ])
  381. const getImageUrl = (value) => {
  382. // 注意:这里的路径是相对于当前 .vue 文件的路径
  383. return new URL(`../../assets/images/email-${value}.png`, import.meta.url).href
  384. }
  385. const aiShow = ref(false)
  386. const aiButtonClick = (item) => {
  387. switch(item.value) {
  388. case 'close':
  389. aiShow.value = false
  390. break;
  391. default:
  392. if(!form.value.content){
  393. ElMessage.error('请输入邮件内容')
  394. return false
  395. }
  396. getPromptText(item)
  397. break;
  398. }
  399. }
  400. const aiPrompt = ref('')
  401. let customerParams = {
  402. customerId: [],
  403. customerName: [],
  404. liaisonName: []
  405. }
  406. const getPromptText = (item) => {
  407. let selectionText = ckeditorRef.value?.ckeditor.getSelection().getSelectedText()
  408. let templateCode = {
  409. 'polish': 'EMAIL_POLISHING',
  410. 'rewrite': 'EMAIL_CONTINUATION',
  411. 'error': 'EMAIL_CORRECTION'
  412. }
  413. let content = selectionText || form.value.content
  414. let mailParams = {
  415. content: content,
  416. subject: form.value.subject,
  417. templateCode: templateCode[item.value]
  418. }
  419. let params = { ...mailParams }
  420. if(customerParams.customerId.length > 0) {
  421. params.customerId = customerParams.customerId.join(",")
  422. params.customerName = customerParams.customerName.join(",")
  423. params.liaisonName = customerParams.liaisonName.join(",")
  424. }
  425. getPrompt(params).then(res => {
  426. if(res.success) {
  427. aiVisible.value = true
  428. sendSocketMessage(res.result)
  429. aiPrompt.value = res.result
  430. }
  431. })
  432. }
  433. const aiVisible = ref(false)
  434. const aiLoading = ref(false)
  435. const ws = ref(null)
  436. const socketOpen = ref(false)
  437. const aiMsg = ref('')
  438. const aiDone = ref(false)
  439. const initWebSocket = (msg) => {
  440. aiLoading.value = true
  441. let tokenConfig = localStorage.getItem('Admin-Token')
  442. ws.value = new WebSocket(import.meta.env.VITE_APP_WS_HOST, [tokenConfig])
  443. //监听是否连接成功
  444. ws.value.onopen = () => {
  445. console.log('ws连接状态:' + ws.value.readyState);
  446. //连接成功则发送一个数据
  447. // this.ws.send('连接成功')
  448. sendSocketMessage(msg)
  449. socketOpen.value = true
  450. }
  451. //接听服务器发回的信息并处理展示
  452. ws.value.onmessage = (res) => {
  453. aiLoading.value = false
  454. if(res.data == '[done]') {
  455. aiDone.value = true
  456. if(ws.value) {
  457. ws.value.close();
  458. }
  459. return false
  460. }
  461. aiMsg.value += res.data
  462. }
  463. //监听连接关闭事件
  464. ws.value.onclose = () => {
  465. //监听整个过程中websocket的状态
  466. console.log('ws连接状态:' + ws.value.readyState);
  467. socketOpen.value = false
  468. ws.value = null
  469. }
  470. //监听并处理error事件
  471. ws.value.onerror = function (error) {
  472. console.log(error);
  473. socketOpen.value = false
  474. ws.value = null
  475. }
  476. }
  477. const sendSocketMessage = (msg) => {
  478. aiMsg.value = ''
  479. aiLoading.value = true
  480. aiDone.value = false
  481. if(ws.value == null) {
  482. //如果ws断开连接需要重新连接
  483. initWebSocket(msg)
  484. return
  485. }
  486. ws.value.send(msg)
  487. }
  488. const aiCancel = () => {
  489. aiVisible.value = false
  490. if(ws.value) {
  491. ws.value.close();
  492. }
  493. aiMsg.value = ''
  494. aiLoading.value = true
  495. aiDone.value = false
  496. }
  497. const copyText = async() => {
  498. await navigator.clipboard.writeText(aiMsg.value);
  499. ElMessage.success('复制成功')
  500. }
  501. const setAIContent = (type) => {
  502. if(type == 'insert') {
  503. let editor = ckeditorRef.value?.ckeditor
  504. const range = editor.createRange();
  505. range.selectNodeContents(editor.editable())
  506. range.collapse(false)
  507. // 选择范围,这将设置光标位置
  508. editor.getSelection().selectRanges([range]);
  509. ckeditorRef.value?.ckeditor.insertHtml(aiMsg.value)
  510. }else {
  511. let replacedContent = aiMsg.value
  512. let selectionText = ckeditorRef.value?.ckeditor.getSelection().getSelectedText()
  513. if(selectionText) {
  514. replacedContent = form.value.content.replace(selectionText, aiMsg.value);
  515. }
  516. ckeditorRef.value?.ckeditor.setData(replacedContent)
  517. }
  518. aiCancel()
  519. }
  520. onMounted(() => {
  521. getData()
  522. })
  523. </script>
  524. <style scoped>
  525. .send-email{
  526. height: 100vh;
  527. flex-direction: column;
  528. }
  529. .send-email .header{
  530. height: 56px;
  531. padding: 0 16px;
  532. background: #FFF;
  533. box-shadow: 0px 1px 0px 0px #F0F0F0;
  534. justify-content: space-between;
  535. display: flex;
  536. align-items: center;
  537. }
  538. .send-email .content{
  539. padding: 16px;
  540. column-gap: 16px;
  541. flex: 1;
  542. height: 0;
  543. background: #f2f3f5
  544. }
  545. .send-email .content .send-content{
  546. background: #FFF;
  547. flex: 1;
  548. padding: 16px;
  549. display: flex;
  550. flex-direction: column;
  551. }
  552. .send-email .content .send-input{
  553. margin-bottom: 12px;
  554. display: flex;
  555. align-items: center;
  556. }
  557. .send-email .content .send-input .send-label{
  558. color: #666666;
  559. width: 60px;
  560. margin-right: 8px;
  561. text-align: right;
  562. }
  563. .send-email .content .send-input .el-input{
  564. height: 32px;
  565. }
  566. .send-email .content .send-content .editor-div{
  567. flex: 1;
  568. display: flex;
  569. flex-direction: column;
  570. }
  571. .send-email .content .send-content .editor-div :deep(.editor){
  572. height: 100%;
  573. display: flex;
  574. flex-direction: column;
  575. }
  576. .send-email .content .send-content .editor-div :deep(.editor) .ql-toolbar{
  577. background: #F8F8F8;
  578. }
  579. .msg-item .file-div{
  580. width: 220px;
  581. border: 1px solid #DCDEE2;
  582. border-left: 0;
  583. display: flex;
  584. flex-direction: column;
  585. }
  586. .msg-item .file-div .file-header{
  587. display: flex;
  588. align-items: center;
  589. height: 41px;
  590. background: #f8f8f8;
  591. color: #000;
  592. justify-content: space-between;
  593. padding: 0 10px;
  594. border-bottom: 1px solid #DCDEE2;
  595. }
  596. .msg-item .file-div :deep(.file-upload){
  597. flex: 1;
  598. position: relative;
  599. }
  600. .msg-item .file-div :deep(.file-upload) .el-upload{
  601. height: 100%;
  602. }
  603. .msg-item .file-div :deep(.file-upload) .el-upload .el-upload-dragger{
  604. height: 100%;
  605. padding: 10px;
  606. border: none;
  607. border-radius: 0;
  608. /* background: #0F172B; */
  609. }
  610. .msg-item .file-div :deep(.file-upload) .el-upload-list{
  611. position: absolute;
  612. top: 0;
  613. width: 100%;
  614. }
  615. .msg-item .ai-div{
  616. height: 48px;
  617. border: 1px solid #d1d1d1;
  618. padding: 0 16px;
  619. display: flex;
  620. justify-content: space-between;
  621. align-items: center;
  622. color: #333;
  623. }
  624. .msg-item .ai-button{
  625. padding: 16px 15px;
  626. border: 1px solid #d1d1d1;
  627. border-top: none;
  628. border-bottom: none;
  629. }
  630. .msg-item .ai-button :deep(.el-button){
  631. background: #6366F1;
  632. color: #FFF;
  633. border: none;
  634. }
  635. .msg-item .ai-button :deep(.el-button) img{
  636. margin-right: 5px;
  637. }
  638. .msg-item .ai-button :deep(.el-button):hover{
  639. background: #3441569a;
  640. }
  641. </style>