├── .browserslistrc ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── babel.config.js ├── build ├── icons │ ├── 256x256.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png └── image ├── package.json ├── public └── index.html ├── src ├── App.vue ├── assets │ ├── css │ │ ├── element-variables.scss │ │ ├── global.scss │ │ ├── mixin.scss │ │ └── variables.scss │ ├── iconsvg │ │ ├── chart.svg │ │ ├── components.svg │ │ ├── control.svg │ │ ├── detail.svg │ │ ├── edit.svg │ │ ├── editor.svg │ │ ├── home.svg │ │ ├── icon.svg │ │ ├── list.svg │ │ ├── page.svg │ │ ├── permissions.svg │ │ ├── set.svg │ │ └── svgo.yml │ └── images │ │ ├── .gitkeep │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── bg-smooth.jpg │ │ ├── icon.ico │ │ ├── logo.png │ │ └── qrcode.png ├── background.ts ├── background │ ├── audio │ │ └── index.ts │ ├── autoUpdate.ts │ ├── clearwater │ │ └── index.ts │ ├── database │ │ └── index.ts │ ├── edit-video │ │ └── index.ts │ ├── ffmpeg │ │ ├── audio │ │ │ ├── getAudioInfo.ts │ │ │ └── saveAudio.ts │ │ ├── ffmpeg.ts │ │ ├── helper.ts │ │ ├── utils │ │ │ ├── formatBytes.ts │ │ │ └── formatSeconds.ts │ │ └── video │ │ │ ├── concatVideo.ts │ │ │ ├── cutVideo.ts │ │ │ ├── getScreenshots.ts │ │ │ ├── getVideoInfo.ts │ │ │ └── saveVideo.ts │ ├── index.ts │ ├── terminal │ │ └── index.ts │ └── utils.ts ├── components │ ├── Permission │ │ └── index.vue │ └── Result │ │ ├── index.vue │ │ └── svg │ │ ├── error.vue │ │ ├── info.vue │ │ ├── noFound.vue │ │ ├── serverError.vue │ │ ├── success.vue │ │ ├── unauthorized.vue │ │ └── warning.vue ├── config │ ├── clearwater.ts │ ├── languageList.ts │ ├── settings.ts │ └── videoConfig.ts ├── main.ts ├── router │ └── index.ts ├── services │ ├── audio.ts │ ├── clearwater.ts │ ├── discern.ts │ ├── indexData.ts │ ├── request.ts │ ├── user.ts │ └── weight.ts ├── shims-vue.d.ts ├── store │ ├── global.ts │ ├── index.ts │ └── user.ts ├── utils │ ├── localToken.ts │ └── validate.ts └── views │ ├── 404 │ └── index.vue │ ├── audio │ ├── config.vue │ ├── history.vue │ ├── index.vue │ ├── play.vue │ └── textarea.vue │ ├── clearwater │ ├── config.ts │ ├── index.vue │ ├── table.vue │ └── utils.ts │ ├── discern │ └── index.vue │ ├── edit-video │ ├── config.vue │ ├── index.vue │ └── textarea.vue │ ├── home │ ├── PageLoading │ │ └── index.vue │ └── index.vue │ ├── index.vue │ ├── login │ ├── index.vue │ └── qrcode.png │ ├── terminal │ └── index.vue │ └── weight │ └── index.vue ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | #运行环境 2 | NODE_ENV = 'development' 3 | VUE_APP_APIHOST=http://localhost:8080 4 | 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | #运行环境 2 | NODE_ENV = 'production' 3 | 4 | VUE_APP_APIHOST=https://zimeitizizhu.com/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | data 3 | dist_electron -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **此版本暂时不维护,请进入一下链接 ,下载相关软件资源** , 2 | > [https://github.com/linqian02/axvideo/](https://github.com/lqomg/wvovw/) 3 | 4 | 5 | > **AX Video 爱写视频**,是一个桌面软件,自动将 Markdown 文档转成一段视频的短视频编辑器,支持操作界面 6 | 文档里面的视频、音频、图片网址,都会抓取后插入视频,还可以根据文字生成人工语音的旁白朗读。 7 | 8 | 9 | > 问题反馈请提issues 或者 , 进入QQ群 `576301295 ` 反馈。 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/build/icons/icon.ico -------------------------------------------------------------------------------- /build/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/build/icons/icon.png -------------------------------------------------------------------------------- /build/image: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zimeitizizhu", 3 | "version": "0.1.3", 4 | "private": true, 5 | "description": "自媒体自助桌面版本", 6 | "scripts": { 7 | "serve": "vue-cli-service electron:serve", 8 | "build": "vue-cli-service electron:build", 9 | "rebuild": "./node_modules/.bin/electron-rebuild.cmd", 10 | "web:build": "vue-cli-service build", 11 | "web:serve": "vue-cli-service serve", 12 | "postinstall": "electron-builder install-app-deps", 13 | "postuninstall": "electron-builder install-app-deps" 14 | }, 15 | "main": "background.js", 16 | "dependencies": { 17 | "@ffmpeg-installer/ffmpeg": "^1.0.20", 18 | "@ffprobe-installer/ffprobe": "^1.1.0", 19 | "axios": "^0.21.1", 20 | "child-process-promise": "^2.2.1", 21 | "core-js": "^3.8.3", 22 | "electron-updater": "^4.3.8", 23 | "element-plus": "^1.0.1-beta.27", 24 | "localforage": "^1.9.0", 25 | "md5-node": "^1.0.1", 26 | "nedb": "^1.8.0", 27 | "normalize.css": "^8.0.1", 28 | "silly-datetime": "^0.1.2", 29 | "vue": "3.0.7", 30 | "vue-router": "^4.0.3", 31 | "vuex": "^4.0.0-rc.2", 32 | "vuex-persistedstate": "^4.0.0-beta.3" 33 | }, 34 | "devDependencies": { 35 | "@types/electron-devtools-installer": "^2.2.0", 36 | "@vue/cli-plugin-babel": "~4.5.10", 37 | "@vue/cli-plugin-router": "~4.5.10", 38 | "@vue/cli-plugin-typescript": "~4.5.10", 39 | "@vue/cli-plugin-vuex": "~4.5.10", 40 | "@vue/cli-service": "~4.5.10", 41 | "@vue/compiler-sfc": "3.0.7", 42 | "electron": "6.0.9", 43 | "electron-debug": "^3.2.0", 44 | "electron-devtools-installer": "^3.1.1", 45 | "electron-rebuild": "2.3.4", 46 | "node-sass": "^4.14.1", 47 | "ffmpeg-concat": "^1.3.3", 48 | "fluent-ffmpeg": "^2.1.2", 49 | "sass-loader": "^8.0.2", 50 | "typescript": "~3.9.7", 51 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.5" 52 | }, 53 | "__npminstall_done": false 54 | } 55 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 98 | 127 | -------------------------------------------------------------------------------- /src/assets/css/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量 */ 2 | $--color-primary: #009688; 3 | 4 | /// color|1|Font Color|2 5 | $--color-text-primary: #303133 !default; 6 | 7 | /* 改变 icon 字体路径变量,必需 */ 8 | $--font-path: '~element-plus/lib/theme-chalk/fonts'; 9 | 10 | @import '~element-plus/packages/theme-chalk/src/index'; 11 | 12 | $heading-color: rgba(0, 0, 0, 0.85); // 标题色 13 | 14 | // Menu 15 | $menu-collapsed-width: 54px; 16 | 17 | // dark theme 18 | $menu-dark-bg: #222834; 19 | $menu-dark-submenu-bg: #000c17; 20 | $menu-dark-color: rgba(#FFF, 0.65); 21 | $menu-dark-highlight-color: #fff; 22 | 23 | 24 | /* 重置 NavMenu */ 25 | .index-layout-leftmenu { 26 | background-color: $menu-dark-bg; 27 | border: none; 28 | 29 | 30 | .el-submenu .el-menu { 31 | background-color: $menu-dark-submenu-bg; 32 | } 33 | 34 | .el-submenu__title, 35 | .el-menu-item { 36 | height: 40px; 37 | line-height: 40px; 38 | color: $menu-dark-color; 39 | .icon { 40 | margin-right: 5px; 41 | vertical-align: middle; 42 | width: 24px; 43 | text-align: center; 44 | color: $menu-dark-color; 45 | } 46 | 47 | &:focus, 48 | &:hover { 49 | color: $menu-dark-highlight-color !important; 50 | background:none; 51 | .icon { 52 | color: $menu-dark-highlight-color !important; 53 | } 54 | } 55 | } 56 | 57 | .is-active>.el-submenu__title { 58 | color: $menu-dark-highlight-color !important; 59 | .icon { 60 | color: $menu-dark-highlight-color !important; 61 | } 62 | } 63 | .el-submenu .el-menu-item.is-active, 64 | .el-menu-item.is-active{ 65 | color: $menu-dark-highlight-color !important; 66 | background-color: $--color-primary !important; 67 | } 68 | .el-submenu__title { 69 | margin-top: 4px; 70 | margin-bottom: 4px; 71 | } 72 | .el-menu-item:not(:last-child) { 73 | margin-bottom: 8px; 74 | } 75 | 76 | 77 | /* 收起 */ 78 | &.el-menu--collapse { 79 | width: $menu-collapsed-width; 80 | .el-submenu__title, 81 | .el-menu-item { 82 | .icon { 83 | position: absolute; 84 | left: 15px; 85 | top: 10px; 86 | margin: 0; 87 | } 88 | } 89 | } 90 | 91 | 92 | } 93 | .index-layout-leftmenu-popper { 94 | 95 | .el-menu { 96 | background-color: $menu-dark-submenu-bg; 97 | } 98 | .el-submenu__title, 99 | .el-menu-item { 100 | height: 40px; 101 | line-height: 40px; 102 | color: $menu-dark-color; 103 | .icon { 104 | margin-right: 5px; 105 | vertical-align: middle; 106 | width: 24px; 107 | text-align: center; 108 | color: $menu-dark-color; 109 | } 110 | 111 | &:focus, 112 | &:hover { 113 | color: $menu-dark-highlight-color !important; 114 | background:none; 115 | .icon { 116 | color: $menu-dark-highlight-color !important; 117 | } 118 | } 119 | } 120 | 121 | .is-active>.el-submenu__title { 122 | color: $menu-dark-highlight-color !important; 123 | .icon { 124 | color: $menu-dark-highlight-color !important; 125 | } 126 | } 127 | .el-submenu .el-menu-item.is-active, 128 | .el-menu-item.is-active{ 129 | color: $menu-dark-highlight-color !important; 130 | background-color: $--color-primary !important; 131 | } 132 | .el-submenu__title { 133 | margin-top: 4px; 134 | margin-bottom: 4px; 135 | } 136 | .el-menu-item:not(:last-child) { 137 | margin-bottom: 8px; 138 | } 139 | 140 | 141 | } 142 | 143 | 144 | /* 重置 Drawer */ 145 | .el-drawer { 146 | .el-drawer__header { 147 | padding: 15px 20px; 148 | margin-bottom: 0; 149 | border-bottom: 1px solid #f0f0f0; 150 | span, 151 | button { 152 | outline: none; 153 | } 154 | } 155 | } 156 | 157 | /* 重置 card */ 158 | .cus-card { 159 | .el-card { 160 | border: 0; 161 | } 162 | } 163 | 164 | /*自定义 table 样式*/ 165 | .custom-table { 166 | .el-table__header { 167 | background-color: #F5F7FA; 168 | } 169 | .custom-td-header-one, 170 | .el-table__body tr:hover .custom-td-header-one { 171 | color: #303133; 172 | font-weight: bold; 173 | background-color: #F5F7FA; 174 | } 175 | } 176 | .custom-table-header { 177 | color: #303133; 178 | th { 179 | background-color: #F5F7FA; 180 | } 181 | } 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/assets/css/global.scss: -------------------------------------------------------------------------------- 1 | @import './element-variables.scss'; 2 | @import './variables.scss'; 3 | @import './mixin.scss'; 4 | 5 | .flex-wrap-wrap { 6 | flex-wrap: wrap; 7 | } 8 | 9 | a { 10 | color: #1890ff; 11 | text-decoration: none; 12 | background-color: transparent; 13 | outline: none; 14 | cursor: pointer; 15 | transition: color .3s; 16 | } 17 | a:active, 18 | a:hover { 19 | text-decoration: none; 20 | outline: 0; 21 | } 22 | a:active { 23 | color: #096dd9; 24 | } 25 | a:hover { 26 | color: #40a9ff; 27 | } 28 | 29 | .border-solid-transparent{ 30 | border: solid 1px transparent; 31 | } 32 | 33 | .text-align-right { 34 | text-align: right; 35 | } 36 | 37 | .float-right { 38 | float: right; 39 | } 40 | 41 | .padding-t10 { 42 | padding-top: 10px; 43 | } 44 | 45 | .cursor-pointer { 46 | cursor: pointer; 47 | } -------------------------------------------------------------------------------- /src/assets/css/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin scrollbar { 2 | ::-webkit-scrollbar { 3 | width: 6px; 4 | height: 6px; 5 | } 6 | ::-webkit-scrollbar-thumb { 7 | background: hsla(0,0%,100%,.2); 8 | border-radius: 3px; 9 | box-shadow: inset 0 0 5px hsla(0,0%,100%,.05); 10 | } 11 | ::-webkit-scrollbar-track { 12 | background: hsla(0,0%,100%,.15); 13 | border-radius: 3px; 14 | box-shadow: inset 0 0 5px rgba(37,37,37,.05); 15 | } 16 | } -------------------------------------------------------------------------------- /src/assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | // 主窗口背景色 2 | $mainBgColor: #f0f3f4; 3 | // 左边宽度 4 | $leftSideBarWidth: 200px; 5 | // 头部高度 6 | $headerHeight: 50px; 7 | // 头部下方面包屑高度 8 | $headerBreadcrumbHeight: 34px; 9 | -------------------------------------------------------------------------------- /src/assets/iconsvg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/components.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/control.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/editor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/page.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/permissions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/set.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/1.png -------------------------------------------------------------------------------- /src/assets/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/2.png -------------------------------------------------------------------------------- /src/assets/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/3.png -------------------------------------------------------------------------------- /src/assets/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/4.png -------------------------------------------------------------------------------- /src/assets/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/5.png -------------------------------------------------------------------------------- /src/assets/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/6.png -------------------------------------------------------------------------------- /src/assets/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/7.png -------------------------------------------------------------------------------- /src/assets/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/8.png -------------------------------------------------------------------------------- /src/assets/images/bg-smooth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/bg-smooth.jpg -------------------------------------------------------------------------------- /src/assets/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/icon.ico -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/assets/images/qrcode.png -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { app, globalShortcut, protocol, BrowserWindow, Menu } from 'electron' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' 6 | const isDevelopment = process.env.NODE_ENV !== 'production' 7 | import './background/clearwater'; 8 | import './background/edit-video'; 9 | import './background/audio'; 10 | import './background/terminal' 11 | import updateHandel from './background/autoUpdate'; 12 | // Scheme must be registered before the app is ready 13 | protocol.registerSchemesAsPrivileged([ 14 | { scheme: 'app', privileges: { secure: true, standard: true } } 15 | ]) 16 | 17 | async function createWindow() { 18 | // Create the browser window. 19 | const win = new BrowserWindow({ 20 | width: 1600, 21 | height: 800, 22 | icon: require('path').join(__dirname, '../build/icons/icon.ico'), 23 | webPreferences: { 24 | webSecurity: false, 25 | enableRemoteModule: true, 26 | // Use pluginOptions.nodeIntegration, leave this alone 27 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info 28 | nodeIntegration: (process.env 29 | .ELECTRON_NODE_INTEGRATION as unknown) as boolean 30 | } 31 | }) 32 | globalShortcut.register('CommandOrControl+Shift+x', function () { 33 | win.webContents.openDevTools() 34 | }) 35 | 36 | if (process.env.WEBPACK_DEV_SERVER_URL) { 37 | // Load the url of the dev server if in development mode 38 | await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) 39 | if (!process.env.IS_TEST) win.webContents.openDevTools() 40 | } else { 41 | 42 | // The production environment hides menus 43 | Menu.setApplicationMenu(null); 44 | 45 | createProtocol('app') 46 | // Load the index.html when not in development 47 | win.loadURL('app://./index.html') 48 | } 49 | win.setMenu(null); 50 | updateHandel(win); 51 | } 52 | 53 | // Quit when all windows are closed. 54 | app.on('window-all-closed', () => { 55 | // On macOS it is common for applications and their menu bar 56 | // to stay active until the user quits explicitly with Cmd + Q 57 | if (process.platform !== 'darwin') { 58 | app.quit() 59 | } 60 | }) 61 | 62 | app.on('activate', () => { 63 | // On macOS it's common to re-create a window in the app when the 64 | // dock icon is clicked and there are no other windows open. 65 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 66 | }) 67 | 68 | // This method will be called when Electron has finished 69 | // initialization and is ready to create browser windows. 70 | // Some APIs can only be used after this event occurs. 71 | app.on('ready', async () => { 72 | if (isDevelopment && !process.env.IS_TEST) { 73 | // Install Vue Devtools 74 | try { 75 | await installExtension(VUEJS_DEVTOOLS) 76 | } catch (e) { 77 | console.error('Vue Devtools failed to install:', e.toString()) 78 | } 79 | } 80 | createWindow() 81 | }) 82 | 83 | // Exit cleanly on request from parent process in development mode. 84 | if (isDevelopment) { 85 | if (process.platform === 'win32') { 86 | process.on('message', (data) => { 87 | if (data === 'graceful-exit') { 88 | app.quit() 89 | } 90 | }) 91 | } else { 92 | process.on('SIGTERM', () => { 93 | app.quit() 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/background/audio/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import db from '../database/index'; 3 | import { saveAudio, getAudioInfo } from '../ffmpeg/helper'; 4 | const date = require("silly-datetime"); 5 | const { audioList } = db; 6 | ipcMain.on('save-audio', async (event: any, arg: any) => { 7 | const { url, type, fileName } = arg; 8 | saveAudio(arg, { 9 | error({ filePath, err }) { 10 | event.returnValue = { 11 | code: 404, 12 | filePath, 13 | error: err 14 | } 15 | }, 16 | async end({ filePath, fileName }) { 17 | const data: any = await getAudioInfo(filePath); 18 | const { result, dbData } = data; 19 | if (result.status) { 20 | audioList.insert({ 21 | filePath, 22 | fileName, 23 | type: type || 'mp3', 24 | ...dbData 25 | }) 26 | } 27 | event.returnValue = { 28 | code: 200, 29 | filePath 30 | }; 31 | } 32 | }) 33 | }); 34 | ipcMain.on('delete-audio', (event, id) => { 35 | audioList.remove({ _id: id }, (err) => { 36 | if (err) { 37 | event.returnValue = false; 38 | } else { 39 | event.returnValue = true; 40 | } 41 | }) 42 | }) 43 | ipcMain.on('get-audio-list', async (event, arg) => { 44 | let { pageSize, pageNumber } = arg; 45 | const total = await new Promise((resole, reject) => { 46 | audioList.count({}, (err, n) => { 47 | if (err) { 48 | resole(0) 49 | } else { 50 | resole(n); 51 | } 52 | }) 53 | }) 54 | if (total < pageSize) { 55 | pageNumber = 1; 56 | } 57 | const skip = (pageNumber - 1) * pageSize; 58 | audioList.find({}).sort({ updatedAt: -1 }).skip(skip).limit(pageSize).exec((err, docs) => { 59 | if (err) { 60 | event.returnValue = []; 61 | } 62 | event.returnValue = { 63 | list: docs, 64 | total, 65 | }; 66 | }); 67 | }) 68 | -------------------------------------------------------------------------------- /src/background/autoUpdate.ts: -------------------------------------------------------------------------------- 1 | const autoUpdater = require('electron-updater').autoUpdater 2 | const ipcMain = require('electron').ipcMain 3 | const uploadUrl = process.env.VUE_APP_APIHOST + '/public/app/'; 4 | // 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写 5 | const UPDATE_MESSAGE_STATUS = { 6 | ERROR: 0, 7 | CHECKING: 1, 8 | UPDATE: 2, 9 | NOT_UPDATE: 3, 10 | SUCCESS: 4, 11 | DOWNLOAD: 5 12 | } 13 | 14 | // 通过main进程发送事件给renderer进程,提示更新信息 15 | function sendUpdateMessage(text: any, mainWindow: any) { 16 | mainWindow.webContents.send('message', text) 17 | } 18 | 19 | function updateHandle(mainWindow: any) { 20 | autoUpdater.setFeedURL(uploadUrl); 21 | autoUpdater.on('error', function (error: any) { 22 | sendUpdateMessage({ 23 | code: UPDATE_MESSAGE_STATUS.ERROR, 24 | message: error 25 | }, mainWindow) 26 | }); 27 | autoUpdater.on('checking-for-update', function () { 28 | sendUpdateMessage({ 29 | code: UPDATE_MESSAGE_STATUS.CHECKING 30 | }, mainWindow) 31 | }); 32 | autoUpdater.on('update-available', function (info: any) { 33 | sendUpdateMessage({ 34 | code: UPDATE_MESSAGE_STATUS.UPDATE, 35 | message: info 36 | }, mainWindow) 37 | }); 38 | autoUpdater.on('update-not-available', function (info: any) { 39 | sendUpdateMessage({ 40 | code: UPDATE_MESSAGE_STATUS.NOT_UPDATE, 41 | message: info 42 | }, mainWindow) 43 | }); 44 | 45 | autoUpdater.on('download-progress', function (progressObj: any) { 46 | mainWindow.webContents.send('downloadProgress', progressObj) 47 | }) 48 | autoUpdater.on('update-downloaded', function (...args: any) { 49 | sendUpdateMessage({ 50 | code: UPDATE_MESSAGE_STATUS.DOWNLOAD, 51 | message: args 52 | }, mainWindow); 53 | }); 54 | ipcMain.on('isUpdateNow', (args: any) => { 55 | sendUpdateMessage({ 56 | code: UPDATE_MESSAGE_STATUS.SUCCESS, 57 | message: args 58 | }, mainWindow); 59 | setTimeout(() => { 60 | autoUpdater.quitAndInstall(); 61 | }, 1000) 62 | }); 63 | 64 | ipcMain.on("checkForUpdate", () => { 65 | autoUpdater.checkForUpdates(); 66 | }); 67 | } 68 | 69 | 70 | export default updateHandle -------------------------------------------------------------------------------- /src/background/clearwater/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import db from '../database/index'; 3 | const date = require("silly-datetime"); 4 | const { videoList } = db; 5 | import { getVideoInfo, getScreenshots, saveVideo } from '../ffmpeg/helper'; 6 | ipcMain.on('get-video-info', async (event, { originUrl, url, title }) => { 7 | try { 8 | let videoData: any = await getVideoInfo(url); 9 | let { status, data: dbData, msg } = videoData || {}; 10 | if (!status) throw new Error(msg) 11 | let screenShots = await getScreenshots(url); 12 | dbData.screenShots = screenShots; 13 | dbData.status = 'start'; 14 | dbData.errorMsg = ''; 15 | dbData.originUrl = originUrl; 16 | dbData.originTitle = title || '---'; 17 | console.log(originUrl) 18 | videoList.insert(dbData, (err: any, doc: any) => { 19 | if (err) { 20 | event.sender.send('reply-video-info', { 21 | msg: err, 22 | }); 23 | } else { 24 | dbData.screenShots = screenShots; 25 | dbData.id = doc._id; 26 | dbData.progress = 0; 27 | event.sender.send('reply-video-info', { 28 | msg: '', 29 | videoInfo: dbData 30 | }); // 异步发送 31 | } 32 | }) 33 | } catch (error) { 34 | event.sender.send('reply-video-info', { 35 | msg: error.message, 36 | videoInfo: null 37 | }); 38 | } 39 | 40 | }); 41 | 42 | ipcMain.on('get-video-list', async (event, arg) => { 43 | let { pageSize, pageNumber } = arg; 44 | const total: number = await new Promise((resole, reject) => { 45 | videoList.count({}, (err: any, n: number) => { 46 | if (err) { 47 | resole(0) 48 | } else { 49 | resole(n); 50 | } 51 | }) 52 | }) 53 | if (total < pageSize) { 54 | pageNumber = 1; 55 | } 56 | const skip = (pageNumber - 1) * pageSize; 57 | videoList.find({}).sort({ updatedAt: -1 }).skip(skip).limit(pageSize).exec((err: any, docs: any) => { 58 | if (err) { 59 | event.returnValue = []; 60 | } 61 | event.returnValue = { 62 | list: docs, 63 | total, 64 | }; 65 | }); 66 | }) 67 | 68 | ipcMain.on('delete-video', (event, id) => { 69 | videoList.remove({ _id: id }, (err: any) => { 70 | if (err) { 71 | event.returnValue = false; 72 | } else { 73 | event.returnValue = true; 74 | } 75 | }) 76 | }) 77 | ipcMain.on('save-video', (event, arg) => { 78 | const { id, videosize = '', videotype } = arg; 79 | const size = videosize.split('|') 80 | const convertObj = { 81 | convertType: videotype, 82 | convertWidth: Number(size && size[0]), 83 | convertHeight: Number(size && size[1]), 84 | } 85 | saveVideo(arg, { 86 | start(data: any) { 87 | event.sender.send('reply-save-video', { 88 | tag: "start", 89 | msg: '视频转换开始....', 90 | data: { 91 | filePath: data.filePath, 92 | idx: id 93 | } 94 | }); 95 | }, 96 | end(data: any) { 97 | videoList.update({ _id: id }, { 98 | $set: { 99 | ...convertObj, 100 | updateDate: date.format(new Date(), 'YYYY-MM-DD HH:mm:ss'), 101 | filePath: data.filePath, 102 | status: 'success', 103 | progress: 100 104 | } 105 | }); 106 | event.sender.send('reply-save-video', { 107 | tag: "end", 108 | msg: '视频转换完成', 109 | data: { 110 | ...convertObj, 111 | filePath: data.filePath, 112 | idx: id 113 | } 114 | }); 115 | }, 116 | progress(data: any) { 117 | event.sender.send('reply-save-video', { 118 | tag: "progress", data: { 119 | ...convertObj, 120 | percent: data.percent, 121 | filePath: data.filePath, 122 | idx: id 123 | } 124 | }); 125 | }, 126 | error(data: any) { 127 | 128 | videoList.update({ _id: id }, { $set: { filePath: data.filePath, errorMsg: data.err.message, status: 'error', progress: 0 } }); 129 | event.sender.send('reply-save-video', { 130 | tag: "error", 131 | msg: data.err.message, 132 | data: { 133 | ...convertObj, 134 | err: data.err, 135 | filePath: data.filePath, 136 | idx: id 137 | } 138 | }); 139 | } 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/background/database/index.ts: -------------------------------------------------------------------------------- 1 | import Datastore from 'nedb' 2 | import path from 'path' 3 | import { app } from 'electron' 4 | const filePath = app.getPath('userData'); 5 | const timestampData = true; 6 | const db = { 7 | audioList: new Datastore({ filename: path.join(filePath, '/audio.db'), timestampData, autoload: true }), 8 | videoList: new Datastore({ filename: path.join(filePath, '/videoList.db'), timestampData, autoload: true }), 9 | editVideoList: new Datastore({ filename: path.join(filePath, '/editVideoList.db'), timestampData, autoload: true }) 10 | } 11 | 12 | export default db; -------------------------------------------------------------------------------- /src/background/edit-video/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import db from '../database/index'; 3 | const date = require("silly-datetime"); 4 | const { editVideoList } = db; 5 | import { cutVideo, concatVideo } from '../ffmpeg/helper'; 6 | 7 | ipcMain.on('cut-video', async (event, arg) => { 8 | const { url, times } = arg; 9 | const filePaths = await Promise.all(times.map(async ({ start, end }, index) => { 10 | return new Promise((resolve, reject) => { 11 | cutVideo({ url, start, end }, event, { 12 | start({ filePath }) { 13 | event.sender.send('reply-cut-video', { status: 'start', filePath }) 14 | }, 15 | end({ filePath }) { 16 | event.sender.send('reply-cut-video', { status: 'end', filePath }); 17 | resolve(filePath) 18 | }, 19 | progress({ filePath, percent }) { 20 | event.sender.send('reply-cut-video', { status: 'ing', filePath, percent }) 21 | }, 22 | error({ err, filePath }) { 23 | event.sender.send('reply-cut-video', { status: 'error', error: err, filePath }) 24 | } 25 | }); 26 | }) 27 | })); 28 | console.log(2222) 29 | concatVideo(filePaths, percent => { 30 | event.sender.send('reply-cut-video', { status: 'concat-ing', filePath: '', percent: percent * 100 }) 31 | }).then(path => { 32 | event.sender.send('reply-cut-video', { status: 'concat-end', filePath: path }); 33 | }, err => { 34 | event.sender.send('reply-cut-video', { status: 'concat-error', error: err, filePath: null }) 35 | }) 36 | }); 37 | -------------------------------------------------------------------------------- /src/background/ffmpeg/audio/getAudioInfo.ts: -------------------------------------------------------------------------------- 1 | import ffmpeg from '../ffmpeg'; 2 | import formatBytes from '../utils/formatBytes'; 3 | import formatSeconds from '../utils/formatSeconds'; 4 | const date = require("silly-datetime"); 5 | 6 | 7 | export default function (videoUrl: string) { 8 | let result = { status: false, msg: '获取失败', data: {} }; 9 | let da = {}; 10 | return new Promise((resolve, reject) => { 11 | ffmpeg(videoUrl).ffprobe((err, data) => { 12 | if (err) { 13 | reject(new Error('获取视频信息失败,请检查视频链接是否可访问')); 14 | } else { 15 | const { format } = data; 16 | let { format_long_name, duration, size } = format; 17 | duration = formatSeconds(duration); 18 | size = formatBytes(size); 19 | let adddate = date.format(new Date(), 'YYYY-MM-DD HH:mm:ss'); 20 | da = { format_long_name, duration, size, date: adddate, url: videoUrl }; 21 | result = { status: true, msg: '处理成功', data: da }; 22 | } 23 | resolve({ 24 | result, 25 | dbData: da 26 | }) 27 | }); 28 | }) 29 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/audio/saveAudio.ts: -------------------------------------------------------------------------------- 1 | import ffmpeg, { fileDataDir } from '../ffmpeg'; 2 | import { noop } from 'lodash' 3 | const md5 = require('md5-node'); 4 | const fs = require('fs') 5 | function fileExist(path) { 6 | try { 7 | fs.statSync(path); 8 | return true; 9 | } catch (e) { 10 | //捕获异常 11 | return false; 12 | } 13 | } 14 | export default function ({ url, type, fileName: name }, { 15 | start = noop, 16 | end = noop, 17 | progress = noop, 18 | error = noop 19 | }) { 20 | let fileName = name || md5(url); 21 | const ext = type || 'mp3'; 22 | let file = `${fileDataDir}/${fileName}.${ext}`; 23 | if (fileExist(file)) { 24 | fileName = `${fileName}-${(new Date().getTime()) % 1000000}`; 25 | file = `${fileDataDir}/${fileName}.${ext}`; 26 | } 27 | ffmpeg(url).output(file).on('start', (commandLine) => { 28 | start({ 29 | fileName, 30 | filePath: file 31 | }) 32 | }).on('end', () => { 33 | end({ 34 | fileName, 35 | filePath: file 36 | }); // 异步发送 37 | }).on('progress', (p) => { 38 | progress({ 39 | filePath: file, 40 | percent: p.percent 41 | }); // 异步发送 42 | }).on('error', (err, stdout, stderr) => { 43 | error({ 44 | err, 45 | fileName, 46 | filePath: file 47 | }); // 异步发送 48 | }) 49 | .run(); 50 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/ffmpeg.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | 3 | 4 | const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg'); 5 | // const ffprobeInstaller = require('@ffprobe-installer/ffprobe'); 6 | const ffmpeg = require('fluent-ffmpeg'); 7 | const path = require('path'); 8 | const fs = require('fs'); 9 | const date = require("silly-datetime"); 10 | let ffmpegPath = ffmpegInstaller.path; 11 | ffmpeg.setFfmpegPath(ffmpegPath); 12 | const curDate = date.format(new Date(), 'YYYYMMDD'); 13 | // 设置目录 14 | const fileTmpDir = path.resolve(path.basename(__dirname), '../', 'tmp'); 15 | const fileBaseDataDir = path.resolve(path.basename(__dirname), '../', 'data'); 16 | const fileDataDir = path.resolve(fileBaseDataDir, curDate); 17 | 18 | 19 | function fsExistsSync(paths: string[]) { 20 | try { 21 | fs.statSync(paths); 22 | } catch (error) { 23 | fs.mkdir(paths, (err, data) => { 24 | }); 25 | } 26 | } 27 | fsExistsSync(fileBaseDataDir); 28 | fsExistsSync(fileTmpDir); // 创建临时目录 29 | fsExistsSync(fileDataDir); // 创建文件目录 30 | 31 | 32 | export default ffmpeg; 33 | 34 | export { 35 | fileTmpDir, 36 | fileBaseDataDir, 37 | fileDataDir 38 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import getVideoInfo from './video/getVideoInfo'; 3 | import getScreenshots from './video/getScreenshots'; 4 | import saveVideo from './video/saveVideo'; 5 | import cutVideo from './video/cutVideo'; 6 | import concatVideo from './video/concatVideo' 7 | // audio 8 | import getAudioInfo from './audio/getAudioInfo'; 9 | import saveAudio from './audio/saveAudio'; 10 | 11 | 12 | export { 13 | getVideoInfo, 14 | getScreenshots, 15 | saveVideo, 16 | concatVideo, 17 | cutVideo, 18 | saveAudio, 19 | getAudioInfo 20 | } 21 | -------------------------------------------------------------------------------- /src/background/ffmpeg/utils/formatBytes.ts: -------------------------------------------------------------------------------- 1 | export default function formatBytes(bytes, decimals) { 2 | console.log("bytes", bytes); 3 | if (bytes === 0) return '0 Bytes'; 4 | if (bytes === NaN || bytes === undefined) return '0 Bytes'; 5 | if (bytes === "N/A") return '* Bytes'; 6 | const k = 1000, 7 | dm = decimals + 1 || 3, 8 | sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 9 | i = Math.floor(Math.log(bytes) / Math.log(k)); 10 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/background/ffmpeg/utils/formatSeconds.ts: -------------------------------------------------------------------------------- 1 | export default function formatSeconds(value) { 2 | let theTime = parseInt(value); 3 | let theTime1 = 0; 4 | let theTime2 = 0; 5 | 6 | if (theTime >= 60) { 7 | theTime1 = parseInt(theTime / 60); 8 | theTime = parseInt(theTime % 60); 9 | if (theTime1 >= 60) { 10 | theTime2 = parseInt(theTime1 / 60); 11 | theTime1 = parseInt(theTime1 % 60); 12 | } 13 | } 14 | if (theTime < 10) { 15 | theTime = `0${parseInt(theTime)}`; 16 | } 17 | let result = `${theTime}`; 18 | if (theTime1 >= 0) { 19 | if (theTime1 < 10) { 20 | theTime1 = `0${parseInt(theTime1)}`; 21 | } 22 | result = `${theTime1}:${result}`; 23 | } 24 | if (theTime2 >= 0) { 25 | if (theTime2 < 10) { 26 | theTime2 = `0${parseInt(theTime2)}`; 27 | } 28 | result = `${theTime2}:${result}`; 29 | } 30 | return result; 31 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/video/concatVideo.ts: -------------------------------------------------------------------------------- 1 | import ffmpeg, { fileDataDir } from '../ffmpeg'; 2 | import { isArray } from 'lodash'; 3 | const concat = require('ffmpeg-concat') 4 | const md5 = require('md5-node'); 5 | 6 | export default function (filePaths,onProgress) { 7 | if (!isArray(filePaths)) return []; 8 | const fileName = md5(new Date().getTime()); 9 | const outputPath = `${fileDataDir}/${fileName}.mp4`; 10 | return new Promise(async (resolve, reject) => { 11 | try { 12 | concat({ 13 | output: outputPath, 14 | videos: filePaths, 15 | transition: { 16 | name: 'directionalWipe', 17 | duration: 500 18 | }, 19 | onProgress: onProgress || function(p){ 20 | console.log(p) 21 | } 22 | }).then(() => { 23 | resolve(outputPath) 24 | }) 25 | } catch (error) { 26 | reject(error); 27 | } 28 | }) 29 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/video/cutVideo.ts: -------------------------------------------------------------------------------- 1 | 2 | import ffmpeg, { fileTmpDir } from '../ffmpeg'; 3 | import { noop } from 'lodash'; 4 | const md5 = require('md5-node'); 5 | 6 | export default function (options = {}, event, { 7 | start = noop, 8 | end = noop, 9 | progress = noop, 10 | error = noop 11 | }) { 12 | const { url, start: startTime, end: endTime, ext = 'mp4' } = options; 13 | console.log(startTime, endTime) 14 | const fileName = md5(new Date().getTime() + url); 15 | const filePath = `${fileTmpDir}/${fileName}.${ext}`; 16 | ffmpeg(url).output(filePath) 17 | .setStartTime(startTime) 18 | .setDuration(endTime - startTime) 19 | .on('start', (commandLine) => { 20 | console.log('cutVideo========', commandLine) 21 | start({ 22 | filePath: filePath 23 | }) 24 | }) 25 | .on('end', () => { 26 | end({ 27 | filePath: filePath 28 | }); // 异步发送 29 | }).on('progress', (p) => { 30 | progress({ 31 | filePath: filePath, 32 | percent: p.percent 33 | }); // 异步发送 34 | }).on('error', (err, stdout, stderr) => { 35 | error({ 36 | err, 37 | filePath: filePath 38 | }); // 异步发送 39 | }) 40 | .run(); 41 | } 42 | -------------------------------------------------------------------------------- /src/background/ffmpeg/video/getScreenshots.ts: -------------------------------------------------------------------------------- 1 | 2 | import ffmpeg, { fileDataDir } from '../ffmpeg'; 3 | const md5 = require('md5-node'); 4 | const path = require('path') 5 | export default function (videoUrl: string) { 6 | let key = md5(videoUrl); 7 | return new Promise((resolve, reject) => { 8 | ffmpeg(videoUrl).screenshots({ 9 | timestamps: ['50%'], 10 | filename: key + '-%w.png', 11 | folder: fileDataDir, 12 | size: '320x240', 13 | }).on('end', function () { 14 | let pic = path.resolve(fileDataDir, key + "-320.png") 15 | // let status = "start"; 16 | // let status_msg = "等待开始"; 17 | resolve(pic) 18 | }).on('error', (error: any) => { 19 | reject(new Error('获取视频封面失败:' + error)); 20 | }) 21 | }); 22 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/video/getVideoInfo.ts: -------------------------------------------------------------------------------- 1 | 2 | import ffmpeg from '../ffmpeg'; 3 | import formatBytes from '../utils/formatBytes'; 4 | import formatSeconds from '../utils/formatSeconds'; 5 | const date = require("silly-datetime"); 6 | const md5 = require('md5-node'); 7 | 8 | export default function (videoUrl:string) { 9 | let result = { status: false, msg: '获取失败', data: {} }; 10 | let da = {}; 11 | return new Promise((resolve, reject) => { 12 | ffmpeg(videoUrl).ffprobe((err, data) => { 13 | if (err) { 14 | console.log(err) 15 | reject(new Error(err)); 16 | } else { 17 | const { streams, format } = data; 18 | const { width, height } = streams[0]; 19 | let { filename, format_long_name, duration, size } = format; 20 | let key = md5(filename); 21 | filename = key; 22 | duration = formatSeconds(duration); 23 | size = formatBytes(size); 24 | let adddate = date.format(new Date(), 'YYYY-MM-DD HH:mm:ss'); 25 | da = { width, height, filename, format_long_name, duration, size, date: adddate, url: videoUrl }; 26 | result = { status: true, msg: '处理成功', data: da }; 27 | } 28 | resolve(result) 29 | }); 30 | }) 31 | } -------------------------------------------------------------------------------- /src/background/ffmpeg/video/saveVideo.ts: -------------------------------------------------------------------------------- 1 | import ffmpeg, { 2 | fileDataDir, 3 | } from '../ffmpeg'; 4 | 5 | import { noop } from 'lodash' 6 | const md5 = require('md5-node'); 7 | export default function (arg: any, { 8 | start = noop, 9 | end = noop, 10 | progress = noop, 11 | error = noop 12 | }) { 13 | let { videotype, videosize, url, id } = arg; 14 | const fileName = md5(url); 15 | const ext = (videotype || 'mp4'); 16 | const size = (videosize.replace("|", "x") || '320x240'); 17 | const file = `${fileDataDir}/${fileName}.${ext}`; 18 | ffmpeg(url).output(file).audioCodec('libmp3lame').size(size) 19 | .videoCodec('libx264') 20 | .on('start', (commandLine) => { 21 | console.log('saveVideo=====: ', commandLine) 22 | start({ 23 | filePath: file 24 | }) 25 | }).on('end', () => { 26 | end({ 27 | filePath: file 28 | }); // 异步发送 29 | }).on('progress', (p) => { 30 | progress({ 31 | filePath: file, 32 | percent: p.percent 33 | }); // 异步发送 34 | }).on('error', (err, stdout, stderr) => { 35 | error({ 36 | err, 37 | filePath: file 38 | }); // 异步发送 39 | }) 40 | .run(); 41 | } -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import './clearwater'; 2 | import './edit-video/index'; 3 | import './audio/index' 4 | import './terminal/index' -------------------------------------------------------------------------------- /src/background/terminal/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import { wait } from '../utils'; 3 | const exec = require('child-process-promise').exec; 4 | const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg'); 5 | const command = (cmd: string) => { 6 | return new Promise((resolve, reject) => { 7 | exec(cmd) 8 | .then(function (result: any) { 9 | resolve(result) 10 | }) 11 | .catch(function (err: any) { 12 | console.error('ERROR: ', err); 13 | reject(err); 14 | }); 15 | }) 16 | } 17 | ipcMain.on('terminal-cmd', (event: any, arg: string) => { 18 | let cmd = (arg || '').replace('ffmpeg', ffmpegInstaller.path); 19 | Promise.race([wait(300000), command(cmd)]).then((message: any) => { 20 | message.cmd = cmd; 21 | event.sender.send('terminal-success', message); 22 | }).catch((message: any) => { 23 | message.cmd = cmd; 24 | event.sender.send('terminal-error', message); 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/background/utils.ts: -------------------------------------------------------------------------------- 1 | export const wait = async (time: number) => { 2 | return await new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(true) 5 | }, time); 6 | }) 7 | } -------------------------------------------------------------------------------- /src/components/Permission/index.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/components/Result/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 57 | -------------------------------------------------------------------------------- /src/components/Result/svg/error.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/info.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/noFound.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/serverError.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/success.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/unauthorized.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Result/svg/warning.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/clearwater.ts: -------------------------------------------------------------------------------- 1 | const statusText = { 2 | ing: "转换中", 3 | start: "未开始", 4 | success: "已完成", 5 | error: "转换失败", 6 | }; 7 | const progressStatus = { 8 | ing: "exception", 9 | start: "exception", 10 | success: "success", 11 | error: "warning", 12 | }; 13 | const vSizes = { 14 | '320|240': '320x240', 15 | '640|400': '640x400', 16 | '720|480': '720x480', 17 | '720|576': '720x576', 18 | '1280|720': '1280x720', 19 | '1920|1080': '1920x1080', 20 | } 21 | 22 | export { 23 | statusText, 24 | progressStatus, 25 | vSizes 26 | } -------------------------------------------------------------------------------- /src/config/languageList.ts: -------------------------------------------------------------------------------- 1 | const languageList = [ 2 | { 3 | name: '中英文混合', 4 | value: 'CHN_ENG' 5 | }, 6 | { 7 | name: '英文', 8 | value: 'ENG' 9 | }, 10 | { 11 | name: '葡萄牙语', 12 | value: 'POR' 13 | }, 14 | { 15 | name: '法语', 16 | value: 'FRE' 17 | }, 18 | { 19 | name: '德语', 20 | value: 'GER' 21 | }, 22 | { 23 | name: '意大利语', 24 | value: 'ITA' 25 | }, 26 | { 27 | name: '日语', 28 | value: '日语' 29 | }, 30 | { 31 | name: '韩语', 32 | value: 'KOR' 33 | }, 34 | { 35 | name: '俄语', 36 | value: 'RUS' 37 | } 38 | ]; 39 | 40 | export default languageList; -------------------------------------------------------------------------------- /src/config/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 站点配置 3 | * @author LinQian 4 | */ 5 | export interface SettingsType { 6 | /** 7 | * 站点名称 8 | */ 9 | siteTitle: string; 10 | 11 | /** 12 | * 顶部菜单开启 13 | */ 14 | topNavEnable: boolean; 15 | 16 | /** 17 | * 头部固定开启 18 | */ 19 | headFixed: boolean; 20 | 21 | /** 22 | * 站点本地存储Token 的 Key值 23 | */ 24 | siteTokenKey: string; 25 | 26 | /** 27 | * Ajax请求头发送Token 的 Key值 28 | */ 29 | ajaxHeadersTokenKey: string; 30 | 31 | /** 32 | * Ajax返回值不参加统一验证的api地址 33 | */ 34 | ajaxResponseNoVerifyUrl: string[]; 35 | 36 | /** 37 | * iconfont.cn 项目在线生成的 js 地址 38 | */ 39 | iconfontUrl: string[]; 40 | } 41 | 42 | const settings: SettingsType = { 43 | siteTitle: 'ADMIN-ELEMENT-VUE', 44 | topNavEnable: true, 45 | headFixed: true, 46 | siteTokenKey: 'token', 47 | ajaxHeadersTokenKey: 'authorization', 48 | ajaxResponseNoVerifyUrl: [ 49 | '/user/login', // 用户登录 50 | '/user/info', // 获取用户信息 51 | ], 52 | iconfontUrl: [], 53 | }; 54 | 55 | export default settings; 56 | -------------------------------------------------------------------------------- /src/config/videoConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | const BaiDu = [ 3 | { 4 | name: '情感男声', 5 | value: 3 6 | }, 7 | { 8 | name: '情感女声', 9 | value: 4 10 | }, 11 | { 12 | name: '标准女声', 13 | value: 0 14 | }, 15 | { 16 | name: '标准男声', 17 | value: 1 18 | }, 19 | 20 | ]; 21 | 22 | 23 | const TengXun = [ 24 | { 25 | name: '智侠(男声)', 26 | value: 1000 27 | }, 28 | { 29 | name: '智瑜(女声)', 30 | value: 1001 31 | }, 32 | { 33 | name: '智聆(女声)', 34 | value: 1002 35 | }, 36 | { 37 | name: '智美(客服女声)', 38 | value: 1003 39 | }, 40 | { 41 | name: 'WeJack(英文男声)', 42 | value: 1050 43 | }, 44 | { 45 | name: 'WeRose(英文女声)', 46 | value: 1051 47 | }, 48 | { 49 | name: '云小宁', 50 | value: 1 51 | }, 52 | { 53 | name: '云小晚', 54 | value: 2 55 | }, 56 | { 57 | name: '云小叶', 58 | value: 4 59 | }, 60 | { 61 | name: '云小欣', 62 | value: 5 63 | }, 64 | { 65 | name: '云小龙', 66 | value: 6 67 | }, 68 | { 69 | name: '云小曼', 70 | value: 7 71 | }, 72 | ]; 73 | const XunFei = [ 74 | { 75 | name: '讯飞小燕', 76 | value: 'xiaoyan' 77 | }, 78 | { 79 | name: '讯飞许久', 80 | value: 'aisjiuxu' 81 | }, 82 | { 83 | name: '讯飞小萍', 84 | value: 'aisxping' 85 | }, 86 | { 87 | name: '讯飞小婧', 88 | value: 'aisjinger' 89 | }, 90 | { 91 | name: '讯飞许小宝', 92 | value: 'aisbabyxu' 93 | } 94 | ] 95 | 96 | 97 | interface VoiceConfig { 98 | [k: string]: any 99 | } 100 | const voiceConfig :VoiceConfig = { 101 | voice: [ 102 | { 103 | name: '腾讯语音', 104 | value: 'tengxun', 105 | }, 106 | { 107 | name: '讯飞语音', 108 | value: 'xunfei' 109 | }, 110 | { 111 | name: '百度语音', 112 | value: 'baidu', 113 | }, 114 | ], 115 | volume: { 116 | baidu: { 117 | min: 1, 118 | max: 15, 119 | default: 5 120 | }, 121 | tengxun: { 122 | min: 0, 123 | max: 10, 124 | default: 2 125 | }, 126 | xunfei: { 127 | min: 1, 128 | max: 100, 129 | default: 50 130 | } 131 | }, 132 | speed: { 133 | baidu: { 134 | min: 0, 135 | max: 9, 136 | default: 5 137 | }, 138 | tengxun: { 139 | min: -2, 140 | max: 2, 141 | default: 0 142 | }, 143 | xunfei: { 144 | min: 1, 145 | max: 100, 146 | default: 50 147 | } 148 | }, 149 | pitch: { 150 | baidu: { 151 | min: 0, 152 | max: 9, 153 | default: 5 154 | }, 155 | tengxun: { 156 | min: -2, 157 | max: 2, 158 | default: 0 159 | }, 160 | xunfei: { 161 | min: 1, 162 | max: 100, 163 | default: 50 164 | } 165 | }, 166 | per: { 167 | baidu: 3, 168 | tengxun: 1000, 169 | xunfei: 'xiaoyan' 170 | }, 171 | baidu: BaiDu, 172 | tengxun: TengXun, 173 | xunfei: XunFei 174 | }; 175 | 176 | export { 177 | BaiDu, 178 | TengXun, 179 | XunFei, 180 | voiceConfig 181 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | 3 | // 样式初始化 4 | import 'normalize.css'; 5 | 6 | // 全局样式 7 | import '@/assets/css/global.scss'; 8 | 9 | // 引入 ElementUI 10 | import ElementPlus from 'element-plus'; 11 | 12 | 13 | import App from '@/App.vue'; 14 | import router from '@/router'; 15 | import store from '@/store'; 16 | import packageJSon from '../package.json'; 17 | localStorage.version = packageJSon.version; 18 | const app = createApp(App) 19 | app.use(store); 20 | app.use(router) 21 | app.use(ElementPlus, { size: 'small' }); 22 | app.mount('#app'); -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createRouter, createWebHashHistory } from 'vue-router'; 3 | const routes: Array = [ 4 | { 5 | title: '首页', 6 | path: '/', 7 | redirect: '/home', 8 | component: () => import('@/views/index.vue'), 9 | children: [ 10 | { 11 | title: '首页', 12 | path: '/home', 13 | component: () => import('@/views/home/index.vue') 14 | }, 15 | { 16 | title: '去水印', 17 | path: '/clearwater', 18 | component: () => import('@/views/clearwater/index.vue') 19 | }, 20 | { 21 | title: '语音合成', 22 | path: '/audio', 23 | component: () => import('@/views/audio/index.vue') 24 | }, 25 | { 26 | title: '权重查询', 27 | path: '/weight', 28 | component: () => import('@/views/weight/index.vue') 29 | }, 30 | { 31 | title: '编辑视频', 32 | path: '/discern', 33 | component: () => import('@/views/discern/index.vue') 34 | }, 35 | { 36 | title: '登录', 37 | path: '/login', 38 | component: () => import('@/views/login/index.vue'), 39 | }, 40 | { 41 | title: '终端', 42 | path: '/terminal', 43 | component: () => import('@/views/terminal/index.vue') 44 | }, 45 | ] 46 | }, 47 | 48 | { 49 | title: '404', 50 | path: '/:pathMatch(.*)*', 51 | component: () => import('@/views/404/index.vue'), 52 | } 53 | ] 54 | 55 | const router = createRouter({ 56 | scrollBehavior(/* to, from, savedPosition */) { 57 | return { top: 0 } 58 | }, 59 | history: createWebHashHistory(process.env.BASE_URL), 60 | routes: routes as any, 61 | }); 62 | 63 | export default router; 64 | -------------------------------------------------------------------------------- /src/services/audio.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function getAudio(data: any): Promise { 4 | return request({ 5 | url: '/audio/create', 6 | method: 'post', 7 | data: data 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/services/clearwater.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function dspUrl(url: string): Promise { 4 | return request({ 5 | url: '/dspUrl', 6 | method: 'post', 7 | data: { 8 | url 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/services/discern.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function discern(data: any): Promise { 4 | return request({ 5 | url: '/image/discern', 6 | method: 'post', 7 | data: data 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/services/indexData.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function getIndexData(): Promise { 4 | return request({ 5 | url: '/indexData', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/services/request.ts: -------------------------------------------------------------------------------- 1 | 2 | import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import { ElNotification } from 'element-plus'; 4 | import router from '@/router'; 5 | import settings from '@/config/settings'; 6 | import { getToken, setToken } from '@/utils/localToken'; 7 | export interface ResponseData { 8 | code?: number; 9 | data?: any; 10 | msg?: string; 11 | token?: string; 12 | } 13 | 14 | export const baseURL = process.env.VUE_APP_APIHOST; 15 | 16 | const customCodeMessage: { [key: number]: string } = { 17 | 10002: '当前用户登入信息已失效,请重新登入再操作', // 未登陆 18 | }; 19 | 20 | const serverCodeMessage: { [key: number]: string } = { 21 | 200: '服务器成功返回请求的数据', 22 | 400: 'Bad Request', 23 | 401: 'Unauthorized', 24 | 403: 'Forbidden', 25 | 404: 'Not Found', 26 | 500: '服务器发生错误,请检查服务器(Internal Server Error)', 27 | 502: '网关错误(Bad Gateway)', 28 | 503: '服务不可用,服务器暂时过载或维护(Service Unavailable)', 29 | 504: '网关超时(Gateway Timeout)', 30 | }; 31 | 32 | /** 33 | * 异常处理程序 34 | */ 35 | const errorHandler = (error: any) => { 36 | const { response, message } = error; 37 | if (message === 'CustomError') { 38 | // 自定义错误 39 | const { config, data } = response; 40 | const { url, baseURL } = config; 41 | const { code, msg } = data; 42 | const reqUrl = url.split("?")[0].replace(baseURL, ''); 43 | const noVerifyBool = settings.ajaxResponseNoVerifyUrl.includes(reqUrl); 44 | if (!noVerifyBool) { 45 | ElNotification({ 46 | type: 'error', 47 | title: `提示`, 48 | message: customCodeMessage[code] || msg || 'Error', 49 | }); 50 | 51 | if (code === 10002) { 52 | router.replace('/user/login'); 53 | } 54 | } 55 | } else if (message === 'CancelToken') { 56 | // 取消请求 Token 57 | // eslint-disable-next-line no-console 58 | console.log(message); 59 | } else if (response && response.status) { 60 | const errorText = serverCodeMessage[response.status] || response.statusText; 61 | const { status, request, data = {}, config } = response; 62 | if (status != 403) { 63 | ElNotification({ 64 | type: 'error', 65 | title: `请求错误 ${status}: ${errorText}`, 66 | message: data.message || data.msg || errorText, 67 | }); 68 | } 69 | if (status == 403 && config.url == '/login') { 70 | ElNotification({ 71 | type: 'error', 72 | title: `登录失败 ${status}`, 73 | message: data.msg || errorText, 74 | duration: 3000 75 | }); 76 | } 77 | } else if (!response) { 78 | ElNotification({ 79 | type: 'error', 80 | title: '网络异常', 81 | message: '您的网络发生异常,无法连接服务器', 82 | }); 83 | } 84 | 85 | return Promise.reject(error); 86 | } 87 | 88 | /** 89 | * 配置request请求时的默认参数 90 | */ 91 | 92 | const request = axios.create({ 93 | baseURL, // url = api url + request url 94 | // withCredentials: true, // 当跨域请求时发送cookie 95 | timeout: 40000 // 请求超时时间,5000(单位毫秒) / 0 不做限制 96 | }); 97 | 98 | // 全局设置 - post请求头 99 | request.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; 100 | 101 | /** 102 | * 请求前 103 | * 请求拦截器 104 | */ 105 | request.interceptors.request.use( 106 | async (config: AxiosRequestConfig & { cType?: boolean }) => { 107 | 108 | // 如果设置了cType 说明是自定义 添加 Content-Type类型 为自定义post 做铺垫 109 | // if (config['cType']) { 110 | // config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; 111 | // } 112 | 113 | // 自定义添加token header 114 | const token = await getToken(); 115 | if (!token && config.url != '/login' && config.url != '/indexData') { 116 | ElNotification({ 117 | type: 'warning', 118 | title: '提示', 119 | message: '请先登录', 120 | duration: 1000, 121 | onClose() { 122 | router.push('/login'); 123 | } 124 | }) 125 | return config 126 | } 127 | 128 | config.headers[settings.ajaxHeadersTokenKey] = 'Bearer ' + await getToken(); 129 | 130 | return config; 131 | }, 132 | /* error=> {} */ // 已在 export default catch 133 | ); 134 | 135 | /** 136 | * 请求后 137 | * 响应拦截器 138 | */ 139 | request.interceptors.response.use( 140 | async (response: AxiosResponse) => { 141 | 142 | const res: ResponseData = response.data; 143 | const { code, token } = res; 144 | 145 | // 重置刷新token 146 | if (token) { 147 | await setToken(token); 148 | } 149 | 150 | return response; 151 | }, 152 | /* error => {} */ // 已在 export default catch 153 | ); 154 | 155 | /** 156 | * ajax 导出 157 | * 158 | * Method: get 159 | * Request Headers 160 | * 无 - Content-Type 161 | * Query String Parameters 162 | * name: name 163 | * age: age 164 | * 165 | * Method: post 166 | * Request Headers 167 | * Content-Type:application/json;charset=UTF-8 168 | * Request Payload 169 | * { name: name, age: age } 170 | * Custom config parameters 171 | * { cType: true } Mandatory Settings Content-Type:application/json;charset=UTF-8 172 | * ...... 173 | */ 174 | export default function (config: AxiosRequestConfig): AxiosPromise { 175 | return request(config).then((response: AxiosResponse) => response.data).catch(error => errorHandler(error)); 176 | } 177 | -------------------------------------------------------------------------------- /src/services/user.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function login(code: string, password?: string): Promise { 4 | return request({ 5 | url: '/login', 6 | method: 'post', 7 | data: { 8 | code, 9 | password 10 | } 11 | }); 12 | } 13 | 14 | export async function queryMessage(): Promise { 15 | return request({ 16 | url: '/user/message' 17 | }); 18 | } -------------------------------------------------------------------------------- /src/services/weight.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export async function getWeight(data: any): Promise { 4 | return request({ 5 | url: '/getWeight', 6 | method: 'post', 7 | data: data 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | declare module '@ckeditor/ckeditor5-vue'; 8 | declare module '@ckeditor/ckeditor5-build-decoupled-document'; 9 | declare module "vue-terminal" { 10 | import VueTerminal from 'vue-terminal'; 11 | export default VueTerminal; 12 | }; 13 | declare module '*.json' { 14 | const value: any; 15 | export default value; 16 | } -------------------------------------------------------------------------------- /src/store/global.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Mutation/* , Action */ } from 'vuex'; 3 | 4 | 5 | 6 | export default {}; 7 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import createPersistedState from "vuex-persistedstate"; 3 | import userModel from './user'; 4 | const store = createStore({ 5 | modules: { 6 | user: userModel 7 | }, 8 | plugins: [createPersistedState()] 9 | }) 10 | 11 | export default store; -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { login, queryMessage } from "@/services/user"; 3 | import { removeToken } from "@/utils/localToken"; 4 | import { stat } from 'fs'; 5 | 6 | export interface UserState { 7 | code: string | number; 8 | id: string; 9 | token: string; 10 | name?: string; 11 | status?: string 12 | } 13 | 14 | 15 | export interface ModuleType { 16 | namespaced?: boolean, 17 | name: string, 18 | state: UserState; 19 | mutations: { 20 | saveUser: Mutation; 21 | }; 22 | actions: { 23 | login: Action; 24 | logout: Action; 25 | }; 26 | } 27 | 28 | const initState: UserState = { 29 | code: 0, 30 | name: '', 31 | id: '', 32 | token: '', 33 | } 34 | 35 | const StoreModel: ModuleType = { 36 | namespaced: true, 37 | name: 'user', 38 | state: { 39 | ...initState 40 | }, 41 | mutations: { 42 | saveUser(state, { code, name, id, token }) { 43 | state.code = code || initState.code; 44 | state.token = token || initState.token; 45 | state.id = id || initState.id; 46 | state.name = name || initState.name; 47 | 48 | } 49 | }, 50 | actions: { 51 | async login({ commit, state }, playload) { 52 | try { 53 | const { code, password } = playload; 54 | const response = await login(code, password); 55 | commit('saveUser', { 56 | code: response.code, 57 | id: response.id, 58 | token: response.token, 59 | status: response.status 60 | }); 61 | return true; 62 | } catch (error) { 63 | return false; 64 | } 65 | }, 66 | async logout({ commit }) { 67 | try { 68 | await removeToken(); 69 | commit('saveUser', { ...initState }); 70 | return true; 71 | } catch (error) { 72 | return false; 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | 80 | export default StoreModel; 81 | -------------------------------------------------------------------------------- /src/utils/localToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义 token 操作 3 | * @author LinQian 4 | */ 5 | import localforage from 'localforage'; 6 | import settings from '@/config/settings'; 7 | 8 | /** 9 | * 获取本地Token 10 | */ 11 | export const getToken = async (): Promise => { 12 | return await localforage.getItem(settings.siteTokenKey); 13 | }; 14 | 15 | /** 16 | * 设置存储本地Token 17 | */ 18 | export const setToken = async (token: string): Promise => { 19 | try { 20 | await localforage.setItem(settings.siteTokenKey, token); 21 | return true; 22 | } catch (error) { 23 | return false; 24 | } 25 | }; 26 | 27 | /** 28 | * 移除本地Token 29 | */ 30 | export const removeToken = async (): Promise => { 31 | try { 32 | await localforage.removeItem(settings.siteTokenKey); 33 | return true; 34 | } catch (error) { 35 | return false; 36 | } 37 | }; -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否是外链 3 | * @param {string} path 4 | * @returns {Boolean} 5 | * @author LinQian 6 | */ 7 | export const isExternal = (path: string): boolean => { 8 | return /^(https?:|mailto:|tel:)/.test(path); 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/views/audio/config.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 97 | -------------------------------------------------------------------------------- /src/views/audio/history.vue: -------------------------------------------------------------------------------- 1 | 58 | 128 | -------------------------------------------------------------------------------- /src/views/audio/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 120 | -------------------------------------------------------------------------------- /src/views/audio/play.vue: -------------------------------------------------------------------------------- 1 | 17 | 55 | -------------------------------------------------------------------------------- /src/views/audio/textarea.vue: -------------------------------------------------------------------------------- 1 | 13 | 25 | -------------------------------------------------------------------------------- /src/views/clearwater/config.ts: -------------------------------------------------------------------------------- 1 | const statusText = { 2 | ing: "转换中", 3 | start: "未开始", 4 | success: "已完成", 5 | error: "转换失败", 6 | }; 7 | const progressStatus = { 8 | ing: "exception", 9 | start: "exception", 10 | success: "success", 11 | error: "warning", 12 | }; 13 | const vSizes = { 14 | '320|240': '320x240', 15 | '640|400': '640x400', 16 | '720|480': '720x480', 17 | '720|576': '720x576', 18 | '1280|720': '1280x720', 19 | '1920|1080': '1920x1080', 20 | } 21 | 22 | export { 23 | statusText, 24 | progressStatus, 25 | vSizes 26 | } -------------------------------------------------------------------------------- /src/views/clearwater/index.vue: -------------------------------------------------------------------------------- 1 | 120 | 236 | -------------------------------------------------------------------------------- /src/views/clearwater/table.vue: -------------------------------------------------------------------------------- 1 | 123 | -------------------------------------------------------------------------------- /src/views/clearwater/utils.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | const getVideoInfo = (url: string, originUrl: string, title: string): any => { 3 | ipcRenderer.send("get-video-info", { url, originUrl ,title}); 4 | return new Promise((resolve, reject) => { 5 | try { 6 | ipcRenderer.on("reply-video-info", (event, arg) => { 7 | if (arg) { 8 | resolve(arg); 9 | } else { 10 | resolve(null); 11 | } 12 | }); 13 | } catch (error) { 14 | console.log(error) 15 | reject(error); 16 | } 17 | }) 18 | } 19 | 20 | const saveVideo = (url: string | undefined, videoType: string | undefined, videoSize: string | undefined, id: string): void => { 21 | ipcRenderer.send("save-video", { 22 | videotype: videoType, 23 | videosize: videoSize, 24 | id, 25 | url 26 | }); 27 | } 28 | export { 29 | getVideoInfo, 30 | saveVideo 31 | } -------------------------------------------------------------------------------- /src/views/discern/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 161 | -------------------------------------------------------------------------------- /src/views/edit-video/config.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/views/edit-video/config.vue -------------------------------------------------------------------------------- /src/views/edit-video/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/edit-video/textarea.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/views/edit-video/textarea.vue -------------------------------------------------------------------------------- /src/views/home/PageLoading/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 160 | 210 | -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 68 | 167 | -------------------------------------------------------------------------------- /src/views/login/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqomg/zimeiti/3c6d3c188d61ea1696855883eaf71311ab0a8e9a/src/views/login/qrcode.png -------------------------------------------------------------------------------- /src/views/terminal/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 101 | -------------------------------------------------------------------------------- /src/views/weight/index.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 133 | 140 | 290 | 291 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "jest" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, VUE_APP_PORT } = process.env; 2 | module.exports = { 3 | publicPath: '/', 4 | outputDir: 'dist', 5 | productionSourceMap: false, 6 | devServer: { 7 | host: '0.0.0.0', 8 | port: VUE_APP_PORT || 8000, 9 | // 配置反向代理 10 | /* 11 | proxy: { 12 | '/api': { 13 | target: '', 14 | ws: true, 15 | changeOrigin: true 16 | }, 17 | '/foo': { 18 | target: '' 19 | } 20 | } 21 | */ 22 | 23 | }, 24 | // 修改webpack的配置 25 | configureWebpack: { 26 | devtool: 'source-map', 27 | // 不需要打包的插件 28 | externals: { 29 | // 'vue': 'Vue' 30 | } 31 | }, 32 | chainWebpack(config) { 33 | // config.module 34 | // .rule('svgo') 35 | // .test(/.svg$/) 36 | // .include.add(/iconsvg/) 37 | // .end() 38 | // .use('svgo-loader') 39 | // .loader('svgo-loader') 40 | // .options({ 41 | // }); 42 | 43 | }, 44 | pluginOptions: { 45 | electronBuilder: { 46 | externals: ['fluent-ffmpeg', 'ffmpeg-concat'], 47 | chainWebpackRendererProcess: config => { 48 | config.plugin('define').tap(args => { 49 | args[0]['process.env.FLUENTFFMPEG_COV'] = false; 50 | return args; 51 | }); 52 | }, 53 | nodeIntegration: true, 54 | customFileProtocol: './', 55 | // electron 构建配置 56 | builderOptions: { 57 | productName: '自媒体自助', // 项目名,也是生成的安装文件名 58 | appId: 'zimeitizizhu.com', // 包名 59 | copyright: 'Copyright © 2021-present LinQian', // 版权 60 | nsis: { 61 | perMachine:false, 62 | oneClick: false, // 是否一键安装 63 | allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。 64 | allowToChangeInstallationDirectory: true, // 允许修改安装目录 65 | installerIcon: './build/icons/icon.ico', // 安装图标 66 | uninstallerIcon: './build/icons/icon.ico', // 卸载图标 67 | installerHeaderIcon: './build/icons/icon.ico', // 安装时头部图标 68 | createDesktopShortcut: true, // 创建桌面图标 69 | createStartMenuShortcut: true, // 创建开始菜单图标 70 | shortcutName: '自媒体自助' // 图标名称 71 | }, 72 | publish: [ 73 | { 74 | "provider": "generic", 75 | "url": "https://zimeitizizhu.com/public/app/" 76 | } 77 | ], 78 | dmg: { // macOSdmg 79 | contents: [ 80 | { 81 | "x": 410, 82 | "y": 150, 83 | "type": "link", 84 | "path": "/Applications" 85 | }, 86 | { 87 | "x": 130, 88 | "y": 150, 89 | "type": "file" 90 | } 91 | ] 92 | }, 93 | mac: { // mac 94 | icon: "./build/icons/icon.icns", 95 | artifactName: '${productName}-v${version}-mac.${ext}' 96 | }, 97 | win: { // win 相关配置 98 | artifactName: 'zimeitizizhu-v${version}-setup.${ext}', 99 | target: [ 100 | { 101 | target: "nsis", // 利用nsis制作安装程序 102 | arch: [ // 这个意思是打出来32 bit + 64 bit的包,但是要注意:这样打包出来的安装包体积比较大,所以建议直接打32的安装包。 103 | "x64", 104 | "ia32" 105 | ] 106 | } 107 | ] 108 | }, 109 | linux: { 110 | icon: "./build/icons", 111 | artifactName: '${productName}-v${version}-linux.${ext}' 112 | } 113 | } 114 | } 115 | } 116 | } --------------------------------------------------------------------------------