zhoujump 1 hónapja
szülő
commit
4bcadf0462

+ 1 - 0
.env

@@ -1,5 +1,6 @@
 # just a flag
 ENV = 'production'
 VUE_APP_NAME = 'development'
+VUE_PAY_CENTER = 'https://pay.mall.com'
 # base api
 VUE_APP_BASE_API = ''

BIN
public/static/image/beian.png


BIN
public/static/image/icon/facebook.png


BIN
public/static/image/icon/qq-zone.png


BIN
public/static/image/icon/x.png


BIN
public/static/image/login2.webp


+ 52 - 2
src/App.vue

@@ -8,14 +8,22 @@
       :is-collapse="isCollapse"
       @changeCollapse="isCollapse = !isCollapse"
     />
+    <div :class="['packet-mask',showPacket?'show':'']">
+      <packetList class="packet-list" @close="showPacket=false" />
+    </div>
   </div>
 </template>
 
 <script>
 import i18n from '@/locales/i18n'
+import packetList from '@/views/components/packetList.vue'
 import router from '@/router'
+import { canIShow } from '@/permission'
 export default {
   name: 'App',
+  components: {
+    packetList
+  },
   data() {
     return {
       roles: [],
@@ -23,14 +31,30 @@ export default {
       menuRouter: [],
       menuActive: 0,
       breadcrumb: [],
-      isCollapse: false
+      isCollapse: false,
+      showPacket: false
     }
   },
   created() {
-    this.initStore()
     this.initGuards()
   },
+  mounted() {
+    this.initStore()
+    this.refreshUser()
+    console.log(this.$route)
+    this.$bus.$on('showPacket', (val) => {
+      this.showPacket = val
+    })
+  },
   methods: {
+    refreshUser() {
+      this.$store.dispatch('refreshToken').then((res) => {
+      }).catch(err => {
+        this.$router.push({
+          name: 'login'
+        })
+      })
+    },
     initStore() {
       window.addEventListener('beforeunload', () => {
         localStorage.setItem('store', JSON.stringify(this.$store.state))
@@ -101,6 +125,32 @@ export default {
    width: 100%;
    overflow: hidden;
  }
+ .packet-mask{
+   display: flex;
+   align-items: center;
+   justify-content: center;
+   transition-duration: 300ms;
+   z-index: 9999;
+   position: fixed;
+   left: 0;
+   top: 0;
+   width: 100%;
+   height: 100%;
+   background-color: #00000044;
+   pointer-events: none;
+   opacity: 0;
+   .packet-list{
+     transition-duration: 300ms;
+     transform: translateY(40px);
+   }
+   &.show{
+     pointer-events: auto;
+     opacity: 1;
+     .packet-list{
+       transform: translateY(0);
+     }
+   }
+ }
  textarea{
    font-family: inherit;
  }

+ 19 - 0
src/api/form.js

@@ -195,3 +195,22 @@ export function getMyFields(){
     method: 'get'
   })
 }
+/**
+ * 导入观众
+ * @param file
+ * @param expo_id
+ * @returns {*}
+ */
+export function importAudience(file,expo_id) {
+  return request({
+    url: '/api/form/upload-form',
+    method: 'post',
+    data: {
+      file,
+      expo_id
+    },
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}

+ 23 - 0
src/api/system.js

@@ -75,3 +75,26 @@ export function saveMailSetting(id,from_name,from_email,smtp_port,smtp_server,au
     }
   })
 }
+
+/**
+ * 获取版本列表
+ * @returns {AxiosPromise}
+ */
+export function getVerList (){
+  return request({
+    url: '/api/trade/ver-list',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取权益包列表
+ * @param key EXPOREG=预登记,EXPOREG_INVITATION=邀请函
+ * @returns {AxiosPromise}
+ */
+export function getPackList (key) {
+  return request({
+    url: '/api/trade/packet-list',
+    method: 'get'
+  })
+}

+ 24 - 8
src/layout/index.vue

@@ -8,13 +8,13 @@
         <el-menu :unique-opened="true" class="layout-menu-inner" :collapse="isCollapse" :default-active="menuActive+''">
           <template v-for="(route,index) in menuRouter">
             <template v-if="!route.meta.hidden">
-              <el-submenu :index="index+''" v-if="route.children">
+              <el-submenu :index="index+''" v-if="route.children" :key="route.path">
                 <template slot="title">
                   <i :class="[route.meta.icon,'icon']"></i>
                   <span slot="title">{{ route.meta.title }}</span>
                 </template>
                 <template v-for="(item,subIndex) in route.children">
-                  <el-menu-item v-show="!item.meta.hidden" :index="index+'-'+subIndex" @click="goto(item)" :key="item.path">
+                  <el-menu-item v-if="!item.meta.hidden" :index="index+'-'+subIndex" @click="goto(item)" :key="item.path">
                     <i :class="[item.meta.icon,'icon']"></i>
                     <span slot="title">{{ item.meta.title }}</span>
                   </el-menu-item>
@@ -41,6 +41,7 @@
             <router-link :to="item.path||'/'">{{ item.meta.title }}</router-link>
           </el-breadcrumb-item>
         </el-breadcrumb>
+        <div class="packet" @click="$bus.$emit('showPacket',true)">{{packet.ver_name}}</div>
         <div class="user-info">
           <div class="avatar">
             <img :src="this.user.avatar?this.user.avatar:'/static/image/avatar.webp'" alt="">
@@ -49,10 +50,6 @@
             <div class="nick-name">
               {{ this.user.nickname }}
             </div>
-            <div @click="goto({name:'exhibitorSetting'})" class="button">
-              <span class="el-icon-user"></span>
-              信息设置
-            </div>
             <div @click="goto({name:'systemSetting'})" class="button">
               <span class="el-icon-setting"></span>
               系统设置
@@ -75,6 +72,7 @@
 </template>
 
 <script>
+import { canIShow } from '@/permission'
 export default {
   name: 'Layout',
   props: [
@@ -87,15 +85,25 @@ export default {
   ],
   data() {
     return {
+      packet: {}
     }
   },
   computed: {
     user() { return this.$store.state.user.user }
   },
   mounted() {
-
+    this.getPacket()
   },
   methods: {
+    getPacket() {
+      let appList = this.user.app_list
+      console.log(appList)
+      appList.forEach(item => {
+        if (item.app_code === 'EXPOREG') {
+          this.packet = item
+        }
+      })
+    },
     logout() {
       this.$confirm('确定要退出吗?', {
         confirmButtonText: '确定',
@@ -193,13 +201,21 @@ export default {
         background: white;
         border-bottom: 1px solid #E5E7EB;
         height: 80px;
+        .packet{
+          cursor: pointer;
+          margin-left: auto;
+          font-size: 16px;
+          background-color: #409EFF;
+          padding: 4px 12px;
+          color: white;
+          border-radius: 8px;
+        }
         .user-info{
           position: relative;
           display: flex;
           justify-content: center;
           align-items: center;
           grid-gap: 16px;
-          margin-left: auto;
           .avatar{
             cursor: pointer;
             transition-duration: 300ms;

+ 4 - 4
src/main.js

@@ -5,7 +5,7 @@ import Cookies from 'js-cookie'
 import Element from 'element-ui'
 import VueHighlightJS from 'vue-highlightjs'
 // import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
-import './permission' // permission control
+import permission from '@/permission' // permission control
 // import 'font-awesome/css/font-awesome.min.css'
 import 'element-ui/lib/theme-chalk/index.css';
 import App from './App'
@@ -25,7 +25,7 @@ if (process.env.NODE_ENV === 'production') {
   const { mockXHR } = require('../mock')
   mockXHR()
 }
-
+Vue.directive('permission', permission)
 Vue.use(Element, {
   size: Cookies.get('size') || 'medium' // set element-ui default size
   // locale: enLang // 如果使用中文,无需设置,请删除
@@ -113,9 +113,9 @@ Vue.prototype.generateSlug = function(title) {
 let isInit = false
 Vue.prototype.$getIsInit = function() { return isInit }
 Vue.prototype.$setIsInit = function() { isInit = true }
-
 Vue.prototype.$bus = new Vue()
-new Vue({
+
+export const root = new Vue({
   el: '#app',
   store,
   router,

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 63 - 0
src/permission.js


+ 347 - 40
src/router/index.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import Router from 'vue-router'
 Vue.use(Router)
-
+import { canIShow } from '@/permission'
 import Layout from '@/layout'
 export const constantRoutes = [
   {
@@ -14,7 +14,17 @@ export const constantRoutes = [
         path: 'dashboard',
         component: () => import('@/views/dashboard/index'),
         name: 'Dashboard',
-        meta: { title: '首页看板', icon: 'el-icon-house', roles: 'dashboard' }
+        meta: {
+          title: '首页看板',
+          icon: 'el-icon-house',
+          roles: 'dashboard',
+          func: [
+            {
+              name: '头部概览',
+              roles: 'dashboard.head'
+            }
+          ]
+        }
       },
       {
         path: 'audience',
@@ -26,16 +36,24 @@ export const constantRoutes = [
           roles: 'audience',
           func: [
             {
-              name: '添加观众',
-              roles: 'audience.add'
+              name: '搜索观众',
+              roles: 'audience.search'
             },
             {
-              name: '导入',
-              roles: 'audience.import'
+              name: '按展会筛选',
+              roles: 'audience.select'
             },
             {
-              name: '导出',
-              roles: 'audience.export'
+              roles: 'audience.sendInvite',
+              name: '发送邀请函'
+            },
+            {
+              roles: 'audience.changePage',
+              name: '观众换页'
+            },
+            {
+              name: '观众导入',
+              roles: 'audience.import'
             }
           ]
         }
@@ -44,7 +62,11 @@ export const constantRoutes = [
         path: 'preRegister',
         component: () => import('@/views/preRegManage/index'),
         name: 'preRegManage',
-        meta: { title: '预登记表单', icon: 'el-icon-tickets', roles: 'preReg' },
+        meta: {
+          title: '预登记表单',
+          icon: 'el-icon-tickets',
+          roles: 'preReg'
+        },
         redirect: '/preRegister/list',
         children: [
           {
@@ -60,6 +82,26 @@ export const constantRoutes = [
                 {
                   roles: 'preReg.creat',
                   name: '创建表单'
+                },
+                {
+                  roles: 'preReg.search',
+                  name: '搜索表单'
+                },
+                {
+                  roles: 'preReg.handelEdit',
+                  name: '表单编辑'
+                },
+                {
+                  roles: 'preReg.handelDisable',
+                  name: '表单禁用'
+                },
+                {
+                  roles: 'preReg.handelDelete',
+                  name: '表单删除'
+                },
+                {
+                  roles: 'preReg.changePage',
+                  name: '表单换页'
                 }
               ]
             }
@@ -68,19 +110,72 @@ export const constantRoutes = [
             path: 'edit/:id',
             component: () => import('@/views/preRegManage/edit.vue'),
             name: 'preRegManagEdit',
-            meta: { title: '表单编辑', icon: 'el-icon-edit', hidden: true, roles: 'preReg.edit', collapse: true }
+            meta: {
+              title: '表单编辑',
+              icon: 'el-icon-edit',
+              hidden: true,
+              roles: 'preReg.edit',
+              collapse: true,
+              func: [
+                {
+                  roles: 'preReg.save',
+                  name: '表单保存'
+                },
+                {
+                  roles: 'preReg.copy',
+                  name: '表单复制'
+                },
+                {
+                  roles: 'preReg.editEdit',
+                  name: '表单内容编辑'
+                },
+                {
+                  roles: 'preReg.editName',
+                  name: '表单名称编辑'
+                },
+                {
+                  roles: 'preReg.editDesc',
+                  name: '表单描述编辑'
+                },
+                {
+                  roles: 'preReg.saveCompEdit',
+                  name: '保存组件'
+                }
+              ]
+            }
           },
           {
             path: 'add',
             component: () => import('@/views/preRegManage/edit'),
             name: 'preRegManagAdd',
-            meta: { title: '表单新增', icon: 'el-icon-document-add', roles: 'preReg.edit', collapse: true }
+            meta: {
+              title: '表单新增',
+              icon: 'el-icon-document-add',
+              roles: 'preReg.addNew',
+              collapse: true
+            }
           },
           {
             path: 'comp',
             component: () => import('@/views/preRegManage/compEdit.vue'),
             name: 'preRegManagcomp',
-            meta: { title: '我的组件', icon: 'el-icon-menu', roles: 'preReg.comp' }
+            meta: {
+              title: '我的组件',
+              icon: 'el-icon-menu',
+              roles: 'preReg.comp',
+              func: [
+                {
+                  roles: 'preReg.saveComp',
+                  name: '保存组件'
+                }, {
+                  roles: 'preReg.copyComp',
+                  name: '复制组件'
+                }, {
+                  roles: 'preReg.addComp',
+                  name: '新增组件'
+                }
+              ]
+            }
           }
         ]
       },
@@ -88,39 +183,86 @@ export const constantRoutes = [
         path: 'exhibitor',
         component: () => import('@/views/exhibitorManage/index'),
         name: 'ExhibitorManage',
-        meta: { title: '展会管理', icon: 'el-icon-office-building', roles: 'exhibitor' },
+        meta: {
+          title: '展会管理',
+          icon: 'el-icon-office-building',
+          roles: 'exhibitor'
+        },
         redirect: '/exhibitor/list',
         children: [
           {
             component: () => import('@/views/exhibitorManage/exhibitorList'),
             path: 'list',
             name: 'exhibitorManageList',
-            meta: { title: '展会管理', icon: 'el-icon-edit', roles: 'exhibitor', func: [
-              {
-                roles: 'exhibitor.add',
-                name: '添加展会'
-              },
-              {
-                roles: 'exhibitor.import',
-                name: '导入'
-              },
-              {
-                roles: 'exhibitor.export',
-                name: '导出'
-              }
-            ] }
+            meta: {
+              title: '展会管理',
+              icon: 'el-icon-edit',
+              roles: 'exhibitor.list',
+              func: [
+                {
+                  roles: 'exhibitor.addList',
+                  name: '添加展会'
+                },
+                {
+                  roles: 'exhibitor.search',
+                  name: '搜索展会'
+                },
+                {
+                  roles: 'exhibitor.handelView',
+                  name: '预览展会'
+                },
+                {
+                  roles: 'exhibitor.copyLink',
+                  name: '复制表单连接'
+                },
+                {
+                  roles: 'exhibitor.handelEdit',
+                  name: '编辑展会'
+                },
+                {
+                  roles: 'exhibitor.handelDisable',
+                  name: '禁用展会'
+                },
+                {
+                  roles: 'exhibitor.handelDelete',
+                  name: '删除展会'
+                },
+                {
+                  roles: 'exhibitor.handel',
+                  name: '展会翻页'
+                }
+              ] }
           },
           {
             component: () => import('@/views/exhibitorManage/exhibitorSetting'),
             name: 'exhibitorEdit',
             path: 'edit/:id',
-            meta: { title: '配置展会', icon: 'el-icon-setting', roles: 'exhibitor.setting', hidden: true }
+            meta: {
+              title: '配置展会',
+              icon: 'el-icon-setting',
+              roles: 'exhibitor.setting',
+              hidden: true,
+              func: [
+                {
+                  roles: 'exhibitor.save',
+                  name: '保存展会'
+                },
+                {
+                  roles: 'exhibitor.copyright',
+                  name: '是否显示底部信息开关'
+                }
+              ]
+            }
           },
           {
             component: () => import('@/views/exhibitorManage/exhibitorSetting'),
             name: 'exhibitorAdd',
             path: 'add',
-            meta: { title: '添加展会', icon: 'el-icon-document-add', roles: 'exhibitor.add' }
+            meta: {
+              title: '添加展会',
+              icon: 'el-icon-document-add',
+              roles: 'exhibitor.add'
+            }
           }
         ]
       },
@@ -128,25 +270,81 @@ export const constantRoutes = [
         path: 'invitation',
         component: () => import('@/views/invitationManage/index'),
         name: 'invitationManage',
-        meta: { title: '邀请函模板管理', icon: 'el-icon-files', roles: 'invitation' },
+        meta: {
+          title: '邀请函模板管理',
+          icon: 'el-icon-files',
+          roles: 'invitation'
+        },
         redirect: '/invitation/list',
         children: [{
           path: 'list',
           component: () => import('@/views/invitationManage/list'),
           name: 'invitationManageList',
-          meta: { title: '邀请函模板管理', icon: 'el-icon-edit', roles: 'invitation',collapse: false }
+          meta: {
+            title: '邀请函模板管理',
+            icon: 'el-icon-edit',
+            roles: 'invitation.index',
+            collapse: false,
+            func: [
+              {
+                roles: 'invitation.addList',
+                name: '添加邀请函模板'
+              },
+              {
+                roles: 'invitation.goto',
+                name: '点击进入模板'
+              },
+              {
+                roles: 'invitation.changePage',
+                name: '邀请函模板翻页'
+              }
+            ]
+          }
         },
         {
           path: 'add',
           component: () => import('@/views/invitationManage/edit'),
           name: 'invitationAdd',
-          meta: { title: '邀请函模板新增', icon: 'el-icon-document-add', roles: 'invitation.add',collapse: true }
+          meta: {
+            title: '邀请函模板新增',
+            icon: 'el-icon-document-add',
+            roles: 'invitation.add',
+            collapse: true
+          }
         },
         {
           path: 'edit/:id',
           component: () => import('@/views/invitationManage/edit'),
           name: 'invitationEdit',
-          meta: { title: '邀请函模板编辑', icon: 'el-icon-edit', hidden: true, roles: 'invitation.edit',collapse: true }
+          meta: {
+            title: '邀请函模板编辑',
+            icon: 'el-icon-edit',
+            hidden: true,
+            roles: 'invitation.edit',
+            collapse: true,
+            func: [
+              {
+                roles: 'invitation.save',
+                name: '修改邀请函模板'
+              },
+              {
+                roles: 'invitation.desc',
+                name: '修改邀请函描述'
+              },
+              {
+                roles: 'invitation.rename',
+                name: '重命名邀请函模板'
+              },
+              {
+                roles: 'invitation.select',
+                name: '选择展会'
+              },
+              {
+                roles: 'invitation.editor',
+                name: '允许操作编辑器'
+              }
+            ]
+          }
         }
         ]
       },
@@ -154,7 +352,11 @@ export const constantRoutes = [
         path: 'setting',
         component: () => import('@/views/setting/index'),
         name: 'setting',
-        meta: { title: '信息与配置', icon: 'el-icon-setting', roles: 'setting' },
+        meta: {
+          title: '信息与配置',
+          icon: 'el-icon-setting',
+          roles: 'setting'
+        },
         redirect: '/setting/exhibitor',
         children: [
           {
@@ -169,6 +371,18 @@ export const constantRoutes = [
                 {
                   name: '添加账号',
                   roles: 'setting.account.add'
+                },
+                {
+                  name: '操作账号',
+                  roles: 'setting.account.handel'
+                },
+                {
+                  name: '搜索账号',
+                  roles: 'setting.account.search'
+                },
+                {
+                  name: '账号翻页',
+                  roles: 'setting.account.changePage'
                 }
               ]
             }
@@ -185,6 +399,25 @@ export const constantRoutes = [
                 {
                   name: '添加角色',
                   roles: 'setting.roles.add'
+                }, {
+                  name: '操作角色',
+                  roles: 'setting.roles.handel'
+                },
+                {
+                  name: '搜索角色',
+                  roles: 'setting.roles.search'
+                },
+                {
+                  name: '角色翻页',
+                  roles: 'setting.roles.changePage'
+                },
+                {
+                  name: '角色权限配置',
+                  roles: 'setting.roles.permission'
+                },
+                {
+                  name: '角色权限保存',
+                  roles: 'setting.roles.save'
                 }
               ]
             }
@@ -193,7 +426,36 @@ export const constantRoutes = [
             path: 'system',
             component: () => import('@/views/setting/systemSetting'),
             name: 'systemSetting',
-            meta: { title: '系统信息配置', icon: 'el-icon-setting', roles: 'setting.system' }
+            meta: {
+              title: '系统信息配置',
+              icon: 'el-icon-setting',
+              roles: 'setting.system',
+              func: [
+                {
+                  name: '修改发件邮箱地址',
+                  roles: 'setting.system.sentEmailAddress'
+                },
+                {
+                  name: '修改发件邮箱密码/授权码',
+                  roles: 'setting.system.sentEmailPassword'
+                },
+                {
+                  name: '修改发件邮箱服务器地址',
+                  roles: 'setting.system.sentEmailServer'
+                },
+                {
+                  name: '修改发件邮箱SMTP端口号',
+                  roles: 'setting.system.sentEmailPort'
+                },
+                {
+                  name: '修改发件邮箱SSL加密',
+                  roles: 'setting.system.sentEmailSSL'
+                }, {
+                  name: '保存设置',
+                  roles: 'setting.system.save'
+                }
+              ]
+            }
           }
         ]
       },
@@ -201,27 +463,61 @@ export const constantRoutes = [
         path: '404',
         component: () => import('@/views/errorPage/404'),
         name: '404',
-        meta: { title: '页面不存在哦', icon: 'el-icon-delete-location', hidden: true }
+        meta: {
+          title: '页面不存在哦',
+          icon: 'el-icon-delete-location',
+          hidden: true,
+          roles: '404'
+        }
+      },
+      {
+        path: '401',
+        component: () => import('@/views/errorPage/401'),
+        name: '401',
+        meta: {
+          title: '页面无权限哦',
+          icon: 'el-icon-delete-location',
+          hidden: true,
+          roles: '401'
+        }
       }
     ]
   },
   {
-    path: '/login-new',
+    path: '/login',
     name: 'login',
     component: () => import('@/views/login/index'),
-    hidden: true
+    hidden: true,
+    meta: {
+      title: '登录',
+      icon: 'el-icon-user',
+      hidden: true,
+      roles: 'login'
+    }
   },
   {
     path: '/user/form/:url',
     name: 'userForm',
     component: () => import('@/views/user/form.vue'),
-    hidden: true
+    hidden: true,
+    meta: {
+      title: '用户表单',
+      icon: 'el-icon-user',
+      hidden: true,
+      roles: 'user.form'
+    }
   },
   {
     path: '/user/register',
     name: 'userRegister',
     component: () => import('@/views/user/register.vue'),
-    hidden: true
+    hidden: true,
+    meta: {
+      title: '用户注册',
+      icon: 'el-icon-user',
+      hidden: true,
+      roles: 'user.register'
+    }
   },
   { path: '*', redirect: '/404', hidden: true }
 ]
@@ -231,4 +527,15 @@ const createRouter = () => new Router({
   routes: constantRoutes
 })
 const router = createRouter()
+router.beforeEach((to, from, next) => {
+  canIShow(to.meta.roles).then((res) => {
+    if (res) {
+      next()
+    } else {
+      next({
+        name: '401'
+      })
+    }
+  })
+})
 export default router

+ 28 - 2
src/store/modules/user.js

@@ -18,6 +18,28 @@ export default {
     }
   },
   actions: {
+    refreshToken({ commit }) {
+      return new Promise((resolve, reject) => {
+        getInfo().then(response => {
+          let isAdmin = false
+          response.data.app_list.forEach(app => {
+            if (app.app_code === 'EXPOREG') {
+              isAdmin = true
+            }
+          })
+          commit('SET_USER', {
+            username: response.data.user_name,
+            nickname: response.data.nick_name,
+            avatar: response.data.avatar,
+            email: response.data.email,
+            phone: response.data.phone,
+            isAdmin: isAdmin,
+            app_list: response.data.app_list,
+            permission: response.data.permission
+          })
+        })
+      })
+    },
     tokenLogin({ commit }, playload) {
       return new Promise((resolve, reject) => {
         commit('SET_TOKEN', playload.token)
@@ -34,7 +56,9 @@ export default {
             avatar: response.data.avatar,
             email: response.data.email,
             phone: response.data.phone,
-            isAdmin: isAdmin
+            isAdmin: isAdmin,
+            app_list: response.data.app_list,
+            permission: response.data.permission
           })
           if (!isAdmin) {
             reject('您没有权限访问!')
@@ -73,7 +97,9 @@ export default {
               avatar: response.data.avatar,
               email: response.data.email,
               phone: response.data.phone,
-              isAdmin: isAdmin
+              isAdmin: isAdmin,
+              app_list: response.data.app_list,
+              permission: response.data.permission
             })
             if (payload.savePassword) {
               const savedAccount = {

+ 216 - 12
src/views/audienceManage/index.vue

@@ -8,7 +8,8 @@ import 'hugerte/skins/ui/oxide/skin.js'
 import 'hugerte/skins/ui/oxide/content.js'
 import 'hugerte/skins/content/default/content.js'
 
-import { getAudienceList, sentInvitation } from '@/api/form'
+import XLSX from 'xlsx'
+import { getAudienceList, sentInvitation, getFormList, getFormInfo, importAudience } from '@/api/form'
 import { getExpoList, getMyExpoInfo } from '@/api/expo'
 import { getTemplateList } from '@/api/template'
 import expoPopover from '@/views/components/expoPopover.vue'
@@ -30,6 +31,13 @@ export default Vue.extend({
         last_page: 1,
         is_sending: false
       },
+      import_data: {
+        showImport: false,
+        step: 0,
+        exhibitor_id: '',
+        xlsx: '',
+        file: ''
+      },
       loading: false,
       searchTimer: null,
       searchWord: '',
@@ -48,6 +56,103 @@ export default Vue.extend({
     this.getInvitationList()
   },
   methods: {
+    creatExcel(exhibitor_id) {
+      if (exhibitor_id !== '') {
+        if (this.loading) { return }
+        this.loading = true
+        if (exhibitor_id === 0) {
+          console.log(exhibitor_id)
+          getFormList(1, 1)
+            .then(res => {
+              if (res.data.data.length > 0) {
+                return getFormInfo(res.data.data[0].id)
+              } else {
+                this.$notify({
+                  title: '出错了',
+                  message: '请先创建至少一个表单模板',
+                  type: 'warning'
+                })
+                this.loading = false
+                return
+              }
+            })
+            .then(res2 => {
+              const workBook = XLSX.utils.book_new()
+              const lineData = []
+              res2.data.fields.forEach(field => {
+                lineData.push(field.field_label)
+              })
+              const data = [lineData]
+              const workSheet = XLSX.utils.aoa_to_sheet(data)
+              XLSX.utils.book_append_sheet(workBook, workSheet, '观众表')
+              this.import_data.xlsx = workBook
+              this.loading = false
+            }).catch(err => {
+              this.loading = false
+            })
+        } else {
+          getMyExpoInfo(exhibitor_id).then(res => {
+            if (res.data.form_template_id) {
+              return getFormInfo(res.data.form_template_id)
+            } else {
+              this.$notify({
+                title: '出错了',
+                message: '这个展会没有绑定表单',
+                type: 'error'
+              })
+              this.loading = false
+              return
+            }
+          }).then(res2 => {
+            const workBook = XLSX.utils.book_new()
+            const lineData = []
+            res2.data.fields.forEach(field => {
+              lineData.push(field.field_label)
+            })
+            const data = [lineData]
+            const workSheet = XLSX.utils.aoa_to_sheet(data)
+            XLSX.utils.book_append_sheet(workBook, workSheet, '观众表')
+            this.import_data.xlsx = workBook
+            this.loading = false
+          }).catch(err => {
+            this.loading = false
+          })
+        }
+      }
+    },
+    downloadTemplate() {
+      if (this.import_data.xlsx) {
+        this.import_data.step = 1
+        XLSX.writeFile(this.import_data.xlsx, '观众表.xlsx')
+      }
+    },
+    uploadExcel(event) {
+      this.import_data.file = event.target.files[0]
+      console.log(this.import_data.file)
+    },
+    importData() {
+      if (this.loading) { return }
+      this.loading = true
+      if (this.import_data.file && this.import_data.exhibitor_id !== '') {
+        importAudience(this.import_data.file, this.import_data.exhibitor_id).then(res => {
+          this.$notify({
+            title: '导入完成',
+            message: '添加了' + res.data.update_count + '条数据',
+            type: 'success'
+          })
+          this.loading = false
+        }).catch(err => {
+          this.loading = false
+        })
+      } else {
+        this.$notify({
+          title: '出错了',
+          message: '请选择文件与展会信息',
+          type: 'warning'
+        })
+        this.loading = false
+      }
+    },
     getLabelName(key) {
       const name = {
         address: '地址',
@@ -92,7 +197,7 @@ export default Vue.extend({
     getInvitationList() {
       getTemplateList(++this.invitation_data.page, 10).then(res => {
         console.log(res)
-        this.invitation_data.data = res.data.data
+        this.invitation_data.data = res.data.data || []
         this.last_page = res.data.last_page
         this.page = res.data.current_page
       }).catch(err => {
@@ -198,16 +303,14 @@ export default Vue.extend({
 <template>
   <div class="main-box">
     <div class="head">
-      <el-input v-model="searchWord" prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input" @input="search">
+      <el-input v-model="searchWord" v-permission="'audience.search'" prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input" @input="search">
         <el-button v-if="searchWord" slot="append" icon="el-icon-delete" @click="searchWord='';search()" />
       </el-input>
-      <el-select v-model="expo_id" class="select" placeholder="请选择参展名称" @change="search()">
+      <el-select v-model="expo_id" v-permission="'audience.select'" class="select" placeholder="请选择参展名称" @change="search()">
         <el-option :value="0" label="全部展会" />
         <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
       </el-select>
-      <el-button icon="el-icon-plus" type="primary">添加观众</el-button>
-      <el-button icon="el-icon-upload2">导入</el-button>
-      <el-button icon="el-icon-download">导出</el-button>
+      <el-button v-permission="'audience.import'" type="primary" icon="el-icon-download" @click="import_data.showImport = true">导入观众</el-button>
     </div>
     <div class="body">
       <el-table v-loading="loading" :data="userList" height="100%" class="table">
@@ -267,6 +370,7 @@ export default Vue.extend({
         :total="total"
       />
       <el-pagination
+        v-permission="'audience.changePage'"
         background
         :page-size="page_size"
         layout="prev, pager, next"
@@ -275,6 +379,59 @@ export default Vue.extend({
       />
     </div>
     <el-dialog
+      :visible.sync="import_data.showImport"
+      :append-to-body="true"
+      title="导入观众"
+      custom-class="import-dialog"
+      @close="import_data.showImport=false"
+    >
+      <div class="dialog-body">
+        <el-steps :active="import_data.step" align-center finish-status="success">
+          <el-step title="下载模板" />
+          <el-step title="填写数据" />
+          <el-step title="导入数据" />
+        </el-steps>
+        <div v-if="import_data.step === 0" class="step-cont">
+          <div class="info">选择需要导入的展会:</div>
+          <el-select v-model="import_data.exhibitor_id" class="select" placeholder="请选择参展名称" @change="creatExcel(import_data.exhibitor_id)">
+            <el-option :value="0" label="不选择展会" />
+            <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
+          </el-select>
+          <div class="info">点击下载EXCEL模板:</div>
+          <div>
+            <el-button v-loading="loading" type="primary" @click="downloadTemplate()">下载模板</el-button>
+            <span class="info">或</span>
+            <el-button @click="import_data.step = 2">已有文件,直接导入</el-button>
+          </div>
+        </div>
+        <div v-if="import_data.step === 1" class="step-cont">
+          <img class="img" src="/static/image/import.webp">
+          <div class="info">
+            使用Excel拷贝数据到模板中或使用您的管理系统导入数据至模板中。不想繁琐操作?使用
+            <router-link to="/exhibitor">展会管理</router-link>
+            功能关联表单自动收集!
+          </div>
+          <el-button @click="import_data.step = 2">已完成,去导入</el-button>
+        </div>
+        <div v-if="import_data.step === 2" class="step-cont">
+          <div class="info">选择需要导入的展会:</div>
+          <el-select v-model="import_data.exhibitor_id" class="select" placeholder="请选择参展名称" @change="creatExcel(import_data.exhibitor_id)">
+            <el-option :value="0" label="不选择展会" />
+            <el-option v-for="item in expoList" :value="item.id" :label="item.expo_name" />
+          </el-select>
+          <div class="info">上传EXCEL文件:</div>
+          <div class="upload">
+            {{ import_data.file.name || '点击选择文件或者拖拽文件到这里' }}
+            <input ref="file" accept=".xls,.xlsx" type="file" @change="uploadExcel"></input>
+          </div>
+          <div>
+            <el-button v-loading="loading" type="primary" @click="importData">导入数据</el-button>
+            <el-button @click="import_data.step = 0">下载其它展会模板</el-button>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+    <el-dialog
       top="5vh"
       custom-class="invitation-dialog"
       :append-to-body="true"
@@ -297,17 +454,21 @@ export default Vue.extend({
           >
             <img v-if="temp.pic" :src="ossUrl+temp.pic">
           </div>
+          <div v-if="invitation_data.data.length===0" class="empty">
+            没有模板,
+            <router-link to="/invitation/add" @click.native="closeDialog()">去新建</router-link>
+          </div>
         </div>
         <div class="view">
           <div id="editor" />
         </div>
         <div class="var-list-cont">
           <div class="var-list">
-            <div class="var-item" v-for="(item,key) in invitation_data.userSetting">
-              <div class="title">{{getLabelName(key)}}</div>
-              <div>{{item || '暂无数据'}}</div>
+            <div v-for="(item,key) in invitation_data.userSetting" class="var-item">
+              <div class="title">{{ getLabelName(key) }}</div>
+              <div>{{ item || '暂无数据' }}</div>
               <el-button-group class="button-list">
-                <el-button @click="copy(item)" type="primary" size="mini" icon="el-icon-document-copy">复制</el-button>
+                <el-button type="primary" size="mini" icon="el-icon-document-copy" @click="copy(item)">复制</el-button>
               </el-button-group>
             </div>
           </div>
@@ -315,7 +476,7 @@ export default Vue.extend({
       </div>
       <span slot="footer" class="dialog-footer">
         <el-button @click="closeDialog()">取 消</el-button>
-        <el-button :disabled="invitation_data.is_sending" type="primary" @click="sendInvitation()">
+        <el-button v-permission="'audience.sendInvite'" :disabled="invitation_data.is_sending" type="primary" @click="sendInvitation()">
           发 送
           <span v-if="invitation_data.is_sending" class="el-icon-loading" />
         </el-button>
@@ -375,11 +536,48 @@ export default Vue.extend({
   }
 </style>
 <style lang="scss">
+.import-dialog{
+  .dialog-body{
+    .step-cont{
+      gap: 16px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      padding: 36px 0;
+      .info{
+        margin: 8px;
+        color: grey;
+      }
+      .upload{
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: grey;
+        font-size: 24px;
+        border-radius: 8px;
+        width: 80%;
+        height: 240px;
+        border: 3px dashed grey;
+        position: relative;
+        input {
+          opacity: 0;
+          position: absolute;
+          width: 100%;
+          height: 100%;
+        }
+      }
+      .img{
+        width: 100%;
+      }
+    }
+  }
+}
 .invitation-dialog{
   .dialog-body{
     height: 68vh;
     display: grid;
     grid-template-columns: 240px 1fr 360px;
+    grid-template-rows: auto 1fr;
     grid-gap: 4px 12px;
     .view{
       width: 100%;
@@ -432,6 +630,12 @@ export default Vue.extend({
       overflow-y: auto;
       width: 100%;
       height: 100%;
+      .empty{
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
       .temp-item{
         margin-bottom: 12px;
         width: 100%;

+ 1 - 1
src/views/components/expoPopover.vue

@@ -39,7 +39,7 @@ export default Vue.extend({
     <div class="expo-info">
       <div class="cover loading">
         <img v-if="popover_data.images" :src="ossUrl+popover_data.images[0]">
-        <div v-if="popover_data.form_template_id && popover_data.urla" @click="copyUrl()" class="button">复制表单地址</div>
+        <div v-permission="'exhibitor.copyLink'" v-if="popover_data.form_template_id && popover_data.urla" @click="copyUrl()" class="button">复制表单地址</div>
       </div>
       <div class="info-body">
         <div class="avatar-name">

+ 475 - 0
src/views/components/packetList.vue

@@ -0,0 +1,475 @@
+<script lang="ts">
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: 'PacketList',
+  data() {
+    return {
+      cycle: 'month',
+      showDetail: false,
+      type: 'expoReg'
+    }
+  },
+  methods: {
+    close() {
+      this.$emit('close')
+    },
+    changeCycle(cycle) {
+      this.cycle = cycle
+    },
+    changeType(type) {
+      this.type = type
+    },
+    changeDetail() {
+      this.showDetail = !this.showDetail
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="packet-list">
+    <div class="close el-icon-close" @click="close" />
+    <div class="tab-list">
+      <div class="cycle-tab">
+        <div :class="['cycle-item',cycle==='month'?'active':'']" @click="changeCycle('month')">按月支付</div>
+        <div :class="['cycle-item',cycle==='year'?'active':'']" @click="changeCycle('year')">按年支付</div>
+      </div>
+      <div class="hr"></div>
+      <div class="cycle-tab">
+        <div :class="['cycle-item',type==='expoReg'?'active':'']" @click="changeType('expoReg')">展会预登记</div>
+        <div :class="['cycle-item',type==='invitation'?'active':'']" @click="changeType('invitation')">签证邀请函</div>
+      </div>
+    </div>
+    <div class="packet-outer">
+      <div :class="['packet',showDetail?'':'hide']">
+        <div class="list-head list">
+          <div class="detail" />
+          <div class="head">
+            <div class="name">标准版</div>
+            <div class="info">快速轻松地创建高转化率的落地页</div>
+          </div>
+          <div class="head">
+            <div class="name">专业版</div>
+            <div class="info">最受欢迎</div>
+          </div>
+          <div class="head">
+            <div class="name">定制版</div>
+            <div class="info">根据您组织的独特结构、工作流程和客户需求定制方案</div>
+          </div>
+        </div>
+        <div class="list-price list">
+          <div class="detail" />
+          <div class="price-cont">
+            <div class="price">
+              $<span>39</span>/月
+            </div>
+            <div class="button">立即订购</div>
+          </div>
+          <div class="price-cont">
+            <div class="price">
+              $<span>59</span>/月
+            </div>
+            <div class="button">立即订购</div>
+          </div>
+          <div class="price-cont">
+            <div class="price">联系我们获取定制价格</div>
+            <div class="button">立即联系</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="title">功能介绍</div>
+          </div>
+          <div class="func-cont">
+            <div class="title">核心功能:</div>
+          </div>
+          <div class="func-cont">
+            <div class="title">核心功能:</div>
+          </div>
+          <div class="func-cont">
+            <div class="title">核心功能:</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">快速构建高转化率、移动响应式落地页,提升潜在客户、注册量和销售额,无需开发人员。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">5个预登记落地页</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">无限预登记落地页</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">无限预登记落地页</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">可选3个模板</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">可选模板不限</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">可选模板不限</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">美页易搭不会限制您的增长——每个方案都包含无限流量和转化次数。每次访客完成您的目标(例如,在落地页、网站、弹出窗口或提醒栏上点击号召性用语或提交潜在客户表单)时,都会计为一次转化。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">无限流量和潜在客户</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">无限流量和潜在客户</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">无限流量和潜在客户</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">连接1个自定义域名</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">连接3个自定义域名</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">更多自定义域名</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">标准集成</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">高级集成</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">高级集成</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">社媒分享</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">社媒分享</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">社媒分享</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">UTM追踪</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">UTM追踪</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">UTM追踪</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">1个用户</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">2个用户</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">定制</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">AI翻译功能:❌</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">AI翻译功能:✔️</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">AI翻译功能:✔️</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">您可以将美页易搭落地页发布到任何自定义域名或 URL,并与您现有的广告系列或 URL 结构无缝集成。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">去除易搭标识:❌</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">去除易搭标识:✔️</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">去除易搭标识:✔️</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">识别您的匿名流量——他们是谁、他们来自哪里,以及他们如何与您的网站和落地页互动,这样您就可以了解哪些人访问了您的页面但尚未转化。使用 Audience Insights,您可以定制您的信息传递方式、检查广告定位,并更好地了解您的受众。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">受众洞察:❌</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">受众洞察:✔️</div>
+          </div>
+          <div class="func-cont">
+            <div class="text">受众洞察:✔️</div>
+          </div>
+        </div>
+        <div class="list-func list">
+          <div class="detail" >
+            <div class="text">如果您是服务于多个客户的代理机构或顾问,美页易搭可让您轻松通过单个帐户管理多个品牌。您可以对广告系列进行分组,授予客户子帐户访问权限,并在帐户之间复制落地页、网站、弹窗和警报栏。</div>
+          </div>
+          <div class="func-cont">
+            <div class="text"></div>
+          </div>
+          <div class="func-cont">
+            <div class="text"></div>
+          </div>
+          <div class="func-cont">
+            <div class="text">添加客户子账户</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="button-cont">
+      <div @click="changeDetail" class="button">{{showDetail?'显示更少':'显示更多'}}</div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .packet-list{
+    position: relative;
+    padding: 16px;
+    width: 90%;
+    height: 90%;
+    background-color: white;
+    border-radius: 8px;
+    grid-gap: 16px;
+    display: grid;
+    grid-template-rows: auto 1fr auto;
+    .tab-list{
+      gap: 24px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      .hr{
+        width: 2px;
+        height: 60%;
+        background-color: lightgray;
+      }
+    }
+    .button-cont{
+      display: flex;
+      justify-content: center;
+      .button{
+        user-select: none;
+        color: #2563EB;
+        cursor: pointer;
+        transition-duration: 300ms;
+        border-radius: 24px;
+        border: 1px solid #2563EB;
+        outline: 2px solid #2563EB00;
+        padding: 8px 16px;
+        &:hover{
+          outline: 2px solid #2563EBFF;
+        }
+      }
+    }
+    .packet-outer{
+      position: relative;
+      .packet{
+        height: 100%;
+        width: 100%;
+        left: 0;
+        top: 0;
+        overflow: hidden;
+        overflow-y: auto;
+        position: absolute;
+        .list-head{
+          z-index: 1;
+          position: sticky;
+          top: 0;
+          background-color: white;
+          .detail{
+
+          }
+        }
+        &.hide{
+          .list{
+            grid-template-columns: 0fr 340px 340px 340px;
+            .detail{
+              grid-template-rows: 0fr;
+              grid-template-columns: 0fr;
+            }
+          }
+        }
+        .list{
+          transition-duration: 300ms;
+          margin: auto;
+          display: grid;
+          gap: 16px;
+          justify-content: center;
+          grid-template-columns: 1fr 340px 340px 340px;
+          &:last-child{
+            .func-cont{
+              border-bottom: 2px solid #E3E4E6;
+              border-radius: 0 0 16px 16px;
+            }
+          }
+          &.list-func:hover{
+            div{
+              background-color: #f8f8f8;
+            }
+          }
+          .detail{
+            transition-duration: 300ms;
+            display: grid;
+            grid-template-rows: 1fr;
+            grid-template-columns: 1fr;
+            overflow: hidden;
+            .title{
+              width: calc(90vw - 1100px);
+              min-height: 0;
+              font-weight: bold;
+              text-align: center;
+            }
+            .text{
+              width: calc(90vw - 1100px);
+              min-height: 0;
+              padding: 16px;
+              font-size: 16px;
+              border-bottom: 1px solid #E3E4E6;
+            }
+          }
+          .func-cont{
+            padding: 0 16px;
+            border-left: 2px solid #E3E4E6;
+            border-right: 2px solid #E3E4E6;
+            .text{
+              position: sticky;
+              top: 136px;
+              font-size: 16px;
+            }
+            .title{
+              font-weight: bold;
+            }
+          }
+          .price-cont{
+            display: flex;
+            flex-direction: column;
+            justify-content: flex-end;
+            padding: 16px;
+            padding-top: 8px;
+            border-left: 2px solid #E3E4E6;
+            border-right: 2px solid #E3E4E6;
+            .button{
+              font-weight: bold;
+              border-radius: 4px;
+              border: 1px solid black;
+              padding: 8px;
+              text-align: center;
+              transition-duration: 300ms;
+              &:hover{
+                cursor: pointer;
+                border: 1px solid #2563EB;
+                background-color: #2563EB;
+                color: white;
+              }
+            }
+            .price{
+              margin-bottom: 8px;
+              text-align: center;
+              span{
+                font-size: 48px;
+                font-weight: bold;
+              }
+            }
+          }
+          .head{
+            border: 2px solid #E3E4E6;
+            border-radius: 16px 16px 0 0;
+            padding: 16px;
+            text-align: center;
+            background-color: #F2F5FA;
+            .name{
+              font-size: 24px;
+              font-weight: bold;
+            }
+            .info{
+              margin-top: 8px;
+              color: #7E7E7E;
+              font-size: 16px;
+            }
+          }
+        }
+      }
+
+    }
+    .cycle-tab{
+      gap: 16px;
+      display: flex;
+      justify-content: center;
+      .cycle-item{
+        color: #2563EB;
+        cursor: pointer;
+        transition-duration: 300ms;
+        border-radius: 24px;
+        border: 1px solid #2563EB;
+        outline: 2px solid #2563EB00;
+        padding: 8px 16px;
+        &:hover{
+          outline: 2px solid #2563EBFF;
+        }
+        &.active{
+          font-weight: bold;
+          background-color: #2563EB;
+          color: white;
+        }
+      }
+    }
+    .close{
+      font-size: 24px;
+      right: 8px;
+      top: 8px;
+      position: absolute;
+      padding: 8px;
+      cursor: pointer;
+      transition-duration: 300ms;
+      &:hover{
+        border-radius: 4px;
+        background-color: #ededed;
+      }
+    }
+  }
+</style>

+ 1 - 1
src/views/dashboard/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="main-box">
     <div class="scroll-box">
-      <div class="main-card">
+      <div v-permission="'dashboard.head'" class="main-card">
         <div class="hello-text">{{ getTimeWord() }},{{ user.nickname }}!</div>
         <div class="card-content">
           <div class="expo-text">

+ 44 - 0
src/views/errorPage/401.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="main-box">
+    <div class="text">权限不足,联系管理员开通把!</div>
+    <img class="image" src="/static/image/401.webp"/>
+    <el-button @click="goHome">返回首页</el-button>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "404",
+  methods: {
+    goHome() {
+      this.$router.push({
+        name: 'Dashboard'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .main-box{
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    .text{
+      display: flex;
+      z-index: 1;
+      color: grey;
+    }
+    .image{
+      //margin-top: -8%;
+      width: 32%;
+      image{
+        aspect-ratio: 219/182;
+        width: 100%;
+      }
+    }
+  }
+</style>

+ 7 - 1
src/views/errorPage/404.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="main-box">
+    <div class="text">页面不存在</div>
     <img class="image" src="/static/image/404.webp"/>
     <el-button @click="goHome">返回首页</el-button>
   </div>
@@ -26,8 +27,13 @@ export default {
     flex-direction: column;
     justify-content: center;
     align-items: center;
+    .text{
+      display: flex;
+      z-index: 1;
+      color: grey;
+    }
     .image{
-      margin-top: -8%;
+      //margin-top: -8%;
       width: 32%;
       image{
         aspect-ratio: 219/182;

+ 7 - 8
src/views/exhibitorManage/exhibitorList.vue

@@ -102,12 +102,10 @@ export default Vue.extend({
 <template>
   <div class="main-box">
     <div class="head">
-      <el-input v-model="searchWord" prefix-icon="el-icon-search" placeholder="搜索展商名称" class="input" @input="search">
+      <el-input v-permission="'exhibitor.search'" v-model="searchWord" prefix-icon="el-icon-search" placeholder="搜索展商名称" class="input" @input="search">
         <el-button v-if="searchWord" slot="append" icon="el-icon-delete" @click="searchWord='';search()" />
       </el-input>
-      <el-button icon="el-icon-plus" type="primary" @click="add">添加展商</el-button>
-      <el-button icon="el-icon-upload2">导入</el-button>
-      <el-button icon="el-icon-download">导出</el-button>
+      <el-button v-permission="'exhibitor.addList'" icon="el-icon-plus" type="primary" @click="add">添加展商</el-button>
     </div>
     <div class="body">
       <el-table v-loading="loading" :data="expoList" height="100%" class="table">
@@ -177,11 +175,11 @@ export default Vue.extend({
         >
           <template slot-scope="scope">
             <expo-popover placement="left" trigger="click" :expo-id="''+scope.row.id">
-              <span class="button">预览</span>
+              <span v-permission="'exhibitor.handelView'" class="button">预览</span>
             </expo-popover>
-            <span class="button" @click="edit(scope.row)">编辑</span>
-            <span class="button" @click="setStatus(scope.row)">{{scope.row.status?'启用':'禁用'}}</span>
-            <span class="button del" @click="del(scope.row)">删除</span>
+            <span v-permission="'exhibitor.handelEdit'" class="button" @click="edit(scope.row)">编辑</span>
+            <span v-permission="'exhibitor.handelDisable'" class="button" @click="setStatus(scope.row)">{{scope.row.status?'启用':'禁用'}}</span>
+            <span v-permission="'exhibitor.handelDelete'" class="button del" @click="del(scope.row)">删除</span>
           </template>
         </el-table-column>
       </el-table>
@@ -194,6 +192,7 @@ export default Vue.extend({
         :total="total"
       />
       <el-pagination
+        v-permission="'exhibitor.handel'"
         @current-change="current_page=$event;getList()"
         background
         :page-size="page_size"

+ 7 - 2
src/views/exhibitorManage/exhibitorSetting.vue

@@ -28,7 +28,8 @@ export default Vue.extend({
         urla: '',
         seo_title: '',
         seo_description: '',
-        seo_keywords: ''
+        seo_keywords: '',
+        show_official_footer: '1'
       },
       formList: [],
       loading: false,
@@ -134,7 +135,7 @@ export default Vue.extend({
 <template>
   <div v-loading="loading" class="main-box">
     <div class="save">
-      <el-button type="primary" @click="save">{{ exhibitorSetting.id?'保存修改':'新建展商' }}</el-button>
+      <el-button v-permission="'exhibitor.save'" type="primary" @click="save">{{ exhibitorSetting.id?'保存修改':'新建展商' }}</el-button>
     </div>
     <div class="info">
       <div class="scroll">
@@ -203,6 +204,10 @@ export default Vue.extend({
             <el-option v-for="item in formList" :key="item.id" :value="item.id" :label="item.template_name"></el-option>
           </el-select>
         </div>
+        <div v-permission="'exhibitor.copyright'" class="form-item required">
+          <div class="label">表单底部显示系统信息</div>
+          <el-switch active-value="1" inactive-value="0" v-model="exhibitorSetting.show_official_footer" class="input" />
+        </div>
         <div class="form-item required">
           <div class="label">url短名称</div>
           <el-input v-model="exhibitorSetting.urla" class="input" placeholder="请输入url" />

+ 272 - 73
src/views/invitationManage/edit.vue

@@ -14,73 +14,272 @@ export default Vue.extend({
   ],
   data() {
     return {
-      code: `<table width="100%" cellpadding="0" cellspacing="0" align="center" bgcolor="#84C0F2" width="600">
-    <tbody>
-        <tr>
-            <td></td>
-            <td style="height: 82px"></td>
-            <td></td>
-        </tr>
-        <tr>
-            <td></td>
-            <td align="center" style="font: bold 64px/1.1 Arial, sans-serif;color: white">
-                驾驭未来<br/>
-                新能源时代
-            </td>
-            <td></td>
-        </tr>
-        <tr>
-            <td></td>
-            <td style="height: 8px"></td>
-            <td></td>
-        </tr>
-        <tr>
-            <td width="100"></td>
-            <td align="center" bgcolor="#ffffff" style="font: 24px Arial, sans-serif;color: #84C0F2">
-                {{expo_name}}
-            </td>
-            <td  width="100"></td>
-        </tr>
-        <tr>
-            <td></td>
-            <td style="height: 8px">
-
-            </td>
-            <td></td>
-        </tr>
-        <tr>
-            <td></td>
-            <td align="center" style="font: 18px Arial, sans-serif;color: #FFFFFF">
-                {{start_date}} - {{end_date}}
-            </td>
-            <td></td>
-        </tr>
-        <tr>
-            <td></td>
-            <td style="height: 8px">
-
-            </td>
-            <td></td>
-        </tr>
-        <tr>
-            <th colspan="3">
-                <img style="width: 100%;display:block" src="https://matchexpo.obs.cn-north-1.myhuaweicloud.com/common/2025/0915/68c7713a49819.jpg" />
-            </th>
-        </tr>
-        <tr>
-            <td bgcolor="#ffffff"></td>
-            <td align="center" bgcolor="#ffffff">
-                {{contact_phone}} | {{location}}
-            </td>
-            <td bgcolor="#ffffff"></td>
-        </tr>
-        <tr>
-            <td bgcolor="#ffffff"></td>
-            <td bgcolor="#ffffff" height="12px"></td>
-            <td bgcolor="#ffffff"></td>
-        </tr>
-    </tbody>
-</table>`,
+      code: `<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>邀请函</title>
+</head>
+<body style="padding: 48px 32px;">
+  <div class="invitation-letter-container"
+    style="border: 1px solid #E5E7EB;
+    border-radius: 12px;
+    padding: 32px;"
+  >
+    <div class="head-box"
+      style="display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 32px;"
+    >
+      <div class="head-left-box"
+        style="display: flex;
+        align-items: center;
+        grid-gap: 16px;"
+      >
+        <div class="img-box"
+          style="width: 96px;
+          height: 48px;"
+        >
+          <img src="https://oss.starify.cn/matchpages/share_center/2025/0910/8401/68c0de742aa57/logo.png" alt="logo.png" style="width: 100%;height: 100%;">
+        </div>
+        <div class="head-title-box"
+          style="color: #1A4789;
+          font-weight: bold;"
+        >
+          <div class="head-english-head"
+            style="font-size: 24px;
+            line-height: 1.3;"
+          >
+            CHINAPLAS 2024
+          </div>
+          <div class="head-chinese-head"
+            style="font-size: 18px;
+            line-height: 1.6;"
+          >
+            {{expo_name}}
+          </div>
+        </div>
+      </div>
+      <div class="head-right-box"
+        style="color: #4B5563;
+        font-size: 16px;
+        line-height: 1.5;"
+      >
+        <div class="head-date" style="text-align: end;">
+          日期 / Date:2023年8月16日
+        </div>
+        <div class="head-serial-number" style="text-align: end;">
+          文件编号 / Ref No.:PR-CPS24-35913
+        </div>
+      </div>
+    </div>
+    <div class="content-box-1"
+      style="font-weight: bold;
+      font-size: 20px;
+      line-height: 1.4;
+      margin-bottom: 16px;"
+    >
+      <div class="chinese-content">
+        致相关人士:
+      </div>
+      <div class="english-content" style="color: #4B5563;">
+        To Whom It May Concern:
+      </div>
+    </div>
+    <div class="content-box-2"
+      style="font-weight: bold;
+      font-size: 18px;
+      line-height: 1.6;
+      margin-bottom: 24px;"
+    >
+      <div class="chinese-content">
+        关于:{{expo_name}}
+      </div>
+      <div class="english-content" style="color: #4B5563;">
+        Re: The 36th International Exhibition on Plastics and Rubber Industries
+      </div>
+    </div>
+    <div class="content-box-3"
+      style="font-size: 16px;
+      line-height: 1.5;
+      margin-bottom: 32px;"
+    >
+      <div class="chinese-content">
+        我们很高兴邀请以下代表于2024年4月23日至26日访问上海,{{expo_name}}。
+      </div>
+      <div class="english-content" style="color: #4B5563;">
+        We are pleased to invite the following representative to visit Shanghai from April 23-26, 2024, to attend CHINAPLAS 2024.
+      </div>
+    </div>
+    <div class="info-list" style="margin-bottom: 32px;">
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">姓名</div>
+          <div class="english-label" style="color: #6B7280;">Name</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">{{full_name}}</div>
+          <div class="english-value" style="color: #6B7280;">Zhang Xiaoming</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">性别</div>
+          <div class="english-label" style="color: #6B7280;">Gender</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">男</div>
+          <div class="english-value" style="color: #6B7280;">Male</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">出生日期</div>
+          <div class="english-label" style="color: #6B7280;">Date of Birth</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">1985年6月15日</div>
+          <div class="english-value" style="color: #6B7280;">June 15, 1985</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">国籍</div>
+          <div class="english-label" style="color: #6B7280;">Nationality</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">中国香港特别行政区</div>
+          <div class="english-value" style="color: #6B7280;">Hong Kong SAR, China</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">护照号码</div>
+          <div class="english-label" style="color: #6B7280;">Passport No.</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: center;">
+          <div class="chinese-value">{{id_number}}</div>
+          <div class="english-value" style="color: #6B7280;"></div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">签发日期</div>
+          <div class="english-label" style="color: #6B7280;">Date of Issue</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">2021年3月15日</div>
+          <div class="english-value" style="color: #6B7280;">March 15, 2021</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">有效期至</div>
+          <div class="english-label" style="color: #6B7280;">Date of Expiry</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">2031年3月14日</div>
+          <div class="english-value" style="color: #6B7280;">March 14, 2031</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">签发地点</div>
+          <div class="english-label" style="color: #6B7280;">Place of Issue</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">中国香港特别行政区</div>
+          <div class="english-value" style="color: #6B7280;">Hong Kong SAR, China</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">公司名称</div>
+          <div class="english-label" style="color: #6B7280;">Company Name</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">{{company}}</div>
+          <div class="english-value" style="color: #6B7280;">Hong Kong International Trading Co., Ltd.</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">职务</div>
+          <div class="english-label" style="color: #6B7280;">Position</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">{{position}}</div>
+          <div class="english-value" style="color: #6B7280;">Sales Director</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">城市</div>
+          <div class="english-label" style="color: #6B7280;">City</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-value">{{city}}</div>
+          <div class="english-value" style="color: #6B7280;">Hong Kong</div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">邮编</div>
+          <div class="english-label" style="color: #6B7280;">Postal Code</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: center;">
+          <div class="chinese-value">999077</div>
+          <div class="english-value" style="color: #6B7280;"></div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">联系电话</div>
+          <div class="english-label" style="color: #6B7280;">Phone</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: center;">
+          <div class="chinese-value">{{mobile_country_code}} {{mobile}}</div>
+          <div class="english-value" style="color: #6B7280;"></div>
+        </div>
+      </div>
+      <div class="info-item" style="display: flex; align-items: center;">
+        <div class="label-box" style="padding: 12px 16px; width: 24%; background-color: #F9FAFB; height: 46px; border-bottom: 1px solid #F9FAFB; display: flex; flex-direction: column; justify-content: space-between;">
+          <div class="chinese-label">Email</div>
+          <div class="english-label" style="color: #6B7280;">Email</div>
+        </div>
+        <div class="value-box" style="flex: 1; padding: 12px 16px; border-bottom: 1px solid #e8eaed; height: 46px; display: flex; flex-direction: column; justify-content: center;">
+          <div class="chinese-value">{{email}}</div>
+          <div class="english-value" style="color: #6B7280;"></div>
+        </div>
+      </div>
+    </div>
+    <div class="chinese-text" style="line-height: 1.5;">
+      请为上述代表安排相关签证。我们确认所有机票费用、住宿机票和旅程期间的保险费用将由上述公司承担或由上述代表自行承担。如果您需要任何进一步的信息,请随时与我们联系。
+    </div>
+    <div class="english-text" style="color: #4B5563; line-height: 1.5; margin-bottom: 48px;">
+      Please arrange the relevant visa for the above representative. We confirm that all airfare, accommodation, and travel insurance costs during the journey will be borne by the above company or the representative. If you need any further information, please feel free to contact us.
+    </div>
+    <div class="bottom-box" style="display: flex; justify-content: space-between; align-items: end; position: relative;">
+      <div class="bottom-left-box">
+        <div style="line-height: 1.5;">北京雅展展览服务有限公司</div>
+        <div style="line-height: 1.5; color: #4B5563;">Beijing Adsale Exhibition Services Co., Ltd.</div>
+        <div style="line-height: 1.5;">电话 / Tel:+86 10 8460 0789</div>
+        <div style="line-height: 1.5;">邮箱 / Email:info@adsale.com.cn</div>
+      </div>
+      <div class="bottom-right-box">
+        <div style="line-height: 1.5; text-align: end;">此致</div>
+        <div style="line-height: 1.5; color: #4B5563; text-align: end;">Yours sincerely,</div>
+        <div style="line-height: 1.5; text-align: end;">张淑娴 女士</div>
+        <div style="line-height: 1.5; color: #4B5563; text-align: end;">Ms. Zhang Shuxian</div>
+        <div style="line-height: 1.5; text-align: end;">展会负责人</div>
+        <div style="line-height: 1.5; text-align: end;">Exhibition Director</div>
+      </div>
+      <img src="https://oss.starify.cn/matchpages/share_center/2025/0910/2919/68c11d315441d/DIV%401x.png" alt="DIV@1x.png" style="position: absolute; top: -110px; right: 0;">
+    </div>
+  </div>
+</body>
+</html>`,
       viewCode: '',
       exhibitorSetting: {},
       userSetting: {
@@ -268,8 +467,8 @@ export default Vue.extend({
   <div v-loading="loading" class="main-box">
     <div class="head">
       <div class="head-left">
-        <el-input v-model="templateInfo.name" class="name" />
-        <el-select v-model="expoId" @change="changeExpo">
+        <el-input v-permission="'invitation.rename'" v-model="templateInfo.name" class="name" />
+        <el-select v-permission="'invitation.select'" v-model="expoId" @change="changeExpo">
           <el-option v-for="(expo,index) in expoList" :key="expo.id" :label="expo.expo_name" :value="index" />
         </el-select>
       </div>
@@ -283,10 +482,10 @@ export default Vue.extend({
       >
         <div class="body">
           <span class="label">模板名称</span>
-          <el-input v-model="templateInfo.name" placeholder="请输入模板名称" />
+          <el-input v-permission="'invitation.rename'" v-model="templateInfo.name" placeholder="请输入模板名称" />
           <span class="label">模板描述</span>
-          <el-input v-model="templateInfo.description" type="textarea" rows="6" placeholder="请输入模板描述" />
-          <el-button class="button" type="primary" @click="saveTemp()">保存</el-button>
+          <el-input v-permission="'invitation.desc'" v-model="templateInfo.description" type="textarea" rows="6" placeholder="请输入模板描述" />
+          <el-button v-permission="'invitation.save'" class="button" type="primary" @click="saveTemp()">保存</el-button>
         </div>
         <el-button slot="reference" icon="el-icon-plus" type="primary">保存模板</el-button>
       </el-popover>
@@ -294,7 +493,7 @@ export default Vue.extend({
     </div>
     <div class="body">
       <div class="editor">
-        <div id="editor" class="editor-box" />
+        <div v-permission="'invitation.editor'" id="editor" class="editor-box" />
       </div>
       <div class="viewer">
         <div class="viewer-box">

+ 16 - 4
src/views/invitationManage/list.vue

@@ -5,7 +5,7 @@ export default Vue.extend({
   name: 'Index',
   data() {
     return {
-      current_page: 0,
+      current_page: 1,
       last_page: 1,
       total: 0,
       page_size: 20,
@@ -22,7 +22,7 @@ export default Vue.extend({
       if (this.loading) return
       this.loading = true
       getTemplateList(this.current_page, this.page_size).then(res => {
-        this.tempList = res.data.data
+        this.tempList = res.data.data || []
         this.total = res.data.total
       }).then(res => {
 
@@ -45,12 +45,13 @@ export default Vue.extend({
 <template>
   <div class="main-box">
     <div class="head">
-      <el-button icon="el-icon-plus" type="primary" @click="gotoAdd">添加模板</el-button>
+      <el-button v-permission="'invitation.addList'" icon="el-icon-plus" type="primary" @click="gotoAdd">添加模板</el-button>
     </div>
     <div class="body">
       <div class="scroll-box">
         <div class="model-cont">
-          <div v-for="item in tempList" :key="item.id" class="model-box" @click="gotoEdit(item)">
+          <div class="no-item" v-if="tempList.length === 0">还没有邀请函模板,去新建一个把!</div>
+          <div v-permission="'invitation.goto'" v-for="item in tempList" :key="item.id" class="model-box" @click="gotoEdit(item)">
             <div class="image loading">
               <img class="bg" :src="ossUrl+item.pic" alt="">
               <img class="pic" :src="ossUrl+item.pic" alt="">
@@ -71,6 +72,7 @@ export default Vue.extend({
         :total="total"
       />
       <el-pagination
+        v-permission="'invitation.changePage'"
         background
         @current-change="current_page=$event;getList()"
         :page-size="page_size"
@@ -102,6 +104,16 @@ export default Vue.extend({
         display: grid;
         grid-template-columns: repeat(3, 1fr);
         grid-gap: 24px;
+        .no-item{
+          font-weight: bold;
+          font-size: 24px;
+          height: 60vh;
+          grid-column: span 3;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: gray;
+        }
         .model-box{
           overflow: hidden;
           height: 500px;

+ 80 - 6
src/views/login/index.vue

@@ -260,11 +260,31 @@ export default Vue.extend({
 
 <template>
   <div class="body">
-    <div :style="{height:third_login?'64vh':'auto'}" class="login-cont">
-      <div class="image-left">
-        <img class="image" src="/static/image/login.webp" alt="">
-        <div class="title">展会服务系统</div>
+    <div class="footer">
+      <div class="item">
+        <span>Copyright © what can i say</span>
+      </div>
+      <a class="item" href="https://beian.miit.gov.cn/" target="_blank">
+        <span>闽ICP备1145141919号</span>
+      </a>
+      <a class="item" href="https://beian.miit.gov.cn/" target="_blank">
+        <img class="gongan" src="/static/image/beian.png" alt="" />
+        <span>闽公网安备1145141919号</span>
+      </a>
+    </div>
+    <div class="bg-cont">
+      <div class="text-cont">
+        <div class="logo">LOGO</div>
+        <div class="title">10分钟创建海外观众预登记服务平台</div>
+        <div class="desc">展会活动观众预登记的数字展会工具</div>
       </div>
+      <img class="bg-image" src="/static/image/login2.webp" alt="">
+    </div>
+    <div :style="{height:third_login?'64vh':'auto'}" class="login-cont">
+<!--      <div class="image-left">-->
+<!--        <img class="image" src="/static/image/login.webp" alt="">-->
+<!--        <div class="title">展会服务系统</div>-->
+<!--      </div>-->
       <div class="cont-right">
         <div class="title">
           <div :class="['item',isLogin?'active':'']" @click="isLogin=true">登录</div>
@@ -345,20 +365,74 @@ export default Vue.extend({
 
 <style scoped lang="scss">
   .body{
+    padding: 0 240px;
     width: 100%;
     height: 100%;
     display: flex;
     align-items: center;
-    justify-content: center;
+    justify-content: flex-end;
     background-image: linear-gradient(120deg,#F7FAFF,#A6C5FE);
+    .footer{
+      z-index: 2;
+      position: absolute;
+      left: 0;
+      gap: 12px;
+      bottom: 16px;
+      width: 100%;
+      display: flex;
+      justify-content: center;
+      .item{
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 4px;
+        font-size: 16px;
+        color: grey;
+        text-decoration: none;
+        .gongan{
+          height: 1.2em;
+        }
+      }
+    }
+    .bg-cont{
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 52%;
+      height: 100%;
+      .text-cont{
+        position: absolute;
+        left: 48px;
+        top: 64px;
+        color: #ffffff;
+        .logo{
+
+        }
+        .title{
+          margin-top: 64px;
+          padding-left: 8px;
+          border-left: 3px solid #ffffff;
+          font-size: 36px;
+          font-weight: bold;
+        }
+        .desc{
+          font-size: 20px;
+          margin-top: 16px;
+        }
+      }
+      .bg-image{
+        width: 100%;
+      }
+    }
     .login-cont{
       background: white;
       overflow: hidden;
       display: grid;
       border-radius: 12px;
       box-shadow: 0 0 12px #00000011;
-      grid-template-columns: 1fr 1fr;
+      grid-template-columns: 1fr;
       .image-left{
+        display: none;
         padding: 40px;
         background: #123068;
         position: relative;

+ 2 - 2
src/views/login/third-login.vue

@@ -125,7 +125,7 @@ export default Vue.extend({
       getGoogleLogin().then(res => {
         const google_info = res.data
         const url = new URL(google_info.url)
-        url.searchParams.set('redirect_uri', 'http://localhost:9528' + '/login-new')
+        url.searchParams.set('redirect_uri', location.origin + '/login')
         location.href = url
       })
     },
@@ -134,7 +134,7 @@ export default Vue.extend({
         console.log(res)
         const linkedin_info = res.data
         const url = new URL(linkedin_info.url)
-        url.searchParams.set('redirect_uri', 'http://localhost:9528' + '/login-new')
+        url.searchParams.set('redirect_uri', location.origin + '/login')
         location.href = url
       })
     },

+ 3 - 3
src/views/preRegManage/compEdit.vue

@@ -159,7 +159,7 @@ export default Vue.extend({
 <template>
   <div v-loading="loading" class="main-box">
     <div class="comp-lib">
-      <div class="add el-icon-plus" @click="creatNew" />
+      <div v-permission="'preReg.addComp'" class="add el-icon-plus" @click="creatNew" />
       <div class="title">
         自定义组件库
       </div>
@@ -364,12 +364,12 @@ export default Vue.extend({
             </div>
              <div class="button-list">
                <el-button @click="copyShow=false" size="mini">取消</el-button>
-               <el-button @click="cloneItem(currentData)" type="primary" size="mini">确定</el-button>
+               <el-button v-permission="'preReg.copyComp'" @click="cloneItem(currentData)" type="primary" size="mini">确定</el-button>
              </div>
           </div>
           <el-button slot="reference" type="primary" icon="el-icon-document-copy" circle />
         </el-popover>
-        <el-button type="primary" @click="save">保存组件</el-button>
+        <el-button v-permission="'preReg.saveComp'" type="primary" @click="save">保存组件</el-button>
       </div>
       <div class="body-cont">
         <div class="body">

+ 7 - 7
src/views/preRegManage/edit.vue

@@ -737,7 +737,7 @@ export default Vue.extend({
         <div :class="['tab-item', CompType==='my'?'active':'']" @click="CompType='my'">我的组件</div>
         <div :class="['tab-item', CompType==='quick'?'active':'']" @click="CompType='quick'">快速组件</div>
       </div>
-      <div class="list">
+      <div v-permission="'preReg.editEdit'" class="list">
         <draggable v-model="CompType==='sys'?systemComp:CompType==='my'?customComp:quickComp" :options="{sort:false}" :group="{name:'form',put:false,pull:'clone'}" :clone="cloneItem" class="drag-list">
           <transition-group class="drag-cont">
             <div v-for="(element) in CompType==='sys'?systemComp:CompType==='my'?customComp:quickComp" :key="element.id" :style="{gridColumn:'span '+element.field_data.width}">
@@ -828,7 +828,7 @@ export default Vue.extend({
     </div>
     <div class="form-view" @mouseenter="inForm=true" @mouseleave="inForm=false">
       <div class="scroll-view">
-        <div class="form-cont">
+        <div v-permission="'preReg.editEdit'" class="form-cont">
           <div :class="['form-head',getIndexByKey(currentKey) === -1?'active':'']" @click="currentKey=''">
             <div class="title">{{ formInfo.name }}</div>
             <div class="tips">{{ formInfo.desc }}</div>
@@ -939,19 +939,19 @@ export default Vue.extend({
               </div>
               <div class="button-list">
                 <el-button size="mini" @click="copyFormShow=false">取消</el-button>
-                <el-button type="primary" size="mini" @click="cloneForm()">复制</el-button>
+                <el-button v-permission="'preReg.copy'" type="primary" size="mini" @click="cloneForm()">复制</el-button>
               </div>
             </div>
             <el-button slot="reference" type="primary" icon="el-icon-document-copy" circle />
           </el-popover>
-          <el-button type="primary" @click="save">保存表单<span class="mini-text"> (Ctrl+S)</span></el-button>
+          <el-button v-permission="'preReg.save'" type="primary" @click="save">保存表单<span class="mini-text"> (Ctrl+S)</span></el-button>
         </div>
         <div class="title">表单设定</div>
         <div class="body">
           <div class="tips">表单名称</div>
-          <el-input v-model="formInfo.name" placeholder="请输入表单名称" />
+          <el-input v-permission="'preReg.editName'" v-model="formInfo.name" placeholder="请输入表单名称" />
           <div class="tips">表单介绍</div>
-          <el-input v-model="formInfo.desc" type="textarea" placeholder="请输入表单介绍" />
+          <el-input v-permission="'preReg.editDesc'" v-model="formInfo.desc" type="textarea" placeholder="请输入表单介绍" />
         </div>
       </template>
       <template v-else>
@@ -1127,7 +1127,7 @@ export default Vue.extend({
               </div>
               <div class="button-list">
                 <el-button size="mini" @click="copyShow=false">取消</el-button>
-                <el-button type="primary" size="mini" @click="cloneToMe(currentData)">复制</el-button>
+                <el-button v-permission="'preReg.saveCompEdit'" type="primary" size="mini" @click="cloneToMe(currentData)">复制</el-button>
               </div>
             </div>
             <el-button slot="reference" type="primary" icon="el-icon-folder-add" circle />

+ 7 - 6
src/views/preRegManage/list.vue

@@ -89,11 +89,11 @@ export default Vue.extend({
 <template>
   <div class="main-box">
     <div class="head">
-      <el-input v-model="searchWord" prefix-icon="el-icon-search" placeholder="搜索表单名称" class="input" @input="search">
+      <el-input v-permission="'preReg.search'" v-model="searchWord" prefix-icon="el-icon-search" placeholder="搜索表单名称" class="input" @input="search">
         <el-button v-if="searchWord" slot="append" icon="el-icon-delete" @click="searchWord='';search()" />
       </el-input>
-      <el-button icon="el-icon-plus" type="primary" @click="handleCreate">创建表单</el-button>
-      <el-button icon="el-icon-copy-document">批量复制</el-button>
+      <el-button v-permission="'preReg.creat'" icon="el-icon-plus" type="primary" @click="handleCreate">创建表单</el-button>
+<!--      <el-button icon="el-icon-copy-document">批量复制</el-button>-->
     </div>
     <div class="body">
       <el-table v-loading="loading" :data="formList" height="100%" class="table">
@@ -130,9 +130,9 @@ export default Vue.extend({
           width="200"
         >
           <template slot-scope="scope">
-            <span class="button" @click="edit(scope.row)">编辑</span>
-            <span class="button" @click="setStatus(scope.row)">{{ scope.row.status?'启用':'禁用' }}</span>
-            <span class="button del" @click="del(scope.row)">删除</span>
+            <span v-permission="'preReg.handelEdit'" class="button" @click="edit(scope.row)">编辑</span>
+            <span v-permission="'preReg.handelDisable'" class="button" @click="setStatus(scope.row)">{{ scope.row.status?'启用':'禁用' }}</span>
+            <span v-permission="'preReg.handelDelete'" class="button del" @click="del(scope.row)">删除</span>
           </template>
         </el-table-column>
       </el-table>
@@ -145,6 +145,7 @@ export default Vue.extend({
         :total="total"
       />
       <el-pagination
+        v-permission="'preReg.changePage'"
         background
         @current-change="current_page=$event;getList()"
         :page-size="page_size"

+ 3 - 2
src/views/setting/accountSetting.vue

@@ -9,8 +9,8 @@ export default Vue.extend({
 <template>
   <div class="main-box">
     <div class="head">
-      <el-input prefix-icon="el-icon-search" placeholder="搜索账号" class="input"></el-input>
-      <el-button icon="el-icon-plus" type="primary">添加账号</el-button>
+      <el-input v-permission="'setting.account.search'" prefix-icon="el-icon-search" placeholder="搜索账号" class="input"></el-input>
+      <el-button v-permission="'setting.account.add'" icon="el-icon-plus" type="primary">添加账号</el-button>
     </div>
     <div class="body">
       <el-table height="100%" class="table">
@@ -38,6 +38,7 @@ export default Vue.extend({
     </div>
     <div class="foot">
       <el-pagination
+        v-permission="'setting.account.changePage'"
         background
         :page-size="100"
         layout="total, prev, pager, next"

+ 5 - 4
src/views/setting/rolesSetting.vue

@@ -24,8 +24,8 @@ export default Vue.extend({
   <div class="main-box">
     <div class="list-cont">
       <div class="head">
-        <el-input prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input"></el-input>
-        <el-button icon="el-icon-plus" type="primary">新增角色</el-button>
+        <el-input v-permission="'setting.roles.search'" prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input"></el-input>
+        <el-button v-permission="'setting.roles.add'" icon="el-icon-plus" type="primary">新增角色</el-button>
       </div>
       <div class="body">
         <el-table height="100%" class="table">
@@ -42,6 +42,7 @@ export default Vue.extend({
       </div>
       <div class="foot">
         <el-pagination
+          v-permission="'setting.roles.changePage'"
           background
           :page-size="100"
           layout="total, prev, pager, next"
@@ -51,10 +52,10 @@ export default Vue.extend({
     </div>
     <div class="menu-list">
       <div class="save">
-        <el-button type="primary">保存</el-button>
+        <el-button v-permission="'setting.roles.save'" type="primary">保存</el-button>
       </div>
       <div class="scroll">
-        <div class="menu">
+        <div v-permission="'setting.roles.permission'" class="menu">
           <template v-for="(item,index) in menuRouter">
             <div  class="menu-item">
               <div class="name">

+ 23 - 23
src/views/setting/systemSetting.vue

@@ -46,7 +46,7 @@ export default Vue.extend({
 <template>
   <div v-loading="loading" class="main-box">
     <div class="save">
-      <el-button @click="save" type="primary">保存</el-button>
+      <el-button v-permission="'setting.system.save'" @click="save" type="primary">保存</el-button>
     </div>
     <div class="scroll">
       <div class="setting-box">
@@ -56,51 +56,51 @@ export default Vue.extend({
             <div class="label">
               发件邮箱地址
             </div>
-            <el-input v-model="mailSetting.from_email" placeholder="请输入发件邮箱地址" />
+            <el-input v-permission="'setting.system.sentEmailAddress'" v-model="mailSetting.from_email" placeholder="请输入发件邮箱地址" />
           </div>
           <div class="setting-item">
             <div class="label">
               发件邮箱密码/授权码
             </div>
-            <el-input v-model="mailSetting.auth_code" placeholder="请输入发件邮箱密码/授权码" />
+            <el-input v-permission="'setting.system.sentEmailPassword'" v-model="mailSetting.auth_code" placeholder="请输入发件邮箱密码/授权码" />
           </div>
           <div class="setting-item">
             <div class="label">
               SMTP服务器地址
             </div>
-            <el-input v-model="mailSetting.smtp_server" placeholder="请输入SMTP服务器地址" />
+            <el-input v-permission="'setting.system.sentEmailServer'" v-model="mailSetting.smtp_server" placeholder="请输入SMTP服务器地址" />
           </div>
           <div class="setting-item">
             <div class="label">
               SMTP端口
             </div>
-            <el-input v-model="mailSetting.smtp_port" placeholder="请输入SMTP端口" />
+            <el-input v-permission="'setting.system.sentEmailPort'" v-model="mailSetting.smtp_port" placeholder="请输入SMTP端口" />
           </div>
           <div class="setting-item">
             <div class="label">
               启用SSL
             </div>
-            <el-switch v-model="mailSetting.is_ssl" />
-          </div>
-        </div>
-      </div>
-      <div class="setting-box">
-        <div class="title">
-          邮件设置
-        </div>
-        <div class="setting-list">
-          <div class="setting-item">
-            <div class="label">
-              接收通知邮箱
-            </div>
-            <el-input placeholder="请输入通知邮箱" />
-          </div>
-          <div class="setting-item" />
-          <div class="setting-item">
-            <el-button type="primary">发送测试邮件</el-button>
+            <el-switch v-permission="'setting.system.sentEmailSSL'" v-model="mailSetting.is_ssl" />
           </div>
         </div>
       </div>
+<!--      <div class="setting-box">-->
+<!--        <div class="title">-->
+<!--          邮件设置-->
+<!--        </div>-->
+<!--        <div class="setting-list">-->
+<!--          <div class="setting-item">-->
+<!--            <div class="label">-->
+<!--              接收通知邮箱-->
+<!--            </div>-->
+<!--            <el-input placeholder="请输入通知邮箱" />-->
+<!--          </div>-->
+<!--          <div class="setting-item" />-->
+<!--          <div class="setting-item">-->
+<!--            <el-button type="primary">发送测试邮件</el-button>-->
+<!--          </div>-->
+<!--        </div>-->
+<!--      </div>-->
     </div>
   </div>
 </template>

+ 99 - 0
src/views/user/form.vue

@@ -56,6 +56,28 @@ export default Vue.extend({
         })
       }
     },
+    share(way) {
+      let url = ''
+      let prams = ''
+      if (way === 'twitter') {
+        url = 'https://twitter.com/intent/tweet'
+        prams += ('?url=' + window.location.href)
+        prams += ('&text=' + this.from_data.expo_name + ' | ' + this.from_data.content)
+      }
+      if (way === 'facebook') {
+        url = 'https://www.facebook.com/sharer/sharer.php'
+        prams += ('?u=' + window.location.href)
+        prams += ('&title=' + this.from_data.expo_name)
+        prams += ('&quote=' + this.from_data.content)
+      }
+      if (way === 'linkedin') {
+        url = 'https://www.linkedin.com/shareArticle'
+        prams += ('?url=' + window.location.href)
+        prams += ('&title=' + this.from_data.expo_name)
+        prams += ('&summary=' + this.from_data.content)
+      }
+      window.open(url + encodeURI(prams), '_blank')
+    },
     getJob() {
       if (this.loading || this.jobPage >= this.jobTotal) {
         return
@@ -268,6 +290,34 @@ export default Vue.extend({
           <span v-if="loading" class="el-icon-loading" />
         </el-button>
       </div>
+      <div class="power-by">
+        该表单由主办方{{ from_data.expo_name }}创建。
+      </div>
+    </div>
+    <div class="foot">
+      <div class="power-by">
+        <a target="_blank" href="/">多果表单</a>提供技术支持 by <a target="_blank" href="https://matchexpo.cn/">MatchExpo</a>
+      </div>
+      <div class="share">
+        <div class="text">分享至:</div>
+        <div class="share-list">
+          <!--          <div class="share-icon" @click="share('wechat')">-->
+          <!--            <img src="/static/image/icon/wechat.png">-->
+          <!--          </div>-->
+          <!--          <div class="share-icon" @click="share('qq')">-->
+          <!--            <img src="/static/image/icon/qq-zone.png">-->
+          <!--          </div>-->
+          <div class="share-icon" @click="share('twitter')">
+            <img src="/static/image/icon/x.png">
+          </div>
+          <div class="share-icon" @click="share('facebook')">
+            <img src="/static/image/icon/facebook.png">
+          </div>
+          <div class="share-icon" @click="share('linkedin')">
+            <img src="/static/image/icon/linkin.png">
+          </div>
+        </div>
+      </div>
     </div>
   </div>
 </template>
@@ -277,12 +327,61 @@ export default Vue.extend({
   margin: 36px auto;
   max-width: 800px;
   width: 100%;
+  .foot{
+    padding: 0 12px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 16px;
+    .power-by,a{
+      font-size: 16px;
+      color: grey;
+    }
+    .share{
+      display: flex;
+      align-items: center;
+      .share-list{
+        display: flex;
+        gap: 8px;
+        .share-icon{
+          cursor: pointer;
+          padding: 8px;
+          width: 36px;
+          height: 36px;
+          border-radius: 50%;
+          background-color: #EEEEEE;
+          img{
+            transition-duration: 300ms;
+            filter: brightness(0) opacity(0.5);
+            width: 100%;
+            height: 100%;
+            object-fit: contain;
+          }
+          &:hover{
+            background-color: #F4F4F4;
+            img{
+              filter: brightness(1) opacity(1);
+            }
+          }
+
+        }
+      }
+      .text{
+        font-size: 16px;
+        color: grey;
+      }
+    }
+  }
   .form{
     border-radius: 8px;
     margin-top: 16px;
     padding: 24px;
     box-shadow: 0 0 8px 0 #00000008;
     background: white;
+    .power-by{
+      font-size: 16px;
+      color: grey;
+    }
     .button{
       margin-top: 12px;
       display: flex;