This commit is contained in:
wanglin2 2023-05-03 22:06:52 +08:00
commit c2c9de1c03
29 changed files with 1145 additions and 463 deletions

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",

View File

@ -0,0 +1,122 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/electron/preload.js":
/*!*********************************!*\
!*** ./src/electron/preload.js ***!
\*********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("const { contextBridge, ipcRenderer } = __webpack_require__(/*! electron */ \"electron\")\n\ncontextBridge.exposeInMainWorld('platform', process.platform)\ncontextBridge.exposeInMainWorld('IS_ELECTRON', true)\n\ncontextBridge.exposeInMainWorld('electronAPI', {\n minimize: () => ipcRenderer.send('minimize'),\n maximize: () => ipcRenderer.send('maximize'),\n unmaximize: () => ipcRenderer.send('unmaximize'),\n close: () => ipcRenderer.send('close'),\n create: (id) => ipcRenderer.send('create', id),\n save: (id, data) => ipcRenderer.send('save', id, data),\n})\n\n//# sourceURL=webpack:///./src/electron/preload.js?");
/***/ }),
/***/ 0:
/*!***************************************!*\
!*** multi ./src/electron/preload.js ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("module.exports = __webpack_require__(/*! /Users/lisa/wanglin/github/mind-map/web/src/electron/preload.js */\"./src/electron/preload.js\");\n\n\n//# sourceURL=webpack:///multi_./src/electron/preload.js?");
/***/ }),
/***/ "electron":
/*!***************************!*\
!*** external "electron" ***!
\***************************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = require(\"electron\");\n\n//# sourceURL=webpack:///external_%22electron%22?");
/***/ })
/******/ });

32
web/package-lock.json generated
View File

