Browse Source

your commit message

xsh 1 year ago
parent
commit
354c1118c5

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "jsencrypt": "^3.3.2",
     "jsonlint": "1.6.3",
     "jszip": "3.2.1",
+    "moment": "^2.29.4",
     "normalize.css": "7.0.0",
     "nprogress": "0.2.0",
     "path-to-regexp": "2.4.0",

+ 54 - 0
src/api/super/config/advertise.js

@@ -0,0 +1,54 @@
+import utils from '@/utils/lib'
+
+const Query = query => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superConfigAdvertiseQueryRes = data => {
+      utils.SockEventMap.superConfigAdvertiseQueryRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superConfigAdvertiseQuery', query)
+  })
+}
+
+const Update = info => {
+  return new Promise((resolve, reject) => {
+    utils.SockEventMap.superConfigAdvertiseUpdateRes = data => {
+      utils.SockEventMap.superConfigAdvertiseUpdateRes = undefined
+      if (data.status) {
+        resolve()
+      } else {
+        reject(data.msg)
+      }
+    }
+    utils.SendWss('superConfigAdvertiseUpdate', info)
+  })
+}
+
+const Delete = ids => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superConfigAdvertiseDeleteRes = data => {
+      utils.SockEventMap.superConfigAdvertiseDeleteRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superConfigAdvertiseDelete', ids)
+  })
+}
+
+const Add = info => {
+  return new Promise((resolve, reject) => {
+    utils.SockEventMap.superConfigAdvertiseAddRes = data => {
+      utils.SockEventMap.superConfigAdvertiseAddRes = undefined
+      if (data.status) {
+        resolve()
+      } else {
+        reject(data.msg)
+      }
+    }
+    utils.SendWss('superConfigAdvertiseAdd', info)
+  })
+}
+
+export default {
+  Query, Update, Delete, Add
+}
+

+ 29 - 0
src/api/super/config/param.js

@@ -0,0 +1,29 @@
+import utils from '@/utils/lib'
+
+const Query = query => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superConfigParamListRes = data => {
+      utils.SockEventMap.superConfigParamListRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superConfigParamList', query)
+  })
+}
+
+const Update = info => {
+  return new Promise((resolve, reject) => {
+    utils.SockEventMap.superConfigParamUpdateRes = data => {
+      utils.SockEventMap.superConfigParamUpdateRes = undefined
+      if (data.status) {
+        resolve()
+      } else {
+        reject(data.msg)
+      }
+    }
+    utils.SendWss('superConfigParamUpdate', info)
+  })
+}
+
+export default {
+  Query, Update
+}

+ 0 - 1
src/api/super/config/wine.js

@@ -35,7 +35,6 @@ const Delete = ids => {
 }
 
 const Add = info => {
-  console.log(info)
   return new Promise((resolve, reject) => {
     utils.SockEventMap.superConfigWineAddRes = data => {
       utils.SockEventMap.superConfigWineAddRes = undefined

+ 54 - 0
src/api/super/role/admin.js

@@ -0,0 +1,54 @@
+import utils from '@/utils/lib'
+
+const Query = query => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superRoleAdminQueryRes = data => {
+      utils.SockEventMap.superRoleAdminQueryRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superRoleAdminQuery', query)
+  })
+}
+
+const Update = info => {
+  return new Promise((resolve, reject) => {
+    utils.SockEventMap.superRoleAdminUpdateRes = data => {
+      utils.SockEventMap.superRoleAdminUpdateRes = undefined
+      if (data.status) {
+        resolve()
+      } else {
+        reject(data.msg)
+      }
+    }
+    utils.SendWss('superRoleAdminUpdate', info)
+  })
+}
+
+const Delete = ids => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superRoleAdminDeleteRes = data => {
+      utils.SockEventMap.superRoleAdminDeleteRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superRoleAdminDelete', ids)
+  })
+}
+
+const Add = info => {
+  return new Promise((resolve, reject) => {
+    utils.SockEventMap.superRoleAdminAddRes = data => {
+      utils.SockEventMap.superRoleAdminAddRes = undefined
+      if (data.status) {
+        resolve()
+      } else {
+        reject(data.msg)
+      }
+    }
+    utils.SendWss('superRoleAdminAdd', info)
+  })
+}
+
+export default {
+  Query, Update, Delete, Add
+}
+

+ 25 - 0
src/api/super/role/worker.js

@@ -0,0 +1,25 @@
+import utils from '@/utils/lib'
+
+const QueryManager = query => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superConfigDeviceQueryManagerRes = data => {
+      utils.SockEventMap.superConfigDeviceQueryManagerRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superConfigDeviceQueryManager', query)
+  })
+}
+
+const Query = query => {
+  return new Promise(resolve => {
+    utils.SockEventMap.superRoleWorkerQueryRes = data => {
+      utils.SockEventMap.superRoleWorkerQueryRes = undefined
+      resolve(data)
+    }
+    utils.SendWss('superRoleWorkerQuery', query)
+  })
+}
+
+export default {
+  Query, QueryManager
+}

