傅豪杰 7 mesiacov pred
rodič
commit
c3cfa0dcfb
100 zmenil súbory, kde vykonal 23713 pridanie a 0 odobranie
  1. 22 0
      .gitignore
  2. 5 0
      babel.config.js
  3. 101 0
      element-ui-personal/component/Message/index.js
  4. 110 0
      element-ui-personal/component/Message/main.vue
  5. 5 0
      element-ui-personal/index.js
  6. 1 0
      element-ui-personal/style/index.scss
  7. 58 0
      element-ui-personal/style/message.scss
  8. 9563 0
      package-lock.json
  9. 38 0
      package.json
  10. 10318 0
      pnpm-lock.yaml
  11. BIN
      public/favicon.ico
  12. 46 0
      public/index.html
  13. 200 0
      public/static/echarts/macarons.js
  14. BIN
      public/static/img/dot1.png
  15. BIN
      public/static/img/dot2.png
  16. BIN
      public/static/img/dot3.png
  17. BIN
      public/static/img/dot4.png
  18. BIN
      public/static/img/fileType/doc.png
  19. BIN
      public/static/img/fileType/pdf.png
  20. BIN
      public/static/img/fileType/ppt.png
  21. BIN
      public/static/img/fileType/rar.png
  22. BIN
      public/static/img/fileType/txt.png
  23. BIN
      public/static/img/fileType/xls.png
  24. BIN
      public/static/img/fileType/zip.png
  25. 1 0
      public/static/img/logo.svg
  26. 1 0
      public/static/js/cjpeg.min.js
  27. 34 0
      public/static/js/exceljs.min.js
  28. 33 0
      public/static/js/pngquant.min.js
  29. 1 0
      public/static/json/region-pca.json
  30. 7 0
      public/static/lightgallery/lg-pager.min.js
  31. 7 0
      public/static/lightgallery/lg-thumbnail.min.js
  32. 7 0
      public/static/lightgallery/lg-zoom.min.js
  33. 1 0
      public/static/live2d/live2d.min.js
  34. 80 0
      public/static/live2d/waifu-tips.json
  35. 279 0
      public/static/live2d/waifu.css
  36. 419 0
      public/static/tinymce/langs/zh_CN.js
  37. 11 0
      src/App.vue
  38. 26 0
      src/api/account/index.js
  39. 3 0
      src/api/doc/history.js
  40. 23 0
      src/api/doc/purchase/inbound.js
  41. 23 0
      src/api/doc/purchase/order.js
  42. 23 0
      src/api/doc/sell/order.js
  43. 23 0
      src/api/doc/sell/outbound.js
  44. 11 0
      src/api/file/index.js
  45. 13 0
      src/api/message/manage.js
  46. 7 0
      src/api/message/user.js
  47. 5 0
      src/api/record/index.js
  48. 114 0
      src/api/request.js
  49. 9 0
      src/api/statistic/index.js
  50. 7 0
      src/api/stock/current.js
  51. 11 0
      src/api/system/category.js
  52. 11 0
      src/api/system/customer.js
  53. 9 0
      src/api/system/department.js
  54. 9 0
      src/api/system/resource.js
  55. 11 0
      src/api/system/role.js
  56. 11 0
      src/api/system/supplier.js
  57. 13 0
      src/api/system/user.js
  58. 9 0
      src/asset/icon/index.js
  59. 1 0
      src/asset/icon/svg/bug.svg
  60. 1 0
      src/asset/icon/svg/check.svg
  61. 1 0
      src/asset/icon/svg/develop.svg
  62. 1 0
      src/asset/icon/svg/documentation.svg
  63. 1 0
      src/asset/icon/svg/eye-open.svg
  64. 1 0
      src/asset/icon/svg/eye.svg
  65. 1 0
      src/asset/icon/svg/home.svg
  66. 1 0
      src/asset/icon/svg/icon.svg
  67. 1 0
      src/asset/icon/svg/message.svg
  68. 1 0
      src/asset/icon/svg/money.svg
  69. 1 0
      src/asset/icon/svg/password.svg
  70. 1 0
      src/asset/icon/svg/qq.svg
  71. 1 0
      src/asset/icon/svg/sell.svg
  72. 1 0
      src/asset/icon/svg/shopping.svg
  73. 1 0
      src/asset/icon/svg/show.svg
  74. 1 0
      src/asset/icon/svg/stock.svg
  75. 1 0
      src/asset/icon/svg/system.svg
  76. 1 0
      src/asset/icon/svg/user.svg
  77. 48 0
      src/component/Charts/PieChart.vue
  78. 55 0
      src/component/CollapseCard/index.vue
  79. 51 0
      src/component/Empty/DefaultEmptyImage.vue
  80. 40 0
      src/component/Empty/index.vue
  81. 49 0
      src/component/Empty/style.scss
  82. 39 0
      src/component/ExtraArea/index.vue
  83. 39 0
      src/component/Icon/index.vue
  84. 23 0
      src/component/LinerProgress/index.vue
  85. 38 0
      src/component/LinerProgress/style.scss
  86. 139 0
      src/component/OrgTree/index.vue
  87. 121 0
      src/component/OrgTree/node.vue
  88. 261 0
      src/component/OrgTree/style.scss
  89. 304 0
      src/component/RegionSelector/Tab/index.vue
  90. 139 0
      src/component/RegionSelector/Tab/style.scss
  91. 82 0
      src/component/RegionSelector/Tree/TreeDialog.vue
  92. 79 0
      src/component/RegionSelector/Tree/index.vue
  93. 33 0
      src/component/RegionSelector/index.vue
  94. 11 0
      src/component/RegionSelector/mixin.js
  95. 11 0
      src/component/RegionSelector/store.js
  96. 6 0
      src/component/Skeleton/constant.js
  97. 50 0
      src/component/Skeleton/index.vue
  98. 140 0
      src/component/Skeleton/style.scss
  99. 218 0
      src/component/TreeSelect/index.vue
  100. 0 0
      src/component/Upload/CardList.vue

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+/test
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    require('@vue/cli-plugin-babel/preset')
+  ]
+}

+ 101 - 0
element-ui-personal/component/Message/index.js

@@ -0,0 +1,101 @@
+import Vue from 'vue'
+import Main from './main.vue'
+import {PopupManager} from 'element-ui/lib/utils/popup'
+import {isVNode} from 'element-ui/lib/utils/vdom'
+
+let MessageConstructor = Vue.extend(Main)
+
+let instance
+let instances = []
+let seed = 1
+
+//键:options.type+'|'+options.message,值:message实例
+let groups = {}
+
+const Message = function (options) {
+    options = options || {}
+    if (typeof options === 'string') {
+        options = {message: options}
+    }
+    if (!options.type) {
+        options.type = 'info'
+    }
+    const group = options.type + '|' + options.message
+    const originalMessage = groups[group]
+    //若不是新的message
+    if (originalMessage) {
+        //旧实例的badge+1,并重新计时(如果有的话)
+        originalMessage.badge++
+        originalMessage.clearTimer(true)
+        originalMessage.startTimer()
+        return originalMessage
+    }
+
+    let userOnClose = options.onClose
+    let id = 'message_' + seed++
+
+    options.onClose = function () {
+        Message.close(id, userOnClose)
+    }
+    instance = new MessageConstructor({data: options})
+    instance.id = id
+    instance.group = group
+    if (isVNode(instance.message)) {
+        instance.$slots.default = [instance.message]
+        instance.message = null
+    }
+    instance.$mount()
+    document.body.appendChild(instance.$el)
+    let verticalOffset = options.offset || 20
+    instances.forEach(item => {
+        verticalOffset += item.$el.offsetHeight + 16
+    })
+    instance.verticalOffset = verticalOffset
+    instance.visible = true
+    instance.$el.style.zIndex = PopupManager.nextZIndex()
+    instances.push(instance)
+    groups[group] = instance
+    return instance
+};
+
+['success', 'warning', 'info', 'error'].forEach(type => {
+    Message[type] = options => {
+        if (typeof options === 'string') {
+            options = {message: options}
+        }
+        options.type = type
+        return Message(options)
+    }
+})
+
+Message.close = function (id, userOnClose) {
+    let len = instances.length
+    let index = -1
+    let removedHeight
+    for (let i = 0; i < len; i++) {
+        if (id === instances[i].id) {
+            removedHeight = instances[i].$el.offsetHeight
+            index = i
+            if (typeof userOnClose === 'function') {
+                userOnClose(instances[i])
+            }
+            delete groups[instances[i].group]
+            instances.splice(i, 1)
+            break
+        }
+    }
+    if (len <= 1 || index === -1 || index > instances.length - 1) return
+    for (let i = index; i < len - 1; i++) {
+        let dom = instances[i].$el
+        dom.style.top =
+            parseInt(dom.style.top, 10) - removedHeight - 16 + 'px'
+    }
+}
+
+Message.closeAll = function () {
+    for (let i = instances.length - 1; i >= 0; i--) {
+        instances[i].close()
+    }
+}
+
+export default Message

+ 110 - 0
element-ui-personal/component/Message/main.vue

@@ -0,0 +1,110 @@
+<template>
+    <transition name="el-message-fade" @after-leave="handleAfterLeave">
+        <div
+            v-show="visible"
+            :class="messageClass"
+            :style="positionStyle"
+            role="alert"
+            @mouseenter="clearTimer"
+            @mouseleave="startTimer"
+        >
+            <i v-if="iconClass" :class="iconClass"/>
+            <i v-else :class="typeClass"/>
+            <slot>
+                <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
+                <p v-else v-html="message" class="el-message__content"/>
+            </slot>
+            <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"/>
+            <div v-if="badge>1" :key="'badge'+badge" class="el-message__badge">{{ badge }}</div>
+            <div v-if="progress&&!closed" :key="'progress'+badge" class="el-message__progress" :style="progressStyle"/>
+        </div>
+    </transition>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            visible: false,
+            badge: 1,//标记值,大于1时显示
+            progress: true,//是否显示进度条
+            pause: false,//是否暂停关闭
+            message: '',
+            duration: 3000,
+            startAt: 0,//上一次开始关闭计时的时间
+            remainTime: 0,//还剩多少毫秒关闭
+            type: '',//默认值改为js控制
+            iconClass: '',
+            customClass: '',
+            onClose: null,
+            showClose: false,
+            closed: false,
+            verticalOffset: 20,
+            timer: null,
+            dangerouslyUseHTMLString: false,
+            center: false
+        }
+    },
+
+    computed: {
+        messageClass() {
+            return [
+                'el-message',
+                this.type && !this.iconClass ? `el-message--${this.type}` : '',
+                this.center ? 'is-center' : '',
+                this.showClose ? 'is-closable' : '',
+                this.customClass
+            ]
+        },
+        typeClass() {
+            return this.type && !this.iconClass
+                ? `el-message__icon el-icon-${this.type}`
+                : ''
+        },
+        positionStyle() {
+            return {
+                'top': `${this.verticalOffset}px`
+            }
+        },
+        progressStyle() {
+            if (!this.progress || this.closed) return 'transform: scaleX(0)'
+            if (this.pause) return `transform: scaleX(${this.remainTime / this.duration});animation-play-state:paused`
+            return `animation-duration: ${this.duration}ms;animation-play-state:running`
+        }
+    },
+
+    watch: {
+        closed(newVal) {
+            if (newVal) this.visible = false
+        }
+    },
+
+    methods: {
+        handleAfterLeave() {
+            this.$destroy(true)
+            this.$el.parentNode.removeChild(this.$el)
+        },
+        close() {
+            this.closed = true
+            typeof this.onClose === 'function' && this.onClose(this)
+        },
+        clearTimer(reset) {
+            this.pause = true
+            this.remainTime = reset === true ? this.duration : this.remainTime - (Date.now() - this.startAt)
+            window.clearTimeout(this.timer)
+        },
+        startTimer() {
+            if (this.duration > 0 && !this.closed) {
+                this.pause = false
+                this.startAt = Date.now()
+                if (this.remainTime === 0) this.remainTime = this.duration
+                this.timer = window.setTimeout(() => !this.closed && this.close(), this.remainTime)
+            }
+        }
+    },
+
+    mounted() {
+        this.startTimer()
+    }
+}
+</script>

+ 5 - 0
element-ui-personal/index.js

@@ -0,0 +1,5 @@
+import Message from "./component/Message"
+
+export default function (Vue) {
+    Vue.prototype.$message = Message
+}

+ 1 - 0
element-ui-personal/style/index.scss

@@ -0,0 +1 @@
+@import "./message.scss";

+ 58 - 0
element-ui-personal/style/message.scss

