mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-21 17:21:09 +01:00
642 lines
19 KiB
Vue
642 lines
19 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
|
|
class="tree"
|
|
:data="computedFileTree"
|
|
ref="tree"
|
|
node-key="path"
|
|
:highlight-current="true"
|
|
:default-expanded-keys="expandedPaths"
|
|
@node-contextmenu="onFileRightClick"
|
|
@node-click="onFileClick"
|
|
@node-expand="onDirClick"
|
|
@node-collapse="onDirClick"
|
|
>
|
|
<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)">
|
|
<ul class="action-item-list">
|
|
<li class="action-item" @click="fileDialogVisible = true">
|
|
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)"/>
|
|
<span class="action-item-text">{{$t('Create File')}}</span>
|
|
</li>
|
|
<li class="action-item" @click="dirDialogVisible = true">
|
|
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
|
|
<span class="action-item-text">{{$t('Create Directory')}}</span>
|
|
</li>
|
|
</ul>
|
|
<ul class="action-item-list">
|
|
<li class="action-item" @click="onClickRemoveNav(data)">
|
|
<font-awesome-icon :icon="['fa', 'trash']" color="rgba(3,47,98,.5)"/>
|
|
<span class="action-item-text">{{$t('Remove')}}</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
|
|
class="add-btn-wrapper"
|
|
>
|
|
<el-popover trigger="click" placement="right"
|
|
popper-class="create-item-popover" :visible-arrow="false">
|
|
<ul class="action-item-list">
|
|
<li class="action-item" @click="fileDialogVisible = true">
|
|
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)"/>
|
|
<span class="action-item-text">{{$t('Create File')}}</span>
|
|
</li>
|
|
<li class="action-item" @click="dirDialogVisible = true">
|
|
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
|
|
<span class="action-item-text">{{$t('Create Directory')}}</span>
|
|
</li>
|
|
</ul>
|
|
<el-button
|
|
class="add-btn"
|
|
size="small"
|
|
type="primary"
|
|
icon="el-icon-plus"
|
|
slot="reference"
|
|
@click="onEmptyClick"
|
|
>
|
|
{{$t('Add')}}
|
|
</el-button>
|
|
</el-popover>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div v-if="!showFile" class="file-list">
|
|
{{$t('Please select a file or click the add button 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',
|
|
'.git'
|
|
],
|
|
activeFileNode: {},
|
|
dirDialogVisible: false,
|
|
fileDialogVisible: false,
|
|
nodeExpandedDict: {},
|
|
isShowDeleteNav: 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
|
|
},
|
|
expandedPaths () {
|
|
return Object.keys(this.nodeExpandedDict)
|
|
.map(path => {
|
|
return {
|
|
path,
|
|
expanded: this.nodeExpandedDict[path]
|
|
}
|
|
})
|
|
.filter(d => d.expanded)
|
|
.map(d => d.path)
|
|
}
|
|
},
|
|
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)
|
|
},
|
|
onDirClick (data, node) {
|
|
const vm = this
|
|
setTimeout(() => {
|
|
vm.$set(vm.nodeExpandedDict, data.path, node.expanded)
|
|
}, 0)
|
|
},
|
|
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
|
|
this.$st.sendEv('爬虫详情', '文件', '右键点击导航栏')
|
|
},
|
|
onEmptyClick () {
|
|
const data = { path: '' }
|
|
this.isShowCreatePopoverDict = {}
|
|
this.$set(this.isShowCreatePopoverDict, data.path, true)
|
|
this.activeFileNode = data
|
|
this.$st.sendEv('爬虫详情', '文件', '空白点击添加')
|
|
},
|
|
onHideCreate (data) {
|
|
this.$set(this.isShowCreatePopoverDict, data.path, false)
|
|
this.name = ''
|
|
},
|
|
onClickRemoveNav (data) {
|
|
this.$confirm(this.$t('Are you sure to delete this file/directory?'), this.$t('Notification'), {
|
|
confirmButtonText: this.$t('Confirm'),
|
|
cancelButtonText: this.$t('Cancel'),
|
|
confirmButtonClass: 'danger',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
this.onFileDeleteNav(data.path)
|
|
})
|
|
},
|
|
async onFileDeleteNav (path) {
|
|
await this.$store.dispatch('file/deleteFile', { path })
|
|
await this.$store.dispatch('spider/getFileTree')
|
|
this.$message.success(this.$t('Deleted successfully'))
|
|
this.isShowDelete = false
|
|
this.showFile = false
|
|
this.$st.sendEv('爬虫详情', '文件', '删除')
|
|
},
|
|
clickSpider (filepath) {
|
|
const node = this.$refs['tree'].getNode(filepath)
|
|
const data = node.data
|
|
this.onFileClick(data)
|
|
node.parent.expanded = true
|
|
node.parent.parent.expanded = true
|
|
},
|
|
clickPipeline () {
|
|
const filename = 'pipelines.py'
|
|
for (let i = 0; i < this.computedFileTree.length; i++) {
|
|
const dataLv1 = this.computedFileTree[i]
|
|
const nodeLv1 = this.$refs['tree'].getNode(dataLv1.path)
|
|
if (dataLv1.is_dir) {
|
|
for (let j = 0; j < dataLv1.children.length; j++) {
|
|
const dataLv2 = dataLv1.children[j]
|
|
if (dataLv2.path.match(filename)) {
|
|
this.onFileClick(dataLv2)
|
|
nodeLv1.expanded = true
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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;
|
|
}
|
|
|
|
.action-item-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0
|
|
}
|
|
|
|
.action-item-title {
|
|
padding-top: 10px;
|
|
padding-left: 15px;
|
|
padding-bottom: 5px;
|
|
}
|
|
|
|
.action-item-list .action-item {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 35px;
|
|
padding: 0 0 0 10px;
|
|
margin: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.action-item-list .action-item:last-child {
|
|
border-bottom: 1px solid #eaecef;
|
|
}
|
|
|
|
.action-item-list .action-item:hover {
|
|
background: #F5F7FA;
|
|
}
|
|
|
|
.action-item-list .action-item svg {
|
|
width: 20px;
|
|
}
|
|
|
|
.action-item-list .action-item .action-item-text {
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.add-btn-wrapper {
|
|
width: 220px;
|
|
border-top: 1px solid #eaecef;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.add-btn-wrapper .add-btn {
|
|
width: 80px;
|
|
margin-left: calc(120px - 40px - 10px);
|
|
margin-top: 20px;
|
|
}
|
|
</style>
|