Browse Source

Signed-off-by: zhaobao <528046418@qq.com>

zhaobao 2 years ago
parent
commit
faa9715551
100 changed files with 6888 additions and 1 deletions
  1. 1 1
      package.json
  2. 26 0
      plop-templates/component/index.hbs
  3. 55 0
      plop-templates/component/prompt.js
  4. 16 0
      plop-templates/store/index.hbs
  5. 62 0
      plop-templates/store/prompt.js
  6. 2 0
      plop-templates/utils.js
  7. 26 0
      plop-templates/view/index.hbs
  8. 55 0
      plop-templates/view/prompt.js
  9. BIN
      public/favicon.ico
  10. 16 0
      public/index.html
  11. 11 0
      src/App.vue
  12. 39 0
      src/api/audit.js
  13. 151 0
      src/api/doctor.js
  14. 76 0
      src/api/patient.js
  15. 8 0
      src/api/qiniu.js
  16. 17 0
      src/api/remote-search.js
  17. 63 0
      src/api/user.js
  18. BIN
      src/assets/401_images/401.gif
  19. BIN
      src/assets/404_images/404.png
  20. BIN
      src/assets/404_images/404_cloud.png
  21. BIN
      src/assets/custom-theme/fonts/element-icons.ttf
  22. BIN
      src/assets/custom-theme/fonts/element-icons.woff
  23. 0 0
      src/assets/custom-theme/index.css
  24. BIN
      src/assets/logo/logo_black.png
  25. BIN
      src/assets/logo/logo_white.png
  26. 15 0
      src/assets/tableJson.js
  27. BIN
      src/assets/user.jpg
  28. 111 0
      src/components/BackToTop/index.vue
  29. 82 0
      src/components/Breadcrumb/index.vue
  30. 155 0
      src/components/Charts/Keyboard.vue
  31. 227 0
      src/components/Charts/LineMarker.vue
  32. 271 0
      src/components/Charts/MixChart.vue
  33. 56 0
      src/components/Charts/mixins/resize.js
  34. 166 0
      src/components/DndList/index.vue
  35. 65 0
      src/components/DragSelect/index.vue
  36. 297 0
      src/components/Dropzone/index.vue
  37. 78 0
      src/components/ErrorLog/index.vue
  38. 54 0
      src/components/GithubCorner/index.vue
  39. 44 0
      src/components/Hamburger/index.vue
  40. 180 0
      src/components/HeaderSearch/index.vue
  41. 1779 0
      src/components/ImageCropper/index.vue
  42. 19 0
      src/components/ImageCropper/utils/data2blob.js
  43. 39 0
      src/components/ImageCropper/utils/effectRipple.js
  44. 232 0
      src/components/ImageCropper/utils/language.js
  45. 7 0
      src/components/ImageCropper/utils/mimes.js
  46. 77 0
      src/components/JsonEditor/index.vue
  47. 99 0
      src/components/Kanban/index.vue
  48. 360 0
      src/components/MDinput/index.vue
  49. 31 0
      src/components/MarkdownEditor/default-options.js
  50. 118 0
      src/components/MarkdownEditor/index.vue
  51. 13 0
      src/components/MyChat/IChat/index.js
  52. 234 0
      src/components/MyChat/IChat/src/index.vue
  53. 24 0
      src/components/MyChat/IChat/src/store/cache.js
  54. 34 0
      src/components/MyChat/IChat/src/store/helper.js
  55. 148 0
      src/components/MyChat/IChat/src/store/index.js
  56. 157 0
      src/components/MyChat/IChat/src/store/watcher.js
  57. 230 0
      src/components/MyChat/MChat/chatTabs.vue
  58. 14 0
      src/components/MyChat/MChat/index.js
  59. 387 0
      src/components/MyChat/MChat/index.vue
  60. 374 0
      src/components/MyChat/chat/chatList.vue
  61. 69 0
      src/components/MyChat/chat/convertContext.js
  62. 18 0
      src/components/MyChat/chat/emoji.js
  63. BIN
      src/components/MyChat/chat/emoji/0.gif
  64. BIN
      src/components/MyChat/chat/emoji/1.gif
  65. BIN
      src/components/MyChat/chat/emoji/10.gif
  66. BIN
      src/components/MyChat/chat/emoji/11.gif
  67. BIN
      src/components/MyChat/chat/emoji/12.gif
  68. BIN
      src/components/MyChat/chat/emoji/13.gif
  69. BIN
      src/components/MyChat/chat/emoji/14.gif
  70. BIN
      src/components/MyChat/chat/emoji/15.gif
  71. BIN
      src/components/MyChat/chat/emoji/16.gif
  72. BIN
      src/components/MyChat/chat/emoji/17.gif
  73. BIN
      src/components/MyChat/chat/emoji/18.gif
  74. BIN
      src/components/MyChat/chat/emoji/19.gif
  75. BIN
      src/components/MyChat/chat/emoji/2.gif
  76. BIN
      src/components/MyChat/chat/emoji/20.gif
  77. BIN
      src/components/MyChat/chat/emoji/21.gif
  78. BIN
      src/components/MyChat/chat/emoji/22.gif
  79. BIN
      src/components/MyChat/chat/emoji/23.gif
  80. BIN
      src/components/MyChat/chat/emoji/24.gif
  81. BIN
      src/components/MyChat/chat/emoji/25.gif
  82. BIN
      src/components/MyChat/chat/emoji/26.gif
  83. BIN
      src/components/MyChat/chat/emoji/27.gif
  84. BIN
      src/components/MyChat/chat/emoji/28.gif
  85. BIN
      src/components/MyChat/chat/emoji/29.gif
  86. BIN
      src/components/MyChat/chat/emoji/3.gif
  87. BIN
      src/components/MyChat/chat/emoji/30.gif
  88. BIN
      src/components/MyChat/chat/emoji/31.gif
  89. BIN
      src/components/MyChat/chat/emoji/32.gif
  90. BIN
      src/components/MyChat/chat/emoji/33.gif
  91. BIN
      src/components/MyChat/chat/emoji/34.gif
  92. BIN
      src/components/MyChat/chat/emoji/35.gif
  93. BIN
      src/components/MyChat/chat/emoji/36.gif
  94. BIN
      src/components/MyChat/chat/emoji/37.gif
  95. BIN
      src/components/MyChat/chat/emoji/38.gif
  96. BIN
      src/components/MyChat/chat/emoji/39.gif
  97. BIN
      src/components/MyChat/chat/emoji/4.gif
  98. BIN
      src/components/MyChat/chat/emoji/40.gif
  99. BIN
      src/components/MyChat/chat/emoji/41.gif
  100. BIN
      src/components/MyChat/chat/emoji/42.gif

+ 1 - 1
package.json

@@ -22,7 +22,7 @@
     "driver.js": "0.9.5",
     "dropzone": "5.5.1",
     "echarts": "4.2.1",
-    "element-ui": "2.13.2",
+    "element-ui": "^2.15.13",
     "file-saver": "2.0.1",
     "fuse.js": "3.4.4",
     "iscroll": "^5.2.0",

+ 26 - 0
plop-templates/component/index.hbs

@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}

+ 55 - 0
plop-templates/component/prompt.js

@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate vue component',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'component name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'Components require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{properCase name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/components/${name}/index.vue`,
+      templateFile: 'plop-templates/component/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}

+ 16 - 0
plop-templates/store/index.hbs

@@ -0,0 +1,16 @@
+{{#if state}}
+const state = {}
+{{/if}}
+
+{{#if mutations}}
+const mutations = {}
+{{/if}}
+
+{{#if actions}}
+const actions = {}
+{{/if}}
+
+export default {
+  namespaced: true,
+  {{options}}
+}

+ 62 - 0
plop-templates/store/prompt.js

@@ -0,0 +1,62 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate store',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'store name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: 'state',
+      value: 'state',
+      checked: true
+    },
+    {
+      name: 'mutations',
+      value: 'mutations',
+      checked: true
+    },
+    {
+      name: 'actions',
+      value: 'actions',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (!value.includes('state') || !value.includes('mutations')) {
+        return 'store require at least state and mutations'
+      }
+      return true
+    }
+  }
+  ],
+  actions(data) {
+    const name = '{{name}}'
+    const { blocks } = data
+    const options = ['state', 'mutations']
+    const joinFlag = `,
+  `
+    if (blocks.length === 3) {
+      options.push('actions')
+    }
+
+    const actions = [{
+      type: 'add',
+      path: `src/store/modules/${name}.js`,
+      templateFile: 'plop-templates/store/index.hbs',
+      data: {
+        options: options.join(joinFlag),
+        state: blocks.includes('state'),
+        mutations: blocks.includes('mutations'),
+        actions: blocks.includes('actions')
+      }
+    }]
+    return actions
+  }
+}

+ 2 - 0
plop-templates/utils.js

@@ -0,0 +1,2 @@
+exports.notEmpty = name => v =>
+  !v || v.trim() === '' ? `${name} is required` : true

+ 26 - 0
plop-templates/view/index.hbs

@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}

+ 55 - 0
plop-templates/view/prompt.js

@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate a view',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'view name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'View require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/views/${name}/index.vue`,
+      templateFile: 'plop-templates/view/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}

BIN
public/favicon.ico


+ 16 - 0
public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="renderer" content="webkit">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+    <script language="javascript" src="https://webapi.amap.com/maps?v=1.3&key=0438acccffb1f4af3c7f63253c3ec021"></script>    
+  </body>
+</html>

+ 11 - 0
src/App.vue

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

+ 39 - 0
src/api/audit.js

@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+export function updateMemory(data) {
+  return request({
+    url: '/memasst/update',
+    method: 'POST',
+    data
+  })
+}
+export function createMemory(data) {
+  return request({
+    url: '/memasst/add',
+    method: 'post',
+    data
+  })
+}
+export function getMemorylistByPage(params) {
+  return request({
+    url: '/memasst/page',
+    params
+  })
+}
+export function getSportGradeByAccountName(data) {
+  return request({
+    url: '/user/getSportGradeByAccountName',
+    data
+  })
+}
+export function getSportGradeById(data) {
+  return request({
+    url: '/user/getSportGradeById',
+    data
+  })
+}
+export function getPhoneNumberApi(data) {
+  return request({
+    url: '/user/getIphoneNum',
+    data
+  })
+}

+ 151 - 0
src/api/doctor.js

@@ -0,0 +1,151 @@
+import request from '@/utils/request'
+
+export function updateDoctorInfo(data) {
+  return request({
+    url: '/doctor/update',
+    method: 'post',
+    data
+  })
+}
+export function diagnosisList(params) {
+  return request({
+    url: '/doctor/doctorView/page',
+    params
+  })
+}
+export function diagnosisDetail(seekId) {
+  return request({
+    url: '/doctor/patient/seek/getSeekById/' + seekId
+  })
+}
+export function diagnosisSave(seekId) {
+  return request({
+    url: `/doctor/diagnosis/${seekId}/save`
+  })
+}
+export function ReceivedOrder(seekId, doctorId) {
+  return request({
+    url: `/patient/seek/diagnosis/${seekId}/${doctorId}`
+  })
+}
+
+export function creatPatient(data) {
+  return request({
+    url: '/user/createAccountForPatient',
+    method: 'post',
+    data
+  })
+}
+/** 患者治疗分页 */
+export function treatmentList(params) {
+  return request({
+    url: '/patient/treatment/page',
+    params
+  })
+}
+export function treatmentInfo(treatmentId) {
+  return request({
+    url: '/patient/treatment/getById/' + treatmentId
+  })
+}
+export function updateTreatmentInfo(data) {
+  return request({
+    url: '/patient/aiPatientTreatmentDesc/update',
+    method: 'post',
+    data
+  })
+}
+
+/** 医生移除自己确诊的病人 */
+export function removeDiagnosisPatient(seekId = []) {
+  seekId = seekId.join(',')
+  return request({
+    url: '/doctor/del/seeks',
+    method: 'post',
+    data: { seekId }
+  })
+}
+/** 医生移除治疗病人 */
+export function removeTreatmentPatient(seekId = []) {
+  seekId = seekId.join(',')
+  return request({
+    url: '/patient/treatment/doctorDelTreatment',
+    method: 'post',
+    data: { seekId }
+  })
+}
+export function removePatient(treatmentId) {
+  return request({
+    url: `/patient/treatment/delById/${treatmentId}`
+  })
+}
+
+export function diagnosis(data) {
+  const seekId = data.seekId
+  return request({
+    url: `/doctor/diagnosis/${seekId}/save`,
+    method: 'post',
+    data
+  })
+}
+export function diagnosisDetails(seekId) {
+  return request({
+    url: `/doctor/patient/seek/getSeekById/${seekId}`
+  })
+}
+export function therapyList(params) {
+  return request({
+    url: '/patient/aiPatientTreatmentDesc/page',
+    params
+  })
+}
+/* 医生创建患者账号时 检测接口*/
+export function detectionAccount(accountId) {
+  return request({
+    url: '/patient/getByAccountId/' + accountId
+  })
+}
+/* 检测医生注册状态*/
+export function getInfoByAccountId(accountId) {
+  return request({
+    url: '/doctor/getInfoByAccountId/' + accountId
+  })
+}
+/** 文章接口 */
+// 文章筛选列表
+export function getArticleType() {
+  return request({
+    url: '/user/illtype/list'
+  })
+}
+// 增加或者修改
+export function articleHandle(params) {
+  return request({
+    url: '/user/docmgr/update',
+    method: 'post',
+    params
+  })
+}
+// 分页查询
+export function getArticleList(params) {
+  return request({
+    url: '/user/docmgr/page',
+    params
+  })
+}
+/** 病变相似检索 */
+export function LesionSimilariTyretrievalApi(data) {
+  return request({
+    url: '/patient/seek/ai/getLesionSimilariTyretrieval',
+    method: 'post',
+    data
+  })
+}
+/** 根据病症筛选文章 */
+export function docArticleApi(params) {
+  console.log(params)
+  return request({
+    url: '/user/docmgr/illnesstype/page',
+    params
+  })
+}

+ 76 - 0
src/api/patient.js

@@ -0,0 +1,76 @@
+import request from '@/utils/request'
+export function updatePatientrInfo(data) {
+  return request({
+    url: '/patient/update',
+    method: 'post',
+    data
+  })
+}
+/** 上传医学影像*/
+export function uploadImage(data) {
+  return request({
+    url: '/upload/file',
+    method: 'post',
+    headers: { 'Content-Type': 'multipart/form-data' },
+    data
+  })
+}
+/** 文本检索*/
+export function searchByText(data) {
+  return request({
+    url: '/patient/seek/ai/firstDiseaseScreening',
+    method: 'post',
+    data
+  })
+}
+/** 提交检索*/
+export function searchSubmit(data) {
+  return request({
+    url: '/patient/seek/auxiliarydiagnosis',
+    method: 'post',
+    data
+  })
+}
+/** 未接单再次咨询 */
+export function secondauxiliarydiagnosis(data) {
+  return request({
+    url: '/patient/seek/secondauxiliarydiagnosis',
+    method: 'post',
+    data
+  })
+}
+/** 患者记录列表 `status` int(1)
+    * DEFAULT '0' COMMENT '0 待诊断、
+    * 1 待确认、2 诊断完成',
+*/
+export function recordList(params) {
+  return request({
+    url: '/patient/seek/page',
+    params
+  })
+}
+export function patientInfo(seekId) {
+  return request({
+    url: `/patient/seek/getSeekById/${seekId}`
+  })
+}
+export function consultationDoctor(data) { // 咨询医生
+  return request({
+    url: '/patient/seek/consultationDoctor',
+    method: 'post',
+    data
+  })
+}
+export function recommend(data) { // AI医疗机构或药店推荐接口
+  return request({
+    url: '/patient/seek/ai/recommend',
+    method: 'post',
+    data
+  })
+}
+export function patientSubmit(seekId, satisfaction) { // 患者确认评价
+  return request({
+    url: `/patient/seek/satisfaction/${seekId}/${satisfaction}`,
+    method: 'post'
+  })
+}

+ 8 - 0
src/api/qiniu.js

@@ -0,0 +1,8 @@
+import request from '@/utils/request'
+
+export function getToken() {
+  return request({
+    url: '/qiniu/upload/token', // 假地址 自行替换
+    method: 'get'
+  })
+}

+ 17 - 0
src/api/remote-search.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function searchUser(name) {
+  return request({
+    url: '/vue-element-admin/search/user',
+    method: 'get',
+    params: { name }
+  })
+}
+
+export function transactionList(query) {
+  return request({
+    url: '/vue-element-admin/transaction/list',
+    method: 'get',
+    params: query
+  })
+}

+ 63 - 0
src/api/user.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+// import Qs from 'qs'
+// Qs.stringify(data)
+export function login(params) {
+  return request({
+    url: '/user/login',
+    params
+  })
+}
+export function getInfo(params) {
+  return request({
+    url: '/user/getUserInfo',
+    params
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/user/logout',
+    method: 'post'
+  })
+}
+export function register(data) {
+  return request({
+    url: '/user/register',
+    method: 'post',
+    data: data
+  })
+}
+/** 公用上传接口 */
+export function uploadFile(data) {
+  return request({
+    url: '/upload/file',
+    method: 'post',
+    headers: { 'Content-Type': 'multipart/form-data' },
+    data
+  })
+}
+export function updatePwd(data) {
+  return request({
+    url: '/user/updatePwd',
+    method: 'post',
+    data
+  })
+}
+export function getInfoByAccountName(accountName) {
+  return request({
+    url: `/user/getInfoByAccountName/${accountName}`
+  })
+}
+export function updateSportVideo(data) {
+  return request({
+    url: '/user/updateSportVideo',
+    method: 'post',
+    data
+  })
+}
+export function getSportGradeById(params) {
+  return request({
+    url: '/user/getSportGradeById',
+    params
+  })
+}

BIN
src/assets/401_images/401.gif


BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


BIN
src/assets/custom-theme/fonts/element-icons.ttf


BIN
src/assets/custom-theme/fonts/element-icons.woff


File diff suppressed because it is too large
+ 0 - 0
src/assets/custom-theme/index.css


BIN
src/assets/logo/logo_black.png


BIN
src/assets/logo/logo_white.png


+ 15 - 0
src/assets/tableJson.js

@@ -0,0 +1,15 @@
+import itemImage02 from '@/views/patient/record/assets/item02.png';
+export default[
+  {id:'1',patientName:"张三",date:'2017-10-31  23:12:00',desc:'我经常肚子疼,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'2',patientName:"李四",date:'2018-08-31  23:12:00',desc:'我经常咳嗽,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'3',patientName:"王五",date:'2021-11-31  23:12:00',desc:'我经常腿疼,我想知道是什么…',image:"",ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'4',patientName:"张三",date:'2017-10-31  23:12:00',desc:'我经常肚子疼,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'5',patientName:"李四",date:'2018-08-31  23:12:00',desc:'我经常咳嗽,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'6',patientName:"王五",date:'2021-11-31  23:12:00',desc:'我经常腿疼,我想知道是什么…',image:"",ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'7',patientName:"张三",date:'2017-10-31  23:12:00',desc:'我经常肚子疼,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'8',patientName:"李四",date:'2018-08-31  23:12:00',desc:'我经常咳嗽,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'9',patientName:"王五",date:'2021-11-31  23:12:00',desc:'我经常腿疼,我想知道是什么…',image:"",ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'10',patientName:"张三",date:'2017-10-31  23:12:00',desc:'我经常肚子疼,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'11',patientName:"李四",date:'2018-08-31  23:12:00',desc:'我经常咳嗽,我想知道是什么…',image:itemImage02,ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'},
+  {id:'12',patientName:"王五",date:'2021-11-31  23:12:00',desc:'我经常腿疼,我想知道是什么…',image:"",ai:'我经常肚子疼,我想知道是我经常肚子疼,我想知道是什么…'}
+];

BIN
src/assets/user.jpg


+ 111 - 0
src/components/BackToTop/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <transition :name="transitionName">
+    <div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
+      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height:16px;width:16px"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'BackToTop',
+  props: {
+    visibilityHeight: {
+      type: Number,
+      default: 400
+    },
+    backPosition: {
+      type: Number,
+      default: 0
+    },
+    customStyle: {
+      type: Object,
+      default: function() {
+        return {
+          right: '50px',
+          bottom: '50px',
+          width: '40px',
+          height: '40px',
+          'border-radius': '4px',
+          'line-height': '45px',
+          background: '#e7eaf1'
+        }
+      }
+    },
+    transitionName: {
+      type: String,
+      default: 'fade'
+    }
+  },
+  data() {
+    return {
+      visible: false,
+      interval: null,
+      isMoving: false
+    }
+  },
+  mounted() {
+    window.addEventListener('scroll', this.handleScroll)
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleScroll)
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  methods: {
+    handleScroll() {
+      this.visible = window.pageYOffset > this.visibilityHeight
+    },
+    backToTop() {
+      if (this.isMoving) return
+      const start = window.pageYOffset
+      let i = 0
+      this.isMoving = true
+      this.interval = setInterval(() => {
+        const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
+        if (next <= this.backPosition) {
+          window.scrollTo(0, this.backPosition)
+          clearInterval(this.interval)
+          this.isMoving = false
+        } else {
+          window.scrollTo(0, next)
+        }
+        i++
+      }, 16.7)
+    },
+    easeInOutQuad(t, b, c, d) {
+      if ((t /= d / 2) < 1) return c / 2 * t * t + b
+      return -c / 2 * (--t * (t - 2) - 1) + b
+    }
+  }
+}
+</script>
+
+<style scoped>
+.back-to-ceiling {
+  position: fixed;
+  display: inline-block;
+  text-align: center;
+  cursor: pointer;
+}
+
+.back-to-ceiling:hover {
+  background: #d5dbe7;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity .5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0
+}
+
+.back-to-ceiling .Icon {
+  fill: #9aaabf;
+  background: none;
+}
+</style>

