Demo:恢复误删的文件

This commit is contained in:
街角小林
2024-07-05 10:57:06 +08:00
parent f54f92c303
commit ac72c0c1dc

View File

@@ -0,0 +1,723 @@
<template>
<div class="toolbarContainer" :class="{ isDark: isDark }">
<div class="toolbar" ref="toolbarRef">
<!-- 节点操作 -->
<div class="toolbarBlock">
<ToolbarNodeBtnList :list="horizontalList"></ToolbarNodeBtnList>
<!-- 更多 -->
<el-popover
v-model="popoverShow"
placement="bottom-end"
width="120"
trigger="hover"
v-if="showMoreBtn"
:style="{ marginLeft: horizontalList.length > 0 ? '20px' : 0 }"
>
<ToolbarNodeBtnList
dir="v"
:list="verticalList"
@click.native="popoverShow = false"
></ToolbarNodeBtnList>
<div slot="reference" class="toolbarBtn">
<span class="icon iconfont icongongshi"></span>
<span class="text">{{ $t('toolbar.more') }}</span>
</div>
</el-popover>
</div>
<!-- 导出 -->
<div class="toolbarBlock">
<div class="toolbarBtn" @click="openDirectory" v-if="!isMobile">
<span class="icon iconfont icondakai"></span>
<span class="text">{{ $t('toolbar.directory') }}</span>
</div>
<el-tooltip
effect="dark"
:content="$t('toolbar.newFileTip')"
placement="bottom"
v-if="!isMobile"
>
<div class="toolbarBtn" @click="createNewLocalFile">
<span class="icon iconfont iconxinjian"></span>
<span class="text">{{ $t('toolbar.newFile') }}</span>
</div>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('toolbar.openFileTip')"
placement="bottom"
v-if="!isMobile"
>
<div class="toolbarBtn" @click="openLocalFile">
<span class="icon iconfont iconwenjian1"></span>
<span class="text">{{ $t('toolbar.openFile') }}</span>
</div>
</el-tooltip>
<div class="toolbarBtn" @click="saveLocalFile" v-if="!isMobile">
<span class="icon iconfont iconlingcunwei"></span>
<span class="text">{{ $t('toolbar.saveAs') }}</span>
</div>
<div class="toolbarBtn" @click="$bus.$emit('showImport')">
<span class="icon iconfont icondaoru"></span>
<span class="text">{{ $t('toolbar.import') }}</span>
</div>
<div
class="toolbarBtn"
@click="$bus.$emit('showExport')"
style="margin-right: 0;"
>
<span class="icon iconfont iconexport"></span>
<span class="text">{{ $t('toolbar.export') }}</span>
</div>
<!-- 本地文件树 -->
<div
class="fileTreeBox"
v-if="fileTreeVisible"
:class="{ expand: fileTreeExpand }"
>
<div class="fileTreeToolbar">
<div class="fileTreeName">
{{ rootDirName ? '/' + rootDirName : '' }}
</div>
<div class="fileTreeActionList">
<div
class="btn"
:class="[
fileTreeExpand ? 'el-icon-arrow-up' : 'el-icon-arrow-down'
]"
@click="fileTreeExpand = !fileTreeExpand"
></div>
<div
class="btn el-icon-close"
@click="fileTreeVisible = false"
></div>
</div>
</div>
<div class="fileTreeWrap">
<el-tree
:props="fileTreeProps"
:load="loadFileTreeNode"
:expand-on-click-node="false"
node-key="id"
lazy
>
<span class="customTreeNode" slot-scope="{ node, data }">
<div class="treeNodeInfo">
<span
class="treeNodeIcon iconfont"
:class="[
data.type === 'file' ? 'iconwenjian' : 'icondakai'
]"
></span>
<span class="treeNodeName">{{ node.label }}</span>
</div>
<div class="treeNodeBtnList" v-if="data.type === 'file'">
<el-button
type="text"
size="mini"
v-if="data.enableEdit"
@click="editLocalFile(data)"
>编辑</el-button
>
<el-button
type="text"
size="mini"
v-else
@click="importLocalFile(data)"
>导入</el-button
>
</div>
</span>
</el-tree>
</div>
</div>
</div>
</div>
<NodeImage></NodeImage>
<NodeHyperlink></NodeHyperlink>
<NodeIcon></NodeIcon>
<NodeNote></NodeNote>
<NodeTag></NodeTag>
<Export></Export>
<Import ref="ImportRef"></Import>
</div>
</template>
<script>
import NodeImage from './NodeImage'
import NodeHyperlink from './NodeHyperlink'
import NodeIcon from './NodeIcon'
import NodeNote from './NodeNote'
import NodeTag from './NodeTag'
import Export from './Export'
import Import from './Import'
import { mapState } from 'vuex'
import { Notification } from 'element-ui'
import exampleData from 'simple-mind-map/example/exampleData'
import { getData } from '../../../api'
import ToolbarNodeBtnList from './ToolbarNodeBtnList.vue'
import { throttle, isMobile } from 'simple-mind-map/src/utils/index'
/**
* @Author: 王林
* @Date: 2021-06-24 22:54:58
* @Desc: 工具栏
*/
let fileHandle = null
export default {
name: 'Toolbar',
components: {
NodeImage,
NodeHyperlink,
NodeIcon,
NodeNote,
NodeTag,
Export,
Import,
ToolbarNodeBtnList
},
data() {
return {
isMobile: isMobile(),
list: [
'back',
'forward',
'painter',
'siblingNode',
'childNode',
'deleteNode',
'image',
'icon',
'link',
'note',
'tag',
'summary',
'associativeLine',
'formula',
// 'attachment',
'outerFrame',
'annotation'
],
horizontalList: [],
verticalList: [],
showMoreBtn: true,
popoverShow: false,
fileTreeProps: {
label: 'name',
children: 'children',
isLeaf: 'leaf'
},
fileTreeVisible: false,
rootDirName: '',
fileTreeExpand: true
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark,
isHandleLocalFile: state => state.isHandleLocalFile
})
},
watch: {
isHandleLocalFile(val) {
if (!val) {
Notification.closeAll()
}
}
},
created() {
this.$bus.$on('write_local_file', this.onWriteLocalFile)
},
mounted() {
this.computeToolbarShow()
this.computeToolbarShowThrottle = throttle(this.computeToolbarShow, 300)
window.addEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$on('lang_change', this.computeToolbarShowThrottle)
},
beforeDestroy() {
this.$bus.$off('write_local_file', this.onWriteLocalFile)
window.removeEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$off('lang_change', this.computeToolbarShowThrottle)
},
methods: {
// 计算工具按钮如何显示
computeToolbarShow() {
const windowWidth = window.innerWidth - 40
const all = [...this.list]
let index = 1
const loopCheck = () => {
if (index > all.length) return done()
this.horizontalList = all.slice(0, index)
this.$nextTick(() => {
const width = this.$refs.toolbarRef.getBoundingClientRect().width
if (width < windowWidth) {
index++
loopCheck()
} else if (index > 0 && width > windowWidth) {
index--
this.horizontalList = all.slice(0, index)
done()
}
})
}
const done = () => {
this.verticalList = all.slice(index)
this.showMoreBtn = this.verticalList.length > 0
}
loopCheck()
},
// 监听本地文件读写
onWriteLocalFile(content) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.writeLocalFile(content)
}, 1000)
},
// 加载本地文件树
async loadFileTreeNode(node, resolve) {
try {
let dirHandle
if (node.level === 0) {
dirHandle = await window.showDirectoryPicker()
this.rootDirName = dirHandle.name
} else {
dirHandle = node.data.handle
}
const dirList = []
const fileList = []
for await (const [key, value] of dirHandle.entries()) {
const isFile = value.kind === 'file'
if (isFile && !/\.(smm|xmind|md|json)$/.test(value.name)) {
continue
}
const enableEdit = isFile && /\.smm$/.test(value.name)
const data = {
id: key,
name: value.name,
type: value.kind,
handle: value,
leaf: isFile,
enableEdit
}
if (isFile) {
fileList.push(data)
} else {
dirList.push(data)
}
}
resolve([...dirList, ...fileList])
} catch (error) {
console.log(error)
this.fileTreeVisible = false
resolve([])
if (error.toString().includes('aborted')) {
return
}
this.$message.warning(this.$t('toolbar.notSupportTip'))
}
},
// 扫描本地文件夹
openDirectory() {
this.fileTreeVisible = false
this.fileTreeExpand = true
this.rootDirName = ''
this.$nextTick(() => {
this.fileTreeVisible = true
})
},
// 编辑指定文件
editLocalFile(data) {
if (data.handle) {
fileHandle = data.handle
this.readFile()
}
},
// 导入指定文件
async importLocalFile(data) {
try {
const file = await data.handle.getFile()
this.$refs.ImportRef.onChange({
raw: file,
name: file.name
})
this.$refs.ImportRef.confirm()
} catch (error) {
console.log(error)
}
},
// 打开本地文件
async openLocalFile() {
try {
let [_fileHandle] = await window.showOpenFilePicker({
types: [
{
description: '',
accept: {
'application/json': ['.smm']
}
}
],
excludeAcceptAllOption: true,
multiple: false
})
if (!_fileHandle) {
return
}
fileHandle = _fileHandle
if (fileHandle.kind === 'directory') {
this.$message.warning(this.$t('toolbar.selectFileTip'))
return
}
this.readFile()
} catch (error) {
console.log(error)
if (error.toString().includes('aborted')) {
return
}
this.$message.warning(this.$t('toolbar.notSupportTip'))
}
},
// 读取本地文件
async readFile() {
let file = await fileHandle.getFile()
let fileReader = new FileReader()
fileReader.onload = async () => {
this.$store.commit('setIsHandleLocalFile', true)
this.setData(fileReader.result)
Notification.closeAll()
Notification({
title: this.$t('toolbar.tip'),
message: `${this.$t('toolbar.editingLocalFileTipFront')}${
file.name
}${this.$t('toolbar.editingLocalFileTipEnd')}`,
duration: 0,
showClose: true
})
}
fileReader.readAsText(file)
},
// 渲染读取的数据
setData(str) {
try {
let data = JSON.parse(str)
if (typeof data !== 'object') {
throw new Error(this.$t('toolbar.fileContentError'))
}
if (data.root) {
this.isFullDataFile = true
} else {
this.isFullDataFile = false
data = {
...exampleData,
root: data
}
}
this.$bus.$emit('setData', data)
} catch (error) {
console.log(error)
this.$message.error(this.$t('toolbar.fileOpenFailed'))
}
},
// 写入本地文件
async writeLocalFile(content) {
if (!fileHandle || !this.isHandleLocalFile) {
return
}
if (!this.isFullDataFile) {
content = content.root
}
let string = JSON.stringify(content)
const writable = await fileHandle.createWritable()
await writable.write(string)
await writable.close()
},
// 创建本地文件
async createNewLocalFile() {
await this.createLocalFile(exampleData)
},
// 另存为
async saveLocalFile() {
let data = getData()
await this.createLocalFile(data)
},
// 创建本地文件
async createLocalFile(content) {
try {
let _fileHandle = await window.showSaveFilePicker({
types: [
{
description: '',
accept: { 'application/json': ['.smm'] }
}
],
suggestedName: this.$t('toolbar.defaultFileName')
})
if (!_fileHandle) {
return
}
const loading = this.$loading({
lock: true,
text: this.$t('toolbar.creatingTip'),
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
fileHandle = _fileHandle
this.$store.commit('setIsHandleLocalFile', true)
this.isFullDataFile = true
await this.writeLocalFile(content)
await this.readFile()
loading.close()
} catch (error) {
console.log(error)
if (error.toString().includes('aborted')) {
return
}
this.$message.warning(this.$t('toolbar.notSupportTip'))
}
}
}
}
</script>
<style lang="less" scoped>
.toolbarContainer {
&.isDark {
.toolbar {
color: hsla(0, 0%, 100%, 0.9);
.toolbarBlock {
background-color: #262a2e;
.fileTreeBox {
background-color: #262a2e;
/deep/ .el-tree {
background-color: #262a2e;
&.el-tree--highlight-current {
.el-tree-node.is-current > .el-tree-node__content {
background-color: hsla(0, 0%, 100%, 0.05) !important;
}
}
.el-tree-node:focus > .el-tree-node__content {
background-color: hsla(0, 0%, 100%, 0.05) !important;
}
.el-tree-node__content:hover,
.el-upload-list__item:hover {
background-color: hsla(0, 0%, 100%, 0.02) !important;
}
}
.fileTreeWrap {
.customTreeNode {
.treeNodeInfo {
color: #fff;
}
.treeNodeBtnList {
.el-button {
padding: 7px 5px;
}
}
}
}
}
}
.toolbarBtn {
.icon {
background: transparent;
border-color: transparent;
}
&:hover {
&:not(.disabled) {
.icon {
background: hsla(0, 0%, 100%, 0.05);
}
}
}
&.disabled {
color: #54595f;
}
}
}
}
.toolbar {
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 20px;
width: max-content;
display: flex;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(26, 26, 26, 0.8);
z-index: 2;
.toolbarBlock {
display: flex;
background-color: #fff;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
margin-right: 20px;
flex-shrink: 0;
position: relative;
&:last-of-type {
margin-right: 0;
}
.fileTreeBox {
position: absolute;
left: 0;
top: 68px;
width: 100%;
height: 30px;
background-color: #fff;
padding: 12px 5px;
padding-top: 0;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 5px;
min-width: 200px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
&.expand {
height: 300px;
.fileTreeWrap {
visibility: visible;
}
}
.fileTreeToolbar {
width: 100%;
height: 30px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e9e9e9;
margin-bottom: 12px;
padding-left: 12px;
.fileTreeName {
}
.fileTreeActionList {
.btn {
font-size: 18px;
margin-left: 12px;
cursor: pointer;
}
}
}
.fileTreeWrap {
width: 100%;
height: 100%;
overflow: auto;
visibility: hidden;
.customTreeNode {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
padding-right: 5px;
.treeNodeInfo {
display: flex;
align-items: center;
.treeNodeIcon {
margin-right: 5px;
opacity: 0.7;
}
.treeNodeName {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.treeNodeBtnList {
display: flex;
align-items: center;
}
}
}
}
}
.toolbarBtn {
display: flex;
justify-content: center;
flex-direction: column;
cursor: pointer;
margin-right: 20px;
&:last-of-type {
margin-right: 0;
}
&:hover {
&:not(.disabled) {
.icon {
background: #f5f5f5;
}
}
}
&.active {
.icon {
background: #f5f5f5;
}
}
&.disabled {
color: #bcbcbc;
cursor: not-allowed;
pointer-events: none;
}
.icon {
display: flex;
height: 26px;
background: #fff;
border-radius: 4px;
border: 1px solid #e9e9e9;
justify-content: center;
flex-direction: column;
text-align: center;
padding: 0 5px;
}
.text {
margin-top: 3px;
}
}
}
}
</style>