@@ -0,0 +1,58 @@
+.el-message {
+    overflow: visible;
+
+    &__badge {
+        animation: el-message-badge .42s;
+        padding: 4px 8px;
+        position: absolute;
+        box-shadow: 0 1px 3px rgba(#000, .2), 0 1px 1px rgba(#000, .14), 0 2px 1px -1px rgba(#000, .12);
+        background-color: $--color-danger;
+        color: $--color-white;
+        border-radius: 4px;
+        top: -10px;
+        left: -10px;
+        font-size: 12px;
+        line-height: 12px;
+    }
+
+    &__progress {
+        z-index: -1;
+        position: absolute;
+        height: 3px;
+        bottom: 0;
+        left: 10px;
+        right: 10px;
+        animation: el-message-progress linear;
+        background-color: currentColor;
+        opacity: .3;
+        transform-origin: 0 50%;
+        transform: scaleX(0);
+    }
+}
+
+@keyframes el-message-badge {
+    15% {
+        transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg)
+    }
+    30% {
+        transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg)
+    }
+    45% {
+        transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg)
+    }
+    60% {
+        transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg)
+    }
+    75% {
+        transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg)
+    }
+}
+
+@keyframes el-message-progress {
+    0% {
+        transform: scaleX(1)
+    }
+    100% {
+        transform: scaleX(0)
+    }
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 9563 - 0
package-lock.json


+ 38 - 0
package.json

@@ -0,0 +1,38 @@
+{
+  "name": "element-ui-admin-full",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "svgo": "svgo -f src/asset/icon/svg"
+  },
+  "dependencies": {
+    "axios": "^0.27.2",
+    "decimal.js": "^10.3.1",
+    "el-admin-layout": "^0.9.11",
+    "element-ui": "2.15.7",
+    "file-saver": "^2.0.5",
+    "js-base64": "^3.7.2",
+    "js-md5": "0.7.3",
+    "nprogress": "^0.2.0",
+    "pako": "1.0.11",
+    "socket.io-client": "2.3.1",
+    "vue": "2.6.14",
+    "vue-count-to": "^1.0.13",
+    "vue-router": "^3.0.0",
+    "vuex": "^3.0.0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^5.0.6",
+    "@vue/cli-service": "^5.0.6",
+    "sass": "^1.52.3",
+    "sass-loader": "^13.0.0",
+    "svg-sprite-loader": "^6.0.11",
+    "svgo": "^2.8.0",
+    "vue-template-compiler": "2.6.14"
+  },
+  "browserslist": [
+    "Chrome > 80"
+  ]
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 10318 - 0
pnpm-lock.yaml


BIN
public/favicon.ico


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 46 - 0
public/index.html


+ 200 - 0
public/static/echarts/macarons.js

@@ -0,0 +1,200 @@
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['exports', './echarts'], factory)
+    }
+    else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
+        // CommonJS
+        factory(exports, require('./echarts'))
+    }
+    else {
+        // Browser globals
+        factory({}, root.echarts)
+    }
+}(this, function (exports, echarts) {
+    var log = function (msg) {
+        if (typeof console !== 'undefined') {
+            console && console.error && console.error(msg)
+        }
+    }
+    if (!echarts) {
+        log('ECharts is not Loaded')
+        return
+    }
+
+    var colorPalette = [
+        '#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80',
+        '#8d98b3', '#e5cf0d', '#97b552', '#95706d', '#dc69aa',
+        '#07a2a4', '#9a7fd1', '#588dd5', '#f5994e', '#c05050',
+        '#59678c', '#c9ab00', '#7eb00a', '#6f5553', '#c14089'
+    ]
+
+
+    var theme = {
+        color: colorPalette,
+
+        title: {
+            textStyle: {
+                fontWeight: 'normal',
+                color: '#008acd'
+            }
+        },
+
+        visualMap: {
+            itemWidth: 15,
+            color: ['#5ab1ef', '#e0ffff']
+        },
+
+        toolbox: {
+            iconStyle: {
+                normal: {
+                    borderColor: colorPalette[0]
+                }
+            }
+        },
+
+        tooltip: {
+            backgroundColor: 'rgba(50,50,50,0.5)',
+            axisPointer: {
+                type: 'line',
+                lineStyle: {
+                    color: '#008acd'
+                },
+                crossStyle: {
+                    color: '#008acd'
+                },
+                shadowStyle: {
+                    color: 'rgba(200,200,200,0.2)'
+                }
+            }
+        },
+
+        dataZoom: {
+            dataBackgroundColor: '#efefff',
+            fillerColor: 'rgba(182,162,222,0.2)',
+            handleColor: '#008acd'
+        },
+
+        grid: {
+            borderColor: '#eee'
+        },
+
+        categoryAxis: {
+            axisLine: {
+                lineStyle: {
+                    color: '#008acd'
+                }
+            },
+            splitLine: {
+                lineStyle: {
+                    color: ['#eee']
+                }
+            }
+        },
+
+        valueAxis: {
+            axisLine: {
+                lineStyle: {
+                    color: '#008acd'
+                }
+            },
+            splitArea: {
+                show: true,
+                areaStyle: {
+                    color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
+                }
+            },
+            splitLine: {
+                lineStyle: {
+                    color: ['#eee']
+                }
+            }
+        },
+
+        timeline: {
+            lineStyle: {
+                color: '#008acd'
+            },
+            controlStyle: {
+                normal: {color: '#008acd'},
+                emphasis: {color: '#008acd'}
+            },
+            symbol: 'emptyCircle',
+            symbolSize: 3
+        },
+
+        line: {
+            smooth: true,
+            symbol: 'emptyCircle',
+            symbolSize: 3
+        },
+
+        candlestick: {
+            itemStyle: {
+                normal: {
+                    color: '#d87a80',
+                    color0: '#2ec7c9',
+                    lineStyle: {
+                        color: '#d87a80',
+                        color0: '#2ec7c9'
+                    }
+                }
+            }
+        },
+
+        scatter: {
+            symbol: 'circle',
+            symbolSize: 4
+        },
+
+        map: {
+            label: {
+                normal: {
+                    textStyle: {
+                        color: '#d87a80'
+                    }
+                }
+            },
+            itemStyle: {
+                normal: {
+                    borderColor: '#eee',
+                    areaColor: '#ddd'
+                },
+                emphasis: {
+                    areaColor: '#fe994e'
+                }
+            }
+        },
+
+        graph: {
+            color: colorPalette
+        },
+
+        gauge: {
+            axisLine: {
+                lineStyle: {
+                    color: [[0.2, '#2ec7c9'], [0.8, '#5ab1ef'], [1, '#d87a80']],
+                    width: 10
+                }
+            },
+            axisTick: {
+                splitNumber: 10,
+                length: 15,
+                lineStyle: {
+                    color: 'auto'
+                }
+            },
+            splitLine: {
+                length: 22,
+                lineStyle: {
+                    color: 'auto'
+                }
+            },
+            pointer: {
+                width: 5
+            }
+        }
+    }
+
+    echarts.registerTheme('macarons', theme)
+}))

BIN
public/static/img/dot1.png


BIN
public/static/img/dot2.png


BIN
public/static/img/dot3.png


BIN
public/static/img/dot4.png


BIN
public/static/img/fileType/doc.png


BIN
public/static/img/fileType/pdf.png


BIN
public/static/img/fileType/ppt.png


BIN
public/static/img/fileType/rar.png


BIN
public/static/img/fileType/txt.png


BIN
public/static/img/fileType/xls.png


BIN
public/static/img/fileType/zip.png


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
public/static/img/logo.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
public/static/js/cjpeg.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 34 - 0
public/static/js/exceljs.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 33 - 0
public/static/js/pngquant.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
public/static/json/region-pca.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 7 - 0
public/static/lightgallery/lg-pager.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 7 - 0
public/static/lightgallery/lg-thumbnail.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 7 - 0
public/static/lightgallery/lg-zoom.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
public/static/live2d/live2d.min.js


+ 80 - 0
public/static/live2d/waifu-tips.json

@@ -0,0 +1,80 @@
+{
+  "mouseover": [
+    {
+      "selector": "#waifu #live2d",
+      "text": [
+        "干嘛呢你,快把手拿开~~",
+        "鼠…鼠标放错地方了!",
+        "你要干嘛呀?",
+        "喵喵喵?",
+        "怕怕(ノ≧∇≦)ノ",
+        "非礼呀!救命!",
+        "这样的话,只能使用武力了!",
+        "我要生气了哦",
+        "不要动手动脚的!",
+        "真…真的是不知羞耻!",
+        "Hentai!"
+      ]
+    },
+    {
+      "selector": "#waifu-tool el-icon-s-promotion",
+      "text": [
+        "要不要来玩飞机大战?",
+        "这个按钮上写着“不要点击”。",
+        "怎么,你想来和我玩个游戏?",
+        "听说这样可以蹦迪!"
+      ]
+    },
+    {
+      "selector": "#waifu-tool .el-icon-refresh",
+      "text": [
+        "你是不是不爱人家了呀,呜呜呜~",
+        "要见见我的姐姐嘛?",
+        "想要看我妹妹嘛?",
+        "要切换看板娘吗?"
+      ]
+    },
+    {
+      "selector": "#waifu-tool .el-icon-s-operation",
+      "text": [
+        "喜欢换装 PLAY 吗?",
+        "这次要扮演什么呢?",
+        "变装!",
+        "让我们看看接下来会发生什么!"
+      ]
+    },
+    {
+      "selector": "#waifu-tool .el-icon-camera",
+      "text": [
+        "你要给我拍照呀?一二三~茄子~",
+        "要不,我们来合影吧!",
+        "保持微笑就好了~"
+      ]
+    },
+    {
+      "selector": "#waifu-tool .el-icon-switch-button",
+      "text": [
+        "到了要说再见的时候了吗?",
+        "呜呜 QAQ 后会有期……",
+        "不要抛弃我呀……",
+        "我们,还能再见面吗……",
+        "哼,你会后悔的!"
+      ]
+    }
+  ],
+  "click": [
+    {
+      "selector": "#waifu #live2d",
+      "text": [
+        "是…是不小心碰到了吧…",
+        "萝莉控是什么呀?",
+        "你看到我的小熊了吗?",
+        "再摸的话我可要报警了!⌇●﹏●⌇",
+        "110 吗,这里有个变态一直在摸我(ó﹏ò。)",
+        "不要摸我了,我会告诉老婆来打你的!",
+        "干嘛动我呀!小心我咬你!",
+        "别摸我,有什么好摸的!"
+      ]
+    }
+  ]
+}

+ 279 - 0
public/static/live2d/waifu.css

@@ -0,0 +1,279 @@
+#waifu {
+    bottom: -1000px;
+    right: 20px;
+    line-height: 0;
+    position: fixed;
+    transform: translateY(3px);
+    transition: transform .3s ease-in-out, bottom 3s ease-in-out;
+    z-index: 1;
+}
+
+#waifu:hover {
+    transform: translateY(0);
+}
+
+#waifu-tips {
+    animation: shake 50s ease-in-out 5s infinite;
+    background-color: rgba(236, 217, 188, .5);
+    border: 1px solid rgba(224, 186, 140, .62);
+    border-radius: 12px;
+    box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
+    font-size: 14px;
+    line-height: 24px;
+    margin: -30px 20px;
+    min-height: 70px;
+    opacity: 0;
+    overflow: hidden;
+    padding: 5px 10px;
+    position: absolute;
+    text-overflow: ellipsis;
+    transition: opacity 1s;
+    width: 250px;
+    word-break: break-all;
+}
+
+#waifu-tips.waifu-tips-active {
+    opacity: 1;
+    transition: opacity .2s;
+}
+
+#waifu-tips span {
+    color: #0099cc;
+}
+
+#waifu #live2d {
+    cursor: grab;
+    position: relative;
+}
+
+#waifu #live2d:active {
+    cursor: grabbing;
+}
+
+#waifu-tool {
+    color: #aaa;
+    opacity: 0;
+    position: absolute;
+    right: -10px;
+    top: 70px;
+    transition: opacity 1s;
+}
+
+#waifu:hover #waifu-tool {
+    opacity: 1;
+}
+
+#waifu-tool span {
+    color: #5b6c7d;
+    cursor: pointer;
+    display: block;
+    line-height: 30px;
+    text-align: center;
+    transition: color .3s;
+}
+
+#waifu-tool span:hover {
+    color: #0684bd; /* #34495e */
+}
+
+@keyframes shake {
+    2% {
+        transform: translate(.5px, -1.5px) rotate(-.5deg);
+    }
+
+    4% {
+        transform: translate(.5px, 1.5px) rotate(1.5deg);
+    }
+
+    6% {
+        transform: translate(1.5px, 1.5px) rotate(1.5deg);
+    }
+
+    8% {
+        transform: translate(2.5px, 1.5px) rotate(.5deg);
+    }
+
+    10% {
+        transform: translate(.5px, 2.5px) rotate(.5deg);
+    }
+
+    12% {
+        transform: translate(1.5px, 1.5px) rotate(.5deg);
+    }
+
+    14% {
+        transform: translate(.5px, .5px) rotate(.5deg);
+    }
+
+    16% {
+        transform: translate(-1.5px, -.5px) rotate(1.5deg);
+    }
+
+    18% {
+        transform: translate(.5px, .5px) rotate(1.5deg);
+    }
+
+    20% {
+        transform: translate(2.5px, 2.5px) rotate(1.5deg);
+    }
+
+    22% {
+        transform: translate(.5px, -1.5px) rotate(1.5deg);
+    }
+
+    24% {
+        transform: translate(-1.5px, 1.5px) rotate(-.5deg);
+    }
+
+    26% {
+        transform: translate(1.5px, .5px) rotate(1.5deg);
+    }
+
+    28% {
+        transform: translate(-.5px, -.5px) rotate(-.5deg);
+    }
+
+    30% {
+        transform: translate(1.5px, -.5px) rotate(-.5deg);
+    }
+
+    32% {
+        transform: translate(2.5px, -1.5px) rotate(1.5deg);
+    }
+
+    34% {
+        transform: translate(2.5px, 2.5px) rotate(-.5deg);
+    }
+
+    36% {
+        transform: translate(.5px, -1.5px) rotate(.5deg);
+    }
+
+    38% {
+        transform: translate(2.5px, -.5px) rotate(-.5deg);
+    }
+
+    40% {
+        transform: translate(-.5px, 2.5px) rotate(.5deg);
+    }
+
+    42% {
+        transform: translate(-1.5px, 2.5px) rotate(.5deg);
+    }
+
+    44% {
+        transform: translate(-1.5px, 1.5px) rotate(.5deg);
+    }
+
+    46% {
+        transform: translate(1.5px, -.5px) rotate(-.5deg);
+    }
+
+    48% {
+        transform: translate(2.5px, -.5px) rotate(.5deg);
+    }
+
+    50% {
+        transform: translate(-1.5px, 1.5px) rotate(.5deg);
+    }
+
+    52% {
+        transform: translate(-.5px, 1.5px) rotate(.5deg);
+    }
+
+    54% {
+        transform: translate(-1.5px, 1.5px) rotate(.5deg);
+    }
+
+    56% {
+        transform: translate(.5px, 2.5px) rotate(1.5deg);
+    }
+
+    58% {
+        transform: translate(2.5px, 2.5px) rotate(.5deg);
+    }
+
+    60% {
+        transform: translate(2.5px, -1.5px) rotate(1.5deg);
+    }
+
+    62% {
+        transform: translate(-1.5px, .5px) rotate(1.5deg);
+    }
+
+    64% {
+        transform: translate(-1.5px, 1.5px) rotate(1.5deg);
+    }
+
+    66% {
+        transform: translate(.5px, 2.5px) rotate(1.5deg);
+    }
+
+    68% {
+        transform: translate(2.5px, -1.5px) rotate(1.5deg);
+    }
+
+    70% {
+        transform: translate(2.5px, 2.5px) rotate(.5deg);
+    }
+
+    72% {
+        transform: translate(-.5px, -1.5px) rotate(1.5deg);
+    }
+
+    74% {
+        transform: translate(-1.5px, 2.5px) rotate(1.5deg);
+    }
+
+    76% {
+        transform: translate(-1.5px, 2.5px) rotate(1.5deg);
+    }
+
+    78% {
+        transform: translate(-1.5px, 2.5px) rotate(.5deg);
+    }
+
+    80% {
+        transform: translate(-1.5px, .5px) rotate(-.5deg);
+    }
+
+    82% {
+        transform: translate(-1.5px, .5px) rotate(-.5deg);
+    }
+
+    84% {
+        transform: translate(-.5px, .5px) rotate(1.5deg);
+    }
+
+    86% {
+        transform: translate(2.5px, 1.5px) rotate(.5deg);
+    }
+
+    88% {
+        transform: translate(-1.5px, .5px) rotate(1.5deg);
+    }
+
+    90% {
+        transform: translate(-1.5px, -.5px) rotate(-.5deg);
+    }
+
+    92% {
+        transform: translate(-1.5px, -1.5px) rotate(1.5deg);
+    }
+
+    94% {
+        transform: translate(.5px, .5px) rotate(-.5deg);
+    }
+
+    96% {
+        transform: translate(2.5px, -.5px) rotate(-.5deg);
+    }
+
+    98% {
+        transform: translate(-1.5px, -1.5px) rotate(-.5deg);
+    }
+
+    0%, 100% {
+        transform: translate(0, 0) rotate(0);
+    }
+}

