Kaynağa Gözat

Merge branch 'developer'

yuwenjun1997 2 yıl önce
ebeveyn
işleme
60e31df7d4

+ 4 - 4
.env.development

@@ -21,8 +21,8 @@ VUE_APP_SOCKET_SERVER = 'wss://zplma-b.caimei365.com/websocket?sessionSource=zpl
 # VUE_APP_SOCKET_SERVER = 'ws://192.168.2.200:8012/websocket?sessionSource=zplm_admin'
 
 # 网站地址
-VUE_APP_LOCAL = 'http://192.168.2.92:9527'
-# VUE_APP_LOCAL = 'http://zplm-b.caimei365.com'
+# VUE_APP_LOCAL = 'https://192.168.2.92:9527'
+VUE_APP_LOCAL = 'https://zplm-b.caimei365.com'
 
 # 认证通页面
 VUE_APP_WWW_HOST = 'https://192.168.2.92:8888'
@@ -33,6 +33,6 @@ VUE_APP_WWW_HOST = 'https://192.168.2.92:8888'
 VUE_APP_PAY_LOCAL = 'http://zplm-b.caimei365.com'
 
 # 文件上传路径
-VUE_APP_UPLOAD_DIR = 'dev/authFile/'
-# VUE_APP_UPLOAD_DIR = 'beta/authFile/'
+# VUE_APP_UPLOAD_DIR = 'dev/authFile/'
+VUE_APP_UPLOAD_DIR = 'beta/authFile/'
 

BIN
dist.rar


+ 99 - 0
src/api/activity.js

@@ -0,0 +1,99 @@
+import request from '@/utils/request'
+
+// 获取抖音挑战赛活动状态
+export function fetchDouyinActivityStatus(params) {
+  return request({
+    url: '/auth/get/activity/time',
+    method: 'get',
+    params
+  })
+}
+
+// 获取抖音挑战赛活动状态
+export function updateDouyinActivityStatus(data) {
+  return request({
+    url: '/auth/save/activity/time',
+    method: 'post',
+    data
+  })
+}
+
+// 获取抖音挑战赛视频列表
+export function fetchDouyinVideoList(params) {
+  return request({
+    url: '/auth/get/published/video/list',
+    method: 'get',
+    params
+  })
+}
+
+// 获取抖音挑战赛视频列表
+export function onSendDouyinCommand(data) {
+  return request({
+    url: '/auth/sms/send/douyin',
+    method: 'post',
+    data
+  })
+}
+
+// 获取已上传视频机构列表
+export function fetchClubList(params) {
+  return request({
+    url: '/auth/get/auth/party/list',
+    method: 'get',
+    params
+  })
+}
+
+// 校access_token 是否过期
+export function checkDouyinAccessToken() {
+  return request({
+    url: '/auth/check/accesstoken',
+    method: 'get'
+  })
+}
+
+// 获取accessToken
+export function getDouyinAccessToken(params) {
+  return request({
+    url: '/auth/get/douying/acesstoken',
+    method: 'get',
+    params
+  })
+}
+
+// 获取抖音视频发布分享码 h5
+export function getDouyinShareH5(data) {
+  return request({
+    url: '/auth/upload/video/to/douyin',
+    method: 'post',
+    data
+  })
+}
+
+// 删除抖音视频
+export function removeVideo(params) {
+  return request({
+    url: 'auth/del/video',
+    method: 'get',
+    params
+  })
+}
+
+// 下载抖音视频
+export function downloadVideo(params) {
+  return request({
+    url: '/auth/downLoad/chose/zip',
+    method: 'get',
+    params
+  })
+}
+
+// 获取抖音视频详情
+export function getShareVideo(params) {
+  return request({
+    url: '/auth/get/info/by/id',
+    method: 'get',
+    params
+  })
+}

+ 1 - 1
src/permission.js

@@ -11,7 +11,7 @@ if (userInfo) {
 
 // 路由白名单
 const whiteList = ['/login']
-const shareList = ['Share', 'SharePayVip', 'SharePaySuccess', 'SharePayFaild']
+const shareList = ['Share', 'SharePayVip', 'SharePaySuccess', 'SharePayFaild', 'DouyinResult', 'DouyinShareSchema']
 
 // 路由拦截器
 router.beforeEach(async(to, from, next) => {

+ 2 - 0
src/router/index.js

@@ -28,6 +28,7 @@ import normalPersonnel from './module/normal/personnel'
 import normalVip from './module/normal/vip'
 import normalUser from './module/normal/user'
 import normalSettings from './module/normal/settings'
+import activityRoutes from './module/normal/activity'
 
 // 需要权限访问的路由列表
 export const asyncRoutes = [
@@ -45,6 +46,7 @@ export const asyncRoutes = [
   ...normalPersonal,
   ...normalPersonnel,
   ...normalSettings,
+  ...activityRoutes,
   ...normalUser
 ]
 

+ 18 - 0
src/router/module/base.js

@@ -7,11 +7,28 @@ export default [
     component: () => import(/* webpackChunkName: "common-page" */ '@/views/index'),
     hidden: true
   },
+  {
+    name: 'DouyinResult',
+    path: '/douyin',
+    component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/redirect/douyin'),
+    hidden: true
+  },
+  {
+    name: 'DouyinShareSchema',
+    path: '/douyin/schema',
+    component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/redirect/schema'),
+    hidden: true
+  },
   {
     path: '/login',
     component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/login'),
     hidden: true
   },
+  {
+    path: '/code',
+    component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/auth/code'),
+    hidden: true
+  },
   {
     path: '/proxy',
     component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/proxy'),
@@ -68,6 +85,7 @@ export default [
       }
     ]
   },
+
   {
     path: '/404',
     component: () => import(/* webpackChunkName: "common-page" */ '@/views/common/error-page/404'),

+ 24 - 0
src/router/module/normal/activity.js

@@ -0,0 +1,24 @@
+/* Layout */
+import Layout from '@/layout'
+
+// 资料管理页面路由
+const activityRoutes = [
+  {
+    path: '/challenge',
+    component: Layout,
+    alwaysShow: true,
+    redirect: '/video',
+    name: 'Challenge',
+    meta: { title: '挑战赛', noCache: true },
+    children: [
+      {
+        path: 'video',
+        component: () => import('@/views/normal/activity/video'),
+        name: 'ChallengeVideoList',
+        meta: { title: '短视频', noCache: true }
+      }
+    ]
+  }
+]
+
+export default activityRoutes

+ 42 - 5
src/styles/index.scss

@@ -21,7 +21,8 @@ label {
 
 html {
   height: 100%;
-  box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
 }
 
 #app {
@@ -31,7 +32,8 @@ html {
 *,
 *:before,
 *:after {
-  box-sizing: inherit;
+  -webkit-box-sizing: inherit;
+          box-sizing: inherit;
 }
 
 .no-padding {
@@ -145,7 +147,16 @@ aside {
   width: 100%;
   text-align: right;
   padding-right: 20px;
+  -webkit-transition: 600ms ease position;
   transition: 600ms ease position;
+  background: -webkit-gradient(
+    linear,
+    left top, right top,
+    from(rgba(32, 182, 249, 1)),
+    color-stop(0%, rgba(32, 182, 249, 1)),
+    color-stop(100%, rgba(33, 120, 241, 1)),
+    to(rgba(33, 120, 241, 1))
+  );
   background: linear-gradient(
     90deg,
     rgba(32, 182, 249, 1) 0%,
@@ -286,6 +297,18 @@ aside {
 .display {
   .el-input {
     input {
+      &::-webkit-input-placeholder {
+        color: #606266;
+      }
+      &::-moz-placeholder {
+        color: #606266;
+      }
+      &:-ms-input-placeholder {
+        color: #606266;
+      }
+      &::-ms-input-placeholder {
+        color: #606266;
+      }
       &::placeholder {
         color: #606266;
       }
@@ -341,9 +364,15 @@ aside {
 }
 
 .el-dialog__wrapper {
+  display: -webkit-box;
+  display: -ms-flexbox;
   display: flex;
-  justify-content: center;
-  align-items: center;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
 
   .el-dialog {
     margin: 0;
@@ -397,7 +426,8 @@ aside {
     height: 100%;
     position: absolute;
     left: 50%;
-    transform: translateX(-50%);
+    -webkit-transform: translateX(-50%);
+            transform: translateX(-50%);
   }
 }
 
@@ -414,3 +444,10 @@ aside {
 .max-width {
   width: 100%;
 }
+
+.el-table-column--selection {
+  .cell {
+    padding-left: 10px;
+    padding-right: 10px;
+  }
+}

+ 31 - 23
src/utils/index.js

@@ -28,8 +28,8 @@ export function parseTime(time, cFormat) {
   if (typeof time === 'object') {
     date = time
   } else {
-    if ((typeof time === 'string')) {
-      if ((/^[0-9]+$/.test(time))) {
+    if (typeof time === 'string') {
+      if (/^[0-9]+$/.test(time)) {
         // support "1548221490638"
         time = parseInt(time)
       } else {
@@ -39,7 +39,7 @@ export function parseTime(time, cFormat) {
       }
     }
 
-    if ((typeof time === 'number') && (time.toString().length === 10)) {
+    if (typeof time === 'number' && time.toString().length === 10) {
       time = time * 1000
     }
     date = new Date(time)
@@ -56,7 +56,9 @@ export function parseTime(time, cFormat) {
   const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
     const value = formatObj[key]
     // Note: getDay() returns 0 on Sunday
-    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    if (key === 'a') {
+      return ['日', '一', '二', '三', '四', '五', '六'][value]
+    }
     return value.toString().padStart(2, '0')
   })
   return time_str
@@ -91,17 +93,7 @@ export function formatTime(time, option) {
   if (option) {
     return parseTime(time, option)
   } else {
-    return (
-      d.getMonth() +
-      1 +
-      '月' +
-      d.getDate() +
-      '日' +
-      d.getHours() +
-      '时' +
-      d.getMinutes() +
-      '分'
-    )
+    return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
   }
 }
 
@@ -135,7 +127,7 @@ export function byteLength(str) {
     const code = str.charCodeAt(i)
     if (code > 0x7f && code <= 0x7ff) s++
     else if (code > 0x7ff && code <= 0xffff) s += 2
-    if (code >= 0xDC00 && code <= 0xDFFF) i--
+    if (code >= 0xdc00 && code <= 0xdfff) i--
   }
   return s
 }
@@ -161,7 +153,7 @@ export function cleanArray(actual) {
 export function param(json) {
   if (!json) return ''
   return cleanArray(
-    Object.keys(json).map(key => {
+    Object.keys(json).map((key) => {
       if (json[key] === undefined) return ''
       return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
     })
@@ -179,7 +171,7 @@ export function param2Obj(url) {
   }
   const obj = {}
   const searchArr = search.split('&')
-  searchArr.forEach(v => {
+  searchArr.forEach((v) => {
     const index = v.indexOf('=')
     if (index !== -1) {
       const name = v.substring(0, index)
@@ -213,7 +205,7 @@ export function objectMerge(target, source) {
   if (Array.isArray(source)) {
     return source.slice()
   }
-  Object.keys(source).forEach(property => {
+  Object.keys(source).forEach((property) => {
     const sourceProperty = source[property]
     if (typeof sourceProperty === 'object') {
       target[property] = objectMerge(target[property], sourceProperty)
@@ -252,9 +244,7 @@ export function toggleClass(element, className) {
   if (nameIndex === -1) {
     classString += '' + className
   } else {
-    classString =
-      classString.substr(0, nameIndex) +
-      classString.substr(nameIndex + className.length)
+    classString = classString.substr(0, nameIndex) + classString.substr(nameIndex + className.length)
   }
   element.className = classString
 }
@@ -324,7 +314,7 @@ export function deepClone(source) {
     throw new Error('error arguments', 'deepClone')
   }
   const targetObj = source.constructor === Array ? [] : {}
-  Object.keys(source).forEach(keys => {
+  Object.keys(source).forEach((keys) => {
     if (source[keys] && typeof source[keys] === 'object') {
       targetObj[keys] = deepClone(source[keys])
     } else {
@@ -422,3 +412,21 @@ export function countDown(diff, loadTime, item, callback) {
   }
   round(diff - loadTime)
 }
+
+/**
+ * 拼接url链接
+ * @param {*} url url链接地址
+ * @param {*} queryObj query对象
+ * @returns url
+ */
+export function generateQueryUrl(url, queryObj) {
+  url = new URL(url)
+  var query = url.searchParams
+
+  for (const key in queryObj) {
+    if (Object.hasOwnProperty.call(queryObj, key)) {
+      query.append(key, queryObj[key])
+    }
+  }
+  return url
+}

+ 15 - 2
src/views/admin/logistics-licensed/components/club-list.vue

@@ -11,6 +11,16 @@
           @keyup.enter.native="handleFilter"
         />
       </div>
+      <div class="filter-control">
+        <span>认证编号:</span>
+        <el-input
+          v-model="listQuery.authCode"
+          placeholder="认证编号"
+          style="width: 280px"
+          class="filter-item"
+          @keyup.enter.native="handleFilter"
+        />
+      </div>
       <div class="filter-control">
         <el-button type="primary" @click="getList">查询</el-button>
       </div>
@@ -30,6 +40,8 @@
 
       <el-table-column label="机构名称" align="center" prop="authParty" />
 
+      <el-table-column label="认证编号" align="center" width="240" prop="authCode" />
+
       <el-table-column label="创建时间" class-name="status-col" width="160px">
         <template slot-scope="{ row }">
           <span>{{ row.createTime | formatTime }}</span>
@@ -52,7 +64,7 @@
             查看
           </el-button>
           <el-button type="primary" size="mini" @click="navigationTo(`/logistics/device-list?authId=${row.authId}`)">
-            查看设备认证
+            设备列表
           </el-button>
           <el-button v-if="sendStatus === 0" type="primary" size="mini" @click="onSend(row)"> 寄送 </el-button>
           <el-button v-else type="primary" size="mini" @click="onLookRecord(row)"> 寄送记录 </el-button>
@@ -142,7 +154,8 @@ export default {
         pageSize: 10, // 分页
         status: '',
         listType: 4,
-        sendStatus: ''
+        sendStatus: '',
+        authCode: ''
       },
       formData: {
         authId: '',

+ 145 - 6
src/views/admin/logistics-licensed/licensed-record.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="page-section">
     <div class="app-container">
+      <div class="control"><el-button size="small" type="primary" @click="onChange">修改</el-button></div>
       <div class="row">
         <div class="col">货物名称:</div>
         <div class="col">{{ detailData.authParty }}授权牌</div>
@@ -13,7 +14,6 @@
         <div class="col">物流单号:</div>
         <div class="col">{{ detailData.logisticsNumber }}</div>
       </div>
-
       <div class="title">物流详情</div>
       <el-divider />
       <div class="info">
@@ -23,17 +23,87 @@
         </div>
       </div>
     </div>
+
+    <!-- 物流信息修改 -->
+    <el-dialog title="机构授权牌寄送" :visible.sync="dialogVisible" width="40%">
+      <el-form ref="ruleForm" :model="formData" :rules="rules" label-width="100px">
+        <el-form-item label="快递公司:" prop="companyId">
+          <el-select v-model="formData.companyId" placeholder="请选择快递公司" clearable>
+            <template v-for="item in companyList">
+              <el-option :key="item.id" :label="item.companyName" :value="item.id" />
+            </template>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="物流编号:" prop="logisticsNumber">
+          <el-input v-model="formData.logisticsNumber" placeholder="请输入物流编号" />
+        </el-form-item>
+        <el-form-item v-if="formData.companyId === 222" label="手机号:" prop="mobile">
+          <el-input v-model="formData.mobile" placeholder="请输入收货人手机号" maxlength="11" show-word-limit />
+        </el-form-item>
+        <el-form-item label="图片备注:" prop="imageRemark">
+          <el-input v-show="false" v-model="formData.imageRemark" />
+          <upload-image
+            tip="请上传jpg/png格式的图片,最大不超过5M,最多可上传6张"
+            :multiple="true"
+            :limit="6"
+            accept=".jpg,.png"
+            :image-list="imageRemarkList"
+            :before-upload="beforeRemarkImageUpload"
+            @success="uploadRemarkImageSuccess"
+            @remove="handleRemarkImageRemove"
+          />
+        </el-form-item>
+        <el-form-item label="备注:" prop="remarks">
+          <el-input v-model="formData.remarks" type="textarea" rows="4" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="onSubmitLogistics">确 定</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { logisticsDetails } from '@/api/logistics'
+import { fetchCompanyList, logisticsDetails, logisticsSend } from '@/api/logistics'
+import UploadImage from '@/components/UploadImage'
+import { isMobile } from '@/utils/validate'
 export default {
+  components: { UploadImage },
   data() {
+    // 表单校验手机号
+    const mobileValidate = (rule, value, callback) => {
+      if (!isMobile(value)) {
+        callback(new Error('手机号格式不正确'))
+      } else {
+        callback()
+      }
+    }
     return {
       authId: '',
       detailData: {},
-      logisticInfo: []
+      logisticInfo: [],
+      dialogVisible: false,
+      formData: {
+        authId: '',
+        companyId: '',
+        logisticsNumber: '',
+        imageList: '',
+        remarks: '',
+        imageRemark: '',
+        mobile: ''
+      },
+      rules: {
+        companyId: [{ required: true, message: '快递公司名称不能为空', trigger: ['change'] }],
+        logisticsNumber: [{ required: true, message: '物流编号不能为空', trigger: ['blur'] }],
+        mobile: [
+          { required: true, message: '收货人手机号不能为空', trigger: ['blur'] },
+          { validator: mobileValidate, trigger: ['blur'] }
+        ]
+      },
+      companyList: [],
+      imageRemarkList: []
     }
   },
   created() {
@@ -45,11 +115,73 @@ export default {
       try {
         const res = await logisticsDetails({ authId: this.authId })
         this.detailData = { ...this.detailData, ...res.data }
-        this.logisticInfo = res.data.info && JSON.parse(res.data.info)
-        console.log(this.logisticInfo)
+        if (res.data.info) {
+          this.logisticInfo = JSON.parse(res.data.info)
+        }
+        this.initLogistData(res.data)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    initLogistData(data) {
+      this.formData.authId = this.authId
+      this.formData.companyId = data.companyCode
+      this.formData.logisticsNumber = data.logisticsNumber
+      this.formData.imageList = data.imageList
+      this.formData.remarks = data.remarks
+      this.formData.imageRemark = data.remarksImage
+      this.formData.mobile = data.mobile
+      if (data.imageList && data.imageList.length > 0) {
+        this.imageRemarkList = data.imageList.map((item) => ({ name: '图片备注', url: item }))
+      }
+    },
+
+    onChange() {
+      this.fetchCompanyList()
+      this.dialogVisible = true
+    },
+    async onSubmitLogistics() {
+      try {
+        await this.$refs.ruleForm.validate()
       } catch (error) {
         console.log(error)
+        return
       }
+      try {
+        await logisticsSend(this.formData)
+        this.$message.success('发货成功')
+        this.dialogVisible = false
+        this.getList()
+      } catch (error) {
+        this.$message.error('发货失败')
+      }
+    },
+
+    async fetchCompanyList() {
+      try {
+        const res = await fetchCompanyList()
+        this.companyList = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    uploadRemarkImageSuccess({ response, file, fileList }) {
+      this.imageRemarkList = fileList
+      this.formData.imageRemark = fileList.length
+      this.formData.imageList = fileList.map((item) => item.response.data)
+    },
+    handleRemarkImageRemove({ file, fileList }) {
+      this.imageRemarkList = fileList
+      this.formData.imageRemark = fileList.length
+      this.formData.imageList = fileList.map((item) => item.response.data)
+    },
+    beforeRemarkImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 1
+      if (!flag) {
+        this.$message.error('医疗许可证图片大小不能超过 1MB!')
+      }
+      return flag
     }
   }
 }
@@ -58,15 +190,22 @@ export default {
 <style lang="scss" scoped>
 .page-section {
   width: 100%;
-  height: calc(100vh - 84px);
+  min-height: calc(100vh - 84px);
   background: #f7f7f7;
   box-sizing: border-box;
   padding: 20px;
 }
 
 .app-container {
+  position: relative;
   background: #fff;
   box-sizing: border-box;
+
+  .control {
+    position: absolute;
+    right: 20px;
+    top: 20px;
+  }
   .row {
     display: flex;
     justify-content: flex-start;

+ 3 - 0
src/views/common/auth/code.vue

@@ -0,0 +1,3 @@
+<template>
+  <div />
+</template>

+ 16 - 0
src/views/common/redirect/douyin.vue

@@ -0,0 +1,16 @@
+<template>
+  <div>123</div>
+</template>
+
+<script>
+import { getQueryObject } from '@/utils'
+import { setStorage } from '@/utils/storage'
+export default {
+  created() {
+    const urlQuery = getQueryObject()
+    urlQuery.state = urlQuery.state.slice(0, urlQuery.state.indexOf('#'))
+    setStorage('zp_douyin_code', urlQuery.code)
+    window.location.href = window.location.origin
+  }
+}
+</script>

+ 100 - 0
src/views/common/redirect/schema.vue

@@ -0,0 +1,100 @@
+<template>
+  <div v-loading="isRequest" class="page">
+    <template v-if="!isRequest">
+      <!-- <img src="~@/assets/404_images/404.png" alt=""> -->
+      <span v-if="errorText">{{ errorText }}</span>
+    </template>
+    <a ref="schema" :href="schema" />
+    <!-- <div>{{ isIOS }}</div> -->
+  </div>
+</template>
+
+<script>
+import { getDouyinShareH5, getShareVideo } from '@/api/activity'
+export default {
+  data() {
+    return {
+      isRequest: true,
+      schema: '',
+      errorText: '',
+      userAgent: null,
+      isIOS: false
+    }
+  },
+  created() {
+    this.isRequest = true
+    this.initUserAgent()
+  },
+  methods: {
+    // 初始化userAgent
+    initUserAgent() {
+      const u = navigator.userAgent
+      // const isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1 // 判断是否是 android终端
+      this.isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) // 判断是否是 iOS终端
+      this.getShareVideo()
+    },
+    // 获取要分享的视频信息
+    async getShareVideo() {
+      try {
+        const id = this.$route.query.id
+        const res = await getShareVideo({ videoId: id })
+        this.getDouyinSchema(res.data)
+        if (res.data.status === 1) {
+          return (this.errorText = '该视频已发布过')
+        }
+      } catch (error) {
+        this.isRequest = false
+        this.errorText = error.msg
+      }
+    },
+    // 获取抖音schema
+    async getDouyinSchema(row) {
+      try {
+        const res = await getDouyinShareH5({
+          title: row.title,
+          videoPath: row.ossUrl,
+          authId: row.authId
+        })
+        // 对 ios 和 Android 进行区分
+        this.schema = this.isIOS
+          ? res.data.replace('snssdk1128://openplatform', 'snssdk1128://deeplink/openplatform')
+          : res.data
+        // 触发链接
+        this.$nextTick(() => {
+          this.$refs.schema.click()
+          setTimeout(() => {
+            window.location.href = 'https://www.caimei365.com'
+          }, 3000)
+        })
+      } catch (error) {
+        this.errorText = error.msg
+      } finally {
+        this.isRequest = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.page {
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding-top: 120px;
+  img {
+    width: 30%;
+  }
+
+  span {
+    font-size: 1.2em;
+    margin-top: 30px;
+    color: #51a3ef;
+    font-weight: bold;
+  }
+}
+</style>

+ 8 - 0
src/views/components/ClubDetail/index.vue

@@ -35,6 +35,14 @@
       <div class="col">运营联系人手机号:</div>
       <div class="col">{{ clubInfo.linkMobile }}</div>
     </div>
+    <div class="row">
+      <div class="col">认证编号:</div>
+      <div class="col">{{ clubInfo.authCode }}</div>
+    </div>
+    <div class="row">
+      <div class="col">认证日期:</div>
+      <div class="col">{{ clubInfo.authDate | formatTime }}</div>
+    </div>
     <div class="row">
       <div class="col">logo:</div>
       <div class="col">

+ 549 - 0
src/views/normal/activity/video/index.vue

@@ -0,0 +1,549 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container activity">
+      <div class="filter-control">
+        <span>活动时间:</span>
+        <el-date-picker
+          v-model="activityFormData.time"
+          format="yyyy-MM-dd HH:mm:ss"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :disabled="activityFormData.status === 1"
+        />
+      </div>
+      <div class="filter-control">
+        <span>活动状态:</span>
+        <el-switch
+          v-model="activityFormData.status"
+          el-switch
+          :active-value="1"
+          :inactive-value="0"
+          @change="onUpdateActivityStatus"
+        />
+      </div>
+    </div>
+    <el-divider />
+    <div class="filter-container">
+      <div class="filter-control">
+        <span>所属机构:</span>
+        <el-select v-model="listQuery.authId" placeholder="所属机构" clearable @change="getList">
+          <template v-for="(item, index) in clubList">
+            <el-option :key="index" :label="item.authParty" :value="item.authId" />
+          </template>
+        </el-select>
+      </div>
+      <div class="filter-control">
+        <span>登录账号:</span>
+        <el-input v-model="listQuery.userName" placeholder="登录账号" @keyup.enter.native="getList" />
+      </div>
+      <div class="filter-control">
+        <el-button type="primary" @click="getList">查询</el-button>
+        <el-button type="primary" :disabled="selectionList.length === 0" @click="onDownload('select')">下载</el-button>
+      </div>
+    </div>
+    <!-- 列表 -->
+    <el-table
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%"
+      header-row-class-name="tableHeader"
+      @selection-change="onSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="序号" :index="indexMethod" align="center" width="80" type="index" />
+      <el-table-column label="视频" align="center" width="100">
+        <template slot-scope="{ row }"><el-image class="cover" :src="row.cover" /></template>
+      </el-table-column>
+      <el-table-column label="登录账号" align="center" prop="userName" width="240" />
+      <el-table-column label="机构名称" align="center" prop="authParty" />
+      <el-table-column label="浏览量" align="center" prop="playCount" width="120" />
+      <el-table-column label="点赞数" align="center" prop="diggCount" width="120" />
+      <el-table-column label="排名" align="center" prop="rankNum" width="120" />
+      <el-table-column label="抖音上传状态" align="center" width="120">
+        <template slot-scope="{ row }">
+          <span v-if="row.status === 1" class="status success">已上传</span>
+          <span v-if="row.status === 0" class="status danger">未上传</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="上传时间" align="center" width="160">
+        <template slot-scope="{ row }">
+          <span>{{ row.releaseTime | formatDate }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="440">
+        <template slot-scope="{ row }">
+          <permission-button
+            v-if="row.status !== 1"
+            size="mini"
+            type="primary"
+            @click="onSyancDouyin(row)"
+          >上传至抖音</permission-button>
+          <permission-button size="mini" type="primary" @click="onSaveCommand(row)">抖音口令</permission-button>
+          <permission-button size="mini" type="primary" @click="onPlayVideo(row)">播放视频</permission-button>
+          <permission-button size="mini" type="primary" @click="onDownload('single', row)">下载</permission-button>
+          <permission-button size="mini" type="primary" @click="onRemove(row)">删除</permission-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 上传抖音 -->
+    <el-dialog
+      title="发布抖音"
+      :visible.sync="scanDialog"
+      width="300px"
+      center
+      :close-on-click-modal="false"
+      @close="onScanClose"
+    >
+      <el-image class="dy-code" :src="qrcodeUrl" />
+      <div class="tip">请使用抖音app扫码发布视频</div>
+    </el-dialog>
+
+    <!-- 抖音口令 -->
+    <el-dialog
+      title="抖音口令"
+      :visible.sync="commandDialog"
+      width="30%"
+      :show-close="false"
+      :close-on-click-modal="false"
+      @close="onCommandClose"
+    >
+      <el-form ref="commandForm" :model="commandFormData" :rules="commandRules">
+        <el-form-item prop="content">
+          <el-input v-model="commandFormData.content" type="textarea" :rows="5" placeholder="请输入抖音口令" />
+        </el-form-item>
+      </el-form>
+      <div class="control">
+        <el-button @click="commandDialog = false">取消</el-button>
+        <el-button type="primary" @click="onCommandSubmit">提交</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 视频播放 -->
+    <el-dialog title="视频播放" :visible.sync="videoDialog" width="800px" center :close-on-click-modal="false">
+      <video class="video-play" :src="videoUrl" controls />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  checkDouyinAccessToken,
+  fetchClubList,
+  fetchDouyinActivityStatus,
+  fetchDouyinVideoList,
+  getDouyinAccessToken,
+  onSendDouyinCommand,
+  removeVideo,
+  updateDouyinActivityStatus
+} from '@/api/activity'
+
+import Qrcode from 'qrcode'
+import { generateQueryUrl } from '@/utils'
+import { mapGetters } from 'vuex'
+import { getStorage, removeStorage } from '@/utils/storage'
+import { downloadWithUrl } from '@/utils/tools'
+
+export default {
+  data() {
+    return {
+      // 视频播放
+      videoDialog: false,
+      videoUrl: '',
+      listQuery: {
+        status: '',
+        authId: '',
+        userName: '',
+        authParty: '',
+        pageNum: 1,
+        pageSize: 10
+      },
+      listLoading: false,
+      list: [],
+      // 活动表单数据
+      activityFormData: {
+        time: [],
+        status: 0
+      },
+      scanDialog: false,
+      commandDialog: false,
+      qrcodeUrl: '',
+      // 抖音口令表单数据
+      commandFormData: {
+        authId: '',
+        content: ''
+      },
+      commandRules: {
+        content: [{ required: true, message: '口令不能为空', trigger: ['blur'] }]
+      },
+      clubList: [],
+      selectionList: []
+    }
+  },
+  computed: {
+    ...mapGetters(['proxyInfo'])
+  },
+  created() {
+    this.getList()
+    this.getClubList()
+    this.fetchActivityStatus()
+  },
+  methods: {
+    // 获取活动状态
+    async fetchActivityStatus() {
+      try {
+        const res = await fetchDouyinActivityStatus()
+        if (!res.data) return
+        const { startTime, endTime } = res.data
+        if (startTime && endTime) {
+          this.activityFormData.time = [startTime, endTime]
+        }
+        this.activityFormData.status = res.data.status
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 验证活动时间
+    checkActivityTime(startTime, endTime) {
+      startTime = new Date(startTime).getTime()
+      endTime = new Date(endTime).getTime()
+      const nowTime = Date.now()
+      if (startTime < nowTime) {
+        return 1
+      }
+      if (endTime < startTime) {
+        return 2
+      }
+      if (endTime < nowTime) {
+        return 3
+      }
+    },
+
+    // 更新活动时间状态
+    onUpdateActivityStatus(value) {
+      let flag = -1
+
+      const dialog = {
+        0: '请先设置活动时间后,再来开启活动按钮',
+        1: '活动开始时间不能小于当前时间',
+        2: '活动结束时间不能小于活动开始时间',
+        3: '你设置的活动时间已失效,请重新设置有效活动时间后,再来开启活动按钮'
+      }
+
+      if (!this.activityFormData.time) {
+        flag = 0
+      } else {
+        const [start, end] = this.activityFormData.time
+        flag = this.checkActivityTime(start, end)
+      }
+
+      if (flag > -1 && value === 1) {
+        this.$confirm(dialog[flag], '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+          closeOnClickModal: false,
+          showClose: false
+        })
+          .then(() => {
+            this.activityFormData.status = 0
+          })
+          .catch(() => {
+            this.activityFormData.status = 0
+          })
+        return
+      }
+
+      this.updateActivityStatus()
+    },
+
+    // 更新活动状态
+    async updateActivityStatus() {
+      try {
+        const {
+          status,
+          time: [startTime, endTime]
+        } = this.activityFormData
+        await updateDouyinActivityStatus({
+          startTime,
+          endTime,
+          status
+        })
+        this.$message.success(`活动已${status ? '开启' : '关闭'}`)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 获取视频列表
+    getList() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchDouyinVideoList()
+    },
+
+    // 获取视频列表
+    async fetchDouyinVideoList() {
+      try {
+        this.listLoading = true
+        const res = await fetchDouyinVideoList(this.listQuery)
+        this.list = res.data.list.map((item, index) => {
+          if (item.rankingStatus === 1) {
+            item.rankNum = index + 1
+          }
+          return item
+        })
+        this.listLoading = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 表格数据选中
+    onSelectionChange(data) {
+      this.selectionList = data
+    },
+
+    // 表格索引
+    indexMethod(index) {
+      return index + this.listQuery.pageSize * (this.listQuery.pageNum - 1) + 1
+    },
+
+    // 获取已发布视频机构列表
+    async getClubList(list) {
+      try {
+        const res = await fetchClubList()
+        this.clubList = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 上传抖音
+    async onSyancDouyin(row) {
+      // 代理模式下不允许操作
+      if (this.proxyInfo) {
+        return this.$confirm('请退出代操作,使用供应商账号登陆后操作!', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消'
+        })
+          .then(() => {})
+          .catch(() => {})
+      }
+      try {
+        // 校验token
+        const { data: tokenFlag } = await checkDouyinAccessToken()
+        if (!tokenFlag) {
+          this.douyinLogin(row)
+          return
+        }
+        const url = process.env.VUE_APP_LOCAL + `/#/douyin/schema?id=${row.id}`
+        await this.generateQrcode(url)
+        // 获取分享码
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 登录抖音
+    async douyinLogin(row) {
+      const code = getStorage('zp_douyin_code')
+      if (!code) {
+        // 获取token
+        const url = generateQueryUrl('https://open.douyin.com/platform/oauth/connect', {
+          client_key: 'awwwvh9tsnvo54w1',
+          response_type: 'code',
+          scope: 'video.data,video.list,trial.whitelist,data.external.item,h5.share',
+          redirect_uri: process.env.VUE_APP_LOCAL + '#/douyin',
+          state: '/challenge/video'
+        })
+        window.open(url, '_blank')
+        return
+      }
+      removeStorage('zp_douyin_code')
+      try {
+        await getDouyinAccessToken({ code })
+        this.onSyancDouyin(row)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 抖音视频对话框关闭
+    onScanClose() {
+      this.getList()
+    },
+
+    // 生成h5分享码
+    async generateQrcode(str) {
+      try {
+        const res = await Qrcode.toDataURL(str, {})
+        this.qrcodeUrl = res
+        this.scanDialog = true
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 提交抖音口令
+    async onCommandSubmit() {
+      try {
+        await this.$refs.commandForm.validate()
+        await onSendDouyinCommand(this.commandFormData)
+        this.commandDialog = false
+        this.$message.success('抖音口令保存成功')
+        this.getList()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 保存抖音口令
+    async onSaveCommand(row) {
+      this.commandDialog = true
+      this.commandFormData.authId = row.authId
+      this.commandFormData.content = row.dyCommand
+    },
+
+    // 清楚表单信息
+    onCommandClose() {
+      this.commandFormData.authId = ''
+      this.$refs.commandForm.resetFields()
+    },
+
+    // 播放视频
+    onPlayVideo(row) {
+      this.videoDialog = true
+      this.videoUrl = row.ossUrl
+    },
+
+    // 下载视频
+    onDownload(type, row) {
+      if (type === 'single') {
+        this.downloadVideo([row.id + ','])
+      } else {
+        // this.downloadVideo[]
+        const ids = this.selectionList.map((item) => item.id)
+        this.downloadVideo(ids)
+      }
+    },
+
+    // 下载视频
+    async downloadVideo(fileIdList) {
+      try {
+        const filedIds = fileIdList.join(',')
+        const text = await this.$confirm(`确认下载所选视频?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).catch(() => {
+          this.exportClubList = []
+          this.$message.info('已取消操作')
+        })
+        if (text !== 'confirm') return
+        let notification = null
+        notification = this.$notify({
+          title: '提示',
+          message: `正在下载视频,请勿重复操作!`,
+          duration: 0
+        })
+        const downUrl = `${process.env.VUE_APP_BASE_API}/auth/downLoad/chose/zip?fileId=${filedIds}`
+        downloadWithUrl(downUrl, '抖音视频', {
+          headers: {
+            'X-Token': this.$store.getters.token
+          }
+        })
+          .then((res) => {
+            this.getList()
+          })
+          .catch((err) => {
+            console.log(err)
+            this.$message.error(`下载抖音视频失败`)
+          })
+          .finally(() => {
+            notification.close()
+          })
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 删除视频
+    async onRemove(row) {
+      try {
+        await this.$confirm('此操作将永久删除该视频, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        this.removeVideo(row)
+      } catch (error) {
+        console.log(error)
+        this.$message.info('已取消操作')
+      }
+    },
+
+    // 删除视频
+    async removeVideo(row) {
+      try {
+        await removeVideo({ videoId: row.id })
+        this.$message.success('删除视频成功')
+        this.getList()
+      } catch (error) {
+        console.log(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.activity {
+  .filter-control {
+    margin-bottom: 0;
+  }
+}
+
+.cover {
+  width: 80px;
+  display: block;
+}
+
+.el-divider {
+  margin: 12px 0;
+}
+
+.el-dialog {
+  .control {
+    text-align: right;
+  }
+
+  .video-play {
+    display: block;
+    width: 100%;
+    height: 480px;
+    background: #666;
+  }
+
+  .dy-code {
+    width: 160px;
+    height: 160px;
+    display: block;
+    margin: 0 auto;
+  }
+
+  .tip {
+    font-size: 14px;
+    text-align: center;
+    color: #666;
+    padding: 16px 0 8px;
+  }
+}
+</style>

+ 3 - 5
vue.config.js

@@ -32,6 +32,7 @@ module.exports = {
   devServer: {
     port: port,
     open: true,
+    disableHostCheck: true,
     overlay: {
       warnings: false,
       errors: true
@@ -70,10 +71,7 @@ module.exports = {
     config.plugins.delete('prefetch')
 
     // set svg-sprite-loader
-    config.module
-      .rule('svg')
-      .exclude.add(resolve('src/icons'))
-      .end()
+    config.module.rule('svg').exclude.add(resolve('src/icons')).end()
     config.module
       .rule('icons')
       .test(/\.svg$/)
@@ -86,7 +84,7 @@ module.exports = {
       })
       .end()
 
-    config.when(process.env.NODE_ENV !== 'development', config => {
+    config.when(process.env.NODE_ENV !== 'development', (config) => {
       config
         .plugin('ScriptExtHtmlWebpackPlugin')
         .after('html')