Browse Source

Merge branch 'developerB' into developer

yuwenjun1997 2 years ago
parent
commit
5cbc28acb7

+ 40 - 0
src/api/system.js

@@ -147,3 +147,43 @@ export function sysMenuTree(params) {
     params
   })
 }
+
+/** 获取授权牌模板列表 */
+export function fetchAuthTempList(params) {
+  return request({
+    url: '/auth/template/list',
+    method: 'get',
+    params
+  })
+}
+
+/** 添加编辑保存授权牌模板 */
+export function authTempSave(data) {
+  return request({
+    url: '/auth/template/save',
+    method: 'post',
+    data
+  })
+}
+
+/** 添加编辑保存授权牌模板 */
+export function authTempUpdate(data) {
+  // return request({
+  //   url: '/shop/authImage/update/all',
+  //   method: 'post',
+  //   data
+  // })
+  return fetch(`${process.env.VUE_APP_BASE_API}/shop/authImage/update/all`, {
+    body: JSON.stringify(data),
+    method: 'POST'
+  })
+}
+
+/** 获取机构可用模板(供应商可用模板) */
+export function authTempUsed(params) {
+  return request({
+    url: '/auth/template/form/data',
+    method: 'GET',
+    params
+  })
+}

BIN
src/assets/img/drag-auth-temp.jpg


BIN
src/assets/img/drag-qrcode-temp.png


+ 89 - 0
src/components/DragLayout/dragmove.js

@@ -0,0 +1,89 @@
+// https://github.com/knadh/dragmove.js
+// Kailash Nadh (c) 2020.
+// MIT License.
+
+let _loaded = false
+const _callbacks = []
+const _isTouch = window.ontouchstart !== undefined
+
+export const dragmove = function(target, handler, onStart, onEnd) {
+  // Register a global event to capture mouse moves (once).
+  if (!_loaded) {
+    document.addEventListener(_isTouch ? 'touchmove' : 'mousemove', function(e) {
+      let c = e
+      if (e.touches) {
+        c = e.touches[0]
+      }
+
+      // On mouse move, dispatch the coords to all registered callbacks.
+      for (var i = 0; i < _callbacks.length; i++) {
+        _callbacks[i](c.clientX, c.clientY)
+      }
+    })
+  }
+
+  _loaded = true
+  let isMoving = false
+  let hasStarted = false
+  let startX = 0
+  let startY = 0
+  let lastX = 0
+  let lastY = 0
+
+  // On the first click and hold, record the offset of the pointer in relation
+  // to the point of click inside the element.
+  handler.addEventListener(_isTouch ? 'touchstart' : 'mousedown', function(e) {
+    e.stopPropagation()
+    e.preventDefault()
+    if (target.dataset.dragEnabled === 'false') {
+      return
+    }
+
+    let c = e
+    if (e.touches) {
+      c = e.touches[0]
+    }
+
+    isMoving = true
+    startX = target.offsetLeft - c.clientX
+    startY = target.offsetTop - c.clientY
+  })
+
+  // On leaving click, stop moving.
+  document.addEventListener(_isTouch ? 'touchend' : 'mouseup', function(e) {
+    if (onEnd && hasStarted) {
+      onEnd(target, parseInt(target.style.left), parseInt(target.style.top))
+    }
+
+    isMoving = false
+    hasStarted = false
+  })
+
+  // Register mouse-move callback to move the element.
+  _callbacks.push(function move(x, y) {
+    if (!isMoving) {
+      return
+    }
+
+    if (!hasStarted) {
+      hasStarted = true
+      if (onStart) {
+        onStart(target, lastX, lastY)
+      }
+    }
+
+    lastX = x + startX
+    lastY = y + startY
+
+    // If boundary checking is on, don't let the element cross the viewport.
+    if (target.dataset.dragBoundary === 'true') {
+      lastX = Math.min(window.innerWidth - target.offsetWidth, Math.max(0, lastX))
+      lastY = Math.min(window.innerHeight - target.offsetHeight, Math.max(0, lastY))
+    }
+
+    target.style.left = lastX + 'px'
+    target.style.top = lastY + 'px'
+  })
+}
+
+export { dragmove as default }