+ 82 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 155 - 0
src/components/Charts/Keyboard.vue

@@ -0,0 +1,155 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      const xAxisData = []
+      const data = []
+      const data2 = []
+      for (let i = 0; i < 50; i++) {
+        xAxisData.push(i)
+        data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
+        data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
+      }
+      this.chart.setOption({
+        backgroundColor: '#08263a',
+        grid: {
+          left: '5%',
+          right: '5%'
+        },
+        xAxis: [{
+          show: false,
+          data: xAxisData
+        }, {
+          show: false,
+          data: xAxisData
+        }],
+        visualMap: {
+          show: false,
+          min: 0,
+          max: 50,
+          dimension: 0,
+          inRange: {
+            color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
+          }
+        },
+        yAxis: {
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            textStyle: {
+              color: '#4a657a'
+            }
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#08263f'
+            }
+          },
+          axisTick: {
+            show: false
+          }
+        },
+        series: [{
+          name: 'back',
+          type: 'bar',
+          data: data2,
+          z: 1,
+          itemStyle: {
+            normal: {
+              opacity: 0.4,
+              barBorderRadius: 5,
+              shadowBlur: 3,
+              shadowColor: '#111'
+            }
+          }
+        }, {
+          name: 'Simulate Shadow',
+          type: 'line',
+          data,
+          z: 2,
+          showSymbol: false,
+          animationDelay: 0,
+          animationEasing: 'linear',
+          animationDuration: 1200,
+          lineStyle: {
+            normal: {
+              color: 'transparent'
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: '#08263a',
+              shadowBlur: 50,
+              shadowColor: '#000'
+            }
+          }
+        }, {
+          name: 'front',
+          type: 'bar',
+          data,
+          xAxisIndex: 1,
+          z: 3,
+          itemStyle: {
+            normal: {
+              barBorderRadius: 5
+            }
+          }
+        }],
+        animationEasing: 'elasticOut',
+        animationEasingUpdate: 'elasticOut',
+        animationDelay(idx) {
+          return idx * 20
+        },
+        animationDelayUpdate(idx) {
+          return idx * 20
+        }
+      })
+    }
+  }
+}
+</script>

+ 227 - 0
src/components/Charts/LineMarker.vue

@@ -0,0 +1,227 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      this.chart.setOption({
+        backgroundColor: '#394056',
+        title: {
+          top: 20,
+          text: 'Requests',
+          textStyle: {
+            fontWeight: 'normal',
+            fontSize: 16,
+            color: '#F1F1F3'
+          },
+          left: '1%'
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        },
+        legend: {
+          top: 20,
+          icon: 'rect',
+          itemWidth: 14,
+          itemHeight: 5,
+          itemGap: 13,
+          data: ['CMCC', 'CTCC', 'CUCC'],
+          right: '4%',
+          textStyle: {
+            fontSize: 12,
+            color: '#F1F1F3'
+          }
+        },
+        grid: {
+          top: 100,
+          left: '2%',
+          right: '2%',
+          bottom: '2%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          boundaryGap: false,
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
+        }],
+        yAxis: [{
+          type: 'value',
+          name: '(%)',
+          axisTick: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          axisLabel: {
+            margin: 10,
+            textStyle: {
+              fontSize: 14
+            }
+          },
+          splitLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        }],
+        series: [{
+          name: 'CMCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(137, 189, 27, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(137, 189, 27, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(137,189,27)',
+              borderColor: 'rgba(137,189,2,0.27)',
+              borderWidth: 12
+
+            }
+          },
+          data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
+        }, {
+          name: 'CTCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(0, 136, 212, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(0, 136, 212, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(0,136,212)',
+              borderColor: 'rgba(0,136,212,0.2)',
+              borderWidth: 12
+
+            }
+          },
+          data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
+        }, {
+          name: 'CUCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(219, 50, 51, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(219, 50, 51, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(219,50,51)',
+              borderColor: 'rgba(219,50,51,0.2)',
+              borderWidth: 12
+            }
+          },
+          data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
+        }]
+      })
+    }
+  }
+}
+</script>

+ 271 - 0
src/components/Charts/MixChart.vue

@@ -0,0 +1,271 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+      const xData = (function() {
+        const data = []
+        for (let i = 1; i < 13; i++) {
+          data.push(i + 'month')
+        }
+        return data
+      }())
+      this.chart.setOption({
+        backgroundColor: '#344b58',
+        title: {
+          text: 'statistics',
+          x: '20',
+          top: '20',
+          textStyle: {
+            color: '#fff',
+            fontSize: '22'
+          },
+          subtextStyle: {
+            color: '#90979c',
+            fontSize: '16'
+          }
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            textStyle: {
+              color: '#fff'
+            }
+          }
+        },
+        grid: {
+          left: '5%',
+          right: '5%',
+          borderWidth: 0,
+          top: 150,
+          bottom: 95,
+          textStyle: {
+            color: '#fff'
+          }
+        },
+        legend: {
+          x: '5%',
+          top: '10%',
+          textStyle: {
+            color: '#90979c'
+          },
+          data: ['female', 'male', 'average']
+        },
+        calculable: true,
+        xAxis: [{
+          type: 'category',
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          splitLine: {
+            show: false
+          },
+          axisTick: {
+            show: false
+          },
+          splitArea: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+
+          },
+          data: xData
+        }],
+        yAxis: [{
+          type: 'value',
+          splitLine: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+          },
+          splitArea: {
+            show: false
+          }
+        }],
+        dataZoom: [{
+          show: true,
+          height: 30,
+          xAxisIndex: [
+            0
+          ],
+          bottom: 30,
+          start: 10,
+          end: 80,
+          handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
+          handleSize: '110%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff' },
+          borderColor: '#90979c'
+
+        }, {
+          type: 'inside',
+          show: true,
+          height: 15,
+          start: 1,
+          end: 35
+        }],
+        series: [{
+          name: 'female',
+          type: 'bar',
+          stack: 'total',
+          barMaxWidth: 35,
+          barGap: '10%',
+          itemStyle: {
+            normal: {
+              color: 'rgba(255,144,128,1)',
+              label: {
+                show: true,
+                textStyle: {
+                  color: '#fff'
+                },
+                position: 'insideTop',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            709,
+            1917,
+            2455,
+            2610,
+            1719,
+            1433,
+            1544,
+            3285,
+            5208,
+            3372,
+            2484,
+            4078
+          ]
+        },
+
+        {
+          name: 'male',
+          type: 'bar',
+          stack: 'total',
+          itemStyle: {
+            normal: {
+              color: 'rgba(0,191,183,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            327,
+            1776,
+            507,
+            1200,
+            800,
+            482,
+            204,
+            1390,
+            1001,
+            951,
+            381,
+            220
+          ]
+        }, {
+          name: 'average',
+          type: 'line',
+          stack: 'total',
+          symbolSize: 10,
+          symbol: 'circle',
+          itemStyle: {
+            normal: {
+              color: 'rgba(252,230,48,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            1036,
+            3693,
+            2962,
+            3810,
+            2519,
+            1915,
+            1748,
+            4675,
+            6209,
+            4323,
+            2865,
+            4298
+          ]
+        }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 56 - 0
src/components/Charts/mixins/resize.js

@@ -0,0 +1,56 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null,
+      $_resizeHandler: null
+    }
+  },
+  mounted() {
+    this.initListener()
+  },
+  activated() {
+    if (!this.$_resizeHandler) {
+      // avoid duplication init
+      this.initListener()
+    }
+
+    // when keep-alive chart activated, auto resize
+    this.resize()
+  },
+  beforeDestroy() {
+    this.destroyListener()
+  },
+  deactivated() {
+    this.destroyListener()
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.$_resizeHandler()
+      }
+    },
+    initListener() {
+      this.$_resizeHandler = debounce(() => {
+        this.resize()
+      }, 100)
+      window.addEventListener('resize', this.$_resizeHandler)
+
+      this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+      this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    destroyListener() {
+      window.removeEventListener('resize', this.$_resizeHandler)
+      this.$_resizeHandler = null
+
+      this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    resize() {
+      const { chart } = this
+      chart && chart.resize()
+    }
+  }
+}

+ 166 - 0
src/components/DndList/index.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="dndList">
+    <div :style="{width:width1}" class="dndList-list">
+      <h3>{{ list1Title }}</h3>
+      <draggable :set-data="setData" :list="list1" group="article" class="dragArea">
+        <div v-for="element in list1" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle">
+            {{ element.id }}[{{ element.author }}] {{ element.title }}
+          </div>
+          <div style="position:absolute;right:0px;">
+            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
+              <i style="color:#ff4949" class="el-icon-delete" />
+            </span>
+          </div>
+        </div>
+      </draggable>
+    </div>
+    <div :style="{width:width2}" class="dndList-list">
+      <h3>{{ list2Title }}</h3>
+      <draggable :list="list2" group="article" class="dragArea">
+        <div v-for="element in list2" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle2" @click="pushEle(element)">
+            {{ element.id }} [{{ element.author }}] {{ element.title }}
+          </div>
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DndList',
+  components: { draggable },
+  props: {
+    list1: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list2: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list1Title: {
+      type: String,
+      default: 'list1'
+    },
+    list2Title: {
+      type: String,
+      default: 'list2'
+    },
+    width1: {
+      type: String,
+      default: '48%'
+    },
+    width2: {
+      type: String,
+      default: '48%'
+    }
+  },
+  methods: {
+    isNotInList1(v) {
+      return this.list1.every(k => v.id !== k.id)
+    },
+    isNotInList2(v) {
+      return this.list2.every(k => v.id !== k.id)
+    },
+    deleteEle(ele) {
+      for (const item of this.list1) {
+        if (item.id === ele.id) {
+          const index = this.list1.indexOf(item)
+          this.list1.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList2(ele)) {
+        this.list2.unshift(ele)
+      }
+    },
+    pushEle(ele) {
+      for (const item of this.list2) {
+        if (item.id === ele.id) {
+          const index = this.list2.indexOf(item)
+          this.list2.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList1(ele)) {
+        this.list1.push(ele)
+      }
+    },
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dndList {
+  background: #fff;
+  padding-bottom: 40px;
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+  .dndList-list {
+    float: left;
+    padding-bottom: 30px;
+    &:first-of-type {
+      margin-right: 2%;
+    }
+    .dragArea {
+      margin-top: 15px;
+      min-height: 50px;
+      padding-bottom: 30px;
+    }
+  }
+}
+
+.list-complete-item {
+  cursor: pointer;
+  position: relative;
+  font-size: 14px;
+  padding: 5px 12px;
+  margin-top: 4px;
+  border: 1px solid #bfcbd9;
+  transition: all 1s;
+}
+
+.list-complete-item-handle {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 50px;
+}
+
+.list-complete-item-handle2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 20px;
+}
+
+.list-complete-item.sortable-chosen {
+  background: #4AB7BD;
+}
+
+.list-complete-item.sortable-ghost {
+  background: #30B08F;
+}
+
+.list-complete-enter,
+.list-complete-leave-active {
+  opacity: 0;
+}
+</style>

+ 65 - 0
src/components/DragSelect/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
+    <slot />
+  </el-select>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragSelect',
+  props: {
+    value: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    selectVal: {
+      get() {
+        return [...this.value]
+      },
+      set(val) {
+        this.$emit('input', [...val])
+      }
+    }
+  },
+  mounted() {
+    this.setSort()
+  },
+  methods: {
+    setSort() {
+      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          dataTransfer.setData('Text', '')
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+        },
+        onEnd: evt => {
+          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
+          this.value.splice(evt.newIndex, 0, targetRow)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drag-select {
+  ::v-deep {
+    .sortable-ghost {
+      opacity: .8;
+      color: #fff !important;
+      background: #42b983 !important;
+    }
+
+    .el-tag {
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 297 - 0
src/components/Dropzone/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <div :id="id" :ref="id" :action="url" class="dropzone">
+    <input type="file" name="file">
+  </div>
+</template>
+
+<script>
+import Dropzone from 'dropzone'
+import 'dropzone/dist/dropzone.css'
+// import { getToken } from 'api/qiniu';
+
+Dropzone.autoDiscover = false
+
+export default {
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+    url: {
+      type: String,
+      required: true
+    },
+    clickable: {
+      type: Boolean,
+      default: true
+    },
+    defaultMsg: {
+      type: String,
+      default: '上传图片'
+    },
+    acceptedFiles: {
+      type: String,
+      default: ''
+    },
+    thumbnailHeight: {
+      type: Number,
+      default: 200
+    },
+    thumbnailWidth: {
+      type: Number,
+      default: 200
+    },
+    showRemoveLink: {
+      type: Boolean,
+      default: true
+    },
+    maxFilesize: {
+      type: Number,
+      default: 2
+    },
+    maxFiles: {
+      type: Number,
+      default: 3
+    },
+    autoProcessQueue: {
+      type: Boolean,
+      default: true
+    },
+    useCustomDropzoneOptions: {
+      type: Boolean,
+      default: false
+    },
+    defaultImg: {
+      default: '',
+      type: [String, Array]
+    },
+    couldPaste: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dropzone: '',
+      initOnce: true
+    }
+  },
+  watch: {
+    defaultImg(val) {
+      if (val.length === 0) {
+        this.initOnce = false
+        return
+      }
+      if (!this.initOnce) return
+      this.initImages(val)
+      this.initOnce = false
+    }
+  },
+  mounted() {
+    const element = document.getElementById(this.id)
+    const vm = this
+    this.dropzone = new Dropzone(element, {
+      clickable: this.clickable,
+      thumbnailWidth: this.thumbnailWidth,
+      thumbnailHeight: this.thumbnailHeight,
+      maxFiles: this.maxFiles,
+      maxFilesize: this.maxFilesize,
+      dictRemoveFile: 'Remove',
+      addRemoveLinks: this.showRemoveLink,
+      acceptedFiles: this.acceptedFiles,
+      autoProcessQueue: this.autoProcessQueue,
+      dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
+      dictMaxFilesExceeded: '只能一个图',
+      previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
+      init() {
+        const val = vm.defaultImg
+        if (!val) return
+        if (Array.isArray(val)) {
+          if (val.length === 0) return
+          val.map((v, i) => {
+            const mockFile = { name: 'name' + i, size: 12345, url: v }
+            this.options.addedfile.call(this, mockFile)
+            this.options.thumbnail.call(this, mockFile, v)
+            mockFile.previewElement.classList.add('dz-success')
+            mockFile.previewElement.classList.add('dz-complete')
+            vm.initOnce = false
+            return true
+          })
+        } else {
+          const mockFile = { name: 'name', size: 12345, url: val }
+          this.options.addedfile.call(this, mockFile)
+          this.options.thumbnail.call(this, mockFile, val)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          vm.initOnce = false
+        }
+      },
+      accept: (file, done) => {
+        /* 七牛*/
+        // const token = this.$store.getters.token;
+        // getToken(token).then(response => {
+        //   file.token = response.data.qiniu_token;
+        //   file.key = response.data.qiniu_key;
+        //   file.url = response.data.qiniu_url;
+        //   done();
+        // })
+        done()
+      },
+      sending: (file, xhr, formData) => {
+        // formData.append('token', file.token);
+        // formData.append('key', file.key);
+        vm.initOnce = false
+      }
+    })
+
+    if (this.couldPaste) {
+      document.addEventListener('paste', this.pasteImg)
+    }
+
+    this.dropzone.on('success', file => {
+      vm.$emit('dropzone-success', file, vm.dropzone.element)
+    })
+    this.dropzone.on('addedfile', file => {
+      vm.$emit('dropzone-fileAdded', file)
+    })
+    this.dropzone.on('removedfile', file => {
+      vm.$emit('dropzone-removedFile', file)
+    })
+    this.dropzone.on('error', (file, error, xhr) => {
+      vm.$emit('dropzone-error', file, error, xhr)
+    })
+    this.dropzone.on('successmultiple', (file, error, xhr) => {
+      vm.$emit('dropzone-successmultiple', file, error, xhr)
+    })
+  },
+  destroyed() {
+    document.removeEventListener('paste', this.pasteImg)
+    this.dropzone.destroy()
+  },
+  methods: {
+    removeAllFiles() {
+      this.dropzone.removeAllFiles(true)
+    },
+    processQueue() {
+      this.dropzone.processQueue()
+    },
+    pasteImg(event) {
+      const items = (event.clipboardData || event.originalEvent.clipboardData).items
+      if (items[0].kind === 'file') {
+        this.dropzone.addFile(items[0].getAsFile())
+      }
+    },
+    initImages(val) {
+      if (!val) return
+      if (Array.isArray(val)) {
+        val.map((v, i) => {
+          const mockFile = { name: 'name' + i, size: 12345, url: v }
+          this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          return true
+        })
+      } else {
+        const mockFile = { name: 'name', size: 12345, url: val }
+        this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+        this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
+        mockFile.previewElement.classList.add('dz-success')
+        mockFile.previewElement.classList.add('dz-complete')
+      }
+    }
+
+  }
+}
+</script>
+
+<style scoped>
+    .dropzone {
+        border: 2px solid #E5E5E5;
+        font-family: 'Roboto', sans-serif;
+        color: #777;
+        transition: background-color .2s linear;
+        padding: 5px;
+    }
+
+    .dropzone:hover {
+        background-color: #F6F6F6;
+    }
+
+    i {
+        color: #CCC;
+    }
+
+    .dropzone .dz-image img {
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone input[name='file'] {
+        display: none;
+    }
+
+    .dropzone .dz-preview .dz-image {
+        border-radius: 0px;
+    }
+
+    .dropzone .dz-preview:hover .dz-image img {
+        transform: none;
+        filter: none;
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone .dz-preview .dz-details {
+        bottom: 0px;
+        top: 0px;
+        color: white;
+        background-color: rgba(33, 150, 243, 0.8);
+        transition: opacity .2s linear;
+        text-align: left;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
+        background-color: transparent;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:hover span {
+        background-color: transparent;
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-remove {
+        position: absolute;
+        z-index: 30;
+        color: white;
+        margin-left: 15px;
+        padding: 10px;
+        top: inherit;
+        bottom: 15px;
+        border: 2px white solid;
+        text-decoration: none;
+        text-transform: uppercase;
+        font-size: 0.8rem;
+        font-weight: 800;
+        letter-spacing: 1.1px;
+        opacity: 0;
+    }
+
+    .dropzone .dz-preview:hover .dz-remove {
+        opacity: 1;
+    }
+
+    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
+        margin-left: -40px;
+        margin-top: -50px;
+    }
+
+    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
+        color: white;
+        font-size: 5rem;
+    }
+</style>

+ 78 - 0
src/components/ErrorLog/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div v-if="errorLogs.length>0">
+    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
+      <el-button style="padding: 8px 10px;" size="small" type="danger">
+        <svg-icon icon-class="bug" />
+      </el-button>
+    </el-badge>
+
+    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
+      <div slot="title">
+        <span style="padding-right: 10px;">Error Log</span>
+        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
+      </div>
+      <el-table :data="errorLogs" border>
+        <el-table-column label="Message">
+          <template slot-scope="{row}">
+            <div>
+              <span class="message-title">Msg:</span>
+              <el-tag type="danger">
+                {{ row.err.message }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 10px;">Info: </span>
+              <el-tag type="warning">
+                {{ row.vm.$vnode.tag }} error in {{ row.info }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 16px;">Url: </span>
+              <el-tag type="success">
+                {{ row.url }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="Stack">
+          <template slot-scope="scope">
+            {{ scope.row.err.stack }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorLog',
+  data() {
+    return {
+      dialogTableVisible: false
+    }
+  },
+  computed: {
+    errorLogs() {
+      return this.$store.getters.errorLogs
+    }
+  },
+  methods: {
+    clearAll() {
+      this.dialogTableVisible = false
+      this.$store.dispatch('errorLog/clearErrorLog')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.message-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: bold;
+  padding-right: 8px;
+}
+</style>

+ 54 - 0
src/components/GithubCorner/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
+    <svg
+      width="80"
+      height="80"
+      viewBox="0 0 250 250"
+      style="fill:#40c9c6; color:#fff;"
+      aria-hidden="true"
+    >
+      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
+      <path
+        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
+        fill="currentColor"
+        style="transform-origin: 130px 106px;"
+        class="octo-arm"
+      />
+      <path
+        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
+        fill="currentColor"
+        class="octo-body"
+      />
+    </svg>
+  </a>
+</template>
+
+<style scoped>
+.github-corner:hover .octo-arm {
+  animation: octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+  0%,
+  100% {
+    transform: rotate(0)
+  }
+  20%,
+  60% {
+    transform: rotate(-25deg)
+  }
+  40%,
+  80% {
+    transform: rotate(10deg)
+  }
+}
+
+@media (max-width:500px) {
+  .github-corner:hover .octo-arm {
+    animation: none
+  }
+  .github-corner .octo-arm {
+    animation: octocat-wave 560ms ease-in-out
+  }
+}
+</style>

+ 44 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 180 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="Search"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    ::v-deep .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 1779 - 0
src/components/ImageCropper/index.vue

@@ -0,0 +1,1779 @@
+<template>
+  <div v-show="value" class="vue-image-crop-upload">
+    <div class="vicp-wrap">
+      <div class="vicp-close" @click="off">
+        <i class="vicp-icon4" />
+      </div>
+
+      <div v-show="step == 1" class="vicp-step1">
+        <div
+          class="vicp-drop-area"
+          @dragleave="preventDefault"
+          @dragover="preventDefault"
+          @dragenter="preventDefault"
+          @click="handleClick"
+          @drop="handleChange"
+        >
+          <i v-show="loading != 1" class="vicp-icon1">
+            <i class="vicp-icon1-arrow" />
+            <i class="vicp-icon1-body" />
+            <i class="vicp-icon1-bottom" />
+          </i>
+          <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
+          <span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span>
+          <input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange">
+        </div>
+        <div v-show="hasError" class="vicp-error">
+          <i class="vicp-icon2" />
+          {{ errorMsg }}
+        </div>
+        <div class="vicp-operate">
+          <a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 2" class="vicp-step2">
+        <div class="vicp-crop">
+          <div v-show="true" class="vicp-crop-left">
+            <div class="vicp-img-container">
+              <img
+                ref="img"
+                :src="sourceImgUrl"
+                :style="sourceImgStyle"
+                class="vicp-img"
+                draggable="false"
+                @drag="preventDefault"
+                @dragstart="preventDefault"
+                @dragend="preventDefault"
+                @dragleave="preventDefault"
+                @dragover="preventDefault"
+                @dragenter="preventDefault"
+                @drop="preventDefault"
+                @touchstart="imgStartMove"
+                @touchmove="imgMove"
+                @touchend="createImg"
+                @touchcancel="createImg"
+                @mousedown="imgStartMove"
+                @mousemove="imgMove"
+                @mouseup="createImg"
+                @mouseout="createImg"
+              >
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1" />
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2" />
+            </div>
+
+            <div class="vicp-range">
+              <input
+                :value="scale.range"
+                type="range"
+                step="1"
+                min="0"
+                max="100"
+                @input="zoomChange"
+              >
+              <i
+                class="vicp-icon5"
+                @mousedown="startZoomSub"
+                @mouseout="endZoomSub"
+                @mouseup="endZoomSub"
+              />
+              <i
+                class="vicp-icon6"
+                @mousedown="startZoomAdd"
+                @mouseout="endZoomAdd"
+                @mouseup="endZoomAdd"
+              />
+            </div>
+
+            <div v-if="!noRotate" class="vicp-rotate">
+              <i @mousedown="startRotateLeft" @mouseout="endRotate" @mouseup="endRotate">↺</i>
+              <i @mousedown="startRotateRight" @mouseout="endRotate" @mouseup="endRotate">↻</i>
+            </div>
+          </div>
+          <div v-show="true" class="vicp-crop-right">
+            <div class="vicp-preview">
+              <div v-if="!noSquare" class="vicp-preview-item">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+              <div v-if="!noCircle" class="vicp-preview-item vicp-preview-item-circle">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{ lang.btn.save }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 3" class="vicp-step3">
+        <div class="vicp-upload">
+          <span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span>
+          <div class="vicp-progress-wrap">
+            <span v-show="loading === 1" :style="progressStyle" class="vicp-progress" />
+          </div>
+          <div v-show="hasError" class="vicp-error">
+            <i class="vicp-icon2" />
+            {{ errorMsg }}
+          </div>
+          <div v-show="loading === 2" class="vicp-success">
+            <i class="vicp-icon3" />
+            {{ lang.success }}
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
+        </div>
+      </div>
+      <canvas v-show="false" ref="canvas" :width="width" :height="height" />
+    </div>
+  </div>
+</template>
+
+<script>
+'use strict'
+import request from '@/utils/request'
+import language from './utils/language.js'
+import mimes from './utils/mimes.js'
+import data2blob from './utils/data2blob.js'
+import effectRipple from './utils/effectRipple.js'
+export default {
+  props: {
+    // 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    field: {
+      type: String,
+      default: 'avatar'
+    },
+    // 原名key,类似于id,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    ki: {
+      type: Number,
+      default: 0
+    },
+    // 显示该控件与否
+    value: {
+      type: Boolean,
+      default: true
+    },
+    // 上传地址
+    url: {
+      type: String,
+      default: ''
+    },
+    // 其他要上传文件附带的数据,对象格式
+    params: {
+      type: Object,
+      default: null
+    },
+    // Add custom headers
+    headers: {
+      type: Object,
+      default: null
+    },
+    // 剪裁图片的宽
+    width: {
+      type: Number,
+      default: 200
+    },
+    // 剪裁图片的高
+    height: {
+      type: Number,
+      default: 200
+    },
+    // 不显示旋转功能
+    noRotate: {
+      type: Boolean,
+      default: true
+    },
+    // 不预览圆形图片
+    noCircle: {
+      type: Boolean,
+      default: false
+    },
+    // 不预览方形图片
+    noSquare: {
+      type: Boolean,
+      default: false
+    },
+    // 单文件大小限制
+    maxSize: {
+      type: Number,
+      default: 10240
+    },
+    // 语言类型
+    langType: {
+      type: String,
+      default: 'zh'
+    },
+    // 语言包
+    langExt: {
+      type: Object,
+      default: null
+    },
+    // 图片上传格式
+    imgFormat: {
+      type: String,
+      default: 'png'
+    },
+    // 是否支持跨域
+    withCredentials: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    const { imgFormat, langType, langExt, width, height } = this
+    let isSupported = true
+    const allowImgFormat = ['jpg', 'png']
+    const tempImgFormat =
+      allowImgFormat.indexOf(imgFormat) === -1 ? 'jpg' : imgFormat
+    const lang = language[langType] ? language[langType] : language['en']
+    const mime = mimes[tempImgFormat]
+    // 规范图片格式
+    this.imgFormat = tempImgFormat
+    if (langExt) {
+      Object.assign(lang, langExt)
+    }
+    if (typeof FormData !== 'function') {
+      isSupported = false
+    }
+    return {
+      // 图片的mime
+      mime,
+      // 语言包
+      lang,
+      // 浏览器是否支持该控件
+      isSupported,
+      // 浏览器是否支持触屏事件
+      // eslint-disable-next-line no-prototype-builtins
+      isSupportTouch: document.hasOwnProperty('ontouchstart'),
+      // 步骤
+      step: 1, // 1选择文件 2剪裁 3上传
+      // 上传状态及进度
+      loading: 0, // 0未开始 1正在 2成功 3错误
+      progress: 0,
+      // 是否有错误及错误信息
+      hasError: false,
+      errorMsg: '',
+      // 需求图宽高比
+      ratio: width / height,
+      // 原图地址、生成图片地址
+      sourceImg: null,
+      sourceImgUrl: '',
+      createImgUrl: '',
+      // 原图片拖动事件初始值
+      sourceImgMouseDown: {
+        on: false,
+        mX: 0, // 鼠标按下的坐标
+        mY: 0,
+        x: 0, // scale原图坐标
+        y: 0
+      },
+      // 生成图片预览的容器大小
+      previewContainer: {
+        width: 100,
+        height: 100
+      },
+      // 原图容器宽高
+      sourceImgContainer: {
+        // sic
+        width: 240,
+        height: 184 // 如果生成图比例与此一致会出现bug,先改成特殊的格式吧,哈哈哈
+      },
+      // 原图展示属性
+      scale: {
+        zoomAddOn: false, // 按钮缩放事件开启
+        zoomSubOn: false, // 按钮缩放事件开启
+        range: 1, // 最大100
+        rotateLeft: false, // 按钮向左旋转事件开启
+        rotateRight: false, // 按钮向右旋转事件开启
+        degree: 0, // 旋转度数
+        x: 0,
+        y: 0,
+        width: 0,
+        height: 0,
+        maxWidth: 0,
+        maxHeight: 0,
+        minWidth: 0, // 最宽
+        minHeight: 0,
+        naturalWidth: 0, // 原宽
+        naturalHeight: 0
+      }
+    }
+  },
+  computed: {
+    // 进度条样式
+    progressStyle() {
+      const { progress } = this
+      return {
+        width: progress + '%'
+      }
+    },
+    // 原图样式
+    sourceImgStyle() {
+      const { scale, sourceImgMasking } = this
+      const top = scale.y + sourceImgMasking.y + 'px'
+      const left = scale.x + sourceImgMasking.x + 'px'
+      return {
+        top,
+        left,
+        width: scale.width + 'px',
+        height: scale.height + 'px',
+        transform: 'rotate(' + scale.degree + 'deg)', // 旋转时 左侧原始图旋转样式
+        '-ms-transform': 'rotate(' + scale.degree + 'deg)', // 兼容IE9
+        '-moz-transform': 'rotate(' + scale.degree + 'deg)', // 兼容FireFox
+        '-webkit-transform': 'rotate(' + scale.degree + 'deg)', // 兼容Safari 和 chrome
+        '-o-transform': 'rotate(' + scale.degree + 'deg)' // 兼容 Opera
+      }
+    },
+    // 原图蒙版属性
+    sourceImgMasking() {
+      const { width, height, ratio, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sicRatio = sic.width / sic.height // 原图容器宽高比
+      let x = 0
+      let y = 0
+      let w = sic.width
+      let h = sic.height
+      let scale = 1
+      if (ratio < sicRatio) {
+        scale = sic.height / height
+        w = sic.height * ratio
+        x = (sic.width - w) / 2
+      }
+      if (ratio > sicRatio) {
+        scale = sic.width / width
+        h = sic.width / ratio
+        y = (sic.height - h) / 2
+      }
+      return {
+        scale, // 蒙版相对需求宽高的缩放
+        x,
+        y,
+        width: w,
+        height: h
+      }
+    },
+    // 原图遮罩样式
+    sourceImgShadeStyle() {
+      const { sourceImgMasking, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sim = sourceImgMasking
+      const w =
+        sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2
+      const h =
+        sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    },
+    previewStyle() {
+      const { ratio, previewContainer } = this
+      const pc = previewContainer
+      let w = pc.width
+      let h = pc.height
+      const pcRatio = w / h
+      if (ratio < pcRatio) {
+        w = pc.height * ratio
+      }
+      if (ratio > pcRatio) {
+        h = pc.width / ratio
+      }
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      if (newValue && this.loading !== 1) {
+        this.reset()
+      }
+    }
+  },
+  created() {
+    // 绑定按键esc隐藏此插件事件
+    document.addEventListener('keyup', this.closeHandler)
+  },
+  destroyed() {
+    document.removeEventListener('keyup', this.closeHandler)
+  },
+  methods: {
+    // 点击波纹效果
+    ripple(e) {
+      effectRipple(e)
+    },
+    // 关闭控件
+    off() {
+      setTimeout(() => {
+        this.$emit('input', false)
+        this.$emit('close')
+        if (this.step === 3 && this.loading === 2) {
+          this.setStep(1)
+        }
+      }, 200)
+    },
+    // 设置步骤
+    setStep(no) {
+      // 延时是为了显示动画效果呢,哈哈哈
+      setTimeout(() => {
+        this.step = no
+      }, 200)
+    },
+    /* 图片选择区域函数绑定
+     ---------------------------------------------------------------*/
+    preventDefault(e) {
+      e.preventDefault()
+      return false
+    },
+    handleClick(e) {
+      if (this.loading !== 1) {
+        if (e.target !== this.$refs.fileinput) {
+          e.preventDefault()
+          if (document.activeElement !== this.$refs) {
+            this.$refs.fileinput.click()
+          }
+        }
+      }
+    },
+    handleChange(e) {
+      e.preventDefault()
+      if (this.loading !== 1) {
+        const files = e.target.files || e.dataTransfer.files
+        this.reset()
+        if (this.checkFile(files[0])) {
+          this.setSourceImg(files[0])
+        }
+      }
+    },
+    /* ---------------------------------------------------------------*/
+    // 检测选择的文件是否合适
+    checkFile(file) {
+      const { lang, maxSize } = this
+      // 仅限图片
+      if (file.type.indexOf('image') === -1) {
+        this.hasError = true
+        this.errorMsg = lang.error.onlyImg
+        return false
+      }
+      // 超出大小
+      if (file.size / 1024 > maxSize) {
+        this.hasError = true
+        this.errorMsg = lang.error.outOfSize + maxSize + 'kb'
+        return false
+      }
+      return true
+    },
+    // 重置控件
+    reset() {
+      this.loading = 0
+      this.hasError = false
+      this.errorMsg = ''
+      this.progress = 0
+    },
+    // 设置图片源
+    setSourceImg(file) {
+      const fr = new FileReader()
+      fr.onload = e => {
+        this.sourceImgUrl = fr.result
+        this.startCrop()
+      }
+      fr.readAsDataURL(file)
+    },
+    // 剪裁前准备工作
+    startCrop() {
+      const {
+        width,
+        height,
+        ratio,
+        scale,
+        sourceImgUrl,
+        sourceImgMasking,
+        lang
+      } = this
+      const sim = sourceImgMasking
+      const img = new Image()
+      img.src = sourceImgUrl
+      img.onload = () => {
+        const nWidth = img.naturalWidth
+        const nHeight = img.naturalHeight
+        const nRatio = nWidth / nHeight
+        let w = sim.width
+        let h = sim.height
+        let x = 0
+        let y = 0
+        // 图片像素不达标
+        if (nWidth < width || nHeight < height) {
+          this.hasError = true
+          this.errorMsg = lang.error.lowestPx + width + '*' + height
+          return false
+        }
+        if (ratio > nRatio) {
+          h = w / nRatio
+          y = (sim.height - h) / 2
+        }
+        if (ratio < nRatio) {
+          w = h * nRatio
+          x = (sim.width - w) / 2
+        }
+        scale.range = 0
+        scale.x = x
+        scale.y = y
+        scale.width = w
+        scale.height = h
+        scale.degree = 0
+        scale.minWidth = w
+        scale.minHeight = h
+        scale.maxWidth = nWidth * sim.scale
+        scale.maxHeight = nHeight * sim.scale
+        scale.naturalWidth = nWidth
+        scale.naturalHeight = nHeight
+        this.sourceImg = img
+        this.createImg()
+        this.setStep(2)
+      }
+    },
+    // 鼠标按下图片准备移动
+    imgStartMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const { sourceImgMouseDown, scale } = this
+      const simd = sourceImgMouseDown
+      simd.mX = et.screenX
+      simd.mY = et.screenY
+      simd.x = scale.x
+      simd.y = scale.y
+      simd.on = true
+    },
+    // 鼠标按下状态下移动,图片移动
+    imgMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const {
+        sourceImgMouseDown: { on, mX, mY, x, y },
+        scale,
+        sourceImgMasking
+      } = this
+      const sim = sourceImgMasking
+      const nX = et.screenX
+      const nY = et.screenY
+      const dX = nX - mX
+      const dY = nY - mY
+      let rX = x + dX
+      let rY = y + dY
+      if (!on) return
+      if (rX > 0) {
+        rX = 0
+      }
+      if (rY > 0) {
+        rY = 0
+      }
+      if (rX < sim.width - scale.width) {
+        rX = sim.width - scale.width
+      }
+      if (rY < sim.height - scale.height) {
+        rY = sim.height - scale.height
+      }
+      scale.x = rX
+      scale.y = rY
+    },
+    // 按钮按下开始向右旋转
+    startRotateRight(e) {
+      const { scale } = this
+      scale.rotateRight = true
+      const rotate = () => {
+        if (scale.rotateRight) {
+          const degree = ++scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 按钮按下开始向左旋转
+    startRotateLeft(e) {
+      const { scale } = this
+      scale.rotateLeft = true
+      const rotate = () => {
+        if (scale.rotateLeft) {
+          const degree = --scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 停止旋转
+    endRotate() {
+      const { scale } = this
+      scale.rotateLeft = false
+      scale.rotateRight = false
+    },
+    // 按钮按下开始放大
+    startZoomAdd(e) {
+      const { scale } = this
+      scale.zoomAddOn = true
+      const zoom = () => {
+        if (scale.zoomAddOn) {
+          const range = scale.range >= 100 ? 100 : ++scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消放大
+    endZoomAdd(e) {
+      this.scale.zoomAddOn = false
+    },
+    // 按钮按下开始缩小
+    startZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = true
+      const zoom = () => {
+        if (scale.zoomSubOn) {
+          const range = scale.range <= 0 ? 0 : --scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消缩小
+    endZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = false
+    },
+    zoomChange(e) {
+      this.zoomImg(e.target.value)
+    },
+    // 缩放原图
+    zoomImg(newRange) {
+      const { sourceImgMasking, scale } = this
+      const {
+        maxWidth,
+        maxHeight,
+        minWidth,
+        minHeight,
+        width,
+        height,
+        x,
+        y
+      } = scale
+      const sim = sourceImgMasking
+      // 蒙版宽高
+      const sWidth = sim.width
+      const sHeight = sim.height
+      // 新宽高
+      const nWidth = minWidth + ((maxWidth - minWidth) * newRange) / 100
+      const nHeight = minHeight + ((maxHeight - minHeight) * newRange) / 100
+      // 新坐标(根据蒙版中心点缩放)
+      let nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x)
+      let nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y)
+      // 判断新坐标是否超过蒙版限制
+      if (nX > 0) {
+        nX = 0
+      }
+      if (nY > 0) {
+        nY = 0
+      }
+      if (nX < sWidth - nWidth) {
+        nX = sWidth - nWidth
+      }
+      if (nY < sHeight - nHeight) {
+        nY = sHeight - nHeight
+      }
+      // 赋值处理
+      scale.x = nX
+      scale.y = nY
+      scale.width = nWidth
+      scale.height = nHeight
+      scale.range = newRange
+      setTimeout(() => {
+        if (scale.range === newRange) {
+          this.createImg()
+        }
+      }, 300)
+    },
+    // 生成需求图片
+    createImg(e) {
+      const {
+        mime,
+        sourceImg,
+        scale: { x, y, width, height, degree },
+        sourceImgMasking: { scale }
+      } = this
+      const canvas = this.$refs.canvas
+      const ctx = canvas.getContext('2d')
+      if (e) {
+        // 取消鼠标按下移动状态
+        this.sourceImgMouseDown.on = false
+      }
+      canvas.width = this.width
+      canvas.height = this.height
+      ctx.clearRect(0, 0, this.width, this.height)
+      // 将透明区域设置为白色底边
+      ctx.fillStyle = '#fff'
+      ctx.fillRect(0, 0, this.width, this.height)
+      ctx.translate(this.width * 0.5, this.height * 0.5)
+      ctx.rotate((Math.PI * degree) / 180)
+      ctx.translate(-this.width * 0.5, -this.height * 0.5)
+      ctx.drawImage(
+        sourceImg,
+        x / scale,
+        y / scale,
+        width / scale,
+        height / scale
+      )
+      this.createImgUrl = canvas.toDataURL(mime)
+    },
+    prepareUpload() {
+      const { url, createImgUrl, field, ki } = this
+      this.$emit('crop-success', createImgUrl, field, ki)
+      if (typeof url === 'string' && url) {
+        this.upload()
+      } else {
+        this.off()
+      }
+    },
+    // 上传图片
+    upload() {
+      const {
+        lang,
+        imgFormat,
+        mime,
+        url,
+        params,
+        field,
+        ki,
+        createImgUrl
+      } = this
+      const fmData = new FormData()
+      fmData.append(
+        field,
+        data2blob(createImgUrl, mime),
+        field + '.' + imgFormat
+      )
+      // 添加其他参数
+      if (typeof params === 'object' && params) {
+        Object.keys(params).forEach(k => {
+          fmData.append(k, params[k])
+        })
+      }
+      // 监听进度回调
+      // const uploadProgress = (event) => {
+      //   if (event.lengthComputable) {
+      //     this.progress = 100 * Math.round(event.loaded) / event.total
+      //   }
+      // }
+      // 上传文件
+      this.reset()
+      this.loading = 1
+      this.setStep(3)
+      request({
+        url,
+        method: 'post',
+        data: fmData
+      })
+        .then(resData => {
+          this.loading = 2
+          this.$emit('crop-upload-success', resData.data)
+        })
+        .catch(err => {
+          if (this.value) {
+            this.loading = 3
+            this.hasError = true
+            this.errorMsg = lang.fail
+            this.$emit('crop-upload-fail', err, field, ki)
+          }
+        })
+    },
+    closeHandler(e) {
+      if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {
+        this.off()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@charset "UTF-8";
+@-webkit-keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@-webkit-keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+@keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+.vue-image-crop-upload {
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.65);
+  -webkit-tap-highlight-color: transparent;
+  -moz-tap-highlight-color: transparent;
+}
+.vue-image-crop-upload .vicp-wrap {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 600px;
+  height: 330px;
+  padding: 25px;
+  background-color: #fff;
+  border-radius: 2px;
+  -webkit-animation: vicp 0.12s ease-in;
+  animation: vicp 0.12s ease-in;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close {
+  position: absolute;
+  right: -30px;
+  top: -30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
+  position: relative;
+  display: block;
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+  -webkit-transition: -webkit-transform 0.18s;
+  transition: -webkit-transform 0.18s;
+  transition: transform 0.18s;
+  transition: transform 0.18s, -webkit-transform 0.18s;
+  -webkit-transform: rotate(0);
+  -ms-transform: rotate(0);
+  transform: rotate(0);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after,
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  content: "";
+  position: absolute;
+  top: 12px;
+  left: 4px;
+  width: 20px;
+  height: 3px;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  background-color: #fff;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
+  -webkit-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  transform: rotate(90deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
+  display: block;
+  margin: 0 auto 6px;
+  width: 42px;
+  height: 42px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-arrow {
+  display: block;
+  margin: 0 auto;
+  width: 0;
+  height: 0;
+  border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
+  border-left: 14.7px solid transparent;
+  border-right: 14.7px solid transparent;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-body {
+  display: block;
+  width: 12.6px;
+  height: 14.7px;
+  margin: 0 auto;
+  background-color: rgba(0, 0, 0, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-bottom {
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  display: block;
+  height: 12.6px;
+  border: 6px solid rgba(0, 0, 0, 0.3);
+  border-top: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
+  display: block;
+  padding: 15px;
+  font-size: 14px;
+  color: #666;
+  line-height: 30px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-no-supported-hint {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 30px;
+  width: 100%;
+  height: 60px;
+  line-height: 30px;
+  background-color: #eee;
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
+  cursor: pointer;
+  border-color: rgba(0, 0, 0, 0.1);
+  background-color: rgba(0, 0, 0, 0.05);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container {
+  position: relative;
+  display: block;
+  width: 240px;
+  height: 180px;
+  background-color: #e5e5e0;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img {
+  position: absolute;
+  display: block;
+  cursor: move;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  position: absolute;
+  background-color: rgba(241, 242, 243, 0.8);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-1 {
+  top: 0;
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-2 {
+  bottom: 0;
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate {
+  position: relative;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i {
+  display: block;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  line-height: 18px;
+  text-align: center;
+  font-size: 12px;
+  font-weight: bold;
+  background-color: rgba(0, 0, 0, 0.08);
+  color: #fff;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:first-child {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:last-child {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range {
+  position: relative;
+  margin: 30px 0 10px 0;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  position: absolute;
+  top: 0;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.08);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5:hover,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5 {
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::after {
+  position: absolute;
+  content: "";
+  display: block;
+  top: 3px;
+  left: 8px;
+  width: 2px;
+  height: 12px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"] {
+  display: block;
+  padding-top: 5px;
+  margin: 0 auto;
+  width: 180px;
+  height: 8px;
+  vertical-align: top;
+  background: transparent;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  cursor: pointer;
+  /* 滑块
+               ---------------------------------------------------------------*/
+  /* 轨道
+               ---------------------------------------------------------------*/
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus {
+  outline: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -webkit-appearance: none;
+  appearance: none;
+  margin-top: -3px;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -moz-appearance: none;
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border: none;
+  border-radius: 100%;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-moz-range-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-ms-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  margin-top: -4px;
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-runnable-track {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  cursor: pointer;
+  background: transparent;
+  border-color: transparent;
+  color: transparent;
+  height: 6px;
+  border-radius: 2px;
+  border: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.15);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-webkit-slider-runnable-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-moz-range-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.45);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.25);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview {
+  height: 150px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item {
+  position: relative;
+  padding: 5px;
+  width: 100px;
+  height: 100px;
+  float: left;
+  margin-right: 16px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  span {
+  position: absolute;
+  bottom: -30px;
+  width: 100%;
+  font-size: 14px;
+  color: #bbb;
+  display: block;
+  text-align: center;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  img {
+  position: absolute;
+  display: block;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  padding: 3px;
+  background-color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle {
+  margin-right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle
+  img {
+  border-radius: 100%;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed #ddd;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
+  display: block;
+  padding: 15px;
+  font-size: 16px;
+  color: #999;
+  line-height: 30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
+  margin-top: 12px;
+  background-color: rgba(0, 0, 0, 0.08);
+  border-radius: 3px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress {
+  position: relative;
+  display: block;
+  height: 5px;
+  border-radius: 3px;
+  background-color: #4a7;
+  -webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  -webkit-transition: width 0.15s linear;
+  transition: width 0.15s linear;
+  background-image: -webkit-linear-gradient(
+    135deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-image: linear-gradient(
+    -45deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-size: 40px 40px;
+  -webkit-animation: vicp_progress 0.5s linear infinite;
+  animation: vicp_progress 0.5s linear infinite;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress::after {
+  content: "";
+  position: absolute;
+  display: block;
+  top: -3px;
+  right: -3px;
+  width: 9px;
+  height: 9px;
+  border: 1px solid rgba(245, 246, 247, 0.7);
+  -webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  border-radius: 100%;
+  background-color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
+  height: 100px;
+  line-height: 100px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate {
+  position: absolute;
+  right: 20px;
+  bottom: 20px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a {
+  position: relative;
+  float: left;
+  display: block;
+  margin-left: 10px;
+  width: 100px;
+  height: 36px;
+  line-height: 36px;
+  text-align: center;
+  cursor: pointer;
+  font-size: 14px;
+  color: #4a7;
+  border-radius: 2px;
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
+  background-color: rgba(0, 0, 0, 0.03);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  display: block;
+  font-size: 14px;
+  line-height: 24px;
+  height: 24px;
+  color: #d10;
+  text-align: center;
+  vertical-align: top;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
+  position: absolute;
+  top: 3px;
+  left: 6px;
+  width: 6px;
+  height: 10px;
+  border-width: 0 2px 2px 0;
+  border-color: #4a7;
+  border-style: solid;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  content: "";
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after,
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
+  content: "";
+  position: absolute;
+  top: 9px;
+  left: 4px;
+  width: 13px;
+  height: 2px;
+  background-color: #d10;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.e-ripple {
+  position: absolute;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.15);
+  background-clip: padding-box;
+  pointer-events: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  -webkit-transform: scale(0);
+  -ms-transform: scale(0);
+  transform: scale(0);
+  opacity: 1;
+}
+.e-ripple.z-active {
+  opacity: 0;
+  -webkit-transform: scale(2);
+  -ms-transform: scale(2);
+  transform: scale(2);
+  -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out,
+    -webkit-transform 0.6s ease-out;
+}
+</style>

+ 19 - 0
src/components/ImageCropper/utils/data2blob.js

@@ -0,0 +1,19 @@
+/**
+ * database64文件格式转换为2进制
+ *
+ * @param  {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
+ * @param  {[String]} mime [description]
+ * @return {[blob]}      [description]
+ */
+export default function(data, mime) {
+  data = data.split(',')[1]
+  data = window.atob(data)
+  var ia = new Uint8Array(data.length)
+  for (var i = 0; i < data.length; i++) {
+    ia[i] = data.charCodeAt(i)
+  }
+  // canvas.toDataURL 返回的默认格式就是 image/png
+  return new Blob([ia], {
+    type: mime
+  })
+}

+ 39 - 0
src/components/ImageCropper/utils/effectRipple.js

@@ -0,0 +1,39 @@
+/**
+ * 点击波纹效果
+ *
+ * @param  {[event]} e        [description]
+ * @param  {[Object]} arg_opts [description]
+ * @return {[bollean]}          [description]
+ */
+export default function(e, arg_opts) {
+  var opts = Object.assign({
+    ele: e.target, // 波纹作用元素
+    type: 'hit', // hit点击位置扩散center中心点扩展
+    bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+  }, arg_opts)
+  var target = opts.ele
+  if (target) {
+    var rect = target.getBoundingClientRect()
+    var ripple = target.querySelector('.e-ripple')
+    if (!ripple) {
+      ripple = document.createElement('span')
+      ripple.className = 'e-ripple'
+      ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+      target.appendChild(ripple)
+    } else {
+      ripple.className = 'e-ripple'
+    }
+    switch (opts.type) {
+      case 'center':
+        ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
+        ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
+        break
+      default:
+        ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
+        ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
+    }
+    ripple.style.backgroundColor = opts.bgc
+    ripple.className = 'e-ripple z-active'
+    return false
+  }
+}

+ 232 - 0
src/components/ImageCropper/utils/language.js

@@ -0,0 +1,232 @@
+export default {
+  zh: {
+    hint: '点击,或拖动图片至此处',
+    loading: '正在上传……',
+    noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
+    success: '上传成功',
+    fail: '图片上传失败',
+    preview: '头像预览',
+    btn: {
+      off: '取消',
+      close: '关闭',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '仅限图片格式',
+      outOfSize: '单文件大小不能超过 ',
+      lowestPx: '图片最低像素为(宽*高):'
+    }
+  },
+  'zh-tw': {
+    hint: '點擊,或拖動圖片至此處',
+    loading: '正在上傳……',
+    noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
+    success: '上傳成功',
+    fail: '圖片上傳失敗',
+    preview: '頭像預覽',
+    btn: {
+      off: '取消',
+      close: '關閉',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '僅限圖片格式',
+      outOfSize: '單文件大小不能超過 ',
+      lowestPx: '圖片最低像素為(寬*高):'
+    }
+  },
+  en: {
+    hint: 'Click or drag the file here to upload',
+    loading: 'Uploading…',
+    noSupported: 'Browser is not supported, please use IE10+ or other browsers',
+    success: 'Upload success',
+    fail: 'Upload failed',
+    preview: 'Preview',
+    btn: {
+      off: 'Cancel',
+      close: 'Close',
+      back: 'Back',
+      save: 'Save'
+    },
+    error: {
+      onlyImg: 'Image only',
+      outOfSize: 'Image exceeds size limit: ',
+      lowestPx: 'Image\'s size is too low. Expected at least: '
+    }
+  },
+  ro: {
+    hint: 'Atinge sau trage fișierul aici',
+    loading: 'Se încarcă',
+    noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
+    success: 'S-a încărcat cu succes',
+    fail: 'A apărut o problemă la încărcare',
+    preview: 'Previzualizează',
+
+    btn: {
+      off: 'Anulează',
+      close: 'Închide',
+      back: 'Înapoi',
+      save: 'Salvează'
+    },
+
+    error: {
+      onlyImg: 'Doar imagini',
+      outOfSize: 'Imaginea depășește limita de: ',
+      loewstPx: 'Imaginea este prea mică; Minim: '
+    }
+  },
+  ru: {
+    hint: 'Нажмите, или перетащите файл в это окно',
+    loading: 'Загружаю……',
+    noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
+    success: 'Загрузка выполнена успешно',
+    fail: 'Ошибка загрузки',
+    preview: 'Предпросмотр',
+    btn: {
+      off: 'Отменить',
+      close: 'Закрыть',
+      back: 'Назад',
+      save: 'Сохранить'
+    },
+    error: {
+      onlyImg: 'Только изображения',
+      outOfSize: 'Изображение превышает предельный размер: ',
+      lowestPx: 'Минимальный размер изображения: '
+    }
+  },
+  'pt-br': {
+    hint: 'Clique ou arraste o arquivo aqui para carregar',
+    loading: 'Carregando…',
+    noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
+    success: 'Sucesso ao carregar imagem',
+    fail: 'Falha ao carregar imagem',
+    preview: 'Pré-visualizar',
+    btn: {
+      off: 'Cancelar',
+      close: 'Fechar',
+      back: 'Voltar',
+      save: 'Salvar'
+    },
+    error: {
+      onlyImg: 'Apenas imagens',
+      outOfSize: 'A imagem excede o limite de tamanho: ',
+      lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
+    }
+  },
+  fr: {
+    hint: 'Cliquez ou glissez le fichier ici.',
+    loading: 'Téléchargement…',
+    noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
+    success: 'Téléchargement réussit',
+    fail: 'Téléchargement echoué',
+    preview: 'Aperçu',
+    btn: {
+      off: 'Annuler',
+      close: 'Fermer',
+      back: 'Retour',
+      save: 'Enregistrer'
+    },
+    error: {
+      onlyImg: 'Image uniquement',
+      outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
+      lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
+    }
+  },
+  nl: {
+    hint: 'Klik hier of sleep een afbeelding in dit vlak',
+    loading: 'Uploaden…',
+    noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
+    success: 'Upload succesvol',
+    fail: 'Upload mislukt',
+    preview: 'Voorbeeld',
+    btn: {
+      off: 'Annuleren',
+      close: 'Sluiten',
+      back: 'Terug',
+      save: 'Opslaan'
+    },
+    error: {
+      onlyImg: 'Alleen afbeeldingen',
+      outOfSize: 'De afbeelding is groter dan: ',
+      lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
+    }
+  },
+  tr: {
+    hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
+    loading: 'Yükleniyor…',
+    noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
+    success: 'Yükleme başarılı',
+    fail: 'Yüklemede hata oluştu',
+    preview: 'Önizle',
+    btn: {
+      off: 'İptal',
+      close: 'Kapat',
+      back: 'Geri',
+      save: 'Kaydet'
+    },
+    error: {
+      onlyImg: 'Sadece resim',
+      outOfSize: 'Resim yükleme limitini aşıyor: ',
+      lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
+    }
+  },
+  'es-MX': {
+    hint: 'Selecciona o arrastra una imagen',
+    loading: 'Subiendo...',
+    noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
+    success: 'Subido exitosamente',
+    fail: 'Sucedió un error',
+    preview: 'Vista previa',
+    btn: {
+      off: 'Cancelar',
+      close: 'Cerrar',
+      back: 'Atras',
+      save: 'Guardar'
+    },
+    error: {
+      onlyImg: 'Unicamente imagenes',
+      outOfSize: 'La imagen excede el tamaño maximo:',
+      lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
+    }
+  },
+  de: {
+    hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
+    loading: 'Hochladen…',
+    noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
+    success: 'Upload erfolgreich',
+    fail: 'Upload fehlgeschlagen',
+    preview: 'Vorschau',
+    btn: {
+      off: 'Abbrechen',
+      close: 'Schließen',
+      back: 'Zurück',
+      save: 'Speichern'
+    },
+    error: {
+      onlyImg: 'Nur Bilder',
+      outOfSize: 'Das Bild ist zu groß: ',
+      lowestPx: 'Das Bild ist zu klein. Mindestens: '
+    }
+  },
+  ja: {
+    hint: 'クリック・ドラッグしてファイルをアップロード',
+    loading: 'アップロード中...',
+    noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
+    success: 'アップロード成功',
+    fail: 'アップロード失敗',
+    preview: 'プレビュー',
+    btn: {
+      off: 'キャンセル',
+      close: '閉じる',
+      back: '戻る',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '画像のみ',
+      outOfSize: '画像サイズが上限を超えています。上限: ',
+      lowestPx: '画像が小さすぎます。最小サイズ: '
+    }
+  }
+}

+ 7 - 0
src/components/ImageCropper/utils/mimes.js

@@ -0,0 +1,7 @@
+export default {
+  'jpg': 'image/jpeg',
+  'png': 'image/png',
+  'gif': 'image/gif',
+  'svg': 'image/svg+xml',
+  'psd': 'image/photoshop'
+}

+ 77 - 0
src/components/JsonEditor/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="json-editor">
+    <textarea ref="textarea" />
+  </div>
+</template>
+
+<script>
+import CodeMirror from 'codemirror'
+import 'codemirror/addon/lint/lint.css'
+import 'codemirror/lib/codemirror.css'
+import 'codemirror/theme/rubyblue.css'
+require('script-loader!jsonlint')
+import 'codemirror/mode/javascript/javascript'
+import 'codemirror/addon/lint/lint'
+import 'codemirror/addon/lint/json-lint'
+
+export default {
+  name: 'JsonEditor',
+  /* eslint-disable vue/require-prop-types */
+  props: ['value'],
+  data() {
+    return {
+      jsonEditor: false
+    }
+  },
+  watch: {
+    value(value) {
+      const editorValue = this.jsonEditor.getValue()
+      if (value !== editorValue) {
+        this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+      }
+    }
+  },
+  mounted() {
+    this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
+      lineNumbers: true,
+      mode: 'application/json',
+      gutters: ['CodeMirror-lint-markers'],
+      theme: 'rubyblue',
+      lint: true
+    })
+
+    this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+    this.jsonEditor.on('change', cm => {
+      this.$emit('changed', cm.getValue())
+      this.$emit('input', cm.getValue())
+    })
+  },
+  methods: {
+    getValue() {
+      return this.jsonEditor.getValue()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.json-editor {
+  height: 100%;
+  position: relative;
+
+  ::v-deep {
+    .CodeMirror {
+      height: auto;
+      min-height: 300px;
+    }
+
+    .CodeMirror-scroll {
+      min-height: 300px;
+    }
+
+    .cm-s-rubyblue span.cm-string {
+      color: #F08047;
+    }
+  }
+}
+</style>

+ 99 - 0
src/components/Kanban/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="board-column">
+    <div class="board-column-header">
+      {{ headerText }}
+    </div>
+    <draggable
+      :list="list"
+      v-bind="$attrs"
+      class="board-column-content"
+      :set-data="setData"
+    >
+      <div v-for="element in list" :key="element.id" class="board-item">
+        {{ element.name }} {{ element.id }}
+      </div>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DragKanbanDemo',
+  components: {
+    draggable
+  },
+  props: {
+    headerText: {
+      type: String,
+      default: 'Header'
+    },
+    options: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    list: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  methods: {
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.board-column {
+  min-width: 300px;
+  min-height: 100px;
+  height: auto;
+  overflow: hidden;
+  background: #f0f0f0;
+  border-radius: 3px;
+
+  .board-column-header {
+    height: 50px;
+    line-height: 50px;
+    overflow: hidden;
+    padding: 0 20px;
+    text-align: center;
+    background: #333;
+    color: #fff;
+    border-radius: 3px 3px 0 0;
+  }
+
+  .board-column-content {
+    height: auto;
+    overflow: hidden;
+    border: 10px solid transparent;
+    min-height: 60px;
+    display: flex;
+    justify-content: flex-start;
+    flex-direction: column;
+    align-items: center;
+
+    .board-item {
+      cursor: pointer;
+      width: 100%;
+      height: 64px;
+      margin: 5px 0;
+      background-color: #fff;
+      text-align: left;
+      line-height: 54px;
+      padding: 5px 10px;
+      box-sizing: border-box;
+      box-shadow: 0px 1px 3px 0 rgba(0, 0, 0, 0.2);
+    }
+  }
+}
+</style>
+

+ 360 - 0
src/components/MDinput/index.vue

@@ -0,0 +1,360 @@
+<template>
+  <div :class="computedClasses" class="material-input__component">
+    <div :class="{iconClass:icon}">
+      <i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon" />
+      <input
+        v-if="type === 'email'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="email"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'url'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="url"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'number'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :step="step"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="number"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'password'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :required="required"
+        type="password"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'tel'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="tel"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'text'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="text"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <span class="material-input-bar" />
+      <label class="material-label">
+        <slot />
+      </label>
+    </div>
+  </div>
+</template>
+
+<script>
+// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
+
+export default {
+  name: 'MdInput',
+  props: {
+    /* eslint-disable */
+    icon: String,
+    name: String,
+    type: {
+      type: String,
+      default: 'text'
+    },
+    value: [String, Number],
+    placeholder: String,
+    readonly: Boolean,
+    disabled: Boolean,
+    min: String,
+    max: String,
+    step: String,
+    minlength: Number,
+    maxlength: Number,
+    required: {
+      type: Boolean,
+      default: true
+    },
+    autoComplete: {
+      type: String,
+      default: 'off'
+    },
+    validateEvent: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      currentValue: this.value,
+      focus: false,
+      fillPlaceHolder: null
+    }
+  },
+  computed: {
+    computedClasses() {
+      return {
+        'material--active': this.focus,
+        'material--disabled': this.disabled,
+        'material--raised': Boolean(this.focus || this.currentValue) // has value
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      this.currentValue = newValue
+    }
+  },
+  methods: {
+    handleModelInput(event) {
+      const value = event.target.value
+      this.$emit('input', value)
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.change', [value])
+        }
+      }
+      this.$emit('change', value)
+    },
+    handleMdFocus(event) {
+      this.focus = true
+      this.$emit('focus', event)
+      if (this.placeholder && this.placeholder !== '') {
+        this.fillPlaceHolder = this.placeholder
+      }
+    },
+    handleMdBlur(event) {
+      this.focus = false
+      this.$emit('blur', event)
+      this.fillPlaceHolder = null
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.blur', [this.currentValue])
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  // Fonts:
+  $font-size-base: 16px;
+  $font-size-small: 18px;
+  $font-size-smallest: 12px;
+  $font-weight-normal: normal;
+  $font-weight-bold: bold;
+  $apixel: 1px;
+  // Utils
+  $spacer: 12px;
+  $transition: 0.2s ease all;
+  $index: 0px;
+  $index-has-icon: 30px;
+  // Theme:
+  $color-white: white;
+  $color-grey: #9E9E9E;
+  $color-grey-light: #E0E0E0;
+  $color-blue: #2196F3;
+  $color-red: #F44336;
+  $color-black: black;
+  // Base clases:
+  %base-bar-pseudo {
+    content: '';
+    height: 1px;
+    width: 0;
+    bottom: 0;
+    position: absolute;
+    transition: $transition;
+  }
+
+  // Mixins:
+  @mixin slided-top() {
+    top: - ($font-size-base + $spacer);
+    left: 0;
+    font-size: $font-size-base;
+    font-weight: $font-weight-bold;
+  }
+
+  // Component:
+  .material-input__component {
+    margin-top: 36px;
+    position: relative;
+    * {
+      box-sizing: border-box;
+    }
+    .iconClass {
+      .material-input__icon {
+        position: absolute;
+        left: 0;
+        line-height: $font-size-base;
+        color: $color-blue;
+        top: $spacer;
+        width: $index-has-icon;
+        height: $font-size-base;
+        font-size: $font-size-base;
+        font-weight: $font-weight-normal;
+        pointer-events: none;
+      }
+      .material-label {
+        left: $index-has-icon;
+      }
+      .material-input {
+        text-indent: $index-has-icon;
+      }
+    }
+    .material-input {
+      font-size: $font-size-base;
+      padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
+      display: block;
+      width: 100%;
+      border: none;
+      line-height: 1;
+      border-radius: 0;
+      &:focus {
+        outline: none;
+        border: none;
+        border-bottom: 1px solid transparent; // fixes the height issue
+      }
+    }
+    .material-label {
+      font-weight: $font-weight-normal;
+      position: absolute;
+      pointer-events: none;
+      left: $index;
+      top: 0;
+      transition: $transition;
+      font-size: $font-size-small;
+    }
+    .material-input-bar {
+      position: relative;
+      display: block;
+      width: 100%;
+      &:before {
+        @extend %base-bar-pseudo;
+        left: 50%;
+      }
+      &:after {
+        @extend %base-bar-pseudo;
+        right: 50%;
+      }
+    }
+    // Disabled state:
+    &.material--disabled {
+      .material-input {
+        border-bottom-style: dashed;
+      }
+    }
+    // Raised state:
+    &.material--raised {
+      .material-label {
+        @include slided-top();
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-input-bar {
+        &:before,
+        &:after {
+          width: 50%;
+        }
+      }
+    }
+  }
+
+  .material-input__component {
+    background: $color-white;
+    .material-input {
+      background: none;
+      color: $color-black;
+      text-indent: $index;
+      border-bottom: 1px solid $color-grey-light;
+    }
+    .material-label {
+      color: $color-grey;
+    }
+    .material-input-bar {
+      &:before,
+      &:after {
+        background: $color-blue;
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-label {
+        color: $color-blue;
+      }
+    }
+    // Errors:
+    &.material--has-errors {
+      &.material--active .material-label {
+        color: $color-red;
+      }
+      .material-input-bar {
+        &:before,
+        &:after {
+          background: transparent;
+        }
+      }
+    }
+  }
+</style>

+ 31 - 0
src/components/MarkdownEditor/default-options.js

@@ -0,0 +1,31 @@
+// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
+export default {
+  minHeight: '200px',
+  previewStyle: 'vertical',
+  useCommandShortcut: true,
+  useDefaultHTMLSanitizer: true,
+  usageStatistics: false,
+  hideModeSwitch: false,
+  toolbarItems: [
+    'heading',
+    'bold',
+    'italic',
+    'strike',
+    'divider',
+    'hr',
+    'quote',
+    'divider',
+    'ul',
+    'ol',
+    'task',
+    'indent',
+    'outdent',
+    'divider',
+    'table',
+    'image',
+    'link',
+    'divider',
+    'code',
+    'codeblock'
+  ]
+}

+ 118 - 0
src/components/MarkdownEditor/index.vue

@@ -0,0 +1,118 @@
+<template>
+  <div :id="id" />
+</template>
+
+<script>
+// deps for editor
+import 'codemirror/lib/codemirror.css' // codemirror
+import 'tui-editor/dist/tui-editor.css' // editor ui
+import 'tui-editor/dist/tui-editor-contents.css' // editor content
+
+import Editor from 'tui-editor'
+import defaultOptions from './default-options'
+
+export default {
+  name: 'MarkdownEditor',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    id: {
+      type: String,
+      required: false,
+      default() {
+        return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    options: {
+      type: Object,
+      default() {
+        return defaultOptions
+      }
+    },
+    mode: {
+      type: String,
+      default: 'markdown'
+    },
+    height: {
+      type: String,
+      required: false,
+      default: '300px'
+    },
+    language: {
+      type: String,
+      required: false,
+      default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
+    }
+  },
+  data() {
+    return {
+      editor: null
+    }
+  },
+  computed: {
+    editorOptions() {
+      const options = Object.assign({}, defaultOptions, this.options)
+      options.initialEditType = this.mode
+      options.height = this.height
+      options.language = this.language
+      return options
+    }
+  },
+  watch: {
+    value(newValue, preValue) {
+      if (newValue !== preValue && newValue !== this.editor.getValue()) {
+        this.editor.setValue(newValue)
+      }
+    },
+    language(val) {
+      this.destroyEditor()
+      this.initEditor()
+    },
+    height(newValue) {
+      this.editor.height(newValue)
+    },
+    mode(newValue) {
+      this.editor.changeMode(newValue)
+    }
+  },
+  mounted() {
+    this.initEditor()
+  },
+  destroyed() {
+    this.destroyEditor()
+  },
+  methods: {
+    initEditor() {
+      this.editor = new Editor({
+        el: document.getElementById(this.id),
+        ...this.editorOptions
+      })
+      if (this.value) {
+        this.editor.setValue(this.value)
+      }
+      this.editor.on('change', () => {
+        this.$emit('input', this.editor.getValue())
+      })
+    },
+    destroyEditor() {
+      if (!this.editor) return
+      this.editor.off('change')
+      this.editor.remove()
+    },
+    setValue(value) {
+      this.editor.setValue(value)
+    },
+    getValue() {
+      return this.editor.getValue()
+    },
+    setHtml(value) {
+      this.editor.setHtml(value)
+    },
+    getHtml() {
+      return this.editor.getHtml()
+    }
+  }
+}
+</script>

+ 13 - 0
src/components/MyChat/IChat/index.js

@@ -0,0 +1,13 @@
+
+/**
+ *  chat interface 插槽
+ */
+
+// 导入组件,组件必须声明 name
+import IChat from './src'
+
+IChat.install = function(Vue) {
+  Vue.component(IChat.name, IChat)
+}
+
+export default IChat

+ 234 - 0
src/components/MyChat/IChat/src/index.vue

@@ -0,0 +1,234 @@
+<template>
+  <ChatMain
+    ref="chat"
+    :mine="mine"
+    :friends="friends"
+    :groups="groups"
+    :chats="channel"
+    :config="config"
+    @bindEvent="handleEvent"
+  />
+</template>
+
+<script>
+import ChatMain from '../../main/index'
+import { createStore, mapStates } from './store/helper'
+
+export default {
+  name: 'IChat',
+  components: {
+    ChatMain
+  },
+  props: {
+    config: {
+      type: Object,
+      default: function() {
+        return {}
+      }
+    },
+    mine: {
+      type: Object,
+      default: function() {
+        return {}
+      }
+    }
+  },
+  computed: {
+    ...mapStates({ channel: 'channel', groups: 'groups', friends: 'friends' })
+  },
+  watch: {},
+  data() {
+    // 注册 store
+    this.store = createStore()
+    return {}
+  },
+  methods: {
+    // 初始化
+    setData({ friends = [], groups = [], messages = [] }) {
+      this.setFriends(friends)
+      this.setGroups(groups)
+      this.setSystemMessage(messages)
+    },
+    setFriends(friends = []) {
+      this.store.commit('setFriends', friends)
+      for (const item of friends) {
+        if (!item.userList || item.userList.length < 0) {
+          continue
+        }
+        for (const user of item.userList) {
+          user.type = 'friend'
+          this.store.commit('replaceChannel', user)
+        }
+      }
+    },
+    setGroups(groups = []) {
+      this.store.commit('setGroups', groups)
+      for (const item of groups) {
+        item.type = 'group'
+        this.store.commit('replaceChannel', item)
+      }
+    },
+    // 设置系统消息
+    setSystemMessage(messages = []) {
+      this.store.commit('setSystemMessage', messages)
+      this.store.replaceSystemMessage()
+    },
+    bindEvent(event, data) {
+      this.$emit('bindEvent', event, data)
+    },
+    handleEvent(event, data) {
+      switch (event) {
+        case 'addGroup':
+          this.handleAddGroup(data)
+          break
+        case 'renameGroup':
+          this.handleRenameGroup(data)
+          break
+          // 创建聊天对话
+        case 'buildChat':
+          this.handleBuildChat(data)
+          break
+        case 'removeChat':
+          this.handleDelChat(data)
+          break
+        case 'acceptFriend':
+          this.handleAcceptFriend(data)
+          break
+        case 'rejectFriend': // 拒接好友申请
+          this.handleRejectFriend(data)
+          break
+        case 'clickGroupUser':
+          this.handleApplyForFriend(data)
+          break
+        default:
+          this.bindEvent(event, data)
+          break
+      }
+    },
+    // 系统消息 如果是空的则为 创建系统消息频道
+    newSystemMessage(message) {
+      if (message) {
+        this.store.newSystemMessage(message)
+        this.store.buildSysChannel()
+      }
+    },
+    getMessage(message) {
+      this.store.checkingMessage(message)
+      this.$nextTick(() => {
+        this.$refs.chat.handleMessage(message)
+      })
+    },
+
+    handleBuildChat(data) {
+      const { model, callback } = data
+      this.store.buildChannel(model)
+      // 切换
+      callback()
+    },
+    handleDelChat(data) {
+      this.store.removeChannel(data)
+    },
+    handleAcceptFriend(data) {
+      const { id, from, message } = data
+      const title = `同意${from.name}的好友申请`
+      const options = this.store.getFriendGroup()
+      this.$confirmFriend(title, {
+        info: {
+          avatar: from.avatar,
+          name: from.name,
+          options,
+          message
+        }
+      })
+        .then(({ selected }) => {
+          this.bindEvent('acceptFriend', { data, selected })
+          this.$INotify({
+            title: '提示',
+            message: `好友${from.name}添加成功!`,
+            type: 'success'
+          })
+        })
+        .catch(() => {
+          console.log('你拒接了xx的好友申请!')
+        })
+    },
+    handleRejectFriend(data) {
+      const { from } = data
+      const title = `拒接${from.name}的好友申请`
+      this.$IConfirm(title, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          this.bindEvent('rejectFriend', data)
+        })
+        .catch(() => {
+        })
+    },
+    handleAddGroup(data) {
+      this.$IPrompt('请输入分组名', '添加分组', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      })
+        .then(({ value }) => {
+          this.bindEvent('addGroup', value)
+        })
+        .catch(() => {
+        })
+    },
+    handleRenameGroup(data) {
+      this.$IPrompt('请输入分组名', '分组重命名', {
+        inputValue: data.name,
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      })
+        .then(({ value }) => {
+          data.name = value
+          this.bindEvent('renameGroup', data)
+        })
+        .catch(() => {
+        })
+    },
+    handleApplyForFriend(event) {
+      const { key, value } = event
+      const { id, name, avatar } = value
+      const options = this.store.getFriendGroup()
+      if (id === this.mine.id) {
+        this.$INotify({
+          title: '提示',
+          message: '你点了点自己',
+          type: 'success'
+        })
+        return
+      }
+      const title = `${this.mine.username}-添加好友`
+      this.$applyFriend(title, {
+        info: {
+          avatar,
+          name: name,
+          options
+        }
+      }).then((data) => {
+        const model = {
+          from: {
+            id: this.mine.id,
+            name: this.mine.username,
+            avatar: this.mine.avatar
+          },
+          to: {
+            id,
+            name,
+            avatar
+          },
+          message: data.value
+        }
+        this.bindEvent('applyFriend', model)
+      }).catch(() => {
+      })
+    }
+  }
+}
+</script>
+
+<style scoped></style>

+ 24 - 0
src/components/MyChat/IChat/src/store/cache.js

@@ -0,0 +1,24 @@
+
+import { setStore, getStore, removeStore, clearStore } from '../../../util/cache'
+
+/**
+ * 这里对cache类型进行封装
+ * 目前的需求 缓存类型并不适合使用 cookies作为存储方案
+ *
+ */
+
+export const setCache = (key, params, type = 'session') => {
+  setStore({ name: key, content: params, type })
+}
+
+export const getCache = (key) => {
+  return getStore({ name: key }) || undefined
+}
+
+export const removeCache = (key) => {
+  removeStore({ name: key })
+}
+
+export const clearCache = () => {
+  clearStore({ type: true })
+}

+ 34 - 0
src/components/MyChat/IChat/src/store/helper.js

@@ -0,0 +1,34 @@
+import Store from './index'
+
+export function createStore(initialState = {}) {
+  const store = new Store()
+  // 装填 配置
+  Object.keys(initialState).forEach(key => {
+    store.states[key] = initialState[key]
+  })
+  return store
+}
+
+export function mapStates(mapper) {
+  const res = {}
+  Object.keys(mapper).forEach(key => {
+    const value = mapper[key]
+    let fn
+    if (typeof value === 'string') {
+      fn = function() {
+        return this.store.states[value]
+      }
+    } else if (typeof value === 'function') {
+      fn = function() {
+        return value.call(this, this.store.states)
+      }
+    } else {
+      console.error('invalid value type')
+    }
+    if (fn) {
+      res[key] = fn
+    }
+  })
+  return res
+}
+

+ 148 - 0
src/components/MyChat/IChat/src/store/index.js

@@ -0,0 +1,148 @@
+import Watcher from './watcher'
+
+/**
+ *  channel 的数据过滤列表
+ */
+const channel_group_model = ['id', 'name', 'type', 'userList', 'notices', 'avatar']
+const channel_friend_model = ['id', 'name', 'type', 'online', 'avatar']
+
+import { setCache, removeCache } from './cache'
+
+/**
+ * cache key
+ */
+const key_channels = 'key_channels'
+const key_sys_msg = 'key_sys_msg'
+
+Watcher.prototype.mutations = {
+
+  setFriends(states, friends) {
+    states.friends = friends
+  },
+  // 针对单人删除
+  setFriend(states, item) {
+
+  },
+
+  setGroups(states, groups) {
+    states.groups = groups
+  },
+  // 对话排序变更
+  changeChannel() {
+
+  },
+  // 插入对话
+  insertChannel(states, channel) {
+    states.channel.unshift(filterChannelParams(channel))
+    setCache(key_channels, states.channel)
+  },
+  // 替换
+  replaceChannel(states, channel) {
+    const { id, type } = channel
+    const len = states.channel.length
+    if (len < 1) {
+      return
+    }
+    for (let i = 0; i < len; i++) {
+      const model = states.channel[i]
+      if (model.id === id && model.type === type) {
+        replaceChannelParams(channel, model)
+        break
+      }
+    }
+    setCache(key_channels, states.channel)
+  },
+  // 删除对话
+  removeChannel(states, index) {
+    // delete 会留下  空项
+    // delete states.channel[index]
+    states.channel.splice(index, 1)
+    setCache(key_channels, states.channel)
+  },
+  // 设置系统消息
+  setSystemMessage(states, data) {
+    states.systemMessage.messages = data
+  },
+  // 插入新的消息
+  updateSystemMessage(states, message) {
+    const len = states.systemMessage.messages.length
+    let mark = -1
+    for (let i = 0; i < len; i++) {
+      const item = states.systemMessage.messages[i]
+      if (item.id === message.id) {
+        mark = i
+        break
+      }
+    }
+    if (mark < 0) {
+      states.systemMessage.messages.push(message)
+    } else {
+      states.systemMessage.messages[mark] = message
+    }
+  }
+}
+
+function indexSystemMessage(list = []) {
+  const channels = list
+  const len = channels.length
+  for (let i = 0; i < len; i++) {
+    const model = channels[i]
+    if (model.id === -1 && model.type === 'sys_msg') {
+      return i
+    }
+  }
+}
+/**
+ *
+ *  过滤一些非必要属性
+ * @param {*} channel
+ */
+function filterChannelParams(channel) {
+  const data = {}; let filters = []
+  if (channel.type === 'group') {
+    filters = channel_group_model
+  } else if (channel.type === 'friend') {
+    filters = channel_friend_model
+  } else if (channel.type === 'sys_msg') {
+    return channel
+  }
+
+  for (const key of filters) {
+    data[key] = channel[key]
+  }
+  return data
+}
+
+/**
+ *
+ * 上面注释掉的方法是为了警示自己。
+ * 上方 的方法和这个方法的本质差别在于一个是过滤出新的对象,一个是改变对象的引用
+ * 对应组件中改变的是chat这个属性,里面内容改变vue是无法监听到的。
+ *
+ * @param {*} data
+ * @param {*} channel
+ */
+function replaceChannelParams(data, channel) {
+  let filters = []
+  if (data.type === 'group') {
+    filters = channel_group_model
+  } else if (data.type === 'friend') {
+    filters = channel_friend_model
+  } else if (data.type === 'sys_msg') {
+    channel = data
+  }
+  for (const key of filters) {
+    channel[key] = data[key]
+  }
+}
+
+// 提交属性
+Watcher.prototype.commit = function(name, ...args) {
+  const mutations = this.mutations
+  if (mutations[name]) {
+    mutations[name].apply(this, [this.states].concat(args))
+  } else {
+    throw new Error(`Action not found: ${name}`)
+  }
+}
+export default Watcher

+ 157 - 0
src/components/MyChat/IChat/src/store/watcher.js

@@ -0,0 +1,157 @@
+import Vue from 'vue'
+import { setCache, getCache, removeCache } from './cache'
+
+/**
+ * cache key
+ */
+const key_channels = 'key_channels'
+const key_sys_msg = 'key_sys_msg'
+
+/**
+ *  默认带有 频道
+ *  1. 系统消息
+ * @type {*[]}
+ */
+const default_system_message =
+    {
+      // 频道id
+      id: -1,
+      name: '系统消息',
+      type: 'sys_msg',
+      messages: []
+    }
+
+/**
+ *  一个store 模式来为ichat 做全局状态管理
+ */
+export default Vue.extend({
+  data() {
+    return {
+      states: {
+        // 全部数据
+        data: {},
+        mine: {},
+        // 好友列表
+        friends: [],
+        // 自己
+        groups: [],
+        // 对话列表
+        channel: getCache(key_channels) || [],
+        // systemMessage: getCache(key_sys_msg) || default_system_message,
+        // channel: [],
+        systemMessage: default_system_message
+
+        // 系统消息
+
+      }
+    }
+  },
+  // 方法
+  methods: {
+
+    // 创建频道
+    buildChannel(channel) {
+      const channels = this.states.channel
+      const len = channels.length
+      if (len < 1) {
+        // 直接插入
+        this.commit('insertChannel', channel)
+        return
+      }
+      let flag = true
+      const { id, type } = channel
+      for (let i = 0; i < len; i++) {
+        const model = channels[i]
+        if (model.id === id && model.type === type) {
+          flag = false
+        }
+      }
+      if (flag) {
+        this.commit('insertChannel', channel)
+      }
+    },
+    buildSysChannel() {
+      this.buildChannel(this.states.systemMessage)
+      this.replaceSystemMessage()
+    },
+    replaceSystemMessage() {
+      this.commit('replaceChannel', this.states.systemMessage)
+    },
+    // 检查消息 如果
+    checkingMessage(message) {
+      const channels = this.states.channel
+      const len = channels.length
+      let flag = true
+      const { id, type } = message
+      if (len < 1) {
+        flag = true
+      } else {
+        for (let i = 0; i < len; i++) {
+          const model = channels[i]
+          if (model.id === id && model.type === type) {
+            flag = false
+          }
+        }
+      }
+
+      if (flag) {
+        let channel
+        if (message.type === 'group') {
+          const { groups } = this.states
+          for (const group of groups) {
+            if (group.id === id) {
+              channel = group
+              channel.type = 'group'
+              channel.groupName = '群组'
+              break
+            }
+          }
+        } else if (message.type === 'friend') {
+          const { friends } = this.states
+          for (const friend of friends) {
+            const { userList } = friend
+            for (const user of userList) {
+              if (user.id === id) {
+                channel = user
+                channel.type = 'friend'
+                channel.groupName = '我的好友'
+                break
+              }
+            }
+          }
+        }
+        this.commit('insertChannel', channel)
+      }
+    },
+    // 新的系统消息
+    newSystemMessage(message) {
+      this.commit('updateSystemMessage', message)
+    },
+    removeChannel({ id, type }) {
+      const channels = this.states.channel
+      const len = channels.length
+      if (len < 1) {
+        // 空数组
+        return
+      }
+      let flag = 0
+      for (let i = 0; i < len; i++) {
+        const model = channels[i]
+        if (model.id === id && model.type === type) {
+          flag = i
+        }
+      }
+      this.commit('removeChannel', flag)
+    },
+    // 获取好友分组
+    getFriendGroup() {
+      const options = []
+      this.states.friends.forEach(item => {
+        options.push({ label: item.name, key: item.index, value: item.index })
+      })
+      return options
+    }
+  }
+
+})
+

+ 230 - 0
src/components/MyChat/MChat/chatTabs.vue

@@ -0,0 +1,230 @@
+<script>
+import { default_avatar } from '../util/constant'
+
+function noop() {
+
+}
+
+export default {
+  name: 'MChatTabs',
+  // 注入父级属性
+  inject: ['rootChat'],
+  props: {
+    panes: {
+      type: Array,
+      default: () => []
+    },
+    callTabRemove: {
+      type: Function,
+      default: noop
+    },
+    callTabClick: {
+      type: Function,
+      default: noop
+    },
+    callRightBoxShow: {
+      type: Function,
+      default: noop
+    }
+  },
+  data() {
+    return {
+      stickyTop: 0,
+      zIndex: 1,
+      // 缩小模式
+      miniMode: false,
+      // 用于记录窗口是否被打开,写得有点烂
+      tempRecordDisplay: undefined
+    }
+  },
+  computed: {
+    stickyActive() {
+      return this.stickyTop > 0
+    }
+  },
+  methods: {
+    handleScroll(event) {
+      this.stickyTop = event.target.scrollTop
+    },
+    // 处理缩小模式
+    handleMiniMode() {
+      this.miniMode = !this.miniMode
+      if (this.miniMode && this.rootChat.chatDisplay) {
+        this.tempRecordDisplay = this.rootChat.chatDisplay
+        this.callRightBoxShow()
+      } else {
+        if (!this.miniMode && this.tempRecordDisplay !== this.rootChat.chatDisplay) {
+          this.callRightBoxShow()
+        }
+      }
+    },
+    // 处理 chat box是否要打开
+    handleBoxSwitch() {
+      this.callRightBoxShow()
+      this.tempRecordDisplay = this.rootChat.chatDisplay
+      this.miniMode = false
+    },
+    // 处理未读信息
+    handleUnread() {}
+
+  },
+  render() {
+    const {
+      rootChat,
+      panes,
+      stickyTop,
+      miniMode,
+      handleScroll,
+      handleMiniMode,
+      handleBoxSwitch,
+      callTabRemove,
+      callTabClick
+
+    } = this
+    // 如果只有一个chat的情况
+    if (rootChat.alone) return
+    const { config, chatDisplay } = rootChat
+    const el_chat_tabs = this._l(panes, (pane, index) => {
+      const { active, chat, unread } = pane
+      // 判断下是否是缩小模式如果是只放过激活的
+      if (miniMode && !active) {
+        return
+      }
+      let { name, id, avatar, online } = chat
+      const tabName = name + id + index
+      pane.index = `${index}`
+      // 是否有头像
+      avatar = avatar || default_avatar
+
+      // 对话是否激活
+      const label = name
+      const el_tab_lable = <span class='im-label'> {label}</span>
+      const el_unread_badge =
+                    unread > 0 ? <span class='badge'>{unread}</span> : ''
+      let offline = false
+      if (chat.type === 'friend') {
+        offline = !online
+      }
+
+      let el_chat_tab_remove = ''
+      if (config.tabRemove) {
+        el_chat_tab_remove = (
+          <i
+            class='im-icon m-icon-error'
+            on-click={(ev) => {
+              ev.stopPropagation()
+              callTabRemove({ pane, ev })
+            }}
+          ></i>
+        )
+      }
+
+      const el_chat_tab = (
+        <li
+          class={{
+            'im-chat-tab': true,
+            'offline': offline,
+            'im-this': active }}
+          id={`tab-${tabName}`}
+          key={`tab-${tabName}`}
+          on-click={(ev) => {
+            ev.stopPropagation()
+            callTabClick({ pane, ev })
+          }}
+        >
+          {el_unread_badge}
+          <img
+            src={avatar}
+            on-click={(ev) => {
+              ev.stopPropagation()
+              callTabClick({ pane, ev })
+            }}
+          />
+          {el_tab_lable}
+          {el_chat_tab_remove}
+        </li>
+      )
+
+      return el_chat_tab
+    })
+    // render对icon css的方式不支持只能
+
+    let el_tabs_bar = ''
+
+    if (config.brief) {
+      const el_icon = (
+        <i
+          class={{
+            'im-icon': true,
+            'btn-pane-show': true,
+            'm-icon-arrow-right': !chatDisplay,
+            'm-icon-arrow-left': chatDisplay
+          }}
+          on-click={() => {
+            handleBoxSwitch()
+          }}
+        ></i>
+      )
+
+      el_tabs_bar = (
+        <li
+          class={{
+            'im-chat-tab': true,
+            'im-tabs-title': true,
+            active: true
+          }}
+          style={{
+            top: stickyTop + 'px'
+          }}
+        >
+          <span class='im-label im-box-setwin ' on-click={() => { handleMiniMode() }}>
+            <a class='im-btn-min' href='javascript:;'>
+              <cite></cite>
+            </a>
+          </span>
+          {el_icon}
+        </li>
+      )
+    }
+
+    return (
+      <ul
+        class={{
+          'im-chat-tabs': true,
+          'normal': !miniMode,
+          'tabs-shadow': !chatDisplay
+        }}
+        on-mousedown={(ev) => {
+          rootChat.handPanesDrag(ev)
+        }}
+        on-scroll={(ev) => {
+          handleScroll(ev)
+        }}
+      >
+        {el_tabs_bar}
+        {el_chat_tabs}
+      </ul>
+    )
+  }
+}
+</script>
+
+<style scoped>
+    .badge {
+        position: absolute;
+        top: 12px;
+        left: 10px;
+        transform: translateY(-50%) translateX(100%);
+        background-color: #f56c6c;
+        border-radius: 10px;
+        color: #fff;
+        display: inline-block;
+        font-size: 12px;
+        height: 18px;
+        line-height: 18px;
+        padding: 0 6px;
+        text-align: center;
+        white-space: nowrap;
+        border: 1px solid #fff;
+    }
+</style>

+ 14 - 0
src/components/MyChat/MChat/index.js

@@ -0,0 +1,14 @@
+
+/**
+ *  chat interface 插槽
+ */
+
+// 导入组件,组件必须声明 name
+import MChat from './index.vue'
+// import '../styles/im.scss'
+
+MChat.install = function(Vue) {
+  Vue.component(MChat.name, MChat)
+}
+
+export default MChat

+ 387 - 0
src/components/MyChat/MChat/index.vue

@@ -0,0 +1,387 @@
+<script>
+import MChatTabs from './chatTabs'
+import MChatIndex from '../chat'
+import { playTipSound } from '../util/play'
+import { layerPosition, layerDrag } from '../util/layer'
+
+export default {
+  name: 'Mchat',
+  components: {
+    MChatTabs,
+    MChatIndex
+  },
+  provide() {
+    return {
+      rootChat: this
+    }
+  },
+  props: {
+    config: {
+      type: Object,
+      default: () => ({
+        rightBox: false,
+        brief: false,
+        voice: false,
+        notice: false,
+        fixed: true
+      })
+    },
+    mine: {
+      type: Object,
+      default: () => ({
+        id: '10001',
+        username: 'jule-meteor',
+        status: 'online',
+        sign: '与其感慨路难行,不如马上出发!',
+        avatar: '/avatar/avatar_meteor.png'
+      })
+    },
+    chats: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      panes: [],
+      selected: '0',
+      // 主体是否隐藏
+      display: true,
+      // chats是否隐藏
+      chatDisplay: true,
+      width: '800'
+    }
+  },
+  computed: {
+    alone() {
+      let flag = true
+      if (this.chats.length > 1) {
+        flag = false
+      }
+      return flag
+    }
+  },
+  watch: {
+    alone(nv, ov) {
+      if (nv) {
+        this.chatDisplay = true
+      }
+    },
+    'config.brief'(nv, ov) {
+      if (!nv) {
+        this.chatDisplay = true
+      }
+    },
+    // 观察窗口变化
+    panes(nv, ov) {
+      // 如果 窗口是从0变多
+      const o_len = ov.length
+      if (o_len === 0) {
+        this.$nextTick(() => {
+          this.initChatPosition()
+        })
+      }
+    }
+  },
+  created() {
+    // 设定一些监听的事件
+    this.$im.on('getMessage', (msg) => {
+      this.getMessage(msg)
+    })
+  },
+  mounted() {
+    this.calcPaneInstances()
+  },
+  updated() {
+    this.calcPaneInstances()
+  },
+  methods: {
+    // 初始化窗口的位置
+    initChatPosition() {
+      if (this.config.fixed) return
+      const el = this.$refs.chat
+      if (el) {
+        this.$nextTick(() => {
+          layerPosition(el, 'rt')
+        })
+      }
+    },
+    handleTabClick({ pane }) {
+      this.selected = pane.index
+      this.handleEnterFocus(pane)
+    },
+    handleTabRemove({ pane, ev }) {
+      const { name, type, id } = pane.chat
+      this.$emit('removeChat', { id, name, type })
+    },
+    // enterBox 关闭事件
+    handleChatRemove() {
+      const { name, type, id } = this.getCurrent()
+      this.$emit('removeChat', { id, name, type })
+    },
+    handleRightBoxShow() {
+      this.chatDisplay = !this.chatDisplay
+    },
+    // 对话内容点击
+    handleTalkClick(item) {
+      this.$emit('talkClick', item)
+    },
+    // 讲话用户头像点击
+    handleTalkUserClick(item) {
+      this.$emit('talkUserClick', item)
+    },
+    handleChatHeaderClick(pane) {
+      this.$emit('chatInfo', pane)
+    },
+    handleLoadHistory(data) {
+      this.$emit('loadHistory', data)
+    },
+    //  获得当前对话框
+    getCurrent() {
+      const { chat, taleList } = this.getCurrentPane()
+      return { chat, taleList }
+    },
+    //  获得当前对话框内部使用
+    getCurrentPane() {
+      for (const pane of this.panes) {
+        // 激活的就是当前的
+        if (pane.active) {
+          return pane
+        }
+      }
+    },
+    // 收到消息
+    getMessage(message) {
+      this.$nextTick(() => {
+        const voice = this.config.voice
+        // 提示音
+        if (voice) {
+          if (!message.mine) {
+            playTipSound()
+          }
+        }
+        this.panes.forEach((item) => {
+          const { chat } = item
+          if (chat.id !== message.id || chat.type !== message.type) return
+
+          item.getMessage(message)
+        })
+      })
+    },
+    handleEvent(event, data) {
+      switch (event) {
+        default:
+          this.bindChatEvent(event, data)
+          break
+      }
+    },
+    handleUploadEvent(data, fn) {
+      this.$emit('uploadEvent', data, fn)
+    },
+    // 处理对话框Setting
+    handleChatSet(event) {
+      console.log('最小化函数')
+      const { name, type, id } = this.getCurrent().chat
+      this.$emit(event, { id, name, type })
+    },
+    handleEnter(content) {
+      const chat = this.getCurrent().chat
+      const mine = this.mine
+      const message = {
+        // 自己的信息
+        mine: {
+          id: mine.id,
+          username: mine.username,
+          avatar: mine.avatar,
+          mine: true
+        },
+        // 目标
+        to: {
+          id: chat.id,
+          name: chat.name,
+          type: chat.type,
+          avatar: chat.avatar
+        },
+        // 内容
+        content,
+        // 数据类型
+        type: 'text',
+        // 发起的时间戳
+        timestamp: new Date()
+      }
+      // 是否写回去
+      this.$emit('sendMessage', message)
+    },
+    handPanesDrag(e) {
+      if (this.config.fixed) return
+      const el = this.$refs.chat
+      layerDrag(e, el)
+    },
+    // 输入框对焦
+    handleEnterFocus(pane) {
+      if (!pane) {
+        pane = this.getCurrentPane()
+      }
+      setTimeout(() => {
+        if (!pane || pane.$children.length < 1) {
+          return
+        }
+        const childrenEl = pane.$children.filter(
+          (item) =>
+            item.$vnode.tag &&
+                            item.$vnode.componentOptions &&
+                            item.$vnode.componentOptions.Ctor.options.name === 'enter-box'
+        )
+        if (childrenEl.length > 0) {
+          childrenEl[0].handleInputFocus()
+        }
+      }, 200)
+    },
+    // 生成panes的数据
+    calcPaneInstances(isForceUpdate = false) {
+      // 绕了一圈最终决定由chat-box 来决定 chat-tabs的属性
+      if (this.$children) {
+        const childPanes = this.$children.filter(
+          (item) =>
+            item.$vnode.tag &&
+                            item.$vnode.componentOptions &&
+                            item.$vnode.componentOptions.Ctor.options.name === 'mchat-index'
+        )
+        // update indeed
+        const panes = childPanes.map((item) => item.$vnode.componentInstance)
+        const panesChanged = !(
+          panes.length === this.panes.length &&
+                        panes.every((pane, index) => pane === this.panes[index])
+        )
+        if (isForceUpdate || panesChanged) {
+          this.selected = '0'
+          this.panes = panes
+          this.handleEnterFocus(panes[0])
+        }
+      } else if (this.panes.length !== 0) {
+        this.panes = []
+      }
+    }
+  },
+  render() {
+    const {
+      handPanesDrag,
+      config,
+      chats,
+      panes,
+      handleTabClick,
+      handleTabRemove,
+      handleRightBoxShow,
+      handleChatRemove,
+      handleTalkClick,
+      handleTalkUserClick,
+      handleChatHeaderClick,
+      handleLoadHistory,
+      handleUploadEvent,
+      handleEnter,
+      handleChatSet,
+      chatDisplay,
+      alone
+    } = this
+    if (chats.length < 1) return
+    // 窗口页面
+    const el_chat_panes = this._l(chats, (chat) => {
+      const data_chat = {
+        props: {
+          chat,
+          config,
+          callLoadHistory: handleLoadHistory,
+          emitMessage: handleEnter,
+          callChatClose: handleChatRemove,
+          callTalkClick: handleTalkClick,
+          callTalkUserClick: handleTalkUserClick,
+          callHeaderClick: handleChatHeaderClick
+        },
+        ref: 'MChatIndex',
+        on: {
+          uploadEvent: function(data, fn) {
+            handleUploadEvent(data, fn)
+          }
+        }
+      }
+      return <m-chat-index {...data_chat}></m-chat-index>
+    })
+
+    // 标签页面
+    const el_chat_tabs = {
+      props: {
+        panes,
+        callTabRemove: handleTabRemove,
+        callTabClick: handleTabClick,
+        callRightBoxShow: handleRightBoxShow
+      },
+      ref: 'MChatTabs'
+    }
+    return (
+      <div>
+        <div
+          class={{
+            'fixed': config.fixed,
+            'im-layer layer-anim im-box im-chat': true,
+            'chat-show': chatDisplay,
+            alone: alone
+          }}
+          ref='chat'
+          style={{
+            'z-index': 1002,
+            display: 'inline'
+          }}
+        >
+          <div
+            class='im-layer-title'
+            style='cursor: move;'
+            on-mousedown={(ev) => {
+              handPanesDrag(ev)
+            }}
+          ></div>
+          <div class='im-layer-tabs im-layer-content'>
+            <m-chat-tabs {...el_chat_tabs}></m-chat-tabs>
+            {el_chat_panes}
+          </div>
+          <span class='im-box-setwin'>
+            <i
+              class='m-icon-top'
+              title='置顶'
+              on-click={(ev) => handleChatSet('chatTop')}
+            ></i>
+            <i
+              class='m-icon-minus'
+              title='最小化'
+              on-click={(ev) => handleChatSet('chatMin')}
+            ></i>
+            <i
+              class='m-icon-maxus'
+              title='最大化'
+              on-click={(ev) => handleChatSet('chatMax')}
+            ></i>
+            <i
+              class='m-icon-close'
+              title='关闭'
+              on-click={(ev) => handleChatSet('chatClose')}
+            ></i>
+          </span>
+          <span class='im-icon-resize'></span>
+        </div>
+      </div>
+    )
+  }
+}
+</script>
+
+<style scoped>
+    .chat-show {
+        -webkit-box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
+        box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
+        min-width: 500px;
+        width: 800px;
+    }
+
+    .chat-show.alone {
+        width: 620px;
+    }
+</style>

+ 374 - 0
src/components/MyChat/chat/chatList.vue

@@ -0,0 +1,374 @@
+<script>
+import {
+  ConvertContext,
+  dateFormat
+} from './convertContext'
+import Scroll from '../util/scroll'
+
+function noop() {}
+
+export default {
+  name: 'ChatList',
+  componentName: 'ChatList',
+  props: {
+    // 是否是当前对话框
+    current: {
+      type: Boolean,
+      default: false
+    },
+    // 聊天记录
+    list: {
+      type: Array,
+      default: () => []
+    },
+    config: {
+      type: Object,
+      default: () => ({
+        notice: false,
+        downBtn: false
+      })
+    },
+    // 聊天记录点击事件
+    callTalkClick: {
+      type: Function,
+      default: noop
+    },
+    // 聊天用户点击事件
+    callTalkUserClick: {
+      type: Function,
+      default: noop
+    }
+  },
+  data() {
+    return {
+      // 锁
+      lock: false,
+      scroll: null,
+      scrollTimer: null,
+      // 标题前消息
+      beforeTitle: '',
+      // 标题时间
+      titleTimer: '',
+      // 加载与否。
+      loaded: false,
+      // 下载历史
+      loadHistory: false,
+      // 历史是否下载了
+      historyBtnShow: false
+    }
+  },
+  computed: {
+    // 是否在最下面
+    isBottom() {
+      return this.scroll && this.scroll.isBottom
+    },
+    // 未读
+    unread() {
+      const { unread = 0 } = this.scroll || {}
+      this.$emit('messageUnread', unread)
+      return unread
+    }
+
+  },
+  watch: {
+    // 当前窗口切换
+    current(newVal, oldVal) {
+      if (newVal) {
+        const reset = this.isBottom
+        this.scrollRefresh()
+        if (reset) {
+          this.scrollBottom()
+        }
+      }
+      if (!newVal && oldVal) {
+        this.scroll.read()
+      }
+    },
+    // 锁
+    lock(newVal) {
+      if (newVal) {
+        setTimeout(() => {
+          if (this.lock) {
+            this.lock = false
+          }
+        }, 1000)
+      }
+    },
+    list(newVal) {
+      if (newVal) {
+        this.$nextTick(() => {
+          // 更新节点
+          this.updateNode()
+          // 下载记录历史
+          /**
+             *    这里的延时的原因
+             *    是因为发送图片,render的时间太长
+             */
+          setTimeout(() => {
+            if (this.loadHistory) {
+              this.closeTopTip()
+              this.scrollRefresh()
+              this.scroll.toBeforePosition()
+            }
+            if (this.current && this.isBottom) {
+              this.scrollBottom()
+            }
+            // 还有一种情况是自己发送的。。
+          }
+          , 100)
+        })
+      }
+    },
+    //
+    'config.scrollToButton'(newVal) {
+      if (newVal) {
+        this.scrollBottom()
+      }
+    },
+    // 未读
+    unread(newVal) {
+      // if (newVal) {
+      //   this.beforeTitle && this.resetTitle(this.beforeTitle);
+      //   this.saveTitle();
+      //   this.changeTitle();
+      //   if (this.config.notice) {
+      //     this.showBrowser();
+      //   }
+      // } else {
+      //   this.resetTitle(this.beforeTitle);
+      // }
+    }
+  },
+  updated() {
+    // 解决聊天窗口发生变化,滚动条不会自动计算问题。
+    this.calcLoaded()
+  },
+  mounted() {
+    this.createScroll()
+  },
+  methods: {
+    // 计算是否已经加载
+    calcLoaded() {
+      const el = this.$el
+      if (!el) return
+      const width = el.clientWidth
+      // 有宽度表示页面被显示了,并且只会触发一次。
+      if (width > 0) {
+        if (!this.loaded) {
+          this.loaded = true
+          this.scrollRefresh()
+        }
+      } else {
+        this.loaded = false
+      }
+    },
+    bindClickUser(data) {
+      const userInfo = {
+        id: data.id,
+        username: data.username,
+        mine: data.mine
+      }
+      this.callTalkUserClick(userInfo)
+    },
+    // 拉取历史记录
+    handleHistory() {
+      // 锁住拉取
+      this.loadHistory = true
+      this.$emit('loadHistory')
+    },
+    /** ****  滚动条设置 ******/
+    createScroll() {
+      const that = this
+      const dom = this.$refs.scroller
+      this.scroll = new Scroll(dom, {
+        click: true,
+        scrollbars: true,
+        mouseWheel: true,
+        preventDefault: false,
+        interactiveScrollbars: true,
+        hijackInternalLinks: true
+        // useTransform: false,
+      })
+      // copy code
+      dom.addEventListener(
+        'ontouchstart' in window ? 'touchstart' : 'mousedown',
+        function(e) {
+        // 阻止冒泡事件
+          e.stopPropagation()
+        }
+      )
+
+      // 停止滚动时触发。
+      this.scroll.on('scrollEnd', function() {
+        that.scrollTop()
+        that.scroll.savePosition()
+      })
+    },
+
+    // 读取 历史记录强行拉动滚动条
+    scrollTop() {
+      // 是否触顶
+      const { isTop } = this.scroll
+      if (isTop) {
+        // 后期自动拉取历史再改进
+        this.historyBtnShow = true
+        return
+      }
+      this.closeTopTip()
+    },
+    scrollUp() {
+      if (this.scroll) {
+        this.scrollRefresh()
+        this.scroll.scrollTo(0, 0, 200)
+      }
+    },
+    scrollBottom() {
+      if (this.scroll) {
+        this.scrollRefresh()
+        this.scroll.scrollTo(0, this.scroll.maxScrollY, 200)
+      }
+    },
+    closeTopTip() {
+      this.loadHistory = false
+      this.historyBtnShow = false
+    },
+    // 节点加载
+    updateNode() {
+      const parent = this.$refs.main
+      if (!parent) return
+      const childs = parent.children
+      for (const el of childs) {
+        const top = el.offsetTop
+        this.scroll.addNode(top, el)
+      }
+    },
+    // 刷新滚动条长度
+    scrollRefresh() {
+      this.scroll.refresh()
+    },
+    /** ** 滚动条结束 ********/
+    /** * 标签标题  开始***/
+    saveTitle() {
+      const { title } = document
+      this.beforeTitle = title
+    },
+    resetTitle(title) {
+      document.title = title
+      clearTimeout(this.titleTimer)
+    },
+    changeTitle() {
+      const that = this
+      let flage = 0
+      change()
+      function change() {
+        let title = '【未读】'
+        if (flage) {
+          title = '【' + that.unread + '条】'
+        }
+        flage = !flage
+        that.titleTimer = setTimeout(() => {
+          that.resetTitle(title + that.beforeTitle)
+          change()
+        }, 1000)
+      }
+    },
+    showBrowser() {
+      if (window.Notification && Notification.permission !== 'denied') {
+        const { unread } = this
+        Notification.requestPermission(function(status) {
+          if (status === 'granted') {
+            new Notification('新消息', {
+              body: `您总共有${unread}条消息未读。`
+            })
+          }
+        })
+      }
+    }
+    /** **标签标题 结束 ***/
+  },
+  render(h) {
+    const {
+      list,
+      historyBtnShow,
+      scrollUp,
+      scrollBottom,
+      callTalkClick,
+      bindClickUser,
+      handleHistory
+    } = this
+
+    /**
+       *  渲染 如果要提供更好的 如 图片显示效果,需要更改 vnode的构建
+       *  图片方面可以参考 element-ui image的处理
+        */
+    const el_record_list = this._l(list, (item) => {
+      const contentHtml = h('div', {
+        domProps: {
+          innerHTML: ConvertContext(item.content)
+        }
+      })
+      const leftName = item.mine ? '' : item.username
+      const rightName = item.mine ? item.username : ''
+      const time = dateFormat(item.timestamp)
+      return (
+        <li class={{ 'content-mine': item.mine }}>
+          <div class='content-user' >
+            <img src={item.avatar} on-click={() => bindClickUser(item)}/>
+            <cite>
+              {leftName}
+              <i>{time}</i> {rightName}
+            </cite>
+          </div>
+          <div
+            class='content-text'
+            on-click={() => callTalkClick(item)}
+          >
+            {' '}
+            {contentHtml}
+          </div>
+        </li>
+      )
+    })
+
+    let el_history_log
+    if (historyBtnShow) {
+      el_history_log = (
+        <div class='history_label' on-click={() => handleHistory()}>
+          查看更多消息
+        </div>
+      )
+    }
+
+    const el_chat_list = (
+      <div
+        class={{
+          'im-chat-content': true,
+          listActive: false
+        }}
+        ref='scroller'
+      >
+        {el_history_log}
+        <ul ref='main'class='talk-list' >{el_record_list}</ul>
+
+        <div class='scrollButton' on-click={() => scrollUp()}>
+          <i class='up  m-icon-arrow-up'></i>
+        </div>
+        <div class='scrollButton' on-click={() => scrollBottom()}>
+          <i class='down m-icon-arrow-down'></i>
+        </div>
+      </div>
+    )
+
+    return el_chat_list
+  }
+}
+</script>
+
+<style >
+.iScrollVerticalScrollbar.iScrollLoneScrollbar {
+  z-index: 1 !important;
+  right: 13px !important;
+  margin-top: 11px;
+  margin-bottom: 11px;
+}
+</style>

+ 69 - 0
src/components/MyChat/chat/convertContext.js

@@ -0,0 +1,69 @@
+/**
+ * 用得到的工具类
+ */
+import emojis from './emoji'
+
+// 转换 聊天内容
+export function ConvertContext(content) {
+  // 支持的html标签
+  var html = function(end) {
+    return new RegExp('\\n*\\[' + (end || '') + '(pre|div|p|table|thead|th|tbody|tr|td|ul|li|ol|li|dl|dt|dd|h2|h3|h4|h5)([\\s\\S]*?)\\]\\n*', 'g')
+  }
+  content = (content || '').replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&amp;')
+    .replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;').replace(/"/g, '&quot;') // XSS
+    .replace(/@(\S+)(\s+?|$)/g, '@<a href="javascript:;">$1</a>$2') // 转义@
+  // .replace(/\s{2}/g, '&nbsp') //转义空格
+    .replace(/img\[([^\s]+?)\]/g, function(img) { // 转义图片
+      return '<img class="im-content-img" src="' + img.replace(/(^img\[)|(\]$)/g, '') + '">'
+    })
+    .replace(/file\([\s\S]+?\)\[[\s\S]*?\]/g, function(str) { // 转义文件
+      var href = (str.match(/file\(([\s\S]+?)\)\[/) || [])[1]
+      var text = (str.match(/\)\[([\s\S]*?)\]/) || [])[1]
+      if (!href) return str
+      return '<a class="layui-layim-file" href="' + href + '" download target="_blank"><i class="layui-icon">&#xe61e;</i><cite>' + (text || href) + '</cite></a>'
+    })
+    .replace(/emoji\[([^\s[\]]+?)\]/g, function(emoji) { // 转义表情
+      var alt = emoji.replace(/^emoji/g, '')
+      return '<img alt="' + alt + '" title="' + alt + '" src="' + emojis[alt] + '">'
+    }).replace(/audio\[([^\s]+?)\]/g, function(i) {
+      return '<div data-src="' + i.replace(/(^audio\[)|(\]$)/g, '') + '"> <audio  controls  src="' + i.replace(/(^audio\[)|(\]$)/g, '') + '" ></audio></div>'
+    }).replace(/video\[([^\s]+?)\]/g, function(i) {
+      return '<video controls="controls" src="' + i.replace(/(^video\[)|(\]$)/g, '') + '"></video>'
+    }).replace(/a\([\s\S]+?\)\[[\s\S]*?\]/g, function(str) { // 转义链接
+      var href = (str.match(/a\(([\s\S]+?)\)\[/) || [])[1]
+      var text = (str.match(/\)\[([\s\S]*?)\]/) || [])[1]
+      if (!href) return str
+      return '<a href="' + href + '" target="_blank">' + (text || href) + '</a>'
+    }).replace(html(), '<$1 $2>').replace(html('/'), '</$1>') // 转移HTML代码
+    .replace(/\n/g, '<br>')
+    // 转义换行
+
+  return (content)
+}
+
+export function ConvertRecord(data) {
+  const contenxt = ConvertContext(data.content)
+  const tiem = dateFormat(data.timestamp)
+  const leftName = data.mine ? '' : data.username
+  const rightName = data.mine ? data.username : ''
+
+  const mineHtml = data.mine ? "class='layim-chat-mine'" : ''
+  let htmlContext = '<li ' + mineHtml + '>'
+  htmlContext += "<div class='layim-chat-user'> <img src='http://www.qsfj.com/data/upload/201805/f_b68ccc238a400550805c6b7ed1df9d6c.jpg' />"
+  htmlContext += '<cite>' + leftName + '<i>' + tiem + '</i>' + rightName + '</cite> </div>'
+  htmlContext += "<div class='layim-chat-text'>" + contenxt + '</div></li>'
+
+  return htmlContext
+}
+
+// 补齐数位
+var digit = function(num) {
+  return num < 10 ? '0' + (num | 0) : num
+}
+
+export function dateFormat(timestamp) {
+  var d = new Date(timestamp || new Date())
+  return d.getFullYear() + '-' + digit(d.getMonth() + 1) + '-' + digit(d.getDate()) +
+        ' ' + digit(d.getHours()) + ':' + digit(d.getMinutes()) + ':' + digit(d.getSeconds())
+}
+

+ 18 - 0
src/components/MyChat/chat/emoji.js

@@ -0,0 +1,18 @@
+/**
+ *  表情库
+ *
+ *
+ */
+
+const emoji_dir = './emoji/'
+
+// 表情库
+var emojis = (function() {
+  var alt = ['[微笑]', '[嘻嘻]', '[哈哈]', '[可爱]', '[可怜]', '[挖鼻]', '[吃惊]', '[害羞]', '[挤眼]', '[闭嘴]', '[鄙视]', '[爱你]', '[泪]', '[偷笑]', '[亲亲]', '[生病]', '[太开心]', '[白眼]', '[右哼哼]', '[左哼哼]', '[嘘]', '[衰]', '[委屈]', '[吐]', '[哈欠]', '[抱抱]', '[怒]', '[疑问]', '[馋嘴]', '[拜拜]', '[思考]', '[汗]', '[困]', '[睡]', '[钱]', '[失望]', '[酷]', '[色]', '[哼]', '[鼓掌]', '[晕]', '[悲伤]', '[抓狂]', '[黑线]', '[阴险]', '[怒骂]', '[互粉]', '[心]', '[伤心]', '[猪头]', '[熊猫]', '[兔子]', '[ok]', '[耶]', '[good]', '[NO]', '[赞]', '[来]', '[弱]', '[草泥马]', '[神马]', '[囧]', '[浮云]', '[给力]', '[围观]', '[威武]', '[奥特曼]', '[礼物]', '[钟]', '[话筒]', '[蜡烛]', '[蛋糕]']; var arr = {}
+  alt.forEach((item, index) => {
+    arr[item] = require(emoji_dir + index + '.gif')
+  })
+  return arr
+}())
+
+export default emojis

BIN
src/components/MyChat/chat/emoji/0.gif


BIN
src/components/MyChat/chat/emoji/1.gif


BIN
src/components/MyChat/chat/emoji/10.gif


BIN
src/components/MyChat/chat/emoji/11.gif


BIN
src/components/MyChat/chat/emoji/12.gif


BIN
src/components/MyChat/chat/emoji/13.gif


BIN
src/components/MyChat/chat/emoji/14.gif


BIN
src/components/MyChat/chat/emoji/15.gif


BIN
src/components/MyChat/chat/emoji/16.gif


BIN
src/components/MyChat/chat/emoji/17.gif


BIN
src/components/MyChat/chat/emoji/18.gif


BIN
src/components/MyChat/chat/emoji/19.gif


BIN
src/components/MyChat/chat/emoji/2.gif


BIN
src/components/MyChat/chat/emoji/20.gif


BIN
src/components/MyChat/chat/emoji/21.gif


BIN
src/components/MyChat/chat/emoji/22.gif


BIN
src/components/MyChat/chat/emoji/23.gif


BIN
src/components/MyChat/chat/emoji/24.gif


BIN
src/components/MyChat/chat/emoji/25.gif


BIN
src/components/MyChat/chat/emoji/26.gif


BIN
src/components/MyChat/chat/emoji/27.gif


BIN
src/components/MyChat/chat/emoji/28.gif


BIN
src/components/MyChat/chat/emoji/29.gif


BIN
src/components/MyChat/chat/emoji/3.gif


BIN
src/components/MyChat/chat/emoji/30.gif


BIN
src/components/MyChat/chat/emoji/31.gif


BIN
src/components/MyChat/chat/emoji/32.gif


BIN
src/components/MyChat/chat/emoji/33.gif


BIN
src/components/MyChat/chat/emoji/34.gif


BIN
src/components/MyChat/chat/emoji/35.gif


BIN
src/components/MyChat/chat/emoji/36.gif


BIN
src/components/MyChat/chat/emoji/37.gif


BIN
src/components/MyChat/chat/emoji/38.gif


BIN
src/components/MyChat/chat/emoji/39.gif


BIN
src/components/MyChat/chat/emoji/4.gif


BIN
src/components/MyChat/chat/emoji/40.gif


BIN
src/components/MyChat/chat/emoji/41.gif


BIN
src/components/MyChat/chat/emoji/42.gif


Some files were not shown because too many files changed in this diff