zhoujump 3 miesięcy temu
rodzic
commit
a6df5b5013

+ 1 - 0
package.json

@@ -23,6 +23,7 @@
     "driver.js": "0.9.5",
     "dropzone": "5.5.1",
     "echarts": "4.2.1",
+    "element-china-area-data": "^6.1.0",
     "element-ui": "2.13.2",
     "file-saver": "2.0.1",
     "font-awesome": "^4.7.0",

BIN
public/static/image/avatar.webp


BIN
public/static/image/cover.webp


BIN
public/static/image/login.webp


+ 21 - 1
src/App.vue

@@ -7,7 +7,27 @@
 <script>
 import i18n from '@/locales/i18n'
 export default {
-  name: 'App'
+  name: 'App',
+  data() {
+    return {
+      roles: []
+    }
+  },
+  created() {
+    this.initStore()
+  },
+  methods: {
+    initStore() {
+      window.addEventListener('beforeunload', () => {
+        localStorage.setItem('store', JSON.stringify(this.$store.state))
+      })
+      try {
+        localStorage.getItem('store') && this.$store.replaceState(JSON.parse(localStorage.getItem('store')))
+      } catch (e) {
+        localStorage.removeItem('store')
+      }
+    }
+  }
 }
 </script>
 <style lang="scss">

+ 117 - 5
src/layout/index.vue

@@ -7,7 +7,7 @@
       <div class="layout-menu">
         <el-menu class="layout-menu-inner" :collapse="isCollapse" :default-active="menuActive+''">
           <template v-for="(route,index) in menuRouter">
-            <template v-show="!route.meta.hidden">
+            <template v-if="!route.meta.hidden">
               <el-submenu :index="index+''" v-if="route.children">
                 <template slot="title">
                   <i :class="[route.meta.icon,'icon']"></i>
@@ -36,11 +36,33 @@
           <div class="icon-2"></div>
           <div class="icon-3"></div>
         </div>
-        <el-breadcrumb separator="/">
+        <el-breadcrumb class="breadcrumb" separator="/">
           <el-breadcrumb-item v-for="item in breadcrumb" :key="item.path">
             <router-link :to="item.path||'/'">{{ item.meta.title }}</router-link>
           </el-breadcrumb-item>
         </el-breadcrumb>
+        <div class="user-info">
+          <div class="avatar">
+
+          </div>
+          <div class="info-box">
+            <div class="nick-name">
+              {{ this.user.nickname }}
+            </div>
+            <div @click="goto({name:'exhibitorSetting'})" class="button">
+              <span class="el-icon-user"></span>
+              信息设置
+            </div>
+            <div @click="goto({name:'systemSetting'})" class="button">
+              <span class="el-icon-setting"></span>
+              系统设置
+            </div>
+            <div @click="logout" class="button">
+              <span class="el-icon-switch-button"></span>
+              退出登录
+            </div>
+          </div>
+        </div>
       </div>
       <div class="layout-body">
         <div class="body-inner" :class="{'animation':isAnimation}">
@@ -53,8 +75,6 @@
 </template>
 
 <script>
-import { isCoordSupported } from 'echarts/lib/component/dataZoom/helper'
-
 export default {
   name: 'Layout',
   data() {
@@ -66,11 +86,30 @@ export default {
       isAnimation: false
     }
   },
+  computed: {
+    user() { return this.$store.state.user.user }
+  },
   mounted() {
     this.refreshRoute()
     this.initGuards()
   },
   methods: {
+    logout() {
+      this.$confirm('确定要退出吗?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        callback: action => {
+          if (action === 'confirm') {
+            this.$store.dispatch('logout').then(() => {
+              this.$router.push({
+                name: 'login'
+              })
+            })
+          }
+        }
+      })
+    },
     changeCollapse() {
       this.isCollapse = !this.isCollapse
     },