+ 193 - 0
src/components/DragLayout/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="drag-layout">
+    <div class="drag-content" :class="{ padding: isDrag }">
+      <div id="drag__container" :style="imageStyle">
+        <div id="drag__move" :style="ewmStyle">
+          <el-tooltip v-model="tooltipShow" effect="dark" content="二维码在这里!" placement="top-start" :manual="true">
+            <div style="width: 100%; height: 100%" />
+          </el-tooltip>
+        </div>
+      </div>
+    </div>
+    <div class="drag__control">
+      <el-button @click="onCancel">取 消</el-button>
+      <el-button type="primary" @click="onConfirm">确 定</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { dragmove } from './dragmove.js'
+export default {
+  name: 'DragLayout',
+  props: {
+    qw: {
+      type: Number,
+      default: 53
+    },
+    qh: {
+      type: Number,
+      default: 53
+    },
+    oldX: {
+      type: Number,
+      default: 0
+    },
+    oldY: {
+      type: Number,
+      default: 0
+    },
+    image: {
+      type: String,
+      default: ''
+    },
+    size: {
+      type: String,
+      default: ''
+    },
+    isDrag: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      tooltipShow: false,
+      dragContainer: null,
+      snapThreshold: 0, // 边界
+      x: 0,
+      y: 0
+    }
+  },
+  computed: {
+    imageWidth() {
+      return this.size.split(',')[0] + 'px'
+    },
+    imageHeight() {
+      return this.size.split(',')[1] + 'px'
+    },
+    imageStyle() {
+      return {
+        width: this.imageWidth,
+        height: this.imageHeight,
+        background: `url(${this.image}) no-repeat center`,
+        'background-size': `${this.imageWidth} ${this.imageHeight}`
+      }
+    },
+    ewmStyle() {
+      console.log({
+        left: this.oldX + 'px',
+        top: this.oldY + 'px',
+        width: this.qw + 'px',
+        height: this.qh + 'px',
+        'background-size': `${this.qw + 'px'} ${this.qh + 'px'}`
+      })
+
+      return {
+        // cursor: this.isDrag ? 'move' : '',
+        left: this.oldX + 'px',
+        top: this.oldY + 'px',
+        width: this.qw + 'px',
+        height: this.qh + 'px',
+        'background-size': `${this.qw + 'px'} ${this.qh + 'px'}`
+      }
+    }
+  },
+  mounted() {
+    if (!this.isDrag) return
+    this.dragContainer = document.querySelector('#drag__container')
+    dragmove(
+      document.querySelector('#drag__container #drag__move'),
+      document.querySelector('#drag__container #drag__move'),
+      this.onStart,
+      this.onEnd
+    )
+    setTimeout(() => {
+      this.tooltipShow = true
+    }, 2000)
+  },
+  methods: {
+    // 取消
+    onCancel() {
+      this.x = this.oldX
+      this.y = this.oldY
+      this.$emit('cancel', { x: this.x, y: this.y })
+      this.tooltipShow = false
+    },
+
+    // 确认
+    onConfirm() {
+      this.$emit('confirm', { x: this.x, y: this.y })
+      this.tooltipShow = false
+    },
+
+    // 开始拖动
+    onStart(el, x, y) {
+      this.tooltipShow = false
+    },
+
+    // 结束拖动
+    onEnd(el, x, y) {
+      this.tooltipShow = true
+      if (x < this.snapThreshold) {
+        el.style.left = 0
+        el.style.right = 'auto'
+      }
+      if (y < 0) {
+        el.style.top = 0
+        el.style.bottom = 'auto'
+      }
+      if (x > this.dragContainer.offsetWidth - el.offsetWidth) {
+        el.style.right = 0
+        el.style.left = 'auto'
+      }
+      if (y > this.dragContainer.offsetHeight - el.offsetHeight) {
+        el.style.bottom = 0
+        el.style.top = 'auto'
+      }
+
+      const { offsetWidth, offsetHeight } = this.dragContainer
+
+      if (x < 0) {
+        this.x = 0
+      } else if (x > offsetWidth - this.qw) {
+        this.x = offsetWidth - this.qw
+      } else {
+        this.x = x
+      }
+
+      if (y < 0) {
+        this.y = 0
+      } else if (y > offsetHeight - this.qh) {
+        this.y = offsetHeight - this.qh
+      } else {
+        this.y = y
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.drag-content {
+  background: #f1f1f1;
+  &.padding {
+    padding: 50px 0;
+  }
+  #drag__container {
+    position: relative;
+    margin: 0 auto;
+    overflow: hidden;
+    background: #fff;
+  }
+  #drag__move {
+    position: absolute;
+    background-color: skyblue;
+    background: url(~@/assets/img/drag-qrcode-temp.png) no-repeat center;
+  }
+}
+
+.drag__control {
+  margin-top: 24px;
+  text-align: right;
+}
+</style>

+ 2 - 0
src/components/index.js

@@ -1,9 +1,11 @@
 import AuditStatus from './AuditStatus'
 import AuditButtonGroup from './AuditButtonGroup'
+import Pagination from './Pagination'
 
 const install = (Vue) => {
   Vue.component(AuditStatus.name, AuditStatus)
   Vue.component(AuditButtonGroup.name, AuditButtonGroup)
+  Vue.component(Pagination.name, Pagination)
 }
 
 export default {

+ 34 - 22
src/components/qrcode/index.vue

@@ -9,15 +9,16 @@
       <div class="btn down-btn" @click="handleDown">下载二维码</div>
       <div class="btn close-btn" @click="handleClose">关闭</div>
     </div>
-    <canvas id="canvas" style="display:none" />
+    <canvas id="canvas" style="display: none" />
     <div v-if="isVisible" class="mask" @click="handleClose" />
-    <a id="downloadLink" href="#" style="display:none" />
+    <a id="downloadLink" href="#" style="display: none" />
   </div>
 </template>
 
 <script>
 import QRCode from 'qrcode'
 import downImage from '@/assets/img/qrcode-bg-down.jpg'
+import { mapGetters } from 'vuex'
 export default {
   name: 'Qrcode',
   props: {
@@ -34,9 +35,13 @@ export default {
     return {
       imgUrl: '',
       wwwServer: process.env.VUE_APP_BASE_SERVER,
+      wwwHost: process.env.VUE_APP_WWW_HOST,
       qrcodePath: ''
     }
   },
+  computed: {
+    ...mapGetters(['authUserId'])
+  },
   created() {
     this.initQrcode()
   },
@@ -60,7 +65,11 @@ export default {
     // 初始化二维码
     async initQrcode() {
       console.log(this.productInfo)
-      this.qrcodePath = `${this.wwwServer}/product/auth/product-${this.productInfo?.productId}.html`
+      if (this.authUserId === 12) {
+        this.qrcodePath = `${this.wwwHost}/12/ross/approve/device/detail?id=${this.productInfo?.productId}`
+      } else {
+        this.qrcodePath = `${this.wwwServer}/product/auth/product-${this.productInfo?.productId}.html`
+      }
       // 二维码配置
       const options = {
         width: 192,
@@ -75,7 +84,11 @@ export default {
     },
     // 生成下载的文件
     async createDownFile(callback) {
-      this.qrcodePath = `${this.wwwServer}/product/auth/product-${this.productInfo?.productId}.html`
+      if (this.authUserId === 12) {
+        this.qrcodePath = `${this.wwwHost}/12/ross/approve/device/detail?id=${this.productInfo?.productId}`
+      } else {
+        this.qrcodePath = `${this.wwwServer}/product/auth/product-${this.productInfo?.productId}.html`
+      }
       const strHeader = this.productInfo.productName
       const strFooter1 = '仪器SN码:'
       const strFooter2 = this.hanldeSNcode(this.productInfo.snCode)
@@ -117,7 +130,7 @@ export default {
         let temp = ''
         const row = []
         for (let a = 0; a < chr.length; a++) {
-          if (ctx.measureText(temp).width >= (w - 290)) {
+          if (ctx.measureText(temp).width >= w - 290) {
             row.push(temp)
             temp = ''
           }
@@ -150,20 +163,20 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.code-container{
+.code-container {
   position: relative;
   z-index: 999999;
 }
-.mask{
+.mask {
   position: fixed;
   top: 0;
   left: 0;
   z-index: 999998;
   width: 100vw;
   height: 100vh;
-  background: rgba(0,0,0,.5);
+  background: rgba(0, 0, 0, 0.5);
 }
-.qrcode{
+.qrcode {
   z-index: 999999;
   position: fixed;
   left: 50%;
@@ -172,31 +185,31 @@ export default {
   width: 300px;
   height: 400px;
   background: url(../../assets/img/qrcode-bg-show.png) no-repeat center;
-  .content{
+  .content {
     width: 192px;
     height: 192px;
     position: absolute;
     left: 54px;
     bottom: 46px;
   }
-  .down-btn{
+  .down-btn {
     left: 0;
   }
-  .close-btn{
+  .close-btn {
     right: 0;
   }
-  .title{
+  .title {
     position: absolute;
     top: 32px;
     left: 50%;
     transform: translateX(-50%);
-    width:256px;
+    width: 256px;
     text-align: center;
     font-size: 16px;
     color: #fff;
     font-weight: bold;
   }
-  .sncode{
+  .sncode {
     position: absolute;
     top: 78px;
     left: 50%;
@@ -205,19 +218,18 @@ export default {
     width: 192px;
     color: #0e9ef0;
     font-size: 14px;
-    span{
+    span {
       font-weight: bold;
     }
   }
-
 }
-.btn  {
+.btn {
   position: absolute;
   bottom: -60px;
-	width: 108px;
-	height: 32px;
-	background-image: linear-gradient(-90deg,	#50c0ff 0%,#0e90dc 100%);
-	border-radius: 4px;
+  width: 108px;
+  height: 32px;
+  background-image: linear-gradient(-90deg, #50c0ff 0%, #0e90dc 100%);
+  border-radius: 4px;
   text-align: center;
   line-height: 32px;
   color: #fff;

+ 2 - 0
src/router/index.js

@@ -26,6 +26,7 @@ import normalPersonal from './module/normal/personal'
 import normalPersonnel from './module/normal/personnel'
 import normalVip from './module/normal/vip'
 import normalUser from './module/normal/user'
+import normalSettings from './module/normal/settings'
 
 // 需要权限访问的路由列表
 export const asyncRoutes = [
@@ -41,6 +42,7 @@ export const asyncRoutes = [
   ...normalMaterial,
   ...normalPersonal,
   ...normalPersonnel,
+  ...normalSettings,
   ...normalUser
 ]
 

+ 7 - 0
src/router/module/admin/settings.js

@@ -54,6 +54,13 @@ const settingRoutes = [
         hidden: true,
         meta: { title: '修改用户', noCache: true },
         component: () => import('@/views/admin/settings/accounts/edit')
+      },
+      {
+        path: 'auth-card',
+        name: 'SettingsAuthCard',
+        hidden: true,
+        meta: { title: '授权牌模板', noCache: true },
+        component: () => import('@/views/admin/settings/auth-card')
       }
     ]
   }

+ 63 - 0
src/router/module/normal/settings.js

@@ -0,0 +1,63 @@
+/* Layout */
+import Layout from '@/layout'
+
+const settingRoutes = [
+  {
+    path: '/normal/settings',
+    component: Layout,
+    redirect: '/normal/settings/auth-card',
+    name: 'NormalSettings',
+    meta: { title: '系统设置', icon: 'el-icon-s-tools', noCache: true },
+    children: [
+      {
+        path: 'roles',
+        name: 'NormalSettingsRoles',
+        meta: { title: '角色管理', noCache: true },
+        component: () => import('@/views/normal/settings/roles')
+      },
+      {
+        path: 'roles/add',
+        name: 'NormalSettingsRolesAdd',
+        hidden: true,
+        meta: { title: '添加角色', noCache: true },
+        component: () => import('@/views/normal/settings/roles/edit')
+      },
+      {
+        path: 'roles/edit',
+        name: 'NormalSettingsRolesEdit',
+        hidden: true,
+        meta: { title: '修改角色', noCache: true },
+        component: () => import('@/views/normal/settings/roles/edit')
+      },
+      {
+        path: 'accounts',
+        name: 'NormalSettingsAccounts',
+        meta: { title: '用户管理', noCache: true },
+        component: () => import('@/views/normal/settings/accounts')
+      },
+      {
+        path: 'accounts/add',
+        name: 'NormalSettingsAccountsAdd',
+        hidden: true,
+        meta: { title: '添加用户', noCache: true },
+        component: () => import('@/views/normal/settings/accounts/edit')
+      },
+      {
+        path: 'accounts/edit',
+        name: 'NormalSettingsAccountsEdit',
+        hidden: true,
+        meta: { title: '修改用户', noCache: true },
+        component: () => import('@/views/normal/settings/accounts/edit')
+      },
+      {
+        path: 'auth-card',
+        name: 'NormalSettingsAuthCard',
+        hidden: true,
+        meta: { title: '授权牌模板', noCache: false },
+        component: () => import('@/views/normal/settings/auth-card')
+      }
+    ]
+  }
+]
+
+export default settingRoutes

+ 7 - 2
src/styles/index.scss

@@ -186,10 +186,10 @@ aside {
 // 状态颜色
 .status {
   &.success {
-    color: #409EFF;
+    color: #409eff;
   }
   &.danger {
-    color: #F56C6C;
+    color: #f56c6c;
     cursor: help;
   }
   &.warning {
@@ -350,3 +350,8 @@ aside {
     margin-top: 0 !important;
   }
 }
+
+.form-label-tip {
+  font-size: 14px;
+  color: #999;
+}

+ 15 - 0
src/utils/index.js

@@ -224,6 +224,21 @@ export function objectMerge(target, source) {
   return target
 }
 
+/**
+ * Merges one object to another object
+ * @param {Object} target
+ * @param {(Object)} source
+ * @returns {Object}
+ */
+export function objectCover(target, source) {
+  for (const key in source) {
+    if (Object.hasOwnProperty.call(target, key)) {
+      target[key] = source[key]
+    }
+  }
+  return target
+}
+
 /**
  * @param {HTMLElement} element
  * @param {string} className

+ 167 - 0
src/views/admin/settings/auth-card/components/supplier-list.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="supplier-list">
+    <div class="filter-container">
+      <div class="filter-control">
+        <span>供应商名称:</span>
+        <el-input
+          v-model="listQuery.shopName"
+          size="mini"
+          placeholder="供应商名称"
+          @keyup.enter.native="filterSupplierList"
+        />
+      </div>
+      <div class="filter-control">
+        <span>供应商类型:</span>
+        <el-select
+          v-model="listQuery.shopType"
+          placeholder="供应商类型"
+          size="mini"
+          clearable
+          @change="filterSupplierList"
+        >
+          <el-option label="所有类型" value="" />
+          <el-option label="代理商" :value="2" />
+          <el-option label="品牌方" :value="1" />
+        </el-select>
+      </div>
+      <div class="filter-control">
+        <span>手机号:</span>
+        <el-input
+          v-model="listQuery.mobile"
+          size="mini"
+          placeholder="手机号"
+          @keyup.enter.native="filterSupplierList"
+        />
+      </div>
+      <div class="filter-control">
+        <el-button type="primary" size="mini" @click="filterSupplierList">查询</el-button>
+      </div>
+    </div>
+    <el-table
+      v-infinite-scroll="onLoad"
+      :data="supplierList"
+      border
+      class="infinite-list hide-table-check-all"
+      style="overflow: auto"
+      :infinite-scroll-delay="300"
+      :infinite-scroll-immediate="false"
+      header-row-class-name="tableHeader"
+      highlight-current-row
+      @row-click="handleSelectSupplier"
+    >
+      >
+      <el-table-column label="选择" width="55" align="center">
+        <template slot-scope="{ row }">
+          <el-radio v-model="selectedAuthUserId" :label="row.authUserId"><span v-show="false">1</span></el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column label="序号" type="index" width="80" align="center" />
+      <el-table-column property="name" label="供应商名称" />
+      <el-table-column label="供应商类型" align="center" width="120">
+        <template slot-scope="{ row }">
+          <span v-if="row.shopType === 1">品牌方</span>
+          <span v-else>代理商</span>
+        </template>
+      </el-table-column>
+      <!-- <el-table-column label="登录账号" align="center">
+        <template slot-scope="{ row }">
+          <span v-if="row.loginAccount">{{ row.loginAccount }}</span>
+          <span v-else>未绑定</span>
+        </template>
+      </el-table-column> -->
+      <el-table-column prop="mobile" label="手机号" align="center" width="160" />
+      <el-table-column prop="linkMan" label="联系人" align="center" />
+    </el-table>
+    <div class="control">
+      <el-button @click="onCancel">取 消</el-button>
+      <el-button type="primary" @click="onConfirm">确定</el-button>
+    </div>
+  </div>
+</template>
+<script>
+import { fetchSupplierList } from '@/api/supplier'
+import { deepClone } from '@/utils'
+export default {
+  name: 'SupplierList',
+  data() {
+    return {
+      listQuery: {
+        shopName: '',
+        shopType: '',
+        mobile: '',
+        pageNum: 1,
+        pageSize: 10
+      },
+      supplierList: [],
+      selectedAuthUserId: '',
+      selectedSupplierInfo: null
+    }
+  },
+  created() {
+    this.filterSupplierList()
+  },
+  methods: {
+    // 取消
+    onCancel() {
+      this.selectedAuthUserId = ''
+      this.selectedSupplierInfo = null
+      this.$emit('cancel', this.selectedSupplierInfo)
+    },
+    // 确认
+    onConfirm() {
+      this.$emit('confirm', deepClone(this.selectedSupplierInfo))
+      this.selectedAuthUserId = ''
+      this.selectedSupplierInfo = null
+    },
+    // 确认选中的供应商
+    handleSubmitSupplier() {
+      this.dialogTableVisible = false
+      this.currentSupplierList = [this.selectedSupplierInfo]
+    },
+    // 选择供应商
+    handleSelectSupplier(e) {
+      this.selectedAuthUserId = e.authUserId
+      this.selectedSupplierInfo = e
+    },
+    // 选择供应商对话框关闭事件
+    handleDialogClosed() {
+      this.listQuery.pageNum = 1
+      this.listQuery.shopName = ''
+      this.listQuery.shopType = ''
+      this.listQuery.mobile = ''
+      this.listQuery.linkMan = ''
+    },
+    // 筛选供应商
+    filterSupplierList() {
+      this.listQuery.pageNum = 1
+      this.supplierList = []
+      this.fetchSupplierList()
+    },
+    // 获取供应商列表
+    fetchSupplierList() {
+      fetchSupplierList(this.listQuery).then((res) => {
+        console.log(res)
+        this.supplierList = [...this.supplierList, ...res.data.list]
+        this.hasNextPage = res.data.hasNextPage
+        this.total = res.data.total
+      })
+    },
+    onLoad() {
+      if (!this.hasNextPage) return
+      this.listQuery.pageNum++
+      this.fetchSupplierList()
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.supplier-list {
+  .control {
+    text-align: right;
+    margin-top: 12px;
+  }
+  .infinite-list {
+    height: 400px;
+  }
+}
+</style>

+ 346 - 0
src/views/admin/settings/auth-card/index.vue

@@ -0,0 +1,346 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-control">
+        <span>所属供应商:</span>
+        <el-select
+          v-model="listQuery.authUserId"
+          placeholder="请选择供应商"
+          clearable
+          style="width: 200px"
+          class="filter-item"
+          @change="getList"
+        >
+          <template v-for="item in supplierList">
+            <el-option :key="item.authUserId" :label="item.name" :value="item.authUserId" />
+          </template>
+        </el-select>
+      </div>
+      <div class="filter-control">
+        <el-button type="primary" @click="getList">查询</el-button>
+        <el-button type="primary" @click="addCardVisible = true">添加</el-button>
+      </div>
+    </div>
+    <el-table v-loading="listLoading" style="width: 100%" header-row-class-name="tableHeader" :data="list" border fit>
+      <el-table-column :index="indexMethod" label="序号" type="index" width="80" align="center" />
+      <el-table-column label="授权牌模板" align="center" width="200px">
+        <template slot-scope="{ row }">
+          <el-tooltip effect="dark" content="点击查看全图" placement="top-start">
+            <el-image :src="row.templateImage" :preview-src-list="[row.templateImage]" fit="cover" />
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column prop="shopName" label="所属供应商" align="center" />
+      <el-table-column label="操作" align="center" width="320px">
+        <template slot-scope="{ row }">
+          <el-button type="primary" size="mini" @click="onChangeSupplier(row)">更换供应商</el-button>
+          <el-button type="primary" size="mini" @click="onTempEdit(row)">编辑</el-button>
+          <!-- 隐藏其一 -->
+          <el-button v-if="row.status === 0" type="info" size="mini" @click="onTempStatusChange(row)">启用</el-button>
+          <el-button v-else type="danger" size="mini" @click="onTempStatusChange(row)">停用</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 供应商列表 -->
+    <el-dialog title="选择供应商" :visible.sync="selectSupplierVisible" width="66%" @close="onSelectSupplierClose">
+      <supplier-list @cancel="selectSupplierVisible = false" @confirm="onSupplierConfirm('edit', $event)" />
+    </el-dialog>
+    <!-- 上传图片 -->
+    <el-dialog title="添加授权牌模板" :visible.sync="addCardVisible" width="40%" @closed="onAddCardClose">
+      <el-dialog width="66%" title="选择供应商" :visible.sync="addCardInnerVisible" append-to-body>
+        <supplier-list @cancel="addCardInnerVisible = false" @confirm="onSupplierConfirm('add', $event)" />
+      </el-dialog>
+      <el-form ref="addCardForm" :model="formData" label-width="120px" :rules="rules">
+        <el-form-item label="授权牌模板:" prop="templateImage">
+          <el-input v-show="false" v-model="formData.templateImage" />
+          <upload-image
+            :limit="1"
+            tip="建议尺寸:542px * 546px"
+            :image-list="tempList"
+            @success="uploadTempImageSuccess"
+            @remove="handleLogoImageRemove"
+          />
+        </el-form-item>
+        <el-form-item label="二维码位置:" prop="qrPosition">
+          <el-input
+            v-model="formData.qrPosition"
+            placeholder="请输入二维码位置坐标,以英文逗号隔开,分别为二维码左上角X,Y轴坐标(例如:100,100)"
+          />
+        </el-form-item>
+        <el-form-item label="二维码大小:" prop="qrSize">
+          <el-input v-model.number="formData.qrSize" placeholder="请输入二维码大小(例如:53)" />
+        </el-form-item>
+        <el-form-item label="LOGO大小:" prop="logoSize">
+          <el-input v-model="formData.logoSize" placeholder="请输入授权牌logo建议尺寸宽高(例如:212,98)" />
+        </el-form-item>
+        <el-form-item label="所属供应商:" prop="authUserId">
+          <el-input v-show="false" v-model="formData.authUserId" />
+          <div class="current-info">
+            <span v-if="selectSupplier" class="supplier-name">{{ selectSupplier.name }}</span>
+            <span class="supplier-change" @click="addCardInnerVisible = true">
+              {{ selectSupplier ? '更换供应商' : '选择供应商' }}
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="addCardVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="isLoading" @click="onConfirm">确定</el-button>
+      </div>
+    </el-dialog>
+    <!-- 页码 -->
+    <pagination
+      :total="total"
+      :page.sync="listQuery.pageNum"
+      :limit.sync="listQuery.pageSize"
+      @pagination="getList(listQuery)"
+    />
+  </div>
+</template>
+<script>
+import UploadImage from '@/components/UploadImage'
+import SupplierList from './components/supplier-list.vue'
+import { authTempSave, fetchAuthTempList } from '@/api/system'
+import { fetchSupplierList } from '@/api/supplier'
+import { objectCover } from '@/utils'
+export default {
+  components: {
+    UploadImage,
+    SupplierList
+  },
+  data() {
+    const validateQrPosition = (rule, value, callback) => {
+      if (!value) return callback()
+      if (value.split(',').length === 2) {
+        return callback()
+      }
+      callback(new Error('二维码坐标格式不正确,请改为(X坐标,Y坐标) => (100,100)'))
+    }
+
+    const validateLogoSize = (rule, value, callback) => {
+      if (!value) return callback()
+      if (value.split(',').length === 2) {
+        return callback()
+      }
+      callback(new Error('logo建议尺寸格式不正确,请改为(宽,高)格式'))
+    }
+
+    return {
+      listLoading: false,
+      isLoading: false,
+      listQuery: {
+        listType: 1,
+        authUserId: '',
+        pageSize: 15,
+        pageNum: 1
+      },
+      list: [],
+      total: 0,
+      supplierList: [],
+      formData: {
+        templateImage: '',
+        authUserId: '',
+        qrPosition: '',
+        qrSize: '',
+        logoSize: ''
+      },
+      addCardVisible: false,
+      addCardInnerVisible: false,
+      selectSupplierVisible: false,
+      selectSupplier: null,
+      // 授权牌模板列表
+      tempList: [],
+      // 需要更换供应商的授权牌
+      currentCard: null,
+      rules: {
+        templateImage: [{ required: true, message: '请上传授权牌模板', trigger: ['change'] }],
+        authUserId: [{ required: true, message: '请选择供应商', trigger: ['change'] }],
+        qrPosition: [{ validator: validateQrPosition, trigger: ['blur'] }],
+        qrSize: [{ required: true, message: '请填写二维码大小', trigger: ['blur'] }],
+        logoSize: [
+          { required: true, message: '请输入授权牌logo建议尺寸宽高', trigger: ['blur'] },
+          { validator: validateLogoSize, trigger: ['blur'] }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.fetchSupplierList()
+  },
+  methods: {
+    // 获取授权牌模板列表
+    getList() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchAuthTempList(this.listQuery)
+    },
+
+    // 获取授权牌模板列表
+    async fetchAuthTempList() {
+      this.listLoading = true
+      try {
+        const res = await fetchAuthTempList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+        this.listLoading = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 编辑授权牌模板
+    onTempEdit(row) {
+      this.formData.templateId = row.templateId
+      this.formData.authUserId = row.authUserId
+      this.formData.qrPosition = row.qrPosition
+      this.formData.qrSize = row.qrSize
+      this.formData.logoSize = row.logoSize
+      this.formData.templateImage = row.templateImage
+      if (row.templateImage) {
+        this.tempList = [{ name: '授权牌模板', url: row.templateImage }]
+      }
+      this.addCardVisible = true
+    },
+
+    // 获取供应商列表
+    async fetchSupplierList() {
+      try {
+        const res = await fetchSupplierList({
+          listType: 1,
+          brandId: '', // 品牌id
+          linkMan: '', // 联系人
+          mobile: '', // 手机号
+          pageNum: 1, // 页码
+          pageSize: 1000, // 分页大小
+          shopName: '', // 供应商名称
+          shopType: '', // 供应商类型
+          loginAccount: '' // 登录账号
+        })
+        this.supplierList = res.data.list
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 选中供应商
+    onSupplierConfirm(type, e) {
+      type === 'add' ? (this.addCardInnerVisible = false) : (this.selectSupplierVisible = false)
+      this.selectSupplier = e
+      this.formData.authUserId = e.authUserId
+      if (type === 'edit') {
+        this.onChangeSupplierSubmit()
+      }
+    },
+
+    // 修改供应商操作
+    onChangeSupplier(row) {
+      this.currentCard = row
+      this.selectSupplierVisible = true
+    },
+
+    // 修改模板启用状态
+    onTempStatusChange(row) {
+      row.status = row.status === 0 ? 1 : 0
+      this.authTempSave(row)
+    },
+
+    // 修改供应商提交
+    onChangeSupplierSubmit() {
+      this.currentCard.authUserId = this.formData.authUserId
+      this.authTempSave(this.currentCard)
+    },
+
+    // 修改授权牌供应商弹窗关闭
+    onSelectSupplierClose() {
+      this.onAddCardClose()
+    },
+
+    // 添加授权牌弹窗关闭
+    onAddCardClose() {
+      this.selectSupplier = null
+      this.formData.templateId = ''
+      this.formData.authUserId = ''
+      this.formData.qrPosition = ''
+      this.formData.qrSize = ''
+      this.formData.logoSize = ''
+      this.formData.templateImage = ''
+      this.tempList = []
+      this.$refs.addCardForm?.clearValidate()
+    },
+
+    // 添加授权牌提交
+    async onConfirm() {
+      try {
+        this.isLoading = true
+        await this.$refs.addCardForm.validate()
+        await this.authTempSave(this.formData)
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isLoading = false
+      }
+    },
+
+    // 授权牌信息保存
+    async authTempSave(data) {
+      const submitData = objectCover(
+        {
+          templateId: '',
+          templateImage: '',
+          authUserId: '',
+          status: 1,
+          qrPosition: '',
+          logoSize: '',
+          qrSize: ''
+        },
+        data
+      )
+      try {
+        await authTempSave(submitData)
+        this.$message.success('操作成功')
+        this.getList()
+        this.addCardVisible = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 授权牌模板上传
+    uploadTempImageSuccess({ response, file, fileList }) {
+      this.tempList = fileList
+      this.formData.templateImage = response.data
+    },
+    handleLogoImageRemove({ file, fileList }) {
+      this.tempList = fileList
+      this.formData.templateImage = ''
+    },
+
+    // 表格序号
+    indexMethod(index) {
+      return index + this.listQuery.pageSize * (this.listQuery.pageNum - 1) + 1
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.el-image {
+  max-width: 120px;
+  max-height: 30px;
+  display: block;
+  margin: 0 auto;
+}
+
+.current-info {
+  font-size: 14px;
+  .supplier-name {
+    color: #333;
+    margin-right: 6px;
+  }
+  .supplier-change {
+    color: #409eff;
+    text-decoration: underline;
+    cursor: pointer;
+  }
+}
+</style>

+ 40 - 24
src/views/admin/supplier/edit.vue

@@ -25,26 +25,33 @@
         />
       </el-form-item>
 
-      <el-form-item v-if="submitForm.shopType === 1" clearable label="供应商名称:" prop="brandId">
-        <el-select
-          v-if="editType === 'add'"
-          v-model="submitForm.brandId"
-          placeholder="请选择品牌"
-          style="width: 100%"
-          filterable
-          @change="handleBrandChange"
-        >
-          <el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
-        </el-select>
-        <el-input
-          v-else
-          v-model="submitForm.shopName"
-          placeholder="请输入供应商名称"
-          maxlength="50"
-          show-word-limit
-          :disabled="editType === 'edit'"
-        />
-      </el-form-item>
+      <template v-if="submitForm.shopType === 1">
+        <el-form-item clearable label="供应商名称:" prop="brandId">
+          <el-select
+            v-if="editType === 'add'"
+            v-model="submitForm.brandId"
+            placeholder="请选择品牌"
+            style="width: 100%"
+            filterable
+            @change="handleBrandChange"
+          >
+            <el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+          <el-input
+            v-else
+            v-model="submitForm.shopName"
+            placeholder="请输入供应商名称"
+            maxlength="50"
+            show-word-limit
+            :disabled="editType === 'edit'"
+          />
+        </el-form-item>
+
+        <!-- 制造商 -->
+        <el-form-item label="制造商:">
+          <el-input v-model="submitForm.manufacturer" placeholder="请输入制造商名称" maxlength="50" show-word-limit />
+        </el-form-item>
+      </template>
       <!-- 供应商名称END -->
 
       <el-form-item label="手机号:" prop="mobile">
@@ -186,6 +193,10 @@
           </el-select>
         </el-form-item>
 
+        <el-form-item label="制造商:">
+          <el-input v-model="subForm.manufacturer" placeholder="请输入制造商名称" maxlength="50" show-word-limit />
+        </el-form-item>
+
         <el-form-item label="品牌logo:" class="no-input" prop="brandAuthLogo">
           <upload-image
             tip="建议尺寸:200px * 200px"
@@ -312,7 +323,8 @@ export default {
         appId: '',
         appSecret: '',
         qrCodeImage: '', // 微信公众号二维码
-        logo: '' // 代理商logo
+        logo: '', // 代理商logo
+        manufacturer: '' // 制造商
       },
       // 表单2
       subForm: {
@@ -324,7 +336,8 @@ export default {
         statementContent: '', // 声明内容
         statementFileId: null, // 声明文件id
         statementImage: '', // 声明图片
-        statementLink: '' // 声明链接
+        statementLink: '', // 声明链接
+        manufacturer: '' // 制造商
       },
       supplierBrands: [],
       brandList: [],
@@ -430,6 +443,7 @@ export default {
         this.submitForm.countryId = shopInfo.countryId
         this.submitForm.brandAuthLogo = shopInfo.brandAuthLogo
         this.submitForm.securityLink = shopInfo.securityLink
+        this.submitForm.manufacturer = shopInfo.manufacturer
         this.submitForm.brandName = shopInfo.brandName
 
         if (shopInfo.brandAuthLogo) {
@@ -504,7 +518,8 @@ export default {
         wxAccountType: 2,
         appId: '',
         appSecret: '',
-        qrCodeImage: '' // 微信公众号二维码
+        qrCodeImage: '', // 微信公众号二维码
+        manufacturer: '' // 制造商
       }
       for (const key in params) {
         if (Object.hasOwnProperty.call(this.submitForm, key)) {
@@ -546,6 +561,7 @@ export default {
           countryId: this.submitForm.countryId,
           brandAuthLogo: this.submitForm.brandAuthLogo,
           securityLink: this.submitForm.securityLink,
+          manufacturer: this.submitForm.manufacturer, // 制造商
           statementType: 1,
           statementContent: '',
           statementFileId: null,
@@ -553,7 +569,6 @@ export default {
           statementLink: ''
         }
       ]
-
       return params
     },
     // 大理上添加
@@ -674,6 +689,7 @@ export default {
     // 添加品牌对话框关闭
     handleDialogAddBrandClosed() {
       this.subForm.securityLink = ''
+      this.subForm.manufacturer = ''
       this.subForm.statementType = 1
       this.statementFileName = ''
       this.statementImageList = []

+ 29 - 1
src/views/normal/club/device/edit.vue

@@ -45,9 +45,36 @@
         <el-input v-model="formData.productImage" class="hiddenInput" />
       </el-form-item>
        -->
+
+      <!-- **************** 新方法配置授权牌 START ******************* -->
+      <!-- <el-form-item label="授权牌:">
+        <el-radio-group v-model="formData.addQrCodeFlag" size="mini">
+          <el-radio :label="0" border>模板库生成</el-radio>
+          <el-radio :label="1" border>自定义上传</el-radio>
+        </el-radio-group>
+        <div style="margin-top: 12px">
+
+          <template v-if="formData.addQrCodeFlag === 1">
+            <upload-image
+              tip="建议尺寸:150px * 112px"
+              :image-list="certificateImageList"
+              :before-upload="beforeCertificateImageUpload"
+              @success="uploadCertificateImageSuccess"
+              @remove="handleCertificateImageRemove"
+            />
+            <el-input v-model="formData.certificateImage" class="hiddenInput" />
+          </template>
+          <el-image v-else style="width:148px;height:148px" />
+        </div>
+      </el-form-item> -->
+
+      <!-- **************** 新方法配置授权牌 End ******************* -->
+
+      <!-- **************** 旧方法配置授权牌 START ******************* -->
+      <!-- 上传授权牌 -->
       <el-form-item label="授权牌:" prop="certificateImage">
         <upload-image
-          tip="建议尺寸:150px * 112px"
+          tip="建议尺寸:542px * 546px"
           :image-list="certificateImageList"
           :before-upload="beforeCertificateImageUpload"
           @success="uploadCertificateImageSuccess"
@@ -66,6 +93,7 @@
       <el-form-item v-if="formData.addQrCodeFlag === 1" label="选择模板:">
         <auth-card-template v-model="formData.addTemplateType" :image-list="certificateImageList" />
       </el-form-item>
+      <!-- **************** 旧方法配置授权牌 END ******************* -->
 
       <el-form-item label="购买渠道:">
         <el-input v-model="formData.purchaseWay" placeholder="请输入购买渠道" />

+ 187 - 12
src/views/normal/club/edit.vue

@@ -32,6 +32,14 @@
       <el-form-item label="联系电话:" prop="mobile">
         <el-input v-model="formData.mobile" placeholder="请输入联系方式" clearable />
       </el-form-item>
+
+      <el-form-item label="认证编号:" prop="authCode">
+        <el-input v-model="formData.authCode" placeholder="请输认证编号" clearable />
+      </el-form-item>
+
+      <el-form-item label="认证日期:" prop="authDate">
+        <el-date-picker v-model="formData.authDate" type="date" placeholder="选择日期" style="width: 100%" />
+      </el-form-item>
       <!-- <el-form-item label="手机号:" prop="userMobile">
         <el-input v-model="formData.userMobile" placeholder="请输入手机号" clearable maxlength="11" show-word-limit />
       </el-form-item> -->
@@ -46,6 +54,7 @@
         />
       </el-form-item>
       <el-form-item label="门头照:" prop="banner">
+        <div class="form-label-tip">至少上传一张机构门店图,最多上传6张)</div>
         <el-input v-show="false" v-model="formData.banner" />
         <upload-image
           tip="至少上传一张,最多6张;建议尺寸:542px * 542px"
@@ -57,6 +66,50 @@
         />
       </el-form-item>
 
+      <!-- **************** 新方法配置授权牌 START ******************* -->
+      <el-form-item label="授权牌:">
+        <el-radio-group v-model="formData.authImageType" size="mini">
+          <el-radio :label="1" border>模板库生成</el-radio>
+          <el-radio :label="2" border>自定义上传</el-radio>
+        </el-radio-group>
+        <div style="margin-top: 12px">
+          <template v-if="formData.authImageType === 2">
+            <upload-image
+              tip="建议尺寸:542px * 546px"
+              :image-list="authImageList"
+              :before-upload="beforeAuthImageUpload"
+              @success="uploadAuthImageSuccess"
+              @remove="handleAuthImageRemove"
+            />
+            <el-input v-show="false" v-model="formData.authImage" />
+          </template>
+          <template v-else>
+            <el-image
+              v-if="formData.authImage"
+              style="width: 148px; height: 148px"
+              :src="formData.authImage"
+              :preview-src-list="[formData.authImage]"
+            />
+          </template>
+        </div>
+      </el-form-item>
+
+      <el-form-item v-if="formData.authImageType === 1" label="授权牌logo:">
+        <div class="form-label-tip">授权牌logo(提示:授权牌logo与机构名称组合)</div>
+        <div style="margin-top: 8px">
+          <upload-image
+            :tip="authImageLogoUploadTip"
+            :image-list="authImageLogoList"
+            :before-upload="beforeAuthImageLogoUpload"
+            @success="uploadAuthImageLogoSuccess"
+            @remove="handleAuthImageLogoRemove"
+          />
+          <el-input v-show="false" v-model="formData.authImageLogo" />
+        </div>
+      </el-form-item>
+
+      <!-- **************** 新方法配置授权牌 End ******************* -->
+
       <el-form-item label="机构类型:">
         <el-radio-group v-model="formData.firstClubType">
           <el-radio :label="1">医美</el-radio>
@@ -146,6 +199,8 @@ import { mapGetters } from 'vuex'
 import { saveBrandAuth, getAuthFormData } from '@/api/auth'
 import { getAddress } from '@/api/common'
 import { isPoint, isMobile, isNumber } from '@/utils/validate'
+import { formatDate } from '@/utils'
+import { authTempUsed } from '@/api/system'
 
 export default {
   components: {
@@ -212,7 +267,13 @@ export default {
         empNum: '',
         firstClubType: 1,
         secondClubType: 1,
-        medicalLicenseImage: ''
+        medicalLicenseImage: '',
+        // 新增授权牌字段
+        authCode: '',
+        authDate: '',
+        authImageType: '',
+        authImageLogo: '',
+        authImage: ''
       },
       rules: {
         name: [{ required: true, message: '机构名称不能为空', trigger: ['blur', 'change'] }],
@@ -225,7 +286,8 @@ export default {
         remarks: [{ required: true, message: '店铺备注不能为空', trigger: 'blur' }],
         medicalLicenseImage: [{ required: true, message: '请上传医疗许可证', trigger: 'change' }],
         empNum: [{ required: true, message: '员工人数不能为空', trigger: 'change' }],
-        userMobile: [{ validator: validatePhoneNumber, trigger: 'change' }]
+        userMobile: [{ validator: validatePhoneNumber, trigger: 'change' }],
+        authImageLogo: [{ required: true, message: '请上传授权牌logo', trigger: 'change' }]
       },
       // logo图片列表
       logoList: [],
@@ -234,7 +296,16 @@ export default {
       // 级联选择的地址
       address: '',
       // 医疗许可证图片
-      licenseImageList: []
+      licenseImageList: [],
+      // 授权牌照图片列表
+      authImageList: [],
+      // 授权牌logo列表
+      authImageLogoList: [],
+      // 验证
+      validatorFields: {
+        authImageLogoWidth: 100,
+        authImageLogoHeight: 100
+      }
     }
   },
   computed: {
@@ -265,12 +336,17 @@ export default {
     // 位置坐标
     lnglat() {
       return this.formData.point ? this.formData.point.split(',') : null
+    },
+    // 授权牌logo上传提示
+    authImageLogoUploadTip() {
+      return `限制尺寸:${this.validatorFields.authImageLogoWidth}px *${this.validatorFields.authImageLogoHeight}px`
     }
   },
   created() {
     this.editType = this.$route.query.type || 'add'
     this.authId = this.$route.query.id
     this.initFormData()
+    this.fetchAuthTempUsed()
   },
   methods: {
     // 地图定位
@@ -320,8 +396,13 @@ export default {
         this.formData.customFlag = res.data.customFlag
         this.formData.remarks = res.data.remarks
 
-        this.logoList = [{ name: '', url: res.data.logo }]
-        this.bannerList = res.data.bannerList.map((item) => ({ name: '', url: item }))
+        if (res.data.logo) {
+          this.logoList = [{ name: '机构logo', url: res.data.logo }]
+        }
+
+        if (res.data.bannerList.length > 0) {
+          this.bannerList = res.data.bannerList.map((item, index) => ({ name: `门头照${index}`, url: item }))
+        }
 
         this.formData.address = [res.data.provinceId, res.data.cityId, res.data.townId]
         // this.formData.address = '广东省/深圳市/福田区'
@@ -331,11 +412,28 @@ export default {
         this.formData.empNum = res.data.empNum
         this.formData.firstClubType = res.data.firstClubType
         this.formData.secondClubType = res.data.secondClubType
+
         this.formData.medicalLicenseImage = res.data.medicalLicenseImage
+        if (res.data.medicalLicenseImage) {
+          this.licenseImageList = [{ name: '医疗许可证', url: res.data.medicalLicenseImage }]
+        }
 
         this.formData.userMobile = res.data.userMobile
 
-        this.licenseImageList = [{ name: '', url: res.data.medicalLicenseImage }]
+        // 授权牌相关字段
+        this.formData.authCode = res.data.authCode
+        this.formData.authDate = res.data.authDate
+        this.formData.authImageType = res.data.authImageType
+
+        this.formData.authImageLogo = res.data.authImageLogo
+        if (res.data.authImageLogo) {
+          this.authImageLogoList = [{ name: '授权牌logo', url: res.data.authImageLogo }]
+        }
+
+        this.formData.authImage = res.data.authImage
+        if (res.data.authImage) {
+          this.authImageList = [{ name: '自定义授权牌', url: res.data.authImage }]
+        }
       })
     },
 
@@ -357,7 +455,12 @@ export default {
           firstClubType,
           secondClubType,
           medicalLicenseImage,
-          userMobile
+          userMobile,
+          authCode,
+          authDate,
+          authImageType,
+          authImageLogo,
+          authImage
         } = this.formData
 
         const authUserId = this.proxyInfo?.authUserId || this.authUserId
@@ -380,11 +483,17 @@ export default {
           firstClubType,
           secondClubType,
           medicalLicenseImage,
-          userMobile
+          userMobile,
+          authCode,
+          authDate: authDate ? formatDate(authDate, 'yyyy.MM.DD') : '',
+          authImageType,
+          authImageLogo,
+          authImage
         }
 
         data.bannerList = this.bannerList.map((item) => (item.response ? item.response.data : item.url))
-
+        console.log(data)
+        // return
         saveBrandAuth(data)
           .then((res) => {
             const h = this.$createElement
@@ -393,12 +502,12 @@ export default {
               message: h('i', { style: 'color: #333' }, `已修改授权机构:"${this.formData.name}"`),
               duration: 3000
             })
-            this.$refs.submitForm.resetFields()
+            // this.$refs.submitForm.resetFields()
             this.$store.dispatch('tagsView/delView', this.$route)
             this.$router.push('/club/list')
           })
           .catch((err) => {
-            this.$message.danger(err.msg)
+            console.log(err)
           })
       })
     },
@@ -407,6 +516,25 @@ export default {
       if (node.length <= 0) return
       this.address = node[0].pathLabels.join()
     },
+
+    // 获取当前机构可用授权牌模板
+    async fetchAuthTempUsed() {
+      try {
+        const res = await authTempUsed({
+          authUserId: this.authUserId,
+          authFlag: 1,
+          status: 1
+        })
+        if (res.data) {
+          const [width, height] = res.data.logoSize.split(',')
+          this.validatorFields.authImageLogoWidth = width
+          this.validatorFields.authImageLogoHeight = height
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
     // logo上传
     uploadLogoSuccess({ response, file, fileList }) {
       this.logoList = fileList
@@ -458,6 +586,54 @@ export default {
         this.$message.error('医疗许可证图片大小不能超过 1MB!')
       }
       return flag
+    },
+
+    // 授权牌照上传
+    beforeAuthImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传授权牌图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+    uploadAuthImageSuccess({ response, file, fileList }) {
+      this.authImageList = fileList
+      this.formData.authImage = response.data
+    },
+    handleAuthImageRemove({ file, fileList }) {
+      this.authImageList = fileList
+      this.formData.authImage = ''
+    },
+
+    // 授权牌logo上传
+    beforeAuthImageLogoUpload(file) {
+      return new Promise((resolve, reject) => {
+        if (file.size > 5 * 1024 * 1024) {
+          this.$message.error('授权牌logo图片大小不能超过 5MB!')
+          reject('图片大小超出最大限制')
+        }
+        const image = new Image()
+        image.src = URL.createObjectURL(file)
+        image.onload = (e) => {
+          const { path } = e
+          const { naturalWidth, naturalHeight } = path[0]
+          const { authImageLogoWidth: width, authImageLogoHeight: height } = this.validatorFields
+          if (naturalWidth > width || naturalHeight > height) {
+            this.$message.error('图片尺寸校验未通过')
+            reject('图片尺寸校验未通过')
+          } else {
+            resolve('图片尺寸校验通过')
+          }
+        }
+      })
+    },
+    uploadAuthImageLogoSuccess({ response, file, fileList }) {
+      this.authImageLogoList = fileList
+      this.formData.authImageLogo = response.data
+    },
+    handleAuthImageLogoRemove({ file, fileList }) {
+      this.authImageLogoList = fileList
+      this.formData.authImageLogo = ''
     }
   }
 }
@@ -484,7 +660,6 @@ export default {
     width: 140px;
   }
 }
-
 .attributes {
   padding-top: 16px;
 }

+ 170 - 0
src/views/normal/settings/accounts/edit.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="app-container menus-edit">
+    <el-form ref="userInfoFormRef" label-width="100px" :model="userInfo" :rules="rules">
+      <!-- <el-form-item label="用户头像:">
+        <el-input v-show="false" v-model="user.avatar" />
+        <upload-image tip="建议尺寸:60 * 60px" />
+      </el-form-item> -->
+      <el-form-item label="登录名:" prop="username">
+        <el-input v-model="userInfo.username" placeholder="登录名" />
+      </el-form-item>
+      <el-form-item v-if="!userInfo.id" label="密码:" prop="password">
+        <el-input v-model="userInfo.password" placeholder="密码" />
+      </el-form-item>
+      <el-form-item label="联系人:">
+        <el-input v-model="userInfo.linkMan" placeholder="联系人" />
+      </el-form-item>
+      <el-form-item label="手机号:" prop="mobile">
+        <el-input v-model="userInfo.mobile" placeholder="手机号" />
+      </el-form-item>
+      <el-form-item label="状态:">
+        <el-radio-group v-model="userInfo.status">
+          <el-radio :label="1">启用</el-radio>
+          <el-radio :label="0">停用</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="用户角色:" prop="roleList">
+        <el-checkbox-group v-model="userInfo.roleList">
+          <el-checkbox v-for="item in roleList" :key="item.id" :label="item.id">{{ item.roleName }}</el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item>
+        <!-- 确认 取消 -->
+        <div class="control-box">
+          <el-button type="warning" @click="$_back">返回</el-button>
+          <el-button type="primary" @click="onSubmit">保存</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { fetchRoleList, createUser, getUser, updateUser } from '@/api/system'
+import { isMobile } from '@/utils/validate'
+
+export default {
+  data() {
+    // 校验角色
+    const validateRoleList = (rule, value, callback) => {
+      if (value instanceof Array) {
+        if (value.length === 0) {
+          callback(new Error('请至少选择一个角色'))
+        } else {
+          callback()
+        }
+      } else {
+        callback(new Error('角色列表为一个数组'))
+      }
+    }
+    // 校验手机号
+    const validateMobile = (rule, value, callback) => {
+      if (value && isMobile(value)) {
+        callback()
+      } else {
+        callback(new Error('请输入合法的手机号'))
+      }
+    }
+    return {
+      editType: 'add',
+      userInfo: {
+        id: 0,
+        username: '',
+        password: '',
+        linkMan: '',
+        mobile: '',
+        status: 1,
+        roleList: [],
+        roleIds: ''
+      },
+      roleList: [],
+      rules: {
+        username: [
+          { required: true, message: '请输入登录名', trigger: 'blur' },
+          { min: 2, max: 15, message: '长度在 2 到 15 个字符', trigger: 'blur' }
+        ],
+        mobile: [{ validator: validateMobile, trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+        roleList: [{ required: true, validator: validateRoleList, trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.editType = this.$route.query.type
+    this.fetchRoleList()
+    if (this.editType === 'edit') {
+      this.userInfo.id = this.$route.query.id
+      this.fetchUserInfo()
+    }
+  },
+  methods: {
+    // 获取角色列表
+    fetchRoleList() {
+      fetchRoleList().then(res => {
+        this.roleList = [...this.roleList, ...res.data.list]
+      })
+    },
+    // 修改保存用户信息
+    updateUserInfo() {
+      this.userInfo.roleIds = this.userInfo.roleList.join(',')
+      updateUser(this.userInfo.id, this.userInfo).then(res => {
+        this.$message.success('修改用户成功')
+        this.$store.dispatch('tagsView/delView', this.$route)
+        this.$router.back()
+      })
+    },
+    // 保存用户信息
+    createUserInfo() {
+      this.userInfo.roleIds = this.userInfo.roleList.join(',')
+      createUser(this.userInfo).then(res => {
+        this.$message.success('添加用户成功')
+        this.$store.dispatch('tagsView/delView', this.$route)
+        this.$router.back()
+      })
+    },
+    // 提交表单
+    onSubmit() {
+      this.$refs.userInfoFormRef.validate(valide => {
+        if (!valide) return
+        if (this.editType === 'add') {
+          this.createUserInfo()
+        } else {
+          this.updateUserInfo()
+        }
+      })
+    },
+    // 获取用户信息
+    fetchUserInfo() {
+      getUser(this.userInfo.id).then(res => {
+        this.setUserInfo(res.data)
+      })
+    },
+    // 设置用户信息
+    setUserInfo(data) {
+      console.log(data)
+      for (const key in this.userInfo) {
+        if (Object.hasOwnProperty.call(data, key)) {
+          this.userInfo[key] = data[key]
+        }
+      }
+      this.userInfo.roleList = data.roleIds.split(',').map(item => parseInt(item))
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.menus-edit {
+  width: 600px;
+  margin: 0 auto;
+  margin-top: 100px;
+}
+.control-box {
+  margin: 20px 0;
+  text-align: center;
+  .el-button {
+    width: 120px;
+    margin: 0 8px;
+  }
+}
+</style>

+ 118 - 0
src/views/normal/settings/accounts/index.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区域START -->
+    <div class="filter-container">
+      <div class="filter-control">
+        <span>启用状态:</span>
+        <el-select
+          v-model="listQuery.status"
+          placeholder="启用状态"
+          clearable
+          style="width: 200px"
+          class="filter-item"
+          @change="getList()"
+        >
+          <el-option label="全部" value="" />
+          <el-option label="已启用" :value="0" />
+          <el-option label="未启用" :value="1" />
+        </el-select>
+      </div>
+      <div class="filter-control">
+        <el-button type="primary" @click="$_navigationTo('accounts/add?type=add')">添加用户</el-button>
+      </div>
+    </div>
+    <!-- 搜索区域END -->
+    <el-table
+      v-loading="listLoading"
+      :data="list"
+      style="width: 100%"
+      border
+      fit
+      class="table-cell"
+      header-row-class-name="tableHeader"
+    >
+      <el-table-column :index="indexMethod" label="序号" type="index" width="80" align="center" />
+      <!-- <el-table-column prop="title" label="头像" align="center" width="60">
+        <template slot-scope="{ row }">
+          <img :src="row.avatar" class="avatar" alt="">
+        </template>
+      </el-table-column> -->
+      <el-table-column prop="username" label="登录名" align="center" />
+      <el-table-column prop="linkMan" label="联系人" align="center" />
+      <el-table-column label="手机号" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.mobile || '—' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="角色描述" align="center">
+        <template slot-scope="{ row }">
+          <span>{{ row.roleDesc || '—' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="启用状态" align="center" width="100">
+        <template slot-scope="{ row }">
+          <el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200">
+        <template slot-scope="{ row }">
+          <el-button type="primary" size="mini" :disabled="!row.id" @click="$_navigationTo(`accounts/edit?type=edit&id=${row.id}`)">编辑</el-button>
+          <el-button type="danger" size="mini" :disabled="!row.id" @click="removeUser(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchUserList, deleteUser } from '@/api/system'
+export default {
+  data() {
+    return {
+      listLoading: false,
+      listQuery: {
+        status: '',
+        pageSize: 10,
+        pageNum: 1
+      },
+      list: []
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    indexMethod(index) {
+      return index + this.listQuery.pageSize * (this.listQuery.pageNum - 1) + 1
+    },
+    // 获取列表
+    getList() {
+      this.listQuery.pageNum = 1
+      this.list = []
+      this.fetchUserList()
+    },
+    // 获取用户列表
+    fetchUserList() {
+      fetchUserList(this.listQuery).then(res => {
+        console.log(res)
+        this.list = [...this.list, ...res.data.list]
+      })
+    },
+    // 删除用户
+    removeUser(row) {
+      deleteUser(row.id).then(res => {
+        this.$message.success('删除用户成功')
+        this.getList()
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.avatar{
+  display: block;
+  width: 40px;
+  height: 40px;
+}
+</style>

+ 142 - 0
src/views/normal/settings/auth-card/components/supplier-list.vue

@@ -0,0 +1,142 @@
+<template>
+  <div>
+    <div class="filter-container">
+      <div class="filter-control">
+        <span>供应商名称:</span>
+        <el-input
+          v-model="listQuery.shopName"
+          size="mini"
+          placeholder="供应商名称"
+          clearable
+          @keyup.enter.native="fetchSupplierList"
+        />
+      </div>
+      <div class="filter-control">
+        <span>供应商类型:</span>
+        <el-select
+          v-model="listQuery.shopType"
+          placeholder="供应商类型"
+          size="mini"
+          clearable
+          @change="fetchSupplierList"
+        >
+          <el-option label="所有类型" value="" />
+          <el-option label="代理商" :value="2" />
+          <el-option label="品牌方" :value="1" />
+        </el-select>
+      </div>
+      <div class="filter-control">
+        <span>手机号:</span>
+        <el-input
+          v-model="listQuery.mobile"
+          clearable
+          size="mini"
+          placeholder="手机号"
+          @keyup.enter.native="fetchSupplierList"
+        />
+      </div>
+      <div class="filter-control">
+        <el-button type="primary" size="mini" @click="filterSupplierList">查询</el-button>
+      </div>
+    </div>
+    <el-table
+      v-infinite-scroll="onLoad"
+      :data="supplierList"
+      border
+      class="infinite-list hide-table-check-all"
+      style="overflow: auto"
+      :infinite-scroll-delay="300"
+      :infinite-scroll-immediate="false"
+      header-row-class-name="tableHeader"
+      highlight-current-row
+      @row-click="handleSelectSupplier"
+    >
+      >
+      <el-table-column label="选择" width="55" align="center">
+        <template slot-scope="{ row }">
+          <el-radio v-model="selectedAuthUserId" :label="row.authUserId"><span v-show="false">1</span></el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column label="序号" type="index" width="80" align="center" />
+      <el-table-column property="name" label="供应商名称" />
+      <el-table-column label="供应商类型" align="center" width="120">
+        <template slot-scope="{ row }">
+          <span v-if="row.shopType === 1">品牌方</span>
+          <span v-else>代理商</span>
+        </template>
+      </el-table-column>
+      <!-- <el-table-column label="登录账号" align="center">
+        <template slot-scope="{ row }">
+          <span v-if="row.loginAccount">{{ row.loginAccount }}</span>
+          <span v-else>未绑定</span>
+        </template>
+      </el-table-column> -->
+      <el-table-column prop="mobile" label="手机号" align="center" width="160" />
+      <el-table-column prop="linkMan" label="联系人" align="center" />
+    </el-table>
+  </div>
+</template>
+<script>
+import { fetchSupplierList } from '@/api/supplier'
+export default {
+  name: 'SupplierList',
+  data() {
+    return {
+      listQuery: {
+        shopName: '',
+        shopType: '',
+        mobile: '',
+        pageNum: 1,
+        pageSize: 10
+      },
+      supplierList: [],
+      selectedAuthUserId: ''
+    }
+  },
+  created() {
+    this.filterSupplierList()
+  },
+  methods: {
+    // 确认选中的供应商
+    handleSubmitSupplier() {
+      this.dialogTableVisible = false
+      this.currentSupplierList = [this.selectedSupplierInfo]
+    },
+    // 选择供应商
+    handleSelectSupplier(e) {
+      this.selectedAuthUserId = e.authUserId
+      this.selectedSupplierInfo = e
+      this.$emit('selected', e)
+    },
+    // 选择供应商对话框关闭事件
+    handleDialogClosed() {
+      this.listQuery.pageNum = 1
+      this.listQuery.shopName = ''
+      this.listQuery.shopType = ''
+      this.listQuery.mobile = ''
+      this.listQuery.linkMan = ''
+    },
+    // 筛选供应商
+    filterSupplierList() {
+      this.pageNum = 1
+      this.supplierList = []
+      this.fetchSupplierList()
+    },
+    // 获取供应商列表
+    fetchSupplierList() {
+      fetchSupplierList(this.listQuery).then((res) => {
+        console.log(res)
+        this.supplierList = [...this.supplierList, ...res.data.list]
+        this.hasNextPage = res.data.hasNextPage
+        this.total = res.data.total
+      })
+    },
+    onLoad() {
+      if (!this.hasNextPage) return
+      this.listQuery.pageNum++
+      this.fetchSupplierList()
+    }
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 227 - 0
src/views/normal/settings/auth-card/index.vue

@@ -0,0 +1,227 @@
+<template>
+  <div class="app-container">
+    <!-- 筛选 -->
+    <!-- <div class="filter-container">
+      <div class="filter-control">
+        <el-button type="primary" :loading="isLoading" @click="authTempUpdate">
+          <span>{{ isLoading ? '正在更新...' : '一键生效' }}</span>
+        </el-button>
+      </div>
+    </div> -->
+    <!-- <div class="tip">提示:请勿频繁更新授权牌模板,如有需要,在更新授权牌模板后点击左上角一键生效按钮即可</div> -->
+
+    <!-- 列表 -->
+    <el-table v-loading="listLoading" style="width: 100%" header-row-class-name="tableHeader" :data="list" border fit>
+      <el-table-column :index="indexMethod" label="序号" type="index" width="80" align="center" />
+      <el-table-column label="授权牌模板" align="center" width="200px">
+        <template slot-scope="{ row }">
+          <!-- <el-tooltip effect="dark" content="点击查看全图" placement="top-start">
+            <el-image :src="row.templateImage" :preview-src-list="[row.templateImage]" fit="cover" />
+          </el-tooltip> -->
+          <el-image :src="row.templateImage" fit="cover" />
+        </template>
+      </el-table-column>
+      <el-table-column label="使用位置" align="center">
+        <template slot-scope="{ row }">
+          <el-radio v-model="row.authFlag" :label="1" @change="onClubAuthTempChange(row)">机构认证</el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="280px">
+        <template slot-scope="{ row }">
+          <el-button type="primary" size="mini" @click="onPreview(row)">查看</el-button>
+          <el-button type="primary" size="mini" @click="onDragQrcode(row)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 拖拽选取二维码位置 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dragDialogVisible" :fullscreen="fullscreen" @closed="onDragClose">
+      <drag-layout
+        v-if="dragVisible"
+        :qw="current.qw"
+        :qh="current.qh"
+        :old-x="current.oldX"
+        :old-y="current.oldY"
+        :image="current.templateImage"
+        :size="current.templateSize"
+        :is-drag="isDrag"
+        @confirm="onDragConfirm"
+        @cancel="dragDialogVisible = false"
+      />
+    </el-dialog>
+
+    <!-- 页码 -->
+    <pagination
+      :total="total"
+      :page.sync="listQuery.pageNum"
+      :limit.sync="listQuery.pageSize"
+      @pagination="getList(listQuery)"
+    />
+  </div>
+</template>
+<script>
+import DragLayout from '@/components/DragLayout/index.vue'
+import { authTempSave, authTempUpdate, fetchAuthTempList } from '@/api/system'
+import { mapGetters } from 'vuex'
+
+export default {
+  name: 'NormalSettingsAuthCard',
+  components: {
+    DragLayout
+  },
+  data() {
+    return {
+      isLoading: false,
+      dragDialogVisible: false,
+      dragVisible: false,
+      listLoading: false,
+      listQuery: {
+        listType: 2,
+        authUserId: '',
+        pageSize: 15,
+        pageNum: 1
+      },
+      list: [],
+      total: 0,
+      current: null,
+      dialogTitle: '设置二维码位置',
+      fullscreen: true,
+      isDrag: true
+    }
+  },
+  computed: {
+    ...mapGetters(['authUserId'])
+  },
+  created() {
+    this.listQuery.authUserId = this.authUserId
+    this.getList()
+  },
+  methods: {
+    // 一键生效
+    async authTempUpdate() {
+      this.isLoading = true
+      let notification = null
+      notification = this.$notify({
+        title: '提示',
+        message: `模板生效同步中,请勿重复操作!`,
+        duration: 0
+      })
+      try {
+        const res = await authTempUpdate({ authUserId: this.authUserId })
+        console.log(res)
+        this.$message.success('模板更新成功')
+      } catch (error) {
+        console.log(error)
+        this.$message.error(`生效失败,请重试!`)
+      } finally {
+        notification.close()
+        this.isLoading = false
+      }
+    },
+
+    // 确定二维码坐标
+    async onDragConfirm(e) {
+      if (!this.isDrag) return (this.dragDialogVisible = false)
+      try {
+        await authTempSave({
+          templateId: this.current.templateId,
+          qrPosition: [e.x, e.y].join(',')
+        })
+        this.$message.success('模板修改成功')
+        this.getList()
+        this.dragDialogVisible = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 选中模板切换
+    async onClubAuthTempChange(row) {
+      this.list.forEach((item) => {
+        if (item.templateId !== row.templateId) item.authFlag = 0
+      })
+      try {
+        await authTempSave({
+          authUserId: this.authUserId,
+          templateId: row.templateId,
+          authFlag: 1
+        })
+        this.getList()
+        this.$message.success('模板使用成功')
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 预览图片
+    onPreview(row) {
+      this.fullscreen = false
+      this.dialogTitle = '预览'
+      this.isDrag = false
+      this.onDragQrcode(row)
+    },
+
+    // 编辑二维码
+    onDragQrcode(row) {
+      this.current = row
+      if (row.qrSize) {
+        this.current.qw = parseFloat(row.qrSize)
+        this.current.qh = parseFloat(row.qrSize)
+      }
+      const arrs = row.qrPosition ? row.qrPosition.split(',') : [0, 0]
+      this.current.oldX = parseFloat(arrs[0])
+      this.current.oldY = parseFloat(arrs[1])
+      this.dragDialogVisible = true
+      this.dragVisible = true
+    },
+
+    // 拖拽框关闭
+    onDragClose() {
+      console.log(123)
+      this.fullscreen = true
+      this.dialogTitle = '设置二维码位置'
+      this.isDrag = true
+      this.dragVisible = false
+    },
+
+    // 获取授权牌模板列表
+    getList() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchAuthTempList(this.listQuery)
+    },
+
+    // 获取授权牌模板列表
+    async fetchAuthTempList() {
+      this.listLoading = true
+      try {
+        const res = await fetchAuthTempList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+        this.listLoading = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 表格序号
+    indexMethod(index) {
+      return index + this.listQuery.pageSize * (this.listQuery.pageNum - 1) + 1
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.el-image {
+  max-width: 120px;
+  max-height: 30px;
+  display: block;
+  margin: 0 auto;
+}
+
+.tip {
+  margin: 12px 0;
+  font-size: 14px;
+  color: #f56c6c;
+}
+</style>

+ 143 - 0
src/views/normal/settings/roles/edit.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="app-container roles-edit">
+    <el-form ref="ruleRef" label-width="100px" :model="role" :rules="rules">
+      <el-form-item label="角色名称:" prop="roleName">
+        <el-input v-model="role.roleName" placeholder="角色名称" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="角色描述:" prop="roleDesc">
+        <el-input v-model="role.roleDesc" placeholder="角色描述" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="角色授权:" prop="menuIds">
+        <el-input v-show="false" v-model="role.menuIds" />
+        <el-tree
+          ref="tree"
+          :data="menuTree"
+          show-checkbox
+          default-expand-all
+          node-key="id"
+          highlight-current
+          :props="defaultProps"
+          @check-change="getCheckedKeys"
+        />
+      </el-form-item>
+      <el-form-item>
+        <!-- 确认 取消 -->
+        <div class="control-box">
+          <el-button type="warning" @click="$_back">返回</el-button>
+          <el-button type="primary" @click="handleSubmit">保存</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { sysMenuTree, createRole, getRole, updateRole } from '@/api/system'
+export default {
+  data() {
+    return {
+      editType: '',
+      role: {
+        id: '',
+        roleName: '',
+        roleDesc: '',
+        menuIds: ''
+      },
+      rules: {
+        roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
+        menuIds: [{ required: true, message: '角色授权不能为空', trigger: 'change' }]
+      },
+      menuTree: [],
+      defaultProps: {
+        children: 'subMenus',
+        label: 'title'
+      }
+    }
+  },
+  created() {
+    this.editType = this.$route.query.type
+    if (this.editType === 'edit') {
+      this.role.id = parseInt(this.$route.query.id)
+      this.fetchRoleInfo()
+    }
+    this.getMenuTree()
+  },
+  methods: {
+    getCheckedNodes() {
+      console.log(this.$refs.tree.getCheckedNodes())
+    },
+    getCheckedKeys() {
+      // console.log(this.$refs.tree.getCheckedNodes(false, true).map(item => item.id))
+      this.role.menuIds = this.$refs.tree.getCheckedNodes(false, true).map(item => item.id).join(',')
+    },
+    // 获取菜单权限列表
+    getMenuTree() {
+      sysMenuTree({ menuType: 1 }).then(res => {
+        this.menuTree = res.data
+      })
+    },
+    // 设置角色信息
+    setRoleInfo(data) {
+      for (const key in this.role) {
+        if (Object.hasOwnProperty.call(this.role, key)) {
+          this.role[key] = data[key]
+        }
+      }
+      console.log(this.role.menuIds.split(','))
+      this.$refs.tree.setCheckedKeys([...this.role.menuIds.split(',')])
+    },
+    // 获取角色信息
+    fetchRoleInfo() {
+      getRole(this.role.id).then(res => {
+        console.log(res.data)
+        this.setRoleInfo(res.data)
+      })
+    },
+    // 修改角色
+    updateRole() {
+      updateRole(this.role.id, this.role).then(res => {
+        console.log(res)
+        this.$message.success('修改角色成功')
+        this.$store.dispatch('tagsView/delView', this.$route)
+        this.$router.back()
+      })
+    },
+    // 创建角色
+    createRole() {
+      createRole(this.role).then(res => {
+        console.log(res)
+        this.$message.success('添加角色成功')
+        this.$store.dispatch('tagsView/delView', this.$route)
+        this.$router.back()
+      })
+    },
+    // 提交
+    handleSubmit() {
+      this.$refs.ruleRef.validate(valide => {
+        if (!valide) return
+        if (this.editType === 'add') {
+          this.createRole()
+        } else {
+          this.updateRole()
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.roles-edit {
+  width: 600px;
+  margin: 0 auto;
+  margin-top: 40px;
+}
+.control-box {
+  margin: 20px 0;
+  text-align: center;
+  .el-button {
+    width: 120px;
+    margin: 0 8px;
+  }
+}
+</style>

+ 107 - 0
src/views/normal/settings/roles/index.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-control">
+        <el-button type="primary" @click="$_navigationTo('roles/add?type=add')">添加角色</el-button>
+      </div>
+    </div>
+    <!-- 搜索区域END -->
+    <el-table
+      v-loading="listLoading"
+      :data="list"
+      style="width: 100%"
+      border
+      fit
+      class="table-cell"
+      header-row-class-name="tableHeader"
+    >
+      <el-table-column :index="indexMethod" label="序号" type="index" width="80" align="center" />
+      <el-table-column prop="roleName" label="角色名" align="center" />
+      <el-table-column label="角色描述" align="center">
+        <template slot-scope="{ row }">
+          <span v-if="row.roleDesc">{{ row.roleDesc }}</span>
+          <span v-else>—</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" width="160px">
+        <template slot-scope="{ row }">
+          <span v-if="row.createTime">{{ row.createTime | formatTime }}</span>
+          <span v-else>—</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" width="160px">
+        <template slot-scope="{ row }">
+          <span v-if="row.updateTime">{{ row.updateTime | formatTime }}</span>
+          <span v-else>—</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200">
+        <template slot-scope="{ row }">
+          <el-button type="primary" size="mini" @click="$_navigationTo(`roles/edit?type=edit&id=${row.id}`)">编辑</el-button>
+          <el-button type="danger" size="mini" @click="removeRole(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchRoleList, deleteRole } from '@/api/system'
+export default {
+  data() {
+    return {
+      listLoading: false,
+      listQuery: {
+        status: '',
+        pageSize: 10,
+        pageNum: 1
+      },
+      list: []
+    }
+  },
+  created() {
+    this.fetchRoleList()
+  },
+  methods: {
+    indexMethod(index) {
+      return index + this.listQuery.pageSize * (this.listQuery.pageNum - 1) + 1
+    },
+    getList() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchRoleList()
+    },
+    // 获取角色列表
+    fetchRoleList() {
+      fetchRoleList(this.listQuery).then(res => {
+        this.list = [...this.list, ...res.data.list]
+      })
+    },
+    // 删除角色
+    removeRole(row) {
+      this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          deleteRole(row.id).then(res => {
+            this.$message.success('删除角色成功')
+            this.getList()
+          })
+        })
+        .catch(() => {
+          this.$message.info('已取消删除')
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.avatar {
+  display: block;
+  width: 40px;
+  height: 40px;
+}
+</style>