+ 134 - 0
src/components/Upload/XImgUpload.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="x-upload">
+    <div class="upload-container">
+      <label
+        class="upload-icon"
+        for="upload-file"
+      >
+        <i class="el-icon-plus" />
+      </label>
+      <input
+        id="upload-file"
+        class="upload-input"
+        type="file"
+        accept="image/*"
+        @change="tirggerFile($event)"
+      >
+      <div
+        v-if="imgSrc"
+        class="upload-img"
+      >
+        <img :src="imgSrc">
+        <div class="mask">
+          <i
+            class="el-icon-delete"
+            @click="deleteImgSrc"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'XImgUpload',
+  components: {
+  },
+  data() {
+    return {
+      imgFile: [],
+      imgSrc: ''
+    }
+  },
+  methods: {
+    tirggerFile(evnet) {
+      const isLt8M = evnet.target.files[0].size / 1024 / 1024 < 8
+      if (!isLt8M) {
+        this.$message.error('图片大小不能大于8M')
+        return false
+      }
+      this.imgFile = evnet.target.files
+      // 创建reader对象
+      const reader = new FileReader()
+      // 传入文件 (参数必须是blob对象)
+      reader.readAsDataURL(this.imgFile[0])
+      // 加载完成
+      reader.onloadend = (event) => {
+        // url = reader
+        this.imgSrc = reader.result
+      }
+      this.$emit('onImgChange', this.imgFile)
+    },
+    deleteImgSrc() {
+      (this.imgFile = []), (this.imgSrc = '')
+      this.$emit('onImgChange', this.imgFile)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.x-upload {
+}
+.upload-container {
+  width: 100px;
+  height: 100px;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed;
+  border-radius: 5px;
+  color: #999;
+  cursor: pointer;
+  z-index: 1;
+}
+.upload-container:hover, .upload-container:active {
+  color: #3576ff;
+}
+.upload-icon {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+}
+
+.upload-input {
+  display: none !important;
+}
+
+.upload-img {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+.upload-img img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  opacity: 0;
+  transition: opacity 0.3s ease-out;
+  cursor: pointer;
+}
+.mask:hover {
+  opacity: 1;
+}
+
+</style>

+ 144 - 0
src/components/Upload/XVideoUpload.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="x-upload">
+    <div class="upload-container">
+      <label
+        class="upload-icon"
+        for="upload-file"
+      >
+        <i class="el-icon-plus" />
+      </label>
+      <input
+        id="upload-file"
+        class="upload-input"
+        type="file"
+        accept="video/*"
+        @change="tirggerFile($event)"
+      >
+      <div
+        v-if="imgSrc"
+        class="upload-img"
+      >
+        <video
+          :src="imgSrc"
+          controls
+          :poster="imgSrc"
+        />
+        <div class="mask">
+          <i
+            class="el-icon-delete"
+            @click="deleteImgSrc"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'XVideoUpload',
+  components: {
+  },
+  data() {
+    return {
+      imgFile: [],
+      imgSrc: ''
+    }
+  },
+  methods: {
+    tirggerFile(evnet) {
+      this.imgFile = evnet.target.files
+      // 创建reader对象
+
+      this.imgSrc = this.getObjectURL(this.imgFile[0])
+      this.$emit('onImgChange', this.imgFile)
+    },
+    deleteImgSrc() {
+      (this.imgFile = []), (this.imgSrc = '')
+      this.$emit('onImgChange', this.imgFile)
+    },
+    getObjectURL(file) {
+      var url = null
+      // 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
+      if (window.createObjectURL !== undefined) {
+        // basic
+        url = window.createObjectURL(file)
+      } else if (window.URL !== undefined) {
+        // mozilla(firefox)
+        url = window.URL.createObjectURL(file)
+      } else if (window.webkitURL !== undefined) {
+        // webkit or chrome
+        url = window.webkitURL.createObjectURL(file)
+      }
+      return url
+    }
+
+  }
+}
+</script>
+
+<style scoped>
+.x-upload {
+}
+.upload-container {
+  width: 200px;
+  height: 200px;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed;
+  border-radius: 5px;
+  color: #999;
+  cursor: pointer;
+  z-index: 1;
+}
+.upload-container:hover, .upload-container:active {
+  color: #3576ff;
+}
+.upload-icon {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+}
+
+.upload-input {
+  display: none !important;
+}
+
+.upload-img {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+.upload-img video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.mask {
+  width: 50px;
+  height: 50px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  opacity: 0;
+  transition: opacity 0.3s ease-out;
+  cursor: pointer;
+}
+.mask:hover {
+  opacity: 1;
+}
+
+</style>

+ 2 - 0
src/main.js

@@ -18,6 +18,7 @@ import './permission' // permission control
 import './utils/error-log' // error log
 
 import * as filters from './filters' // global filters
+import moment from 'moment'
 
 /**
  * If you don't want to use mock-server
@@ -40,6 +41,7 @@ Object.keys(filters).forEach(key => {
 })
 
 Vue.config.productionTip = false
+Vue.prototype.$moment = moment
 
 new Vue({
   el: '#app',

+ 1 - 1
src/router/super.js

@@ -100,7 +100,7 @@ const superRoutes = [
         }
       }, {
         path: 'param',
-        component: () => import('@/views/super/config/advertise/index'),
+        component: () => import('@/views/super/config/param/index'),
         name: '系统参数',
         meta: {
           title: '系统参数',

+ 38 - 0
src/utils/index.js

@@ -333,3 +333,41 @@ export function removeClass(ele, cls) {
     ele.className = ele.className.replace(reg, ' ')
   }
 }
+
+/**
+ * 图片流转base64
+ *
+ * */
+export function uploadImgToBase64(file) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()// html5提供的一种异步文件读取机制
+    reader.readAsDataURL(file)// 将文件读取为Base64编码的数据URL
+    reader.onload = function() { // 图片转base64完成后返回reader对象
+      resolve(reader)
+    }
+    reader.onerror = reject
+  })
+}
+
+// 视频转 base64
+export function videoToBase64(file) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.readAsDataURL(file)
+    reader.onload = () => {
+      const base64 = reader.result
+      resolve(base64)
+    }
+    reader.onerror = error => reject(error)
+  })
+}
+
+// 数字变成时分秒
+export function convertSeconds(val) {
+  // 转换为小时、分钟和秒
+  var hours = Math.floor(val / 3600)
+  var minutes = Math.floor((val % 3600) / 60)
+  var seconds = val % 60
+  var result = hours + '小时 ' + minutes + '分钟 ' + seconds + '秒'
+  return result
+}

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

@@ -9,7 +9,9 @@
       label-position="left"
     >
       <div class="title-container">
-        <h3 class="title">管 理 后 台</h3>
+        <h3 class="title">
+          管 理 后 台
+        </h3>
       </div>
 
       <el-form-item prop="account">
@@ -27,7 +29,12 @@
         />
       </el-form-item>
 
-      <el-tooltip v-model="capsTooltip" content="大写锁已打开" placement="right" manual>
+      <el-tooltip
+        v-model="capsTooltip"
+        content="大写锁已打开"
+        placement="right"
+        manual
+      >
         <el-form-item prop="password">
           <span class="svg-container">
             <svg-icon icon-class="password" />
@@ -45,7 +52,10 @@
             @blur="capsTooltip = false"
             @keyup.enter.native="handleLogin"
           />
-          <span class="show-pwd" @click="showPwd">
+          <span
+            class="show-pwd"
+            @click="showPwd"
+          >
             <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
           </span>
         </el-form-item>
@@ -116,6 +126,9 @@ export default {
       this.$refs.password.focus()
     }
   },
