zhoujump vor 2 Monaten
Ursprung
Commit
312ea8267a

+ 1 - 1
.env

@@ -1,5 +1,5 @@
 # just a flag
-ENV = 'development'
+ENV = 'production'
 VUE_APP_NAME = 'development'
 # base api
 VUE_APP_BASE_API = ''

+ 3 - 1
.env.development

@@ -2,4 +2,6 @@
 ENV = 'development'
 VUE_APP_NAME = 'development'
 # base api
-VUE_APP_BASE_API = ''
+VUE_APP_BASE_API = 'https://exporeg-test-api.matchexpo.cn'
+
+VUE_APP_OSS_DOMAIN = 'https://matchexpo.obs.cn-north-1.myhuaweicloud.com'

+ 1 - 1
.env.test

@@ -1,5 +1,5 @@
 # just a flag
-ENV = 'development'
+ENV = 'test'
 VUE_APP_NAME = 'development'
 # base api
 VUE_APP_BASE_API = ''

+ 5 - 2
package.json

@@ -16,10 +16,10 @@
   },
   "dependencies": {
     "@google/genai": "^0.13.0",
-    "axios": "^0.21.1",
+    "axios": "^0.27.0",
     "clipboard": "2.0.4",
-    "codemirror": "5.45.0",
     "core-js": "3.6.5",
+    "dom-to-image-more": "^3.7.1",
     "driver.js": "0.9.5",
     "dropzone": "5.5.1",
     "echarts": "4.2.1",
@@ -69,6 +69,9 @@
     "html-webpack-plugin": "3.2.0",
     "lint-staged": "8.1.5",
     "mockjs": "1.0.1-beta3",
+    "monaco-editor": "^0.30.1",
+    "monaco-editor-locales-plugin": "^0.0.3",
+    "monaco-editor-webpack-plugin": "^6.0.0",
     "plop": "2.3.0",
     "postcss-px-to-viewport": "^1.1.1",
     "raw-loader": "^4.0.2",

+ 61 - 2
src/App.vue

@@ -1,20 +1,34 @@
 <template>
   <div id="app" class="custom-theme">
-    <router-view />
+    <router-view
+      :is-animation="isAnimation"
+      :menu-router="menuRouter"
+      :menu-active="menuActive"
+      :breadcrumb="breadcrumb"
+      :is-collapse="isCollapse"
+      @changeCollapse="isCollapse = !isCollapse"
+    />
   </div>
 </template>
 
 <script>
 import i18n from '@/locales/i18n'
+import router from '@/router'
 export default {
   name: 'App',
   data() {
     return {
-      roles: []
+      roles: [],
+      isAnimation: false,
+      menuRouter: [],
+      menuActive: 0,
+      breadcrumb: [],
+      isCollapse: false,
     }
   },
   created() {
     this.initStore()
+    this.initGuards()
   },
   methods: {
     initStore() {
@@ -26,6 +40,51 @@ export default {
       } catch (e) {
         localStorage.removeItem('store')
       }
+    },
+    initGuards() {
+      this.$router.afterEach((to, from, next) => {
+        this.refreshRoute()
+      })
+      this.$router.beforeEach((to, from, next) => {
+        if (!router.app.$store.getters.token) {
+          if (to.path === '/login') {
+            next()
+          } else {
+            next('/login')
+          }
+        }
+        this.isAnimation = true
+        setTimeout(() => {
+          this.isAnimation = false
+          next()
+        }, 400)
+      })
+      if (!router.app.$store.getters.token) {
+        this.$router.push('/login')
+      }
+    },
+    refreshRoute() {
+      this.menuRouter = this.$router.options.routes[0].children
+      console.log(this.menuRouter)
+      this.breadcrumb = this.$route.matched
+      this.menuRouter.forEach((item, index) => {
+        if (item.name === this.$route.name) {
+          this.menuActive = index
+          if (item.meta.collapse !== undefined) {
+            this.isCollapse = !!item.meta.collapse
+          }
+        }
+        if (item.children) {
+          item.children.forEach((child, subIndex) => {
+            if (child.name === this.$route.name) {
+              this.menuActive = index + '-' + subIndex
+              if (child.meta.collapse !== undefined) {
+                this.isCollapse = !!child.meta.collapse
+              }
+            }
+          })
+        }
+      })
     }
   }
 }

+ 126 - 0
src/api/expo.js

@@ -0,0 +1,126 @@
+import request from '@/utils/request'
+
+/**
+ * 获取展商列表接口
+ * @param {number} page 页码
+ * @param {number} page_size 每页数量
+ * @param {string} expo_name 展商名称
+ * @returns {*}
+ */
+export function getExpoList(page, page_size, expo_name) {
+  return request({
+    url: '/api/expo/list',
+    method: 'get',
+    params: {
+      page,
+      page_size,
+      expo_name
+    }
+  })
+}
+/**
+ * 获取自己展商详情接口
+ * @param id 展商ID
+ * @returns {*}
+ */
+export function getMyExpoInfo(id) {
+  return request({
+    url: '/api/expo/my-info',
+    method: 'get',
+    params: {
+      id
+    }
+  })
+}
+/**
+ * 获取展会详情接口
+ * @param key 展商urla
+ * @returns {*}
+ */
+export function getExpoInfo(key) {
+  return request({
+    url: '/api/expo/info',
+    method: 'get',
+    params: {
+      key
+    }
+  })
+}
+/**
+ * 保存展会信息
+ * @param {number} id
+ * @param {string} expo_name
+ * @param {string} start_date
+ * @param {string} end_date
+ * @param {string} location
+ * @param {string} organizer
+ * @param {string} contact_phone
+ * @param {string} contact_email
+ * @param {string} content
+ * @param {string} logo
+ * @param {array} images
+ * @param {object} social_links
+ * @param {string} form_template_id
+ * @param {string} page_template_id
+ * @param {string} urla
+ * @param {string} seo_title
+ * @param {string} seo_description
+ * @param {string} seo_keywords
+ * @returns {*}
+ */
+export function saveExpo(id,expo_name,start_date,end_date,location,organizer,contact_phone,contact_email,content,logo,images,social_links,form_template_id,page_template_id,urla,seo_title,seo_description,seo_keywords) {
+  return request({
+    url: '/api/expo/info',
+    method: 'post',
+    data:{
+      id,
+      expo_name,
+      start_date,
+      end_date,
+      location,
+      organizer,
+      contact_phone,
+      contact_email,
+      content,
+      logo,
+      images,
+      social_links,
+      form_template_id,
+      page_template_id,
+      urla,
+      seo_title,
+      seo_description,
+      seo_keywords
+    }
+  })
+}
+/**
+ * 删除展会接口
+ * @param id 展商ID
+ * @returns {*}
+ */
+export  function deleteExpo(id) {
+  return request({
+    url: '/api/expo/info',
+    method: 'delete',
+    data:{
+      id
+    }
+  })
+}
+/**
+ * 设置展会状态接口
+ * @param id 展商ID
+ * @param status 状态
+ * @returns {*}
+ */
+export function setExpoStatus(id,status) {
+  return request({
+    url: '/api/expo/status',
+    method: 'post',
+    data:{
+      id,
+      status
+    }
+  })
+}

+ 89 - 0
src/api/form.js

@@ -0,0 +1,89 @@
+import request from '@/utils/request'
+
+/**
+ * 获取表单列表
+ * @param page
+ * @param page_size
+ * @param template_name
+ * @returns {*}
+ */
+export function getFormList(page, page_size, template_name){
+  return request({
+    url: '/api/form/temp-list',
+    method: 'get',
+    params: {
+      page,
+      page_size,
+      template_name
+    }
+  })
+}
+
+/**
+ * 获取表单详情
+ * @param template_id
+ * @returns {*}
+ */
+export function getFormInfo(template_id){
+  return request({
+    url: '/api/form/temp-info',
+    method: 'get',
+    params: {
+      template_id
+    }
+  })
+}
+
+/**
+ * 保存展会模板
+ * @param id
+ * @param template_name
+ * @param description
+ * @param fields
+ * @returns {*}
+ */
+export function saveForm(id,template_name,description,fields){
+  return request({
+    url: '/api/form/temp',
+    method: 'post',
+    data: {
+      id,
+      template_name,
+      description,
+       fields
+    }
+  })
+}
+
+ /**
+ * 删除表单
+ * @param id
+  * @param status
+ * @returns {*}
+ */
+ export function deleteForm(id,status) {
+   return request({
+     url: '/api/form/temp',
+     method: 'delete',
+     data:{
+       id,
+       status
+     }
+   })
+ }
+ /**
+ * 设置表单状态
+ * @param id
+ * @param status
+ * @returns {*}
+ */
+ export function setFormStatus(id,status) {
+   return request({
+     url: '/api/form/temp-status',
+     method: 'post',
+     data:{
+       id,
+       status
+     }
+   })
+ }

+ 39 - 0
src/api/system.js

@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+
+/**
+ * 上传接口
+ * @param file
+ * @param title
+ * @param alt
+ * @param url
+ * @param filename
+ * @param size
+ * @param ext
+ * @param pic
+ * @param resolution
+ * @param mine_type
+ * @param etag
+ * @returns {*}
+ */
+export function upload(file,title,alt,url,filename,size,ext,pic,resolution,mine_type,etag) {
+  return request({
+    url: '/api/attachment/upload',
+    method: 'post',
+    data: {
+      file,
+      title,
+      alt,
+      url,
+      filename,
+      size,
+      ext,
+      pic,
+      resolution,
+      mine_type,
+      etag
+    },
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}

+ 56 - 0
src/api/template.js

@@ -0,0 +1,56 @@
+import request from '@/utils/request'
+
+/**
+ * 保存邮件模板
+ * @param id
+ * @param name
+ * @param description
+ * @param content
+ * @param pic
+ * @returns {*}
+ */
+export function saveTemplate(id,name,description,content,pic){
+  return request({
+    url: '/api/invitation/temp-save',
+    method: 'post',
+    data: {
+      id,
+      name,
+      description,
+      content,
+      pic
+    }
+  })
+}
+
+/**
+ * 删除邮件模板
+ * @param id
+ * @returns {*}
+ */
+export function deleteTemplate(id){
+  return request({
+    url: '/api/invitation/temp',
+    method: 'delete',
+    data: {
+      id
+    }
+  })
+}
+
+/**
+ * 获取邮件模板列表
+ * @param page
+ * @param page_size
+ * @returns {*}
+ */
+export function getTemplateList(page, page_size){
+  return request({
+    url: '/api/invitation/temp-list',
+    method: 'get',
+    params: {
+      page,
+      page_size
+    }
+  })
+}

+ 43 - 0
src/api/user.js

@@ -1 +1,44 @@
 import request from '@/utils/request'
+
+/**
+ * 登录接口
+ * @param {string} user_name 用户名
+ * @param {string} password 密码
+ * @param {number} login_type 登录类型 0:帐号密码登录 1:短信登录 2:微信登录 3:QQ登录
+ * @param {number} login_portal 登录入口 0:web 1:app
+ * @returns {*}
+ */
+export function login(user_name, password, login_type, login_portal) {
+  return request({
+    url: '/api/user/login',
+    method: 'post',
+    data: {
+      user_name,
+      password,
+      login_type,
+      login_portal
+    }
+  })
+}
+/**
+ * 获取用户信息接口
+ * @returns {*}
+ */
+export function getInfo() {
+  return request({
+    url: '/api/user/base-account',
+    method: 'post',
+    params: {}
+  })
+}
+/**
+ * 登出接口
+ * @returns {*}
+ */
+export function logout() {
+  return request({
+    url: '/api/user/logout',
+    method: 'post',
+    params: {}
+  })
+}

+ 21 - 48
src/layout/index.vue

@@ -5,7 +5,7 @@
         <img src="" alt="">
       </div>
       <div class="layout-menu">
-        <el-menu class="layout-menu-inner" :collapse="isCollapse" :default-active="menuActive+''">
+        <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">
@@ -31,7 +31,7 @@
     </div>
     <div class="layout-right">
       <div class="layout-breadcrumb">
-        <div :class="['icon',isCollapse?'collapse':'']" @click="changeCollapse">
+        <div :class="['icon',isCollapse?'collapse':'']" @click="$emit('changeCollapse')">
           <div class="icon-1"></div>
           <div class="icon-2"></div>
           <div class="icon-3"></div>
@@ -43,7 +43,7 @@
         </el-breadcrumb>
         <div class="user-info">
           <div class="avatar">
-
+            <img :src="this.user.avatar?this.user.avatar:'/static/image/avatar.webp'" alt="">
           </div>
           <div class="info-box">
             <div class="nick-name">
@@ -66,7 +66,7 @@
       </div>
       <div class="layout-body">
         <div class="body-inner" :class="{'animation':isAnimation}">
-          <router-view class="view-inner" />
+          <router-view @completion-init="$emit('completionInit')" :is-completion-init="isCompletionInit" class="view-inner" />
         </div>
         <div class="body-inner trans" :class="{'animation':isAnimation}" />
       </div>
@@ -77,21 +77,23 @@
 <script>
 export default {
   name: 'Layout',
+  props: [
+    'isAnimation',
+    'menuRouter',
+    'menuActive',
+    'breadcrumb',
+    'isCollapse',
+    'isCompletionInit'
+  ],
   data() {
     return {
-      isCollapse: false,
-      menuRouter: [],
-      menuActive: 0,
-      breadcrumb: [],
-      isAnimation: false
     }
   },
   computed: {
     user() { return this.$store.state.user.user }
   },
   mounted() {
-    this.refreshRoute()
-    this.initGuards()
+
   },
   methods: {
     logout() {
@@ -111,49 +113,13 @@ export default {
       })
     },
     changeCollapse() {
-      this.isCollapse = !this.isCollapse
+      this.$emit('changeCollapse')
     },
     goto(route) {
       if ('/' + route.path === this.$route.path) { return }
       this.$router.push({
         name: route.name
       })
-      this.refreshRoute()
-    },
-    initGuards() {
-      this.$router.afterEach((to, from, next) => {
-        this.refreshRoute()
-      })
-      this.$router.beforeEach((to, from, next) => {
-        this.isAnimation = true
-        setTimeout(() => {
-          this.isAnimation = false
-          next()
-        }, 400)
-      })
-    },
-    refreshRoute() {
-      this.menuRouter = this.$router.options.routes[0].children
-      console.log(this.menuRouter)
-      this.breadcrumb = this.$route.matched
-      this.menuRouter.forEach((item, index) => {
-        if (item.name === this.$route.name) {
-          this.menuActive = index
-          if (item.meta.collapse !== undefined) {
-            this.isCollapse = !!item.meta.collapse
-          }
-        }
-        if (item.children) {
-          item.children.forEach((child, subIndex) => {
-            if (child.name === this.$route.name) {
-              this.menuActive = index + '-' + subIndex
-              if (child.meta.collapse !== undefined) {
-                this.isCollapse = !!child.meta.collapse
-              }
-            }
-          })
-        }
-      })
     }
   }
 }
@@ -172,6 +138,7 @@ export default {
       grid-template-rows: 80px 1fr;
     }
     .layout-left{
+      height: 100%;
       position: relative;
       z-index: 1;
       box-shadow: 0 2px 6px -4px #00000022,0 10px 10px -3px #00000022;
@@ -217,6 +184,7 @@ export default {
       }
     }
     .layout-right{
+      height: 100%;
       .layout-breadcrumb{
         grid-gap: 24px;
         padding: 0 24px;
@@ -240,6 +208,11 @@ export default {
             aspect-ratio: 1;
             background: #b0b2b6;
             z-index: 5;
+            overflow: hidden;
+            img{
+              width: 100%;
+              height: 100%;
+            }
           }
           .info-box{
             backdrop-filter: blur(16px);

+ 5 - 0
src/main.js

@@ -109,6 +109,11 @@ Vue.prototype.generateSlug = function(title) {
     .replace(/-+/g, '-')
     .replace(/^-|-$/g, '') // 新增首尾连字符清理
 }
+
+let isInit = false
+Vue.prototype.$getIsInit = function() { return isInit }
+Vue.prototype.$setIsInit = function() { isInit = true }
+
 Vue.prototype.$bus = new Vue()
 new Vue({
   el: '#app',

+ 73 - 41
src/router/index.js

@@ -17,7 +17,7 @@ export const constantRoutes = [
         meta: { title: '首页看板', icon: 'el-icon-house', roles: 'dashboard' }
       },
       {
-        path: 'audiencemanage',
+        path: 'audience',
         component: () => import('@/views/audienceManage/index'),
         name: 'audienceManage',
         meta: {
@@ -41,18 +41,18 @@ export const constantRoutes = [
         }
       },
       {
-        path: 'preregmanage',
+        path: 'preRegister',
         component: () => import('@/views/preRegManage/index'),
         name: 'preRegManage',
         meta: { title: '预登记表单', icon: 'el-icon-tickets', roles: 'preReg' },
-        redirect: '/preregmanage/list',
+        redirect: '/preRegister/list',
         children: [
           {
             path: 'list',
             component: () => import('@/views/preRegManage/list'),
-            name: 'list',
+            name: 'preRegManageList',
             meta: {
-              title: '预登记表单管理',
+              title: '表单管理',
               icon: 'el-icon-edit',
               roles: 'preReg.list',
               collapse: false,
@@ -65,42 +65,84 @@ export const constantRoutes = [
             }
           },
           {
-            path: 'edit',
+            path: 'edit/:id',
             component: () => import('@/views/preRegManage/edit'),
-            name: 'edit',
-            meta: { title: '预登记表单编辑', icon: 'el-icon-edit', hidden: true, roles: 'preReg.edit', collapse: true }
+            name: 'preRegManagEdit',
+            meta: { title: '表单编辑', icon: 'el-icon-edit', hidden: true, roles: 'preReg.edit', collapse: true }
+          },
+          {
+            path: 'add',
+            component: () => import('@/views/preRegManage/edit'),
+            name: 'preRegManagAdd',
+            meta: { title: '表单新增', icon: 'el-icon-document-add', roles: 'preReg.edit', collapse: true }
           }
         ]
       },
       {
-        path: 'exhibitormanage',
-        component: () => import('@/views/audienceManage/index'),
+        path: 'exhibitor',
+        component: () => import('@/views/exhibitorManage/index'),
         name: 'ExhibitorManage',
-        meta: {
-          title: '展商管理',
-          icon: 'el-icon-office-building',
-          roles: 'exhibitor',
-          func: [
-            {
-              roles: 'exhibitor.add',
-              name: '添加展商'
-            },
-            {
-              roles: 'exhibitor.import',
-              name: '导入'
-            },
-            {
-              roles: 'exhibitor.export',
-              name: '导出'
-            }
-          ]
-        }
+        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: '导出'
+              }
+            ] }
+          },
+          {
+            component: () => import('@/views/exhibitorManage/exhibitorSetting'),
+            name: 'exhibitorEdit',
+            path: 'edit/:id',
+            meta: { title: '配置展商', icon: 'el-icon-setting', roles: 'exhibitor.setting', hidden: true }
+          },
+          {
+            component: () => import('@/views/exhibitorManage/exhibitorSetting'),
+            name: 'exhibitorAdd',
+            path: 'add',
+            meta: { title: '添加展商', icon: 'el-icon-document-add', roles: 'exhibitor.add' }
+          }
+        ]
       },
       {
-        path: 'invitationmanage',
+        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' }
+        },
+        {
+          path: 'add',
+          component: () => import('@/views/invitationManage/edit'),
+          name: 'invitationAdd',
+          meta: { title: '邀请函模板新增', icon: 'el-icon-document-add', roles: 'invitation.add' }
+        },
+        {
+          path: 'edit/:id',
+          component: () => import('@/views/invitationManage/edit'),
+          name: 'invitationEdit',
+          meta: { title: '邀请函模板编辑', icon: 'el-icon-edit', hidden: true, roles: 'invitation.edit' }
+        }
+        ]
       },
       {
         path: 'setting',
@@ -177,14 +219,4 @@ const createRouter = () => new Router({
   routes: constantRoutes
 })
 const router = createRouter()
-router.beforeEach((to, from, next) => {
-  if (router.app.$store.getters.token) {
-    if (to.path === '/login') {
-      next()
-    } else {
-      next('/login')
-    }
-  }
-  next()
-})
 export default router

+ 2 - 1
src/store/getters.js

@@ -1,4 +1,5 @@
 const getters = {
-
+  token: state => state.user.token,
+  user: state => state.user.user
 }
 export default getters

+ 43 - 17
src/store/modules/user.js

@@ -1,8 +1,9 @@
+// eslint-disable-next-line no-unused-vars
+import { login, getInfo } from '@/api/user'
 export default {
   state: {
     token: '',
-    user: {},
-    autoLogin: false
+    user: {}
   },
   mutations: {
     SET_TOKEN: (state, token) => {
@@ -11,31 +12,56 @@ export default {
     SET_USER: (state, user) => {
       state.user = user
     },
-    SET_AUTO_LOGIN: (state, autoLogin) => {
-      state.autoLogin = autoLogin
-    },
     SET_LOGOUT: (state) => {
       state.token = ''
       state.user = {}
-      state.autoLogin = false
     }
   },
   actions: {
-    login({ commit }, username, password) {
-      return new Promise((resolve, reject) => {
-        reject(new Error('登录失败'))
-      })
-    },
-    testLogin({ commit }, token) {
+    /**
+     * 登录
+     * @param commit
+     * @param payload
+     * @param payload.savePassword 是否保存密码
+     * @param payload.username 用户名
+     * @param payload.password 密码
+     * @returns {Promise<unknown>}
+     */
+    login({ commit }, payload) {
       return new Promise((resolve, reject) => {
-        commit('SET_USER', {
-          username: 'admin',
-          nickname: '测试登录'
+        login(payload.username, payload.password, 0, 0).then(response => {
+          commit('SET_TOKEN', response.data.api_token)
+          getInfo().then(response => {
+            console.log(response)
+            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
+            })
+            if (payload.savePassword) {
+              const savedAccount = {
+                username: payload.username,
+                password: payload.password
+              }
+              localStorage.setItem('savedAccount', JSON.stringify(savedAccount))
+            } else {
+              localStorage.removeItem('savedAccount')
+            }
+            resolve('登录成功')
+          })
+        }).catch(error => {
+          console.log(error)
+          reject('登录失败')
         })
-        commit('SET_TOKEN', token)
-        resolve('登录成功')
       })
     },
+    /**
+     * 登出
+     * @param commit
+     * @returns {Promise<unknown>}
+     */
     logout({ commit }) {
       return new Promise((resolve, reject) => {
         commit('SET_LOGOUT')

+ 1 - 6
src/utils/request.js

@@ -21,13 +21,8 @@ export const createRequest = function(baseURL) {
   service.interceptors.request.use(
     config => {
       // do something before request is sent
-
       if (store.getters.token) {
-        // let each request carry token
-        // ['X-Token'] is a custom headers key
-        // please modify it according to the actual situation
-        // config.headers['X-Token'] = getToken()
-        config.headers['token'] = getToken()
+        config.headers['api-token'] = store.getters.token
       }
       return config
     },

+ 109 - 3
src/views/dashboard/index.vue

@@ -1,13 +1,119 @@
 <template>
-  <div>123</div>
+  <div class="main-box">
+    <div class="scroll-box">
+      <div class="main-card" >
+        <div class="hello-text">{{ getTimeWord() }},{{ user.nickname }}!</div>
+        <div class="card-content">
+          <div class="expo-text">
+            <span>距离展会结束还有</span>
+          </div>
+          <div class="quick-nav">
+            <router-link class="nav-item" to="audience">
+              <span class="icon el-icon-user"></span>
+              <span class="text">观众管理</span>
+            </router-link>
+            <router-link class="nav-item" to="preRegister">
+              <span class="icon el-icon-tickets"></span>
+              <span class="text">表单管理</span>
+            </router-link>
+            <router-link class="nav-item" to="exhibitor">
+              <span class="icon el-icon-office-building"></span>
+              <span class="text">展商管理</span>
+            </router-link>
+            <router-link class="nav-item" to="invitation">
+              <span class="icon el-icon-files"></span>
+              <span class="text">模板管理</span>
+            </router-link>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
 export default {
-  name: "dashboard"
+  name: 'Dashboard',
+  data() {
+    return {
+
+    }
+  },
+  computed: {
+    user() { return this.$store.state.user.user }
+  },
+  mounted() {
+
+  },
+  methods: {
+    getTimeWord() {
+      const time = new Date().getHours()
+      if (time >= 0 && time < 6) return '凌晨好'
+      if (time >= 6 && time < 9) return '早上好'
+      if (time >= 9 && time < 12) return '上午好'
+      if (time >= 12 && time < 14) return '中午好'
+      if (time >= 14 && time < 17) return '下午好'
+      if (time >= 17 && time < 19) return '傍晚好'
+      if (time >= 19 && time < 21) return '晚上好'
+      return '夜深了'
+    }
+  }
 }
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+@use '@/styles/variables.scss' as *;
+.main-box {
+  height: 100%;
+  width: 100%;
+  .scroll-box {
+    padding: 2px;
+    position: absolute;
+    width: calc(100% - 32px);
+    height: calc(100% - 32px);
+    overflow: hidden;
+    overflow-y: auto;
+    .main-card{
+      background: $menuActiveBg;
+      padding: 36px 36px 18px;
+      border-radius: 8px;
+      .hello-text{
+        color: $menuActiveText;
+        font-size: 28px;
+        font-weight: bold;
+      }
+      .card-content{
+        margin-top: 16px;
+        display: grid;
+        grid-template-columns: 1fr 2fr;
+        .expo-text{
 
+        }
+        .quick-nav{
+          display: flex;
+          justify-content: flex-end;
+          .nav-item{
+            transition-duration: 300ms;
+            padding: 16px 32px;
+            border-radius: 16px;
+            text-decoration: unset;
+            color: $menuActiveText;
+            display: flex;
+            flex-direction: column;
+            .icon{
+              margin-bottom: 4px;
+              font-size: 32px;
+            }
+            .text{
+              font-size: 16px;
+            }
+            &:hover{
+              background: #004DFF11;
+            }
+          }
+        }
+      }
+    }
+  }
+}
 </style>

+ 396 - 0
src/views/exhibitorManage/exhibitorList.vue

@@ -0,0 +1,396 @@
+<script>
+import Vue from 'vue'
+import { getExpoList, deleteExpo, setExpoStatus, getMyExpoInfo } from '@/api/expo'
+export default Vue.extend({
+  name: 'Index',
+  data() {
+    return {
+      expoList: [],
+      current_page: 0,
+      last_page: 1,
+      total: 0,
+      page_size: 20,
+      searchWord: '',
+      searchTimer: null,
+      loading: false,
+      popover_data: {},
+      ossUrl: process.env.VUE_APP_OSS_DOMAIN
+    }
+  },
+  mounted() {
+    this.getList()
+  },
+  methods: {
+    getDetail(row) {
+      getMyExpoInfo(row.id).then(res => {
+        this.popover_data = res.data
+        this.popover_data.images = JSON.parse(res.data.images)
+        this.popover_data.social_links = JSON.parse(res.data.social_links)
+      })
+    },
+    search(event) {
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+      }
+      this.searchTimer = setTimeout(() => {
+        this.current_page = 0
+        this.getList()
+      }, 500)
+    },
+    edit(row) {
+      console.log(row)
+      this.$router.push({ path: '/exhibitor/edit/' + row.id })
+    },
+    setStatus(row) {
+      this.loading = true
+      setExpoStatus(row.id, row.status ? 0 : 1).then(res => {
+        this.loading = false
+        console.log(res)
+        console.log(this.refresh)
+        this.refresh()
+      }).catch(err => {
+        this.loading = false
+        console.log(err)
+      })
+    },
+    del(row) {
+      this.$confirm('确定删除展商"' + row.expo_name + '"吗?此操作无法撤销!', '删除展商', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        callback: action => {
+          if (action === 'confirm') {
+            deleteExpo(row.id).then(res => {
+              this.loading = false
+              this.refresh()
+            }).catch(err => {
+              this.loading = false
+              console.log(err)
+            })
+          }
+        }
+      })
+    },
+    refresh() {
+      if (this.loading || this.current_page > this.last_page) return
+      this.loading = true
+      getExpoList(this.current_page, this.page_size, this.searchWord).then(res => {
+        console.log(res)
+        this.current_page = res.data.current_page
+        this.last_page = res.data.last_page
+        this.total = res.data.total
+        this.expoList = res.data.data
+        this.loading = false
+      })
+    },
+    getList() {
+      if (this.loading || this.current_page >= this.last_page) return
+      this.loading = true
+      getExpoList(++this.current_page, this.page_size, this.searchWord).then(res => {
+        console.log(res)
+        this.current_page = res.data.current_page
+        this.last_page = res.data.last_page
+        this.total = res.data.total
+        this.expoList = res.data.data
+        this.loading = false
+      })
+    },
+    add() {
+      this.$router.push({ path: '/exhibitor/add' })
+    },
+    goto(url) {
+      window.open(url, '_blank')
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="head">
+      <el-input 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>
+    </div>
+    <div class="body">
+      <el-table v-loading="loading" :data="expoList" height="100%" class="table">
+        <el-table-column
+          label="展商名称"
+          prop="expo_name"
+          width="300"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          label="状态"
+          width="80"
+        >
+          <template slot-scope="scope">
+            <div :class="['status','type-'+scope.row.status]">
+              {{ ['正常','禁用'][scope.row.status] }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="展会地点"
+          prop="location"
+          width="300"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          label="主办单位"
+          prop="organizer"
+          width="300"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          label="联系电话"
+          prop="contact_phone"
+          width="200"
+        />
+        <el-table-column
+          label="联系邮箱"
+          prop="contact_email"
+          width="200"
+        />
+        <el-table-column
+          label="开始时间"
+          prop="start_date"
+          width="200"
+        />
+        <el-table-column
+          label="结束时间"
+          prop="end_date"
+          width="200"
+        />
+        <el-table-column
+          label="最后编辑"
+          prop="update_time"
+          width="200"
+        />
+        <el-table-column
+          label="操作"
+          fixed="right"
+          width="200"
+        >
+          <template slot-scope="scope">
+            <el-popover popper-class="popover" trigger="click" placement="left" width="600" @show="popover_data={}" @after-enter="getDetail(scope.row)">
+              <div class="expo-info">
+                <div class="cover loading">
+                  <img v-if="popover_data.images" :src="ossUrl+popover_data.images[0]">
+                </div>
+                <div class="info-body">
+                  <div class="avatar-name">
+                    <div class="avatar loading">
+                      <img v-if="popover_data.logo" :src="ossUrl+popover_data.logo">
+                    </div>
+                    <div class="name-cont">
+                      <div :class="['name',popover_data.expo_name!==undefined?'':'loading']">{{ popover_data.expo_name }}</div>
+                      <div :class="['sub-name',popover_data.organizer!==undefined?'':'loading']">{{ popover_data.organizer }}</div>
+                      <div class="contact">
+                        <div class="phone">
+                          <i class="icon el-icon-mobile-phone" />
+                          <div :class="['phone-num link',popover_data.contact_phone!==undefined?'':'loading']" @click="goto('tel:'+popover_data.contact_phone)">{{ popover_data.contact_phone }}</div>
+                        </div>
+                        <div class="email">
+                          <i class="icon el-icon-message" />
+                          <div :class="['email-addr link',popover_data.contact_email!==undefined?'':'loading']" @click="goto('mailto:'+popover_data.contact_email)">{{ popover_data.contact_email }}</div>
+                        </div>
+                        <div class="date">
+                          <i class="icon el-icon-date" />
+                          <div :class="['date-range',popover_data.start_date!==undefined?'':'loading']">{{ popover_data.start_date }} - {{ popover_data.end_date }}</div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                  <div :class="['desc',popover_data.content!==undefined?'':'loading']">
+                    {{ popover_data.content }}
+                  </div>
+                  <div v-if="popover_data.social_links" class="social_links">
+                    <div class="social-title">社交媒体</div>
+                    <div v-if="popover_data.social_links.facebook" class="social-item">
+                      <span>facebook:</span><span class="link" @click="goto(popover_data.social_links.facebook)">{{ popover_data.social_links.facebook }}</span>
+                    </div>
+                    <div v-if="popover_data.social_links.twitter" class="social-item">
+                      <span>twitter:</span><span class="link" @click="goto(popover_data.social_links.twitter)">{{ popover_data.social_links.twitter }}</span>
+                    </div>
+                    <div v-if="popover_data.social_links.linkedin" class="social-item">
+                      <span>linkedin:</span><span class="link" @click="goto(popover_data.social_links.linkedin)">{{ popover_data.social_links.linkedin }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <span slot="reference" class="button">预览</span>
+            </el-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>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="foot">
+      <el-pagination
+        background
+        :page-size="page_size"
+        layout="total"
+        :total="total"
+      />
+      <el-pagination
+        background
+        :page-size="page_size"
+        layout="prev, pager, next"
+        :total="total"
+      />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  @use '@/styles/variables.scss' as *;
+  .main-box{
+    height: 100%;
+    display: grid;
+    grid-template-rows: auto 1fr auto;
+    grid-gap: 24px;
+    .head{
+      display: flex;
+      .input{
+        width: 50%;
+        margin-right: auto;
+      }
+    }
+    .body{
+      height: 100%;
+      position: relative;
+      .table{
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        .button{
+          cursor: pointer;
+          padding: 0 5px;
+          color: $menuActiveText;
+          &.del{
+            color: #DC2626;
+          }
+        }
+        .status{
+          padding: 0 12px;
+          border-radius: 32px;
+          width: fit-content;
+          &.type-0{
+            background: #DCFCE7;
+            color: #166534;
+          }
+          &.type-1{
+            background: #FEE2E2;
+            color: #991B1B;
+          }
+        }
+      }
+    }
+    .foot{
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+</style>
+<style lang="scss">
+  .popover{
+    padding: 0;
+    .expo-info{
+      position: relative;
+      width: 100%;
+      .link{
+        cursor: pointer;
+        text-decoration: underline;
+      }
+      .info-body{
+        padding: 16px;
+        .social_links{
+          margin-top: 12px;
+          .social-title{
+            font-weight: bold;
+          }
+          .social-item{
+
+          }
+        }
+        .desc{
+          margin-top: 12px;
+          &.loading{
+            height: 60px;
+            width: 100%;
+          }
+        }
+        .avatar-name{
+          display: flex;
+          grid-gap: 8px;
+          .name-cont{
+            flex: 1;
+            .name{
+              font-size: 20px;
+              font-weight: bold;
+              &.loading{
+                height: 26px;
+                width: 120px;
+              }
+            }
+            .sub-name{
+              color: gray;
+              &.loading{
+                margin-top: 8px;
+                margin-bottom: 8px;
+                height: 20px;
+                width: 160px;
+              }
+            }
+            .contact{
+              flex-wrap: wrap;
+              display: flex;
+              grid-gap: 0 12px;
+              .phone,.email,.date{
+                display: flex;
+                align-items: center;
+                grid-gap: 4px;
+                div{
+                  &.loading{
+                    height: 16px;
+                    width: 120px;
+                  }
+                }
+              }
+            }
+          }
+          .avatar{
+            width: 70px;
+            height: 70px;
+            border-radius: 50%;
+            overflow: hidden;
+            img{
+              display: block;
+              object-fit: cover;
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+      }
+      .cover{
+        width: 100%;
+        aspect-ratio: 3/1;
+        img{
+          display: block;
+          object-fit: cover;
+          width: 100%;
+          height: 100%;
+        }
+      }
+    }
+  }
+</style>

+ 456 - 0
src/views/exhibitorManage/exhibitorSetting.vue

@@ -0,0 +1,456 @@
+<script>
+import Vue from 'vue'
+import { saveExpo, getMyExpoInfo } from '@/api/expo'
+import { upload } from '@/api/system'
+export default Vue.extend({
+  name: 'ExhibitorSetting',
+  data() {
+    return {
+      exhibitorSetting: {
+        id: '',
+        expo_name: '',
+        start_date: '',
+        end_date: '',
+        location: '',
+        organizer: '',
+        contact_phone: '',
+        contact_email: '',
+        content: '',
+        logo: '/common/2025/0915/68c7b25cc9e26.webp',
+        images: ['/common/2025/0915/68c7b263bc4b1.webp'],
+        social_links: {
+          facebook: '',
+          twitter: '',
+          linkedin: ''
+        },
+        form_template_id: '',
+        page_template_id: '',
+        urla: '',
+        seo_title: '',
+        seo_description: '',
+        seo_keywords: ''
+      },
+      loading: false,
+      ossUrl: process.env.VUE_APP_OSS_DOMAIN
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  methods: {
+    init() {
+      if (this.$route.params.id) {
+        this.loading = true
+        this.exhibitorSetting.id = this.$route.params.id
+        getMyExpoInfo(this.exhibitorSetting.id).then(res => {
+          this.exhibitorSetting = res.data
+          this.exhibitorSetting.images = JSON.parse(this.exhibitorSetting.images)
+          this.exhibitorSetting.social_links = JSON.parse(this.exhibitorSetting.social_links)
+          this.loading = false
+          console.log(res)
+        }).catch(err => {
+          this.loading = false
+          console.log(err)
+        })
+      }
+    },
+    uploadImage(event, type) {
+      this.loading = true
+      console.log(event.target.files[0])
+      upload(event.target.files[0]).then(res => {
+        console.log(res)
+        if (type === 'logo') {
+          this.exhibitorSetting.logo = res.data.file
+        }
+        if (type === 'cover') {
+          this.exhibitorSetting.images[0] = res.data.file
+          this.exhibitorSetting.images.push()
+        }
+        this.loading = false
+      }).catch(err => {
+        this.loading = false
+        console.log(err)
+        this.$message.error('上传图片时遇到问题')
+      })
+    },
+    save() {
+      if (
+        this.exhibitorSetting.expo_name === '' ||
+        this.exhibitorSetting.start_date === '' ||
+        this.exhibitorSetting.end_date === '' ||
+        this.exhibitorSetting.location === '' ||
+        this.exhibitorSetting.organizer === '' ||
+        this.exhibitorSetting.contact_phone === '' ||
+        this.exhibitorSetting.contact_email === '' ||
+        this.exhibitorSetting.urla === ''
+      ) {
+        this.$message.error('请将信息填写完整')
+        return
+      }
+      if (this.loading) {
+        return
+      }
+      this.loading = true
+      saveExpo(
+        this.exhibitorSetting.id,
+        this.exhibitorSetting.expo_name,
+        this.exhibitorSetting.start_date,
+        this.exhibitorSetting.end_date,
+        this.exhibitorSetting.location,
+        this.exhibitorSetting.organizer,
+        this.exhibitorSetting.contact_phone,
+        this.exhibitorSetting.contact_email,
+        this.exhibitorSetting.content,
+        this.exhibitorSetting.logo,
+        this.exhibitorSetting.images,
+        this.exhibitorSetting.social_links,
+        this.exhibitorSetting.form_template_id,
+        this.exhibitorSetting.page_template_id,
+        this.exhibitorSetting.urla,
+        this.exhibitorSetting.seo_title,
+        this.exhibitorSetting.seo_description,
+        this.exhibitorSetting.seo_keywords
+      ).then(res => {
+        console.log(res)
+        this.$message.success('保存成功')
+        this.loading = false
+        this.$router.push('/exhibitor/list')
+      }).catch(err => {
+        console.log(err)
+        this.loading = false
+        this.$message.error('保存失败')
+      })
+    }
+  }
+})
+</script>
+
+<template>
+  <div v-loading="loading" class="main-box">
+    <div class="save">
+      <el-button type="primary" @click="save">{{ exhibitorSetting.id?'保存修改':'新建展商' }}</el-button>
+    </div>
+    <div class="info">
+      <div class="scroll">
+        <div class="form-item required">
+          <div class="label">展会名称 </div>
+          <el-input v-model="exhibitorSetting.expo_name" class="input" placeholder="请输入展会名称" />
+        </div>
+        <div class="form-item required">
+          <div class="label">展会时间</div>
+          <div class="time-cont">
+            <el-date-picker
+              v-model="exhibitorSetting.start_date"
+              type="date"
+              placeholder="开始时间"
+              value-format="yyyy-MM-dd"
+              style="width: 100%;"
+            />
+            <el-date-picker
+              v-model="exhibitorSetting.end_date"
+              type="date"
+              placeholder="结束时间"
+              value-format="yyyy-MM-dd"
+              style="width: 100%;"
+            />
+          </div>
+        </div>
+        <div class="form-item required">
+          <div class="label">展会地点</div>
+          <el-input v-model="exhibitorSetting.location" class="input" placeholder="请输入展会地点" />
+        </div>
+        <div class="form-item required">
+          <div class="label">主办单位</div>
+          <el-input v-model="exhibitorSetting.organizer" class="input" placeholder="请输入主办单位" />
+        </div>
+        <div class="form-item required">
+          <div class="label">联系电话</div>
+          <el-input v-model="exhibitorSetting.contact_phone" class="input" placeholder="请输入联系电话" />
+        </div>
+        <div class="form-item required">
+          <div class="label">联系邮箱</div>
+          <el-input v-model="exhibitorSetting.contact_email" class="input" placeholder="请输入联系邮箱" />
+        </div>
+        <div class="form-item">
+          <div class="label">社交账号</div>
+          <div class="social-list">
+            <div class="social-item">
+              <el-input v-model="exhibitorSetting.social_links.facebook" placeholder="请输入facebook主页地址">
+                <template slot="append">facebook</template>
+              </el-input>
+            </div>
+            <div class="social-item">
+              <el-input v-model="exhibitorSetting.social_links.twitter" placeholder="请输入twitter主页地址">
+                <template slot="append">twitter</template>
+              </el-input>
+            </div>
+            <div class="social-item">
+              <el-input v-model="exhibitorSetting.social_links.linkedin" placeholder="请输入linkedin主页地址">
+                <template slot="append">linkedin</template>
+              </el-input>
+            </div>
+          </div>
+        </div>
+        <div class="form-item required">
+          <div class="label">url短名称</div>
+          <el-input v-model="exhibitorSetting.urla" class="input" placeholder="请输入url" />
+        </div>
+        <div class="form-item">
+          <div class="label">SEO标题</div>
+          <el-input v-model="exhibitorSetting.seo_title" class="input" placeholder="请输入SEO标题" />
+        </div>
+        <div class="form-item">
+          <div class="label">SEO关键字</div>
+          <el-input v-model="exhibitorSetting.seo_keywords" class="input" placeholder="请输入SEO关键字" />
+        </div>
+        <div class="form-item">
+          <div class="label">SEO描述</div>
+          <el-input v-model="exhibitorSetting.seo_description" class="input" placeholder="请输入SEO描述" />
+        </div>
+      </div>
+    </div>
+    <div class="desc">
+      <div class="scroll">
+        <div class="vision-cont">
+          <img :src="ossUrl + exhibitorSetting.images[0]" alt="" class="image loading">
+          <div class="avatar-name">
+            <img :src="ossUrl + exhibitorSetting.logo" alt="" class="avatar loading">
+            <div>
+              <div class="name">
+                {{ exhibitorSetting.expo_name||'示例展会名称' }}
+              </div>
+              <div class="exhibitor">
+                {{ exhibitorSetting.organizer||'示例主办方名称' }}
+              </div>
+            </div>
+            <div class="avatar-upload">
+              <div class="upload-icon">
+                <span class="el-icon-upload icon" />
+                <span class="text">上传logo</span>
+              </div>
+              <input type="file" class="uploader" @change="uploadImage($event,'logo')">
+            </div>
+          </div>
+          <div class="cover-upload">
+            <div class="upload-icon">
+              <span class="el-icon-upload icon" />
+              <span class="text">上传主视觉图</span>
+            </div>
+            <input type="file" class="uploader" @change="uploadImage($event,'cover')">
+          </div>
+        </div>
+        <el-input v-model="exhibitorSetting.content" rows="12" type="textarea" class="text-area" placeholder="请输入展会介绍" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .main-box{
+    height: 100%;
+    width: 100%;
+    display: grid;
+    grid-gap: 16px;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: auto 1fr;
+    .save{
+      display: flex;
+      justify-content: flex-end;
+      grid-column: span 2;
+    }
+    .info{
+      position: relative;
+      height: 100%;
+      width: 100%;
+      .scroll{
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+        overflow: hidden;
+        overflow-y: auto;
+      }
+      .form-item{
+        margin: 16px 0;
+        .label{
+          margin-bottom: 8px;
+        }
+        .social-list{
+          margin-bottom: 12px;
+          display: flex;
+          flex-direction: column;
+          grid-gap: 8px;
+          .drag-cont{
+            display:block;
+            min-height: 60px;
+            .social-item{
+              position: relative;
+              margin-bottom: 6px;
+              .handel{
+                color: gray;
+                height: 100%;
+                width: 20px;
+                position: absolute;
+                top: 0;
+                left: 0;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                cursor: move;
+              }
+              .el-select{
+                width: 160px;
+              }
+            }
+          }
+        }
+        .time-cont{
+          display: grid;
+          grid-template-columns: 1fr 1fr;
+          grid-gap: 16px;
+        }
+        &.required{
+          .label::after{
+            content: '*';
+            color: red;
+            margin-left: 4px;
+          }
+        }
+      }
+    }
+    .desc{
+      height: 100%;
+      width: 100%;
+      position: relative;
+      .scroll{
+        padding: 8px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+        overflow: hidden;
+        overflow-y: auto;
+      }
+      .text-area{
+        margin-top: 24px;
+      }
+      .vision-cont{
+        position: relative;
+        box-shadow: 0 0 8px 0 #00000022;
+        overflow: hidden;
+        width: 100%;
+        border-radius: 16px;
+        transition-duration: 300ms;
+        &:hover{
+          transform: translateY(-2px);
+          box-shadow: 0 2px 12px 0 #00000022;
+        }
+        .cover-upload{
+          position: absolute;
+          top: 0;
+          left: 0;
+          width: 100%;
+          aspect-ratio: 2.4;
+          .upload-icon{
+            overflow: hidden;
+            transition-duration: 300ms;
+            opacity: 0;
+            color: gray;
+            width: 100%;
+            height: 100%;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            backdrop-filter: blur(0px);
+            .icon{
+              font-size: 48px;
+            }
+          }
+          &:hover{
+            .upload-icon{
+              backdrop-filter: blur(8px);
+              opacity: 1;
+            }
+          }
+          .uploader{
+            opacity: 0;
+            width: 100%;
+            height: 100%;
+            position: absolute;
+            left: 0;
+            top: 0;
+          }
+        }
+        .avatar-name{
+          position: relative;
+          padding: 24px;
+          display: grid;
+          grid-template-columns: auto 1fr;
+          align-items: center;
+          grid-gap: 24px;
+          .avatar-upload{
+            position: absolute;
+            top: 24px;
+            left: 24px;
+            width: 80px;
+            height: 80px;
+            .upload-icon{
+              border-radius: 50%;
+              transition-duration: 300ms;
+              opacity: 0;
+              color: gray;
+              width: 100%;
+              height: 100%;
+              display: flex;
+              flex-direction: column;
+              align-items: center;
+              justify-content: center;
+              .icon{
+                margin-top: -8px;
+                font-size: 32px;
+              }
+              .text{
+                line-height: 1;
+                font-size: 12px;
+              }
+            }
+            &:hover{
+              .upload-icon{
+                backdrop-filter: blur(8px);
+                opacity: 1;
+              }
+            }
+            .uploader{
+              opacity: 0;
+              width: 100%;
+              height: 100%;
+              position: absolute;
+              left: 0;
+              top: 0;
+            }
+          }
+          .avatar{
+            width: 80px;
+            height: 80px;
+            border-radius: 50%;
+          }
+          .name{
+            font-size: 24px;
+            font-weight: bold;
+          }
+          .exhibitor{
+            color: gray;
+          }
+        }
+        .image{
+          object-fit: cover;
+          width: 100%;
+          aspect-ratio: 2.4;
+        }
+      }
+    }
+  }
+</style>

+ 15 - 0
src/views/exhibitorManage/index.vue

@@ -0,0 +1,15 @@
+<script>
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: "index"
+})
+</script>
+
+<template>
+  <router-view></router-view>
+</template>
+
+<style scoped>
+
+</style>

+ 320 - 0
src/views/invitationManage/edit.vue

@@ -0,0 +1,320 @@
+<script>
+import Vue from 'vue'
+import domtoimage from 'dom-to-image-more'
+import * as monaco from 'monaco-editor'
+import { upload } from '@/api/system'
+import { saveTemplate } from '@/api/template'
+
+export default Vue.extend({
+  name: 'Index',
+  components: {
+  },
+  props: [
+    'isCompletionInit'
+  ],
+  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">
+                {{exhibitorName}}
+            </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">
+                {{startTime}} - {{endTime}}
+            </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">
+                400-000-0000 | {{address}}
+            </td>
+            <td bgcolor="#ffffff"></td>
+        </tr>
+        <tr>
+            <td bgcolor="#ffffff"></td>
+            <td bgcolor="#ffffff" height="12px"></td>
+            <td bgcolor="#ffffff"></td>
+        </tr>
+    </tbody>
+</table>`,
+      viewCode: '',
+      exhibitorSetting: {
+        name: '测试名称',
+        startTime: '测试时间',
+        endTime: '测试时间',
+        address: '测试地址',
+        exhibitorName: '测试数据',
+        telephone: '测试电话',
+        email: '测试邮件',
+        socialMedia: [],
+        mainPicture: '/static/image/cover.webp',
+        logo: '/static/image/avatar.webp',
+        description: ''
+      },
+      timer: null,
+      editor: null,
+      templateInfo: {
+        id: '',
+        name: '未命名表单',
+        description: '',
+        pic: ''
+      },
+      loading: false
+    }
+  },
+  mounted() {
+    this.init()
+    this.initEditor()
+    this.parseCode()
+  },
+  methods: {
+    init() {
+      if (this.$route.params.id) {
+        this.loading = true
+        this.templateInfo.id = this.$route.params.id
+        let templateInfo = JSON.parse(sessionStorage.getItem('invitationInfo'))
+        this.templateInfo.name = templateInfo.name
+        this.templateInfo.description = templateInfo.description
+        this.code = templateInfo.content
+        console.log(templateInfo)
+        this.loading = false
+      }
+    },
+    saveTemp() {
+      if (this.loading) { return }
+      this.loading = true
+      domtoimage
+        .toPng(document.querySelector('#invitation-viewer'))
+        .then((dataUrl) => {
+          var arr = dataUrl.split(','); var mime = arr[0].match(/:(.*?);/)[1]
+          var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n)
+          while (n--) {
+            u8arr[n] = bstr.charCodeAt(n)
+          }
+          const picFile = new File([u8arr], 'invitation.png', { type: mime })
+          upload(picFile).then(res => {
+            saveTemplate(this.templateInfo.id, this.templateInfo.name, this.templateInfo.description, this.code , res.data.file)
+              .then(res => {
+                console.log(res)
+                this.loading = false
+              }).catch(err => {
+                this.loading = false
+              })
+          }).catch(err => {
+            this.loading = false
+          })
+        })
+    },
+    parseCode() {
+      if (this.timer) { clearTimeout(this.timer) }
+      this.timer = setTimeout(() => {
+        let processedCode = this.code
+        const regex = /\{\{([^}]+)\}\}/g
+        processedCode = processedCode.replace(regex, (match, key) => {
+          const trimmedKey = key.trim()
+          if (Object.prototype.hasOwnProperty.call(this.exhibitorSetting, trimmedKey)) {
+            return this.exhibitorSetting[trimmedKey]
+          }
+          return match
+        })
+        this.viewCode = processedCode
+      }, 500)
+    },
+    initEditor() {
+      console.log(this.$isCompletionInit)
+      if (this.$getIsInit() === false) {
+        this.$setIsInit()
+        monaco.languages.registerCompletionItemProvider('html', {
+          triggerCharacters: ['{'],
+          provideCompletionItems: (model, position) => {
+            const textUntilPosition = model.getValueInRange({
+              startLineNumber: position.lineNumber,
+              startColumn: 1,
+              endLineNumber: position.lineNumber,
+              endColumn: position.column
+            })
+            const match = textUntilPosition.match(/\{\{$/)
+            if (!match) {
+              return { suggestions: [] }
+            }
+            const suggestions = Object.keys(this.exhibitorSetting).map(key => {
+              return {
+                label: key,
+                kind: monaco.languages.CompletionItemKind.Field,
+                insertText: key,
+                detail: `插入展会数据: ${key}`,
+                range: {
+                  startLineNumber: position.lineNumber,
+                  startColumn: position.column,
+                  endLineNumber: position.lineNumber,
+                  endColumn: position.column
+                }
+              }
+            })
+            return { suggestions }
+          }
+        })
+      }
+      this.editor = monaco.editor.create(document.getElementById('editor'), {
+        value: this.code,
+        language: 'html',
+        theme: 'vs-light',
+        automaticLayout: true,
+        minimap: {
+          enabled: false
+        }
+      })
+      this.editor.onDidChangeModelContent(() => {
+        this.code = this.editor.getValue()
+        this.parseCode()
+      })
+    },
+    popoverOpen() {
+
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="main-box" v-loading="loading">
+    <div class="head">
+      <el-input v-model="templateInfo.name" class="name" />
+      <el-popover
+        popper-class="popover"
+        placement="left-start"
+        width="500"
+        trigger="click"
+        @after-enter="popoverOpen"
+      >
+        <div class="body">
+          <span class="label">模板名称</span>
+          <el-input 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>
+        </div>
+        <el-button slot="reference" icon="el-icon-plus" type="primary">保存模板</el-button>
+      </el-popover>
+
+    </div>
+    <div class="body">
+      <div class="editor">
+        <div id="editor" class="editor-box" />
+      </div>
+      <div class="viewer">
+        <div class="viewer-box">
+          <div id="invitation-viewer" v-html="viewCode" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.main-box{
+  height: 100%;
+  width: 100%;
+  display: grid;
+  grid-template-rows: auto 1fr;
+  grid-gap: 24px;
+  .body{
+    height: 100%;
+    position: relative;
+    display: grid;
+    grid-gap: 16px;
+    grid-template-columns: 1fr 1fr;
+    .editor{
+      border: 1px lightgray solid;
+      border-radius: 8px;
+      overflow: hidden;
+      position: relative;
+      .editor-box{
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+      }
+    }
+    .viewer{
+      overflow: hidden;
+      border: 1px lightgray solid;
+      border-radius: 8px;
+      position: relative;
+      .viewer-box{
+        overflow: hidden;
+        overflow-y: auto;
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+      }
+    }
+  }
+  .head{
+    display: flex;
+    justify-content: space-between;
+    .name{
+      width: 50%;
+    }
+  }
+}
+</style>
+<style>
+.popover{
+  .body{
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    .button{
+      margin-top: 16px;
+    }
+  }
+}
+</style>

+ 3 - 112
src/views/invitationManage/index.vue

@@ -1,4 +1,4 @@
-<script>
+<script lang="ts">
 import Vue from 'vue'
 
 export default Vue.extend({
@@ -7,118 +7,9 @@ export default Vue.extend({
 </script>
 
 <template>
-  <div class="main-box">
-    <div class="scroll-box">
-      <div class="model-cont">
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-        <div class="model-box">
-          <div class="image loading">
-            <img src="" alt="">
-          </div>
-          <div class="info-box">
-            <div class="name">科技风格邀请函</div>
-            <div class="time">更新时间:2025-08-04</div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
+  <router-view></router-view>
 </template>
 
 <style scoped>
-.main-box{
-  height: 100%;
-  width: 100%;
-  .scroll-box{
-    padding: 2px;
-    position: absolute;
-    width: calc(100% - 18px);
-    height: calc(100% - 24px);
-    overflow: hidden;
-    overflow-y: auto;
-    .model-cont{
-      display: grid;
-      grid-template-columns: repeat(3, 1fr);
-      grid-gap: 24px;
-      .model-box{
-        overflow: hidden;
-        height: 500px;
-        border-radius: 8px;
-        box-shadow: 0 1px 2px 0 #00000022;
-        transition-duration: 300ms;
-        .image{
-          height: 400px;
-          width: 100%;
-          img{
-            height: 100%;
-            width: 100%;
-            object-fit: cover;
-          }
-        }
-        .info-box{
-          height: 100px;
-          width: 100%;
-          padding: 16px;
-          display: flex;
-          flex-direction: column;
-          justify-content: space-between;
-          .name{
-            font-size: 23px;
-            font-weight: bold;
-          }
-          .time{
-            color: #6B7280;
-          }
-        }
-        &:hover{
-          scale: 1.02;
-        }
-      }
-    }
-  }
-}
+
 </style>

+ 162 - 0
src/views/invitationManage/list.vue

@@ -0,0 +1,162 @@
+<script>
+import Vue from 'vue'
+import { getTemplateList } from '@/api/template'
+export default Vue.extend({
+  name: 'Index',
+  data() {
+    return {
+      current_page: 0,
+      last_page: 1,
+      total: 0,
+      page_size: 20,
+      loading: false,
+      tempList: [],
+      ossUrl: process.env.VUE_APP_OSS_DOMAIN
+    }
+  },
+  mounted() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      if (this.loading) return
+      this.loading = true
+      getTemplateList(++this.current_page, this.page_size).then(res => {
+        this.tempList = res.data.data
+        this.total = res.data.total
+      }).then(res => {
+
+      })
+        .catch(err => {
+
+        })
+    },
+    gotoAdd() {
+      this.$router.push({ path: '/invitation/add' })
+    },
+    gotoEdit(item) {
+      sessionStorage.setItem('invitationInfo', JSON.stringify(item))
+      this.$router.push({ path: '/invitation/edit/' + item.id })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="head">
+      <el-button 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="image loading">
+              <img class="bg" :src="ossUrl+item.pic" alt="">
+              <img class="pic" :src="ossUrl+item.pic" alt="">
+            </div>
+            <div class="info-box">
+              <div class="name">{{ item.name }}</div>
+              <div class="time">更新时间:{{ item.update_time }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="foot">
+      <el-pagination
+        background
+        :page-size="page_size"
+        layout="total"
+        :total="total"
+      />
+      <el-pagination
+        background
+        :page-size="page_size"
+        layout="prev, pager, next"
+        :total="total"
+      />
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.main-box{
+  height: 100%;
+  width: 100%;
+  display: grid;
+  grid-template-rows: auto 1fr auto;
+  grid-gap: 24px;
+  .body{
+    height: 100%;
+    position: relative;
+    .scroll-box{
+      padding: 2px;
+      position: absolute;
+      width: calc(100% + 13px);
+      height: 100%;
+      overflow: hidden;
+      overflow-y: auto;
+      .model-cont{
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        grid-gap: 24px;
+        .model-box{
+          overflow: hidden;
+          height: 500px;
+          border-radius: 8px;
+          box-shadow: 0 1px 2px 0 #00000022;
+          transition-duration: 300ms;
+          .image{
+            overflow: hidden;
+            position: relative;
+            height: 360px;
+            width: 100%;
+            img{
+              height: 100%;
+              width: 100%;
+              position: absolute;
+              left: 0;
+              top: 0;
+              &.bg{
+                scale: 1.1;
+                object-fit: cover;
+                filter: blur(10px);
+              }
+              &.pic{
+                object-fit: contain;
+              }
+            }
+          }
+          .info-box{
+            height: 100px;
+            width: 100%;
+            padding: 16px;
+            display: flex;
+            flex-direction: column;
+            justify-content: space-between;
+            .name{
+              font-size: 23px;
+              font-weight: bold;
+            }
+            .time{
+              color: #6B7280;
+            }
+          }
+          &:hover{
+            scale: 1.02;
+          }
+        }
+      }
+    }
+  }
+  .head{
+    display: flex;
+    justify-content: flex-end;
+  }
+  .foot{
+    display: flex;
+    justify-content: space-between;
+  }
+}
+</style>

+ 35 - 16
src/views/login/index.vue

@@ -2,35 +2,52 @@
 import Vue from 'vue'
 
 export default Vue.extend({
-  name: "index",
+  name: 'Index',
   data() {
     return {
       username: '',
       password: '',
-      autoLogin: false,
+      savePassword: false,
+      loading: false,
       env: process.env.NODE_ENV
     }
   },
   mounted() {
+    this.getSavedAccount()
   },
   methods: {
+    getSavedAccount() {
+      const account = localStorage.getItem('savedAccount')
+      if (account) {
+        this.username = account.username
+        this.password = account.password
+        this.savePassword = true
+      }
+    },
     login() {
+      if (!this.username) {
+        this.$message.error('请输入用户名')
+        return
+      }
+      if (!this.password) {
+        this.$message.error('请输入密码')
+        return
+      }
+      if (this.loading) {
+        return
+      }
+      this.loading = true
       this.$store.dispatch('login', {
         username: this.username,
-        password: this.password
+        password: this.password,
+        savePassword: this.savePassword
       }).then(res => {
+        this.loading = false
         this.$router.push('/')
       }).catch(err => {
-        console.log(err)
+        this.loading = false
         this.$message.error(err.message)
       })
-    },
-    testLogin() {
-      this.$store.dispatch('testLogin', { token: this.username }).then(res => {
-        this.$router.push('/')
-      }).catch(err => {
-        this.$message.success(err.message)
-      })
     }
   }
 })
@@ -45,11 +62,13 @@ export default Vue.extend({
       </div>
       <div class="cont-right">
         <div class="title">登录</div>
-        <el-input prefix-icon="el-icon-user" placeholder="请输入用户名" class="input" v-model="username"></el-input>
-        <el-input prefix-icon="el-icon-unlock" placeholder="请输入密码" show-password class="input" v-model="password"></el-input>
-        <el-checkbox v-model="autoLogin">下次自动登录</el-checkbox>
-        <el-button @click="login" type="primary" class="button">登录</el-button>
-        <el-button v-if="env === 'development'" @click="testLogin" class="test-button">用户名作为token测试登录</el-button>
+        <el-input v-model="username" prefix-icon="el-icon-user" placeholder="请输入用户名" class="input" />
+        <el-input v-model="password" prefix-icon="el-icon-unlock" placeholder="请输入密码" show-password class="input" />
+        <el-checkbox v-model="savePassword">记住密码</el-checkbox>
+        <el-button type="primary" class="button" @click="login">
+          <span>登录</span>
+          <span class="el-icon-loading" v-if="loading"></span>
+        </el-button>
       </div>
     </div>
   </div>

+ 177 - 349
src/views/preRegManage/edit.vue

@@ -1,12 +1,9 @@
 <script lang="ts">
 import Vue from 'vue'
+import { saveForm, getFormInfo } from '@/api/form'
 import guide from '@/views/guide/index'
 import draggable from 'vuedraggable'
 import countryCode from '@/lib/countryCode.json'
-import {
-  pcTextArr,
-  pcaTextArr
-} from "element-china-area-data"
 
 export default Vue.extend({
   name: 'Edit',
@@ -18,169 +15,238 @@ export default Vue.extend({
     return {
       compList: [
         {
-          type: 'tips',
-          label: '将我拖拽至右边试试吧',
-          width: 6,
+          type: 'input',
+          name: 'first_name',
+          label: '请在下方输入名字',
+          value: '',
+          width: 1,
+          placeholder: '请输入名字',
           required: false
         },
         {
           type: 'input',
-          label: '请在下方输入内容',
+          name: 'last_name',
+          label: '请在下方输入姓氏',
           value: '',
-          width: 6,
-          placeholder: '请输入',
-          pattern: '^[\\s\\S]*$',
+          width: 1,
+          placeholder: '请输入姓氏',
+          required: false
+        },
+        {
+          type: 'select',
+          name: 'id_type',
+          label: '请在下方选择证件类型',
+          value: '',
+          width: 1,
+          options: [
+            { label: '身份证', value: 'id' },
+            { label: '护照', value: 'passport' },
+          ],
+          placeholder: '请选择',
+          required: false
+        },
+        {
+          type: 'input',
+          name: 'id_number',
+          label: '请在下方输入证件号码',
+          value: '',
+          width: 1,
+          placeholder: '请输入证件号码',
           required: false
         },
         {
           type: 'phone',
+          name: 'mobile',
           label: '请在下方输入手机号码',
           value: '',
           country: 'CN',
-          width: 6,
+          width: 1,
           placeholder: '请输入手机号',
           required: false
         },
         {
           type: 'email',
+          name: 'email',
           label: '请在下方输入邮箱',
           value: '',
-          width: 6,
+          width: 1,
           placeholder: '请输入邮箱',
           codePlaceholder: '请输入验证码',
           required: false
         },
         {
-          type: 'select',
-          label: '请在下方选择内容',
+          type: 'input',
+          name: 'company',
+          label: '请在下方输入公司名称',
           value: '',
           width: 6,
-          options: [
-            { label: '东坡肉', value: '东坡肉' },
-            { label: '酱肘子', value: '酱肘子' },
-            { label: '白切鸡', value: '白切鸡' }
-          ],
-          placeholder: '请选择',
+          placeholder: '请输入公司名称',
           required: false
         },
         {
-          type: 'checkbox',
-          label: '请选择一至多项内容',
+          type: 'input',
+          name: 'department',
+          label: '请在下方输入部门名称',
           value: '',
-          width: 6,
-          options: [
-            { label: '东坡肉', value: '东坡肉' },
-            { label: '酱肘子', value: '酱肘子' },
-            { label: '白切鸡', value: '白切鸡' }
-          ],
+          width: 1,
+          placeholder: '请输入部门名称',
           required: false
         },
         {
-          type: 'radio',
-          label: '请选择一项内容',
+          type: 'select',
+          name: 'position',
+          label: '请在下方选择职位',
           value: '',
-          width: 6,
+          width: 1,
           options: [
-            { label: '东坡肉', value: '东坡肉' },
-            { label: '酱肘子', value: '酱肘子' },
-            { label: '白切鸡', value: '白切鸡' }
+            { label: '职位', value: '职位' }
           ],
+          placeholder: '请选择',
           required: false
         },
         {
-          type: 'number',
-          label: '请在下方输入数字',
-          max: 10,
-          min: 0,
-          step: 1,
-          value: 0,
-          width: 6,
-          required: false
-        },
-        {
-          type: 'slider',
-          label: '请在下方滑动',
-          max: 10,
-          min: 0,
-          step: 1,
-          value: 0,
-          width: 6,
-          required: false
-        },
-        {
-          type: 'textarea',
-          label: '请在下方输入内容',
+          type: 'select',
+          name: 'country',
+          label: '请在下方选择国家',
           value: '',
-          maxRows: 4,
-          minRows: 2,
-          width: 6,
-          placeholder: '请输入',
-          required: false,
-          pattern: '^[\\s\\S]*$'
-        },
-        {
-          type: 'region',
-          label: '请在下方选择位置',
-          range: 2,
-          width: 6,
-          value: [],
-          placeholder: '请选择所在区域',
+          width: 1,
+          options: [
+            { label: '国家', value: '国家' }
+          ],
+          placeholder: '请选择',
           required: false
         },
         {
-          type: 'time',
-          label: '请在下方选择时间',
+          type: 'select',
+          name: 'province',
+          label: '请在下方选择省份',
           value: '',
-          width: 6,
-          placeholder: '请选择时间',
+          width: 1,
+          options: [
+            { label: '省份', value: '省份' }
+          ],
+          placeholder: '请选择',
           required: false
         },
         {
-          type: 'timeRange',
-          label: '请在下方选择时间范围',
+          type: 'select',
+          name: 'city',
+          label: '请在下方选择市',
           value: '',
-          width: 6,
-          rangeSeparator: '至',
-          placeholder: '开始时间',
-          endPlaceholder: '结束时间',
+          width: 1,
+          options: [
+            { label: '市', value: '市' }
+          ],
+          placeholder: '请选择',
           required: false
         },
         {
-          type: 'date',
-          label: '请在下方选择日期',
+          type: 'input',
+          name: 'address',
+          label: '请在下方输入地址',
           value: '',
-          width: 6,
-          placeholder: '请选择日期',
+          width: 1,
+          placeholder: '请输入地址',
           required: false
         },
         {
-          type: 'dateRange',
-          label: '请在下方选择日期范围',
+          type: 'select',
+          name: 'industry',
+          label: '请在下方选择行业',
           value: '',
-          width: 6,
-          rangeSeparator: '至',
-          placeholder: '开始日期',
-          endPlaceholder: '结束日期',
+          width: 1,
+          options: [
+            { label: '行业', value: '行业' }
+          ],
+          placeholder: '请选择',
           required: false
-        }
+        },
       ],
       formData: [],
       trashData: [],
       formInfo: {
         name: '未命名表单',
-        desc: '从右侧拖动组件到表单区域'
+        desc: '从右侧拖动组件到表单区域',
+        id: ''
       },
       isDrag: false,
       currentKey: '',
       currentData: {},
       hoverKey: '',
       selectInput: '',
-      pcTextArr,
-      pcaTextArr,
-      countryCode
+      countryCode,
+      loading: false
     }
   },
+  mounted() {
+    this.init()
+  },
   methods: {
+    init() {
+      function getType(name) {
+        if(['first_name','last_name','id_number','company','department','address'].includes(name)) {
+          return 'input'
+        }
+        if(['id_type','position','province','city','industry'].includes(name)) {
+          return 'select'
+        }
+        if(['mobile'].includes(name)) {
+          return 'phone'
+        }
+        if(['email'].includes(name)) {
+          return 'email'
+        }
+      }
+      if (this.$route.params.id) {
+        this.loading = true
+        this.formInfo.id = this.$route.params.id
+        getFormInfo(this.formInfo.id).then(res => {
+          this.loading = false
+          this.formInfo.name = res.data.template_name
+          this.formInfo.desc = res.data.description
+          res.data.fields.forEach(item => {
+            console.log(item)
+            this.formData.push({
+              key: item.id,
+              type: getType(item.field_name),
+              name: item.field_name,
+              label: item.field_label,
+              value: '',
+              width: 1,
+              placeholder: '请输入',
+              required: item.is_required
+            })
+          })
+          console.log(res)
+        }).catch(err => {
+          this.loading = false
+        })
+      }
+    },
+    save() {
+      if (this.loading) {
+        return
+      }
+      this.loading = true
+      const saveData = []
+      this.formData.forEach(item => {
+        saveData.push({
+          field_name: item.name,
+          field_label: item.label,
+          is_required: item.required,
+        })
+      })
+      saveForm(this.formInfo.id, this.formInfo.name, this.formInfo.desc, saveData).then(res => {
+        this.loading = false
+        this.$message.success('保存成功')
+        this.$router.push('/preRegister/list')
+        console.log(res)
+      }).catch(err => {
+        this.loading = false
+        this.$message.error('保存失败')
+        console.log(err)
+      })
+    },
     choseComp(element) {
       this.currentKey = element.key
       this.currentData = element
@@ -192,7 +258,7 @@ export default Vue.extend({
         type: 'warning',
         callback: action => {
           if (action === 'confirm') {
-            let index = this.getIndexByKey(element.key)
+            const index = this.getIndexByKey(element.key)
             this.formData.splice(index, 1)
             this.currentKey = ''
           }
@@ -215,9 +281,9 @@ export default Vue.extend({
       newItem.key = newItem.type + new Date().getTime()
       return newItem
     },
-    removeTrash(element){
-      let index = this.getIndexByKey(element.key)
-      this.trashData.splice(index,1)
+    removeTrash(element) {
+      const index = this.getIndexByKey(element.key)
+      this.trashData.splice(index, 1)
     },
     hover(element) {
       if (this.isDrag) return
@@ -243,7 +309,7 @@ export default Vue.extend({
 </script>
 
 <template>
-  <div class="main-box">
+  <div v-loading="loading" class="main-box">
     <div class="comp-lib">
       <div class="title">
         组件库
@@ -251,10 +317,7 @@ export default Vue.extend({
       <div class="list">
         <draggable v-model="compList" :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 compList" :key="element.type">
-              <div v-if="element.type==='tips'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-              </div>
+            <div v-for="(element) in compList" :key="element.name">
               <div v-if="element.type==='input'" :class="[element.type,'form-item']">
                 <div class="tips">{{ element.label }}</div>
                 <el-input :value="element.value" :placeholder="element.placeholder" />
@@ -284,55 +347,6 @@ export default Vue.extend({
                   <el-option v-for="item in element.options" :key="item.value" :label="item.label" :value="item.value" />
                 </el-select>
               </div>
-              <div v-if="element.type==='checkbox'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-checkbox-group :value="element.value">
-                  <el-checkbox v-for="item in element.options" :key="item.value" :label="item.label" />
-                </el-checkbox-group>
-              </div>
-              <div v-if="element.type==='radio'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-radio-group :value="element.value">
-                  <el-radio v-for="item in element.options" :key="item.value" :label="item.label" />
-                </el-radio-group>
-              </div>
-              <div v-if="element.type==='number'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-input-number :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
-              </div>
-              <div v-if="element.type==='slider'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-slider :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
-              </div>
-              <div v-if="element.type==='textarea'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-input :value="element.value" type="textarea" :placeholder="element.placeholder" :autosize="{ minRows: element.minRows, maxRows: element.maxRows}" />
-              </div>
-              <div v-if="element.type==='region'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-cascader
-                  :placeholder="element.placeholder"
-                  size="large"
-                  :options="element.range===2?pcTextArr:pcaTextArr"
-                  value="element.value">
-                </el-cascader>
-              </div>
-              <div v-if="element.type==='time'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-time-picker :value="element.value" :placeholder="element.placeholder" />
-              </div>
-              <div v-if="element.type==='timeRange'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-time-picker is-range :value="element.value" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
-              </div>
-              <div v-if="element.type==='date'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-date-picker :value="element.value" :placeholder="element.placeholder" />
-              </div>
-              <div v-if="element.type==='dateRange'" :class="[element.type,'form-item']">
-                <div class="tips">{{ element.label }}</div>
-                <el-date-picker :value="element.value" type="daterange" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
-              </div>
             </div>
           </transition-group>
         </draggable>
@@ -348,9 +362,6 @@ export default Vue.extend({
           <draggable v-model="formData" :group="{name:'form'}" :options="{sort:true,animation:300}" class="form-body" @start="handleStart" @end="handleEnd">
             <transition-group class="drag-cont">
               <div v-for="element in formData" :key="element.key" :style="{gridColumn:'span '+element.width}" :class="[element.required?'required':'']" @click="choseComp(element)" @mouseenter="hover(element)" @mouseleave="blur(element)">
-                <div v-if="element.type==='tips'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                </div>
                 <div v-if="element.type==='input'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
                   <div class="tips">{{ element.label }}</div>
                   <el-input :value="element.value" :placeholder="element.placeholder" />
@@ -380,55 +391,6 @@ export default Vue.extend({
                     <el-option v-for="item in element.options" :key="item.value" :label="item.label" :value="item.value" />
                   </el-select>
                 </div>
-                <div v-if="element.type==='checkbox'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-checkbox-group :value="element.value">
-                    <el-checkbox v-for="item in element.options" :key="item.value" :label="item.label" />
-                  </el-checkbox-group>
-                </div>
-                <div v-if="element.type==='radio'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-radio-group :value="element.value">
-                    <el-radio v-for="item in element.options" :key="item.value" :label="item.label" />
-                  </el-radio-group>
-                </div>
-                <div v-if="element.type==='number'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-input-number :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
-                </div>
-                <div v-if="element.type==='slider'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-slider :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
-                </div>
-                <div v-if="element.type==='textarea'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-input :value="element.value" type="textarea" :placeholder="element.placeholder" :autosize="{ minRows: element.minRows, maxRows: element.maxRows}" />
-                </div>
-                <div v-if="element.type==='region'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-cascader
-                    :placeholder="element.placeholder"
-                    size="large"
-                    :options="element.range===2?pcTextArr:pcaTextArr"
-                    value="element.value">
-                  </el-cascader>
-                </div>
-                <div v-if="element.type==='time'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-time-picker :value="element.value" :placeholder="element.placeholder" />
-                </div>
-                <div v-if="element.type==='timeRange'" :class="[element.type,'view','form-item',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-time-picker is-range :value="element.value" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
-                </div>
-                <div v-if="element.type==='date'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-date-picker :value="element.value" :placeholder="element.placeholder" />
-                </div>
-                <div v-if="element.type==='dateRange'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
-                  <div class="tips">{{ element.label }}</div>
-                  <el-date-picker :value="element.value" type="daterange" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
-                </div>
               </div>
             </transition-group>
           </draggable>
@@ -438,18 +400,18 @@ export default Vue.extend({
     <div class="comp-edit">
       <template v-if="getIndexByKey(currentKey) === -1">
         <div class="button">
-          <el-button type="primary">保存表单</el-button>
+          <el-button type="primary" @click="save">保存表单</el-button>
         </div>
         <div class="title">表单设定</div>
         <div class="body">
           <div class="tips">表单名称</div>
           <el-input v-model="formInfo.name" placeholder="请输入表单名称" />
           <div class="tips">表单介绍</div>
-          <el-input type="textarea" v-model="formInfo.desc" placeholder="请输入表单介绍" />
+          <el-input v-model="formInfo.desc" type="textarea" placeholder="请输入表单介绍" />
         </div>
       </template>
       <template v-else>
-        <div></div>
+        <div />
         <div class="title">
           <span class="el-icon-arrow-left icon" @click="currentKey=''" />
           组件设定
@@ -462,152 +424,18 @@ export default Vue.extend({
               video="/static/guide/表单项介绍.jpg"
               title="表单项介绍"
               text="展示在输入框上方的大段文本,用于介绍和提示该处应输入的内容。"
-            ></guide>
+            />
           </div>
-          <el-input v-model="currentData.label" type="textarea" placeholder="请输入表单项介绍"></el-input>
+          <el-input v-model="currentData.label" type="textarea" placeholder="请输入表单项介绍" />
 
           <template>
             <div class="tips">是否必填</div>
             <el-switch v-model="currentData.required" />
           </template>
 
-          <template v-if="['input','select','textarea','time','date','region','email','phone'].includes(currentData.type)">
-            <div class="tips">
-              提示文字
-              <guide
-                video="/static/guide/提示文字.jpg"
-                title="提示文字"
-                text="用户输入内容前,展示在输入框中的简短文本,用于提示该处应输入的内容。"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.placeholder" placeholder="请输入提示文字"></el-input>
-          </template>
-
-          <template v-if="['input','textarea'].includes(currentData.type)">
-            <div class="tips">验证规则</div>
-            <el-input v-model="currentData.pattern" :rows="3" type="textarea" placeholder="规则正则表达式"></el-input>
-            <el-select class="pattern-select" @change="currentData.pattern=$event" placeholder="选择预设规则">
-              <el-option label="不做判断" value="^[\s\S]*$"></el-option>
-              <el-option label="国内手机号" value="^1\d{10}$"></el-option>
-              <el-option label="国内身份证号码" value="^[1-9]\d{5}(19\d{2}|20\d{2})((0[1-9])|(1[0-2]))((0[1-9])|([1-2]\d)|(3[0-1]))\d{3}(\d|X|x)$"></el-option>
-              <el-option label="电子邮箱" value="^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"></el-option>
-            </el-select>
-          </template>
-
-          <template v-if="currentData.type==='region'">
-            <div class="tips">
-              地区范围
-            </div>
-            <el-radio-group size="small" v-model="currentData.range">
-              <el-radio-button :label="2">省/市</el-radio-button>
-              <el-radio-button :label="3">省/市/县</el-radio-button>
-            </el-radio-group>
-          </template>
-
-          <template v-if="['timeRange','dateRange'].includes(currentData.type)">
-            <div class="tips">
-              起始提示文字
-              <guide
-                video="/static/guide/起始提示文字.jpg"
-                title="起始提示文字"
-                text="时间与日期类型组件输入内容前,显示在开始时间输入框中,用于提示的文字内容"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.placeholder" placeholder="请输入起始提示文字"></el-input>
-            <div class="tips">
-              结束提示文字
-              <guide
-                video="/static/guide/结束提示文字.jpg"
-                title="结束提示文字"
-                text="时间与日期类型组件输入内容前,显示在结束时间输入框中,用于提示的文字内容"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.endPlaceholder" placeholder="请输入结束提示文字"></el-input>
-            <div class="tips">
-              范围分隔符
-              <guide
-                video="/static/guide/范围分隔符.jpg"
-                title="范围分隔符"
-                text="时间与日期类型组件中,开始与结束提示文字中间的分隔文字。"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.rangeSeparator" placeholder="请输入范围分隔符"></el-input>
-          </template>
-
-          <template v-if="['select','radio','checkbox'].includes(currentData.type)">
-            <div class="tips">
-              选项设定
-              <guide
-                video="/static/guide/选项设定.mp4"
-                title="选项设定"
-                text="在单选、多选、选择器组件中,编辑可供用户选择的选择项目。"
-              ></guide>
-            </div>
-            <div class="select-list">
-              <div class="item">
-                <draggable :options="{sort:true,animation:300}" v-model="currentData.options">
-                  <transition-group class="select-inner">
-                    <el-input v-for="(item,index) in currentData.options" :key="item.value" :value="item.label" disabled>
-                      <div slot="prefix" class="handel">⠿</div>
-                      <el-button type="danger" @click="removeSelectItem(index)" slot="append" icon="el-icon-delete"></el-button>
-                    </el-input>
-                  </transition-group>
-                </draggable>
-                <el-input v-model="selectInput" @keyup.enter.native="addSelectItem" placeholder="请输入选项">
-                  <el-button @click="addSelectItem" slot="append" icon="el-icon-plus"></el-button>
-                </el-input>
-              </div>
-            </div>
-          </template>
-
-          <template v-if="['number','slider'].includes(currentData.type)">
-            <div class="tips">
-              最小值
-              <guide
-                video="/static/guide/最小值.jpg"
-                title="最小值"
-                text="数字输入类型的组件中,限制可以输入的最小数字。"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.min" @change="currentData.value=$event-0"/>
-            <div class="tips">
-              最大值
-              <guide
-                video="/static/guide/最大值.jpg"
-                title="最大值"
-                text="数字输入类型的组件中,限制可以输入的最大数字。"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.max"/>
-            <div class="tips">
-              步进值
-              <guide
-                video="/static/guide/步进值.mp4"
-                title="步进值"
-                text="数字输入类型的组件中,限制输入的数字为多少的倍数。"
-              ></guide>
-            </div>
-            <el-input v-model="currentData.step"/>
-          </template>
-
-          <div class="tips">
-            组件宽度
-            <guide
-              video="/static/guide/组件宽度.mp4"
-              title="组件宽度"
-              text="设置所选定组件的宽度,有1/3、1/2、2/3、占据全部宽度,总共四种尺寸供选择。"
-            ></guide>
-          </div>
-          <el-radio-group size="small" v-model="currentData.width">
-            <el-radio-button :label="2">窄</el-radio-button>
-            <el-radio-button :label="3">中</el-radio-button>
-            <el-radio-button :label="4">宽</el-radio-button>
-            <el-radio-button :label="6">长</el-radio-button>
-          </el-radio-group>
-
         </div>
         <div class="button">
-          <el-button @click="removeComp(currentData)" type="danger">删除组件</el-button>
+          <el-button type="danger" @click="removeComp(currentData)">删除组件</el-button>
         </div>
       </template>
     </div>
@@ -616,8 +444,8 @@ export default Vue.extend({
         <draggable v-model="trashData" :group="{name:'form'}" :options="{sort:false,animation:300}" :class="['trash-cont',trashData.length?'':'hide']">
           <transition-group class="drag-cont">
             <div v-for="element in trashData" :key="element.key" class="form-item view">
-              <div class="tips">{{element.label}}</div>
-              <div @click="removeTrash(element)" class="del el-icon-delete"></div>
+              <div class="tips">{{ element.label }}</div>
+              <div class="del el-icon-delete" @click="removeTrash(element)" />
             </div>
           </transition-group>
         </draggable>
@@ -838,7 +666,7 @@ export default Vue.extend({
           .form-body{
             .drag-cont{
               display: grid;
-              grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
+              grid-template-columns: 1fr;
               width: 100%;
               min-height: 300px;
               align-content: start;

+ 958 - 0
src/views/preRegManage/editBack.vue

@@ -0,0 +1,958 @@
+<script lang="ts">
+import Vue from 'vue'
+import { saveForm, getFormInfo } from '@/api/form'
+import guide from '@/views/guide/index'
+import draggable from 'vuedraggable'
+import countryCode from '@/lib/countryCode.json'
+import {
+  pcTextArr,
+  pcaTextArr
+} from 'element-china-area-data'
+
+export default Vue.extend({
+  name: 'Edit',
+  components: {
+    draggable,
+    guide
+  },
+  data() {
+    return {
+      compList: [
+        {
+          type: 'tips',
+          label: '将我拖拽至右边试试吧',
+          width: 6,
+          required: false
+        },
+        {
+          type: 'input',
+          label: '请在下方输入内容',
+          value: '',
+          width: 6,
+          placeholder: '请输入',
+          pattern: '^[\\s\\S]*$',
+          required: false
+        },
+        {
+          type: 'phone',
+          label: '请在下方输入手机号码',
+          value: '',
+          country: 'CN',
+          width: 6,
+          placeholder: '请输入手机号',
+          required: false
+        },
+        {
+          type: 'email',
+          label: '请在下方输入邮箱',
+          value: '',
+          width: 6,
+          placeholder: '请输入邮箱',
+          codePlaceholder: '请输入验证码',
+          required: false
+        },
+        {
+          type: 'select',
+          label: '请在下方选择内容',
+          value: '',
+          width: 6,
+          options: [
+            { label: '东坡肉', value: '东坡肉' },
+            { label: '酱肘子', value: '酱肘子' },
+            { label: '白切鸡', value: '白切鸡' }
+          ],
+          placeholder: '请选择',
+          required: false
+        },
+        {
+          type: 'checkbox',
+          label: '请选择一至多项内容',
+          value: '',
+          width: 6,
+          options: [
+            { label: '东坡肉', value: '东坡肉' },
+            { label: '酱肘子', value: '酱肘子' },
+            { label: '白切鸡', value: '白切鸡' }
+          ],
+          required: false
+        },
+        {
+          type: 'radio',
+          label: '请选择一项内容',
+          value: '',
+          width: 6,
+          options: [
+            { label: '东坡肉', value: '东坡肉' },
+            { label: '酱肘子', value: '酱肘子' },
+            { label: '白切鸡', value: '白切鸡' }
+          ],
+          required: false
+        },
+        {
+          type: 'number',
+          label: '请在下方输入数字',
+          max: 10,
+          min: 0,
+          step: 1,
+          value: 0,
+          width: 6,
+          required: false
+        },
+        {
+          type: 'slider',
+          label: '请在下方滑动',
+          max: 10,
+          min: 0,
+          step: 1,
+          value: 0,
+          width: 6,
+          required: false
+        },
+        {
+          type: 'textarea',
+          label: '请在下方输入内容',
+          value: '',
+          maxRows: 4,
+          minRows: 2,
+          width: 6,
+          placeholder: '请输入',
+          required: false,
+          pattern: '^[\\s\\S]*$'
+        },
+        {
+          type: 'region',
+          label: '请在下方选择位置',
+          range: 2,
+          width: 6,
+          value: [],
+          placeholder: '请选择所在区域',
+          required: false
+        },
+        {
+          type: 'time',
+          label: '请在下方选择时间',
+          value: '',
+          width: 6,
+          placeholder: '请选择时间',
+          required: false
+        },
+        {
+          type: 'timeRange',
+          label: '请在下方选择时间范围',
+          value: '',
+          width: 6,
+          rangeSeparator: '至',
+          placeholder: '开始时间',
+          endPlaceholder: '结束时间',
+          required: false
+        },
+        {
+          type: 'date',
+          label: '请在下方选择日期',
+          value: '',
+          width: 6,
+          placeholder: '请选择日期',
+          required: false
+        },
+        {
+          type: 'dateRange',
+          label: '请在下方选择日期范围',
+          value: '',
+          width: 6,
+          rangeSeparator: '至',
+          placeholder: '开始日期',
+          endPlaceholder: '结束日期',
+          required: false
+        }
+      ],
+      formData: [],
+      trashData: [],
+      formInfo: {
+        name: '未命名表单',
+        desc: '从右侧拖动组件到表单区域',
+        id: ''
+      },
+      isDrag: false,
+      currentKey: '',
+      currentData: {},
+      hoverKey: '',
+      selectInput: '',
+      pcTextArr,
+      pcaTextArr,
+      countryCode,
+      loading: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  methods: {
+    init() {
+      if (this.$route.params.id) {
+        this.loading = true
+        this.formInfo.id = this.$route.params.id
+        getFormInfo(this.formInfo.id).then(res => {
+          this.loading = false
+          console.log(res)
+        }).catch(err => {
+          this.loading = false
+        })
+      }
+    },
+    save() {
+      if (this.loading) {
+        return
+      }
+      this.loading = true
+      saveForm(this.formInfo.id, this.formInfo.name, this.formInfo.desc, this.formData).then(res => {
+        this.loading = false
+        this.$message.success('保存成功')
+        this.$router.push('/preRegister/list')
+        console.log(res)
+      }).catch(err => {
+        this.loading = false
+        this.$message.error('保存失败')
+        console.log(err)
+      })
+    },
+    choseComp(element) {
+      this.currentKey = element.key
+      this.currentData = element
+    },
+    removeComp(element) {
+      this.$confirm('确定删除吗?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        callback: action => {
+          if (action === 'confirm') {
+            const index = this.getIndexByKey(element.key)
+            this.formData.splice(index, 1)
+            this.currentKey = ''
+          }
+        }
+      })
+    },
+    addSelectItem() {
+      if (this.selectInput.length === 0) { return }
+      this.currentData.options.push({
+        label: this.selectInput,
+        value: this.selectInput
+      })
+      this.selectInput = ''
+    },
+    removeSelectItem(index) {
+      this.currentData.options.splice(index, 1)
+    },
+    cloneItem(item) {
+      const newItem = JSON.parse(JSON.stringify(item))
+      newItem.key = newItem.type + new Date().getTime()
+      return newItem
+    },
+    removeTrash(element) {
+      const index = this.getIndexByKey(element.key)
+      this.trashData.splice(index, 1)
+    },
+    hover(element) {
+      if (this.isDrag) return
+      this.hoverKey = element.key
+    },
+    blur(element) {
+      this.hoverKey = ''
+    },
+    getIndexByKey(key) {
+      return this.formData.indexOf(this.formData.find(item => item.key === key))
+    },
+    handleStart() {
+      this.isDrag = true
+      this.formData.forEach(item => {
+        item.hover = false
+      })
+    },
+    handleEnd() {
+      this.isDrag = false
+    }
+  }
+})
+</script>
+
+<template>
+  <div v-loading="loading" class="main-box">
+    <div class="comp-lib">
+      <div class="title">
+        组件库
+      </div>
+      <div class="list">
+        <draggable v-model="compList" :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 compList" :key="element.type">
+              <div v-if="element.type==='tips'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+              </div>
+              <div v-if="element.type==='input'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-input :value="element.value" :placeholder="element.placeholder" />
+              </div>
+              <div v-if="element.type==='email'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <div>
+                  <el-input v-model="element.value" :placeholder="element.placeholder" />
+                </div>
+                <div class="code-input">
+                  <el-input v-model="element.value" :placeholder="element.codePlaceholder" />
+                  <el-button>获取验证码</el-button>
+                </div>
+              </div>
+              <div v-if="element.type==='phone'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <div class="phone-input">
+                  <el-select v-model="element.country" :placeholder="element.placeholder">
+                    <el-option v-for="(item,index) in countryCode" :key="item.country_code+index" :label="'+'+item.phone_code+'('+item.chinese_name+')'" :value="item.country_code" />
+                  </el-select>
+                  <el-input v-model="element.value" :placeholder="element.placeholder" />
+                </div>
+              </div>
+              <div v-if="element.type==='select'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-select :value="element.value" :placeholder="element.placeholder">
+                  <el-option v-for="item in element.options" :key="item.value" :label="item.label" :value="item.value" />
+                </el-select>
+              </div>
+              <div v-if="element.type==='checkbox'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-checkbox-group :value="element.value">
+                  <el-checkbox v-for="item in element.options" :key="item.value" :label="item.label" />
+                </el-checkbox-group>
+              </div>
+              <div v-if="element.type==='radio'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-radio-group :value="element.value">
+                  <el-radio v-for="item in element.options" :key="item.value" :label="item.label" />
+                </el-radio-group>
+              </div>
+              <div v-if="element.type==='number'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-input-number :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
+              </div>
+              <div v-if="element.type==='slider'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-slider :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
+              </div>
+              <div v-if="element.type==='textarea'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-input :value="element.value" type="textarea" :placeholder="element.placeholder" :autosize="{ minRows: element.minRows, maxRows: element.maxRows}" />
+              </div>
+              <div v-if="element.type==='region'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-cascader
+                  :placeholder="element.placeholder"
+                  size="large"
+                  :options="element.range===2?pcTextArr:pcaTextArr"
+                  value="element.value"
+                />
+              </div>
+              <div v-if="element.type==='time'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-time-picker :value="element.value" :placeholder="element.placeholder" />
+              </div>
+              <div v-if="element.type==='timeRange'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-time-picker is-range :value="element.value" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
+              </div>
+              <div v-if="element.type==='date'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-date-picker :value="element.value" :placeholder="element.placeholder" />
+              </div>
+              <div v-if="element.type==='dateRange'" :class="[element.type,'form-item']">
+                <div class="tips">{{ element.label }}</div>
+                <el-date-picker :value="element.value" type="daterange" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
+              </div>
+            </div>
+          </transition-group>
+        </draggable>
+      </div>
+    </div>
+    <div class="form-view">
+      <div class="scroll-view">
+        <div 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>
+          </div>
+          <draggable v-model="formData" :group="{name:'form'}" :options="{sort:true,animation:300}" class="form-body" @start="handleStart" @end="handleEnd">
+            <transition-group class="drag-cont">
+              <div v-for="element in formData" :key="element.key" :style="{gridColumn:'span '+element.width}" :class="[element.required?'required':'']" @click="choseComp(element)" @mouseenter="hover(element)" @mouseleave="blur(element)">
+                <div v-if="element.type==='tips'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                </div>
+                <div v-if="element.type==='input'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-input :value="element.value" :placeholder="element.placeholder" />
+                </div>
+                <div v-if="element.type==='email'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <div>
+                    <el-input v-model="element.value" :placeholder="element.placeholder" />
+                  </div>
+                  <div class="code-input">
+                    <el-input v-model="element.value" :placeholder="element.codePlaceholder" />
+                    <el-button>获取验证码</el-button>
+                  </div>
+                </div>
+                <div v-if="element.type==='phone'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <div class="phone-input">
+                    <el-select v-model="element.country" :placeholder="element.placeholder">
+                      <el-option v-for="(item,index) in countryCode" :key="item.country_code+index" :label="'+'+item.phone_code+'('+item.chinese_name+')'" :value="item.country_code" />
+                    </el-select>
+                    <el-input v-model="element.value" :placeholder="element.placeholder" />
+                  </div>
+                </div>
+                <div v-if="element.type==='select'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-select :value="element.value" :placeholder="element.placeholder">
+                    <el-option v-for="item in element.options" :key="item.value" :label="item.label" :value="item.value" />
+                  </el-select>
+                </div>
+                <div v-if="element.type==='checkbox'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-checkbox-group :value="element.value">
+                    <el-checkbox v-for="item in element.options" :key="item.value" :label="item.label" />
+                  </el-checkbox-group>
+                </div>
+                <div v-if="element.type==='radio'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-radio-group :value="element.value">
+                    <el-radio v-for="item in element.options" :key="item.value" :label="item.label" />
+                  </el-radio-group>
+                </div>
+                <div v-if="element.type==='number'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-input-number :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
+                </div>
+                <div v-if="element.type==='slider'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-slider :value="element.value" :max="element.max" :min="element.min" :step="element.step" prefix-icon="el-icon-user" :placeholder="element.placeholder" />
+                </div>
+                <div v-if="element.type==='textarea'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-input :value="element.value" type="textarea" :placeholder="element.placeholder" :autosize="{ minRows: element.minRows, maxRows: element.maxRows}" />
+                </div>
+                <div v-if="element.type==='region'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-cascader
+                    :placeholder="element.placeholder"
+                    size="large"
+                    :options="element.range===2?pcTextArr:pcaTextArr"
+                    value="element.value"
+                  />
+                </div>
+                <div v-if="element.type==='time'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-time-picker :value="element.value" :placeholder="element.placeholder" />
+                </div>
+                <div v-if="element.type==='timeRange'" :class="[element.type,'view','form-item',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-time-picker is-range :value="element.value" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
+                </div>
+                <div v-if="element.type==='date'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-date-picker :value="element.value" :placeholder="element.placeholder" />
+                </div>
+                <div v-if="element.type==='dateRange'" :class="[element.type,'form-item','view',{'active':element.key===hoverKey||element.key===currentKey}]">
+                  <div class="tips">{{ element.label }}</div>
+                  <el-date-picker :value="element.value" type="daterange" :start-placeholder="element.placeholder" :end-placeholder="element.endPlaceholder" :range-separator="element.rangeSeparator" />
+                </div>
+              </div>
+            </transition-group>
+          </draggable>
+        </div>
+      </div>
+    </div>
+    <div class="comp-edit">
+      <template v-if="getIndexByKey(currentKey) === -1">
+        <div class="button">
+          <el-button type="primary" @click="save">保存表单</el-button>
+        </div>
+        <div class="title">表单设定</div>
+        <div class="body">
+          <div class="tips">表单名称</div>
+          <el-input v-model="formInfo.name" placeholder="请输入表单名称" />
+          <div class="tips">表单介绍</div>
+          <el-input v-model="formInfo.desc" type="textarea" placeholder="请输入表单介绍" />
+        </div>
+      </template>
+      <template v-else>
+        <div />
+        <div class="title">
+          <span class="el-icon-arrow-left icon" @click="currentKey=''" />
+          组件设定
+        </div>
+        <div class="body">
+
+          <div class="tips">
+            表单项介绍
+            <guide
+              video="/static/guide/表单项介绍.jpg"
+              title="表单项介绍"
+              text="展示在输入框上方的大段文本,用于介绍和提示该处应输入的内容。"
+            />
+          </div>
+          <el-input v-model="currentData.label" type="textarea" placeholder="请输入表单项介绍" />
+
+          <template>
+            <div class="tips">是否必填</div>
+            <el-switch v-model="currentData.required" />
+          </template>
+
+          <template v-if="['input','select','textarea','time','date','region','email','phone'].includes(currentData.type)">
+            <div class="tips">
+              提示文字
+              <guide
+                video="/static/guide/提示文字.jpg"
+                title="提示文字"
+                text="用户输入内容前,展示在输入框中的简短文本,用于提示该处应输入的内容。"
+              />
+            </div>
+            <el-input v-model="currentData.placeholder" placeholder="请输入提示文字" />
+          </template>
+
+          <template v-if="['input','textarea'].includes(currentData.type)">
+            <div class="tips">验证规则</div>
+            <el-input v-model="currentData.pattern" :rows="3" type="textarea" placeholder="规则正则表达式" />
+            <el-select class="pattern-select" placeholder="选择预设规则" @change="currentData.pattern=$event">
+              <el-option label="不做判断" value="^[\s\S]*$" />
+              <el-option label="国内手机号" value="^1\d{10}$" />
+              <el-option label="国内身份证号码" value="^[1-9]\d{5}(19\d{2}|20\d{2})((0[1-9])|(1[0-2]))((0[1-9])|([1-2]\d)|(3[0-1]))\d{3}(\d|X|x)$" />
+              <el-option label="电子邮箱" value="^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" />
+            </el-select>
+          </template>
+
+          <template v-if="currentData.type==='region'">
+            <div class="tips">
+              地区范围
+            </div>
+            <el-radio-group v-model="currentData.range" size="small">
+              <el-radio-button :label="2">省/市</el-radio-button>
+              <el-radio-button :label="3">省/市/县</el-radio-button>
+            </el-radio-group>
+          </template>
+
+          <template v-if="['timeRange','dateRange'].includes(currentData.type)">
+            <div class="tips">
+              起始提示文字
+              <guide
+                video="/static/guide/起始提示文字.jpg"
+                title="起始提示文字"
+                text="时间与日期类型组件输入内容前,显示在开始时间输入框中,用于提示的文字内容"
+              />
+            </div>
+            <el-input v-model="currentData.placeholder" placeholder="请输入起始提示文字" />
+            <div class="tips">
+              结束提示文字
+              <guide
+                video="/static/guide/结束提示文字.jpg"
+                title="结束提示文字"
+                text="时间与日期类型组件输入内容前,显示在结束时间输入框中,用于提示的文字内容"
+              />
+            </div>
+            <el-input v-model="currentData.endPlaceholder" placeholder="请输入结束提示文字" />
+            <div class="tips">
+              范围分隔符
+              <guide
+                video="/static/guide/范围分隔符.jpg"
+                title="范围分隔符"
+                text="时间与日期类型组件中,开始与结束提示文字中间的分隔文字。"
+              />
+            </div>
+            <el-input v-model="currentData.rangeSeparator" placeholder="请输入范围分隔符" />
+          </template>
+
+          <template v-if="['select','radio','checkbox'].includes(currentData.type)">
+            <div class="tips">
+              选项设定
+              <guide
+                video="/static/guide/选项设定.mp4"
+                title="选项设定"
+                text="在单选、多选、选择器组件中,编辑可供用户选择的选择项目。"
+              />
+            </div>
+            <div class="select-list">
+              <div class="item">
+                <draggable v-model="currentData.options" :options="{sort:true,animation:300}">
+                  <transition-group class="select-inner">
+                    <el-input v-for="(item,index) in currentData.options" :key="item.value" :value="item.label" disabled>
+                      <div slot="prefix" class="handel">⠿</div>
+                      <el-button slot="append" type="danger" icon="el-icon-delete" @click="removeSelectItem(index)" />
+                    </el-input>
+                  </transition-group>
+                </draggable>
+                <el-input v-model="selectInput" placeholder="请输入选项" @keyup.enter.native="addSelectItem">
+                  <el-button slot="append" icon="el-icon-plus" @click="addSelectItem" />
+                </el-input>
+              </div>
+            </div>
+          </template>
+
+          <template v-if="['number','slider'].includes(currentData.type)">
+            <div class="tips">
+              最小值
+              <guide
+                video="/static/guide/最小值.jpg"
+                title="最小值"
+                text="数字输入类型的组件中,限制可以输入的最小数字。"
+              />
+            </div>
+            <el-input v-model="currentData.min" @change="currentData.value=$event-0" />
+            <div class="tips">
+              最大值
+              <guide
+                video="/static/guide/最大值.jpg"
+                title="最大值"
+                text="数字输入类型的组件中,限制可以输入的最大数字。"
+              />
+            </div>
+            <el-input v-model="currentData.max" />
+            <div class="tips">
+              步进值
+              <guide
+                video="/static/guide/步进值.mp4"
+                title="步进值"
+                text="数字输入类型的组件中,限制输入的数字为多少的倍数。"
+              />
+            </div>
+            <el-input v-model="currentData.step" />
+          </template>
+
+          <div class="tips">
+            组件宽度
+            <guide
+              video="/static/guide/组件宽度.mp4"
+              title="组件宽度"
+              text="设置所选定组件的宽度,有1/3、1/2、2/3、占据全部宽度,总共四种尺寸供选择。"
+            />
+          </div>
+          <el-radio-group v-model="currentData.width" size="small">
+            <el-radio-button :label="2">窄</el-radio-button>
+            <el-radio-button :label="3">中</el-radio-button>
+            <el-radio-button :label="4">宽</el-radio-button>
+            <el-radio-button :label="6">长</el-radio-button>
+          </el-radio-group>
+
+        </div>
+        <div class="button">
+          <el-button type="danger" @click="removeComp(currentData)">删除组件</el-button>
+        </div>
+      </template>
+    </div>
+    <div :class="['trash-bin',isDrag?'':'hide']">
+      <div class="trash-inner">
+        <draggable v-model="trashData" :group="{name:'form'}" :options="{sort:false,animation:300}" :class="['trash-cont',trashData.length?'':'hide']">
+          <transition-group class="drag-cont">
+            <div v-for="element in trashData" :key="element.key" class="form-item view">
+              <div class="tips">{{ element.label }}</div>
+              <div class="del el-icon-delete" @click="removeTrash(element)" />
+            </div>
+          </transition-group>
+        </draggable>
+      </div>
+      <div class="text">回收站</div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+  .main-box{
+    position: relative;
+    overflow: hidden;
+    padding: 0 !important;
+    height: 100%;
+    width: 100%;
+    display: grid;
+    grid-template-columns: 360px 1fr 360px;
+    grid-template-rows: 1fr;
+    .form-item{
+      cursor: grab;
+      background: white;
+      margin-bottom: -1px;
+      border-top: 1px solid lightgrey;
+      border-bottom: 1px solid lightgrey;
+      padding: 24px;
+      .tips{
+        font-size: 15px;
+        color: #989898;
+        margin-bottom: 8px;
+      }
+      &.email{
+        .code-input{
+          display: flex;
+          grid-gap: 8px;
+          margin-top: 8px;
+        }
+      }
+      &.phone{
+        .phone-input{
+          display: flex;
+          grid-gap: 8px;
+        }
+      }
+      &.select{
+        .el-select{
+          width: 100%;
+        }
+      }
+      &.number{
+        .el-input-number{
+          width: 100%;
+        }
+      }
+      &.time,&.timeRange{
+        .el-date-editor{
+          width: 100%;
+        }
+      }
+      &.date,&.dateRange{
+        .el-date-editor{
+          width: 100%;
+        }
+      }
+      &.region{
+        .el-cascader{
+          width: 100%;
+        }
+      }
+      &.view{
+        transition-duration: 300ms;
+        border-radius: 8px;
+        border: 3px solid #2563EB00;
+        padding: 12px 12px;
+        margin-bottom: 0;
+        &.tips{
+          padding: 12px 12px;
+        }
+        &.active{
+          border: 3px solid #2563EB;
+        }
+      }
+    }
+    .trash-bin{
+      box-shadow: -4px 8px 12px 0 #00000044;
+      padding: 8px;
+      position: absolute;
+      border-radius: 32px 32px 0 0;
+      z-index: 3;
+      right: 200px;
+      bottom: 0;
+      width: 360px;
+      height: 150px;
+      background: lightgrey;
+      transition-duration: 300ms;
+      .trash-inner{
+        position: relative;
+        outline: #e4e4e4 5px solid;
+        border-radius: 24px;
+        width: 100%;
+        height: 60px;
+        background-image: linear-gradient( #b3b3b3, #2b2b2b);
+        .trash-cont{
+          overflow: hidden;
+          border-radius: 24px;
+          position: absolute;
+          width: 100%;
+          height: 200%;
+          left: 0;
+          bottom: 0;
+          &.hide{
+            height: 100%;
+          }
+          .drag-cont{
+            padding: 6px 12px 0;
+            overflow: hidden;
+            overflow-y: scroll;
+            display: block;
+            width: calc(100% + 20px);
+            height: 100%;
+            .form-item{
+              position: sticky;
+              left: 0;
+              top: 0;
+              border-bottom-right-radius: 0;
+              border-bottom-left-radius: 0;
+               border: 1px solid lightgrey;
+              height: 100%;
+              .del{
+                position: absolute;
+                right: 16px;
+                top: 16px;
+                cursor: pointer;
+              }
+            }
+          }
+        }
+      }
+      .text{
+        width: 100%;
+        text-align: center;
+        margin-top: 16px;
+        font-size: 36px;
+        font-weight: bold;
+        color: gray;
+        text-shadow: 4px 4px 4px 4px #ffffff;
+      }
+      &.hide{
+        bottom: -160px;
+      }
+      &:hover{
+        bottom: -60px;
+      }
+    }
+    .comp-lib{
+      z-index: 1;
+      position: relative;
+      display: grid;
+      grid-template-rows: auto 1fr;
+      box-shadow: 0 1px 4px 0 #00000022;
+      .title{
+        padding: 24px;
+      }
+      .list{
+        position: relative;
+        .drag-list{
+          position: absolute;
+          top: 0;
+          left: 0;
+          height: 100%;
+          width: 100%;
+          overflow: hidden;
+          overflow-y: auto;
+          .drag-cont{
+            width: 100%;
+            height: 100%;
+            display: block;
+          }
+        }
+      }
+    }
+    .form-view{
+      position: relative;
+      background: #F9FAFB;
+      .scroll-view{
+        position: absolute;
+        left: 0;
+        top: 0;
+        height: 100%;
+        width: 100%;
+        overflow: hidden;
+        overflow-y: scroll;
+        .form-cont{
+          padding: 12px;
+          border-radius: 16px;
+          margin: 24px;
+          background: white;
+          width: calc(100% - 48px);
+          box-shadow: 0 1px 4px 0 #00000022;
+          .form-head{
+            padding: 12px;
+            border-radius: 8px;
+            transition-duration: 300ms;
+            border: 3px solid #2563EB00;
+            &:hover,&.active{
+              border: 3px solid #2563EB;
+            }
+            .title{
+              font-size: 36px;
+              font-weight: bold;
+            }
+            .tips{
+              font-size: 20px;
+              color: #989898;
+              margin-bottom: 6px;
+            }
+          }
+          .form-body{
+            .drag-cont{
+              display: grid;
+              grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
+              width: 100%;
+              min-height: 300px;
+              align-content: start;
+              .form-item{
+                height: fit-content;
+              }
+              .required{
+                .tips{
+                  &::after{
+                    margin-left: 4px;
+                    content: '*';
+                    color: red;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    .comp-edit{
+      z-index: 1;
+      position: relative;
+      box-shadow: 0 1px 4px 0 #00000022;
+      display: grid;
+      grid-template-rows: auto auto 1fr auto;
+      .body{
+        padding: 0 16px;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+        .tips{
+          width: 100%;
+          font-size: 16px;
+          color: #505050;
+          margin-bottom: 6px;
+          margin-top: 12px;
+        }
+        .pattern-select{
+          margin-top: 6px;
+          width: 100%;
+        }
+        .select-list{
+          width: 100%;
+          .handel{
+            height: 100%;
+            display: flex;
+            align-items: center;
+          }
+          .select-inner{
+            .el-input__inner{
+              cursor: grab;
+            }
+          }
+        }
+      }
+      .button{
+        display: flex;
+        justify-content: flex-end;
+        padding: 16px;
+      }
+      .title{
+        padding: 16px;
+        display: flex;
+        align-items: center;
+        .icon{
+          margin-left: -4px;
+          margin-top: -4px;
+          margin-right: 8px;
+          font-size: 24px;
+          cursor: pointer;
+          padding: 4px;
+          border-radius: 8px;
+          transition-duration: 300ms;
+          &:hover{
+            color: #2563EB;
+            background: #ececec;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 146 - 24
src/views/preRegManage/list.vue

@@ -1,17 +1,93 @@
 <script>
 import Vue from 'vue'
-
+import { getFormList,setFormStatus,deleteForm } from '@/api/form'
 export default Vue.extend({
   name: "index",
   data() {
     return {
-      formList: []
+      formList: [],
+      current_page: 0,
+      last_page: 1,
+      total: 0,
+      page_size: 20,
+      searchWord: '',
+      searchTimer: null,
+      loading: false,
     }
   },
+  mounted() {
+    this.getList()
+  },
   methods: {
     handleCreate() {
-      this.$router.push({ path: '/preregmanage/edit' })
-    }
+      this.$router.push({ path: '/preRegister/add' })
+    },
+    search(event) {
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+      }
+      this.searchTimer = setTimeout(() => {
+        this.current_page = 0
+        this.getList()
+      }, 500)
+    },
+    edit(row) {
+      console.log(row)
+      this.$router.push({ path: '/preRegister/edit/' + row.id })
+    },
+    setStatus(row) {
+      this.loading = true
+      setFormStatus(row.id, row.status ? 0 : 1).then(res => {
+        this.loading = false
+        console.log(res)
+        this.refresh()
+      }).catch(err => {
+        this.loading = false
+        console.log(err)
+      })
+    },
+    del(row) {
+      this.$confirm('确定删除表单"' + row.template_name + '"吗?此操作无法撤销!', '删除表单', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        callback: action => {
+          if (action === 'confirm') {
+            deleteForm(row.id).then(res => {
+              this.loading = false
+              this.refresh()
+            }).catch(err => {
+              this.loading = false
+              console.log(err)
+            })
+          }
+        }
+      })
+    },
+    refresh() {
+      if (this.loading || this.current_page > this.last_page) return
+      this.loading = true
+      getFormList(this.current_page, this.page_size, this.searchWord).then(res => {
+        console.log(res)
+        this.current_page = res.data.current_page
+        this.last_page = res.data.last_page
+        this.total = res.data.total
+        this.formList = res.data.data
+        this.loading = false
+      })
+    },
+    getList() {
+      if (this.loading || this.current_page >= this.last_page) return
+      this.loading = true
+      getFormList(++this.current_page, this.page_size, this.searchWord).then(res => {
+        console.log(res)
+        this.current_page = res.data.current_page
+        this.last_page = res.data.last_page
+        this.total = res.data.total
+        this.formList = res.data.data
+        this.loading = false
+      })
+    },
   }
 })
 </script>
@@ -19,47 +95,76 @@ 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-input v-model="searchWord" @input="search" prefix-icon="el-icon-search" placeholder="搜索表单名称" class="input">
+        <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>
     </div>
     <div class="body">
-      <el-table height="100%" class="table">
+      <el-table v-loading="loading" :data="formList" height="100%" class="table">
         <el-table-column
+          :show-overflow-tooltip="true"
           label="表单名称"
-          width="260">
+          prop="template_name"
+          width="300">
         </el-table-column>
         <el-table-column
-          label="创建时间"
-          width="260">
+          label="状态"
+          width="80"
+        >
+          <template slot-scope="scope">
+            <div :class="['status','type-'+scope.row.status]">
+              {{ ['正常','禁用'][scope.row.status] }}
+            </div>
+          </template>
         </el-table-column>
         <el-table-column
-          label="状态">
+          :show-overflow-tooltip="true"
+          label="表单描述"
+          prop="description"
+          width="300"
+        >
         </el-table-column>
         <el-table-column
           label="数量">
         </el-table-column>
         <el-table-column
-          label="最近提交时间"
-          width="260">
+          label="最后编辑"
+          prop="update_time"
+          width="200">
         </el-table-column>
         <el-table-column
-          label="操作">
+          label="操作"
+          fixed="right"
+          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>
+          </template>
         </el-table-column>
       </el-table>
     </div>
     <div class="foot">
       <el-pagination
         background
-        :page-size="100"
-        layout="total, prev, pager, next"
-        :total="1000">
-      </el-pagination>
+        :page-size="page_size"
+        layout="total"
+        :total="total"
+      />
+      <el-pagination
+        background
+        :page-size="page_size"
+        layout="prev, pager, next"
+        :total="total"
+      />
     </div>
   </div>
 </template>
 
-<style scoped>
+<style scoped lang="scss">
+  @use '@/styles/variables.scss' as *;
   .main-box{
     height: 100%;
     display: grid;
@@ -81,15 +186,32 @@ export default Vue.extend({
         position: absolute;
         top: 0;
         left: 0;
+        .button{
+          cursor: pointer;
+          padding: 0 5px;
+          color: $menuActiveText;
+          &.del{
+            color: #DC2626;
+          }
+        }
+        .status{
+          padding: 0 12px;
+          border-radius: 32px;
+          width: fit-content;
+          &.type-0{
+            background: #DCFCE7;
+            color: #166534;
+          }
+          &.type-1{
+            background: #FEE2E2;
+            color: #991B1B;
+          }
+        }
       }
     }
     .foot{
-      .el-pagination{
-        display: flex;
-        .el-pagination__total{
-          margin-right: auto;
-        }
-      }
+      display: flex;
+      justify-content: space-between;
     }
   }
 </style>

+ 1 - 0
src/views/setting/rolesSetting.vue

@@ -146,6 +146,7 @@ export default Vue.extend({
           padding: 10px 0 10px 16px;
           .switch{
             margin-right: 16px;
+
           }
           .name{
             color: $menuText;

+ 0 - 3
src/views/setting/systemSetting.vue

@@ -39,9 +39,6 @@ export default Vue.extend({
             </div>
             <el-input placeholder="请输入SMTP端口"></el-input>
           </div>
-          <div class="setting-item">
-            <el-button type="primary">发送测试邮件</el-button>
-          </div>
         </div>
       </div>
       <div class="setting-box">

+ 10 - 2
vue.config.js

@@ -1,7 +1,8 @@
 'use strict'
 const path = require('path')
 const defaultSettings = require('./src/settings.js')
-
+const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
+const MonacoLocalesPlugin = require('monaco-editor-locales-plugin');
 function resolve(dir) {
   return path.join(__dirname, dir)
 }
@@ -64,7 +65,14 @@ module.exports = {
       alias: {
         '@': resolve('src')
       }
-    }
+    },
+    plugins: [
+      new MonacoWebpackPlugin(),
+      new MonacoLocalesPlugin({
+        languages: ['zh-cn', 'en'],
+        defaultLanguage: 'zh-cn'
+      })
+    ]
   },
   chainWebpack(config) {
     // it can improve the speed of the first screen, it is recommended to turn on preload