+ 419 - 0
public/static/tinymce/langs/zh_CN.js

@@ -0,0 +1,419 @@
+tinymce.addI18n('zh_CN',{
+"Redo": "\u91cd\u505a",
+"Undo": "\u64a4\u9500",
+"Cut": "\u526a\u5207",
+"Copy": "\u590d\u5236",
+"Paste": "\u7c98\u8d34",
+"Select all": "\u5168\u9009",
+"New document": "\u65b0\u6587\u4ef6",
+"Ok": "\u786e\u5b9a",
+"Cancel": "\u53d6\u6d88",
+"Visual aids": "\u7f51\u683c\u7ebf",
+"Bold": "\u7c97\u4f53",
+"Italic": "\u659c\u4f53",
+"Underline": "\u4e0b\u5212\u7ebf",
+"Strikethrough": "\u5220\u9664\u7ebf",
+"Superscript": "\u4e0a\u6807",
+"Subscript": "\u4e0b\u6807",
+"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
+"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
+"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
+"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
+"Justify": "\u4e24\u7aef\u5bf9\u9f50",
+"Bullet list": "\u9879\u76ee\u7b26\u53f7",
+"Numbered list": "\u7f16\u53f7\u5217\u8868",
+"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
+"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
+"Close": "\u5173\u95ed",
+"Formats": "\u683c\u5f0f",
+"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
+"Headers": "\u6807\u9898",
+"Header 1": "\u6807\u98981",
+"Header 2": "\u6807\u98982",
+"Header 3": "\u6807\u98983",
+"Header 4": "\u6807\u98984",
+"Header 5": "\u6807\u98985",
+"Header 6": "\u6807\u98986",
+"Headings": "\u6807\u9898",
+"Heading 1": "\u6807\u98981",
+"Heading 2": "\u6807\u98982",
+"Heading 3": "\u6807\u98983",
+"Heading 4": "\u6807\u98984",
+"Heading 5": "\u6807\u98985",
+"Heading 6": "\u6807\u98986",
+"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
+"Div": "Div",
+"Pre": "Pre",
+"Code": "\u4ee3\u7801",
+"Paragraph": "\u6bb5\u843d",
+"Blockquote": "\u5f15\u6587\u533a\u5757",
+"Inline": "\u6587\u672c",
+"Blocks": "\u57fa\u5757",
+"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
+"Fonts": "\u5b57\u4f53",
+"Font Sizes": "\u5b57\u53f7",
+"Class": "\u7c7b\u578b",
+"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
+"OR": "\u6216",
+"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
+"Upload": "\u4e0a\u4f20",
+"Block": "\u5757",
+"Align": "\u5bf9\u9f50",
+"Default": "\u9ed8\u8ba4",
+"Circle": "\u7a7a\u5fc3\u5706",
+"Disc": "\u5b9e\u5fc3\u5706",
+"Square": "\u65b9\u5757",
+"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
+"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
+"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
+"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Anchor...": "\u951a\u70b9...",
+"Name": "\u540d\u79f0",
+"Id": "\u6807\u8bc6\u7b26",
+"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
+"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
+"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
+"Special character...": "\u7279\u6b8a\u5b57\u7b26...",
+"Source code": "\u6e90\u4ee3\u7801",
+"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
+"Language": "\u8bed\u8a00",
+"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
+"Color Picker": "\u9009\u8272\u5668",
+"R": "R",
+"G": "G",
+"B": "B",
+"Left to right": "\u4ece\u5de6\u5230\u53f3",
+"Right to left": "\u4ece\u53f3\u5230\u5de6",
+"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
+"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
+"Title": "\u6807\u9898",
+"Keywords": "\u5173\u952e\u8bcd",
+"Description": "\u63cf\u8ff0",
+"Robots": "\u673a\u5668\u4eba",
+"Author": "\u4f5c\u8005",
+"Encoding": "\u7f16\u7801",
+"Fullscreen": "\u5168\u5c4f",
+"Action": "\u64cd\u4f5c",
+"Shortcut": "\u5feb\u6377\u952e",
+"Help": "\u5e2e\u52a9",
+"Address": "\u5730\u5740",
+"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
+"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
+"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
+"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
+"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
+"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
+"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
+"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
+"Plugins": "\u63d2\u4ef6",
+"Handy Shortcuts": "\u5feb\u6377\u952e",
+"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
+"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
+"Image description": "\u56fe\u7247\u63cf\u8ff0",
+"Source": "\u5730\u5740",
+"Dimensions": "\u5927\u5c0f",
+"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
+"General": "\u666e\u901a",
+"Advanced": "\u9ad8\u7ea7",
+"Style": "\u6837\u5f0f",
+"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
+"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
+"Border": "\u8fb9\u6846",
+"Insert image": "\u63d2\u5165\u56fe\u7247",
+"Image...": "\u56fe\u7247...",
+"Image list": "\u56fe\u7247\u5217\u8868",
+"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
+"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
+"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
+"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
+"Edit image": "\u7f16\u8f91\u56fe\u7247",
+"Image options": "\u56fe\u7247\u9009\u9879",
+"Zoom in": "\u653e\u5927",
+"Zoom out": "\u7f29\u5c0f",
+"Crop": "\u88c1\u526a",
+"Resize": "\u8c03\u6574\u5927\u5c0f",
+"Orientation": "\u65b9\u5411",
+"Brightness": "\u4eae\u5ea6",
+"Sharpen": "\u9510\u5316",
+"Contrast": "\u5bf9\u6bd4\u5ea6",
+"Color levels": "\u989c\u8272\u5c42\u6b21",
+"Gamma": "\u4f3d\u9a6c\u503c",
+"Invert": "\u53cd\u8f6c",
+"Apply": "\u5e94\u7528",
+"Back": "\u540e\u9000",
+"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
+"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
+"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Text to display": "\u663e\u793a\u6587\u5b57",
+"Url": "\u5730\u5740",
+"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
+"Current window": "\u5f53\u524d\u7a97\u53e3",
+"None": "\u65e0",
+"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
+"Remove link": "\u5220\u9664\u94fe\u63a5",
+"Anchors": "\u951a\u70b9",
+"Link...": "\u94fe\u63a5...",
+"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
+"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
+"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
+"Link list": "\u94fe\u63a5\u5217\u8868",
+"Insert video": "\u63d2\u5165\u89c6\u9891",
+"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
+"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
+"Alternative source": "\u955c\u50cf",
+"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
+"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
+"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
+"Embed": "\u5185\u5d4c",
+"Media...": "\u591a\u5a92\u4f53...",
+"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
+"Page break": "\u5206\u9875\u7b26",
+"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
+"Preview": "\u9884\u89c8",
+"Print...": "\u6253\u5370...",
+"Save": "\u4fdd\u5b58",
+"Find": "\u67e5\u627e",
+"Replace with": "\u66ff\u6362\u4e3a",
+"Replace": "\u66ff\u6362",
+"Replace all": "\u5168\u90e8\u66ff\u6362",
+"Previous": "\u4e0a\u4e00\u4e2a",
+"Next": "\u4e0b\u4e00\u4e2a",
+"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
+"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
+"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
+"Find whole words only": "\u5168\u5b57\u5339\u914d",
+"Spell check": "\u62fc\u5199\u68c0\u67e5",
+"Ignore": "\u5ffd\u7565",
+"Ignore all": "\u5168\u90e8\u5ffd\u7565",
+"Finish": "\u5b8c\u6210",
+"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
+"Insert table": "\u63d2\u5165\u8868\u683c",
+"Table properties": "\u8868\u683c\u5c5e\u6027",
+"Delete table": "\u5220\u9664\u8868\u683c",
+"Cell": "\u5355\u5143\u683c",
+"Row": "\u884c",
+"Column": "\u5217",
+"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
+"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
+"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
+"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
+"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
+"Delete row": "\u5220\u9664\u884c",
+"Row properties": "\u884c\u5c5e\u6027",
+"Cut row": "\u526a\u5207\u884c",
+"Copy row": "\u590d\u5236\u884c",
+"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
+"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
+"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
+"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
+"Delete column": "\u5220\u9664\u5217",
+"Cols": "\u5217",
+"Rows": "\u884c",
+"Width": "\u5bbd",
+"Height": "\u9ad8",
+"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
+"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
+"Show caption": "\u663e\u793a\u6807\u9898",
+"Left": "\u5de6\u5bf9\u9f50",
+"Center": "\u5c45\u4e2d",
+"Right": "\u53f3\u5bf9\u9f50",
+"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
+"Scope": "\u8303\u56f4",
+"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
+"H Align": "\u6c34\u5e73\u5bf9\u9f50",
+"V Align": "\u5782\u76f4\u5bf9\u9f50",
+"Top": "\u9876\u90e8\u5bf9\u9f50",
+"Middle": "\u5782\u76f4\u5c45\u4e2d",
+"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
+"Header cell": "\u8868\u5934\u5355\u5143\u683c",
+"Row group": "\u884c\u7ec4",
+"Column group": "\u5217\u7ec4",
+"Row type": "\u884c\u7c7b\u578b",
+"Header": "\u8868\u5934",
+"Body": "\u8868\u4f53",
+"Footer": "\u8868\u5c3e",
+"Border color": "\u8fb9\u6846\u989c\u8272",
+"Insert template...": "\u63d2\u5165\u6a21\u677f...",
+"Templates": "\u6a21\u677f",
+"Template": "\u6a21\u677f",
+"Text color": "\u6587\u5b57\u989c\u8272",
+"Background color": "\u80cc\u666f\u8272",
+"Custom...": "\u81ea\u5b9a\u4e49...",
+"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
+"No color": "\u65e0",
+"Remove color": "\u79fb\u9664\u989c\u8272",
+"Table of Contents": "\u5185\u5bb9\u5217\u8868",
+"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
+"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
+"Word count": "\u5b57\u6570",
+"Count": "\u8ba1\u6570",
+"Document": "\u6587\u6863",
+"Selection": "\u9009\u62e9",
+"Words": "\u5355\u8bcd",
+"Words: {0}": "\u5b57\u6570\uff1a{0}",
+"{0} words": "{0} \u5b57",
+"File": "\u6587\u4ef6",
+"Edit": "\u7f16\u8f91",
+"Insert": "\u63d2\u5165",
+"View": "\u89c6\u56fe",
+"Format": "\u683c\u5f0f",
+"Table": "\u8868\u683c",
+"Tools": "\u5de5\u5177",
+"Powered by {0}": "\u7531{0}\u9a71\u52a8",
+"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
+"Image title": "\u56fe\u7247\u6807\u9898",
+"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
+"Border style": "\u8fb9\u6846\u6837\u5f0f",
+"Error": "\u9519\u8bef",
+"Warn": "\u8b66\u544a",
+"Valid": "\u6709\u6548",
+"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
+"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
+"System Font": "\u7cfb\u7edf\u5b57\u4f53",
+"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
+"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
+"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
+"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
+"example": "\u793a\u4f8b",
+"Search": "\u641c\u7d22",
+"All": "\u5168\u90e8",
+"Currency": "\u8d27\u5e01",
+"Text": "\u6587\u5b57",
+"Quotations": "\u5f15\u7528",
+"Mathematical": "\u6570\u5b66",
+"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
+"Symbols": "\u7b26\u53f7",
+"Arrows": "\u7bad\u5934",
+"User Defined": "\u81ea\u5b9a\u4e49",
+"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
+"currency sign": "\u8d27\u5e01\u7b26\u53f7",
+"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
+"colon sign": "\u5192\u53f7",
+"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
+"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
+"lira sign": "\u91cc\u62c9\u7b26\u53f7",
+"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
+"naira sign": "\u5948\u62c9\u7b26\u53f7",
+"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
+"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
+"won sign": "\u97e9\u5143\u7b26\u53f7",
+"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
+"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
+"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
+"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
+"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
+"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
+"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
+"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
+"austral sign": "\u6fb3\u5143\u7b26\u53f7",
+"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
+"cedi sign": "\u585e\u5730\u7b26\u53f7",
+"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
+"spesmilo sign": "spesmilo\u7b26\u53f7",
+"tenge sign": "\u575a\u6208\u7b26\u53f7",
+"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
+"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
+"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
+"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
+"ruble sign": "\u5362\u5e03\u7b26\u53f7",
+"yen character": "\u65e5\u5143\u5b57\u6837",
+"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
+"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
+"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
+"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
+"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
+"People": "\u4eba\u7c7b",
+"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
+"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
+"Activity": "\u6d3b\u52a8",
+"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
+"Objects": "\u7269\u4ef6",
+"Flags": "\u65d7\u5e1c",
+"Characters": "\u5b57\u7b26",
+"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
+"{0} characters": "{0} \u4e2a\u5b57\u7b26",
+"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
+"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
+"Update": "\u66f4\u65b0",
+"Color swatch": "\u989c\u8272\u6837\u672c",
+"Turquoise": "\u9752\u7eff\u8272",
+"Green": "\u7eff\u8272",
+"Blue": "\u84dd\u8272",
+"Purple": "\u7d2b\u8272",
+"Navy Blue": "\u6d77\u519b\u84dd",
+"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
+"Dark Green": "\u6df1\u7eff\u8272",
+"Medium Blue": "\u4e2d\u84dd\u8272",
+"Medium Purple": "\u4e2d\u7d2b\u8272",
+"Midnight Blue": "\u6df1\u84dd\u8272",
+"Yellow": "\u9ec4\u8272",
+"Orange": "\u6a59\u8272",
+"Red": "\u7ea2\u8272",
+"Light Gray": "\u6d45\u7070\u8272",
+"Gray": "\u7070\u8272",
+"Dark Yellow": "\u6697\u9ec4\u8272",
+"Dark Orange": "\u6df1\u6a59\u8272",
+"Dark Red": "\u6df1\u7ea2\u8272",
+"Medium Gray": "\u4e2d\u7070\u8272",
+"Dark Gray": "\u6df1\u7070\u8272",
+"Light Green": "\u6d45\u7eff\u8272",
+"Light Yellow": "\u6d45\u9ec4\u8272",
+"Light Red": "\u6d45\u7ea2\u8272",
+"Light Purple": "\u6d45\u7d2b\u8272",
+"Light Blue": "\u6d45\u84dd\u8272",
+"Dark Purple": "\u6df1\u7d2b\u8272",
+"Dark Blue": "\u6df1\u84dd\u8272",
+"Black": "\u9ed1\u8272",
+"White": "\u767d\u8272",
+"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
+"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
+"history": "\u5386\u53f2",
+"styles": "\u6837\u5f0f",
+"formatting": "\u683c\u5f0f\u5316",
+"alignment": "\u5bf9\u9f50",
+"indentation": "\u7f29\u8fdb",
+"permanent pen": "\u8bb0\u53f7\u7b14",
+"comments": "\u5907\u6ce8",
+"Format Painter": "\u683c\u5f0f\u5237",
+"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6",
+"Capitalization": "\u5927\u5199",
+"lowercase": "\u5c0f\u5199",
+"UPPERCASE": "\u5927\u5199",
+"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199",
+"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027",
+"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...",
+"Font": "\u5b57\u4f53",
+"Size": "\u5b57\u53f7",
+"More...": "\u66f4\u591a...",
+"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00",
+"Select...": "\u9009\u62e9...",
+"Preferences": "\u9996\u9009\u9879",
+"Yes": "\u662f",
+"No": "\u5426",
+"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15",
+"Version": "\u7248\u672c",
+"Anchor": "\u951a\u70b9",
+"Special character": "\u7279\u6b8a\u7b26\u53f7",
+"Code sample": "\u4ee3\u7801\u793a\u4f8b",
+"Color": "\u989c\u8272",
+"Emoticons": "\u8868\u60c5",
+"Document properties": "\u6587\u6863\u5c5e\u6027",
+"Image": "\u56fe\u7247",
+"Insert link": "\u63d2\u5165\u94fe\u63a5",
+"Target": "\u6253\u5f00\u65b9\u5f0f",
+"Link": "\u94fe\u63a5",
+"Poster": "\u5c01\u9762",
+"Media": "\u5a92\u4f53",
+"Print": "\u6253\u5370",
+"Prev": "\u4e0a\u4e00\u4e2a",
+"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
+"Whole words": "\u5168\u5b57\u5339\u914d",
+"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
+"Caption": "\u6807\u9898",
+"Insert template": "\u63d2\u5165\u6a21\u677f"
+});