@@ -95,6 +134,7 @@ export default {
     },
     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) {
@@ -134,7 +174,7 @@ export default {
     .layout-left{
       position: relative;
       z-index: 1;
-      box-shadow: 0 4px 6px -4px #00000022,0 10px 15px -3px #00000022;
+      box-shadow: 0 2px 6px -4px #00000022,0 10px 10px -3px #00000022;
       background: white;
       .layout-logo{
         border-bottom: 1px solid #E5E7EB;
@@ -185,6 +225,78 @@ export default {
         background: white;
         border-bottom: 1px solid #E5E7EB;
         height: 80px;
+        .user-info{
+          position: relative;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          grid-gap: 16px;
+          margin-left: auto;
+          .avatar{
+            cursor: pointer;
+            transition-duration: 300ms;
+            border-radius: 50%;
+            height: 42px;
+            aspect-ratio: 1;
+            background: #b0b2b6;
+            z-index: 5;
+          }
+          .info-box{
+            display: flex;
+            flex-direction: column;
+            align-items: flex-end;
+            padding: 16px;
+            position: absolute;
+            transform-origin: 100% 0;
+            right: 0;
+            z-index: 4;
+            top: 0;
+            width: 220px;
+            height: 200px;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 1px 4px 0 #00000022;
+            opacity: 0;
+            pointer-events: none;
+            transition-duration: 300ms;
+            scale: 0.9;
+            .nick-name{
+              text-align: right;
+              width: 100%;
+              overflow: hidden;
+              font-size: 20px;
+              font-weight: bold;
+              color: $menuText;
+              padding-right: 48px;
+              padding-bottom: 16px;
+            }
+            .button{
+              text-align: right;
+              border-radius: 4px;
+              width: 100%;
+              transition-duration: 300ms;
+              cursor: pointer;
+              padding: 8px 24px;
+              color: $menuText;
+              background: $menuBg;
+              &:hover{
+                color: $menuActiveText;
+                background: $menuActiveBg;
+              }
+            }
+          }
+          &:hover{
+            .avatar{
+              scale: 1.4;
+              box-shadow: 0 0 6px 2px #00000022;
+            }
+            .info-box{
+              scale: 1;
+              opacity: 1;
+              pointer-events: auto;
+            }
+          }
+        }
         .icon{
           transition-duration: 300ms;
           cursor: pointer;

Plik diff jest za duży
+ 1394 - 0
src/lib/countryCode.json


+ 123 - 58
src/router/index.js

@@ -1,37 +1,8 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-
 Vue.use(Router)
 
-/* Layout */
 import Layout from '@/layout'
-/* Router Modules */
-/**
- * Note: sub-menu only appear when route children.length >= 1
- * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
- *
- * hidden: true                   if set true, item will not show in the sidebar(default is false)
- * alwaysShow: true               if set true, will always show the root menu
- *                                if not set alwaysShow, when item has more than one children route,
- *                                it will becomes nested mode, otherwise not show the root menu
- * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
- * name:'router-name'             the name is used by <keep-alive> (must set!!!)
- * meta : {
-    roles: ['admin','editor']    control the page roles (you can set multiple roles)
-    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
-    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
-    noCache: true                if set true, the page will no be cached(default is false)
-    affix: true                  if set true, the tag will affix in the tags-view
-    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
-    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
-  }
- */
-
-/**
- * constantRoutes
- * a base page that does not have permission requirements 不需要权限控制的路由所有人都能访问
- * all roles can be accessed
- */
 export const constantRoutes = [
   {
     path: '/',
@@ -43,32 +14,61 @@ export const constantRoutes = [
         path: 'dashboard',
         component: () => import('@/views/dashboard/index'),
         name: 'Dashboard',
-        meta: { title: '首页看板', icon: 'el-icon-house', affix: true }
+        meta: { title: '首页看板', icon: 'el-icon-house', roles: 'dashboard' }
       },
       {
         path: 'audiencemanage',
         component: () => import('@/views/audienceManage/index'),
         name: 'audienceManage',
-        meta: { title: '观众管理', icon: 'el-icon-user', affix: true }
+        meta: {
+          title: '观众管理',
+          icon: 'el-icon-user',
+          roles: 'audience',
+          func: [
+            {
+              name: '添加观众',
+              roles: 'audience.add'
+            },
+            {
+              name: '导入',
+              roles: 'audience.import'
+            },
+            {
+              name: '导出',
+              roles: 'audience.export'
+            }
+          ]
+        }
       },
       {
         path: 'preregmanage',
         component: () => import('@/views/preRegManage/index'),
         name: 'preRegManage',
-        meta: { title: '预登记表单', icon: 'el-icon-tickets', affix: true },
+        meta: { title: '预登记表单', icon: 'el-icon-tickets', roles: 'preReg' },
         redirect: '/preregmanage/list',
         children: [
           {
             path: 'list',
             component: () => import('@/views/preRegManage/list'),
             name: 'list',
-            meta: { title: '预登记表单管理', icon: 'el-icon-edit', affix: true, collapse: false }
+            meta: {
+              title: '预登记表单管理',
+              icon: 'el-icon-edit',
+              roles: 'preReg.list',
+              collapse: false,
+              func: [
+                {
+                  roles: 'preReg.creat',
+                  name: '创建表单'
+                }
+              ]
+            }
           },
           {
             path: 'edit',
             component: () => import('@/views/preRegManage/edit'),
             name: 'edit',
-            meta: { title: '预登记表单编辑', icon: 'el-icon-edit', affix: true, hidden: true, collapse: true }
+            meta: { title: '预登记表单编辑', icon: 'el-icon-edit', hidden: true, roles: 'preReg.edit', collapse: true }
           }
         ]
       },
@@ -76,50 +76,115 @@ export const constantRoutes = [
         path: 'exhibitormanage',
         component: () => import('@/views/audienceManage/index'),
         name: 'ExhibitorManage',
-        meta: { title: '展商管理', icon: 'el-icon-office-building', affix: true }
+        meta: {
+          title: '展商管理',
+          icon: 'el-icon-office-building',
+          roles: 'exhibitor',
+          func: [
+            {
+              roles: 'exhibitor.add',
+              name: '添加展商'
+            },
+            {
+              roles: 'exhibitor.import',
+              name: '导入'
+            },
+            {
+              roles: 'exhibitor.export',
+              name: '导出'
+            }
+          ]
+        }
       },
       {
         path: 'invitationmanage',
         component: () => import('@/views/invitationManage/index'),
         name: 'invitationManage',
-        meta: { title: '邀请函模板管理', icon: 'el-icon-files', affix: true }
+        meta: { title: '邀请函模板管理', icon: 'el-icon-files', roles: 'invitation' }
       },
       {
         path: 'setting',
-        component: () => import('@/views/audienceManage/index'),
+        component: () => import('@/views/setting/index'),
         name: 'setting',
-        meta: { title: '主办方信息设置', icon: 'el-icon-setting', affix: true }
+        meta: { title: '信息与配置', icon: 'el-icon-setting', roles: 'setting' },
+        redirect: '/setting/exhibitor',
+        children: [
+          {
+            path: 'exhibitor',
+            component: () => import('@/views/setting/exhibitorSetting'),
+            name: 'exhibitorSetting',
+            meta: { title: '主办方信息配置', icon: 'el-icon-data-line', roles: 'setting.exhibitor' }
+          },
+          {
+            path: 'account',
+            component: () => import('@/views/setting/accountSetting'),
+            name: 'accountSetting',
+            meta: {
+              title: '系统账号管理',
+              icon: 'el-icon-user',
+              roles: 'setting.account',
+              func: [
+                {
+                  name: '添加账号',
+                  roles: 'setting.account.add'
+                }
+              ]
+            }
+          },
+          {
+            path: 'roles',
+            component: () => import('@/views/setting/rolesSetting.vue'),
+            name: 'accountRoles',
+            meta: {
+              title: '系统角色管理',
+              icon: 'el-icon-suitcase-1',
+              roles: 'setting.roles',
+              func: [
+                {
+                  name: '添加角色',
+                  roles: 'setting.roles.add'
+                }
+              ]
+            }
+          },
+          {
+            path: 'system',
+            component: () => import('@/views/setting/systemSetting'),
+            name: 'systemSetting',
+            meta: { title: '系统信息配置', icon: 'el-icon-setting', roles: 'setting.system' }
+          }
+        ]
+      },
+      {
+        path: '404',
+        component: () => import('@/views/errorPage/404'),
+        name: '404',
+        meta: { title: '页面不存在哦', icon: 'el-icon-delete-location', hidden: true }
       }
     ]
   },
   {
-    path: '/404',
-    component: () => import('@/views/errorPage/404'),
-    name: '404',
+    path: '/login',
+    name: 'login',
+    component: () => import('@/views/login/index'),
     hidden: true
-  }
-]
-
-/**
- * asyncRoutes
- * the routes that need to be dynamically loaded based on user roles
- * 需要授权的路由
- */
-export const asyncRoutes = [
+  },
   { path: '*', redirect: '/404', hidden: true }
 ]
-
 const createRouter = () => new Router({
   // mode: 'history', // require service support
   scrollBehavior: () => ({ y: 0 }),
   routes: constantRoutes
 })
-
 const router = createRouter()
-// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
-export function resetRouter() {
-  const newRouter = createRouter()
-  router.matcher = newRouter.matcher // reset router
-}
-
+router.beforeEach((to, from, next) => {
+  if (router.app.$store.getters.token) {
+    if (to.path === '/login') {
+      next()
+    } else {
+      next('/login')
+    }
+  }
+  next()
+})
 export default router

+ 46 - 0
src/store/modules/user.js

@@ -0,0 +1,46 @@
+export default {
+  state: {
+    token: '',
+    user: {},
+    autoLogin: false
+  },
+  mutations: {
+    SET_TOKEN: (state, token) => {
+      state.token = token
+    },
+    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) {
+      return new Promise((resolve, reject) => {
+        commit('SET_USER', {
+          username: 'admin',
+          nickname: '测试登录'
+        })
+        commit('SET_TOKEN', token)
+        resolve('登录成功')
+      })
+    },
+    logout({ commit }) {
+      return new Promise((resolve, reject) => {
+        commit('SET_LOGOUT')
+        resolve('退出登录成功')
+      })
+    }
+  }
+}

+ 1 - 1
src/styles/variables.scss

@@ -14,7 +14,7 @@ $subMenuActiveText: #212121; // https://github.com/ElemeFE/element/issues/12951
 $menuBg: #ffffff;
 $menuActiveBg: #004dff11;
 $menuActive: #2563EB;
-$menuHover: #E9EFFD;
+$menuHover: #c0d3ff33;
 $subMenuBg: #ffffff;
 $subMenuHover: #f5f7fa;
 $sideBarWidth: 260px;

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

@@ -1,5 +1,5 @@
 <template>
-
+  <div>404</div>
 </template>
 
 <script>

+ 119 - 0
src/views/login/index.vue

@@ -0,0 +1,119 @@
+<script>
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: "index",
+  data() {
+    return {
+      username: '',
+      password: '',
+      autoLogin: false,
+      env: process.env.NODE_ENV
+    }
+  },
+  mounted() {
+  },
+  methods: {
+    login() {
+      this.$store.dispatch('login', {
+        username: this.username,
+        password: this.password
+      }).then(res => {
+        this.$router.push('/')
+      }).catch(err => {
+        console.log(err)
+        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)
+      })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="body">
+    <div class="login-cont">
+      <div class="image-left">
+        <img class="image" src="/static/image/login.webp" alt="">
+        <div class="title">展会服务系统</div>
+      </div>
+      <div class="cont-right">
+        <div class="title">登录</div>
+        <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>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+  .body{
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-image: linear-gradient(120deg,#F7FAFF,#A6C5FE);
+    .login-cont{
+      background: white;
+      overflow: hidden;
+      display: grid;
+      border-radius: 12px;
+      box-shadow: 0 0 12px #00000011;
+      grid-template-columns: 1fr 1fr;
+      .image-left{
+        padding: 40px;
+        background: #123068;
+        position: relative;
+        .image{
+          position: absolute;
+          object-fit: cover;
+          left: 0;
+          top: 0;
+          width: 100%;
+          height: 100%;
+        }
+        .title{
+          position: relative;
+          color: white;
+          font-size: 28px;
+          font-weight: bold;
+        }
+      }
+      .cont-right{
+        padding: 40px;
+        display: flex;
+        flex-direction: column;
+        grid-gap: 16px;
+        .title{
+          font-weight: bold;
+          margin-bottom: 26px;
+          font-size: 28px;
+        }
+        .button{
+          font-size: 18px;
+          margin-top: 48px;
+          width: 400px;
+        }
+        .test-button{
+          margin-left: unset;
+          font-size: 18px;
+          width: 400px;
+        }
+        .input{
+          font-size: 16px;
+          width: 400px;
+        }
+      }
+    }
+  }
+</style>

+ 195 - 28
src/views/preRegManage/edit.vue

@@ -2,6 +2,12 @@
 import Vue from 'vue'
 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: {
@@ -14,48 +20,72 @@ export default Vue.extend({
         {
           type: 'tips',
           label: '将我拖拽至右边试试吧',
-          width: 2
+          width: 6,
+          required: false
         },
         {
           type: 'input',
           label: '请在下方输入内容',
           value: '',
-          width: 2,
-          placeholder: '请输入'
+          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: 1,
+          width: 6,
           options: [
             { label: '东坡肉', value: '东坡肉' },
             { label: '酱肘子', value: '酱肘子' },
             { label: '白切鸡', value: '白切鸡' }
           ],
-          placeholder: '请选择'
+          placeholder: '请选择',
+          required: false
         },
         {
           type: 'checkbox',
           label: '请选择一至多项内容',
           value: '',
-          width: 1,
+          width: 6,
           options: [
             { label: '东坡肉', value: '东坡肉' },
             { label: '酱肘子', value: '酱肘子' },
             { label: '白切鸡', value: '白切鸡' }
-          ]
+          ],
+          required: false
         },
         {
           type: 'radio',
           label: '请选择一项内容',
           value: '',
-          width: 1,
+          width: 6,
           options: [
             { label: '东坡肉', value: '东坡肉' },
             { label: '酱肘子', value: '酱肘子' },
             { label: '白切鸡', value: '白切鸡' }
-          ]
+          ],
+          required: false
         },
         {
           type: 'number',
@@ -64,7 +94,8 @@ export default Vue.extend({
           min: 0,
           step: 1,
           value: 0,
-          width: 1
+          width: 6,
+          required: false
         },
         {
           type: 'slider',
@@ -73,7 +104,8 @@ export default Vue.extend({
           min: 0,
           step: 1,
           value: 0,
-          width: 1
+          width: 6,
+          required: false
         },
         {
           type: 'textarea',
@@ -81,40 +113,55 @@ export default Vue.extend({
           value: '',
           maxRows: 4,
           minRows: 2,
-          width: 2,
-          placeholder: '请输入'
+          width: 6,
+          placeholder: '请输入',
+          required: false,
+          pattern: '^[\\s\\S]*$'
+        },
+        {
+          type: 'region',
+          label: '请在下方选择位置',
+          range: 2,
+          width: 6,
+          value: [],
+          placeholder: '请选择所在区域',
+          required: false
         },
         {
           type: 'time',
           label: '请在下方选择时间',
           value: '',
-          width: 2,
-          placeholder: '请选择时间'
+          width: 6,
+          placeholder: '请选择时间',
+          required: false
         },
         {
           type: 'timeRange',
           label: '请在下方选择时间范围',
           value: '',
-          width: 2,
+          width: 6,
           rangeSeparator: '至',
           placeholder: '开始时间',
-          endPlaceholder: '结束时间'
+          endPlaceholder: '结束时间',
+          required: false
         },
         {
           type: 'date',
           label: '请在下方选择日期',
           value: '',
-          width: 2,
-          placeholder: '请选择日期'
+          width: 6,
+          placeholder: '请选择日期',
+          required: false
         },
         {
           type: 'dateRange',
           label: '请在下方选择日期范围',
           value: '',
-          width: 2,
+          width: 6,
           rangeSeparator: '至',
           placeholder: '开始日期',
-          endPlaceholder: '结束日期'
+          endPlaceholder: '结束日期',
+          required: false
         }
       ],
       formData: [],
@@ -127,7 +174,10 @@ export default Vue.extend({
       currentKey: '',
       currentData: {},
       hoverKey: '',
-      selectInput: ''
+      selectInput: '',
+      pcTextArr,
+      pcaTextArr,
+      countryCode
     }
   },
   methods: {
@@ -209,6 +259,25 @@ export default Vue.extend({
                 <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">
@@ -239,6 +308,15 @@ export default Vue.extend({
                 <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" />
@@ -269,7 +347,7 @@ export default Vue.extend({
           </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}" @click="choseComp(element)" @mouseenter="hover(element)" @mouseleave="blur(element)">
+              <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>
@@ -277,6 +355,25 @@ export default Vue.extend({
                   <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">
@@ -307,6 +404,15 @@ export default Vue.extend({
                   <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" />
@@ -359,7 +465,12 @@ export default Vue.extend({
           </div>
           <el-input v-model="currentData.label" type="textarea" placeholder="请输入表单项介绍"></el-input>
 
-          <template v-if="['input','select','textarea','time','date'].includes(currentData.type)">
+          <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
@@ -371,6 +482,27 @@ export default Vue.extend({
             <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">
               起始提示文字
@@ -462,12 +594,14 @@ export default Vue.extend({
             <guide
               video="/static/guide/组件宽度.mp4"
               title="组件宽度"
-              text="设置所选定组件的宽度,使其占据全部宽度或者只占据一半宽度。"
+              text="设置所选定组件的宽度,有1/3、1/2、2/3、占据全部宽度,总共四种尺寸供选择。"
             ></guide>
           </div>
           <el-radio-group size="small" v-model="currentData.width">
-            <el-radio-button :label="1">窄</el-radio-button>
-            <el-radio-button :label="2">宽</el-radio-button>
+            <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>
@@ -514,6 +648,19 @@ export default Vue.extend({
         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%;
@@ -534,6 +681,11 @@ export default Vue.extend({
           width: 100%;
         }
       }
+      &.region{
+        .el-cascader{
+          width: 100%;
+        }
+      }
       &.view{
         transition-duration: 300ms;
         border-radius: 8px;
@@ -589,6 +741,8 @@ export default Vue.extend({
               position: sticky;
               left: 0;
               top: 0;
+              border-bottom-right-radius: 0;
+              border-bottom-left-radius: 0;
                border: 1px solid lightgrey;
               height: 100%;
               .del{
@@ -683,13 +837,22 @@ export default Vue.extend({
           .form-body{
             .drag-cont{
               display: grid;
-              grid-template-columns: 1fr 1fr;
+              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;
+                  }
+                }
+              }
             }
           }
         }
@@ -713,6 +876,10 @@ export default Vue.extend({
           margin-bottom: 6px;
           margin-top: 12px;
         }
+        .pattern-select{
+          margin-top: 6px;
+          width: 100%;
+        }
         .select-list{
           width: 100%;
           .handel{

+ 83 - 0
src/views/setting/accountSetting.vue

@@ -0,0 +1,83 @@
+<script>
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: "index"
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="head">
+      <el-input prefix-icon="el-icon-search" placeholder="搜索账号" class="input"></el-input>
+      <el-button icon="el-icon-plus" type="primary">添加账号</el-button>
+    </div>
+    <div class="body">
+      <el-table height="100%" class="table">
+        <el-table-column
+          label="账号">
+        </el-table-column>
+        <el-table-column
+          label="姓名">
+        </el-table-column>
+        <el-table-column
+          label="角色"
+          width="260">
+        </el-table-column>
+        <el-table-column
+          label="最后登录时间"
+          width="260">
+        </el-table-column>
+        <el-table-column
+          label="状态">
+        </el-table-column>
+        <el-table-column
+          label="操作">
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="foot">
+      <el-pagination
+        background
+        :page-size="100"
+        layout="total, prev, pager, next"
+        :total="1000">
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.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;
+    }
+  }
+  .foot{
+    .el-pagination{
+      display: flex;
+      .el-pagination__total{
+        margin-right: auto;
+      }
+    }
+  }
+}
+</style>

+ 344 - 0
src/views/setting/exhibitorSetting.vue

@@ -0,0 +1,344 @@
+<script>
+import Vue from 'vue'
+import draggable from 'vuedraggable'
+export default Vue.extend({
+  name: "exhibitorSetting",
+  components: {
+    draggable
+  },
+  data() {
+    return {
+      exhibitorSetting: {
+        name: '',
+        startTime: '',
+        endTime: '',
+        address: '',
+        exhibitorName: '',
+        telephone: '',
+        email: '',
+        socialMedia: [],
+        mainPicture: '/static/image/cover.webp',
+        logo: '/static/image/avatar.webp',
+        description: ''
+      }
+    }
+  },
+  methods: {
+    addSocial() {
+      this.exhibitorSetting.socialMedia.push({
+        key: Date.now(),
+        type: '',
+        url: ''
+      })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="save">
+      <el-button type="primary">保存</el-button>
+    </div>
+    <div class="info">
+      <div class="scroll">
+        <div class="form-item required">
+          <div class="label">展会名称 </div>
+          <el-input class="input" v-model="exhibitorSetting.name" placeholder="请输入展会名称"></el-input>
+        </div>
+        <div class="form-item required">
+          <div class="label">展会时间</div>
+          <div class="time-cont">
+            <el-date-picker
+              v-model="exhibitorSetting.startTime"
+              type="date"
+              placeholder="开始时间"
+              value-format="yyyy-MM-dd"
+              style="width: 100%;"
+            ></el-date-picker>
+            <el-date-picker
+              v-model="exhibitorSetting.endTime"
+              type="date"
+              placeholder="结束时间"
+              value-format="yyyy-MM-dd"
+              style="width: 100%;"
+            ></el-date-picker>
+          </div>
+        </div>
+        <div class="form-item required">
+          <div class="label">展会地点</div>
+          <el-input class="input" v-model="exhibitorSetting.address" placeholder="请输入展会地点"></el-input>
+        </div>
+        <div class="form-item required">
+          <div class="label">主办单位</div>
+          <el-input class="input" v-model="exhibitorSetting.exhibitorName" placeholder="请输入主办单位"></el-input>
+        </div>
+        <div class="form-item required">
+          <div class="label">联系电话</div>
+          <el-input class="input" v-model="exhibitorSetting.telephone" placeholder="请输入联系电话"></el-input>
+        </div>
+        <div class="form-item required">
+          <div class="label">联系邮箱</div>
+          <el-input class="input" v-model="exhibitorSetting.email" placeholder="请输入联系邮箱"></el-input>
+        </div>
+        <div class="form-item">
+          <div class="label">社交账号</div>
+          <draggable v-model="exhibitorSetting.socialMedia" :options="{sort:true,animation:300,handel:'.handel'}" class="social-list">
+            <transition-group class="drag-cont">
+              <div v-for="(item,index) in exhibitorSetting.socialMedia" :key="item.key" class="social-item">
+                <el-input v-model="item.url" placeholder="请输入链接">
+                  <el-select slot="prepend" v-model="item.type" placeholder="请选择">
+                    <el-option label="微信公众号" value="wechatPM"></el-option>
+                  </el-select>
+                </el-input>
+                <div class="handel">⠿</div>
+              </div>
+            </transition-group>
+          </draggable>
+          <el-button @click="addSocial" icon="el-icon-plus">添加社交媒体</el-button>
+        </div>
+      </div>
+    </div>
+    <div class="desc">
+      <div class="scroll">
+        <div class="vision-cont">
+          <img :src="exhibitorSetting.mainPicture" alt="" class="image loading">
+          <div class="avatar-name">
+            <img :src="exhibitorSetting.logo" alt="" class="avatar loading">
+            <div>
+              <div class="name">
+                {{ exhibitorSetting.name||'示例展会名称' }}
+              </div>
+              <div class="exhibitor">
+                {{ exhibitorSetting.exhibitorName||'示例主办方名称' }}
+              </div>
+            </div>
+            <div class="avatar-upload">
+              <div class="upload-icon">
+                <span class="el-icon-upload icon"></span>
+                <span class="text">上传logo</span>
+              </div>
+              <input type="file" class="uploader">
+            </div>
+          </div>
+          <div class="cover-upload">
+            <div class="upload-icon">
+              <span class="el-icon-upload icon"></span>
+              <span class="text">上传主视觉图</span>
+            </div>
+            <input type="file" class="uploader">
+          </div>
+        </div>
+        <el-input rows="12" type="textarea" class="text-area" v-model="exhibitorSetting.description" placeholder="请输入展会介绍">
+        </el-input>
+      </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;
+          .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/setting/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>

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

@@ -0,0 +1,194 @@
+<script>
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: "rolesSetting",
+  data() {
+    return {
+      menuList:[],
+      menuRouter: [],
+    }
+  },
+  mounted() {
+    this.getRouter()
+  },
+  methods: {
+    getRouter() {
+      this.menuRouter = this.$router.options.routes[0].children
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="list-cont">
+      <div class="head">
+        <el-input prefix-icon="el-icon-search" placeholder="搜索观众姓名/手机号/邮箱" class="input"></el-input>
+        <el-button icon="el-icon-plus" type="primary">新增角色</el-button>
+      </div>
+      <div class="body">
+        <el-table height="100%" class="table">
+          <el-table-column
+            label="角色名称">
+          </el-table-column>
+          <el-table-column
+            label="创建日期">
+          </el-table-column>
+          <el-table-column
+            label="操作">
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="foot">
+        <el-pagination
+          background
+          :page-size="100"
+          layout="total, prev, pager, next"
+          :total="1000">
+        </el-pagination>
+      </div>
+    </div>
+    <div class="menu-list">
+      <div class="save">
+        <el-button type="primary">保存</el-button>
+      </div>
+      <div class="scroll">
+        <div class="menu">
+          <div class="menu-item" v-for="(item,index) in menuRouter">
+            <div class="name">
+              <span :class="item.meta.icon">{{item.meta.title}}</span>
+              <el-switch></el-switch>
+            </div>
+            <div class="children" v-if="item.meta.func">
+              <div class="menu-item" v-for="(func,index) in item.meta.func">
+                <div class="name">
+                  <span>{{func.name}}</span>
+                  <el-switch></el-switch>
+                </div>
+              </div>
+            </div>
+            <div class="children" v-if="item.children">
+              <div class="menu-item" v-for="(child,index) in item.children">
+                <div class="name">
+                  <span :class="child.meta.icon">{{child.meta.title}}</span>
+                  <el-switch></el-switch>
+                </div>
+                <div class="children" v-if="child.meta.func">
+                  <div class="menu-item" v-for="(func,index) in child.meta.func">
+                    <div class="name">
+                      <span>{{func.name}}</span>
+                      <el-switch></el-switch>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  @use '@/styles/variables.scss' as *;
+  .main-box{
+    overflow: hidden;
+    padding: 0 !important;
+    height: 100%;
+    width: 100%;
+    display: grid;
+    grid-template-columns: 1fr 420px;
+    .list-cont,.menu-list{
+      height: 100%;
+      width: 100%;
+      position: relative;
+    }
+    .menu-list{
+      position: relative;
+      .save{
+        display: flex;
+        justify-content: flex-end;
+        padding: 16px;
+        background: #F9FAFB;
+      }
+      .scroll{
+        background: #F9FAFB;
+        border-radius: 0 16px 16px 0;
+        padding: 16px;
+        overflow: hidden;
+        overflow-y: auto;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: calc(100% - 64px);
+      }
+      .menu{
+        padding: 8px;
+        background: white;
+        border-radius: 8px;
+        box-shadow: 0 0 8px 0 #00000018;
+        width: 100%;
+        .menu-item{
+          border-radius: 8px;
+          cursor: default;
+          border-right: 2px solid transparent;
+          padding: 10px 16px;
+          .name{
+            color: $menuText;
+            font-size: 17px;
+            display: flex;
+            justify-content: space-between;
+            span{
+              &::before{
+                margin-right: 8px
+              }
+            }
+          }
+          .children{
+            margin-top: 10px;
+          }
+          &:hover{
+            background: $menuHover;
+          }
+        }
+      }
+    }
+    .list-cont{
+      z-index: 2;
+      box-shadow: 0 0 4px 0 #00000022;
+      padding: 16px;
+      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;
+        }
+      }
+      .foot{
+        .el-pagination{
+          display: flex;
+          .el-pagination__total{
+            margin-right: auto;
+          }
+        }
+      }
+    }
+  }
+</style>

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

@@ -0,0 +1,109 @@
+<script>
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: "systemSetting"
+})
+</script>
+
+<template>
+  <div class="main-box">
+    <div class="save">
+      <el-button type="primary">保存</el-button>
+    </div>
+    <div class="scroll">
+      <div class="setting-box">
+        <div class="title">发信服务器设置</div>
+        <div class="setting-list">
+          <div class="setting-item">
+            <div class="label">
+              发件邮箱地址
+            </div>
+            <el-input placeholder="请输入发件邮箱地址"></el-input>
+          </div>
+          <div class="setting-item">
+            <div class="label">
+              发件邮箱密码/授权码
+            </div>
+            <el-input placeholder="请输入发件邮箱密码/授权码"></el-input>
+          </div>
+          <div class="setting-item">
+            <div class="label">
+              SMTP服务器地址
+            </div>
+            <el-input placeholder="请输入SMTP服务器地址"></el-input>
+          </div>
+          <div class="setting-item">
+            <div class="label">
+              SMTP端口
+            </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">
+        <div class="title">
+          邮件设置
+        </div>
+        <div class="setting-list">
+          <div class="setting-item">
+            <div class="label">
+              接收通知邮箱
+            </div>
+            <el-input placeholder="请输入通知邮箱"></el-input>
+          </div>
+          <div class="setting-item"></div>
+          <div class="setting-item">
+            <el-button type="primary">发送测试邮件</el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+  .main-box{
+    position: relative;
+    height: 100%;
+    width: 100%;
+    display: grid;
+    grid-template-rows: auto 1fr;
+    .save{
+      display: flex;
+      justify-content: flex-end;
+    }
+    .scroll{
+      padding: 16px;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+      overflow-y: auto;
+      .setting-box{
+        &:not(:first-child){
+          margin-top: 32px;
+        }
+        .title{
+          margin-bottom: 16px;
+          font-size: 24px;
+          font-weight: bold;
+        }
+        .setting-list{
+          display: grid;
+          grid-template-columns: repeat(2, 1fr);
+          grid-gap: 16px;
+          .setting-item{
+            .label{
+              font-size: 16px;
+              color: gray;
+              margin-bottom: 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+</style>