Files
crawlab/frontend/src/components/File/FileList.vue
2020-01-15 20:48:49 +08:00

519 lines
15 KiB
Vue

<template>
<div class="file-list-container">
<el-dialog
:title="$t('New Directory')"
:visible.sync="dirDialogVisible"
width="30%">
<el-form>
<el-form-item :label="$t('Enter new directory name')">
<el-input v-model="name" :placeholder="$t('New directory name')"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dirDialogVisible = false">{{$t('Cancel')}}</el-button>
<el-button type="primary" @click="onAddDir">{{$t('Confirm')}}</el-button>
</span>
</el-dialog>
<el-dialog
:title="$t('New File')"
:visible.sync="fileDialogVisible"
width="30%">
<el-form>
<el-form-item :label="$t('Enter new file name')">
<el-input v-model="name" :placeholder="$t('New file name')"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="fileDialogVisible = false">{{$t('Cancel')}}</el-button>
<el-button type="primary" @click="onAddFile">{{$t('Confirm')}}</el-button>
</span>
</el-dialog>
<div class="file-tree-wrapper">
<el-tree
:data="computedFileTree"
node-key="path"
:highlight-current="true"
@node-contextmenu="onFileRightClick"
@node-click="onFileClick"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<el-popover v-model="isShowCreatePopoverDict[data.path]" trigger="manual" placement="right"
popper-class="create-item-popover" :visible-arrow="false" @hide="onHideCreate(data)">
<div class="create-item-title">
<span class="item-icon">
<font-awesome-icon icon="plus" color="rgba(3,47,98,.5)"/>
</span>
<span class="create-item-text">{{$t('Create')}}</span>
</div>
<ul class="create-item-list">
<li class="create-item" @click="dirDialogVisible = true">
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
<span class="create-item-text">{{$t('Directory')}}</span>
</li>
<li class="create-item" @click="fileDialogVisible = true">
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)"/>
<span class="create-item-text">{{$t('File')}}</span>
</li>
</ul>
<template slot="reference">
<div>
<span class="item-icon">
<font-awesome-icon v-if="data.is_dir" :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.py$/)" :icon="['fab','python']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.js$/)" :icon="['fab','node-js']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.(java|jar|class)$/)" :icon="['fab','java']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.go$/)" :icon="['fab','go']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.zip$/)" :icon="['fa','file-archive']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else icon="file-alt" color="rgba(3,47,98,.5)"/>
</span>
<span class="item-name" :class="isActiveFile(data) ? 'active' : ''">
{{data.name}}
</span>
</div>
</template>
</el-popover>
</span>
</el-tree>
</div>
<div class="main-content">
<div v-if="!showFile" class="file-list">
{{$t('Please select a file on the left.')}}
</div>
<template v-else>
<div class="top-part">
<!--back-->
<div class="action-container">
<el-popover v-model="isShowDelete" trigger="click">
<el-button size="small" type="default" @click="() => this.isShowDelete = false">
{{$t('Cancel')}}
</el-button>
<el-button size="small" type="danger" @click="onFileDelete">
{{$t('Confirm')}}
</el-button>
<template slot="reference">
<el-button type="danger" size="small" style="margin-right: 10px;">
<font-awesome-icon :icon="['fa', 'trash']"/>
{{$t('Remove')}}
</el-button>
</template>
</el-popover>
<el-popover v-model="isShowRename" trigger="click">
<el-input v-model="name" :placeholder="$t('Name')" style="margin-bottom: 10px"/>
<div style="text-align: right">
<el-button size="small" type="warning" @click="onRenameFile">
{{$t('Confirm')}}
</el-button>
</div>
<template slot="reference">
<div>
<el-button type="warning" size="small" style="margin-right: 10px;" @click="onOpenRename">
<font-awesome-icon :icon="['fa', 'redo']"/>
{{$t('Rename')}}
</el-button>
</div>
</template>
</el-popover>
<el-button type="success" size="small" style="margin-right: 10px;" @click="onFileSave">
<font-awesome-icon :icon="['fa', 'save']"/>
{{$t('Save')}}
</el-button>
</div>
<!--./back-->
<!--file path-->
<div class="file-path-container">
<div class="file-path">{{currentPath}}</div>
</div>
<!--./file path-->
</div>
<file-detail/>
</template>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import FileDetail from './FileDetail'
export default {
name: 'FileList',
components: { FileDetail },
data () {
return {
code: 'var hello = \'world\'',
isEdit: false,
showFile: false,
name: '',
isShowAdd: false,
isShowDelete: false,
isShowRename: false,
isShowCreatePopoverDict: {},
currentFilePath: '.',
ignoreFileRegexList: [
'__pycache__',
'md5.txt',
'.pyc'
],
activeFileNode: {},
dirDialogVisible: false,
fileDialogVisible: false
}
},
computed: {
...mapState('spider', [
'fileTree'
]),
...mapState('file', [
'fileList'
]),
currentPath: {
set (value) {
this.$store.commit('file/SET_CURRENT_PATH', value)
},
get () {
return this.$store.state.file.currentPath
}
},
computedFileTree () {
if (!this.fileTree || !this.fileTree.children) return []
let nodes = this.sortFiles(this.fileTree.children)
nodes = this.filterFiles(nodes)
return nodes
}
},
methods: {
onEdit () {
this.isEdit = true
},
onItemClick (item) {
if (item.is_dir) {
// 目录
this.$store.dispatch('file/getFileList', { path: item.path })
} else {
// 文件
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', item.path)
this.$store.dispatch('file/getFileContent', { path: item.path })
}
this.$st.sendEv('爬虫详情', '文件', '点击')
},
async onFileSave () {
await this.$store.dispatch('file/saveFileContent', { path: this.currentPath })
this.$message.success(this.$t('Saved file successfully'))
this.$st.sendEv('爬虫详情', '文件', '保存')
},
async onAddFile () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
arr[arr.length - 1] = this.name
}
const path = arr.join('/')
await this.$store.dispatch('file/addFile', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.fileDialogVisible = false
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', path)
await this.$store.dispatch('file/getFileContent', { path })
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onAddDir () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
arr[arr.length - 1] = this.name
}
const path = arr.join('/')
await this.$store.dispatch('file/addDir', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.dirDialogVisible = false
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onFileDelete () {
await this.$store.dispatch('file/deleteFile', { path: this.currentFilePath })
await this.$store.dispatch('spider/getFileTree')
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.showFile = false
this.$st.sendEv('爬虫详情', '文件', '删除')
},
onOpenRename () {
const arr = this.currentFilePath.split('/')
this.name = arr[arr.length - 1]
},
async onRenameFile () {
await this.$store.dispatch('file/renameFile', { path: this.currentFilePath, newPath: this.name })
await this.$store.dispatch('spider/getFileTree')
const arr = this.currentFilePath.split('/')
arr[arr.length - 1] = this.name
this.currentFilePath = arr.join('/')
this.$store.commit('file/SET_CURRENT_PATH', this.currentFilePath)
this.$message.success(this.$t('Renamed successfully'))
this.isShowRename = false
this.$st.sendEv('爬虫详情', '文件', '重命名')
},
async getFileTree () {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
await this.$store.dispatch('spider/getFileTree', { id })
},
async onFileClick (data) {
if (data.is_dir) {
return
}
this.currentFilePath = data.path
this.onItemClick(data)
},
sortFiles (nodes) {
nodes.forEach(node => {
if (node.is_dir) {
if (!node.children) node.children = []
node.children = this.sortFiles(node.children)
}
})
return nodes.sort((a, b) => {
if ((a.is_dir && b.is_dir) || (!a.is_dir && !b.is_dir)) {
return a.name > b.name ? 1 : -1
} else {
return a.is_dir ? -1 : 1
}
})
},
filterFiles (nodes) {
return nodes.filter(node => {
if (node.is_dir) {
node.children = this.filterFiles(node.children)
}
for (let i = 0; i < this.ignoreFileRegexList.length; i++) {
const regex = this.ignoreFileRegexList[i]
if (node.name.match(regex)) {
return false
}
}
return true
})
},
isActiveFile (node) {
return node.path === this.currentFilePath
},
onFileRightClick (ev, data) {
this.isShowCreatePopoverDict = {}
this.$set(this.isShowCreatePopoverDict, data.path, true)
this.activeFileNode = data
},
onHideCreate (data) {
this.$set(this.isShowCreatePopoverDict, data.path, false)
this.name = ''
}
},
async created () {
await this.getFileTree()
},
mounted () {
this.listener = document.querySelector('body').addEventListener('click', ev => {
this.isShowCreatePopoverDict = {}
})
},
destroyed () {
document.querySelector('body').removeEventListener('click', this.listener)
}
}
</script>
<style scoped lang="scss">
.file-list-container {
margin-left: -15px;
height: 100%;
min-height: 100%;
.top-part {
display: flex;
height: 33px;
margin-bottom: 10px;
.file-path-container {
width: 100%;
padding: 5px;
margin: 0 10px 0 0;
border-radius: 5px;
border: 1px solid #eaecef;
display: flex;
justify-content: space-between;
color: rgba(3, 47, 98, 1);
.left {
width: 100%;
display: flex;
.el-icon-back {
margin-right: 10px;
cursor: pointer;
}
.el-input {
/*height: 22px;*/
width: 100%;
line-height: 10px;
}
}
.el-icon-edit {
cursor: pointer;
}
}
.action-container {
text-align: right;
display: flex;
/*padding: 1px 5px;*/
/*height: 24px;*/
.el-button {
margin: 0;
}
}
}
.file-list {
padding: 10px;
list-style: none;
height: 100%;
overflow-y: auto;
min-height: 100%;
/*border-radius: 5px;*/
/*border: 1px solid #eaecef;*/
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
<style scoped>
.file-path >>> .el-input__inner {
font-size: 14px;
line-height: 18px;
height: 18px;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 2px solid #409EFF;
border-radius: 0;
}
.CodeMirror-line {
padding-right: 20px;
}
.item {
border-bottom: 1px solid #eaecef;
}
.item-icon {
display: inline-block;
width: 18px;
}
.item-name {
font-size: 14px;
color: rgba(3, 47, 98, 1);
}
.add-type-list {
text-align: right;
margin-top: 10px;
}
.add-type {
cursor: pointer;
font-weight: bolder;
}
.file-tree-wrapper {
float: left;
width: 240px;
height: calc(100vh - 200px);
overflow: auto;
}
.file-tree-wrapper >>> .el-tree-node__content {
height: 30px;
}
.file-tree-wrapper >>> .el-tree-node__content .item-name.active {
font-weight: bolder;
}
.main-content {
float: left;
width: calc(100% - 240px);
height: calc(100vh - 200px);
border-left: 1px solid #eaecef;
padding-left: 20px;
}
</style>
<style>
.create-item-popover {
padding: 0;
margin: 0;
}
.create-item-list {
list-style: none;
padding: 0;
margin: 0
}
.create-item-title {
padding-top: 10px;
padding-left: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eaecef;
}
.create-item-list .create-item {
display: flex;
align-items: center;
height: 35px;
padding: 0 0 0 15px;
margin: 0;
cursor: pointer;
}
.create-item-list .create-item:hover {
background: #F5F7FA;
}
.create-item-list .create-item .create-item-text {
margin-left: 5px;
}
</style>