@ -12,6 +12,7 @@
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",
@ -9162,7 +9163,6 @@
"node_modules/fs-extra": {
"version": "7.0.1",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@ -9512,8 +9512,8 @@
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/graceful-readlink": {
"version": "1.0.1",
@ -11237,7 +11237,6 @@
"node_modules/jsonfile": {
"version": "4.0.0",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@ -16879,7 +16878,6 @@
"node_modules/universalify": {
"version": "0.1.2",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
@ -20916,7 +20914,6 @@
"version": "4.5.19",
"dev": true,
"requires": {
"@babel/core": "^7.11.0",
"@babel/helper-compilation-targets": "^7.9.6",
"@babel/helper-module-imports": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
@ -20929,7 +20926,6 @@
"@vue/babel-plugin-jsx": "^1.0.3",
"@vue/babel-preset-jsx": "^1.2.4",
"babel-plugin-dynamic-import-node": "^2.3.3",
"core-js": "^3.6.5",
"core-js-compat": "^3.6.5",
"semver": "^6.1.0"
}
@ -21051,6 +21047,8 @@
},
"@vue/cli-plugin-vuex": {
"version": "4.5.19",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.19.tgz",
"integrity": "sha512-DUmfdkG3pCdkP7Iznd87RfE9Qm42mgp2hcrNcYQYSru1W1gX2dG/JcW8bxmeGSa06lsxi9LEIc/QD1yPajSCZw==",
"dev": true,
"requires": {}
},
@ -21190,6 +21188,8 @@
},
"@vue/preload-webpack-plugin": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
"dev": true,
"requires": {}
},
@ -21364,6 +21364,8 @@
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"requires": {}
},
@ -21409,11 +21411,15 @@
},
"ajv-errors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
"dev": true,
"requires": {}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"requires": {}
},
@ -25546,6 +25552,8 @@
"fs-extra": {
"version": "7.0.1",
"dev": true,
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@ -25788,6 +25796,8 @@
"graceful-fs": {
"version": "4.2.10",
"dev": true
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"graceful-readlink": {
"version": "1.0.1",
@ -26900,6 +26910,8 @@
"jsonfile": {
"version": "4.0.0",
"dev": true,
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"requires": {
"graceful-fs": "^4.1.6"
}
@ -30980,7 +30992,9 @@
},
"universalify": {
"version": "0.1.2",
"dev": true
"dev": true,
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
},
"unpipe": {
"version": "1.0.0",
@ -31839,6 +31853,8 @@
},
"vuex": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==",
"requires": {}
},
"w3c-keyname": {

View File

@ -20,6 +20,7 @@
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",

View File

@ -34,7 +34,17 @@ export const getData = () => {
return simpleDeepClone(exampleData)
} else {
try {
return JSON.parse(store)
let parsedData = JSON.parse(store)
if (window.IS_ELECTRON) {
return simpleDeepClone(exampleData)
let { root, ...rest } = parsedData
return {
...rest,
root: simpleDeepClone(exampleData).root
}
} else {
return parsedData
}
} catch (error) {
return simpleDeepClone(exampleData)
}

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1679621707211') format('woff2'),
url('iconfont.woff?t=1679621707211') format('woff'),
url('iconfont.ttf?t=1679621707211') format('truetype');
src: url('iconfont.woff2?t=1678265970945') format('woff2'),
url('iconfont.woff?t=1678265970945') format('woff'),
url('iconfont.ttf?t=1678265970945') format('truetype');
}
.iconfont {

View File

@ -1,8 +1,10 @@
'use strict'
import { app, protocol, BrowserWindow } from 'electron'
import { app, protocol, BrowserWindow, ipcMain, BrowserView, dialog } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import path from 'path'
const fs = require('fs-extra')
const isDevelopment = process.env.NODE_ENV !== 'production'
// Scheme must be registered before the app is ready
@ -18,22 +20,77 @@ async function createWindow() {
frame: false,
titleBarStyle: 'hiddenInset',
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
// 新建编辑页面
ipcMain.on('create', async (event, id) => {
const win = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
titleBarStyle: 'hiddenInset',
webPreferences: {
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(
process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche/edit/' + id
)
// if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
// Load the index.html when not in development
win.loadURL('app://./index.html/#/workbenche/edit/' + id)
}
})
// 保存
const idToFilePath = {}
ipcMain.on('save', async (event, id, data) => {
if (!idToFilePath[id]) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
const res = dialog.showSaveDialogSync(win, {
title: '保存',
defaultPath: '未命名.smm',
filters: [
{ name: '思维导图', extensions: ['smm'] }
]
})
if (res) {
idToFilePath[id] = res
fs.writeFile(res, data)
}
return
}
fs.writeFile(idToFilePath[id], data)
})
;['minimize', 'maximize', 'unmaximize', 'close'].forEach(item => {
ipcMain.on(item, event => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win[item]()
})
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche')
// if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
win.loadURL('app://./index.html/#/workbenche')
}
}
@ -52,25 +109,14 @@ app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
// if (isDevelopment && !process.env.IS_TEST) {
// // Install Vue Devtools
// try {
// await installExtension(VUEJS_DEVTOOLS)
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
// }
createWindow()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
}

View File

@ -0,0 +1,13 @@
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('platform', process.platform)
contextBridge.exposeInMainWorld('IS_ELECTRON', true)
contextBridge.exposeInMainWorld('electronAPI', {
minimize: () => ipcRenderer.send('minimize'),
maximize: () => ipcRenderer.send('maximize'),
unmaximize: () => ipcRenderer.send('unmaximize'),
close: () => ipcRenderer.send('close'),
create: (id) => ipcRenderer.send('create', id),
save: (id, data) => ipcRenderer.send('save', id, data),
})

View File

@ -14,6 +14,15 @@ Vue.config.productionTip = false
Vue.prototype.$bus = new Vue()
Vue.use(ElementUI)
Vue.use(VueViewer)
Vue.mixin({
data () {
return {
IS_ELECTRON: window.IS_ELECTRON,
IS_MAC: window.platform === 'darwin',
IS_WIN: window.platform === 'win32'
}
}
})
new Vue({
render: h => h(App),

View File

@ -1,5 +1,5 @@
<template>
<div class="editContainer">
<div class="editContainer" :style="{top: IS_ELECTRON ? '40px' : 0}">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count v-if="!isZenMode"></Count>
<Navigator :mindMap="mindMap"></Navigator>
@ -136,6 +136,9 @@ export default {
this.test()
}, 5000)
}
if (window.IS_ELECTRON) {
this.mindMap.keyCommand.addShortcut('Control+s', this.saveToLocal)
}
},
methods: {
/**
@ -409,6 +412,13 @@ export default {
//
removeRichTextPlugin() {
this.mindMap.removePlugin(RichText)
},
saveToLocal() {
let id = this.$route.params.id
let data = this.mindMap.getData(true)
console.log('保存', id, data)
window.electronAPI.save(id, JSON.stringify(data))
}
}
}

View File

@ -1,6 +1,6 @@
<template>
<div class="toolbarContainer">
<div class="toolbar">
<div class="toolbar" :style="{top: IS_ELECTRON ? '40px' : 0}">
<!-- 节点操作 -->
<div class="toolbarBlock">
<div
@ -126,15 +126,15 @@
</div>
<!-- 导出 -->
<div class="toolbarBlock">
<div class="toolbarBtn" @click="createNewLocalFile">
<div class="toolbarBtn" @click="createNewLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont iconxinjian"></span>
<span class="text">{{ $t('toolbar.newFile') }}</span>
</div>
<div class="toolbarBtn" @click="openLocalFile">
<div class="toolbarBtn" @click="openLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont icondakai"></span>
<span class="text">{{ $t('toolbar.openFile') }}</span>
</div>
<div class="toolbarBtn" @click="saveLocalFile">
<div class="toolbarBtn" @click="saveLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont iconlingcunwei"></span>
<span class="text">{{ $t('toolbar.saveAs') }}</span>
</div>

View File

@ -1,6 +1,5 @@
<template>
<div class="workbencheContainer">
<Header></Header>
<div class="workbencheContent">
<router-view></router-view>
</div>
@ -8,13 +7,8 @@
</template>
<script>
import Header from './components/Header.vue';
export default {
name: 'Workbenche',
components: {
Header
}
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<div class="workbencheEmptyContainer">
<div class="icon iconfont iconwushuju"></div>
<div class="tip">暂时没有内容</div>
<div class="desc">没有最近使用的文件记录</div>
<div class="createBtn" @click="create">立即创建</div>
</div>
</template>
<script>
import { create } from '../utils'
export default {
methods: {
create
}
}
</script>
<style lang="less" scoped>
.workbencheEmptyContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.icon {
font-size: 100px;
margin-bottom: 20px;
}
.tip {
font-weight: bold;
margin-bottom: 10px;
}
.desc {
font-size: 12px;
margin-bottom: 20px;
}
.createBtn {
width: 100px;
height: 30px;
border-radius: 5px;
border: 1px solid #409eff;
color: #409eff;
font-size: 14px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
background-color: rgba(64, 158, 255, 0.3);
}
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="workbencheFileListContainer">
<div class="title">最近</div>
<div class="fileListBox">
<Empty></Empty>
</div>
</div>
</template>
<script>
import Empty from '../components/Empty.vue'
export default {
components: {
Empty
}
}
</script>
<style lang="less" scoped>
.workbencheFileListContainer {
flex-grow: 1;
height: 100%;
background-color: #fff;
border-radius: 10px;
padding: 20px;
padding-top: 0;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
font-size: 18px;
border-bottom: 1px solid #e4e7ed;
height: 65px;
line-height: 65px;
flex-shrink: 0;
}
.fileListBox {
flex-grow: 1;
overflow: hidden;
}
}
</style>

View File

@ -1,63 +0,0 @@
<template>
<div class="workbencheHeaderContainer">
<div class="placeholder"></div>
<div class="home noDrag" :class="{active: isInHome}">
<span class="icon iconfont iconzhuye"></span>
<span class="text">主页</span>
</div>
<ScrollTab></ScrollTab>
</div>
</template>
<script>
import ScrollTab from './ScrollTab.vue'
export default {
components: {
ScrollTab
},
computed: {
isInHome() {
return this.$route.path.includes('home')
}
}
}
</script>
<style lang="less" scoped>
.workbencheHeaderContainer {
width: 100%;
height: 40px;
background-color: #ebeef1;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
.placeholder {
width: 100px;
height: 100%;
flex-shrink: 0;
}
.home {
padding: 0 10px;
height: 100%;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
background-color: #e3e6e9;
flex-shrink: 0;
font-size: 14px;
&.active {
background-color: #fff;
}
.icon {
margin-right: 5px;
}
}
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="macControl" v-if="IS_MAC"></div>
</template>
<script>
export default {}
</script>
<style lang="less" scoped>
.macControl {
width: 100px;
height: 100%;
flex-shrink: 0;
}
</style>

View File

@ -1,227 +0,0 @@
<template>
<div class="workbencheScrollTabContainer" ref="container">
<div class="workbencheScrollTabBox noDrag">
<div class="workbencheScrollTabList" ref="list" :style="{ transform: `translateX(${listTranslateX}px)` }">
<div class="workbencheScrollTabItem" v-for="(item, index) in localEditList" :key="item.id"
:style="{ width: tabWidth + 'px' }" :class="{ active: $route.params.id === item.id }"
@click="openTab(item)">
<span class="icon iconfont icondiannao"></span>
<span class="text">{{ item.name }}</span>
<span class="mask"></span>
<span class="icon closeIcon el-icon-close" @click="deleteFile(index)"></span>
</div>
</div>
</div>
<div class="workbencheScrollTabControl noDrag" ref="control">
<div class="workbencheScrollTabBtn el-icon-plus" @click="addFile"></div>
<div class="workbencheScrollTabBtn el-icon-arrow-left" @click="scrollLeft"></div>
<div class="workbencheScrollTabBtn el-icon-arrow-right" @click="scrollRight"></div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import { v4 as uuidv4 } from 'uuid';
const MIN_TAB_WIDTH = 100
const MAX_TAB_WIDTH = 220
export default {
data() {
return {
tabWidth: 0,
tabTotalWidth: 0,
listTranslateX: 0
}
},
computed: {
...mapState(['localEditList']),
},
mounted() {
this.computeTabWidth()
},
methods: {
...mapMutations(['setLocalEditList']),
addFile() {
let item = {
id: uuidv4(),
name: '新建文件',
path: ''
}
this.setLocalEditList([...this.localEditList, item])
this.$nextTick(() => {
this.openTab(item)
this.computeTabWidth()
this.scrollToEnd()
})
},
deleteFile(index) {
let arr = [...this.localEditList]
arr.splice(index, 1)
this.setLocalEditList(arr)
this.$nextTick(() => {
this.computeTabWidth()
})
},
computeTabWidth() {
let containerWidth = this.$refs.container.getBoundingClientRect().width
let controlWidth = this.$refs.control.getBoundingClientRect().width
this.tabTotalWidth = containerWidth - controlWidth
let length = this.localEditList.length
let tabWidth = Math.floor(this.tabTotalWidth / length)
if (tabWidth >= MAX_TAB_WIDTH) {
this.tabWidth = MAX_TAB_WIDTH
} else if (tabWidth <= MIN_TAB_WIDTH) {
this.tabWidth = MIN_TAB_WIDTH
} else {
this.tabWidth = tabWidth
}
},
scrollLeft() {
if (this.listTranslateX + this.tabWidth < 0) {
this.listTranslateX += this.tabWidth
} else {
this.listTranslateX = 0
}
},
scrollRight() {
let maxScroll = this.tabTotalWidth - this.localEditList.length * this.tabWidth
if (this.listTranslateX - this.tabWidth > maxScroll) {
this.listTranslateX -= this.tabWidth
} else {
this.listTranslateX = maxScroll
}
},
scrollToEnd() {
if (this.localEditList.length * this.tabWidth <= this.tabTotalWidth) {
return
}
this.listTranslateX = this.tabTotalWidth - this.localEditList.length * this.tabWidth
},
openTab(item) {
this.$router.push({
name: 'WorkbencheEdit',
params: {
id: item.id
}
})
}
}
}
</script>
<style lang="less" scoped>
.workbencheScrollTabContainer {
flex-grow: 1;
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
.workbencheScrollTabBox {
overflow: hidden;
height: 100%;
.workbencheScrollTabList {
display: flex;
width: max-content;
height: 100%;
transition: all 0.3s;
.workbencheScrollTabItem {
flex-shrink: 0;
height: 100%;
font-size: 12px;
padding: 0 10px;
position: relative;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
.icon {
&.closeIcon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: none;
}
}
.mask {
position: absolute;
right: 0;
top: 0;
width: 20px;
height: 100%;
background-color: #fff;
box-shadow: -10px 0 10px #fff;
display: none;
}
&:hover {
background-color: #fff;
.mask {
display: block;
}
.closeIcon {
display: block;
}
}
&::after {
content: '';
position: absolute;
width: 1px;
height: 10px;
background-color: #999;
top: 50%;
transform: translateY(-50%);
right: 0;
}
&.active {
background-color: #fff;
&::after {
display: none;
}
}
.text {}
}
}
}
.workbencheScrollTabControl {
flex-shrink: 0;
padding: 0 10px;
height: 100%;
display: flex;
.workbencheScrollTabBtn {
width: 20px;
height: 100%;
cursor: pointer;
font-size: 14px;
color: #000;
margin: 0 5px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
</style>

View File

@ -1,75 +1,86 @@
<template>
<div class="workbencheHomeSidebarContainer">
<div class="workbencheMenuList">
<div class="workbencheMenuItem" v-for="item in menuList" :key="item.name"
:class="{ active: $route.name === item.routerName }" @click="toMenu(item.rou
)">
<span class="icon iconfont" :class="[item.icon]"></span>
<span class="text">{{ item.name }}</span>
</div>
</div>
<div class="workbencheSidebarContainer">
<div class="createBtn" @click="create">开始新建</div>
<div class="line"></div>
<div class="btn">
<span class="icon iconfont icondakai"></span>
<span class="text">打开本地文件</span>
</div>
<div class="btn active">
<span class="icon iconfont iconzuijinliulan"></span>
<span class="text">最近文件</span>
</div>
</div>
</template>
<script>
import { create } from '../utils'
export default {
data() {
return {
menuList: [
{
name: '本地',
icon: 'iconbendi1x',
routerName: 'WorkbencheHomeLocal'
}
]
}
},
methods: {
toMenu(routerName) {
this.$router.push(routerName)
}
}
methods: {
create
}
}
</script>
<style lang="less" scoped>
.workbencheHomeSidebarContainer {
width: 80px;
background-color: #fff;
height: 100%;
flex-shrink: 0;
padding: 20px 0;
.workbencheSidebarContainer {
flex-shrink: 0;
width: 200px;
height: 100%;
background-color: #fff;
border-radius: 10px;
margin-right: 20px;
padding: 15px 20px;
.workbencheMenuList {
display: flex;
flex-direction: column;
align-items: center;
.createBtn {
width: 100%;
height: 30px;
background-color: #409eff;
border-radius: 5px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 14px;
user-select: none;
.workbencheMenuItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #000;
width: 60px;
height: 60px;
border-radius: 5px;
cursor: pointer;
&.active {
background-color: #f3fbf4;
color: #56b74b;
}
.icon {
margin-bottom: 5px;
}
.text {
font-size: 14px;
}
}
&:hover {
opacity: 0.9;
}
}
.line {
width: 100%;
height: 1px;
background-color: #e4e7ed;
margin: 20px 0;
}
.btn {
width: 100%;
height: 30px;
background-color: #fff;
border-radius: 5px;
color: #000;
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
user-select: none;
padding: 0 10px;
margin-bottom: 10px;
&.active,
&:hover {
background-color: rgba(64, 158, 255, 0.3);
color: rgba(64, 158, 255, 1);
}
.icon {
margin-right: 10px;
}
}
}
</style>
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="winControl noDrag" v-if="IS_WIN">
<div class="winControlBtn iconfont iconzuixiaohua" @click="minimize"></div>
<div
class="winControlBtn iconfont"
:class="[isMaximize ? 'icon3zuidahua-3' : 'iconzuidahua']"
@click="toggleMaximize"
></div>
<div class="winControlBtn iconfont iconguanbi" @click="close"></div>
</div>
</template>
<script>
export default {
data() {
return {
isMaximize: false
}
},
methods: {
minimize() {
window.electronAPI.minimize()
},
toggleMaximize() {
if (this.isMaximize) {
this.isMaximize = false
window.electronAPI.unmaximize()
} else {
this.isMaximize = true
window.electronAPI.maximize()
}
},
close() {
window.electronAPI.close()
}
}
}
</script>
<style lang="less" scoped>
.winControl {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
.winControlBtn {
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: #ccced1;
}
&.iconguanbi {
&:hover {
background-color: #e81123;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,6 @@
import { v4 as uuid } from 'uuid'
// 打开新的编辑窗口
export const create = () => {
window.electronAPI.create(uuid())
}

View File

@ -1,21 +1,37 @@
<template>
<div class="workbencheHeaderContainer">
<Edit :key="$route.params.id"></Edit>
<div class="workbencheEditContainer">
<div class="workbencheEditHeader">
<MacControl></MacControl>
<WinControl></WinControl>
</div>
<Edit></Edit>
</div>
</template>
<script>
import Edit from '../../Edit/Index.vue';
import Edit from '../../Edit/Index.vue'
import WinControl from '../components/WinControl.vue'
import MacControl from '../components/MacControl.vue'
export default {
components: {
Edit
}
components: {
Edit,
MacControl,
WinControl
},
}
</script>
<style lang="less" scoped>
.workbencheHeaderContainer {
.workbencheEditContainer {
.workbencheEditHeader {
width: 100%;
height: 40px;
background-color: #ebeef1;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
}
}
</style>
</style>

View File

@ -1,32 +1,55 @@
<template>
<div class="workbencheHomeContainer">
<Sidebar></Sidebar>
<div class="workbencheHomeContent">
<router-view></router-view>
</div>
<div class="workbencheHomeContainer">
<div class="workbencheHomeHeader">
<MacControl></MacControl>
<WinControl></WinControl>
</div>
<div class="workbencheHomeContent">
<Sidebar></Sidebar>
<FileList></FileList>
</div>
</div>
</template>
<script>
import WinControl from '../components/WinControl.vue'
import MacControl from '../components/MacControl.vue'
import Sidebar from '../components/Sidebar.vue'
import FileList from '../components/FileList.vue'
export default {
components: {
Sidebar
}
components: {
WinControl,
MacControl,
Sidebar,
FileList
}
}
</script>
<style lang="less" scoped>
.workbencheHomeContainer {
background-color: #f6f8f9;
width: 100%;
height: 100%;
display: flex;
background-color: #f6f8f9;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.workbencheHomeContent {
flex-grow: 1;
padding: 20px;
}
.workbencheHomeHeader {
width: 100%;
height: 40px;
background-color: #ebeef1;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
}
.workbencheHomeContent {
flex-grow: 1;
padding: 20px;
display: flex;
overflow: hidden;
}
}
</style>
</style>

View File

@ -1,20 +0,0 @@
<template>
<div class="workbencheLocalContainer">
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.workbencheLocalContainer {
width: 100%;
height: 100%;
border-radius: 5px;
background-color: #fff;
}
</style>

View File

@ -6,7 +6,6 @@ import routerList from '@/pages/Doc/routerList'
import WorkbenchePage from '@/pages/Workbenche/Index'
import WorkbencheHomePage from '@/pages/Workbenche/views/Home'
import WorkbencheEditPage from '@/pages/Workbenche/views/Edit'
import WorkbencheHomeLocalPage from '@/pages/Workbenche/views/Local'
// 处理没有翻译的章节路由
const handleRouterList = () => {
@ -39,19 +38,12 @@ const routes = [
path: '/workbenche',
name: 'Workbenche',
component: WorkbenchePage,
redirect: '/workbenche/home/local',
redirect: '/workbenche/home',
children: [
{
path: 'home',
name: 'WorkbencheHome',
component: WorkbencheHomePage,
children: [
{
path: 'local',
name: 'WorkbencheHomeLocal',
component: WorkbencheHomeLocalPage,
}
]
},
{
path: 'edit/:id',

View File

@ -1,16 +1,110 @@
const path = require('path');
const path = require('path')
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
publicPath: isDev ? '' : './dist',
outputDir: '../dist',
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src/')
}
}
publicPath: isDev ? '' : './dist',
outputDir: '../dist',
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src/')
}
}
}
},
pluginOptions: {
electronBuilder: {
preload: 'src/electron/preload.js',
builderOptions: {
productName: 'SimpleMindMap',
copyright: 'Copyright © SimpleMindMap',
// compression: "maximum", // 机器好的可以打开,配置压缩,开启后会让 .AppImage 格式的客户端启动缓慢
asar: true,
publish: [
{
provider: 'github',
owner: 'wanglin2',
repo: 'mind-map',
vPrefixedTagName: true,
releaseType: 'draft',
},
],
directories: {
output: 'dist_electron',
},
mac: {
target: [
{
target: 'dmg',
arch: ['x64', 'arm64', 'universal'],
},
],
artifactName: '${productName}-${os}-${version}-${arch}.${ext}',
category: 'public.app-category.utilities',
darkModeSupport: true,
},
win: {
target: [
{
target: 'portable',
arch: ['x64'],
},
{
target: 'nsis',
arch: ['x64'],
},
],
publisherName: 'SimpleMindMap',
icon: 'build/icons/icon.ico',
publish: ['github'],
},
linux: {
target: [
{
target: 'AppImage',
arch: ['x64'],
},
{
target: 'tar.gz',
arch: ['x64', 'arm64'],
},
{
target: 'deb',
arch: ['x64', 'armv7l', 'arm64'],
},
{
target: 'rpm',
arch: ['x64'],
},
{
target: 'snap',
arch: ['x64'],
},
{
target: 'pacman',
arch: ['x64'],
},
],
category: 'Utilities',
icon: './build/icon.icns',
},
dmg: {
icon: 'build/icons/icon.icns',
},
nsis: {
oneClick: true,
perMachine: true,
deleteAppDataOnUninstall: true,
},
},
// 渲染线程的配置文件
chainWebpackRendererProcess: config => {
config.plugin('define').tap(args => {
args[0]['IS_ELECTRON'] = true
return args
})
}
}
}
}