+  created() {
+    this.getList()
+  },
   methods: {
     checkCapslock(e) {
       const { key } = e

+ 695 - 2
src/views/super/config/advertise/index.vue

@@ -1,13 +1,706 @@
 <template>
-  <div>advertise config index</div>
+  <div class="app-container">
+    <div class="filter-container">
+      <el-input
+        v-model="advertise.query.desc"
+        placeholder="请输入关键字"
+        style="width: 300px;"
+        class="filter-item"
+      />
+      <el-select
+        v-model="advertise.query.type"
+        class="filter-item"
+      >
+        <el-option
+          label="全部"
+          :value="-1"
+        />
+        <el-option
+          label="视频"
+          :value="0"
+        />
+        <el-option
+          label="图片"
+          :value="1"
+        />
+      </el-select>
+      <el-button
+        class="filter-item"
+        style="margin-left: 10px;"
+        type="primary"
+        icon="el-icon-search"
+        @click="handleQuery"
+      >
+        查询
+      </el-button>
+      <el-button
+        class="filter-item"
+        @click="reset"
+      >
+        重置
+      </el-button>
+      <el-button
+        plain
+        class="filter-item"
+        style="margin-left: 10px;"
+        type="success"
+        icon="el-icon-plus"
+        @click="handleShowCreate"
+      >
+        添加
+      </el-button>
+      <el-button
+        plain
+        class="filter-item"
+        style="margin-left: 10px;"
+        icon="el-icon-delete"
+        type="danger"
+        @click="handleBatchDelete"
+      >
+        删除
+      </el-button>
+    </div>
+    <el-table
+      :key="tableKey"
+      v-loading="advertise.loading"
+      :data="advertise.list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;"
+      @sort-change="sortChange"
+      @selection-change="selectionChange"
+    >
+      <el-table-column
+        type="selection"
+        width="80"
+        fixed
+        align="center"
+        :selectable="selectableList"
+      />
+      <el-table-column
+        type="index"
+        label="序号"
+        width="80"
+        align="center"
+      />
+      <el-table-column
+        label="ID"
+        prop="id"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="广告类型"
+        align="center"
+        width="200"
+      >
+        <template slot-scope="{ row }">
+          <el-tag
+            v-if="row.type"
+            type="primary"
+          >
+            图片
+          </el-tag>
+          <el-tag
+            v-else
+            type="success"
+          >
+            视频
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="播放顺序"
+        prop="order"
+        align="center"
+        width="100"
+        sortable="custom"
+        :class-name="advertise.sort.order"
+      />
+      <el-table-column
+        label="资源下载地址"
+        prop="src"
+        align="center"
+        width="400"
+      />
+      <el-table-column
+        label="播放时长(s)"
+        align="center"
+        width="200"
+        sortable="custom"
+        :class-name="advertise.sort.duration"
+      >
+        <template slot-scope="{ row }">
+          <span>{{ row.duration / 1000 }}s</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="备注"
+        prop="describe"
+        align="center"
+        width="180"
+      />
+      <el-table-column
+        label="操作"
+        fixed="right"
+        align="center"
+        width="240"
+        class-name="small-padding fixed-width"
+      >
+        <template slot-scope="{row}">
+          <el-button
+            plain
+            type="primary"
+            size="mini"
+            icon="el-icon-edit"
+            @click="handleShowDetail(row)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-if="canDelete(row.id)"
+            plain
+            type="danger"
+            size="mini"
+            icon="el-icon-delete"
+            @click="handleDeleteOne(row.id)"
+          >
+            删除
+          </el-button>
+          <el-button
+            type="text"
+            @click="open(row)"
+          >
+            查看
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      hide-on-single-page
+      :total="advertise.total"
+      :page.sync="advertise.query.page"
+      :limit.sync="advertise.query.limit"
+      @pagination="getList"
+    />
+    <el-dialog
+      width="30%"
+      :visible.sync="detailVisible"
+      :title="detail.title"
+      :close-on-click-modal="false"
+      center
+      @close="resetDetail"
+    >
+      <el-form
+        ref="myForm"
+        label-width="120px"
+        :rules="rules"
+        :model="detail"
+      >
+        <el-form-item
+          label="播放顺序"
+          prop="order"
+        >
+          <el-input
+            v-model="detail.order"
+            type="number"
+            placeholder="请输入播放顺序"
+          />
+        </el-form-item>
+        <el-form-item
+          label="广告类型"
+          prop="type"
+        >
+          <el-select
+            v-model="detail.type"
+            placeholder="请选择广告类型"
+          >
+            <el-option
+              label="图片"
+              :value="true"
+            />
+            <el-option
+              label="视频"
+              :value="false"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="detail.type === true"
+          label="图片"
+          prop="base64"
+        >
+          <template v-if="detail.title === '新增广告'">
+            <XImgUpload @onImgChange="onImgChange" />
+          </template>
+          <template v-else>
+            <div
+              v-if="detailImgShow"
+              class="upload-img"
+            >
+              <img
+                :src="detail.picture"
+                alt=""
+              >
+              <div class="mask">
+                <i
+                  class="el-icon-delete"
+                  @click="detailImgShow = false"
+                />
+              </div>
+            </div>
+            <XImgUpload
+              v-else
+              @onImgChange="onImgChange"
+            />
+          </template>
+        </el-form-item>
+        <el-form-item
+          v-if="detail.type === false"
+          label="视频"
+          prop="base64"
+        >
+          <template v-if="detail.title === '新增广告'">
+            <XVideoUpload @onImgChange="onVideoChange" />
+          </template>
+          <template v-else>
+            <div
+              v-if="videoShow"
+              class="video_box"
+            >
+              <video
+                :src="detail.src"
+                controls
+              />
+              <div class="mask_video">
+                <i
+                  class="el-icon-delete"
+                  @click="videoShow = false"
+                />
+              </div>
+            </div>
+            <XVideoUpload
+              v-else
+              @onImgChange="onVideoChange"
+            />
+          </template>
+        </el-form-item>
+        <el-form-item
+          label="持续时间(s)"
+          prop="duration"
+        >
+          <el-input
+            v-model="detail.duration"
+            type="number"
+            placeholder="请输入持续时间"
+          >
+            <template #append>
+              <span>s</span>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input
+            v-model="detail.describe"
+            placeholder="请输入备注"
+          />
+        </el-form-item>
+      </el-form>
+      <span
+        slot="footer"
+        class="dialog-footer"
+      >
+        <el-button @click="resetDetail">取 消</el-button>
+        <el-button
+          type="primary"
+          @click="onSubmit"
+        >确 定</el-button>
+      </span>
+    </el-dialog>
+    <el-dialog
+      :visible.sync="showModal"
+      width="30%"
+      :close-on-click-modal="false"
+      :title="showType ? '查看图片' : '查看视频'"
+    >
+      <div
+        v-if="showType"
+        class="box_img"
+      >
+        <img
+          :src="showUrl"
+          alt=""
+        >
+      </div>
+      <div
+        v-else
+        class="box_video"
+      >
+        <video
+          :src="showUrl"
+          controls
+        />
+      </div>
+    </el-dialog>
+  </div>
 </template>
 
 <script>
+import api from '@/api/super/config/advertise'
+import Pagination from '@/components/Pagination'
+import XImgUpload from '@/components/Upload/XImgUpload'
+import XVideoUpload from '@/components/Upload/XVideoUpload'
+import { uploadImgToBase64, videoToBase64 } from '@/utils'
+const InitWineId = 10100
 export default {
-  name: 'AdvertiseConfig'
+  name: 'AdvertiseConfig',
+  components: { Pagination, XImgUpload, XVideoUpload },
+  data() {
+    var checkAge = (rule, value, callback) => {
+      value = Number(value)
+      if (!value) {
+        return callback(new Error('不能为空'))
+      }
+      setTimeout(() => {
+        if (value <= 0) {
+          callback(new Error('不能小于等于0'))
+        } else if (value > 65535) {
+          callback(new Error('不能大于65535'))
+        } else {
+          callback()
+        }
+      })
+    }
+    return {
+      tableKey: 0,
+      advertise: {
+        list: [],
+        total: 0,
+        loading: true,
+        sort: {
+          duration: 'ascending',
+          order: 'ascending',
+          degree: 'ascending',
+          density: 'ascending'
+        },
+        query: {
+          page: 1,
+          limit: 10,
+          desc: '',
+          type: -1
+        }
+      },
+      batch: [],
+      detailVisible: false,
+      detail: {
+        title: '',
+        id: 0,
+        order: '',
+        time: new Date(),
+        base64: '',
+        type: '',
+        duration: '',
+        describe: ''
+      },
+      rules: {
+        order: [{ required: true, message: '请填写播放顺序', trigger: 'blur' }],
+        type: [{ required: true, message: '请选择广告类型', trigger: 'change' }],
+        base64: [{ required: true, message: '请上传', trigger: 'blur' }],
+        duration: [{ required: true, message: '请填写播放时间', trigger: 'blur' }, { validator: checkAge, trigger: 'blur' }]
+      },
+      detailImgShow: true,
+      videoShow: true,
+      showModal: false,
+      showType: false,
+      showUrl: ''
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    reset() {
+      this.advertise.query = {
+        page: 1,
+        limit: 10,
+        desc: '',
+        type: -1
+      }
+    },
+    getList() {
+      this.advertise.loading = true
+      api.Query(this.advertise.query).then(res => {
+        for (let i = 0; i < res.ads.length; ++i) {
+          res.ads[i].time = new Date(res.ads[i].time)
+        }
+        this.advertise.list = res.ads
+        this.advertise.total = res.total
+        this.advertise.loading = false
+      })
+    },
+    sortChange(data) {
+      const { prop, order } = data
+      this.advertise.sort[prop] = order
+      const cmp = order === 'ascending' ? (a, b) => a[prop] - b[prop] : (a, b) => b[prop] - a[prop]
+      this.advertise.list.sort(cmp)
+    },
+    selectionChange(val) {
+      this.batch = val.map(e => e.id)
+    },
+    handleQuery() {
+      this.advertise.query.page = 1
+      this.getList()
+    },
+    selectableList(row) {
+      return row.id !== InitWineId
+    },
+    canDelete(id) {
+      return id !== InitWineId
+    },
+    handleDeleteOne(id) {
+      this.$confirm('此操作将删除该酒品, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        api.Delete([id]).then(() => {
+          this.$notify({
+            title: '成功',
+            message: '删除酒品信息成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.getList()
+        }).catch(msg => {
+          this.$notify({
+            title: '失败',
+            message: msg,
+            type: 'error',
+            duration: 0
+          })
+        })
+      })
+    },
+    handleBatchDelete() {
+      if (this.batch.length === 0) return this.$message.info('请勾选需要删除的酒品')
+      this.$confirm('此操作将删除选中的酒品, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        api.Delete(this.batch).then(() => {
+          this.$notify({
+            title: '成功',
+            message: '删除酒品信息成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.getList()
+        }).catch(msg => {
+          this.$notify({
+            title: '失败',
+            message: msg,
+            type: 'error',
+            duration: 0
+          })
+        })
+      })
+    },
+    handleShowCreate() {
+      this.resetDetail()
+      this.detail.title = '新增广告'
+      this.detailVisible = true
+    },
+    handleShowDetail(row) {
+      this.detail = Object.assign({}, row)
+      this.detail.title = '编辑广告'
+      this.detailVisible = true
+    },
+    resetDetail() {
+      var _file = document.getElementById('upload-file')
+      _file.value = ''
+      this.detailVisible = false
+      this.detailImgShow = true
+      this.videoShow = true
+      this.detail = {
+        title: '',
+        id: 0,
+        order: '',
+        time: new Date(),
+        base64: '',
+        type: '',
+        duration: '',
+        describe: ''
+      }
+    },
+    // 拿到图片的文件流
+    onImgChange(imgSrc) {
+      uploadImgToBase64(imgSrc[0]).then(res => {
+        if (res.result) {
+          this.detail.base64 = res.result
+        }
+      })
+    },
+    onVideoChange(imgSrc) {
+      videoToBase64(imgSrc[0]).then(res => {
+        console.log(res)
+        if (res) {
+          this.detail.base64 = res
+        }
+      })
+    },
+    onSubmit() {
+      this.$refs['myForm'].validate((valid) => {
+        if (valid) {
+          if (this.detail.title === '新增广告') {
+            const params = {
+              order: Number(this.detail.order),
+              base64: this.detail.base64,
+              type: this.detail.type,
+              duration: Number(this.detail.duration) * 1000,
+              describe: this.detail.describe
+            }
+            api.Add(params).then(() => {
+              this.$notify({
+                title: '成功',
+                message: '新增广告成功!',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch(msg => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          } else {
+            const params = {
+              id: this.detail.id,
+              order: Number(this.detail.order),
+              base64: this.detail.base64,
+              type: this.detail.type,
+              duration: Number(this.detail.duration) * 1000,
+              describe: this.detail.describe
+            }
+            api.Update(params).then(() => {
+              this.$notify({
+                title: '成功',
+                message: '编辑广告成功!',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch(msg => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          }
+        }
+      })
+    },
+    open(row) {
+      this.showModal = true
+      this.showType = row.type
+      this.showUrl = row.src
+    }
+  }
 }
 </script>
 
 <style scoped>
+.upload-img {
+  width: 100px;
+  height: 100px;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed;
+  border-radius: 5px;
+  color: #999;
+  cursor: pointer;
+  z-index: 1;
+}
+.upload-img img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  opacity: 0;
+  transition: opacity 0.3s ease-out;
+  cursor: pointer;
+}
+.mask:hover {
+  opacity: 1;
+}
+.mask_video {
+  width: 50px;
+  height: 50px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  opacity: 0;
+  transition: opacity 0.3s ease-out;
+  cursor: pointer;
+}
+.mask_video:hover {
+  opacity: 1;
+}
+.video_box {
+  width: 200px;
+  height: 200px;
+  position: relative;
+}
+.video_box video {
+  width: 100%;
+  height: 100%;
+}
+
+.box_img {
+  width: 400px;
+  height: 400px;
+}
+.box_img img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.box_video {
+  width: 400px;
+  height: 400px;
+}
 
+.box_video video {
+  width: 100%;
+  height: 100%;
+}
 </style>

+ 95 - 3
src/views/super/config/param/index.vue

@@ -1,13 +1,105 @@
 <template>
-  <div>param config index</div>
+  <div class="box">
+    <el-form
+      ref="myForm"
+      :model="detail"
+      label-width="200px"
+    >
+      <el-row :gutter="20">
+        <el-col
+          v-for="(item, i) in detail.list"
+          :key="i"
+          :span="12"
+        >
+          <el-form-item
+            :label="item.name"
+            :prop="'list.'+i+'.value'"
+            :rules="[{ required: true, message: item.name+'不能为空', trigger: 'blur' }, {validator: checkAge, trigger: 'blur'}]"
+          >
+            <el-input
+              v-model.number="item.value"
+              type="number"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <br>
+    <el-button
+      style="width: 300px;"
+      type="primary"
+      @click="onSubmit"
+    >
+      保存
+    </el-button>
+  </div>
 </template>
 
 <script>
+import api from '@/api/super/config/param'
 export default {
-  name: 'ParamConfig'
+  name: 'ParamConfig',
+  data() {
+    return {
+      detail: {
+        list: []
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    checkAge(rule, value, callback) {
+      value = Number(value)
+      if (!value) {
+        return callback(new Error('不能为空'))
+      }
+      setTimeout(() => {
+        if (value < 0) {
+          callback(new Error('不能小于0'))
+        } else if (value > 65535) {
+          callback(new Error('不能大于65535'))
+        } else {
+          callback()
+        }
+      })
+    },
+    getList() {
+      api.Query().then(res => {
+        this.detail.list = res.data
+      })
+    },
+    onSubmit() {
+      this.$refs['myForm'].validate((valid) => {
+        if (valid) {
+          api.Update(this.detail.list).then(() => {
+            this.$notify({
+              title: '成功',
+              message: '修改成功',
+              type: 'success',
+              duration: 2000
+            })
+            this.getList()
+          }).catch((msg) => {
+            this.$notify({
+              title: '失败',
+              message: msg,
+              type: 'error',
+              duration: 0
+            })
+          })
+        }
+      })
+    }
+  }
 }
 </script>
 
 <style scoped>
-
+  .box {
+    box-sizing: border-box;
+    padding: 20px;
+    text-align: center;
+  }
 </style>

+ 298 - 17
src/views/super/config/wine/index.vue

@@ -49,8 +49,18 @@
       @sort-change="sortChange"
       @selection-change="selectionChange"
     >
-      <el-table-column type="selection" width="80" align="center" :selectable="selectableList" />
-      <el-table-column type="index" label="序号" width="80" align="center" />
+      <el-table-column
+        type="selection"
+        width="80"
+        align="center"
+        :selectable="selectableList"
+      />
+      <el-table-column
+        type="index"
+        label="序号"
+        width="80"
+        align="center"
+      />
       <el-table-column
         label="酒名"
         prop="name"
@@ -99,12 +109,28 @@
         align="center"
       >
         <template #default="scope">
-          <img style="width: 50px" :src="scope.row.picture" alt="">
+          <img
+            style="width: 50px"
+            :src="scope.row.picture"
+            alt=""
+          >
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" width="240" class-name="small-padding fixed-width">
+      <el-table-column
+        label="操作"
+        fixed="right"
+        align="center"
+        width="240"
+        class-name="small-padding fixed-width"
+      >
         <template slot-scope="{row}">
-          <el-button plain type="primary" size="mini" icon="el-icon-edit" @click="handleShowDetail(row)">
+          <el-button
+            plain
+            type="primary"
+            size="mini"
+            icon="el-icon-edit"
+            @click="handleShowDetail(row)"
+          >
             编辑
           </el-button>
           <el-button
@@ -130,24 +156,151 @@
     />
 
     <el-dialog
+      width="30%"
       :visible.sync="detailVisible"
       :title="detail.title"
       :close-on-click-modal="false"
       center
-    />
+      @close="resetDetail"
+    >
+      <el-form
+        ref="myForm"
+        label-width="120px"
+        :rules="rules"
+        :model="detail"
+      >
+        <el-form-item
+          label="酒名"
+          prop="name"
+        >
+          <el-input
+            v-model="detail.name"
+            placeholder="请输入酒名"
+          />
+        </el-form-item>
+        <el-form-item
+          label="价格"
+          prop="price"
+        >
+          <el-input
+            v-model="detail.price"
+            type="number"
+            placeholder="请输入价格"
+          >
+            <template slot="append">
+              分/两
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item
+          label="度数"
+          prop="degree"
+        >
+          <el-input
+            v-model="detail.degree"
+            type="number"
+            placeholder="请输入度数"
+          >
+            <template slot="append">
+              度(x100)
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item
+          label="密度"
+          prop="density"
+        >
+          <el-input
+            v-model="detail.density"
+            type="number"
+            placeholder="请输入密度"
+          >
+            <template slot="append">
+              g(x1000)/ml
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item
+          v-if="detail.title === '新增酒品'"
+          label="图片"
+          prop="image"
+        >
+          <XImgUpload @onImgChange="onImgChange" />
+        </el-form-item>
+        <el-form-item
+          v-if="detail.title === '酒品详情'"
+          label="图片"
+          prop="image"
+        >
+          <div
+            v-if="detailImgShow"
+            class="upload-img"
+          >
+            <img
+              :src="detail.picture"
+              alt=""
+            >
+            <div class="mask">
+              <i
+                class="el-icon-delete"
+                @click="detailImgShow = false"
+              />
+            </div>
+          </div>
+          <XImgUpload
+            v-else
+            @onImgChange="onImgChange"
+          />
+        </el-form-item>
+        <el-form-item label="简述">
+          <el-input
+            v-model="detail.describe"
+            type="textarea"
+            placeholder="请输入简述"
+            autosize
+          />
+        </el-form-item>
+      </el-form>
+      <span
+        slot="footer"
+        class="dialog-footer"
+      >
+        <el-button @click="resetDetail">取 消</el-button>
+        <el-button
+          type="primary"
+          @click="onSubmit"
+        >确 定</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import api from '@/api/super/config/wine'
-import { parseTime } from '@/utils'
+import { parseTime, uploadImgToBase64 } from '@/utils'
 import Pagination from '@/components/Pagination'
+import XImgUpload from '@/components/Upload/XImgUpload'
 
 const InitWineId = 10100
 export default {
   name: 'WineConfig',
-  components: { Pagination },
+  components: { Pagination, XImgUpload },
   data() {
+    var checkAge = (rule, value, callback) => {
+      value = Number(value)
+      if (!value) {
+        return callback(new Error('不能为空'))
+      }
+      setTimeout(() => {
+        if (value <= 0) {
+          callback(new Error('不能小于等于0'))
+        } else if (value > 65535) {
+          callback(new Error('不能大于65535'))
+        } else {
+          callback()
+        }
+      })
+    }
     return {
       tableKey: 0,
       wine: {
@@ -173,12 +326,32 @@ export default {
         id: 0,
         name: '',
         time: new Date(),
-        price: 0,
-        degree: 0,
-        density: 0,
+        price: '',
+        degree: '',
+        density: '',
+        image: '',
         picture: '',
         describe: ''
-      }
+      },
+      rules: {
+        name: [
+          { required: true, message: '请输入酒名', trigger: 'blur' },
+          { max: 16, message: '长度在16个字符', trigger: 'blur' }
+        ],
+        price: [
+          { validator: checkAge, trigger: 'blur' }
+        ],
+        degree: [
+          { validator: checkAge, trigger: 'blur' }
+        ],
+        density: [
+          { validator: checkAge, trigger: 'blur' }
+        ]
+        // image: [
+        //   { required: true, message: '请上传图片', trigger: 'blur' }
+        // ]
+      },
+      detailImgShow: true
     }
   },
   created() {
@@ -276,22 +449,130 @@ export default {
       this.batch = val.map(e => e.id)
     },
     resetDetail() {
+      var _file = document.getElementById('upload-file')
+      _file.value = ''
+      this.detailVisible = false
+      this.detailImgShow = true
       this.detail = {
         title: '',
         id: 0,
         name: '',
         time: new Date(),
-        price: 0,
-        degree: 0,
-        density: 0,
-        picture: '',
-        describe: ''
+        price: '',
+        degree: '',
+        density: '',
+        describe: '',
+        image: '',
+        picture: ''
       }
+    },
+    // 拿到图片的文件流
+    onImgChange(imgSrc) {
+      uploadImgToBase64(imgSrc[0]).then(res => {
+        if (res.result) {
+          this.detail.image = res.result
+        }
+      })
+    },
+    onSubmit() {
+      this.$refs['myForm'].validate((valid) => {
+        if (valid) {
+          console.log(this.detail)
+          if (this.detail.title === '新增酒品') {
+            const params = {
+              name: this.detail.name,
+              price: Number(this.detail.price),
+              degree: Number(this.detail.degree),
+              density: Number(this.detail.density),
+              image: this.detail.image,
+              describe: this.detail.describe
+            }
+            api.Add(params).then(() => {
+              this.$notify({
+                title: '成功',
+                message: '新增酒品成功',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch((msg) => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          } else {
+            const params = {
+              id: this.detail.id,
+              name: this.detail.name,
+              price: Number(this.detail.price),
+              degree: Number(this.detail.degree),
+              density: Number(this.detail.density),
+              image: this.detail.image,
+              describe: this.detail.describe
+            }
+            api.Update(params).then(() => {
+              this.$notify({
+                title: '成功',
+                message: '编辑酒品成功',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch((msg) => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          }
+        }
+      })
     }
   }
 }
 </script>
 
 <style scoped>
+.upload-img {
+  width: 100px;
+  height: 100px;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed;
+  border-radius: 5px;
+  color: #999;
+  cursor: pointer;
+  z-index: 1;
+}
+.upload-img img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
 
+.mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  opacity: 0;
+  transition: opacity 0.3s ease-out;
+  cursor: pointer;
+}
+.mask:hover {
+  opacity: 1;
+}
 </style>

+ 449 - 3
src/views/super/role/admin/index.vue

@@ -1,13 +1,459 @@
 <template>
-  <div>admin manage index</div>
+  <div class="box">
+    <el-card>
+      <div
+        slot="header"
+        class="clearfix"
+      >
+        <span>指标一览</span>
+      </div>
+      <div class="grid">
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+      </div>
+    </el-card>
+    <br>
+    <div class="app-container">
+      <div class="filter-container">
+        <el-input
+          v-model="admin.query.cond"
+          placeholder="请输入关键字"
+          style="width: 300px;"
+          class="filter-item"
+        />
+        <el-button
+          class="filter-item"
+          style="margin-left: 10px;"
+          type="primary"
+          icon="el-icon-search"
+          @click="handleQuery"
+        >
+          查询
+        </el-button>
+        <el-button
+          plain
+          class="filter-item"
+          style="margin-left: 10px;"
+          type="success"
+          icon="el-icon-plus"
+          @click="handleShowCreate"
+        >
+          添加
+        </el-button>
+        <el-button
+          plain
+          class="filter-item"
+          style="margin-left: 10px;"
+          icon="el-icon-delete"
+          type="danger"
+          @click="handleBatchDelete"
+        >
+          删除
+        </el-button>
+      </div>
+    </div>
+    <el-table
+      :key="tableKey"
+      v-loading="admin.loading"
+      :data="admin.list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;"
+      @sort-change="sortChange"
+      @selection-change="selectionChange"
+    >
+      <el-table-column
+        type="selection"
+        width="80"
+        align="center"
+        :selectable="selectableList"
+      />
+      <el-table-column
+        type="index"
+        label="序号"
+        width="80"
+        align="center"
+      />
+      <el-table-column
+        label="姓名"
+        prop="name"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="账号"
+        prop="account"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="账号"
+        prop="account"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="订单总数"
+        prop="order"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="收入金额"
+        align="center"
+        width="250"
+      >
+        <template slot-scope="{row}">
+          <span>{{ row.income / 100 }}元</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="注册时间"
+        prop="first"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="最近活跃"
+        prop="last"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="操作"
+        fixed="right"
+        align="center"
+        width="240"
+        class-name="small-padding fixed-width"
+      >
+        <template slot-scope="{row}">
+          <el-button
+            plain
+            type="primary"
+            size="mini"
+            icon="el-icon-edit"
+            @click="handleShowDetail(row)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-if="canDelete(row.id)"
+            plain
+            type="danger"
+            size="mini"
+            icon="el-icon-delete"
+            @click="handleDeleteOne(row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      hide-on-single-page
+      :total="admin.total"
+      :page.sync="admin.query.page"
+      :limit.sync="admin.query.limit"
+      @pagination="getList"
+    />
+    <el-dialog
+      width="30%"
+      :visible.sync="detailVisible"
+      :title="detail.title"
+      :close-on-click-modal="false"
+      center
+      @close="resetDetail"
+    >
+      <el-form
+        ref="myForm"
+        label-width="120px"
+        :rules="rules"
+        :model="detail"
+      >
+        <el-form-item
+          label="姓名"
+          prop="name"
+        >
+          <el-input
+            v-model="detail.name"
+            placeholder="请输入姓名"
+          />
+        </el-form-item>
+        <el-form-item
+          label="账号"
+          prop="account"
+        >
+          <el-input
+            v-model="detail.account"
+            placeholder="请输入账号"
+          />
+        </el-form-item>
+        <el-form-item label="密码">
+          <el-input
+            v-model="detail.password"
+            type="password"
+            placeholder="请输入度数"
+          />
+        </el-form-item>
+      </el-form>
+      <span
+        slot="footer"
+        class="dialog-footer"
+      >
+        <el-button @click="resetDetail">取 消</el-button>
+        <el-button
+          type="primary"
+          @click="onSubmit"
+        >确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
 </template>
 
 <script>
+import api from '@/api/super/role/admin'
+import Pagination from '@/components/Pagination'
+import utils from '@/utils/lib'
+const InitWineId = 10100
 export default {
-  name: 'AdminManage'
+  name: 'AdminManage',
+  components: { Pagination },
+  data() {
+    return {
+      tableKey: 0,
+      admin: {
+        list: [],
+        total: 0,
+        loading: true,
+        sort: {
+          time: 'ascending',
+          price: 'ascending',
+          degree: 'ascending',
+          density: 'ascending'
+        },
+        query: {
+          page: 1,
+          limit: 10,
+          cond: ''
+        }
+      },
+      batch: [],
+      detailVisible: false,
+      detail: {
+        title: '',
+        id: 0,
+        name: '',
+        account: '',
+        password: ''
+      },
+      password: '',
+      rules: {
+        name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+        account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    selectableList(row) {
+      return row.id !== InitWineId
+    },
+    canDelete(id) {
+      return id !== InitWineId
+    },
+    handleDeleteOne(id) {
+      this.$confirm('此操作将删除该酒品, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        api.Delete([id]).then(() => {
+          this.$notify({
+            title: '成功',
+            message: '删除酒品信息成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.getList()
+        }).catch(msg => {
+          this.$notify({
+            title: '失败',
+            message: msg,
+            type: 'error',
+            duration: 0
+          })
+        })
+      })
+    },
+    handleBatchDelete() {
+      if (this.batch.length === 0) return this.$message.info('请勾选需要删除的酒品')
+      this.$confirm('此操作将删除选中的酒品, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        api.Delete(this.batch).then(() => {
+          this.$notify({
+            title: '成功',
+            message: '删除酒品信息成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.getList()
+        }).catch(msg => {
+          this.$notify({
+            title: '失败',
+            message: msg,
+            type: 'error',
+            duration: 0
+          })
+        })
+      })
+    },
+    getList() {
+      this.admin.loading = true
+      api.Query(this.admin.query).then(res => {
+        console.log(res)
+        for (let i = 0; i < res.data.list.length; ++i) {
+          res.data.list[i].time = new Date(res.data.list[i].time)
+          res.data.list[i].first = this.$moment(new Date(res.data.list[i].first)).format('YYYY-MM-DD HH:mm:ss')
+          res.data.list[i].last = this.$moment(new Date(res.data.list[i].last)).format('YYYY-MM-DD HH:mm:ss')
+        }
+        this.admin.list = res.data.list
+        this.admin.total = res.data.total
+        this.admin.loading = false
+      }).catch(msg => {
+        this.admin.loading = false
+        this.$notify({
+          title: '失败',
+          message: msg,
+          type: 'error',
+          duration: 0
+        })
+      })
+    },
+    handleQuery() {
+      this.admin.query.page = 1
+      this.getList()
+    },
+    sortChange(data) {
+      const { prop, order } = data
+      this.admin.sort[prop] = order
+      const cmp = order === 'ascending' ? (a, b) => a[prop] - b[prop] : (a, b) => b[prop] - a[prop]
+      this.admin.list.sort(cmp)
+    },
+    selectionChange(val) {
+      this.batch = val.map(e => e.id)
+    },
+    handleShowCreate() {
+      this.detailVisible = true
+      this.detail.title = '新增管理员'
+    },
+    handleShowDetail(row) {
+      this.detailVisible = true
+      this.detail = {
+        title: '编辑管理员',
+        id: row.id,
+        name: row.name,
+        account: row.account,
+        password: ''
+      }
+    },
+    resetDetail() {
+      this.detailVisible = false
+      this.password = ''
+      this.detail = {
+        title: '',
+        id: 0,
+        name: '',
+        account: '',
+        password: ''
+      }
+    },
+    onSubmit() {
+      this.$refs['myForm'].validate((valid) => {
+        if (valid) {
+          if (this.detail.title === '新增管理员') {
+            const params = {
+              name: this.detail.name,
+              account: this.detail.account,
+              password: utils.EncryptHandler.encrypt(this.detail.password)
+            }
+            api.Add(params).then(res => {
+              this.$notify({
+                title: '成功',
+                message: '新增管理员成功',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch((msg) => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          } else {
+            const params = {
+              id: this.detail.id,
+              name: this.detail.name,
+              account: this.detail.account,
+              password: this.detail.password ? utils.EncryptHandler.encrypt(this.detail.password) : ''
+            }
+            api.Update(params).then(() => {
+              this.$notify({
+                title: '成功',
+                message: '编辑管理员成功',
+                type: 'success',
+                duration: 2000
+              })
+              this.resetDetail()
+              this.getList()
+            }).catch((msg) => {
+              this.$notify({
+                title: '失败',
+                message: msg,
+                type: 'error',
+                duration: 0
+              })
+            })
+          }
+        }
+      })
+    }
+  }
 }
 </script>
 
 <style scoped>
-
+  .box {
+    box-sizing: border-box;
+    padding: 20px;
+  }
+  .grid {
+    width: 100%;
+    height: 150px;
+    display: flex;
+    align-items: center;
+  }
+  .grid_item {
+    flex: 1;
+    height: 150px;
+    box-sizing: border-box;
+    border-right: 1px solid #ddd;
+    padding-left: 20px;
+  }
+  .grid_item:last-child {
+    border: 0
+  }
 </style>

+ 221 - 3
src/views/super/role/worker/index.vue

@@ -1,13 +1,231 @@
 <template>
-  <div>worker manage index</div>
+  <div class="box">
+    <el-card>
+      <div
+        slot="header"
+        class="clearfix"
+      >
+        <span>指标一览</span>
+      </div>
+      <div class="grid">
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+        <div class="grid_item" />
+      </div>
+    </el-card>
+    <div class="app-container">
+      <div class="filter-container">
+        <el-input
+          v-model="worker.query.cond"
+          placeholder="请输入关键字"
+          style="width: 300px;"
+          class="filter-item"
+        />
+        <el-select
+          v-model="worker.query.manager"
+          remote
+          filterable
+          :remote-method="handleManagerSearch"
+          :loading="manager.loading"
+          placeholder="管理员"
+          style="margin-left: 10px;"
+          class="filter-item"
+        >
+          <el-option-group
+            v-for="group in manager.groups"
+            :key="group.label"
+            :label="group.label"
+          >
+            <el-option
+              v-for="item in group.options"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-option-group>
+        </el-select>
+        <el-button
+          class="filter-item"
+          style="margin-left: 10px;"
+          type="primary"
+          icon="el-icon-search"
+          @click="handleQuery"
+        >
+          查询
+        </el-button>
+      </div>
+    </div>
+    <el-table
+      :key="tableKey"
+      v-loading="worker.loading"
+      :data="worker.list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;"
+    >
+      <el-table-column
+        type="index"
+        label="序号"
+        width="80"
+        align="center"
+      />
+      <el-table-column
+        label="姓名"
+        prop="name"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="联系方式"
+        prop="phone"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="所属管理员"
+        align="center"
+        width="150"
+      >
+        <template slot-scope="{row}">
+          <span>{{ row.manager.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="累计上酒次数"
+        prop="count"
+        align="center"
+        width="250"
+      />
+      <el-table-column
+        label="平均出勤时长(min)"
+        align="center"
+        width="250"
+      >
+        <template slot-scope="{ row }">
+          <span>{{ convertSeconds(row.cost) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="注册时间"
+        align="center"
+        width="250"
+        prop="first"
+      />
+      <el-table-column
+        label="最近活跃"
+        prop="last"
+        align="center"
+        width="250"
+      />
+    </el-table>
+    <pagination
+      hide-on-single-page
+      :total="worker.total"
+      :page.sync="worker.query.page"
+      :limit.sync="worker.query.limit"
+      @pagination="getList"
+    />
+  </div>
 </template>
 
 <script>
+import api from '@/api/super/role/worker'
+const specialGroup = { label: '聚合型', options: [{ name: '未分配', id: '' }, { name: '所有', id: '*' }] }
 export default {
-  name: 'WorkerManage'
+  name: 'WorkerManage',
+  data() {
+    return {
+      tableKey: 0,
+      detailShow: false,
+      batchShow: false,
+      worker: {
+        list: [],
+        total: 0,
+        loading: true,
+        sort: {
+          first: 'ascending',
+          last: 'ascending'
+        },
+        query: {
+          page: 1,
+          limit: 10,
+          cond: '',
+          manager: ''
+        }
+      },
+      manager: {
+        loading: false,
+        groups: [
+          specialGroup,
+          { label: '具体型', options: [] }
+        ],
+        list: []
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.handleManagerSearch('')
+  },
+  methods: {
+    convertSeconds(val) {
+      // 转换为小时、分钟和秒
+      var hours = Math.floor(val / 3600)
+      var minutes = Math.floor((val % 3600) / 60)
+      var seconds = val % 60
+      var result = hours + '小时 ' + minutes + '分钟 ' + seconds + '秒'
+      return result
+    },
+    handleManagerSearch(value) {
+      this.manager.loading = true
+      api.QueryManager(value).then(res => {
+        this.manager.groups[1].options = res
+        this.manager.loading = false
+      })
+    },
+    getList() {
+      this.worker.loading = true
+      api.Query(this.worker.query).then(res => {
+        for (let i = 0; i < res.data.list.length; ++i) {
+          res.data.list[i].first = this.$moment(new Date(res.data.list[i].first)).format('YYYY-MM-DD HH:mm:ss')
+          res.data.list[i].last = this.$moment(new Date(res.data.list[i].last)).format('YYYY-MM-DD HH:mm:ss')
+        }
+        this.worker.list = res.data.list
+        console.log(this.worker.list)
+        this.worker.total = res.data.total
+        this.worker.loading = false
+      })
+    },
+    handleQuery() {
+      this.worker.query.page = 1
+      this.getList()
+    }
+  }
 }
 </script>
 
 <style scoped>
-
+.box {
+  box-sizing: border-box;
+  padding: 20px;
+}
+.grid {
+  width: 100%;
+  height: 150px;
+  display: flex;
+  align-items: center;
+}
+.grid_item {
+  flex: 1;
+  height: 150px;
+  box-sizing: border-box;
+  border-right: 1px solid #ddd;
+  padding-left: 20px;
+}
+.grid_item:last-child {
+  border: 0
+}
 </style>

+ 1 - 1
vue.config.js

@@ -34,7 +34,7 @@ module.exports = {
     open: true,
     overlay: {
       warnings: false,
-      errors: true
+      errors: false
     },
     before: require('./mock/mock-server.js')
   },