+ 11 - 0
src/App.vue

@@ -0,0 +1,11 @@
+<template>
+    <div id="app">
+        <router-view/>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'App'
+}
+</script>

+ 26 - 0
src/api/account/index.js

@@ -0,0 +1,26 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const login = new PostApi('/account/login')
+
+export const logout = new GetApi('/account/logout')
+
+export const register = new PostApi('/account/register')
+
+export const updateUserPwd = new PostApi('/account/updatePwd')
+
+export const updateAvatar = new GetApi(
+    '/account/updateAvatar',
+    key => ({params: {key: encodeURIComponent(key)}})
+)
+
+export const validate = new GetApi('/account/validate', pwd => ({params: {pwd}}))
+
+export const checkLoginName = new GetApi(
+    '/account/checkLoginName',
+    (name, id) => ({params: {name, id}})
+)
+
+export const checkNickName = new GetApi(
+    '/account/checkNickName',
+    (name, id) => ({params: {name, id}})
+)

+ 3 - 0
src/api/doc/history.js

@@ -0,0 +1,3 @@
+import {PostApi} from "@/api/request"
+
+export const search = new PostApi('/doc/history/search')

+ 23 - 0
src/api/doc/purchase/inbound.js

@@ -0,0 +1,23 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getById = new GetApi('/doc/purchase/inbound/getById', id => ({params: {id}}))
+
+export const getSubById = new GetApi('/doc/purchase/inbound/getSubById', id => ({params: {id}}))
+
+export const search = new PostApi('/doc/purchase/inbound/search')
+
+export const exportExcel = new PostApi('/doc/purchase/inbound/export')
+
+export const add = new PostApi('/doc/purchase/inbound/add')
+
+export const update = new PostApi('/doc/purchase/inbound/update')
+
+export const commit = new PostApi('/doc/purchase/inbound/commit')
+
+export const withdraw = new PostApi('/doc/purchase/inbound/withdraw')
+
+export const pass = new PostApi('/doc/purchase/inbound/pass')
+
+export const reject = new PostApi('/doc/purchase/inbound/reject')
+
+export const del = new GetApi('/doc/purchase/inbound/del', id => ({params: {id}}))

+ 23 - 0
src/api/doc/purchase/order.js

@@ -0,0 +1,23 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getById = new GetApi('/doc/purchase/order/getById', id => ({params: {id}}))
+
+export const getSubById = new GetApi('/doc/purchase/order/getSubById', id => ({params: {id}}))
+
+export const search = new PostApi('/doc/purchase/order/search')
+
+export const exportExcel = new PostApi('/doc/purchase/order/export')
+
+export const add = new PostApi('/doc/purchase/order/add')
+
+export const update = new PostApi('/doc/purchase/order/update')
+
+export const commit = new PostApi('/doc/purchase/order/commit')
+
+export const withdraw = new PostApi('/doc/purchase/order/withdraw')
+
+export const pass = new PostApi('/doc/purchase/order/pass')
+
+export const reject = new PostApi('/doc/purchase/order/reject')
+
+export const del = new GetApi('/doc/purchase/order/del', id => ({params: {id}}))

+ 23 - 0
src/api/doc/sell/order.js

@@ -0,0 +1,23 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getById = new GetApi('/doc/sell/order/getById', id => ({params: {id}}))
+
+export const getSubById = new GetApi('/doc/sell/order/getSubById', id => ({params: {id}}))
+
+export const search = new PostApi('/doc/sell/order/search')
+
+export const exportExcel = new PostApi('/doc/sell/order/export')
+
+export const add = new PostApi('/doc/sell/order/add')
+
+export const update = new PostApi('/doc/sell/order/update')
+
+export const commit = new PostApi('/doc/sell/order/commit')
+
+export const withdraw = new PostApi('/doc/sell/order/withdraw')
+
+export const pass = new PostApi('/doc/sell/order/pass')
+
+export const reject = new PostApi('/doc/sell/order/reject')
+
+export const del = new GetApi('/doc/sell/order/del', id => ({params: {id}}))

+ 23 - 0
src/api/doc/sell/outbound.js

@@ -0,0 +1,23 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getById = new GetApi('/doc/sell/outbound/getById', id => ({params: {id}}))
+
+export const getSubById = new GetApi('/doc/sell/outbound/getSubById', id => ({params: {id}}))
+
+export const search = new PostApi('/doc/sell/outbound/search')
+
+export const exportExcel = new PostApi('/doc/sell/outbound/export')
+
+export const add = new PostApi('/doc/sell/outbound/add')
+
+export const update = new PostApi('/doc/sell/outbound/update')
+
+export const commit = new PostApi('/doc/sell/outbound/commit')
+
+export const withdraw = new PostApi('/doc/sell/outbound/withdraw')
+
+export const pass = new PostApi('/doc/sell/outbound/pass')
+
+export const reject = new PostApi('/doc/sell/outbound/reject')
+
+export const del = new GetApi('/doc/sell/outbound/del', id => ({params: {id}}))

+ 11 - 0
src/api/file/index.js

@@ -0,0 +1,11 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getToken = new GetApi(
+    '/file/getToken',
+    null,
+    promise => promise.then(({data}) => data)
+)
+
+export const deleteUpload = new GetApi('/file/delete', url => ({params: {url: encodeURIComponent(url)}}))
+
+export const deleteUploadBatch = new PostApi('/file/deleteBatch')

+ 13 - 0
src/api/message/manage.js

@@ -0,0 +1,13 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const search = new PostApi(`/message/manage/search`)
+
+export const add = new PostApi(`/message/manage/add`)
+
+export const update = new PostApi(`/message/manage/update`)
+
+export const publish = new PostApi(`/message/manage/publish`)
+
+export const withdraw = new PostApi(`/message/manage/withdraw`)
+
+export const del = new GetApi(`/message/manage/del`, (id, title) => ({params: {id, title}}))

+ 7 - 0
src/api/message/user.js

@@ -0,0 +1,7 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const search = new PostApi(`/message/user/search`)
+
+export const read = new GetApi(`/message/user/read`, id => ({params: {id}}))
+
+export const readAll = new GetApi(`/message/user/readAll`)

+ 5 - 0
src/api/record/index.js

@@ -0,0 +1,5 @@
+import {PostApi} from "@/api/request"
+
+export const searchLoginHistory = new PostApi(`/record/searchLoginHistory`)
+
+export const searchUserAction = new PostApi(`/record/searchUserAction`)

+ 114 - 0
src/api/request.js

@@ -0,0 +1,114 @@
+import axios from 'axios'
+import {apiPrefix} from '@/config'
+import {MessageBox, Notification} from 'element-ui'
+import Message from '@ele/component/Message'
+import store from '@/store'
+import {isLogin} from '@/util/auth'
+
+const instance = axios.create({
+    baseURL: apiPrefix,
+    // withCredentials: true, // send cookies when cross-domain requests
+    timeout: 60000 // request timeout
+})
+
+instance.interceptors.request.use(
+    config => {
+        //登录状态下socket断连时,除登出外中断一切请求
+        if (isLogin() && !store.state.websocket.online && config.url !== '/account/logout') {
+            Message.error('请等待与服务器重新连接')
+            return Promise.reject('')
+        }
+
+        //header添加token
+        if (store.state.user.token) {
+            config.headers['X-Token'] = store.state.user.token
+        }
+        return config
+    },
+    error => Promise.reject(error)
+)
+
+instance.interceptors.response.use(
+    response => {
+        const res = response.data, {responseType = 'json'} = response.config
+
+        //当返回类型非{status,data,msg}的接口请求时,不使用status来判断请求是否成功
+        if (!('status' in res) || res.status === 200) {
+            //当返回类型为json时,返回response.data
+            return responseType === 'json' ? res : response
+        }
+
+        //服务器异常
+        if (res.status === 500) {
+            Message.error(res.msg || '操作失败')
+            return Promise.reject(res.msg)
+        }
+
+        //未登录
+        if (res.status === 401) {
+            if (store.state.user.prepareLogout) return Promise.reject()
+            return MessageBox.alert('请登录后重试', {
+                type: 'warning',
+                beforeClose: (action, instance, done) => {
+                    store.dispatch('user/logout').then(done)
+                }
+            })
+        }
+
+        //没有权限
+        if (res.status === 403) {
+            Message.error(res.msg || '没有权限进行该操作')
+            return Promise.reject(res.msg)
+        }
+
+        //其他错误
+        Message.error(res.msg || '接口有误')
+        return Promise.reject(res)
+    },
+    error => {
+        if (axios.isCancel(error)) return
+        error && Notification.error({
+            title: '错误',
+            message: '请求错误,请稍后重试'
+        })
+        return Promise.reject(error)
+    }
+)
+
+class Api {
+    /**
+     * 数据接口定义
+     * @param url     请求url,不带参数
+     * @param arg     对传入参数的处理方法,返回值将作为axios[get,post]的第二个参数
+     * @param chain   形参为请求返回的promise
+     * @param method  请求方法,小写,get、post...
+     */
+    constructor(url, arg, chain, method) {
+        this.url = url
+        this.arg = arg
+        this.chain = chain
+        this.method = method
+    }
+
+    request(...args) {
+        const params = this.arg ? this.arg(...args) : undefined
+        const promise = instance[this.method](this.url, params).catch(e => console.error(e))
+        return this.chain ? this.chain(promise) : promise
+    }
+}
+
+export class PostApi extends Api {
+    constructor(url, arg, chain) {
+        if (!arg) arg = data => data
+
+        super(url, arg, chain, 'post')
+    }
+}
+
+export class GetApi extends Api {
+    constructor(url, arg, chain) {
+        super(url, arg, chain, 'get')
+    }
+}
+
+export default instance

