daigang 3 жил өмнө
parent
commit
36e2a5ff0f

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "commit": "cz"
   },
   "dependencies": {
+    "app-info-parser": "^1.1.3",
     "axios": "^0.24.0",
     "core-js": "^3.6.5",
     "dom-to-image": "^2.6.0",

+ 3 - 3
src/api/upgrade.js

@@ -60,10 +60,10 @@ export function getVersions (query) {
   })
 }
 
-export function deploy (data) {
-  return confirmAndSend('发布', data.name, {
+export function deployVersion (version) {
+  return confirmAndSend('发布', version.name, {
     url: '/apkUpgradePolicy/add',
     method: 'post',
-    data
+    data: version
   })
 }

+ 0 - 30
src/layout/components/Navbar/UploadDashboard/install.js

@@ -1,30 +0,0 @@
-import Vue from 'vue'
-import Main from './main.vue'
-
-const UploadDashboardConstructor = Vue.extend(Main)
-
-export default {
-  install (Vue) {
-    Vue.prototype.$showUpload = showUpload
-    Vue.prototype.$closeUpload = closeUpload
-  }
-}
-
-let instance
-
-export function showUpload () {
-  if (!instance) {
-    instance = new UploadDashboardConstructor()
-    instance.$mount()
-    document.body.appendChild(instance.$el)
-  }
-  return instance
-}
-
-export function closeUpload () {
-  if (instance) {
-    instance.$destroy()
-    document.body.removeChild(instance.$el)
-    instance = null
-  }
-}

+ 1 - 9
src/layout/components/Sidebar/Item.vue

@@ -20,7 +20,7 @@ export default {
       if (icon.includes('el-icon')) {
         vnodes.push(<i class={[icon, 'sub-el-icon']} />)
       } else {
-        vnodes.push(<svg-icon icon-class={icon}/>)
+        vnodes.push(<svg-icon icon-class={icon} />)
       }
     }
 
@@ -31,11 +31,3 @@ export default {
   }
 }
 </script>
-
-<style scoped>
-.sub-el-icon {
-  color: currentColor;
-  width: 1em;
-  height: 1em;
-}
-</style>

+ 5 - 0
src/layout/components/Sidebar/index.vue

@@ -116,9 +116,14 @@ export default {
 
       .svg-icon,
       .sub-el-icon {
+        color: inherit;
         width: 20px;
         margin-right: 8px;
       }
+
+      .sub-el-icon {
+        height: 22px;
+      }
     }
   }
 }

+ 1 - 2
src/router/index.js

