zgq 5 gadi atpakaļ
vecāks
revīzija
1949120199

+ 5 - 1
package.json

@@ -19,9 +19,12 @@
     "js-cookie": "2.2.0",
     "normalize.css": "7.0.0",
     "nprogress": "0.2.0",
+    "file-saver": "1.3.8",
     "vue": "2.5.17",
     "vue-router": "3.0.1",
-    "vuex": "3.0.1"
+    "vuex": "3.0.1",
+    "vue-i18n": "7.3.2",
+    "xlsx": "^0.11.16"
   },
   "devDependencies": {
     "autoprefixer": "8.5.0",
@@ -63,6 +66,7 @@
     "shelljs": "0.8.2",
     "svg-sprite-loader": "3.8.0",
     "svgo": "1.0.5",
+    "script-loader": "0.7.2",
     "uglifyjs-webpack-plugin": "1.2.7",
     "url-loader": "1.0.1",
     "vue-loader": "15.3.0",

+ 41 - 0
src/api/article.js

@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function fetchList(query) {
+  return request({
+    url: '/user/list',
+    method: 'post',
+    params: query
+  })
+}
+
+export function fetchArticle(id) {
+  return request({
+    url: '/article/detail',
+    method: 'get',
+    params: { id }
+  })
+}
+
+export function fetchPv(pv) {
+  return request({
+    url: '/article/pv',
+    method: 'get',
+    params: { pv }
+  })
+}
+
+export function createArticle(data) {
+  return request({
+    url: '/article/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateArticle(data) {
+  return request({
+    url: '/article/update',
+    method: 'post',
+    data
+  })
+}

+ 100 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"/>
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scrollTo'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 13 - 0
src/directive/waves/index.js

@@ -0,0 +1,13 @@
+import waves from './waves'
+
+const install = function(Vue) {
+  Vue.directive('waves', waves)
+}
+
+if (window.Vue) {
+  window.waves = waves
+  Vue.use(install); // eslint-disable-line
+}
+
+waves.install = install
+export default waves

+ 26 - 0
src/directive/waves/waves.css

@@ -0,0 +1,26 @@
+.waves-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;
+}
+
+.waves-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;
+}

+ 42 - 0
src/directive/waves/waves.js

@@ -0,0 +1,42 @@
+import './waves.css'
+
+export default{
+  bind(el, binding) {
+    el.addEventListener('click', e => {
+      const customOpts = Object.assign({}, binding.value)
+      const opts = Object.assign({
+        ele: el, // 波纹作用元素
+        type: 'hit', // hit 点击位置扩散 center中心点扩展
+        color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+      }, customOpts)
+      const target = opts.ele
+      if (target) {
+        target.style.position = 'relative'
+        target.style.overflow = 'hidden'
+        const rect = target.getBoundingClientRect()
+        let ripple = target.querySelector('.waves-ripple')
+        if (!ripple) {
+          ripple = document.createElement('span')
+          ripple.className = 'waves-ripple'
+          ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+          target.appendChild(ripple)
+        } else {
+          ripple.className = 'waves-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.documentElement.scrollTop || document.body.scrollTop) + 'px'
+            ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || document.body.scrollLeft) + 'px'
+        }
+        ripple.style.backgroundColor = opts.color
+        ripple.className = 'waves-ripple z-active'
+        return false
+      }
+    }, false)
+  }
+}
+

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
src/icons/svg/language.svg


+ 160 - 0
src/lang/en.js

@@ -0,0 +1,160 @@
+export default {
+  route: {
+    dashboard: 'Dashboard',
+    introduction: 'Introduction',
+    documentation: 'Documentation',
+    guide: 'Guide',
+    permission: 'Permission',
+    pagePermission: 'Page Permission',
+    directivePermission: 'Directive Permission',
+    icons: 'Icons',
+    components: 'Components',
+    componentIndex: 'Introduction',
+    tinymce: 'Tinymce',
+    markdown: 'Markdown',
+    jsonEditor: 'JSON Editor',
+    dndList: 'Dnd List',
+    splitPane: 'SplitPane',
+    avatarUpload: 'Avatar Upload',
+    dropzone: 'Dropzone',
+    sticky: 'Sticky',
+    countTo: 'CountTo',
+    componentMixin: 'Mixin',
+    backToTop: 'BackToTop',
+    dragDialog: 'Drag Dialog',
+    dragSelect: 'Drag Select',
+    dragKanban: 'Drag Kanban',
+    charts: 'Charts',
+    keyboardChart: 'Keyboard Chart',
+    lineChart: 'Line Chart',
+    mixChart: 'Mix Chart',
+    example: 'Example',
+    nested: 'Nested Routes',
+    menu1: 'Menu 1',
+    'menu1-1': 'Menu 1-1',
+    'menu1-2': 'Menu 1-2',
+    'menu1-2-1': 'Menu 1-2-1',
+    'menu1-2-2': 'Menu 1-2-2',
+    'menu1-3': 'Menu 1-3',
+    menu2: 'Menu 2',
+    Table: 'Table',
+    dynamicTable: 'Dynamic Table',
+    dragTable: 'Drag Table',
+    inlineEditTable: 'Inline Edit',
+    complexTable: 'Complex Table',
+    treeTable: 'Tree Table',
+    customTreeTable: 'Custom TreeTable',
+    tab: 'Tab',
+    form: 'Form',
+    createArticle: 'Create Article',
+    editArticle: 'Edit Article',
+    articleList: 'Article List',
+    errorPages: 'Error Pages',
+    page401: '401',
+    page404: '404',
+    errorLog: 'Error Log',
+    excel: 'Excel',
+    exportExcel: 'Export Excel',
+    selectExcel: 'Export Selected',
+    uploadExcel: 'Upload Excel',
+    zip: 'Zip',
+    exportZip: 'Export Zip',
+    theme: 'Theme',
+    clipboardDemo: 'Clipboard',
+    i18n: 'I18n',
+    externalLink: 'External Link'
+  },
+  navbar: {
+    logOut: 'Log Out',
+    dashboard: 'Dashboard',
+    github: 'Github',
+    screenfull: 'Screenfull',
+    theme: 'Theme',
+    size: 'Global Size'
+  },
+  login: {
+    title: 'Login Form',
+    logIn: 'Log in',
+    username: 'Username',
+    password: 'Password',
+    any: 'any',
+    thirdparty: 'Or connect with',
+    thirdpartyTips: 'Can not be simulated on local, so please combine you own business simulation! ! !'
+  },
+  documentation: {
+    documentation: 'Documentation',
+    github: 'Github Repository'
+  },
+  permission: {
+    roles: 'Your roles',
+    switchRoles: 'Switch roles',
+    tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.'
+  },
+  guide: {
+    description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',
+    button: 'Show Guide'
+  },
+  components: {
+    documentation: 'Documentation',
+    tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
+    dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
+    stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
+    backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
+    backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
+    imageUploadTips: 'Since I was using only the vue@1 version, and it is not compatible with mockjs at the moment, I modified it myself, and if you are going to use it, it is better to use official version.'
+  },
+  table: {
+    dynamicTips1: 'Fixed header, sorted by header order',
+    dynamicTips2: 'Not fixed header, sorted by click order',
+    dragTips1: 'The default order',
+    dragTips2: 'The after dragging order',
+    title: 'Title',
+    importance: 'Imp',
+    type: 'Type',
+    remark: 'Remark',
+    search: 'Search',
+    add: 'Add',
+    export: 'Export',
+    reviewer: 'reviewer',
+    id: 'ID',
+    date: 'Date',
+    author: 'Author',
+    readings: 'Readings',
+    status: 'Status',
+    actions: 'Actions',
+    edit: 'Edit',
+    publish: 'Publish',
+    draft: 'Draft',
+    delete: 'Delete',
+    cancel: 'Cancel',
+    confirm: 'Confirm'
+  },
+  errorLog: {
+    tips: 'Please click the bug icon in the upper right corner',
+    description: 'Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.',
+    documentation: 'Document introduction'
+  },
+  excel: {
+    export: 'Export',
+    selectedExport: 'Export Selected Items',
+    placeholder: 'Please enter the file name(default excel-list)'
+  },
+  zip: {
+    export: 'Export',
+    placeholder: 'Please enter the file name(default file)'
+  },
+  pdf: {
+    tips: 'Here we use window.print() to implement the feature of downloading pdf.'
+  },
+  theme: {
+    change: 'Change Theme',
+    documentation: 'Theme documentation',
+    tips: 'Tips: It is different from the theme-pick on the navbar is two different skinning methods, each with different application scenarios. Refer to the documentation for details.'
+  },
+  tagsView: {
+    refresh: 'Refresh',
+    close: 'Close',
+    closeOthers: 'Close Others',
+    closeAll: 'Close All'
+  }
+}

+ 160 - 0
src/lang/es.js

@@ -0,0 +1,160 @@
+export default {
+  route: {
+    dashboard: 'Panel de control',
+    introduction: 'Introducción',
+    documentation: 'Documentación',
+    guide: 'Guía',
+    permission: 'Permisos',
+    pagePermission: 'Permisos de la página',
+    directivePermission: 'Permisos de la directiva',
+    icons: 'Iconos',
+    components: 'Componentes',
+    componentIndex: 'Introducción',
+    tinymce: 'Tinymce',
+    markdown: 'Markdown',
+    jsonEditor: 'Editor JSON',
+    dndList: 'Lista Dnd',
+    splitPane: 'Panel dividido',
+    avatarUpload: 'Subir avatar',
+    dropzone: 'Subir ficheros',
+    sticky: 'Sticky',
+    countTo: 'CountTo',
+    componentMixin: 'Mixin',
+    backToTop: 'Ir arriba',
+    dragDialog: 'Drag Dialog',
+    dragSelect: 'Drag Select',
+    dragKanban: 'Drag Kanban',
+    charts: 'Gráficos',
+    keyboardChart: 'Keyboard Chart',
+    lineChart: 'Gráfico de líneas',
+    mixChart: 'Mix Chart',
+    example: 'Ejemplo',
+    nested: 'Rutas anidadass',
+    menu1: 'Menu 1',
+    'menu1-1': 'Menu 1-1',
+    'menu1-2': 'Menu 1-2',
+    'menu1-2-1': 'Menu 1-2-1',
+    'menu1-2-2': 'Menu 1-2-2',
+    'menu1-3': 'Menu 1-3',
+    menu2: 'Menu 2',
+    Table: 'Tabla',
+    dynamicTable: 'Tabla dinámica',
+    dragTable: 'Arrastrar tabla',
+    inlineEditTable: 'Editor',
+    complexTable: 'Complex Table',
+    treeTable: 'Tree Table',
+    customTreeTable: 'Custom TreeTable',
+    tab: 'Pestaña',
+    form: 'Formulario',
+    createArticle: 'Crear artículo',
+    editArticle: 'Editar artículo',
+    articleList: 'Listado de artículos',
+    errorPages: 'Páginas de error',
+    page401: '401',
+    page404: '404',
+    errorLog: 'Registro de errores',
+    excel: 'Excel',
+    exportExcel: 'Exportar a Excel',
+    selectExcel: 'Export seleccionado',
+    uploadExcel: 'Subir Excel',
+    zip: 'Zip',
+    exportZip: 'Exportar a Zip',
+    theme: 'Tema',
+    clipboardDemo: 'Clipboard',
+    i18n: 'I18n',
+    externalLink: 'Enlace externo'
+  },
+  navbar: {
+    logOut: 'Salir',
+    dashboard: 'Panel de control',
+    github: 'Github',
+    screenfull: 'Pantalla completa',
+    theme: 'Tema',
+    size: 'Tamaño global'
+  },
+  login: {
+    title: 'Formulario de acceso',
+    logIn: 'Acceso',
+    username: 'Usuario',
+    password: 'Contraseña',
+    any: 'nada',
+    thirdparty: 'Conectar con',
+    thirdpartyTips: 'No se puede simular en local, así que combine su propia simulación de negocios. ! !'
+  },
+  documentation: {
+    documentation: 'Documentación',
+    github: 'Repositorio Github'
+  },
+  permission: {
+    roles: 'Tus permisos',
+    switchRoles: 'Cambiar permisos',
+    tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.'
+  },
+  guide: {
+    description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',
+    button: 'Ver guía'
+  },
+  components: {
+    documentation: 'Documentación',
+    tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
+    dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
+    stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
+    backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
+    backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
+    imageUploadTips: 'Since I was using only the vue@1 version, and it is not compatible with mockjs at the moment, I modified it myself, and if you are going to use it, it is better to use official version.'
+  },
+  table: {
+    dynamicTips1: 'Fixed header, sorted by header order',
+    dynamicTips2: 'Not fixed header, sorted by click order',
+    dragTips1: 'Orden por defecto',
+    dragTips2: 'The after dragging order',
+    title: 'Título',
+    importance: 'Importancia',
+    type: 'Tipo',
+    remark: 'Remark',
+    search: 'Buscar',
+    add: 'Añadir',
+    export: 'Exportar',
+    reviewer: 'reviewer',
+    id: 'ID',
+    date: 'Fecha',
+    author: 'Autor',
+    readings: 'Lector',
+    status: 'Estado',
+    actions: 'Acciones',
+    edit: 'Editar',
+    publish: 'Publicar',
+    draft: 'Draft',
+    delete: 'Eliminar',
+    cancel: 'Cancelar',
+    confirm: 'Confirmar'
+  },
+  errorLog: {
+    tips: 'Please click the bug icon in the upper right corner',
+    description: 'Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.',
+    documentation: 'Documento de introducción'
+  },
+  excel: {
+    export: 'Exportar',
+    selectedExport: 'Exportar seleccionados',
+    placeholder: 'Por favor escribe un nombre de fichero'
+  },
+  zip: {
+    export: 'Exportar',
+    placeholder: 'Por favor escribe un nombre de fichero'
+  },
+  pdf: {
+    tips: 'Here we use window.print() to implement the feature of downloading pdf.'
+  },
+  theme: {
+    change: 'Cambiar tema',
+    documentation: 'Documentación del tema',
+    tips: 'Tips: It is different from the theme-pick on the navbar is two different skinning methods, each with different application scenarios. Refer to the documentation for details.'
+  },
+  tagsView: {
+    refresh: 'Actualizar',
+    close: 'Cerrar',
+    closeOthers: 'Cerrar otros',
+    closeAll: 'Cerrar todos'
+  }
+}

+ 36 - 0
src/lang/index.js

@@ -0,0 +1,36 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import Cookies from 'js-cookie'
+import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang
+import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang
+import elementEsLocale from 'element-ui/lib/locale/lang/es'// element-ui lang
+import enLocale from './en'
+import zhLocale from './zh'
+import esLocale from './es'
+
+Vue.use(VueI18n)
+
+const messages = {
+  en: {
+    ...enLocale,
+    ...elementEnLocale
+  },
+  zh: {
+    ...zhLocale,
+    ...elementZhLocale
+  },
+  es: {
+    ...esLocale,
+    ...elementEsLocale
+  }
+}
+
+const i18n = new VueI18n({
+  // set locale
+  // options: en | zh | es
+  locale: Cookies.get('language') || 'en',
+  // set locale messages
+  messages
+})
+
+export default i18n

+ 160 - 0
src/lang/zh.js

@@ -0,0 +1,160 @@
+export default {
+  route: {
+    dashboard: '首页',
+    introduction: '简述',
+    documentation: '文档',
+    guide: '引导页',
+    permission: '权限测试页',
+    pagePermission: '页面权限',
+    directivePermission: '指令权限',
+    icons: '图标',
+    components: '组件',
+    componentIndex: '介绍',
+    tinymce: '富文本编辑器',
+    markdown: 'Markdown',
+    jsonEditor: 'JSON编辑器',
+    dndList: '列表拖拽',
+    splitPane: 'Splitpane',
+    avatarUpload: '头像上传',
+    dropzone: 'Dropzone',
+    sticky: 'Sticky',
+    countTo: 'CountTo',
+    componentMixin: '小组件',
+    backToTop: '返回顶部',
+    dragDialog: '拖拽 Dialog',
+    dragSelect: '拖拽 Select',
+    dragKanban: '可拖拽看板',
+    charts: '图表',
+    keyboardChart: '键盘图表',
+    lineChart: '折线图',
+    mixChart: '混合图表',
+    example: '综合实例',
+    nested: '路由嵌套',
+    menu1: '菜单1',
+    'menu1-1': '菜单1-1',
+    'menu1-2': '菜单1-2',
+    'menu1-2-1': '菜单1-2-1',
+    'menu1-2-2': '菜单1-2-2',
+    'menu1-3': '菜单1-3',
+    menu2: '菜单2',
+    Table: 'Table',
+    dynamicTable: '动态Table',
+    dragTable: '拖拽Table',
+    inlineEditTable: 'Table内编辑',
+    complexTable: '综合Table',
+    treeTable: '树形表格',
+    customTreeTable: '自定义树表',
+    tab: 'Tab',
+    form: '表单',
+    createArticle: '创建文章',
+    editArticle: '编辑文章',
+    articleList: '文章列表',
+    errorPages: '错误页面',
+    page401: '401',
+    page404: '404',
+    errorLog: '错误日志',
+    excel: 'Excel',
+    exportExcel: 'Export Excel',
+    selectExcel: 'Export Selected',
+    uploadExcel: 'Upload Excel',
+    zip: 'Zip',
+    exportZip: 'Export Zip',
+    theme: '换肤',
+    clipboardDemo: 'Clipboard',
+    i18n: '国际化',
+    externalLink: '外链'
+  },
+  navbar: {
+    logOut: '退出登录',
+    dashboard: '首页',
+    github: '项目地址',
+    screenfull: '全屏',
+    theme: '换肤',
+    size: '布局大小'
+  },
+  login: {
+    title: '系统登录',
+    logIn: '登录',
+    username: '账号',
+    password: '密码',
+    any: '随便填',
+    thirdparty: '第三方登录',
+    thirdpartyTips: '本地不能模拟,请结合自己业务进行模拟!!!'
+  },
+  documentation: {
+    documentation: '文档',
+    github: 'Github 地址'
+  },
+  permission: {
+    roles: '你的权限',
+    switchRoles: '切换权限',
+    tips: '在某些情况下,不适合使用 v-permission。例如:Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。'
+  },
+  guide: {
+    description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于',
+    button: '打开引导'
+  },
+  components: {
+    documentation: '文档',
+    tinymceTips: '富文本是管理后台一个核心的功能,但同时又是一个有很多坑的地方。在选择富文本的过程中我也走了不少的弯路,市面上常见的富文本都基本用过了,最终权衡了一下选择了Tinymce。更详细的富文本比较和介绍见',
+    dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/Dropzone',
+    stickyTips: '当页面滚动到预设的位置会吸附在顶部',
+    backToTopTips1: '页面滚动到指定位置会在右下角出现返回顶部按钮',
+    backToTopTips2: '可自定义按钮的样式、show/hide、出现的高度、返回的位置 如需文字提示,可在外部使用Element的el-tooltip元素',
+    imageUploadTips: '由于我在使用时它只有vue@1版本,而且和mockjs不兼容,所以自己改造了一下,如果大家要使用的话,优先还是使用官方版本。'
+  },
+  table: {
+    dynamicTips1: '固定表头, 按照表头顺序排序',
+    dynamicTips2: '不固定表头, 按照点击顺序排序',
+    dragTips1: '默认顺序',
+    dragTips2: '拖拽后顺序',
+    title: '标题',
+    importance: '重要性',
+    type: '类型',
+    remark: '点评',
+    search: '搜索',
+    add: '添加',
+    export: '导出',
+    reviewer: '审核人',
+    id: '序号',
+    date: '时间',
+    author: '作者',
+    readings: '阅读数',
+    status: '状态',
+    actions: '操作',
+    edit: '编辑',
+    publish: '发布',
+    draft: '草稿',
+    delete: '删除',
+    cancel: '取 消',
+    confirm: '确 定'
+  },
+  errorLog: {
+    tips: '请点击右上角bug小图标',
+    description: '现在的管理后台基本都是spa的形式了,它增强了用户体验,但同时也会增加页面出问题的可能性,可能一个小小的疏忽就导致整个页面的死锁。好在 Vue 官网提供了一个方法来捕获处理异常,你可以在其中进行错误处理或者异常上报。',
+    documentation: '文档介绍'
+  },
+  excel: {
+    export: '导出',
+    selectedExport: '导出已选择项',
+    placeholder: '请输入文件名(默认excel-list)'
+  },
+  zip: {
+    export: '导出',
+    placeholder: '请输入文件名(默认file)'
+  },
+  pdf: {
+    tips: '这里使用   window.print() 来实现下载pdf的功能'
+  },
+  theme: {
+    change: '换肤',
+    documentation: '换肤文档',
+    tips: 'Tips: 它区别于 navbar 上的 theme-pick, 是两种不同的换肤方法,各自有不同的应用场景,具体请参考文档。'
+  },
+  tagsView: {
+    refresh: '刷新',
+    close: '关闭',
+    closeOthers: '关闭其它',
+    closeAll: '关闭所有'
+  }
+}

+ 4 - 2
src/main.js

@@ -12,8 +12,9 @@ import App from './App'
 import router from './router'
 import store from './store'
 
-import '@/icons' // icon
-import '@/permission' // permission control
+import i18n from './lang' // Internationalization
+import './icons' // icon
+import './permission' // permission control
 
 Vue.use(ElementUI, { locale })
 
@@ -21,6 +22,7 @@ Vue.config.productionTip = false
 
 new Vue({
   el: '#app',
+  i18n,
   router,
   store,
   render: h => h(App)

+ 1 - 1
src/router/index.js

@@ -43,7 +43,7 @@ export const constantRouterMap = [
     component: Layout,
     redirect: '/example/table',
     name: 'Example',
-    meta: { title: 'Example222', icon: 'example' },
+    meta: { title: '首页', icon: 'example' },
     children: [
       {
         path: 'table',

+ 1 - 1
src/utils/request.js

@@ -6,7 +6,7 @@ import { getToken } from '@/utils/auth'
 // 创建axios实例
 const service = axios.create({
   baseURL: process.env.BASE_API, // api 的 base_url
-  timeout: 5000 // 请求超时时间
+  timeout: 1000000 // 请求超时时间
 })
 
 // request拦截器

+ 50 - 0
src/utils/scrollTo.js

@@ -0,0 +1,50 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+// because it's so fucking difficult to detect the scrolling element, just move them all
+function move(amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position() {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+export function scrollTo(to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function() {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}

+ 206 - 0
src/vendor/Export2Excel.js

@@ -0,0 +1,206 @@
+/* eslint-disable */
+require('script-loader!file-saver');
+import XLSX from 'xlsx'
+
+function generateArray(table) {
+  var out = [];
+  var rows = table.querySelectorAll('tr');
+  var ranges = [];
+  for (var R = 0; R < rows.length; ++R) {
+    var outRow = [];
+    var row = rows[R];
+    var columns = row.querySelectorAll('td');
+    for (var C = 0; C < columns.length; ++C) {
+      var cell = columns[C];
+      var colspan = cell.getAttribute('colspan');
+      var rowspan = cell.getAttribute('rowspan');
+      var cellValue = cell.innerText;
+      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
+
+      //Skip ranges
+      ranges.forEach(function (range) {
+        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
+          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
+        }
+      });
+
+      //Handle Row Span
+      if (rowspan || colspan) {
+        rowspan = rowspan || 1;
+        colspan = colspan || 1;
+        ranges.push({
+          s: {
+            r: R,
+            c: outRow.length
+          },
+          e: {
+            r: R + rowspan - 1,
+            c: outRow.length + colspan - 1
+          }
+        });
+      };
+
+      //Handle Value
+      outRow.push(cellValue !== "" ? cellValue : null);
+
+      //Handle Colspan
+      if (colspan)
+        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
+    }
+    out.push(outRow);
+  }
+  return [out, ranges];
+};
+
+function datenum(v, date1904) {
+  if (date1904) v += 1462;
+  var epoch = Date.parse(v);
+  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
+}
+
+function sheet_from_array_of_arrays(data, opts) {
+  var ws = {};
+  var range = {
+    s: {
+      c: 10000000,
+      r: 10000000
+    },
+    e: {
+      c: 0,
+      r: 0
+    }
+  };
+  for (var R = 0; R != data.length; ++R) {
+    for (var C = 0; C != data[R].length; ++C) {
+      if (range.s.r > R) range.s.r = R;
+      if (range.s.c > C) range.s.c = C;
+      if (range.e.r < R) range.e.r = R;
+      if (range.e.c < C) range.e.c = C;
+      var cell = {
+        v: data[R][C]
+      };
+      if (cell.v == null) continue;
+      var cell_ref = XLSX.utils.encode_cell({
+        c: C,
+        r: R
+      });
+
+      if (typeof cell.v === 'number') cell.t = 'n';
+      else if (typeof cell.v === 'boolean') cell.t = 'b';
+      else if (cell.v instanceof Date) {
+        cell.t = 'n';
+        cell.z = XLSX.SSF._table[14];
+        cell.v = datenum(cell.v);
+      } else cell.t = 's';
+
+      ws[cell_ref] = cell;
+    }
+  }
+  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
+  return ws;
+}
+
+function Workbook() {
+  if (!(this instanceof Workbook)) return new Workbook();
+  this.SheetNames = [];
+  this.Sheets = {};
+}
+
+function s2ab(s) {
+  var buf = new ArrayBuffer(s.length);
+  var view = new Uint8Array(buf);
+  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+  return buf;
+}
+
+export function export_table_to_excel(id) {
+  var theTable = document.getElementById(id);
+  var oo = generateArray(theTable);
+  var ranges = oo[1];
+
+  /* original data */
+  var data = oo[0];
+  var ws_name = "SheetJS";
+
+  var wb = new Workbook(),
+    ws = sheet_from_array_of_arrays(data);
+
+  /* add ranges to worksheet */
+  // ws['!cols'] = ['apple', 'banan'];
+  ws['!merges'] = ranges;
+
+  /* add worksheet to workbook */
+  wb.SheetNames.push(ws_name);
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, {
+    bookType: 'xlsx',
+    bookSST: false,
+    type: 'binary'
+  });
+
+  saveAs(new Blob([s2ab(wbout)], {
+    type: "application/octet-stream"
+  }), "test.xlsx")
+}
+
+export function export_json_to_excel({
+  header,
+  data,
+  filename,
+  autoWidth = true,
+  bookType=  'xlsx'
+} = {}) {
+  /* original data */
+  filename = filename || 'excel-list'
+  data = [...data]
+  data.unshift(header);
+  var ws_name = "SheetJS";
+  var wb = new Workbook(),
+    ws = sheet_from_array_of_arrays(data);
+
+  if (autoWidth) {
+    /*设置worksheet每列的最大宽度*/
+    const colWidth = data.map(row => row.map(val => {
+      /*先判断是否为null/undefined*/
+      if (val == null) {
+        return {
+          'wch': 10
+        };
+      }
+      /*再判断是否为中文*/
+      else if (val.toString().charCodeAt(0) > 255) {
+        return {
+          'wch': val.toString().length * 2
+        };
+      } else {
+        return {
+          'wch': val.toString().length
+        };
+      }
+    }))
+    /*以第一行为初始值*/
+    let result = colWidth[0];
+    for (let i = 1; i < colWidth.length; i++) {
+      for (let j = 0; j < colWidth[i].length; j++) {
+        if (result[j]['wch'] < colWidth[i][j]['wch']) {
+          result[j]['wch'] = colWidth[i][j]['wch'];
+        }
+      }
+    }
+    ws['!cols'] = result;
+  }
+
+  /* add worksheet to workbook */
+  wb.SheetNames.push(ws_name);
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, {
+    bookType: bookType,
+    bookSST: false,
+    type: 'binary'
+  });
+  saveAs(new Blob([s2ab(wbout)], {
+    type: "application/octet-stream"
+  }), `${filename}.${bookType}`);
+}

+ 24 - 0
src/vendor/Export2Zip.js

@@ -0,0 +1,24 @@
+/* eslint-disable */
+require('script-loader!file-saver');
+import JSZip from 'jszip'
+
+export function export_txt_to_zip(th, jsonData, txtName, zipName) {
+  const zip = new JSZip()
+  const txt_name = txtName || 'file'
+  const zip_name = zipName || 'file'
+  const data = jsonData
+  let txtData = `${th}\r\n`
+  data.forEach((row) => {
+    let tempStr = ''
+    tempStr = row.toString()
+    txtData += `${tempStr}\r\n`
+  })
+  zip.file(`${txt_name}.txt`, txtData)
+  zip.generateAsync({
+    type: "blob"
+  }).then((blob) => {
+    saveAs(blob, `${zip_name}.zip`)
+  }, (err) => {
+    alert('导出失败')
+  })
+}

+ 1 - 1
src/views/dashboard/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="dashboard-container">
     <div class="dashboard-text">name:{{ name }}</div>
-    <div class="dashboard-text">roles:<span v-for="role in roles" :key="role">{{ role }}</span></div>
+    <!--<div class="dashboard-text">roles:<span v-for="role in roles" :key="role">{{ role }}</span></div>-->
   </div>
 </template>
 

+ 183 - 101
src/views/login/index.vue

@@ -1,72 +1,104 @@
 <template>
   <div class="login-container">
+
     <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
-      <h3 class="title">系统登录</h3>
+
+      <div class="title-container">
+        <h3 class="title">{{ $t('login.title') }}</h3>
+        <lang-select class="set-language"/>
+      </div>
+
       <el-form-item prop="username">
         <span class="svg-container">
           <svg-icon icon-class="user" />
         </span>
-        <el-input v-model="loginForm.username" name="username" type="text" auto-complete="on" placeholder="用户名" />
+        <el-input
+          v-model="loginForm.username"
+          :placeholder="$t('login.username')"
+          name="username"
+          type="text"
+          auto-complete="on"
+        />
       </el-form-item>
+
       <el-form-item prop="password">
         <span class="svg-container">
           <svg-icon icon-class="password" />
         </span>
         <el-input
-          :type="pwdType"
+          :type="passwordType"
           v-model="loginForm.password"
+          :placeholder="$t('login.password')"
           name="password"
           auto-complete="on"
-          placeholder="密码"
           @keyup.enter.native="handleLogin" />
         <span class="show-pwd" @click="showPwd">
           <svg-icon icon-class="eye" />
         </span>
       </el-form-item>
-      <el-form-item>
-        <el-button :loading="loading" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
-          登录
-        </el-button>
-      </el-form-item>
-      <!--<div class="tips">
-        <span style="margin-right:20px;">username: admin</span>
-        <span> password: admin</span>
-      </div>-->
+
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">{{ $t('login.logIn') }}</el-button>
+
+      <div style="position:relative">
+        <div class="tips">
+          <span>{{ $t('login.username') }} : admin</span>
+          <span>{{ $t('login.password') }} : {{ $t('login.any') }}</span>
+        </div>
+        <div class="tips">
+          <span style="margin-right:18px;">{{ $t('login.username') }} : editor</span>
+          <span>{{ $t('login.password') }} : {{ $t('login.any') }}</span>
+        </div>
+
+        <el-button class="thirdparty-button" type="primary" @click="showDialog=true">{{ $t('login.thirdparty') }}</el-button>
+      </div>
     </el-form>
+
+    <el-dialog :title="$t('login.thirdparty')" :visible.sync="showDialog">
+      {{ $t('login.thirdpartyTips') }}
+      <br>
+      <br>
+      <br>
+      <social-sign />
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
 import { isvalidUsername } from '@/utils/validate'
+import LangSelect from '@/components/LangSelect'
+import SocialSign from './socialsignin'
 
 export default {
   name: 'Login',
+  components: { LangSelect, SocialSign },
   data() {
     const validateUsername = (rule, value, callback) => {
       if (!isvalidUsername(value)) {
-        callback(new Error('请输入正确的用户名'))
+        callback(new Error('Please enter the correct user name'))
       } else {
         callback()
       }
     }
-    const validatePass = (rule, value, callback) => {
-      if (value.length < 5) {
-        callback(new Error('密码不能小于5位'))
+    const validatePassword = (rule, value, callback) => {
+      if (value.length < 6) {
+        callback(new Error('The password can not be less than 6 digits'))
       } else {
         callback()
       }
     }
     return {
       loginForm: {
-        username: '',
-        password: ''
+        username: 'admin',
+        password: 'Rxida@2019'
       },
       loginRules: {
         username: [{ required: true, trigger: 'blur', validator: validateUsername }],
-        password: [{ required: true, trigger: 'blur', validator: validatePass }]
+        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
       },
+      passwordType: 'password',
       loading: false,
-      pwdType: 'password',
+      showDialog: false,
       redirect: undefined
     }
   },
@@ -78,12 +110,18 @@ export default {
       immediate: true
     }
   },
+  created() {
+    // window.addEventListener('hashchange', this.afterQRScan)
+  },
+  destroyed() {
+    // window.removeEventListener('hashchange', this.afterQRScan)
+  },
   methods: {
     showPwd() {
-      if (this.pwdType === 'password') {
-        this.pwdType = ''
+      if (this.passwordType === 'password') {
+        this.passwordType = ''
       } else {
-        this.pwdType = 'password'
+        this.passwordType = 'password'
       }
     },
     handleLogin() {
@@ -97,100 +135,144 @@ export default {
             this.loading = false
           })
         } else {
-          this.loading = false
+          console.log('error submit!!')
           return false
         }
       })
+    },
+    afterQRScan() {
+      // const hash = window.location.hash.slice(1)
+      // const hashObj = getQueryObject(hash)
+      // const originUrl = window.location.origin
+      // history.replaceState({}, '', originUrl)
+      // const codeMap = {
+      //   wechat: 'code',
+      //   tencent: 'code'
+      // }
+      // const codeName = hashObj[codeMap[this.auth_type]]
+      // if (!codeName) {
+      //   alert('第三方登录失败')
+      // } else {
+      //   this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
+      //     this.$router.push({ path: '/' })
+      //   })
+      // }
     }
   }
 }
 </script>
 
 <style rel="stylesheet/scss" lang="scss">
-$bg:#2d3a4b;
-$light_gray:#eee;
-
-/* reset element-ui css */
-.login-container {
-  .el-input {
-    display: inline-block;
-    height: 47px;
-    width: 85%;
-    input {
-      background: transparent;
-      border: 0px;
-      -webkit-appearance: none;
-      border-radius: 0px;
-      padding: 12px 5px 12px 15px;
-      color: $light_gray;
-      height: 47px;
-      &:-webkit-autofill {
-        -webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
-        -webkit-text-fill-color: #fff !important;
+  /* 修复input 背景不协调 和光标变色 */
+  /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
+
+  $bg:#283443;
+  $light_gray:#eee;
+  $cursor: #fff;
+
+  @supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
+    .login-container .el-input input{
+      color: $cursor;
+      &::first-line {
+        color: $light_gray;
       }
     }
   }
-  .el-form-item {
-    border: 1px solid rgba(255, 255, 255, 0.1);
-    background: rgba(0, 0, 0, 0.1);
-    border-radius: 5px;
-    color: #454545;
-  }
-}
 
+  /* reset element-ui css */
+  .login-container {
+    .el-input {
+      display: inline-block;
+      height: 47px;
+      width: 85%;
+      input {
+        background: transparent;
+        border: 0px;
+        -webkit-appearance: none;
+        border-radius: 0px;
+        padding: 12px 5px 12px 15px;
+        color: $light_gray;
+        height: 47px;
+        caret-color: $cursor;
+        &:-webkit-autofill {
+          -webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
+          -webkit-text-fill-color: $cursor !important;
+        }
+      }
+    }
+    .el-form-item {
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      background: rgba(0, 0, 0, 0.1);
+      border-radius: 5px;
+      color: #454545;
+    }
+  }
 </style>
 
 <style rel="stylesheet/scss" lang="scss" scoped>
-$bg:#2d3a4b;
-$dark_gray:#889aa4;
-$light_gray:#eee;
-.login-container {
-  position: fixed;
-  height: 100%;
-  width: 100%;
-  background-color: $bg;
-  .login-form {
-    position: absolute;
-    left: 0;
-    right: 0;
-    width: 520px;
-    max-width: 100%;
-    padding: 35px 35px 15px 35px;
-    margin: 120px auto;
-  }
-  .tips {
-    font-size: 14px;
-    color: #fff;
-    margin-bottom: 10px;
-    span {
-      &:first-of-type {
-        margin-right: 16px;
+  $bg:#2d3a4b;
+  $dark_gray:#889aa4;
+  $light_gray:#eee;
+
+  .login-container {
+    min-height: 100%;
+    width: 100%;
+    background-color: $bg;
+    overflow: hidden;
+    .login-form {
+      position: relative;
+      width: 520px;
+      max-width: 100%;
+      padding: 160px 35px 0;
+      margin: 0 auto;
+      overflow: hidden;
+    }
+    .tips {
+      font-size: 14px;
+      color: #fff;
+      margin-bottom: 10px;
+      span {
+        &:first-of-type {
+          margin-right: 16px;
+        }
       }
     }
+    .svg-container {
+      padding: 6px 5px 6px 15px;
+      color: $dark_gray;
+      vertical-align: middle;
+      width: 30px;
+      display: inline-block;
+    }
+    .title-container {
+      position: relative;
+      .title {
+        font-size: 26px;
+        color: $light_gray;
+        margin: 0px auto 40px auto;
+        text-align: center;
+        font-weight: bold;
+      }
+      .set-language {
+        color: #fff;
+        position: absolute;
+        top: 5px;
+        right: 0px;
+      }
+    }
+    .show-pwd {
+      position: absolute;
+      right: 10px;
+      top: 7px;
+      font-size: 16px;
+      color: $dark_gray;
+      cursor: pointer;
+      user-select: none;
+    }
+    .thirdparty-button {
+      position: absolute;
+      right: 0;
+      bottom: 6px;
+    }
   }
-  .svg-container {
-    padding: 6px 5px 6px 15px;
-    color: $dark_gray;
-    vertical-align: middle;
-    width: 30px;
-    display: inline-block;
-  }
-  .title {
-    font-size: 26px;
-    font-weight: 400;
-    color: $light_gray;
-    margin: 0 auto 40px auto;
-    text-align: center;
-    font-weight: bold;
-  }
-  .show-pwd {
-    position: absolute;
-    right: 10px;
-    top: 7px;
-    font-size: 16px;
-    color: $dark_gray;
-    cursor: pointer;
-    user-select: none;
-  }
-}
 </style>

+ 304 - 21
src/views/table/index.vue

@@ -1,77 +1,360 @@
 <template>
   <div class="app-container">
+    <div class="filter-container">
+      <el-input :placeholder="$t('table.title')" v-model="listQuery.title" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter"/>
+      <el-select v-model="listQuery.importance" :placeholder="$t('table.importance')" clearable style="width: 90px" class="filter-item">
+        <el-option v-for="item in importanceOptions" :key="item" :label="item" :value="item"/>
+      </el-select>
+      <el-select v-model="listQuery.type" :placeholder="$t('table.type')" clearable class="filter-item" style="width: 130px">
+        <el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name+'('+item.key+')'" :value="item.key"/>
+      </el-select>
+      <el-select v-model="listQuery.sort" style="width: 140px" class="filter-item" @change="handleFilter">
+        <el-option v-for="item in sortOptions" :key="item.key" :label="item.label" :value="item.key"/>
+      </el-select>
+      <el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">{{ $t('table.search') }}</el-button>
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-edit" @click="handleCreate">{{ $t('table.add') }}</el-button>
+      <el-button v-waves :loading="downloadLoading" class="filter-item" type="primary" icon="el-icon-download" @click="handleDownload">{{ $t('table.export') }}</el-button>
+      <el-checkbox v-model="showReviewer" class="filter-item" style="margin-left:15px;" @change="tableKey=tableKey+1">{{ $t('table.reviewer') }}</el-checkbox>
+    </div>
+
     <el-table
       v-loading="listLoading"
+      :key="tableKey"
       :data="list"
-      element-loading-text="Loading"
       border
       fit
-      highlight-current-row>
-      <el-table-column align="center" label="ID" width="95">
+      highlight-current-row
+      style="width: 100%;"
+      @sort-change="sortChange">
+      <el-table-column :label="$t('table.id')" prop="id" sortable="custom" align="center" width="65">
+        <template slot-scope="scope">
+          <span>{{ scope.row.id }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('table.date')" width="150px" align="center">
         <template slot-scope="scope">
-          {{ scope.$index }}
+          <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="Title">
+      <el-table-column :label="$t('table.title')" min-width="150px">
         <template slot-scope="scope">
-          {{ scope.row.title }}
+          <span class="link-type" @click="handleUpdate(scope.row)">{{ scope.row.title }}</span>
+          <el-tag>{{ scope.row.type | typeFilter }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="Author" width="110" align="center">
+      <el-table-column :label="$t('table.author')" width="110px" align="center">
         <template slot-scope="scope">
           <span>{{ scope.row.author }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="Pageviews" width="110" align="center">
+      <el-table-column v-if="showReviewer" :label="$t('table.reviewer')" width="110px" align="center">
+        <template slot-scope="scope">
+          <span style="color:red;">{{ scope.row.reviewer }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('table.importance')" width="80px">
         <template slot-scope="scope">
-          {{ scope.row.pageviews }}
+          <svg-icon v-for="n in +scope.row.importance" :key="n" icon-class="star" class="meta-item__icon"/>
         </template>
       </el-table-column>
-      <el-table-column class-name="status-col" label="Status" width="110" align="center">
+      <el-table-column :label="$t('table.readings')" align="center" width="95">
+        <template slot-scope="scope">
+          <span v-if="scope.row.pageviews" class="link-type" @click="handleFetchPv(scope.row.pageviews)">{{ scope.row.pageviews }}</span>
+          <span v-else>0</span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('table.status')" class-name="status-col" width="100">
         <template slot-scope="scope">
           <el-tag :type="scope.row.status | statusFilter">{{ scope.row.status }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column align="center" prop="created_at" label="Display_time" width="200">
+      <el-table-column :label="$t('table.actions')" align="center" width="230" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <i class="el-icon-time"/>
-          <span>{{ scope.row.display_time }}</span>
+          <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">{{ $t('table.edit') }}</el-button>
+          <el-button v-if="scope.row.status!='published'" size="mini" type="success" @click="handleModifyStatus(scope.row,'published')">{{ $t('table.publish') }}
+          </el-button>
+          <el-button v-if="scope.row.status!='draft'" size="mini" @click="handleModifyStatus(scope.row,'draft')">{{ $t('table.draft') }}
+          </el-button>
+          <el-button v-if="scope.row.status!='deleted'" size="mini" type="danger" @click="handleModifyStatus(scope.row,'deleted')">{{ $t('table.delete') }}
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
+      <el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="70px" style="width: 400px; margin-left:50px;">
+        <el-form-item :label="$t('table.type')" prop="type">
+          <el-select v-model="temp.type" class="filter-item" placeholder="Please select">
+            <el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name" :value="item.key"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="$t('table.date')" prop="timestamp">
+          <el-date-picker v-model="temp.timestamp" type="datetime" placeholder="Please pick a date"/>
+        </el-form-item>
+        <el-form-item :label="$t('table.title')" prop="title">
+          <el-input v-model="temp.title"/>
+        </el-form-item>
+        <el-form-item :label="$t('table.status')">
+          <el-select v-model="temp.status" class="filter-item" placeholder="Please select">
+            <el-option v-for="item in statusOptions" :key="item" :label="item" :value="item"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="$t('table.importance')">
+          <el-rate v-model="temp.importance" :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" style="margin-top:8px;"/>
+        </el-form-item>
+        <el-form-item :label="$t('table.remark')">
+          <el-input :autosize="{ minRows: 2, maxRows: 4}" v-model="temp.remark" type="textarea" placeholder="Please input"/>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">{{ $t('table.cancel') }}</el-button>
+        <el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">{{ $t('table.confirm') }}</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :visible.sync="dialogPvVisible" title="Reading statistics">
+      <el-table :data="pvData" border fit highlight-current-row style="width: 100%">
+        <el-table-column prop="key" label="Channel"/>
+        <el-table-column prop="pv" label="Pv"/>
+      </el-table>
+      <span slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="dialogPvVisible = false">{{ $t('table.confirm') }}</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
-
 <script>
-import { getList } from '@/api/table'
+import { fetchList, fetchPv, createArticle, updateArticle } from '@/api/article'
+import waves from '@/directive/waves' // Waves directive
+import { parseTime } from '@/utils'
+import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
+
+const calendarTypeOptions = [
+  { key: 'CN', display_name: 'China' },
+  { key: 'US', display_name: 'USA' },
+  { key: 'JP', display_name: 'Japan' },
+  { key: 'EU', display_name: 'Eurozone' }
+]
+
+// arr to obj ,such as { CN : "China", US : "USA" }
+const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {
+  acc[cur.key] = cur.display_name
+  return acc
+}, {})
 
 export default {
+  name: 'ComplexTable',
+  components: { Pagination },
+  directives: { waves },
   filters: {
     statusFilter(status) {
       const statusMap = {
         published: 'success',
-        draft: 'gray',
+        draft: 'info',
         deleted: 'danger'
       }
       return statusMap[status]
+    },
+    typeFilter(type) {
+      return calendarTypeKeyValue[type]
     }
   },
   data() {
     return {
+      tableKey: 0,
       list: null,
-      listLoading: true
+      total: 0,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 20,
+        importance: undefined,
+        title: undefined,
+        type: undefined,
+        sort: '+id'
+      },
+      importanceOptions: [1, 2, 3],
+      calendarTypeOptions,
+      sortOptions: [{ label: 'ID Ascending', key: '+id' }, { label: 'ID Descending', key: '-id' }],
+      statusOptions: ['published', 'draft', 'deleted'],
+      showReviewer: false,
+      temp: {
+        id: undefined,
+        importance: 1,
+        remark: '',
+        timestamp: new Date(),
+        title: '',
+        type: '',
+        status: 'published'
+      },
+      dialogFormVisible: false,
+      dialogStatus: '',
+      textMap: {
+        update: 'Edit',
+        create: 'Create'
+      },
+      dialogPvVisible: false,
+      pvData: [],
+      rules: {
+        type: [{ required: true, message: 'type is required', trigger: 'change' }],
+        timestamp: [{ type: 'date', required: true, message: 'timestamp is required', trigger: 'change' }],
+        title: [{ required: true, message: 'title is required', trigger: 'blur' }]
+      },
+      downloadLoading: false
     }
   },
   created() {
-    this.fetchData()
+    this.getList()
   },
   methods: {
-    fetchData() {
+    getList() {
       this.listLoading = true
-      getList(this.listQuery).then(response => {
+      fetchList(this.listQuery).then(response => {
         this.list = response.data.items
-        this.listLoading = false
+        this.total = response.data.total
+        console.log(this.list)
+        // Just to simulate the time of the request
+        setTimeout(() => {
+          this.listLoading = false
+        }, 1.5 * 1000)
+      }).catch(err)
+      this.listLoading = false
+    },
+    handleFilter() {
+      this.listQuery.page = 1
+      this.getList()
+    },
+    handleModifyStatus(row, status) {
+      this.$message({
+        message: '操作成功',
+        type: 'success'
+      })
+      row.status = status
+    },
+    sortChange(data) {
+      const { prop, order } = data
+      if (prop === 'id') {
+        this.sortByID(order)
+      }
+    },
+    sortByID(order) {
+      if (order === 'ascending') {
+        this.listQuery.sort = '+id'
+      } else {
+        this.listQuery.sort = '-id'
+      }
+      this.handleFilter()
+    },
+    resetTemp() {
+      this.temp = {
+        id: undefined,
+        importance: 1,
+        remark: '',
+        timestamp: new Date(),
+        title: '',
+        status: 'published',
+        type: ''
+      }
+    },
+    handleCreate() {
+      this.resetTemp()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    createData() {
+      this.$refs['dataForm'].validate((valid) => {
+        if (valid) {
+          this.temp.id = parseInt(Math.random() * 100) + 1024 // mock a id
+          this.temp.author = 'vue-element-admin'
+          createArticle(this.temp).then(() => {
+            this.list.unshift(this.temp)
+            this.dialogFormVisible = false
+            this.$notify({
+              title: '成功',
+              message: '创建成功',
+              type: 'success',
+              duration: 2000
+            })
+          })
+        }
+      })
+    },
+    handleUpdate(row) {
+      this.temp = Object.assign({}, row) // copy obj
+      this.temp.timestamp = new Date(this.temp.timestamp)
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    updateData() {
+      this.$refs['dataForm'].validate((valid) => {
+        if (valid) {
+          const tempData = Object.assign({}, this.temp)
+          tempData.timestamp = +new Date(tempData.timestamp) // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464
+          updateArticle(tempData).then(() => {
+            for (const v of this.list) {
+              if (v.id === this.temp.id) {
+                const index = this.list.indexOf(v)
+                this.list.splice(index, 1, this.temp)
+                break
+              }
+            }
+            this.dialogFormVisible = false
+            this.$notify({
+              title: '成功',
+              message: '更新成功',
+              type: 'success',
+              duration: 2000
+            })
+          })
+        }
+      })
+    },
+    handleDelete(row) {
+      this.$notify({
+        title: '成功',
+        message: '删除成功',
+        type: 'success',
+        duration: 2000
+      })
+      const index = this.list.indexOf(row)
+      this.list.splice(index, 1)
+    },
+    handleFetchPv(pv) {
+      fetchPv(pv).then(response => {
+        this.pvData = response.data.pvData
+        this.dialogPvVisible = true
       })
+    },
+    handleDownload() {
+      this.downloadLoading = true
+        import('@/vendor/Export2Excel').then(excel => {
+          const tHeader = ['timestamp', 'title', 'type', 'importance', 'status']
+          const filterVal = ['timestamp', 'title', 'type', 'importance', 'status']
+          const data = this.formatJson(filterVal, this.list)
+          excel.export_json_to_excel({
+            header: tHeader,
+            data,
+            filename: 'table-list'
+          })
+          this.downloadLoading = false
+        })
+    },
+    formatJson(filterVal, jsonData) {
+      return jsonData.map(v => filterVal.map(j => {
+        if (j === 'timestamp') {
+          return parseTime(v[j])
+        } else {
+          return v[j]
+        }
+      }))
     }
   }
 }