+ 9 - 0
src/api/statistic/index.js

@@ -0,0 +1,9 @@
+import {GetApi} from "@/api/request"
+
+export const getFourBlock = new GetApi(`/statistic/getFourBlock`)
+
+export const getDailyProfitStat = new GetApi(`/statistic/getDailyProfitStat`)
+
+export const getDailyFinishOrder = new GetApi(`/statistic/getDailyFinishOrder`)
+
+export const getTotalProfitGoods = new GetApi(`/statistic/getTotalProfitGoods`)

+ 7 - 0
src/api/stock/current.js

@@ -0,0 +1,7 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const search = new PostApi(`/stock/current/search`)
+
+export const getDetail = new GetApi(`/stock/current/getDetail`, cids => ({params: {cids}}))
+
+export const getDetailById = new GetApi(`/stock/current/getDetailById`, ids => ({params: {ids}}))

+ 11 - 0
src/api/system/category.js

@@ -0,0 +1,11 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const search = new PostApi(`/system/category/search`)
+
+export const getAll = new GetApi(`/system/category/getAll`)
+
+export const add = new PostApi(`/system/category/add`)
+
+export const update = new PostApi(`/system/category/update`)
+
+export const del = new PostApi(`/system/category/del`)

+ 11 - 0
src/api/system/customer.js

@@ -0,0 +1,11 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getLimitRegion = new GetApi(`/system/customer/getLimitRegion`)
+
+export const search = new PostApi(`/system/customer/search`)
+
+export const add = new PostApi(`/system/customer/add`)
+
+export const update = new PostApi(`/system/customer/update`)
+
+export const del = new PostApi(`/system/customer/del`)

+ 9 - 0
src/api/system/department.js

@@ -0,0 +1,9 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const get = new GetApi(`/system/department/get`, (all = true) => ({params: {all}}))
+
+export const add = new PostApi(`/system/department/add`)
+
+export const update = new PostApi(`/system/department/update`)
+
+export const del = new PostApi(`/system/department/del`)

+ 9 - 0
src/api/system/resource.js

@@ -0,0 +1,9 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getAll = new GetApi(`/system/resource/getAll`)
+
+export const add = new PostApi(`/system/resource/add`)
+
+export const update = new PostApi(`/system/resource/update`)
+
+export const del = new GetApi(`/system/resource/del`, id => ({params: {id}}))

+ 11 - 0
src/api/system/role.js

@@ -0,0 +1,11 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const search = new PostApi(`/system/role/search`)
+
+export const get = new GetApi(`/system/role/get`)
+
+export const add = new PostApi(`/system/role/add`)
+
+export const update = new PostApi(`/system/role/update`)
+
+export const del = new PostApi(`/system/role/del`)

+ 11 - 0
src/api/system/supplier.js

@@ -0,0 +1,11 @@
+import {GetApi, PostApi} from "@/api/request"
+
+export const getLimitRegion = new GetApi(`/system/supplier/getLimitRegion`)
+
+export const search = new PostApi(`/system/supplier/search`)
+
+export const add = new PostApi(`/system/supplier/add`)
+
+export const update = new PostApi(`/system/supplier/update`)
+
+export const del = new PostApi(`/system/supplier/del`)

+ 13 - 0
src/api/system/user.js

@@ -0,0 +1,13 @@
+import {PostApi} from "@/api/request"
+
+export const search = new PostApi(`/system/user/search`)
+
+export const kick = new PostApi(`/system/user/kick`)
+
+export const add = new PostApi(`/system/user/add`)
+
+export const update = new PostApi(`/system/user/update`)
+
+export const del = new PostApi(`/system/user/del`)
+
+export const resetPwd = new PostApi(`/system/user/resetPwd`)

