index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. <script>
  2. import Vue from 'vue'
  3. import hugerte from 'hugerte'
  4. import 'hugerte/models/dom'
  5. import 'hugerte/icons/default'
  6. import 'hugerte/themes/silver'
  7. import 'hugerte/skins/ui/oxide/skin.js'
  8. import 'hugerte/skins/ui/oxide/content.js'
  9. import 'hugerte/skins/content/default/content.js'
  10. import XLSX from 'xlsx'
  11. import { getAudienceList, sentInvitation, getFormList, getFormInfo, importAudience } from '@/api/form'
  12. import { getExpoList, getMyExpoInfo } from '@/api/expo'
  13. import { getTemplateList } from '@/api/template'
  14. import expoPopover from '@/views/components/expoPopover.vue'
  15. export default Vue.extend({
  16. name: 'Index',
  17. components: { expoPopover },
  18. data() {
  19. return {
  20. expoList: [],
  21. userList: [],
  22. invitation_data: {
  23. code: '',
  24. tempIndex: 0,
  25. data: [],
  26. exhibitorSetting: {},
  27. userSetting: {},
  28. show: false,
  29. page: 0,
  30. last_page: 1,
  31. is_sending: false
  32. },
  33. import_data: {
  34. showImport: false,
  35. step: 0,
  36. exhibitor_id: '',
  37. xlsx: '',
  38. file: ''
  39. },
  40. loading: false,
  41. searchTimer: null,
  42. searchWord: '',
  43. is_export: 0,
  44. expo_id: 0,
  45. current_page: 0,
  46. page_size: 20,
  47. last_page: 1,
  48. total: 0,
  49. ossUrl: process.env.VUE_APP_OSS_DOMAIN
  50. }
  51. },
  52. mounted() {
  53. this.getAudience()
  54. this.getExpoList()
  55. this.getInvitationList()
  56. },
  57. methods: {
  58. creatExcel(exhibitor_id) {
  59. if (exhibitor_id !== '') {
  60. if (this.loading) { return }
  61. this.loading = true
  62. if (exhibitor_id === 0) {
  63. getFormList(1, 1)
  64. .then(res => {
  65. if (res.data.data.length > 0) {
  66. return getFormInfo(res.data.data[0].id)
  67. } else {
  68. this.$notify({
  69. title: '出错了',
  70. message: '请先创建至少一个表单模板',
  71. type: 'warning'
  72. })
  73. this.loading = false
  74. return
  75. }
  76. })
  77. .then(res2 => {
  78. const workBook = XLSX.utils.book_new()
  79. const lineData = []
  80. res2.data.fields.forEach(field => {
  81. lineData.push(field.field_label)
  82. })
  83. const data = [lineData]
  84. const workSheet = XLSX.utils.aoa_to_sheet(data)
  85. XLSX.utils.book_append_sheet(workBook, workSheet, '观众表')
  86. this.import_data.xlsx = workBook
  87. this.loading = false
  88. }).catch(err => {
  89. this.$notify({
  90. title: '提示',
  91. message: '获取表单列表失败:'+err,
  92. type: 'error'
  93. })
  94. this.loading = false
  95. })
  96. } else {
  97. getMyExpoInfo(exhibitor_id).then(res => {
  98. if (res.data.form_template_id) {
  99. return getFormInfo(res.data.form_template_id)
  100. } else {
  101. this.$notify({
  102. title: '出错了',
  103. message: '这个展会没有绑定表单',
  104. type: 'error'
  105. })
  106. this.loading = false
  107. return
  108. }
  109. }).then(res2 => {
  110. const workBook = XLSX.utils.book_new()
  111. const lineData = []
  112. res2.data.fields.forEach(field => {
  113. lineData.push(field.field_label)
  114. })
  115. const data = [lineData]
  116. const workSheet = XLSX.utils.aoa_to_sheet(data)
  117. XLSX.utils.book_append_sheet(workBook, workSheet, '观众表')
  118. this.import_data.xlsx = workBook
  119. this.loading = false
  120. }).catch(err => {
  121. this.$notify({
  122. title: '提示',
  123. message: '获取展会信息失败'+err,
  124. type: 'error'
  125. })
  126. this.loading = false
  127. })
  128. }
  129. }
  130. },
  131. downloadTemplate() {
  132. if (this.import_data.xlsx) {
  133. this.import_data.step = 1
  134. XLSX.writeFile(this.import_data.xlsx, '观众表.xlsx')
  135. }
  136. },
  137. uploadExcel(event) {
  138. this.import_data.file = event.target.files[0]
  139. },
  140. importData() {
  141. if (this.loading) { return }
  142. this.loading = true
  143. if (this.import_data.file && this.import_data.exhibitor_id !== '') {
  144. importAudience(this.import_data.file, this.import_data.exhibitor_id).then(res => {
  145. this.$notify({
  146. title: '导入完成',
  147. message: '添加了' + res.data.update_count + '条数据',
  148. type: 'success'
  149. })
  150. this.loading = false
  151. }).catch(err => {
  152. this.$notify({
  153. title: '提示',
  154. message: '导入观众失败'+err,
  155. type: 'error'
  156. })
  157. this.loading = false
  158. })
  159. } else {
  160. this.$notify({
  161. title: '出错了',
  162. message: '请选择文件与展会信息',
  163. type: 'warning'
  164. })
  165. this.loading = false
  166. }
  167. },
  168. getLabelName(key) {
  169. const name = {
  170. address: '地址',
  171. city: '城市',
  172. company: '公司名称',
  173. country: '国家',
  174. create_time: '创建时间',
  175. department: '部门',
  176. email: '邮箱',
  177. end_date: '结束时间',
  178. expo_name: '展会名称',
  179. expo_id: '展会ID',
  180. full_name: '姓名',
  181. first_name: '姓',
  182. last_name: '名',
  183. form_user_id: '表单用户ID',
  184. id: 'ID',
  185. id_number: '证件号码',
  186. id_type: '证件类型',
  187. industry: '行业',
  188. ip: 'IP',
  189. location: '位置',
  190. mobile: '手机号',
  191. mobile_country_code: '区号',
  192. organizer: '展商名称',
  193. position: '职位',
  194. province: '省/州',
  195. send_invitation_id: '发送邀请函ID',
  196. start_date: '开始时间',
  197. status: '状态',
  198. template_id: '表单模板ID',
  199. template_name: '表单模板名称',
  200. update_time: '更新时间',
  201. user_id: '用户ID'
  202. }
  203. if (name[key]) {
  204. return name[key]
  205. } else {
  206. return key
  207. }
  208. },
  209. getInvitationList() {
  210. getTemplateList(++this.invitation_data.page, 10).then(res => {
  211. this.invitation_data.data = res.data.data || []
  212. this.last_page = res.data.last_page
  213. this.page = res.data.current_page
  214. }).catch(err => {
  215. this.$notify({
  216. title: '提示',
  217. message: '获取邀请函列表失败'+err,
  218. type: 'error'
  219. })
  220. })
  221. },
  222. getExpoList() {
  223. getExpoList(1, 1000).then(res => {
  224. this.expoList = res.data.data
  225. }).catch(err => {
  226. this.$notify({
  227. title: '提示',
  228. message: '获取展会列表失败'+err,
  229. type: 'error'
  230. })
  231. })
  232. },
  233. search(event) {
  234. if (this.searchTimer) {
  235. clearTimeout(this.searchTimer)
  236. }
  237. this.searchTimer = setTimeout(() => {
  238. this.current_page = 1
  239. this.getAudience()
  240. }, 500)
  241. },
  242. getAudience() {
  243. if (this.loading) {
  244. return
  245. }
  246. this.loading = true
  247. getAudienceList(this.current_page, this.page_size, this.searchWord, this.is_export, this.expo_id).then(res => {
  248. this.current_page = res.data.current_page
  249. this.last_page = res.data.last_page
  250. this.total = res.data.total
  251. this.userList = res.data.data
  252. this.loading = false
  253. }).catch(err => {
  254. this.$notify({
  255. title: '提示',
  256. message: '获取观众列表失败'+err,
  257. type: 'error'
  258. })
  259. this.loading = false
  260. })
  261. },
  262. openDialog(row) {
  263. this.invitation_data.show = true
  264. this.invitation_data.userSetting = row
  265. getMyExpoInfo(row.expo_id).then(res => {
  266. this.invitation_data.exhibitorSetting = res.data
  267. this.changeTemp(0)
  268. }).catch(err => {
  269. this.$notify({
  270. title: '提示',
  271. message: '获取展会信息失败'+err,
  272. type: 'error'
  273. })
  274. })
  275. },
  276. changeTemp(index) {
  277. hugerte.init({
  278. selector: '#editor',
  279. skin_url: 'default',
  280. content_css: 'default',
  281. statusbar: false,
  282. toolbar: false,
  283. menubar: false,
  284. height: '100%',
  285. width: '100%',
  286. setup: editor => {
  287. editor.on('drop', event => {
  288. event.preventDefault()
  289. const html = event.dataTransfer.getData('text/html')
  290. editor.insertContent(html)
  291. })
  292. }
  293. }).then(editor => {
  294. this.invitation_data.tempIndex = index
  295. this.parseCode(this.invitation_data.data[index].content)
  296. })
  297. },
  298. dragStart(event, text) {
  299. console.log(text)
  300. const html = `<span>${text}</span>`
  301. event.dataTransfer.setData('text/html', html)
  302. },
  303. parseCode(code) {
  304. const tempDiv = document.createElement('div')
  305. tempDiv.innerHTML = code
  306. const variableSpans = tempDiv.querySelectorAll('span[data-v].hugerte-variable')
  307. variableSpans.forEach(span => {
  308. const key = span.getAttribute('data-v')
  309. if (key in this.invitation_data.exhibitorSetting) {
  310. span.textContent = this.invitation_data.exhibitorSetting[key]
  311. }
  312. if (key in this.invitation_data.userSetting) {
  313. span.textContent = this.invitation_data.userSetting[key]
  314. }
  315. })
  316. this.invitation_data.code = tempDiv.innerHTML
  317. hugerte.activeEditor.setContent(tempDiv.innerHTML)
  318. },
  319. closeDialog() {
  320. this.invitation_data.show = false
  321. hugerte.remove('#editor')
  322. },
  323. sendInvitation() {
  324. this.invitation_data.is_sending = true
  325. const content = hugerte.activeEditor.getContent()
  326. sentInvitation([this.invitation_data.userSetting.id], content).then(res => {
  327. this.closeDialog()
  328. this.$message.success('发送成功')
  329. this.invitation_data.is_sending = false
  330. }).catch(err => {
  331. this.$notify({
  332. title: '提示',
  333. message: '邀请函发送失败'+err,
  334. type: 'error'
  335. })
  336. })
  337. },
  338. copy(text) {
  339. navigator.clipboard.writeText(text).then(() => {
  340. this.$message.success('复制成功')
  341. }).catch(() => {
  342. this.$message.error('复制失败')
  343. })
  344. }
  345. }
  346. })
  347. </script>
  348. <template>
  349. <div class="main-box">
  350. <div class="head">
  351. <el-input v-model="searchWord" v-permission="'audience.search'" prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input" @input="search">
  352. <el-button v-if="searchWord" slot="append" icon="el-icon-delete" @click="searchWord='';search()" />
  353. </el-input>
  354. <el-select v-model="expo_id" v-permission="'audience.select'" class="select" placeholder="请选择参展名称" @change="search()">
  355. <el-option :value="0" label="全部展会" />
  356. <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
  357. </el-select>
  358. <el-button type="primary" icon="el-icon-download" @click="import_data.showImport = true">导入观众</el-button>
  359. </div>
  360. <div class="body">
  361. <el-table v-loading="loading" :data="userList" height="100%" class="table">
  362. <el-table-column type="expand">
  363. <template slot-scope="props">
  364. <el-form class="expand-form" :inline="true" label-position="left">
  365. <el-form-item v-for="(item,key) in props.row" :key="key" :label="getLabelName(key)+':'">
  366. <span>{{ item }}</span>
  367. </el-form-item>
  368. </el-form>
  369. </template>
  370. </el-table-column>
  371. <el-table-column
  372. label="姓名"
  373. prop="full_name"
  374. />
  375. <el-table-column
  376. label="参展名称"
  377. prop="expo_name"
  378. width="400"
  379. :show-overflow-tooltip="true"
  380. >
  381. <template slot-scope="props">
  382. <expo-popover placement="bottom" trigger="click" :expo-id="''+props.row.expo_id">
  383. <span style="cursor: pointer">{{ props.row.expo_name }}</span>
  384. </expo-popover>
  385. </template>
  386. </el-table-column>
  387. <el-table-column
  388. label="邮箱"
  389. prop="email"
  390. />
  391. <el-table-column
  392. label="区号"
  393. width="80"
  394. prop="mobile_country_code"
  395. />
  396. <el-table-column
  397. label="电话"
  398. prop="mobile"
  399. />
  400. <el-table-column
  401. label="操作"
  402. fixed="right"
  403. >
  404. <template slot-scope="scope">
  405. <span class="button" @click="openDialog(scope.row)">发送邀请函</span>
  406. </template>
  407. </el-table-column>
  408. </el-table>
  409. </div>
  410. <div class="foot">
  411. <el-pagination
  412. background
  413. :page-size="page_size"
  414. layout="total"
  415. :total="total"
  416. />
  417. <el-pagination
  418. v-permission="'audience.changePage'"
  419. background
  420. :page-size="page_size"
  421. layout="prev, pager, next"
  422. :total="total"
  423. @current-change="current_page=$event;getAudience()"
  424. />
  425. </div>
  426. <el-dialog
  427. :visible.sync="import_data.showImport"
  428. :append-to-body="true"
  429. title="导入观众"
  430. custom-class="import-dialog"
  431. @close="import_data.showImport=false"
  432. >
  433. <div class="dialog-body">
  434. <el-steps :active="import_data.step" align-center finish-status="success">
  435. <el-step title="下载模板" />
  436. <el-step title="填写数据" />
  437. <el-step title="导入数据" />
  438. </el-steps>
  439. <div v-if="import_data.step === 0" class="step-cont">
  440. <div class="info">选择需要导入的展会:</div>
  441. <el-select v-model="import_data.exhibitor_id" class="select" placeholder="请选择参展名称" @change="creatExcel(import_data.exhibitor_id)">
  442. <el-option :value="0" label="不选择展会" />
  443. <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
  444. </el-select>
  445. <div class="info">点击下载EXCEL模板:</div>
  446. <div>
  447. <el-button v-loading="loading" type="primary" @click="downloadTemplate()">下载模板</el-button>
  448. <span class="info">或</span>
  449. <el-button @click="import_data.step = 2">已有文件,直接导入</el-button>
  450. </div>
  451. </div>
  452. <div v-if="import_data.step === 1" class="step-cont">
  453. <img class="img" src="/static/image/import.webp">
  454. <div class="info">
  455. 使用Excel拷贝数据到模板中或使用您的管理系统导入数据至模板中。不想繁琐操作?使用
  456. <router-link to="/exhibitor">展会管理</router-link>
  457. 功能关联表单自动收集!
  458. </div>
  459. <el-button @click="import_data.step = 2">已完成,去导入</el-button>
  460. </div>
  461. <div v-if="import_data.step === 2" class="step-cont">
  462. <div class="info">选择需要导入的展会:</div>
  463. <el-select v-model="import_data.exhibitor_id" class="select" placeholder="请选择参展名称" @change="creatExcel(import_data.exhibitor_id)">
  464. <el-option :value="0" label="不选择展会" />
  465. <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
  466. </el-select>
  467. <div class="info">上传EXCEL文件:</div>
  468. <div class="upload">
  469. {{ import_data.file.name || '点击选择文件或者拖拽文件到这里' }}
  470. <input ref="file" accept=".xls,.xlsx" type="file" @change="uploadExcel"></input>
  471. </div>
  472. <div>
  473. <el-button v-permission="'audience.import'" v-loading="loading" type="primary" @click="importData">导入数据</el-button>
  474. <el-button @click="import_data.step = 0">下载其它展会模板</el-button>
  475. </div>
  476. </div>
  477. </div>
  478. </el-dialog>
  479. <el-dialog
  480. top="5vh"
  481. custom-class="invitation-dialog"
  482. :append-to-body="true"
  483. title="发送邀请函"
  484. :visible.sync="invitation_data.show"
  485. width="90%"
  486. @close="closeDialog()"
  487. >
  488. <div class="dialog-body">
  489. <div class="title">模板列表</div>
  490. <div class="title">邮件编辑器</div>
  491. <div class="title">观众信息(按住复制按钮可拖拽)</div>
  492. <div class="temp-list">
  493. <div
  494. v-for="(temp,index) in invitation_data.data"
  495. :key="temp.id"
  496. class="temp-item"
  497. :class="index===invitation_data.tempIndex?'active':''"
  498. @click="changeTemp(index)"
  499. >
  500. <img v-if="temp.pic" :src="ossUrl+temp.pic">
  501. </div>
  502. <div v-if="invitation_data.data.length===0" class="empty">
  503. 没有模板,
  504. <router-link to="/invitation/add" @click.native="closeDialog()">去新建</router-link>
  505. </div>
  506. </div>
  507. <div class="view">
  508. <div id="editor" />
  509. </div>
  510. <div class="var-list-cont">
  511. <div class="var-list">
  512. <div v-for="(item,key) in invitation_data.userSetting" class="var-item">
  513. <div class="title">{{ getLabelName(key) }}</div>
  514. <div>{{ item || '暂无数据' }}</div>
  515. <el-button-group class="button-list">
  516. <el-button draggable="true" @dragstart.native="dragStart($event,item)" type="primary" size="mini" icon="el-icon-document-copy" @click="copy(item)"></el-button>
  517. </el-button-group>
  518. </div>
  519. </div>
  520. </div>
  521. </div>
  522. <span slot="footer" class="dialog-footer">
  523. <el-button @click="closeDialog()">取 消</el-button>
  524. <el-button v-permission="'audience.sendInvite'" :disabled="invitation_data.is_sending" type="primary" @click="sendInvitation()">
  525. 发 送
  526. <span v-if="invitation_data.is_sending" class="el-icon-loading" />
  527. </el-button>
  528. </span>
  529. </el-dialog>
  530. </div>
  531. </template>
  532. <style scoped lang="scss">
  533. @use '@/styles/variables.scss' as *;
  534. .main-box{
  535. height: 100%;
  536. display: grid;
  537. grid-template-rows: auto 1fr auto;
  538. grid-gap: 24px;
  539. .head{
  540. display: flex;
  541. .input{
  542. width: 30%;
  543. margin-right: 12px;
  544. }
  545. .select{
  546. width: 20%;
  547. margin-right: auto;
  548. }
  549. }
  550. .body{
  551. height: 100%;
  552. position: relative;
  553. .table{
  554. width: 100%;
  555. height: 100%;
  556. position: absolute;
  557. top: 0;
  558. left: 0;
  559. .button{
  560. cursor: pointer;
  561. padding: 0 5px;
  562. color: $menuActiveText;
  563. &.del{
  564. color: #DC2626;
  565. }
  566. }
  567. .expand-form{
  568. display: grid;
  569. grid-template-columns: 1fr 1fr 1fr;
  570. .el-form-item{
  571. margin: 0;
  572. }
  573. }
  574. }
  575. }
  576. .foot{
  577. display: flex;
  578. justify-content: space-between;
  579. }
  580. }
  581. </style>
  582. <style lang="scss">
  583. .import-dialog{
  584. .dialog-body{
  585. .step-cont{
  586. gap: 16px;
  587. display: flex;
  588. flex-direction: column;
  589. align-items: center;
  590. padding: 36px 0;
  591. .info{
  592. margin: 8px;
  593. color: grey;
  594. }
  595. .upload{
  596. display: flex;
  597. align-items: center;
  598. justify-content: center;
  599. color: grey;
  600. font-size: 24px;
  601. border-radius: 8px;
  602. width: 80%;
  603. height: 240px;
  604. border: 3px dashed grey;
  605. position: relative;
  606. input {
  607. opacity: 0;
  608. position: absolute;
  609. width: 100%;
  610. height: 100%;
  611. }
  612. }
  613. .img{
  614. width: 100%;
  615. }
  616. }
  617. }
  618. }
  619. .invitation-dialog{
  620. .dialog-body{
  621. overflow: hidden;
  622. height: 68vh;
  623. display: grid;
  624. grid-template-columns: 240px 1fr 240px;
  625. grid-template-rows: auto 1fr;
  626. grid-gap: 4px 12px;
  627. .view{
  628. width: 100%;
  629. height: 100%;
  630. }
  631. .var-list-cont{
  632. border: 2px solid #eee;
  633. border-radius: 10px;
  634. width: 100%;
  635. height: 100%;
  636. position: relative;
  637. .var-list{
  638. position: absolute;
  639. top: 0;
  640. left: 0;
  641. width: 100%;
  642. height: 100%;
  643. overflow: hidden;
  644. overflow-y: auto;
  645. .var-item{
  646. position: relative;
  647. padding: 2px 6px;
  648. border-radius: 6px;
  649. border: 2px solid #409EFF00;
  650. border-bottom: 2px solid #00000008;
  651. .button-list{
  652. opacity: 0;
  653. position: absolute;
  654. right: 3px;
  655. top: 3px;
  656. }
  657. .title{
  658. font-weight: bold;
  659. font-size: 12px;
  660. }
  661. &:hover{
  662. border: 2px solid #409EFF;
  663. .button-list{
  664. opacity: 1;
  665. }
  666. }
  667. }
  668. }
  669. }
  670. .temp-list{
  671. border: 2px solid #eee;
  672. border-radius: 10px;
  673. padding: 6px;
  674. overflow: hidden;
  675. overflow-y: auto;
  676. width: 100%;
  677. height: 100%;
  678. .empty{
  679. height: 100%;
  680. display: flex;
  681. justify-content: center;
  682. align-items: center;
  683. }
  684. .temp-item{
  685. margin-bottom: 12px;
  686. width: 100%;
  687. border-radius: 6px;
  688. overflow: hidden;
  689. cursor: pointer;
  690. &.active,&:hover{
  691. box-shadow: 0 0 0 2px #FFFFFF,0 0 0 4px #409EFF ;
  692. }
  693. img{
  694. width: 100%;
  695. }
  696. }
  697. }
  698. }
  699. }
  700. </style>