@@ -1,5 +0,0 @@ | |||||
language: node_js | |||||
node_js: stable | |||||
script: npm run lint | |||||
notifications: | |||||
email: false |
@@ -1,30 +1,4 @@ | |||||
<p align="center"> | |||||
<a href="https://www.iviewui.com"> | |||||
<img width="200" src="https://file.iviewui.com/logo-new.svg"> | |||||
</a> | |||||
</p> | |||||
# iView Admin | |||||
[](https://github.com/iview/iview-admin/releases) | |||||
[](https://travis-ci.org/iview/iview-admin) | |||||
[](https://github.com/vuejs/vue) | |||||
[](https://github.com/iview/iview) | |||||
[]() | |||||
[更新日志](https://github.com/iview/iview-admin/releases) | |||||
[使用文档](https://lison16.github.io/iview-admin-doc/#/) | |||||
[在线访问](https://admin.iviewui.com/) | |||||
[简化版模板](https://github.com/iview/iview-admin/tree/template) | |||||
[教学视频(26课时)](https://segmentfault.com/ls/1650000016221751?utm_source=banner) | |||||
`注:在线版本会在开发版本新小版本发布后更新到相应版本,所以如果想体验最新版本iview-admin,请clone完整项目代码到本地运行。` | |||||
## Install | |||||
# Install | |||||
```bush | ```bush | ||||
// install dependencies | // install dependencies | ||||
npm install | npm install | ||||
@@ -37,151 +11,4 @@ npm run dev | |||||
### Production(Build) | ### Production(Build) | ||||
```bush | ```bush | ||||
npm run build | npm run build | ||||
``` | |||||
## 简介 | |||||
  iView admin是基于Vue.js,搭配使用[iView](https://www.iviewui.com) UI组件库形成的一套后台集成解决方案,由TalkingData前端可视化团队部分成员开发维护。iView admin遵守iView设计和开发约定,风格统一,设计考究,并且更多功能在不停开发中。 | |||||
如果您想查看iview-admin的更新动态,您可以到[更新日志](https://github.com/iview/iview-admin/releases)查看了解最新更新;如果您是新手,想快速入手iview-admin,您可以到[使用教程](https://github.com/iview/iview-admin/wiki)查看讲解;如果您想在线体验iview-admin,您可以到[在线访问](https://admin.iviewui.com/)体验。如果你只是想要一个清醒爽朗的界面,那你可以下载[简化版模板](https://github.com/iview/iview-admin/tree/template)来做开发。 | |||||
## 功能 | |||||
- 登录/登出 | |||||
- 权限管理 | |||||
- 列表过滤 | |||||
- 权限切换 | |||||
- 多语言切换 | |||||
- 组件 | |||||
- 富文本编辑器 | |||||
- Markdown编辑器 | |||||
- 城市级联 | |||||
- 图片预览编辑 | |||||
- 可拖拽列表 | |||||
- 文件上传 | |||||
- 数字渐变 | |||||
- split-pane | |||||
- 表单编辑 | |||||
- 文章发布 | |||||
- 工作流 | |||||
- 表格 | |||||
- 可拖拽排序 | |||||
- 可编辑表格 | |||||
- 行内编辑 | |||||
- 单元格编辑 | |||||
- 可搜索表格 | |||||
- 表格导出数据 | |||||
- 导出为Csv文件 | |||||
- 导出为Xls文件 | |||||
- 表格转图片 | |||||
- 错误页面 | |||||
- 403页面 | |||||
- 404页面 | |||||
- 500页面 | |||||
- 高级路由 | |||||
- 动态路由 | |||||
- 带参页面 | |||||
- 换肤 | |||||
- 收缩侧边栏 | |||||
- tag标签导航 | |||||
- 面包屑导航 | |||||
- 全屏/退出全屏 | |||||
- 锁屏 | |||||
- 消息中心 | |||||
- 个人中心 | |||||
## 文件结构 | |||||
```shell | |||||
. | |||||
├── build 项目构建配置 | |||||
├── config 开发相关配置 | |||||
├── public 打包所需静态资源 | |||||
└── src | |||||
├── api AJAX请求 | |||||
└── assets 项目静态资源 | |||||
├── icons 自定义图标资源 | |||||
└── images 图片资源 | |||||
├── components 业务组件 | |||||
├── config 项目运行配置 | |||||
├── directive 自定义指令 | |||||
├── libs 封装工具函数 | |||||
├── locale 多语言文件 | |||||
├── mock mock模拟数据 | |||||
├── router 路由配置 | |||||
├── store Vuex配置 | |||||
├── view 页面文件 | |||||
└── tests 测试相关 | |||||
``` | |||||
## Links | |||||
- [TalkingData](https://github.com/TalkingData) | |||||
- [iView](https://github.com/iview/iview) | |||||
- [Vue](https://github.com/vuejs/vue) | |||||
- [Webpack](https://github.com/webpack/webpack) | |||||
## 效果展示 | |||||
- 响应式布局首页 | |||||
 | |||||
- 标签导航 | |||||
 | |||||
- 权限管理 | |||||
 | |||||
- 可拖拽列表 | |||||
 | |||||
- 图片预览编辑 | |||||
 | |||||
- 文件上传 | |||||
 | |||||
- 数字渐变 | |||||
 | |||||
- split-pane | |||||
 | |||||
- 文章发布 | |||||
 | |||||
- 工作流 | |||||
 | |||||
- 可拖拽表格 | |||||
 | |||||
- 可编辑表格 | |||||
 | |||||
- 表格导出数据 | |||||
 | |||||
- 表格转图片 | |||||
 | |||||
- 错误页面 | |||||
 | |||||
- 锁屏 | |||||
 | |||||
- 可收缩侧边栏 | |||||
 | |||||
- 主题切换 | |||||
 | |||||
- 消息中心 | |||||
 | |||||
### 💖💖 If you find this project helpful, maybe you can buy me a coffee. 💖💖 | |||||
 | |||||
## License | |||||
[MIT](http://opensource.org/licenses/MIT) | |||||
Copyright (c) 2016-present, iView | |||||
``` |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"pluginsFile": "tests/e2e/plugins/index.js" | |||||
} |
@@ -0,0 +1,69 @@ | |||||
{ | |||||
"name": "iview-admin", | |||||
"version": "2.0.0", | |||||
"author": "Lison<lison.modern@gmail.com>", | |||||
"private": false, | |||||
"scripts": { | |||||
"dev": "vue-cli-service serve --open", | |||||
"build": "vue-cli-service build", | |||||
"lint": "vue-cli-service lint", | |||||
"test:unit": "vue-cli-service test:unit", | |||||
"test:e2e": "vue-cli-service test:e2e" | |||||
}, | |||||
"dependencies": { | |||||
"axios": "^0.18.0", | |||||
"clipboard": "^2.0.0", | |||||
"codemirror": "^5.38.0", | |||||
"countup": "^1.8.2", | |||||
"cropperjs": "^1.2.2", | |||||
"dayjs": "^1.7.7", | |||||
"echarts": "^4.0.4", | |||||
"html2canvas": "^1.0.0-alpha.12", | |||||
"iview": "^3.1.3", | |||||
"iview-area": "^1.5.17", | |||||
"js-cookie": "^2.2.0", | |||||
"simplemde": "^1.11.2", | |||||
"sortablejs": "^1.7.0", | |||||
"tree-table-vue": "^1.1.0", | |||||
"vue": "^2.5.10", | |||||
"vue-i18n": "^7.8.0", | |||||
"vue-router": "^3.0.1", | |||||
"vuedraggable": "^2.16.0", | |||||
"vuex": "^3.0.1", | |||||
"wangeditor": "^3.1.1", | |||||
"xlsx": "^0.13.3" | |||||
}, | |||||
"devDependencies": { | |||||
"@vue/cli-plugin-babel": "^3.0.1", | |||||
"@vue/cli-plugin-eslint": "^3.0.1", | |||||
"@vue/cli-plugin-unit-mocha": "^3.0.1", | |||||
"@vue/cli-service": "^3.0.1", | |||||
"@vue/eslint-config-standard": "^3.0.0-beta.10", | |||||
"@vue/test-utils": "^1.0.0-beta.10", | |||||
"chai": "^4.1.2", | |||||
"eslint-plugin-cypress": "^2.0.1", | |||||
"less": "^2.7.3", | |||||
"less-loader": "^4.0.5", | |||||
"lint-staged": "^6.0.0", | |||||
"mockjs": "^1.0.1-beta3", | |||||
"vue-template-compiler": "^2.5.13" | |||||
}, | |||||
"browserslist": [ | |||||
"> 1%", | |||||
"last 2 versions", | |||||
"not ie <= 8" | |||||
], | |||||
"gitHooks": { | |||||
"pre-commit": "lint-staged" | |||||
}, | |||||
"lint-staged": { | |||||
"*.js": [ | |||||
"vue-cli-service lint", | |||||
"git add" | |||||
], | |||||
"*.vue": [ | |||||
"vue-cli-service lint", | |||||
"git add" | |||||
] | |||||
} | |||||
} |
@@ -1,92 +0,0 @@ | |||||
<template> | |||||
<div class="drag-list-wrapper"> | |||||
<div class="drag-list-con con1"> | |||||
<slot name="left-title"></slot> | |||||
<draggable class="drop-box1" :class="dropConClass.left" :options="options" :value="list1" @input="handleListChange($event, 'left')" @end="handleEnd($event, 'left')"> | |||||
<div class="drag-list-item" v-for="(itemLeft, index) in list1" :key="`drag_li1_${index}`"> | |||||
<slot name="left" :itemLeft="itemLeft">{{ itemLeft }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
<div class="drag-list-con con2"> | |||||
<slot name="right-title"></slot> | |||||
<draggable class="drop-box2" :class="dropConClass.right" :options="options" :value="list2" @input="handleListChange($event, 'right')" @end="handleEnd($event, 'right')"> | |||||
<div class="drag-list-item" v-for="(itemRight, index) in list2" :key="`drag_li2_${index}`"> | |||||
<slot name="right" :itemRight="itemRight">{{ itemRight }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import draggable from 'vuedraggable' | |||||
export default { | |||||
name: 'DragList', | |||||
components: { | |||||
draggable | |||||
}, | |||||
props: { | |||||
list1: { | |||||
type: Array, | |||||
required: true | |||||
}, | |||||
list2: { | |||||
type: Array, | |||||
default: () => [] | |||||
}, | |||||
dropConClass: { | |||||
type: Object, | |||||
default: () => ({}) | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
options: { group: 'drag_list' } | |||||
} | |||||
}, | |||||
methods: { | |||||
handleListChange (value, type) { | |||||
if (type === 'left') this.$emit('update:list1', value) | |||||
else this.$emit('update:list2', value) | |||||
}, | |||||
handleEnd (event, type) { | |||||
const srcClassName = (event.srcElement || event.target).classList[0] | |||||
const targetClassName = event.to.classList[0] | |||||
let src = '' | |||||
let target = '' | |||||
if (srcClassName === targetClassName) { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'left' | |||||
} else { | |||||
src = 'right' | |||||
target = 'right' | |||||
} | |||||
} else { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'right' | |||||
} else { | |||||
src = 'right' | |||||
target = 'left' | |||||
} | |||||
} | |||||
this.$emit('on-change', { | |||||
src: src, | |||||
target: target, | |||||
oldIndex: event.oldIndex, | |||||
newIndex: event.newIndex | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.drag-list-wrapper{ | |||||
height: 100%; | |||||
.drag-list-con{ | |||||
width: 50%; | |||||
float: left; | |||||
} | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import DragList from './drag-list.vue' | |||||
export default DragList |
@@ -1,75 +0,0 @@ | |||||
<template> | |||||
<div class="editor-wrapper"> | |||||
<div :id="editorId"></div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Editor from 'wangeditor' | |||||
import 'wangeditor/release/wangEditor.min.css' | |||||
import { oneOf } from '@/libs/tools' | |||||
export default { | |||||
name: 'Editor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
/** | |||||
* 绑定的值的类型, enum: ['html', 'text'] | |||||
*/ | |||||
valueType: { | |||||
type: String, | |||||
default: 'html', | |||||
validator: (val) => { | |||||
return oneOf(val, ['html', 'text']) | |||||
} | |||||
}, | |||||
/** | |||||
* @description 设置change事件触发时间间隔 | |||||
*/ | |||||
changeInterval: { | |||||
type: Number, | |||||
default: 200 | |||||
}, | |||||
/** | |||||
* @description 是否开启本地存储 | |||||
*/ | |||||
cache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
computed: { | |||||
editorId () { | |||||
return `editor${this._uid}` | |||||
} | |||||
}, | |||||
methods: { | |||||
setHtml (val) { | |||||
this.editor.txt.html(val) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Editor(`#${this.editorId}`) | |||||
this.editor.customConfig.onchange = (html) => { | |||||
let text = this.editor.txt.text() | |||||
if (this.cache) localStorage.editorCache = html | |||||
this.$emit('input', this.valueType === 'html' ? html : text) | |||||
this.$emit('on-change', html, text) | |||||
} | |||||
this.editor.customConfig.onchangeTimeout = this.changeInterval | |||||
// create这个方法一定要在所有配置项之后调用 | |||||
this.editor.create() | |||||
// 如果本地有存储加载本地存储内容 | |||||
let html = this.value || localStorage.editorCache | |||||
if (html) this.editor.txt.html(html) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.editor-wrapper *{ | |||||
z-index: 100 !important; | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import Editor from './editor.vue' | |||||
export default Editor |
@@ -1,2 +0,0 @@ | |||||
import MarkdownEditor from './markdown.vue' | |||||
export default MarkdownEditor |
@@ -1,76 +0,0 @@ | |||||
<template> | |||||
<div class="markdown-wrapper"> | |||||
<textarea ref="editor"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Simplemde from 'simplemde' | |||||
import 'simplemde/dist/simplemde.min.css' | |||||
export default { | |||||
name: 'MarkdownEditor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
options: { | |||||
type: Object, | |||||
default: () => { | |||||
return {} | |||||
} | |||||
}, | |||||
localCache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
editor: null | |||||
} | |||||
}, | |||||
methods: { | |||||
addEvents () { | |||||
this.editor.codemirror.on('change', () => { | |||||
let value = this.editor.value() | |||||
if (this.localCache) localStorage.markdownContent = value | |||||
this.$emit('input', value) | |||||
this.$emit('on-change', value) | |||||
}) | |||||
this.editor.codemirror.on('focus', () => { | |||||
this.$emit('on-focus', this.editor.value()) | |||||
}) | |||||
this.editor.codemirror.on('blur', () => { | |||||
this.$emit('on-blur', this.editor.value()) | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Simplemde(Object.assign(this.options, { | |||||
element: this.$refs.editor | |||||
})) | |||||
/** | |||||
* 事件列表为Codemirror编辑器的事件,更多事件类型,请参考: | |||||
* https://codemirror.net/doc/manual.html#events | |||||
*/ | |||||
this.addEvents() | |||||
let content = localStorage.markdownContent | |||||
if (content) this.editor.value(content) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.markdown-wrapper{ | |||||
.editor-toolbar.fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen ~ .editor-preview-side{ | |||||
z-index: 9999; | |||||
} | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import PasteEditor from './paste-editor.vue' | |||||
export default PasteEditor |
@@ -1,26 +0,0 @@ | |||||
.paste-editor-wrapper{ | |||||
width: 100%; | |||||
height: 100%; | |||||
border: 1px dashed gainsboro; | |||||
textarea.textarea-el{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.CodeMirror{ | |||||
height: 100%; | |||||
padding: 0; | |||||
.CodeMirror-code div .CodeMirror-line > span > span.cm-tab{ | |||||
&::after{ | |||||
content: '→'; | |||||
color: #BFBFBF; | |||||
} | |||||
} | |||||
} | |||||
.first-row{ | |||||
font-weight: 700; | |||||
font-size: 14px; | |||||
} | |||||
.incorrect-row{ | |||||
background: #F5CBD1; | |||||
} | |||||
} |
@@ -1,115 +0,0 @@ | |||||
<template> | |||||
<div class="paste-editor-wrapper"> | |||||
<textarea ref="codemirror" class="textarea-el"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import CodeMirror from 'codemirror' | |||||
import 'codemirror/lib/codemirror.css' | |||||
import { forEach } from '@/libs/tools' | |||||
import createPlaceholder from './plugins/placeholder' | |||||
export default { | |||||
name: 'PasteEditor', | |||||
props: { | |||||
value: Array, | |||||
pasteData: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
placeholder: { | |||||
type: String, | |||||
default: '从网页或其他应用软件复制表格数据,粘贴到这里 。默认第一行是表头,使用回车键添加新行,使用Tab键区分列。' | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
pasteDataArr: [], | |||||
rowArrLength: 0, | |||||
editor: null | |||||
} | |||||
}, | |||||
watch: { | |||||
pasteData (val) { | |||||
if (val === '') { | |||||
this.editor.setValue('') | |||||
} | |||||
} | |||||
}, | |||||
computed: { | |||||
rowNum () { | |||||
return this.pasteDataArr.length | |||||
}, | |||||
colNum () { | |||||
return this.pasteDataArr[0] ? this.pasteDataArr[0].length : 0 | |||||
} | |||||
}, | |||||
methods: { | |||||
handleKeyup (e) { | |||||
this.handleAreaData() | |||||
}, | |||||
/** | |||||
* @description 处理粘贴操作 | |||||
*/ | |||||
handleContentChanged (content) { | |||||
let pasteData = content.trim() | |||||
this.$emit('on-content-change', pasteData) | |||||
let rows = pasteData.split((/[\n\u0085\u2028\u2029]|\r\n?/g)).map(row => { | |||||
return row.split('\t') | |||||
}) | |||||
if (content === '') rows = [] | |||||
this.pasteDataArr = rows | |||||
this.clearLineClass() | |||||
this.checkColNumInEveryRow() | |||||
this.$emit('input', this.pasteDataArr) | |||||
}, | |||||
/** | |||||
* @description 检查除第一行的每一行列数是否与第一行相同 | |||||
*/ | |||||
checkColNumInEveryRow () { | |||||
let i = 0 | |||||
const len = this.rowNum | |||||
if (len === 0) return | |||||
while (++i < len) { | |||||
let item = this.pasteDataArr[i] | |||||
if (item.length !== this.colNum && (!(i === len - 1 && item.length === 1 && item[0] === '') || i !== len - 1)) { | |||||
this.markIncorrectRow(i) | |||||
this.$emit('on-error', i) | |||||
return false | |||||
} | |||||
} | |||||
this.$emit('on-success', this.pasteDataArr) | |||||
return true | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
markIncorrectRow (index) { | |||||
this.editor.addLineClass(index, 'text', 'incorrect-row') | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
clearLineClass () { | |||||
forEach(this.pasteDataArr, (item, index) => { | |||||
this.editor.removeLineClass(index, 'text', 'incorrect-row') | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
createPlaceholder(CodeMirror) | |||||
this.editor = CodeMirror.fromTextArea(this.$refs.codemirror, { | |||||
lineNumbers: true, | |||||
tabSize: 1, | |||||
lineWrapping: true, | |||||
placeholder: this.placeholder | |||||
}) | |||||
this.editor.on('change', (editor) => { | |||||
this.handleContentChanged(editor.getValue()) | |||||
}) | |||||
this.editor.addLineClass(0, 'text', 'first-row') | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './paste-editor.less'; | |||||
</style> |
@@ -1,58 +0,0 @@ | |||||
export default (codemirror) => { | |||||
(function (mod) { | |||||
mod(codemirror) | |||||
})(function (CodeMirror) { | |||||
CodeMirror.defineOption('placeholder', '', function (cm, val, old) { | |||||
var prev = old && old !== CodeMirror.Init | |||||
if (val && !prev) { | |||||
cm.on('blur', onBlur) | |||||
cm.on('change', onChange) | |||||
cm.on('swapDoc', onChange) | |||||
onChange(cm) | |||||
} else if (!val && prev) { | |||||
cm.off('blur', onBlur) | |||||
cm.off('change', onChange) | |||||
cm.off('swapDoc', onChange) | |||||
clearPlaceholder(cm) | |||||
var wrapper = cm.getWrapperElement() | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') | |||||
} | |||||
if (val && !cm.hasFocus()) onBlur(cm) | |||||
}) | |||||
function clearPlaceholder (cm) { | |||||
if (cm.state.placeholder) { | |||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder) | |||||
cm.state.placeholder = null | |||||
} | |||||
} | |||||
function setPlaceholder (cm) { | |||||
clearPlaceholder(cm) | |||||
var elt = cm.state.placeholder = document.createElement('pre') | |||||
elt.style.cssText = 'height: 0; overflow: visible; color: #80848f;' | |||||
elt.style.direction = cm.getOption('direction') | |||||
elt.className = 'CodeMirror-placeholder' | |||||
var placeHolder = cm.getOption('placeholder') | |||||
if (typeof placeHolder === 'string') placeHolder = document.createTextNode(placeHolder) | |||||
elt.appendChild(placeHolder) | |||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild) | |||||
} | |||||
function onBlur (cm) { | |||||
if (isEmpty(cm)) setPlaceholder(cm) | |||||
} | |||||
function onChange (cm) { | |||||
let wrapper = cm.getWrapperElement() | |||||
let empty = isEmpty(cm) | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') + (empty ? ' CodeMirror-empty' : '') | |||||
if (empty) setPlaceholder(cm) | |||||
else clearPlaceholder(cm) | |||||
} | |||||
function isEmpty (cm) { | |||||
return (cm.lineCount() === 1) && (cm.getLine(0) === '') | |||||
} | |||||
}) | |||||
} |
@@ -1,92 +0,0 @@ | |||||
<template> | |||||
<div class="drag-list-wrapper"> | |||||
<div class="drag-list-con con1"> | |||||
<slot name="left-title"></slot> | |||||
<draggable class="drop-box1" :class="dropConClass.left" :options="options" :value="list1" @input="handleListChange($event, 'left')" @end="handleEnd($event, 'left')"> | |||||
<div class="drag-list-item" v-for="(itemLeft, index) in list1" :key="`drag_li1_${index}`"> | |||||
<slot name="left" :itemLeft="itemLeft">{{ itemLeft }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
<div class="drag-list-con con2"> | |||||
<slot name="right-title"></slot> | |||||
<draggable class="drop-box2" :class="dropConClass.right" :options="options" :value="list2" @input="handleListChange($event, 'right')" @end="handleEnd($event, 'right')"> | |||||
<div class="drag-list-item" v-for="(itemRight, index) in list2" :key="`drag_li2_${index}`"> | |||||
<slot name="right" :itemRight="itemRight">{{ itemRight }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import draggable from 'vuedraggable' | |||||
export default { | |||||
name: 'DragList', | |||||
components: { | |||||
draggable | |||||
}, | |||||
props: { | |||||
list1: { | |||||
type: Array, | |||||
required: true | |||||
}, | |||||
list2: { | |||||
type: Array, | |||||
default: () => [] | |||||
}, | |||||
dropConClass: { | |||||
type: Object, | |||||
default: () => ({}) | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
options: { group: 'drag_list' } | |||||
} | |||||
}, | |||||
methods: { | |||||
handleListChange (value, type) { | |||||
if (type === 'left') this.$emit('update:list1', value) | |||||
else this.$emit('update:list2', value) | |||||
}, | |||||
handleEnd (event, type) { | |||||
const srcClassName = (event.srcElement || event.target).classList[0] | |||||
const targetClassName = event.to.classList[0] | |||||
let src = '' | |||||
let target = '' | |||||
if (srcClassName === targetClassName) { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'left' | |||||
} else { | |||||
src = 'right' | |||||
target = 'right' | |||||
} | |||||
} else { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'right' | |||||
} else { | |||||
src = 'right' | |||||
target = 'left' | |||||
} | |||||
} | |||||
this.$emit('on-change', { | |||||
src: src, | |||||
target: target, | |||||
oldIndex: event.oldIndex, | |||||
newIndex: event.newIndex | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.drag-list-wrapper{ | |||||
height: 100%; | |||||
.drag-list-con{ | |||||
width: 50%; | |||||
float: left; | |||||
} | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import DragList from './drag-list.vue' | |||||
export default DragList |
@@ -1,75 +0,0 @@ | |||||
<template> | |||||
<div class="editor-wrapper"> | |||||
<div :id="editorId"></div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Editor from 'wangeditor' | |||||
import 'wangeditor/release/wangEditor.min.css' | |||||
import { oneOf } from '@/libs/tools' | |||||
export default { | |||||
name: 'Editor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
/** | |||||
* 绑定的值的类型, enum: ['html', 'text'] | |||||
*/ | |||||
valueType: { | |||||
type: String, | |||||
default: 'html', | |||||
validator: (val) => { | |||||
return oneOf(val, ['html', 'text']) | |||||
} | |||||
}, | |||||
/** | |||||
* @description 设置change事件触发时间间隔 | |||||
*/ | |||||
changeInterval: { | |||||
type: Number, | |||||
default: 200 | |||||
}, | |||||
/** | |||||
* @description 是否开启本地存储 | |||||
*/ | |||||
cache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
computed: { | |||||
editorId () { | |||||
return `editor${this._uid}` | |||||
} | |||||
}, | |||||
methods: { | |||||
setHtml (val) { | |||||
this.editor.txt.html(val) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Editor(`#${this.editorId}`) | |||||
this.editor.customConfig.onchange = (html) => { | |||||
let text = this.editor.txt.text() | |||||
if (this.cache) localStorage.editorCache = html | |||||
this.$emit('input', this.valueType === 'html' ? html : text) | |||||
this.$emit('on-change', html, text) | |||||
} | |||||
this.editor.customConfig.onchangeTimeout = this.changeInterval | |||||
// create这个方法一定要在所有配置项之后调用 | |||||
this.editor.create() | |||||
// 如果本地有存储加载本地存储内容 | |||||
let html = this.value || localStorage.editorCache | |||||
if (html) this.editor.txt.html(html) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.editor-wrapper *{ | |||||
z-index: 100 !important; | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import Editor from './editor.vue' | |||||
export default Editor |
@@ -1,2 +0,0 @@ | |||||
import MarkdownEditor from './markdown.vue' | |||||
export default MarkdownEditor |
@@ -1,76 +0,0 @@ | |||||
<template> | |||||
<div class="markdown-wrapper"> | |||||
<textarea ref="editor"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Simplemde from 'simplemde' | |||||
import 'simplemde/dist/simplemde.min.css' | |||||
export default { | |||||
name: 'MarkdownEditor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
options: { | |||||
type: Object, | |||||
default: () => { | |||||
return {} | |||||
} | |||||
}, | |||||
localCache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
editor: null | |||||
} | |||||
}, | |||||
methods: { | |||||
addEvents () { | |||||
this.editor.codemirror.on('change', () => { | |||||
let value = this.editor.value() | |||||
if (this.localCache) localStorage.markdownContent = value | |||||
this.$emit('input', value) | |||||
this.$emit('on-change', value) | |||||
}) | |||||
this.editor.codemirror.on('focus', () => { | |||||
this.$emit('on-focus', this.editor.value()) | |||||
}) | |||||
this.editor.codemirror.on('blur', () => { | |||||
this.$emit('on-blur', this.editor.value()) | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Simplemde(Object.assign(this.options, { | |||||
element: this.$refs.editor | |||||
})) | |||||
/** | |||||
* 事件列表为Codemirror编辑器的事件,更多事件类型,请参考: | |||||
* https://codemirror.net/doc/manual.html#events | |||||
*/ | |||||
this.addEvents() | |||||
let content = localStorage.markdownContent | |||||
if (content) this.editor.value(content) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.markdown-wrapper{ | |||||
.editor-toolbar.fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen ~ .editor-preview-side{ | |||||
z-index: 9999; | |||||
} | |||||
} | |||||
</style> |
@@ -1,2 +0,0 @@ | |||||
import PasteEditor from './paste-editor.vue' | |||||
export default PasteEditor |
@@ -1,26 +0,0 @@ | |||||
.paste-editor-wrapper{ | |||||
width: 100%; | |||||
height: 100%; | |||||
border: 1px dashed gainsboro; | |||||
textarea.textarea-el{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.CodeMirror{ | |||||
height: 100%; | |||||
padding: 0; | |||||
.CodeMirror-code div .CodeMirror-line > span > span.cm-tab{ | |||||
&::after{ | |||||
content: '→'; | |||||
color: #BFBFBF; | |||||
} | |||||
} | |||||
} | |||||
.first-row{ | |||||
font-weight: 700; | |||||
font-size: 14px; | |||||
} | |||||
.incorrect-row{ | |||||
background: #F5CBD1; | |||||
} | |||||
} |
@@ -1,115 +0,0 @@ | |||||
<template> | |||||
<div class="paste-editor-wrapper"> | |||||
<textarea ref="codemirror" class="textarea-el"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import CodeMirror from 'codemirror' | |||||
import 'codemirror/lib/codemirror.css' | |||||
import { forEach } from '@/libs/tools' | |||||
import createPlaceholder from './plugins/placeholder' | |||||
export default { | |||||
name: 'PasteEditor', | |||||
props: { | |||||
value: Array, | |||||
pasteData: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
placeholder: { | |||||
type: String, | |||||
default: '从网页或其他应用软件复制表格数据,粘贴到这里 。默认第一行是表头,使用回车键添加新行,使用Tab键区分列。' | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
pasteDataArr: [], | |||||
rowArrLength: 0, | |||||
editor: null | |||||
} | |||||
}, | |||||
watch: { | |||||
pasteData (val) { | |||||
if (val === '') { | |||||
this.editor.setValue('') | |||||
} | |||||
} | |||||
}, | |||||
computed: { | |||||
rowNum () { | |||||
return this.pasteDataArr.length | |||||
}, | |||||
colNum () { | |||||
return this.pasteDataArr[0] ? this.pasteDataArr[0].length : 0 | |||||
} | |||||
}, | |||||
methods: { | |||||
handleKeyup (e) { | |||||
this.handleAreaData() | |||||
}, | |||||
/** | |||||
* @description 处理粘贴操作 | |||||
*/ | |||||
handleContentChanged (content) { | |||||
let pasteData = content.trim() | |||||
this.$emit('on-content-change', pasteData) | |||||
let rows = pasteData.split((/[\n\u0085\u2028\u2029]|\r\n?/g)).map(row => { | |||||
return row.split('\t') | |||||
}) | |||||
if (content === '') rows = [] | |||||
this.pasteDataArr = rows | |||||
this.clearLineClass() | |||||
this.checkColNumInEveryRow() | |||||
this.$emit('input', this.pasteDataArr) | |||||
}, | |||||
/** | |||||
* @description 检查除第一行的每一行列数是否与第一行相同 | |||||
*/ | |||||
checkColNumInEveryRow () { | |||||
let i = 0 | |||||
const len = this.rowNum | |||||
if (len === 0) return | |||||
while (++i < len) { | |||||
let item = this.pasteDataArr[i] | |||||
if (item.length !== this.colNum && (!(i === len - 1 && item.length === 1 && item[0] === '') || i !== len - 1)) { | |||||
this.markIncorrectRow(i) | |||||
this.$emit('on-error', i) | |||||
return false | |||||
} | |||||
} | |||||
this.$emit('on-success', this.pasteDataArr) | |||||
return true | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
markIncorrectRow (index) { | |||||
this.editor.addLineClass(index, 'text', 'incorrect-row') | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
clearLineClass () { | |||||
forEach(this.pasteDataArr, (item, index) => { | |||||
this.editor.removeLineClass(index, 'text', 'incorrect-row') | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
createPlaceholder(CodeMirror) | |||||
this.editor = CodeMirror.fromTextArea(this.$refs.codemirror, { | |||||
lineNumbers: true, | |||||
tabSize: 1, | |||||
lineWrapping: true, | |||||
placeholder: this.placeholder | |||||
}) | |||||
this.editor.on('change', (editor) => { | |||||
this.handleContentChanged(editor.getValue()) | |||||
}) | |||||
this.editor.addLineClass(0, 'text', 'first-row') | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './paste-editor.less'; | |||||
</style> |
@@ -1,58 +0,0 @@ | |||||
export default (codemirror) => { | |||||
(function (mod) { | |||||
mod(codemirror) | |||||
})(function (CodeMirror) { | |||||
CodeMirror.defineOption('placeholder', '', function (cm, val, old) { | |||||
var prev = old && old !== CodeMirror.Init | |||||
if (val && !prev) { | |||||
cm.on('blur', onBlur) | |||||
cm.on('change', onChange) | |||||
cm.on('swapDoc', onChange) | |||||
onChange(cm) | |||||
} else if (!val && prev) { | |||||
cm.off('blur', onBlur) | |||||
cm.off('change', onChange) | |||||
cm.off('swapDoc', onChange) | |||||
clearPlaceholder(cm) | |||||
var wrapper = cm.getWrapperElement() | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') | |||||
} | |||||
if (val && !cm.hasFocus()) onBlur(cm) | |||||
}) | |||||
function clearPlaceholder (cm) { | |||||
if (cm.state.placeholder) { | |||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder) | |||||
cm.state.placeholder = null | |||||
} | |||||
} | |||||
function setPlaceholder (cm) { | |||||
clearPlaceholder(cm) | |||||
var elt = cm.state.placeholder = document.createElement('pre') | |||||
elt.style.cssText = 'height: 0; overflow: visible; color: #80848f;' | |||||
elt.style.direction = cm.getOption('direction') | |||||
elt.className = 'CodeMirror-placeholder' | |||||
var placeHolder = cm.getOption('placeholder') | |||||
if (typeof placeHolder === 'string') placeHolder = document.createTextNode(placeHolder) | |||||
elt.appendChild(placeHolder) | |||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild) | |||||
} | |||||
function onBlur (cm) { | |||||
if (isEmpty(cm)) setPlaceholder(cm) | |||||
} | |||||
function onChange (cm) { | |||||
let wrapper = cm.getWrapperElement() | |||||
let empty = isEmpty(cm) | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') + (empty ? ' CodeMirror-empty' : '') | |||||
if (empty) setPlaceholder(cm) | |||||
else clearPlaceholder(cm) | |||||
} | |||||
function isEmpty (cm) { | |||||
return (cm.lineCount() === 1) && (cm.getLine(0) === '') | |||||
} | |||||
}) | |||||
} |
@@ -1,12 +0,0 @@ | |||||
{ | |||||
"plugins": [ | |||||
"cypress" | |||||
], | |||||
"env": { | |||||
"mocha": true, | |||||
"cypress/globals": true | |||||
}, | |||||
"rules": { | |||||
"strict": "off" | |||||
} | |||||
} |
@@ -1,9 +0,0 @@ | |||||
// https://docs.cypress.io/guides/guides/plugins-guide.html | |||||
module.exports = (on, config) => Object.assign({}, config, { | |||||
fixturesFolder: 'tests/e2e/fixtures', | |||||
integrationFolder: 'tests/e2e/specs', | |||||
screenshotsFolder: 'tests/e2e/screenshots', | |||||
videosFolder: 'tests/e2e/videos', | |||||
supportFile: 'tests/e2e/support/index.js' | |||||
}) |
@@ -1,8 +0,0 @@ | |||||
// https://docs.cypress.io/api/introduction/api.html | |||||
describe('My First Test', () => { | |||||
it('Visits the app root url', () => { | |||||
cy.visit('/') | |||||
cy.contains('h1', 'Welcome to Your Vue.js App') | |||||
}) | |||||
}) |
@@ -1,25 +0,0 @@ | |||||
// *********************************************** | |||||
// This example commands.js shows you how to | |||||
// create various custom commands and overwrite | |||||
// existing commands. | |||||
// | |||||
// For more comprehensive examples of custom | |||||
// commands please read more here: | |||||
// https://on.cypress.io/custom-commands | |||||
// *********************************************** | |||||
// | |||||
// | |||||
// -- This is a parent command -- | |||||
// Cypress.Commands.add("login", (email, password) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a child command -- | |||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a dual command -- | |||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This is will overwrite an existing command -- | |||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) |
@@ -1,20 +0,0 @@ | |||||
// *********************************************************** | |||||
// This example support/index.js is processed and | |||||
// loaded automatically before your test files. | |||||
// | |||||
// This is a great place to put global configuration and | |||||
// behavior that modifies Cypress. | |||||
// | |||||
// You can change the location of this file or turn off | |||||
// automatically serving support files with the | |||||
// 'supportFile' configuration option. | |||||
// | |||||
// You can read more here: | |||||
// https://on.cypress.io/configuration | |||||
// *********************************************************** | |||||
// Import commands.js using ES2015 syntax: | |||||
import './commands' | |||||
// Alternatively you can use CommonJS syntax: | |||||
// require('./commands') |
@@ -1,8 +0,0 @@ | |||||
module.exports = { | |||||
env: { | |||||
mocha: true | |||||
}, | |||||
rules: { | |||||
'import/no-extraneous-dependencies': 'off' | |||||
} | |||||
} |
@@ -1,13 +0,0 @@ | |||||
import { expect } from 'chai' | |||||
import { shallow } from '@vue/test-utils' | |||||
import HelloWorld from '@/components/HelloWorld.vue' | |||||
describe('HelloWorld.vue', () => { | |||||
it('renders props.msg when passed', () => { | |||||
const msg = 'new message' | |||||
const wrapper = shallow(HelloWorld, { | |||||
propsData: { msg } | |||||
}) | |||||
expect(wrapper.text()).to.include(msg) | |||||
}) | |||||
}) |