+ 9 - 0
src/asset/icon/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import VIcon from '@/component/Icon'
+
+Vue.component('v-icon', VIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const keys = req.keys()
+keys.map(req)
+export default keys.map(i => i.match(/\.\/(.*)\.svg/)[1])

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/bug.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/check.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/develop.svg


+ 1 - 0
src/asset/icon/svg/documentation.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/eye-open.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/eye.svg


+ 1 - 0
src/asset/icon/svg/home.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M979.2 473.6H960l-428.8-416c-12.8-12.8-25.6-12.8-38.4 0l-435.2 416H44.8C19.2 473.6 0 492.8 0 518.4v38.4c0 25.6 19.2 44.8 44.8 44.8H160V896c0 25.6 19.2 44.8 44.8 44.8h185.6c25.6 0 44.8-19.2 44.8-44.8V659.2h153.6V896c0 25.6 19.2 44.8 44.8 44.8h185.6c25.6 0 44.8-19.2 44.8-44.8V601.6h115.2c25.6 0 44.8-19.2 44.8-44.8v-38.4c0-25.6-19.2-44.8-44.8-44.8z"/></svg>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/icon.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/message.svg


+ 1 - 0
src/asset/icon/svg/money.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/password.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/qq.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/sell.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/shopping.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/show.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/stock.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/asset/icon/svg/system.svg


+ 1 - 0
src/asset/icon/svg/user.svg

@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

+ 48 - 0
src/component/Charts/PieChart.vue

@@ -0,0 +1,48 @@
+<template>
+    <div :style="{height,width}"/>
+</template>
+
+<script>
+import {logic, resize} from "@/mixin/chart"
+
+export default {
+    mixins: [resize, logic],
+
+    props: {
+        title: String,
+        data: Array
+    },
+
+    watch: {
+        data: {
+            deep: true,
+            handler(val) {
+                this.init(val)
+            }
+        }
+    },
+
+    methods: {
+        init(data) {
+            this.chart && this.chart.setOption({
+                title: {
+                    text: this.title,
+                    left: 'center',
+                    align: 'right'
+                },
+                tooltip: {
+                    trigger: 'item',
+                    formatter: '{b} : {c} ({d}%)'
+                },
+                series: [
+                    {
+                        type: 'pie',
+                        center: ['50%', '58%'],
+                        data
+                    }
+                ]
+            })
+        }
+    }
+}
+</script>

+ 55 - 0
src/component/CollapseCard/index.vue

@@ -0,0 +1,55 @@
+<template>
+    <el-card :class="{collapsed}">
+        <template v-slot:header>
+            <div class="clearfix">
+                <slot name="header">{{ header }}</slot>
+                <i class="el-icon-arrow-up collapse-card-icon"/>
+            </div>
+        </template>
+
+        <slot/>
+    </el-card>
+</template>
+
+<script>
+export default {
+    name: "CollapseCard",
+
+    props: {
+        header: String
+    },
+
+    data: () => ({collapsed: false}),
+
+    methods: {
+        collapse() {
+            this.collapsed = !this.collapsed
+        }
+    },
+
+    mounted() {
+        const dom = this.$el.querySelector('.el-card__header')
+        dom.addEventListener('click', this.collapse)
+        this.$once('hook:beforeDestroy', () => {
+            dom.removeEventListener('click', this.collapse)
+        })
+    }
+}
+</script>
+
+<style>
+.collapse-card-icon {
+    float: right;
+    font-weight: bold;
+    transition: transform .3s;
+    cursor: pointer;
+}
+
+.collapsed .collapse-card-icon {
+    transform: rotate(180deg);
+}
+
+.el-card.collapsed > .el-card__body {
+    display: none;
+}
+</style>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 51 - 0
src/component/Empty/DefaultEmptyImage.vue


+ 40 - 0
src/component/Empty/index.vue

@@ -0,0 +1,40 @@
+<script type="text/jsx">
+/**
+ * 从ant-design-vue处拷贝
+ */
+import DefaultEmptyImage from "./DefaultEmptyImage"
+
+export default {
+    name: "AEmpty",
+
+    functional: true,
+
+    props: {
+        description: String,
+        image: [String, Object],
+        imageStyle: [String, Object]
+    },
+
+    render(h, context) {
+        const {description = '暂无数据', image, imageStyle} = context.props
+        const prefixCls = 'ant-empty'
+        const cls = {[prefixCls]: true}
+
+        let imageNode = null
+        if (typeof image === 'string') {
+            imageNode = <img alt={description} src={image}/>
+        }
+        else imageNode = <DefaultEmptyImage/>
+
+        return (
+            <div class={cls} {...{on: context.listeners}}>
+                <div class={`${prefixCls}-image`} style={imageStyle}>{imageNode}</div>
+                {description && <p class={`${prefixCls}-description`}>{description}</p>}
+                {context.children && <div class={`${prefixCls}-footer`}>{context.children}</div>}
+            </div>
+        )
+    }
+}
+</script>
+
+<style lang="scss" src="./style.scss"></style>

+ 49 - 0
src/component/Empty/style.scss

@@ -0,0 +1,49 @@
+@import "~@/style/var";
+
+.ant-empty {
+    padding: 20px;
+    margin: 0 8px;
+    font-size: 14px;
+    line-height: 22px;
+    text-align: center;
+
+    &-image {
+        height: 100px;
+        margin-bottom: 8px;
+
+        img {
+            height: 100%;
+        }
+
+        svg {
+            height: 100%;
+            margin: auto;
+        }
+    }
+
+    &-description {
+        margin: 0;
+    }
+
+    &-footer {
+        margin-top: 16px;
+    }
+
+    &-normal {
+        margin: 32px 0;
+        color: $--color-text-secondary;
+
+        .ant-empty-image {
+            height: 40px;
+        }
+    }
+
+    &-small {
+        margin: 8px 0;
+        color: $--color-text-secondary;
+
+        .ant-empty-image {
+            height: 35px;
+        }
+    }
+}

+ 39 - 0
src/component/ExtraArea/index.vue

@@ -0,0 +1,39 @@
+<template>
+    <el-row type="flex" justify="space-between">
+        <el-col v-show="!expanded" :span="extraSpan">
+            <slot name="extra"/>
+        </el-col>
+        <div style="display: flex;justify-content: center;align-items: center">
+            <el-link :icon="iconClass" :underline="false" style="font-size: 16px" @click="expanded = !expanded"/>
+        </div>
+        <el-col :span="defaultSpan">
+            <slot/>
+        </el-col>
+    </el-row>
+</template>
+
+<script>
+export default {
+    name: "ExtraArea",
+
+    props: {
+        extra: {type: Number, default: 5}
+    },
+
+    data: () => ({expanded: false}),
+
+    computed: {
+        extraSpan() {
+            return this.expanded ? 0 : this.extra
+        },
+        defaultSpan() {
+            return 23 - this.extraSpan
+        },
+        iconClass() {
+            return `el-icon-arrow-${this.expanded ? 'right' : 'left'}`
+        }
+    }
+}
+</script>
+
+

+ 39 - 0
src/component/Icon/index.vue

@@ -0,0 +1,39 @@
+<script type="text/jsx">
+const SVG_PREFIX = 'svg-'
+
+export default {
+    name: 'VIcon',
+
+    functional: true,
+
+    props: {icon: String},
+
+    render(h, context) {
+        const {data, props: {icon}} = context
+
+        if (!icon) return
+
+        //渲染成svg
+        if (icon.startsWith(SVG_PREFIX)) {
+            return (
+                <svg class="icon svg" aria-hidden="true" {...data}>
+                    <use href={`#icon-${icon.replace(SVG_PREFIX, '')}`}/>
+                </svg>
+            )
+        }
+
+        return <i class={`icon ${icon}`} {...data}/>
+    }
+}
+</script>
+
+<style>
+.icon {
+    width: 1em;
+    height: 1em;
+}
+
+.icon.svg {
+    fill: currentColor;
+}
+</style>

+ 23 - 0
src/component/LinerProgress/index.vue

@@ -0,0 +1,23 @@
+<script type="text/jsx">
+//从muse-ui处搬运,https://muse-ui.org/#/zh-CN/progress
+export default {
+    name: "LinerProgress",
+
+    functional: true,
+
+    props: {show: Boolean},
+
+    render(h, context) {
+        if (context.props.show) {
+            return (
+                <div class="liner-progress">
+                    <div class="liner-progress-background"/>
+                    <div class="linear-progress-indeterminate"/>
+                </div>
+            )
+        }
+    }
+}
+</script>
+
+<style lang="scss" src="./style.scss"></style>

+ 38 - 0
src/component/LinerProgress/style.scss

@@ -0,0 +1,38 @@
+@import "~@/style/var";
+
+.liner-progress {
+    position: relative;
+    height: 4px;
+    display: block;
+    width: 100%;
+    margin: 0;
+    overflow: hidden;
+
+    .liner-progress-background {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        background-color: $--color-primary;
+        opacity: .3;
+    }
+
+    .linear-progress-indeterminate {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        width: 40%;
+        background-color: $--color-primary;
+        animation: linear-progress-animate .84s cubic-bezier(.445, .05, .55, .95) infinite;
+    }
+
+    @keyframes linear-progress-animate {
+        0% {
+            left: -40%;
+        }
+        100% {
+            left: 100%;
+        }
+    }
+}

+ 139 - 0
src/component/OrgTree/index.vue

@@ -0,0 +1,139 @@
+<template>
+    <div class="org-tree-container">
+        <div class="org-tree" :class="{horizontal, collapsable}">
+            <org-tree-node
+                :data="dataCloned"
+                :props="props"
+                :horizontal="horizontal"
+                :label-width="labelWidth"
+                :collapsable="collapsable"
+                :label-class-name="labelClassName"
+                @on-expand="handleExpand"
+                @on-node-click="handleNodeClick"
+            />
+        </div>
+    </div>
+</template>
+
+<script>
+import OrgTreeNode from "./node"
+
+export default {
+    name: 'OrgTree',
+
+    components: {OrgTreeNode},
+
+    props: {
+        data: {required: true},
+        props: {
+            type: Object,
+            default: () => ({
+                id: 'id',
+                label: 'label',
+                expand: 'expand',
+                children: 'children'
+            })
+        },
+        horizontal: Boolean,
+        collapsable: Boolean,
+        labelWidth: [String, Number],
+        labelClassName: [Function, String],
+        expandAll: Boolean
+    },
+
+    data() {
+        return {
+            flatData: {},
+            dataCloned: {}
+        }
+    },
+
+    watch: {
+        data(newData) {
+            this._cloneData(newData)
+            this._mapData(this.dataCloned, item => {
+                const {expand} = this.flatData[item[this.props.id]] || {}
+                if (expand) this.$set(item, this.props.expand, true)
+            })
+            this._toggleExpand(this.dataCloned, this.expandAll)
+        },
+        expandAll(status) {
+            this._toggleExpand(this.dataCloned, status)
+        }
+    },
+
+    methods: {
+        _cloneData(newData) {
+            this.dataCloned = JSON.parse(JSON.stringify(newData))
+        },
+        _setFlatData(data) {
+            this.flatData[data[this.props.id]] = data
+        },
+        /**
+         * 工具方法,用于遍历树状数据的每个节点, fn为在该节点做的操作,其有一个参数即当前节点数据
+         */
+        _mapData(data, fn) {
+            fn(data)
+            const children = data[this.props.children]
+            children && children.forEach(child => this._mapData(child, fn))
+        },
+        /**
+         * 用来便利所有节点数据,将树状数据扁平化存放到flatData,用于数据更新后展开状态的恢复
+         */
+        _updateExpandStatus() {
+            this._mapData(this.dataCloned, this._setFlatData)
+        },
+        _toggleExpand(data, status) {
+            if (Array.isArray(data)) {
+                data.forEach(item => {
+                    this.$set(item, this.props.expand, status)
+                    const children = item[this.props.children]
+                    if (children) {
+                        this._toggleExpand(children, status)
+                    }
+                })
+            }
+            else {
+                this.$set(data, this.props.expand, status)
+                const children = data[this.props.children]
+                if (children) {
+                    this._toggleExpand(children, status)
+                }
+            }
+        },
+        collapse(list) {
+            list.forEach(child => {
+                if (child[this.props.expand]) {
+                    child[this.props.expand] = false
+                }
+                const children = child[this.props.children]
+                children && this.collapse(children)
+            })
+        },
+        handleExpand(data) {
+            if (this.props.expand in data) {
+                data[this.props.expand] = !data[this.props.expand]
+                const children = data[this.props.children]
+                if (!data[this.props.expand] && children) {
+                    this.collapse(children)
+                }
+            }
+            else this.$set(data, this.props.expand, true)
+
+            this.$emit('on-expand', data, data[this.props.expand])
+            this._updateExpandStatus()
+        },
+        handleNodeClick(e, data) {
+            this.$emit('on-node-click', e, data, () => this.handleExpand(data))
+        }
+    },
+
+    mounted() {
+        this._cloneData(this.data)
+        this._updateExpandStatus()
+        this._toggleExpand(this.dataCloned, this.expandAll)
+    }
+}
+</script>
+
+<style lang="scss" src="./style.scss"></style>

+ 121 - 0
src/component/OrgTree/node.vue

@@ -0,0 +1,121 @@
+<script type="text/jsx">
+// 判断是否叶子节点
+const isLeaf = (data, prop) => {
+    return !Array.isArray(data[prop]) || data[prop].length === 0
+}
+
+// 创建 node 节点
+const renderNode = (h, data, context) => {
+    const {props} = context
+    const cls = ['org-tree-node']
+    const childNodes = []
+    const children = data[props.props.children]
+
+    if (isLeaf(data, props.props.children)) {
+        cls.push('is-leaf')
+    }
+    else if (props.collapsable && !data[props.props.expand]) {
+        cls.push('collapsed')
+    }
+
+    childNodes.push(renderLabel(h, data, context))
+
+    if (!props.collapsable || data[props.props.expand]) {
+        childNodes.push(renderChildren(h, children, context))
+    }
+
+    return <div class={cls.join(' ')}>{childNodes}</div>
+}
+
+// 创建 label 节点
+const renderLabel = (h, data, context) => {
+    const {props} = context
+    const label = data[props.props.label]
+    const clickHandler = context.listeners['on-node-click']
+    const mousedownHandler = e => {
+        e.stopPropagation()
+        if (context.listeners['on-node-mousedown']) {
+            context.listeners['on-node-mousedown'](e, data)
+        }
+    }
+    const mouseupHandler = context.listeners['on-node-mouseup']
+    const touchstartHandler = context.listeners['on-node-touchstart']
+    const touchleaveHandler = context.listeners['on-node-touchleave']
+
+    const childNodes = []
+    if (context.parent.$scopedSlots.default) {
+        childNodes.push(context.parent.$scopedSlots.default(data))
+    }
+    else childNodes.push(label)
+
+    if (props.collapsable && !isLeaf(data, props.props.children)) {
+        childNodes.push(renderBtn(h, data, context))
+    }
+
+    const cls = ['org-tree-node-label-inner']
+    let {labelWidth, labelClassName} = props
+    if (typeof labelWidth === 'number') {
+        labelWidth += 'px'
+    }
+    if (typeof labelClassName === 'function') {
+        labelClassName = labelClassName(data)
+    }
+    labelClassName && cls.push(labelClassName)
+
+    return (
+        <div
+            class="org-tree-node-label"
+            on-click={e => clickHandler && clickHandler(e, data)}
+            on-mousedown={mousedownHandler}
+            on-mouseup={e => mouseupHandler && mouseupHandler(e, data)}
+            on-touchstart={e => touchstartHandler && touchstartHandler(e, data)}
+            on-touchleave={e => touchleaveHandler && touchleaveHandler(e, data)}
+        >
+            <div class={cls.join(' ')} style={{width: labelWidth}}>
+                {childNodes}
+            </div>
+        </div>
+    )
+}
+
+// 创建展开折叠按钮
+const renderBtn = (h, data, context) => {
+    const {props} = context
+    const expandHandler = context.listeners['on-expand']
+    const onClock = e => {
+        e.stopPropagation()
+        expandHandler && expandHandler(data)
+    }
+    const onMousedown = e => e.stopPropagation()
+    const cls = ['org-tree-node-btn']
+
+    data[props.props.expand] && cls.push('expanded')
+
+    return (
+        <span class="org-tree-button-wrapper" on-click={onClock} on-mousedown={onMousedown}>
+            <span class={cls.join(' ')}/>
+        </span>
+    )
+}
+
+// 创建 node 子节点
+const renderChildren = (h, list, context) => {
+    if (Array.isArray(list) && list.length) {
+        return (
+            <div class="org-tree-node-children">
+                {list.map(item => renderNode(h, item, context))}
+            </div>
+        )
+    }
+}
+
+export default {
+    name: "OrgTreeNode",
+
+    functional: true,
+
+    render(h, context) {
+        return renderNode(h, context.props.data, context)
+    }
+}
+</script>

+ 261 - 0
src/component/OrgTree/style.scss

@@ -0,0 +1,261 @@
+.org-tree-container {
+    display: inline-block;
+    padding: 15px;
+
+    .org-tree {
+        display: table;
+        text-align: center;
+
+        &:before, &:after {
+            content: '';
+            display: table;
+        }
+
+        &:after {
+            clear: both;
+        }
+    }
+
+    .org-tree-node,
+    .org-tree-node-children {
+        position: relative;
+        margin: 0;
+        padding: 0;
+        list-style-type: none;
+
+        &:before, &:after {
+            transition: all .35s;
+        }
+    }
+
+    .org-tree-node-label {
+        position: relative;
+        display: inline-block;
+        z-index: 1;
+
+        .org-tree-node-label-inner {
+            padding: 10px 15px;
+            text-align: center;
+            border-radius: 3px;
+            box-shadow: 0 1px 5px rgba(0, 0, 0, .15);
+        }
+    }
+
+    .org-tree-node-btn {
+        position: absolute;
+        top: 100%;
+        left: 50%;
+        width: 20px;
+        height: 20px;
+        z-index: 10;
+        margin-left: -10px;
+        margin-top: 10px;
+        background-color: #ffffff;
+        border: 1px solid #ccc;
+        border-radius: 50%;
+        box-shadow: 0 0 2px rgba(0, 0, 0, .15);
+        cursor: pointer;
+        transition: all .35s ease;
+
+        &:hover {
+            background-color: #e7e8e9;
+            transform: scale(1.15);
+        }
+
+        &:before, &:after {
+            content: '';
+            position: absolute;
+        }
+
+        &:before {
+            top: 50%;
+            left: 4px;
+            right: 4px;
+            height: 0;
+            border-top: 1px solid #ccc;
+        }
+
+        &:after {
+            top: 4px;
+            left: 50%;
+            bottom: 4px;
+            width: 0;
+            border-left: 1px solid #ccc;
+        }
+
+        &.expanded:after {
+            border: none;
+        }
+    }
+
+    .org-tree-node {
+        padding-top: 20px;
+        display: table-cell;
+        vertical-align: top;
+
+        &.is-leaf, &.collapsed {
+            padding-left: 10px;
+            padding-right: 10px;
+        }
+
+        &:before, &:after {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 50%;
+            height: 19px;
+        }
+
+        &:after {
+            left: 50%;
+            border-left: 1px solid #ddd;
+        }
+
+        &:not(:first-child):before,
+        &:not(:last-child):after {
+            border-top: 1px solid #ddd;
+        }
+
+    }
+
+    .collapsable .org-tree-node.collapsed {
+        padding-bottom: 30px;
+
+        .org-tree-node-label:after {
+            content: '';
+            position: absolute;
+            top: 100%;
+            left: 0;
+            width: 50%;
+            height: 20px;
+            border-right: 1px solid #ddd;
+        }
+    }
+
+    .org-tree > .org-tree-node {
+        padding-top: 0;
+
+        &:after {
+            border-left: 0;
+        }
+    }
+
+    .org-tree-node-children {
+        padding-top: 20px;
+        display: table;
+
+        &:before {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 50%;
+            width: 0;
+            height: 20px;
+            border-left: 1px solid #ddd;
+        }
+
+        &:after {
+            content: '';
+            display: table;
+            clear: both;
+        }
+    }
+
+    .horizontal {
+        .org-tree-node {
+            display: table-cell;
+            float: none;
+            padding-top: 0;
+            padding-left: 20px;
+
+            &.is-leaf, &.collapsed {
+                padding-top: 10px;
+                padding-bottom: 10px;
+            }
+
+            &:before, &:after {
+                width: 19px;
+                height: 50%;
+            }
+
+            &:after {
+                top: 50%;
+                left: 0;
+                border-left: 0;
+            }
+
+            &:only-child:before {
+                top: 1px;
+                border-bottom: 1px solid #ddd;
+            }
+
+            &:not(:first-child):before,
+            &:not(:last-child):after {
+                border-top: 0;
+                border-left: 1px solid #ddd;
+            }
+
+            &:not(:only-child):after {
+                border-top: 1px solid #ddd;
+            }
+
+            .org-tree-node-inner {
+                display: table;
+            }
+
+        }
+
+        .org-tree-node-label {
+            display: table-cell;
+            vertical-align: middle;
+        }
+
+        &.collapsable .org-tree-node.collapsed {
+            padding-right: 30px;
+
+            .org-tree-node-label:after {
+                top: 0;
+                left: 100%;
+                width: 20px;
+                height: 50%;
+                border-right: 0;
+                border-bottom: 1px solid #ddd;
+            }
+        }
+
+        .org-tree-node-btn {
+            top: 50%;
+            left: 100%;
+            margin-top: -11px;
+            margin-left: 9px;
+        }
+
+        & > .org-tree-node:only-child:before {
+            border-bottom: 0;
+        }
+
+        .org-tree-node-children {
+            display: table-cell;
+            padding-top: 0;
+            padding-left: 20px;
+
+            &:before {
+                top: 50%;
+                left: 0;
+                width: 20px;
+                height: 0;
+                border-left: 0;
+                border-top: 1px solid #ddd;
+            }
+
+            &:after {
+                display: none;
+            }
+
+            & > .org-tree-node {
+                display: block;
+            }
+        }
+    }
+}

+ 304 - 0
src/component/RegionSelector/Tab/index.vue

@@ -0,0 +1,304 @@
+<template>
+    <el-select
+        ref="select"
+        :value="value"
+        :disabled="readonly"
+        :size="size"
+        popper-append-to-body
+        clearable
+        @clear="() => $emit('clear')"
+        @visible-change="visibleChange"
+    >
+        <template v-slot:empty>
+            <div class="rg-header">
+                <h3>行政区划选择</h3>
+
+                <button class="rg-removeall-button" type="button" title="清除已选" @click="removeAll">
+                    <i class="el-icon-delete"/>
+                </button>
+
+                <button class="rg-done-button" type="button" title="完成" @click="done">
+                    <i class="el-icon-check"/>
+                </button>
+            </div>
+
+            <div class="rg-search">
+                <input
+                    ref="searchInput"
+                    v-model="searchText"
+                    class="rg-input"
+                    type="text"
+                    placeholder="输入地区id或名称搜索"
+                    @input="search"
+                >
+            </div>
+
+            <div class="rg-level-tabs">
+                <ul>
+                    <li v-for="(tab,index) in selected" :key="tab.name" :class="{active:index+1 === currentLevel}">
+                        <a href="javascript:" @click="() => clickTab(index,tab)">{{ tab.name }}</a>
+                    </li>
+                </ul>
+            </div>
+
+            <div v-loading="loading" class="rg-results-container">
+                <ul class="rg-results">
+                    <li
+                        v-for="item in items"
+                        :key="item.id"
+                        :class="{'rg-item': true, 'active': isItemActive(item.id)}"
+                        @click="() => clickItem(item)"
+                    >
+                        {{ limit ? item.name + `(${item.value})` : item.name }}
+                    </li>
+                    <li v-if="items.length === 0" class="rg-message-box">无匹配项目</li>
+                </ul>
+            </div>
+        </template>
+    </el-select>
+</template>
+
+<script>
+import {debounce, deepClone} from "@/util"
+import {createLimitTree, getNodeId} from "@/util/tree"
+import {store, init} from '../store'
+import common from '../mixin'
+
+const PROVINCE_CITIES = [
+    {id: '11', name: '北京市'},
+    {id: '12', name: '天津市'},
+    {id: '31', name: '上海市'},
+    {id: '50', name: '重庆市'}
+]
+const DEFAULT_TABS = [{name: '省/直辖市'}, {name: '市'}, {name: '区/县'}, {name: '乡/镇/街道'}]
+
+//根据node的类型生成查找断言,支持{id:'10',...}、{name:'北京市',...}、'10'、'北京市' 等四种类型
+function predicate(node) {
+    if (typeof node === 'object') {
+        const key = node.hasOwnProperty('id') ? 'id' : 'name'
+        return item => item[key] === node[key]
+    }
+    else {
+        const firstCharCode = node.charCodeAt(0)
+        const key = 48 <= firstCharCode && firstCharCode <= 57 ? 'id' : 'name'
+        return item => item[key] === node
+    }
+}
+
+//根据叶子id获取树节点
+function topDownById(tree, id) {
+    //判断深度,深度=id.length/2
+    const depth = Math.floor(id.length / 2)
+    if (depth < 1) return []
+    const result = []
+    for (let i = 1; i <= depth; i++) {
+        const parentId = id.substring(0, i * 2)
+        const parent = tree.find(node => node.id === parentId)
+        if (!parent) return result
+        result.push(parent)
+        if (i === 1 && PROVINCE_CITIES.some(i => i.id === parentId)) {
+            i++
+        }
+        tree = parent.children || []
+    }
+    return result
+}
+
+export default {
+    name: "RegionTabSelector",
+
+    mixins: [common],
+
+    props: {
+        value: [String, Array],
+        maxLevel: {type: Number, default: 3},
+        separation: {type: String, default: ','}
+    },
+
+    data() {
+        return {
+            loading: false,
+            searchText: '',
+            currentLevel: 1,
+            realMaxLevel: this.maxLevel,
+            selected: [],
+            treeData: [],
+            items: []
+        }
+    },
+
+    computed: {
+        regionTree() {
+            return store.data
+        },
+    },
+
+    watch: {
+        currentLevel(v) {
+            this.setItems(v)
+        }
+    },
+
+    methods: {
+        //select的下拉框展开收起
+        visibleChange(v) {
+            if (v) {
+                this.transformValueToSelected()
+                this.$nextTick(() => this.$refs.searchInput.focus())
+            }
+            else this.removeAll()
+        },
+
+        search() {
+            const parent = this.selected[this.currentLevel - 2] || {children: this.treeData}
+
+            if (!this.searchText) return this.items = parent.children
+
+            //以0-9开头的的搜索词都根据id去查
+            const firstCharCode = this.searchText.charCodeAt(0)
+            const useId = 48 <= firstCharCode && firstCharCode <= 57
+            const predicate = (
+                () => useId
+                    ? item => item.id.startsWith(this.searchText)
+                    : item => item.name.startsWith(this.searchText)
+            )()
+
+            this.items = parent.children.filter(predicate)
+        },
+
+        clickTab(index, tab) {
+            //点击当前激活的tab时不处理
+            if (index + 1 === this.currentLevel) return
+
+            this.currentLevel = index + 1
+        },
+
+        clickItem(item) {
+            this.setTabs(item, this.currentLevel)
+            this.nextTab()
+        },
+
+        setTabs(item, level) {
+            const prev = this.selected.slice(0, level - 1)
+            prev.push(item)
+            const next = deepClone(DEFAULT_TABS)
+
+            //选择省份时
+            if (level === 1) {
+                //选择直辖市的时候,最大深度-1,移除'市'tab
+                if (PROVINCE_CITIES.some(i => i.id === item.id)) {
+                    next.splice(1, 1)
+                    this.realMaxLevel = this.maxLevel - 1
+                }
+                else this.realMaxLevel = this.maxLevel
+            }
+
+            this.selected = prev.concat(next.slice(level, this.realMaxLevel))
+        },
+
+        nextTab() {
+            this.searchText = ''
+            //若选择的是最后一级节点,直接完成
+            if (this.currentLevel >= this.realMaxLevel) {
+                this.done()
+            }
+            else this.currentLevel++
+        },
+
+        //判断是否是已选项
+        isItemActive(id) {
+            const selected = this.selected[this.currentLevel - 1]
+            return selected && selected.id === id
+        },
+
+        //根据激活的tab改变显示的行政区划
+        setItems(level) {
+            //顶级节点返回完整的树
+            if (level === 1) this.items = this.treeData
+
+            //否则返回上一级节点的children
+            else {
+                const parent = this.selected[level - 2]
+                this.items = parent ? parent.children || [] : []
+            }
+        },
+
+        //将传入的value转换为已选项
+        transformValueToSelected() {
+            if (!this.value) return
+
+            const result = [],
+                loopArray = Array.isArray(this.value)
+                    ? this.value
+                    : this.value.split(this.separation).filter(Boolean)
+
+            for (let i = 0; i < loopArray.length; i++) {
+                const str = loopArray[i], parent = result[i - 1] || {children: this.treeData}
+                const node = parent.children.find(predicate(str))
+
+                //只要有一次不满足就退出,保留之前的查找结果
+                if (!node) break
+
+                result.push(node)
+            }
+
+            result.forEach((item, index) => this.setTabs(item, index + 1))
+        },
+
+        removeAll() {
+            this.realMaxLevel = this.maxLevel
+            this.selected = deepClone(DEFAULT_TABS)
+            this.currentLevel = 1
+            this.searchText = ''
+            this.setItems(this.currentLevel)
+        },
+
+        done() {
+            const result = this.selected.filter(item => item.id)
+            this.$emit('input', result.map(item => item.name).join(this.separation))
+
+            if (result.length === 0) {
+                this.$emit('select', {}, [])
+                return this.$refs.select.blur()
+            }
+
+            const node = result[result.length - 1]
+            const payload = [node]
+
+            if (this.getChildrenOnSelect) {
+                const ids = getNodeId(node.children)
+                ids.unshift(node.id)
+                payload.push(ids)
+            }
+
+            this.$emit('select', ...payload)
+
+            this.$refs.select.blur()
+        },
+
+        init() {
+            this.loading = true
+            const hasInit = this.regionTree.length > 0
+            const promise = () => hasInit ? Promise.resolve() : init(this.regionDataUrl)
+            return promise()
+                .then(() => this.limit ? this.limitApi() : Promise.resolve())
+                .then(data => {
+                    this.treeData = data ? createLimitTree(this.regionTree, data) : this.regionTree
+                    this.setItems(this.currentLevel)
+                })
+                .finally(() => this.loading = false)
+        }
+    },
+
+    created() {
+        this.search = debounce(this.search, 200)
+        this.selected = deepClone(DEFAULT_TABS)
+    },
+
+    mounted() {
+        this.init()
+    }
+}
+</script>
+
+<style lang="scss" src="./style.scss"></style>

+ 139 - 0
src/component/RegionSelector/Tab/style.scss

@@ -0,0 +1,139 @@
+@import "~@/style/var";
+
+.rg-header,
+.rg-search {
+    padding: 2px 10px 0;
+    background-color: $--color-white;
+}
+
+.rg-header {
+    h3 {
+        padding: 6px;
+        margin: 0;
+        color: $--color-text-primary;
+        font-size: 16px;
+    }
+
+    button {
+        position: absolute;
+        -webkit-appearance: none;
+        padding: 0;
+        cursor: pointer;
+        background: 0 0;
+        border: 0;
+        outline: none;
+        color: $--color-text-secondary;
+        top: 9px;
+
+        i {
+            font-size: 16px;
+        }
+
+        &:hover {
+            color: $--color-text-regular;
+        }
+
+        &.rg-removeall-button {
+            right: 32px
+        }
+
+        &.rg-done-button {
+            right: 8px;
+        }
+    }
+}
+
+.rg-level-tabs {
+    margin-top: 10px;
+
+    ul {
+        padding: 0;
+        margin: 0;
+        line-height: 1.5;
+        border-bottom: $--border-base;
+
+        li {
+            display: inline-block;
+            position: relative;
+
+            a {
+                display: block;
+                padding: 0.2rem 1rem 0.6rem;
+                font-size: 14px;
+                color: $--color-text-secondary;
+                text-decoration: none;
+                cursor: pointer;
+                line-height: 1.43;
+            }
+
+            &.active {
+                a {
+                    color: $--color-text-primary;
+                    font-weight: 600;
+                }
+
+                &::after {
+                    content: "";
+                    display: block;
+                    position: absolute;
+                    bottom: 0;
+                    height: 0.2rem;
+                    width: 100%;
+                    background-color: $--color-text-placeholder;
+                }
+            }
+        }
+    }
+}
+
+.rg-results-container {
+    .rg-results {
+        margin: 0;
+        padding: 5px;
+        width: 384px;
+        line-height: 1.5;
+
+        li {
+            overflow: hidden;
+            padding: 3px 10px;
+            font-size: 14px;
+            cursor: pointer;
+
+            &.rg-item {
+                display: inline-block;
+                color: $--color-text-secondary;
+
+                &:hover:not(.active) {
+                    color: $--color-text-primary;
+                    background-color: rgba($--color-text-placeholder,.2);
+                }
+
+                &.active {
+                    color: $--color-text-primary;
+                    background-color: rgba($--color-text-placeholder,.6);
+                }
+            }
+
+            &.rg-message-box {
+                height: 30px;
+                line-height: 30px;
+                text-align: center;
+                cursor: default;
+            }
+        }
+    }
+}
+
+.rg-input {
+    background-color: rgba($--color-text-placeholder,.2);
+    border: 0;
+    width: 100%;
+    font-size: 14px;
+    padding: 6px;
+    outline: none !important;
+    border-radius: 2px;
+
+    &::-webkit-input-placeholder {
+        color: $--color-text-placeholder;
+    }
+}

+ 82 - 0
src/component/RegionSelector/Tree/TreeDialog.vue

@@ -0,0 +1,82 @@
+<script type="text/jsx">
+import {store, init} from '../store'
+import AbstractDialog from "@/component/abstract/Dialog"
+import {createLimitTree, getNodeId} from "@/util/tree"
+
+export default {
+    components: {AbstractDialog},
+
+    data() {
+        return {
+            loading: false,
+            visible: false,
+            handler: null,
+            limitTree: [],
+            getChildrenOnSelect: false,
+            limit: false,
+            limitApi: null
+        }
+    },
+
+    computed: {
+        regionTree() {
+            return store.data
+        }
+    },
+
+    methods: {
+        closeDialog() {
+            this.visible = false
+        },
+
+        nodeClick(obj) {
+            const payload = [obj]
+
+            if (this.getChildrenOnSelect) {
+                const ids = getNodeId(obj.children)
+                ids.unshift(obj.id)
+                payload.push(ids)
+            }
+
+            this.handler(payload)
+
+            this.closeDialog()
+        },
+
+        init() {
+            this.loading = true
+            const hasInit = this.regionTree.length > 0
+            const promise = () => hasInit ? Promise.resolve() : init(this.regionDataUrl)
+            return promise()
+                .then(() => this.limit ? this.limitApi() : Promise.resolve())
+                .then(data => data && (this.limitTree = createLimitTree(this.regionTree, data)))
+                .finally(() => this.loading = false)
+        }
+    },
+
+    render() {
+        return (
+            <abstract-dialog
+                v-model={this.visible}
+                class="tree-dialog"
+                title="选择行政区域"
+                loading={this.loading}
+                v-on:close={this.closeDialog}
+            >
+                <el-tree
+                    data={this.limit ? this.limitTree : this.regionTree}
+                    expand-on-click-node={false}
+                    node-key="id"
+                    v-on:node-click={this.nodeClick}
+                >
+                    {({data}) => (
+                        <span class="el-tree-node__label">
+                            {this.limit ? data.name + `(${data.value})` : data.name}
+                        </span>
+                    )}
+                </el-tree>
+            </abstract-dialog>
+        )
+    }
+}
+</script>

+ 79 - 0
src/component/RegionSelector/Tree/index.vue

@@ -0,0 +1,79 @@
+<script type="text/jsx">
+import Vue from 'vue'
+import TreeDialog from "./TreeDialog"
+import common from '../mixin'
+
+const TreeDialogConstructor = Vue.extend(TreeDialog)
+
+let limit, full
+
+export default {
+    name: "RegionTreeSelector",
+
+    mixins: [common],
+
+    data() {
+        return {
+            dialogVisible: false,
+            limitTree: []
+        }
+    },
+
+    methods: {
+        openDialog() {
+            if (this.limit) {
+                this.initLimit()
+                return limit.visible = true
+            }
+            else {
+                !full && this.initFull()
+                full.visible = true
+            }
+        },
+
+        handler(payload) {
+            this.$emit('select', ...payload)
+        },
+
+        initLimit() {
+            if (limit) {
+                limit.getChildrenOnSelect = this.getChildrenOnSelect
+                limit.limit = this.limit
+                limit.limitApi = this.limitApi
+                return limit.init()
+            }
+
+            const data = {...this.$props, handler: this.handler}
+            limit = new TreeDialogConstructor({data}).$mount()
+            document.body.appendChild(limit.$el)
+
+            return limit.init()
+        },
+
+        initFull() {
+            if (full) {
+                return full.getChildrenOnSelect = this.getChildrenOnSelect
+            }
+
+            const data = {getChildrenOnSelect: this.getChildrenOnSelect, handler: this.handler}
+            full = new TreeDialogConstructor({data}).$mount()
+            document.body.appendChild(full.$el)
+
+            return full.init()
+        }
+    },
+
+    render() {
+        return (
+            <el-input
+                value={this.value}
+                readonly={this.readonly}
+                size={this.size}
+                clearable
+                v-on:clear={() => this.$emit('clear')}
+                v-on:focus={this.openDialog}
+            />
+        )
+    }
+}
+</script>

+ 33 - 0
src/component/RegionSelector/index.vue

@@ -0,0 +1,33 @@
+<script>
+const Tree = () => import('./Tree').then(_ => _.default)
+const Tab = () => import('./Tab').then(_ => _.default)
+
+export default {
+    name: "RegionSelector",
+
+    functional: true,
+
+    props: {
+        type: {
+            type: String,
+            default: 'tab',
+            validator: v => ['tree', 'tab'].includes(v)
+        },
+
+        //省市地区json数据请求地址
+        regionDataUrl: {
+            type: String,
+            default: `${process.env.BASE_URL}static/json/region-pca.json`
+        }
+    },
+
+    render(h, context) {
+        if (!context.data.props) {
+            context.data.props = {}
+        }
+        context.data.props.regionDataUrl = context.props.regionDataUrl
+
+        return h(context.props.type === 'tree' ? Tree : Tab, context.data)
+    }
+}
+</script>

+ 11 - 0
src/component/RegionSelector/mixin.js

@@ -0,0 +1,11 @@
+export default {
+    props: {
+        value: String,
+        readonly: Boolean,
+        size: String,
+        getChildrenOnSelect: Boolean,
+        limit: Boolean,
+        limitApi: Function,
+        regionDataUrl: String
+    }
+}

+ 11 - 0
src/component/RegionSelector/store.js

@@ -0,0 +1,11 @@
+import Vue from 'vue'
+
+export const store = Vue.observable({
+    data: []
+})
+
+export function init(url) {
+    return fetch(url)
+        .then(r => r.json())
+        .then(r => store.data = r || [])
+}

+ 6 - 0
src/component/Skeleton/constant.js

@@ -0,0 +1,6 @@
+export const skeletonTypes = [
+    'text', 'circle'
+]
+export const skeletonAnimations = [
+    'wave', 'pulse', 'pulse-x', 'pulse-y', 'fade', 'blink', 'none'
+]

+ 50 - 0
src/component/Skeleton/index.vue

@@ -0,0 +1,50 @@
+<script type="text/jsx">
+//由quasar处搬运,https://quasar.dev/vue-components/skeleton
+import {skeletonAnimations, skeletonTypes} from './constant'
+
+export default {
+    name: 'QSkeleton',
+
+    functional: true,
+
+    props: {
+        type: {
+            type: String,
+            validator: v => skeletonTypes.includes(v),
+            default: 'text'
+        },
+        animation: {
+            type: String,
+            validator: v => skeletonAnimations.includes(v),
+            default: 'wave'
+        },
+        className: String,
+        tag: {type: String, default: 'div'},
+        dark: Boolean,
+        square: Boolean,
+        bordered: Boolean,
+        size: String,
+        width: String,
+        height: String
+    },
+
+    render(h, context) {
+        const {type, animation, className, tag, dark, square, bordered, size, width, height} = context.props
+        const style = size !== undefined ? {width: size, height: size} : {width, height}
+        const classes = `q-skeleton--${dark === true ? 'dark' : 'light'} q-skeleton--type-${type}` +
+            (animation !== 'none' ? ` q-skeleton--anim-${animation}` : '') +
+            (square === true ? ' q-skeleton--square' : '') +
+            (bordered === true ? ' q-skeleton--bordered' : '') +
+            (className !== undefined ? ' ' + className : '')
+
+        return h(tag, {
+            staticClass: 'q-skeleton',
+            class: classes,
+            style,
+            on: context.$listeners
+        }, context.children)
+    }
+}
+</script>
+
+<style lang="scss" src="./style.scss"></style>

+ 140 - 0
src/component/Skeleton/style.scss

@@ -0,0 +1,140 @@
+.q-skeleton {
+    background: rgba(0, 0, 0, .12);
+    border-radius: 4px;
+    box-sizing: border-box;
+
+    &::before {
+        content: '\00a0'
+    }
+
+    &--type-circle {
+        height: 48px;
+        width: 48px;
+        border-radius: 50%;
+    }
+
+    &--bordered {
+        border: 1px solid rgba(0, 0, 0, .05)
+    }
+
+    &--square {
+        border-radius: 0;
+    }
+
+    &--anim-fade {
+        animation: q-skeleton--fade 1.5s linear .5s infinite
+    }
+
+    &--anim-pulse {
+        animation: q-skeleton--pulse 1.5s ease-in-out .5s infinite
+    }
+
+    &--anim-pulse-x {
+        animation: q-skeleton--pulse-x 1.5s ease-in-out .5s infinite
+    }
+
+    &--anim-pulse-y {
+        animation: q-skeleton--pulse-y 1.5s ease-in-out .5s infinite
+    }
+
+    &--anim-wave,
+    &--anim-blink,
+    &--anim-pop {
+        position: relative;
+        overflow: hidden;
+        z-index: 1;
+
+        &:after {
+            content: '';
+            position: absolute;
+            top: 0;
+            right: 0;
+            bottom: 0;
+            left: 0;
+            z-index: 0;
+        }
+    }
+
+    &--anim-blink:after {
+        background: rgba(255, 255, 255, .7);
+        animation: q-skeleton--fade 1.5s linear .5s infinite
+    }
+
+    &--anim-wave:after {
+        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .5), transparent);
+        animation: q-skeleton--wave 1.5s linear .5s infinite
+    }
+
+    &--dark {
+        background: rgba(255, 255, 255, 0.05);
+
+        &.q-skeleton--bordered {
+            border: 1px solid rgba(255, 255, 255, .25)
+        }
+
+        &.q-skeleton--anim-wave:after {
+            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent)
+        }
+
+        &.q-skeleton--anim-blink:after {
+            background: rgba(255, 255, 255, .2)
+        }
+    }
+}
+
+@keyframes q-skeleton--fade {
+    0% {
+        opacity: 1
+    }
+    50% {
+        opacity: .4
+    }
+    100% {
+        opacity: 1
+    }
+}
+
+@keyframes q-skeleton--pulse {
+    0% {
+        transform: scale(1)
+    }
+    50% {
+        transform: scale(.85)
+    }
+    100% {
+        transform: scale(1)
+    }
+}
+
+@keyframes q-skeleton--pulse-x {
+    0% {
+        transform: scaleX(1)
+    }
+    50% {
+        transform: scaleX(.75)
+    }
+    100% {
+        transform: scaleX(1)
+    }
+}
+
+@keyframes q-skeleton--pulse-y {
+    0% {
+        transform: scaleY(1)
+    }
+    50% {
+        transform: scaleY(.75)
+    }
+    100% {
+        transform: scaleY(1)
+    }
+}
+
+@keyframes q-skeleton--wave {
+    0% {
+        transform: translateX(-100%)
+    }
+    100% {
+        transform: translateX(100%)
+    }
+}