@@ -198,7 +198,6 @@ export const asyncRoutes = [
     ]
   },
   {
-    dev: true,
     path: '/u',
     component: Layout,
     meta: {
@@ -237,7 +236,7 @@ const router = createRouter()
 // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
 export function resetRouter () {
   const newRouter = createRouter()
-  router.matcher = newRouter.matcher // reset router
+  router.matcher = newRouter.matcher
 }
 
 export default router

+ 17 - 0
src/scss/base/_cover.scss

@@ -0,0 +1,17 @@
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track-piece {
+  background-color: #d3dce6;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 6px;
+  background-color: #99a9bf;
+}
+
+.el-message-box__message {
+  word-wrap: break-word;
+}

+ 1 - 15
src/scss/base/_reset.scss

@@ -15,7 +15,7 @@ body {
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
   font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-  background-color: #F4F7FB;
+  background-color: #f4f7fb;
 }
 
 *,
@@ -41,17 +41,3 @@ ul,
 ol {
   list-style: none;
 }
-
-::-webkit-scrollbar {
-  width: 6px;
-  height: 6px;
-}
-
-::-webkit-scrollbar-track-piece {
-  background-color: #d3dce6;
-}
-
-::-webkit-scrollbar-thumb {
-  border-radius: 6px;
-  background-color: #99a9bf;
-}

+ 0 - 0
src/scss/base/_typography.scss


+ 1 - 1
src/scss/base/index.scss

@@ -1,2 +1,2 @@
 @import 'reset';
-@import 'typography';
+@import 'cover';

+ 79 - 245
src/utils/index.js

@@ -1,9 +1,3 @@
-/**
- * Parse the time to string
- * @param {(Object|string|number)} time
- * @param {string} cFormat
- * @returns {string | null}
- */
 export function parseTime (time, cFormat) {
   if (arguments.length === 0 || !time) {
     return null
@@ -47,11 +41,6 @@ export function parseTime (time, cFormat) {
   return time_str
 }
 
-/**
- * @param {number} time
- * @param {string} option
- * @returns {string}
- */
 export function formatTime (time, option) {
   if (('' + time).length === 10) {
     time = parseInt(time) * 1000
@@ -90,264 +79,109 @@ export function formatTime (time, option) {
   }
 }
 
-/**
- * @param {string} url
- * @returns {Object}
- */
-export function getQueryObject (url) {
-  url = url == null ? window.location.href : url
-  const search = url.substring(url.lastIndexOf('?') + 1)
-  const obj = {}
-  const reg = /([^?&=]+)=([^?&=]*)/g
-  search.replace(reg, (rs, $1, $2) => {
-    const name = decodeURIComponent($1)
-    let val = decodeURIComponent($2)
-    val = String(val)
-    obj[name] = val
-    return rs
-  })
-  return obj
-}
-
-/**
- * @param {string} input value
- * @returns {number} output value
- */
-export function byteLength (str) {
-  // returns the byte length of an utf8 string
-  let s = str.length
-  for (var i = str.length - 1; i >= 0; i--) {
-    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--
-  }
-  return s
-}
-
-/**
- * @param {Array} actual
- * @returns {Array}
- */
-export function cleanArray (actual) {
-  const newArray = []
-  for (let i = 0; i < actual.length; i++) {
-    if (actual[i]) {
-      newArray.push(actual[i])
-    }
-  }
-  return newArray
-}
-
-/**
- * @param {Object} json
- * @returns {Array}
- */
-export function param (json) {
-  if (!json) return ''
-  return cleanArray(
-    Object.keys(json).map(key => {
-      if (json[key] === undefined) return ''
-      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
-    })
-  ).join('&')
-}
-
-/**
- * @param {string} url
- * @returns {Object}
- */
-export function param2Obj (url) {
-  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
-  if (!search) {
-    return {}
-  }
-  const obj = {}
-  const searchArr = search.split('&')
-  searchArr.forEach(v => {
-    const index = v.indexOf('=')
-    if (index !== -1) {
-      const name = v.substring(0, index)
-      const val = v.substring(index + 1, v.length)
-      obj[name] = val
-    }
-  })
-  return obj
-}
-
-/**
- * @param {string} val
- * @returns {string}
- */
-export function html2Text (val) {
-  const div = document.createElement('div')
-  div.innerHTML = val
-  return div.textContent || div.innerText
-}
-
-/**
- * Merges two objects, giving the last one precedence
- * @param {Object} target
- * @param {(Object|Array)} source
- * @returns {Object}
- */
-export function objectMerge (target, source) {
-  if (typeof target !== 'object') {
-    target = {}
-  }
-  if (Array.isArray(source)) {
-    return source.slice()
-  }
-  Object.keys(source).forEach(property => {
-    const sourceProperty = source[property]
-    if (typeof sourceProperty === 'object') {
-      target[property] = objectMerge(target[property], sourceProperty)
-    } else {
-      target[property] = sourceProperty
-    }
-  })
-  return target
-}
-
-/**
- * @param {HTMLElement} element
- * @param {string} className
- */
-export function toggleClass (element, className) {
-  if (!element || !className) {
-    return
-  }
-  let classString = element.className
-  const nameIndex = classString.indexOf(className)
-  if (nameIndex === -1) {
-    classString += '' + className
-  } else {
-    classString =
-      classString.substr(0, nameIndex) +
-      classString.substr(nameIndex + className.length)
-  }
-  element.className = classString
-}
-
-/**
- * @param {string} type
- * @returns {Date}
- */
-export function getTime (type) {
-  if (type === 'start') {
-    return new Date().getTime() - 3600 * 1000 * 24 * 90
-  }
-  return new Date()
-}
-
-/**
- * @param {Function} func
- * @param {number} wait
- * @param {boolean} immediate
- * @return {*}
- */
-export function debounce (func, wait, immediate) {
-  let timeout, args, context, timestamp, result
+export function debounce (fn, wait, immediate) {
+  let timeout, context, args, timestamp
 
   const later = function () {
     // 据上一次触发时间间隔
-    const last = +new Date() - timestamp
+    const last = Date.now() - timestamp
 
     // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
-    if (last < wait && last > 0) {
+    if (last < wait) {
       timeout = setTimeout(later, wait - last)
     } else {
-      timeout = null
-      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
       if (!immediate) {
-        result = func.apply(context, args)
-        if (!timeout) context = args = null
+        fn.apply(context, args)
       }
+      timeout = context = args = null
     }
   }
 
-  return function (...args) {
+  return function (...params) {
+    timestamp = Date.now()
     context = this
-    timestamp = +new Date()
-    const callNow = immediate && !timeout
+    args = params
     // 如果延时不存在,重新设定延时
-    if (!timeout) timeout = setTimeout(later, wait)
-    if (callNow) {
-      result = func.apply(context, args)
-      context = args = null
+    if (!timeout) {
+      timeout = setTimeout(later, wait)
+      if (immediate) {
+        fn.apply(context, args)
+        context = params = null
+      }
     }
-
-    return result
   }
 }
 
-/**
- * This is just a simple version of deep copy
- * Has a lot of edge cases bug
- * If you want to use a perfect deep copy, use lodash's _.cloneDeep
- * @param {Object} source
- * @returns {Object}
- */
-export function deepClone (source) {
-  if (!source && typeof source !== 'object') {
-    throw new Error('error arguments', 'deepClone')
+export function throttle (func, wait, options) {
+  let timeout, context, args
+
+  // 上一次执行回调的时间戳
+  let previous = 0
+
+  // 无传入参数时,初始化 options 为空对象
+  if (!options) {
+    options = {}
   }
-  const targetObj = source.constructor === Array ? [] : {}
-  Object.keys(source).forEach(keys => {
-    if (source[keys] && typeof source[keys] === 'object') {
-      targetObj[keys] = deepClone(source[keys])
-    } else {
-      targetObj[keys] = source[keys]
-    }
-  })
-  return targetObj
-}
 
-/**
- * @param {Array} arr
- * @returns {Array}
- */
-export function uniqueArr (arr) {
-  return Array.from(new Set(arr))
-}
+  const later = function () {
+    // 当设置 { leading: false } 时
+    // 每次触发回调函数后设置 previous 为 0
+    // 不然为当前时间
+    previous = options.leading === false ? 0 : Date.now()
 
-/**
- * @returns {string}
- */
-export function createUniqueString () {
-  const timestamp = +new Date() + ''
-  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
-  return (+(randomNum + timestamp)).toString(32)
-}
+    // 执行函数
+    func.apply(context, args)
 
-/**
- * Check if an element has a class
- * @param {HTMLElement} elm
- * @param {string} cls
- * @returns {boolean}
- */
-export function hasClass (ele, cls) {
-  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
-}
+    timeout = context = args = null
+  }
 
-/**
- * Add class to element
- * @param {HTMLElement} elm
- * @param {string} cls
- */
-export function addClass (ele, cls) {
-  if (!hasClass(ele, cls)) ele.className += ' ' + cls
-}
+  // 每次触发事件回调都执行这个函数
+  // 函数内判断是否执行 func
+  // func 才是我们业务层代码想要执行的函数
+  return function (...params) {
+    // 记录当前时间
+    const now = Date.now()
+
+    // 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
+    // 并且设置了 { leading: false }(表示第一次回调不执行)
+    // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
+    if (!previous && options.leading === false) {
+      previous = now
+    }
+
+    // 距离下次触发 func 还需要等待的时间
+    const remaining = wait - (now - previous)
+    context = this
+    args = params
+
+    // 要么是到了间隔时间了,随即触发方法(remaining <= 0)
+    // 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
+    // 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
+    // 之后便会把 previous 值迅速置为 now
+    if (remaining <= 0 || remaining > wait) {
+      if (timeout) {
+        clearTimeout(timeout)
+
+        // clearTimeout(timeout) 并不会把 timeout 设为 null
+        // 手动设置,便于后续判断
+        timeout = null
+      }
 
-/**
- * Remove class from element
- * @param {HTMLElement} elm
- * @param {string} cls
- */
-export function removeClass (ele, cls) {
-  if (hasClass(ele, cls)) {
-    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
-    ele.className = ele.className.replace(reg, ' ')
+      // 设置 previous 为当前时间
+      previous = now
+
+      // 执行 func 函数
+      func.apply(context, args)
+      if (!timeout) {
+        context = args = null
+      }
+    } else if (!timeout && options.trailing !== false) {
+      // 最后一次需要触发的情况
+      // 如果已经存在一个定时器,则不会进入该 if 分支
+      // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
+      // 间隔 remaining milliseconds 后触发 later 方法
+      timeout = setTimeout(later, remaining)
+    }
   }
 }
 

+ 0 - 58
src/utils/validate.js

@@ -6,15 +6,6 @@ export function isExternal (path) {
   return /^(https?:|mailto:|tel:)/.test(path)
 }
 
-/**
- * @param {string} str
- * @returns {Boolean}
- */
-export function validUsername (str) {
-  const valid_map = ['admin', 'editor']
-  return valid_map.indexOf(str.trim()) >= 0
-}
-
 /**
  * @param {string} url
  * @returns {Boolean}
@@ -24,33 +15,6 @@ export function validURL (url) {
   return reg.test(url)
 }
 
-/**
- * @param {string} str
- * @returns {Boolean}
- */
-export function validLowerCase (str) {
-  const reg = /^[a-z]+$/
-  return reg.test(str)
-}
-
-/**
- * @param {string} str
- * @returns {Boolean}
- */
-export function validUpperCase (str) {
-  const reg = /^[A-Z]+$/
-  return reg.test(str)
-}
-
-/**
- * @param {string} str
- * @returns {Boolean}
- */
-export function validAlphabets (str) {
-  const reg = /^[A-Za-z]+$/
-  return reg.test(str)
-}
-
 /**
  * @param {string} email
  * @returns {Boolean}
@@ -59,25 +23,3 @@ export function validEmail (email) {
   const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
   return reg.test(email)
 }
-
-/**
- * @param {string} str
- * @returns {Boolean}
- */
-export function isString (str) {
-  if (typeof str === 'string' || str instanceof String) {
-    return true
-  }
-  return false
-}
-
-/**
- * @param {Array} arg
- * @returns {Boolean}
- */
-export function isArray (arg) {
-  if (typeof Array.isArray === 'undefined') {
-    return Object.prototype.toString.call(arg) === '[object Array]'
-  }
-  return Array.isArray(arg)
-}

+ 2 - 2
src/views/bigscreen/index.vue

@@ -181,7 +181,7 @@ export default {
   data () {
     return {
       statusOptions: [
-        { value: void 0, label: '全部' },
+        { value: void 0, label: '全部状态' },
         { value: State.READY, label: '待提交' },
         { value: State.SUBMITTED, label: '未审核' },
         { value: State.RESOLVED, label: '已审核' }
@@ -319,7 +319,7 @@ export default {
       addProgram(this.program).then(({ data: id }) => {
         this.handleCloseAddDialog()
         const params = this.params
-        if (params.status && params.status !== State.READY) {
+        if (params.status !== void 0 && params.status !== State.READY) {
           params.status = void 0
         }
         if (params.name && !(new RegExp(params.name).test(this.program.name))) {

+ 38 - 8
src/views/upgrade/deploy/index.vue

@@ -14,8 +14,21 @@
             新增
           </button>
         </div>
+        <el-select
+          v-model="options.params.status"
+          class="l-flex__none c-sibling-item"
+          placeholder="请选择状态"
+          @change="search"
+        >
+          <el-option
+            v-for="item in statusOptions"
+            :key="item.label"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
         <search-input
-          v-model.trim="options.params.originalName"
+          v-model.trim="options.params.name"
           class="l-flex__none c-sibling-item"
           placeholder="升级名称"
           @search="search"
@@ -40,7 +53,7 @@
         show-overflow-tooltip
       />
       <el-table-column
-        prop="versionCode"
+        prop="versionName"
         label="目标版本"
         align="center"
       />
@@ -75,7 +88,7 @@
             type="warning"
             size="medium"
           >
-            待更新
+            待升级
           </el-tag>
           <el-tag
             v-if="scope.row.status === 2"
@@ -83,7 +96,7 @@
             type="success"
             size="medium"
           >
-            待更新
+            已升级
           </el-tag>
           <el-tag
             v-if="scope.row.status === 3"
@@ -229,7 +242,7 @@
 import {
   getApks,
   getVersions,
-  deploy
+  deployVersion
 } from '@/api/upgrade'
 import { createListOptions } from '@/utils'
 import DeviceTree from '@/components/DeviceTree'
@@ -241,7 +254,16 @@ export default {
   },
   data () {
     return {
-      options: createListOptions(),
+      statusOptions: [
+        { value: void 0, label: '全部状态' },
+        { value: 1, label: '待升级' },
+        { value: 2, label: '已升级' },
+        { value: 3, label: '已废弃' }
+      ],
+      options: createListOptions({
+        status: void 0,
+        name: ''
+      }),
       show: false,
       version: {},
       choosingApk: false,
@@ -309,11 +331,19 @@ export default {
         })
         return
       }
-      deploy({
+      deployVersion({
         deviceId: this.$devices.map(device => device.id),
         ...this.version
       }).then(() => {
         this.close()
+        const params = this.options.params
+        const name = this.version
+        if (params.status && params.status !== 1) {
+          params.status = void 0
+        }
+        if (params.name && !(new RegExp(params.name).test(name))) {
+          params.name = ''
+        }
         this.search()
       })
     },
@@ -342,7 +372,7 @@ export default {
       })
     },
     chosenApk ({ id, versionName, versionCode }) {
-      this.apk = `${versionCode} ${versionName}`
+      this.apk = `${versionName} ${versionCode}`
       this.version.fileId = id
       this.choosingApk = false
     },

+ 77 - 27
src/views/upgrade/index.vue

@@ -14,6 +14,19 @@
             新增
           </button>
         </div>
+        <el-select
+          v-model="options.params.status"
+          class="l-flex__none c-sibling-item"
+          placeholder="请选择状态"
+          @change="search"
+        >
+          <el-option
+            v-for="item in statusOptions"
+            :key="item.label"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
         <search-input
           v-model.trim="options.params.name"
           class="l-flex__none c-sibling-item"
@@ -43,6 +56,7 @@
         prop="versionCode"
         label="版本号"
         align="center"
+        show-overflow-tooltip
       />
       <el-table-column
         prop="md5"
@@ -159,26 +173,29 @@
           <el-input
             v-model.trim="apk.name"
             class="c-form__item"
-            maxlength="50"
+            maxlength="100"
             show-word-limit
           />
         </div>
-        <div class="c-form__section">
+        <div class="c-form__section extra o-version-name">
           <span class="c-form__label required">版本名称:</span>
           <el-input
             v-model.trim="apk.versionName"
             class="c-form__item"
+            :disabled="!manual"
             maxlength="50"
             show-word-limit
           />
         </div>
-        <div class="c-form__section extra o-version">
-          <span class="c-form__label required">版本号:</span>
-          <el-input
-            v-model.trim="apk.versionCode"
+        <div class="c-form__section extra o-version-code">
+          <span class="c-form__label">版本号:</span>
+          <el-input-number
+            v-model="apk.versionCode"
             class="c-form__item"
-            maxlength="50"
-            show-word-limit
+            :disabled="!manual"
+            :min="1"
+            :max="99999999999999"
+            step-strictly
           />
         </div>
         <div class="c-form__section">
@@ -254,16 +271,24 @@ import {
   createListOptions,
   parseByte
 } from '@/utils'
+import AppInfoParse from 'app-info-parser'
 
 export default {
   name: 'UpgradeApk',
   data () {
     return {
+      statusOptions: [
+        { value: void 0, label: '全部状态' },
+        { value: 1, label: '启用' },
+        { value: 0, label: '禁用' }
+      ],
       options: createListOptions({
+        status: void 0,
         name: ''
       }),
       show: false,
       apk: {},
+      manual: false,
       showProgress: false,
       progress: 0
     }
@@ -301,16 +326,32 @@ export default {
       this.apk = {
         file: null,
         name: '',
-        vesionCode: '',
-        vesionName: '',
+        versionName: '',
+        versionCode: 1,
         remark: '',
         status: 1
       }
+      this.manual = false
       this.show = true
     },
     onChange ({ raw }) {
       this.$refs.upload.clearFiles()
       this.apk.file = raw
+      this.apk.name = raw.name
+      const loading = this.$showLoading()
+      new AppInfoParse(raw).parse().finally(() => {
+        this.$closeLoading(loading)
+      }).then(({ versionName, versionCode }) => {
+        this.manual = false
+        this.apk.versionName = versionName
+        this.apk.versionCode = versionCode
+      }, () => {
+        this.manual = true
+        this.$message({
+          type: 'warning',
+          message: '解析APK版本失败,请手动填写'
+        })
+      })
     },
     toSave () {
       if (!this.apk.file) {
@@ -327,26 +368,28 @@ export default {
         })
         return
       }
-      if (!this.apk.versionName) {
+      if (this.apk.name.length > 100) {
         this.$message({
           type: 'warning',
-          message: '请填写版本名称'
+          message: '文件名过长'
         })
         return
       }
-      if (!this.apk.versionCode) {
-        this.$message({
-          type: 'warning',
-          message: '请填写版本号'
-        })
-        return
-      }
-      if (!/^\d+(\.\d+)*$/.test(this.apk.versionCode)) {
-        this.$message({
-          type: 'warning',
-          message: '版本号格式不正确,例 1.0.0(major[.minor][.patch])'
-        })
-        return
+      if (this.manual) {
+        if (!this.apk.versionName) {
+          this.$message({
+            type: 'warning',
+            message: '请填写版本名称'
+          })
+          return
+        }
+        if (!/^\d+(\.\d+)*$/.test(this.apk.versionName)) {
+          this.$message({
+            type: 'warning',
+            message: '版本名称格式不正确,例 1.0.0(major[.minor][.patch])'
+          })
+          return
+        }
       }
       const formData = new FormData()
       const { file, name, versionName, versionCode, remark, status } = this.apk
@@ -365,7 +408,10 @@ export default {
         this.showProgress = false
       }).then(() => {
         this.close()
-        const params = this.params
+        const params = this.options.params
+        if (params.status !== void 0 && params.status !== status) {
+          params.status = void 0
+        }
         if (params.name && !(new RegExp(params.name).test(name))) {
           params.name = ''
         }
@@ -398,7 +444,11 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.o-version::after {
+.o-version-name::after {
   content: '例:1.0.0(major[.minor][.patch])'
 }
+
+.o-version-code::after {
+  content: '只能由小往大升'
+}
 </style>

+ 123 - 2
yarn.lock

@@ -1831,6 +1831,18 @@ anymatch@^3.0.0, anymatch@~3.1.2:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
+app-info-parser@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.nlark.com/app-info-parser/download/app-info-parser-1.1.3.tgz#d6de5f803d67db33928d433a395c222cbabc8291"
+  integrity sha1-1t5fgD1n2zOSjUM6OVwiLLq8gpE=
+  dependencies:
+    bplist-parser "^0.2.0"
+    bytebuffer "^5.0.1"
+    cgbi-to-png "^1.0.7"
+    commander "^7.2.0"
+    isomorphic-unzip "^1.1.5"
+    plist "^3.0.1"
+
 aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.nlark.com/aproba/download/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -2090,7 +2102,7 @@ base64-js@1.3.1:
   resolved "https://registry.nlark.com/base64-js/download/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha1-WOzoy3XdB+ce0IxzarxfrE2/jfE=
 
-base64-js@^1.0.2, base64-js@^1.3.1:
+base64-js@^1.0.2, base64-js@^1.3.1, base64-js@^1.5.1:
   version "1.5.1"
   resolved "https://registry.nlark.com/base64-js/download/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   integrity sha1-GxtEAWClv3rUC2UPCVljSBkDkwo=
@@ -2130,6 +2142,11 @@ bfj@^6.1.1:
     hoopy "^0.1.4"
     tryer "^1.0.1"
 
+big-integer@^1.6.44:
+  version "1.6.51"
+  resolved "https://registry.npmmirror.com/big-integer/download/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
+  integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
+
 big.js@^3.1.3:
   version "3.2.0"
   resolved "https://registry.nlark.com/big.js/download/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -2214,6 +2231,13 @@ boolbase@^1.0.0, boolbase@~1.0.0:
   resolved "https://registry.nlark.com/boolbase/download/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
   integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
 
+bplist-parser@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/bplist-parser/download/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e"
+  integrity sha1-Q6nRg+W/nVRSAM6sPnEveeu+jQ4=
+  dependencies:
+    big-integer "^1.6.44"
+
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.nlark.com/brace-expansion/download/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -2322,6 +2346,11 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4
     node-releases "^2.0.1"
     picocolors "^1.0.0"
 
+buffer-crc32@~0.2.3:
+  version "0.2.13"
+  resolved "https://registry.nlark.com/buffer-crc32/download/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.nlark.com/buffer-from/download/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -2351,7 +2380,7 @@ buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
-buffer@^5.5.0:
+buffer@^5.0.7, buffer@^5.1.0, buffer@^5.5.0:
   version "5.7.1"
   resolved "https://registry.nlark.com/buffer/download/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
   integrity sha1-umLnwTEzBTWCGXFghRqPZI6Z7tA=
@@ -2359,11 +2388,23 @@ buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
+bufferpack@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.npmmirror.com/bufferpack/download/bufferpack-0.0.6.tgz#fb3d8738a0e1e4e03bcff99f9a75f9ec18a9d73e"
+  integrity sha1-+z2HOKDh5OA7z/mfmnX57Bip1z4=
+
 builtin-status-codes@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npm.taobao.org/builtin-status-codes/download/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
   integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
 
+bytebuffer@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.nlark.com/bytebuffer/download/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd"
+  integrity sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=
+  dependencies:
+    long "~3"
+
 bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.npm.taobao.org/bytes/download/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -2516,6 +2557,16 @@ caseless@~0.12.0:
   resolved "https://registry.npm.taobao.org/caseless/download/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
+cgbi-to-png@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.nlark.com/cgbi-to-png/download/cgbi-to-png-1.0.7.tgz#c7497580f76f87c2f5d825748a9d902b4072c004"
+  integrity sha1-x0l1gPdvh8L12CV0ip2QK0BywAQ=
+  dependencies:
+    bufferpack "0.0.6"
+    crc "^3.3.0"
+    stream-to-buffer "^0.1.0"
+    streamifier "^0.1.1"
+
 chalk@^1.0.0, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.nlark.com/chalk/download/chalk-1.1.3.tgz?cache=0&sync_timestamp=1627646697260&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fchalk%2Fdownload%2Fchalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -3217,6 +3268,13 @@ cosmiconfig@^7.0.0:
     path-type "^4.0.0"
     yaml "^1.10.0"
 
+crc@^3.3.0:
+  version "3.8.0"
+  resolved "https://registry.nlark.com/crc/download/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
+  integrity sha1-rWAmnCyFb4wpnixMwN5FVpFAVsY=
+  dependencies:
+    buffer "^5.1.0"
+
 create-ecdh@^4.0.0:
   version "4.0.4"
   resolved "https://registry.nlark.com/create-ecdh/download/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -4476,6 +4534,13 @@ faye-websocket@^0.11.3:
   dependencies:
     websocket-driver ">=0.5.1"
 
+fd-slicer@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.nlark.com/fd-slicer/download/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
+  integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
+  dependencies:
+    pend "~1.2.0"
+
 figgy-pudding@^3.5.1:
   version "3.5.2"
   resolved "https://registry.nlark.com/figgy-pudding/download/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@@ -6001,6 +6066,14 @@ isobject@^3.0.0, isobject@^3.0.1:
   resolved "https://registry.npm.taobao.org/isobject/download/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
   integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
 
+isomorphic-unzip@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.nlark.com/isomorphic-unzip/download/isomorphic-unzip-1.1.5.tgz#9e5a18e77e3e760b631ee451f643c784b4f880dd"
+  integrity sha1-nloY534+dgtjHuRR9kPHhLT4gN0=
+  dependencies:
+    buffer "^5.0.7"
+    yauzl "^2.8.0"
+
 isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.nlark.com/isstream/download/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -6440,6 +6513,11 @@ loglevel@^1.6.8:
   resolved "https://registry.nlark.com/loglevel/download/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
   integrity sha1-AF/eL15uRwaPk1/yhXPhJe9y8Zc=
 
+long@~3:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/long/download/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
+  integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=
+
 longest@^2.0.1:
   version "2.0.1"
   resolved "https://registry.nlark.com/longest/download/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8"
@@ -7546,6 +7624,11 @@ pbkdf2@^3.0.3:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+pend@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.nlark.com/pend/download/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+  integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
+
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.nlark.com/performance-now/download/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -7621,6 +7704,14 @@ please-upgrade-node@^3.1.1:
   dependencies:
     semver-compare "^1.0.0"
 
+plist@^3.0.1:
+  version "3.0.4"
+  resolved "https://registry.nlark.com/plist/download/plist-3.0.4.tgz#a62df837e3aed2bb3b735899d510c4f186019cbe"
+  integrity sha1-pi34N+Ou0rs7c1iZ1RDE8YYBnL4=
+  dependencies:
+    base64-js "^1.5.1"
+    xmlbuilder "^9.0.7"
+
 pnp-webpack-plugin@^1.6.4:
   version "1.7.0"
   resolved "https://registry.nlark.com/pnp-webpack-plugin/download/pnp-webpack-plugin-1.7.0.tgz?cache=0&sync_timestamp=1626888209947&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fpnp-webpack-plugin%2Fdownload%2Fpnp-webpack-plugin-1.7.0.tgz#65741384f6d8056f36e2255a8d67ffc20866f5c9"
@@ -9203,6 +9294,23 @@ stream-shift@^1.0.0:
   resolved "https://registry.npm.taobao.org/stream-shift/download/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha1-1wiCgVWasneEJCebCHfaPDktWj0=
 
+stream-to-buffer@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.nlark.com/stream-to-buffer/download/stream-to-buffer-0.1.0.tgz#26799d903ab2025c9bd550ac47171b00f8dd80a9"
+  integrity sha1-JnmdkDqyAlyb1VCsRxcbAPjdgKk=
+  dependencies:
+    stream-to "~0.2.0"
+
+stream-to@~0.2.0:
+  version "0.2.2"
+  resolved "https://registry.nlark.com/stream-to/download/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d"
+  integrity sha1-hDBgmNhf25kLn6MAsbPM9V6O8B0=
+
+streamifier@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.nlark.com/streamifier/download/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f"
+  integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=
+
 strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.npm.taobao.org/strict-uri-encode/download/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@@ -10411,6 +10519,11 @@ ws@^7.3.1, ws@^7.5.0:
   resolved "https://registry.npmmirror.com/ws/download/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
   integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
 
+xmlbuilder@^9.0.7:
+  version "9.0.7"
+  resolved "https://registry.nlark.com/xmlbuilder/download/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+  integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
+
 xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.nlark.com/xtend/download/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -10501,6 +10614,14 @@ yargs@^17.2.1:
     y18n "^5.0.5"
     yargs-parser "^20.2.2"
 
+yauzl@^2.8.0:
+  version "2.10.0"
+  resolved "https://registry.nlark.com/yauzl/download/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+  integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
+  dependencies:
+    buffer-crc32 "~0.2.3"
+    fd-slicer "~1.1.0"
+
 yn@3.1.1:
   version "3.1.1"
   resolved "https://registry.nlark.com/yn/download/yn-3.1.1.tgz?cache=0&sync_timestamp=1628974764210&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fyn%2Fdownload%2Fyn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"