+ 218 - 0
src/component/TreeSelect/index.vue

@@ -0,0 +1,218 @@
+<template>
+    <el-select
+        ref="select"
+        :value="value"
+        :multiple="multiple"
+        :disabled="disabled"
+        :size="size"
+        :clearable="clearable"
+        :placeholder="placeholder"
+        :popper-append-to-body="popperAppendToBody"
+        :automatic-dropdown="automaticDropdown"
+        @input="e => $emit('input',e)"
+        @change="e => $emit('change',e)"
+        @remove-tag="removeTag"
+    >
+        <template v-slot:empty>
+            <div class="tree-select-container">
+                <el-input
+                    ref="search"
+                    v-if="filterable && filterMethod"
+                    v-model="search"
+                    size="mini"
+                    clearable
+                    @input="onSearch"
+                />
+                <el-tree
+                    ref="tree"
+                    :data="data"
+                    :node-key="nodeKey"
+                    :props="props"
+                    :show-checkbox="multiple"
+                    :default-checked-keys="defaultCheckedKeys"
+                    :current-node-key="multiple ? undefined : value"
+                    :highlight-current="!multiple"
+                    :expand-on-click-node="false"
+                    :check-on-click-node="multiple"
+                    :filter-node-method="filterMethod"
+                    :accordion="accordion"
+                    @node-click="nodeClick"
+                    @check="check"
+                >
+                    <slot slot-scope="{node,data}">
+                        <span :class="{'el-tree-node__label':true,'is-disabled':node.disabled}">{{ node.label }}</span>
+                    </slot>
+                </el-tree>
+            </div>
+        </template>
+    </el-select>
+</template>
+
+<script>
+import {debounce} from "@/util"
+import {flatTree} from "@/util/tree"
+
+export default {
+    name: "TreeSelect",
+
+    props: {
+        value: {required: true},
+        data: {type: Array, default: () => []},
+        multiple: Boolean,
+        disabled: Boolean,
+        size: String,
+        clearable: {type: Boolean, default: true},
+        placeholder: String,
+        filterable: Boolean,
+        filterMethod: Function,
+        nodeKey: {type: String, default: 'id'},
+        props: {
+            default() {
+                return {
+                    children: 'children',
+                    label: 'label',
+                    disabled: 'disabled'
+                }
+            }
+        },
+        accordion: {type: Boolean, default: true},
+        popperAppendToBody: Boolean,
+        automaticDropdown: Boolean
+    },
+
+    data() {
+        return {
+            currentNode: null,
+            search: ''
+        }
+    },
+
+    computed: {
+        defaultCheckedKeys() {
+            return this.multiple ? this.value : []
+        }
+    },
+
+    watch: {
+        value(v) {
+            const method = this.multiple ? 'setCheckedKeys' : 'setCurrentKey'
+            this.$refs.tree[method](v || null)
+            if (!v && !this.multiple) this.currentNode = null
+        },
+
+        data() {
+            this.initSelectOptions()
+        }
+    },
+
+    methods: {
+        removeTag(v) {
+            const tree = this.$refs.tree
+            const value = [...this.value]
+
+            //被移除的选项的所有父级的ID
+            const parents = []
+            let removeNode = tree.getNode(v)
+            while (removeNode && removeNode.parent) {
+                parents.push(removeNode.parent.key)
+                removeNode = removeNode.parent
+            }
+
+            //移除所有子级、所有父级
+            for (let i = value.length - 1; i >= 0; i--) {
+                const isChild = this.getNodeParent(tree.getNode(value[i]), ({key}) => key === v)
+                if (isChild || parents.includes(value[i])) {
+                    value.splice(i, 1)
+                }
+            }
+
+            value.length < this.value.length && this.$emit('input', value)
+        },
+
+        onSearch(v) {
+            this.$refs.tree.filter(v)
+        },
+
+        nodeClick(data, node) {
+            if (this.multiple) return
+
+            if (node.disabled) {
+                return this.$refs.tree.setCurrentKey(null)
+            }
+
+            if (this.currentNode === data) {
+                this.currentNode = null
+                this.$refs.tree.setCurrentKey(null)
+            }
+            else this.currentNode = data
+
+            this.$emit('input', this.currentNode && this.currentNode[this.nodeKey], this.currentNode)
+            this.$refs.select.blur()
+        },
+
+        check(data, {checkedKeys}) {
+            this.multiple && this.$emit('input', checkedKeys)
+        },
+
+        getNodeParent(node, predicate) {
+            if (!node || predicate(node)) return node
+            return this.getNodeParent(node.parent, predicate)
+        },
+
+        initSelectOptions() {
+            this.$refs.select.cachedOptions =
+                flatTree(this.data, this.props.children)
+                    .map(i => ({
+                        value: i[this.nodeKey],
+                        currentLabel: i[this.props.label]
+                    }))
+            this.selectOptionsTrigger()
+        },
+
+        //模拟el-select对options的watch
+        selectOptionsTrigger() {
+            const select = this.$refs.select
+            if (select.$isServer) return
+            this.$nextTick(() => {
+                select.broadcast('ElSelectDropdown', 'updatePopper')
+            })
+            if (select.multiple) {
+                select.resetInputHeight()
+            }
+            let inputs = select.$el.querySelectorAll('input')
+            if ([].indexOf.call(inputs, document.activeElement) === -1) {
+                select.setSelected()
+            }
+            if (select.defaultFirstOption && (select.filterable || select.remote) && select.filteredOptionsCount) {
+                select.checkDefaultFirstOption()
+            }
+        }
+    },
+
+    created() {
+        this.onSearch = debounce(this.onSearch)
+    },
+
+    mounted() {
+        this.currentNode = this.$refs.tree.getCurrentNode()
+        this.initSelectOptions()
+    }
+}
+</script>
+
+<style lang="scss">
+@import "~@/style/var";
+
+.tree-select-container {
+    padding: 8px;
+
+    > .el-input {
+        margin-bottom: 8px;
+    }
+
+    .el-tree-node__label.is-disabled {
+        color: $--disabled-color-base;
+        cursor: not-allowed;
+    }
+}
+</style>

+ 0 - 0
src/component/Upload/CardList.vue


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov