├── .browserslistrc ├── babel.config.js ├── .npmrc ├── public ├── font │ ├── iconfont.ttf │ ├── iconfont.woff │ ├── iconfont.woff2 │ ├── iconfont.css │ ├── iconfont.json │ └── demo.css ├── css │ ├── mac.css │ ├── style.css │ └── common.css ├── index.html └── 3.8.10 │ └── dist │ ├── js │ └── i18n │ │ ├── zh_CN.min.js │ │ └── zh_CN.js │ └── css │ └── content-theme │ └── light.min.css ├── src ├── service │ ├── index.ts │ ├── modles │ │ └── Notes.ts │ └── initSequelize.ts ├── App.vue ├── background.ts ├── components │ ├── ILoading.vue │ ├── ICopy.vue │ ├── IMessage │ │ ├── index.css │ │ └── index.ts │ ├── IRightClick │ │ ├── index.css │ │ └── index.ts │ ├── IDropBar.vue │ ├── ITick.vue │ ├── IMessageBox.vue │ ├── ISwitch.vue │ ├── IInput.vue │ └── IHeader.vue ├── shims-vue.d.ts ├── views │ ├── update │ │ └── index.vue │ ├── setting │ │ ├── components │ │ │ ├── Card.vue │ │ │ └── BlockItem.vue │ │ └── index.vue │ ├── ImagePreview │ │ └── index.vue │ ├── main.vue │ ├── index │ │ ├── components │ │ │ ├── Empty.vue │ │ │ ├── Search.vue │ │ │ └── List.vue │ │ └── index.vue │ └── editor │ │ ├── components │ │ ├── ColorMask.vue │ │ └── IEditor.vue │ │ └── index.vue ├── config │ ├── index.ts │ ├── inotesConfig.ts │ └── electronConfig.ts ├── types │ └── notes.d.ts ├── main.ts ├── router │ └── index.ts ├── assets │ ├── loading.svg │ └── empty-content.svg ├── updater.ts ├── less │ └── index.less ├── utils │ ├── errorLog.ts │ ├── file.ts │ └── index.ts ├── start.ts └── store │ └── notes.state.ts ├── .yarnrc ├── .prettierrc.js ├── .gitignore ├── script └── deleteBuild.js ├── tsconfig.json ├── .eslintrc.js ├── README.md ├── .github └── workflows │ └── build.yml ├── vue.config.js ├── package.json ├── CHANGELOG.md └── LICENSE /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" 2 | ELECTRON_CUSTOM_DIR="{{ version }}" 3 | -------------------------------------------------------------------------------- /public/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiyehk/electron-vue3-inote/HEAD/public/font/iconfont.ttf -------------------------------------------------------------------------------- /public/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiyehk/electron-vue3-inote/HEAD/public/font/iconfont.woff -------------------------------------------------------------------------------- /public/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiyehk/electron-vue3-inote/HEAD/public/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/service/index.ts: -------------------------------------------------------------------------------- 1 | import { sequelize } from './initSequelize'; 2 | import { Notes } from './modles/Notes'; 3 | 4 | export { sequelize, Notes }; 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ 2 | ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | semi: true, 5 | trailingComma: 'none', 6 | endOfLine: 'auto' 7 | }; 8 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /public/css/mac.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0 !important; 3 | } 4 | 5 | .search { 6 | box-shadow: 0 0 4px #ddd; 7 | background-color: transparent !important; 8 | } 9 | 10 | .header .drag-header { 11 | margin-top: 0 !important; 12 | } 13 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import startWindow from './start'; 3 | 4 | // 获取锁,判断是否已经启动 5 | const gotTheLock = app.requestSingleInstanceLock(); 6 | 7 | if (!gotTheLock) { 8 | app.quit(); 9 | } else { 10 | startWindow(); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ILoading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | // eslint-disable-next-line @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | 8 | /** 是否是暗黑模式,根据此处去做兼容 */ 9 | declare const isDark: () => boolean; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /dist_electron 5 | /resources 6 | 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | inotesError.log 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /script/deleteBuild.js: -------------------------------------------------------------------------------- 1 | // const rm = require('rimraf'); 2 | // const path = require('path'); 3 | // const pluginOptions = require('../vue.config').pluginOptions; 4 | 5 | // let directories = pluginOptions.electronBuilder.builderOptions.directories; 6 | // let buildPath = ''; 7 | 8 | // if (directories && directories.output) { 9 | // buildPath = directories.output; 10 | // } 11 | 12 | // // 删除作用只用于删除打包前的buildPath || dist_electron 13 | // // dist_electron是默认打包文件夹 14 | // rm(path.join(__dirname, `../../${buildPath || 'dist_electron'}`), () => {}); 15 | -------------------------------------------------------------------------------- /src/views/update/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 26 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { classNames } from './inotesConfig'; 2 | import { browserWindowOption, winURL, disabledKeys, userTasks } from './electronConfig'; 3 | 4 | const isDev = process.env.NODE_ENV === 'development'; 5 | 6 | /** 日志地址 */ 7 | const constErrorLogPath = `/resources/inotesError${isDev ? '-dev' : ''}.log`; 8 | 9 | /** db地址 */ 10 | const constStoragePath = `/resources/db/notes${isDev ? '-dev' : ''}.db`; 11 | 12 | /** 图片地址 */ 13 | const constImagesPath = '/resources/images/'; 14 | 15 | export { 16 | classNames, 17 | browserWindowOption, 18 | winURL, 19 | disabledKeys, 20 | userTasks, 21 | constErrorLogPath, 22 | constStoragePath, 23 | constImagesPath 24 | }; 25 | -------------------------------------------------------------------------------- /src/service/modles/Notes.ts: -------------------------------------------------------------------------------- 1 | import { sequelize } from '../initSequelize'; 2 | import { STRING } from 'sequelize'; 3 | import { NotesModel } from '@/types/notes'; 4 | import { uuid } from '@/utils'; 5 | 6 | export const Notes = sequelize.define( 7 | 'notes', 8 | { 9 | uid: { 10 | type: STRING, 11 | primaryKey: true, 12 | defaultValue: uuid(), 13 | allowNull: false, 14 | /** 15 | * 是否可重复 16 | */ 17 | unique: false 18 | }, 19 | className: STRING(32), 20 | content: STRING(9999999), 21 | markdown: STRING(9999999), 22 | interception: STRING(500) 23 | }, 24 | { 25 | timestamps: true 26 | } 27 | ); 28 | 29 | Notes.sync({ 30 | alter: true 31 | }); 32 | -------------------------------------------------------------------------------- /src/views/setting/components/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= htmlWebpackPlugin.options.title %> 11 | 12 | 13 | 14 | 15 | <% if (htmlWebpackPlugin.options.platform==='darwin' ) { %> 16 | 17 | <% } %> 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/ICopy.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /src/types/notes.d.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | 3 | /** 4 | * model需要的类型 5 | */ 6 | export interface NotesModelType { 7 | uid: string; 8 | className: string; 9 | content: string; 10 | markdown: string; 11 | interception: string; 12 | } 13 | 14 | /** 15 | * 数据库返回的数据类型 16 | */ 17 | export interface DBNotesType { 18 | readonly uid: string; 19 | className: string; 20 | content: string; 21 | interception: string; 22 | markdown: string; 23 | readonly createdAt: Date; 24 | updatedAt: Date; 25 | } 26 | 27 | /** 28 | * 列表中的 29 | * 30 | * remove 是否已删除 31 | */ 32 | export interface DBNotesListType extends DBNotesType { 33 | remove?: boolean; 34 | } 35 | 36 | /** 37 | * typescript创建model写法 38 | * https://stackoverflow.com/questions/60014874/how-to-use-typescript-with-sequelize 39 | */ 40 | export interface NotesModel extends Model, NotesModelType {} 41 | -------------------------------------------------------------------------------- /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 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env" 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 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/components/IMessage/index.css: -------------------------------------------------------------------------------- 1 | .hy-message { 2 | position: fixed; 3 | box-shadow: 0 0 4px #cccccc; 4 | top: 70px; 5 | left: 50%; 6 | transform: translate(-50%, -50%); 7 | background-color: #fff; 8 | white-space: nowrap; 9 | animation: message-fadein 0.2s; 10 | transition: all 0.2s; 11 | } 12 | 13 | @keyframes message-fadein { 14 | 0% { 15 | top: 30px; 16 | opacity: 0; 17 | } 18 | 100% { 19 | top: 70px; 20 | opacity: 1; 21 | } 22 | } 23 | 24 | .hy-message .hy-message-content { 25 | font-size: 14px; 26 | padding: 6px 14px; 27 | border-radius: 4px; 28 | } 29 | 30 | .hy-message .hy-message-content .iconfont { 31 | font-size: 22px; 32 | margin-right: 4px; 33 | } 34 | 35 | .hy-message-success { 36 | color: #19be6b; 37 | border: 1px solid #19be6b; 38 | } 39 | .hy-message-error { 40 | color: #ed4014; 41 | border: 1px solid #ed4014; 42 | } 43 | .hy-message-warning { 44 | color: #ff9900; 45 | border: 1px solid #ff9900; 46 | } -------------------------------------------------------------------------------- /src/views/ImagePreview/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 43 | -------------------------------------------------------------------------------- /src/components/IRightClick/index.css: -------------------------------------------------------------------------------- 1 | #rightClick { 2 | position: fixed; 3 | background-color: #fff; 4 | box-shadow: 0 0 6px #ccc; 5 | border-radius: 2px; 6 | max-width: 200px; 7 | overflow: hidden; 8 | height: 0; 9 | opacity: 0; 10 | transition: all 0.2s; 11 | } 12 | 13 | .right-click-menu-list li { 14 | list-style: none; 15 | font-size: 14px; 16 | padding: 0 10px; 17 | height: 36px; 18 | line-height: 36px; 19 | box-sizing: border-box; 20 | overflow: hidden; 21 | display: flex; 22 | align-items: center; 23 | cursor: pointer; 24 | } 25 | 26 | .right-click-menu-list li:hover { 27 | background-color: #e5e5e5; 28 | } 29 | 30 | .right-click-menu-list li:active { 31 | background-color: #ccc; 32 | } 33 | 34 | .right-click-menu-list li i { 35 | font-size: 18px; 36 | } 37 | 38 | .right-click-menu-list li .right-click-menu-text { 39 | white-space: nowrap; 40 | display: inline-block; 41 | padding: 0 4px; 42 | } 43 | 44 | .right-click-menu-list .disabled-item { 45 | color: #ccc; 46 | cursor: default; 47 | background-color: #fff !important; 48 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['eslint:recommended', '@vue/typescript/recommended', '@vue/prettier'], 7 | parserOptions: { 8 | ecmaVersion: 2020 9 | }, 10 | rules: { 11 | '@typescript-eslint/explicit-module-boundary-types': 0, 12 | '@typescript-eslint/no-non-null-assertion': 0, 13 | '@typescript-eslint/camelcase': 0, 14 | '@typescript-eslint/no-explicit-any': 0, 15 | quotes: [1, 'single'], 16 | semi: 1, 17 | 'no-irregular-whitespace': 2, 18 | 'no-case-declarations': 0, 19 | 'no-undef': 0, 20 | 'eol-last': 1, 21 | 'block-scoped-var': 2, 22 | 'comma-dangle': [2, 'never'], 23 | 'no-dupe-keys': 2, 24 | 'no-empty': 1, 25 | 'no-extra-semi': 2, 26 | 'no-multiple-empty-lines': [1, { max: 1, maxEOF: 1 }], 27 | 'no-trailing-spaces': 1, 28 | 'semi-spacing': [2, { before: false, after: true }], 29 | 'no-unreachable': 1, 30 | 'space-infix-ops': 1, 31 | 'spaced-comment': 1, 32 | 'no-var': 2, 33 | 'no-multi-spaces': 2, 34 | 'comma-spacing': 1 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import outputErrorLog from '@/utils/errorLog'; 5 | import { sequelizeInit } from './service/initSequelize'; 6 | 7 | sequelizeInit(); 8 | 9 | const app = createApp(App); 10 | app.directive('tip', (el, { value }) => { 11 | const { height } = el.dataset; 12 | // 储存最初的高度 13 | if (!height && height !== '0') { 14 | el.dataset.height = el.clientHeight; 15 | } 16 | const clientHeight = height || el.clientHeight; 17 | let cssText = 'transition: all 0.4s;'; 18 | if (value) { 19 | cssText += `height: ${clientHeight}px;opacity: 1;margin-top: 4px;`; 20 | } else { 21 | cssText += 'height: 0;opacity: 0;overflow: hidden;margin-top: 0px;'; 22 | } 23 | el.style.cssText = cssText; 24 | }); 25 | app.directive('mask', (el: HTMLLIElement) => { 26 | const liHei = el.clientHeight as number; 27 | const childHei = el.querySelector('.edit-content')?.clientHeight as number; 28 | if (childHei > liHei) { 29 | el.classList.add('item-mask'); 30 | } 31 | }); 32 | 33 | if (process.env.NODE_ENV !== 'development') { 34 | app.config.errorHandler = outputErrorLog; 35 | } 36 | 37 | app.use(router).mount('#app'); 38 | -------------------------------------------------------------------------------- /src/service/initSequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import sqlite3 from 'sqlite3'; 3 | import { join, dirname } from 'path'; 4 | import { remote } from 'electron'; 5 | import { constStoragePath } from '@/config'; 6 | 7 | const storagePath = join(dirname(remote.app.getPath('exe')), constStoragePath); 8 | console.log(storagePath); 9 | 10 | export const sequelize = new Sequelize({ 11 | database: 'reading', 12 | dialect: 'sqlite', 13 | storage: storagePath, 14 | dialectModule: sqlite3, 15 | logging: false, 16 | pool: { 17 | max: 5, 18 | min: 0, 19 | acquire: 30000, 20 | idle: 10000 21 | } 22 | }); 23 | 24 | export const sequelizeInit = (): void => { 25 | console.log('-----------------------------------------------------------------'); 26 | sequelize 27 | .authenticate() 28 | .then(() => { 29 | // console.clear(); 30 | console.log('Connection has been established successfully.'); 31 | }) 32 | .catch(err => { 33 | console.log('Unable to connect to the database', err); 34 | }); 35 | 36 | // 根据 model自动创建表 37 | sequelize 38 | .sync() 39 | .then(() => { 40 | console.log('init db ok'); 41 | }) 42 | .catch(err => { 43 | console.log('init db error', err); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/main.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 51 | -------------------------------------------------------------------------------- /src/views/setting/components/BlockItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 60 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import { RouteRecordRaw } from 'vue-router'; 3 | import main from '../views/main.vue'; 4 | 5 | const routes: Array = [ 6 | { 7 | path: '/', 8 | name: 'main', 9 | component: main, 10 | children: [ 11 | { 12 | path: '/', 13 | name: 'index', 14 | component: () => import('../views/index/index.vue'), 15 | meta: { 16 | title: 'I便笺' 17 | } 18 | }, 19 | { 20 | path: '/editor', 21 | name: 'editor', 22 | component: () => import('../views/editor/index.vue'), 23 | meta: { 24 | title: '' 25 | } 26 | }, 27 | { 28 | path: '/setting', 29 | name: 'setting', 30 | component: () => import('../views/setting/index.vue'), 31 | meta: { 32 | title: '设置' 33 | } 34 | } 35 | ] 36 | }, 37 | { 38 | path: '/image-preview', 39 | name: 'imagePreview', 40 | component: () => import('../views/ImagePreview/index.vue'), 41 | meta: { 42 | title: '图片预览' 43 | } 44 | } 45 | ]; 46 | 47 | const router = createRouter({ 48 | history: createWebHashHistory(process.env.BASE_URL), 49 | routes 50 | }); 51 | 52 | export default router; 53 | -------------------------------------------------------------------------------- /src/components/IDropBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | 28 | 53 | -------------------------------------------------------------------------------- /src/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/3.8.10/dist/js/i18n/zh_CN.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v5.10.0. 3 | * Original file: /npm/vditor@3.8.10/dist/js/i18n/zh_CN.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | window.VditorI18n={alignCenter:"居中",alignLeft:"居左",alignRight:"居右",alternateText:"替代文本",bold:"粗体",both:"编辑 & 预览",check:"任务列表",close:"关闭",code:"代码块","code-theme":"代码块主题预览",column:"列",comment:"评论",confirm:"确定","content-theme":"内容主题预览",copied:"已复制",copy:"复制","delete-column":"删除列","delete-row":"删除行",devtools:"开发者工具",down:"下",downloadTip:"该浏览器不支持下载功能",edit:"编辑","edit-mode":"切换编辑模式",emoji:"表情",export:"导出",fileTypeError:"文件类型不允许上传,请压缩后再试",footnoteRef:"脚注标识",fullscreen:"全屏切换",generate:"生成中",headings:"标题",heading1:"一级标题",heading2:"二级标题",heading3:"三级标题",heading4:"四级标题",heading5:"五级标题",heading6:"六级标题",help:"帮助",imageURL:"图片地址",indent:"列表缩进",info:"关于","inline-code":"行内代码","insert-after":"末尾插入行","insert-before":"起始插入行",insertColumnLeft:"在左边插入一列",insertColumnRight:"在右边插入一列",insertRowAbove:"在上方插入一行",insertRowBelow:"在下方插入一行",instantRendering:"即时渲染",italic:"斜体",language:"语言",line:"分隔线",link:"链接",linkRef:"引用标识",list:"无序列表",more:"更多",nameEmpty:"文件名不能为空","ordered-list":"有序列表",outdent:"列表反向缩进",outline:"大纲",over:"超过",performanceTip:"实时预览需 ${x}ms,可点击编辑 & 预览按钮进行关闭",preview:"预览",quote:"引用",record:"开始录音/结束录音","record-tip":"该设备不支持录音功能",recording:"录音中...",redo:"重做",remove:"删除",row:"行",spin:"旋转",splitView:"分屏预览",strike:"删除线",table:"表格",textIsNotEmpty:"文本(不能为空)",title:"标题",tooltipText:"提示文本",undo:"撤销",up:"上",update:"更新",upload:"上传图片或文件",uploadError:"上传错误",uploading:"上传中...",wysiwyg:"所见即所得"}; 8 | //# sourceMappingURL=/sm/40bfe49531405215b32c72a5baf4f1b4bfd0d4e857e992c2ea5d9dc0b0538bc0.map -------------------------------------------------------------------------------- /src/components/IMessage/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h, App } from 'vue'; 2 | import './index.css'; 3 | 4 | type MessageType = 'success' | 'info' | 'error' | 'warning'; 5 | 6 | let messageApp: App | null = null; 7 | let messageEl: HTMLDivElement | null = null; 8 | let timeouter: NodeJS.Timeout | null; 9 | 10 | const render = (text: string, type: MessageType) => { 11 | return h( 12 | 'div', 13 | { 14 | class: ['hy-message-content', 'flex-items', type ? `hy-message-${type}` : ''] 15 | }, 16 | [ 17 | h('i', { 18 | class: ['iconfont', 'icon-warning'] 19 | }), 20 | h( 21 | 'span', 22 | { 23 | class: 'hy-message-text' 24 | }, 25 | text 26 | ) 27 | ] 28 | ); 29 | }; 30 | 31 | const useMessage = (text: string, type: MessageType = 'info', duration = 2800): void => { 32 | messageApp = null; 33 | messageApp = createApp({ 34 | setup() { 35 | return () => { 36 | return render(text, type); 37 | }; 38 | } 39 | }); 40 | if (timeouter) { 41 | clearTimeout(timeouter as NodeJS.Timeout); 42 | timeouter = null; 43 | } 44 | if (!messageEl) { 45 | messageEl = document.createElement('div'); 46 | document.body.appendChild(messageEl); 47 | messageEl.classList.add('hy-message'); 48 | messageApp.mount('.hy-message'); 49 | } 50 | timeouter = setTimeout(() => { 51 | (messageEl as HTMLDivElement).style.cssText = 'top: 20px;opacity: 0;'; 52 | setTimeout(() => { 53 | messageEl?.remove(); 54 | messageEl = null; 55 | clearTimeout(timeouter as NodeJS.Timeout); 56 | timeouter = null; 57 | }, 200); 58 | }, duration); 59 | }; 60 | 61 | export default useMessage; 62 | -------------------------------------------------------------------------------- /src/components/ITick.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | 85 | -------------------------------------------------------------------------------- /src/config/inotesConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * - `bold` 加粗 3 | * - `italic` 斜体 4 | * - `underline` 下划线 5 | * - `strikethrough` 删除线 6 | * - `insertUnorderedList` 无序列表 7 | * - `insertOrderedList` 有序列表 8 | * - `image` 图片 9 | */ 10 | export const editorIcons = [ 11 | { 12 | name: 'bold', 13 | title: '加粗', 14 | icon: 'icon-editor-bold' 15 | }, 16 | { 17 | name: 'italic', 18 | title: '斜体', 19 | icon: 'icon-italic' 20 | }, 21 | { 22 | name: 'underline', 23 | title: '下划线', 24 | icon: 'icon-underline' 25 | }, 26 | { 27 | name: 'strikethrough', 28 | title: '删除线', 29 | icon: 'icon-strikethrough' 30 | }, 31 | { 32 | name: 'insertUnorderedList', 33 | title: '无序列表', 34 | icon: 'icon-ul' 35 | }, 36 | { 37 | name: 'insertOrderedList', 38 | title: '有序列表', 39 | icon: 'icon-ol' 40 | // }, 41 | // { 42 | // name: 'image', 43 | // title: '图片', 44 | // icon: 'icon-image' 45 | } 46 | ]; 47 | 48 | /** 49 | * - `yellow-content` 黄色 50 | * - `green-content` 绿色 51 | * - `pink-content` 粉色 52 | * - `purple-content` 紫色 53 | * - `blue-content` 蓝色 54 | * - `gray-content` 灰色 55 | * - `black-content` 黑色 56 | */ 57 | export const classNames = [ 58 | { 59 | className: 'white-content', 60 | title: '白色' 61 | }, 62 | { 63 | className: 'yellow-content', 64 | title: '黄色' 65 | }, 66 | { 67 | className: 'green-content', 68 | title: '绿色' 69 | }, 70 | { 71 | className: 'pink-content', 72 | title: '粉色' 73 | }, 74 | { 75 | className: 'purple-content', 76 | title: '紫色' 77 | }, 78 | { 79 | className: 'blue-content', 80 | title: '蓝色' 81 | }, 82 | { 83 | className: 'gray-content', 84 | title: '灰色' 85 | }, 86 | { 87 | className: 'black-content', 88 | title: '黑色' 89 | } 90 | ]; 91 | -------------------------------------------------------------------------------- /src/views/index/components/Empty.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 35 | 36 | 72 | -------------------------------------------------------------------------------- /src/views/index/components/Search.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 86 | -------------------------------------------------------------------------------- /src/updater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from 'electron-updater'; 2 | import { dialog, BrowserWindow } from 'electron'; 3 | import path from 'path'; 4 | import log from 'electron-log'; 5 | import { winURL } from './config'; 6 | 7 | const isDevelopment = process.env.NODE_ENV === 'development'; 8 | 9 | if (isDevelopment) { 10 | autoUpdater.updateConfigPath = path.join(__dirname, '../dev-app-update.yml'); 11 | } 12 | export default () => { 13 | let win = null; 14 | 15 | // 设置自动下载 16 | autoUpdater.autoDownload = false; 17 | 18 | // 检测是否有新版本 19 | autoUpdater.checkForUpdates(); 20 | 21 | autoUpdater.on('checking-for-update', res => { 22 | log.info('获取版本信息:' + res); 23 | }); 24 | 25 | autoUpdater.on('update-not-available', res => { 26 | log.info('没有可更新版本:' + res); 27 | }); 28 | 29 | autoUpdater.on('update-available', res => { 30 | dialog 31 | .showMessageBox({ 32 | type: 'info', 33 | title: '软件更新', 34 | message: '发现新版本, 确定更新?', 35 | buttons: ['确定', '取消'] 36 | }) 37 | .then(resp => { 38 | if (resp.response == 0) { 39 | createWindow(); 40 | autoUpdater.downloadUpdate(); 41 | } 42 | }); 43 | }); 44 | 45 | async function createWindow() { 46 | win = new BrowserWindow({ 47 | width: 300, 48 | height: 300, 49 | title: '七鹊', 50 | frame: false, 51 | transparent: true, 52 | maximizable: false, 53 | webPreferences: { 54 | nodeIntegration: true, 55 | contextIsolation: false, 56 | enableRemoteModule: true 57 | } 58 | }); 59 | win.loadURL(`${winURL}/#/update`); 60 | } 61 | 62 | autoUpdater.on('download-progress', res => { 63 | log.info('下载监听:' + res); 64 | win.webContents.send('downloadProgress', res); 65 | }); 66 | 67 | autoUpdater.on('update-downloaded', () => { 68 | dialog 69 | .showMessageBox({ 70 | title: '下载完成', 71 | message: '最新版本已下载完成, 退出程序进行安装' 72 | }) 73 | .then(() => { 74 | autoUpdater.quitAndInstall(); 75 | }); 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @primary-color: #027aff; 2 | @success-color: #19be6b; 3 | @warning-color: #ff9900; 4 | @error-color: #ed4014; 5 | @white-color: #ffffff; 6 | @gray-color: #efefef; 7 | 8 | @text-color: #000000; 9 | @text-sub-color: #00000073; 10 | @border-color: #d9d9d9; 11 | @disabled-color: #c5c8ce; 12 | @background-color: #f3f3f3; 13 | @background-sub-color: #eeeeee; 14 | @shadown-color: #cccccc; 15 | 16 | // 头部iconsize 17 | @headerIconFontSize: 22px; 18 | 19 | // 头部高度、底部功能按钮和icon的宽高大小是一致的 20 | @iconSize: 40px; 21 | 22 | .icon { 23 | width: @iconSize; 24 | height: @iconSize; 25 | min-width: @iconSize; 26 | min-height: @iconSize; 27 | outline: none; 28 | border: none; 29 | background-color: transparent; 30 | padding: 0; 31 | position: relative; 32 | 33 | &::before { 34 | content: ''; 35 | position: absolute; 36 | width: 100%; 37 | height: 100%; 38 | top: 0; 39 | left: 0; 40 | z-index: 0; 41 | } 42 | 43 | a { 44 | color: initial; 45 | width: 100%; 46 | height: 100%; 47 | outline: none; 48 | position: relative; 49 | z-index: 1; 50 | } 51 | 52 | .iconfont { 53 | width: 22px; 54 | position: relative; 55 | } 56 | 57 | &:hover { 58 | &::before { 59 | background-color: rgba(0, 0, 0, 0.1); 60 | } 61 | } 62 | } 63 | 64 | .link-style { 65 | color: @primary-color; 66 | cursor: pointer; 67 | } 68 | 69 | .link-margin { 70 | margin-left: 8px; 71 | } 72 | 73 | .gray-text { 74 | color: @disabled-color; 75 | font-size: 12px; 76 | margin-top: 4px; 77 | } 78 | 79 | .block-button { 80 | color: @text-color; 81 | vertical-align: middle; 82 | box-shadow: 0 0 4px #ccc; 83 | display: inline-block; 84 | padding: 6px 14px; 85 | border: none; 86 | transition: all 0.4s; 87 | border-radius: 4px; 88 | background-color: rgba(245, 245, 245, 0.6); 89 | 90 | .iconfont { 91 | font-size: 14px !important; 92 | } 93 | 94 | &:active { 95 | box-shadow: 0 0 4px #ccc inset; 96 | } 97 | } 98 | .block-button.disabled-button { 99 | background-color: #dbdbdb; 100 | box-shadow: none; 101 | color: #b5b5b5; 102 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron+vue3+ts 2 | 3 | > 在Windows环境下,删除`node_modules`重新安装依赖的情况下,会导致`build`报错,需要使用`npm i`进行安装依赖。 4 | > In the Windows environment, deleting `node_modules` and reinstalling dependencies will cause `build` errors, and you need to use `npm i` to install dependencies. 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | ![image](https://user-images.githubusercontent.com/33891067/211135039-eb778337-2249-4442-b050-32cc6ee77814.png) 15 | 16 | ### Windows 17 | 18 | 19 | ### Mac 20 | 21 | 22 | ## 启动 23 | ``` 24 | yarn serve 25 | ``` 26 | 27 | ## 打包 28 | ``` 29 | yarn build 30 | ``` 31 | 32 | ## 教程 33 | 【electron+vue3+ts实战便笺exe】一、搭建框架配置 34 | https://juejin.cn/post/6909723449246089224 35 | 36 | 【electron+vue3+ts实战便笺exe】二、electron+vue3开发内容 37 | https://juejin.cn/post/6909725365107687431 38 | 39 | 【electron+vue3+ts实战便笺exe】终章:markdown编辑器以及右键功能实现 40 | https://juejin.cn/post/7187704994731130938 41 | 42 | ![gif](https://user-images.githubusercontent.com/33891067/126119851-b59a0acb-07b4-4126-9698-961ee0f706a7.gif) 43 | 44 | ``` 45 | electron-vue3-inote 46 | ├── babel.config.js 47 | ├── package.json 48 | ├── public 49 | │ ├── css 50 | │ ├── favicon.ico 51 | │ ├── font 52 | │ └── index.html 53 | ├── script # 打包删除脚本 54 | │ └── deleteBuild.js 55 | ├── src 56 | │ ├── App.vue 57 | │ ├── assets 58 | │ ├── background.ts 59 | │ ├── components 60 | │ ├── config # electron和软件的一些配置项 61 | │ ├── less 62 | │ ├── main.ts 63 | │ ├── router # 路由 64 | │ ├── service # 存放sqlite3 db服务 65 | │ ├── shims-vue.d.ts 66 | │ ├── store 67 | │ ├── types 68 | │ ├── utils 69 | │ └── views 70 | ├── tsconfig.json 71 | └── vue.config.js 72 | ``` 73 | -------------------------------------------------------------------------------- /public/3.8.10/dist/js/i18n/zh_CN.js: -------------------------------------------------------------------------------- 1 | window.VditorI18n = { 2 | 'alignCenter': '居中', 3 | 'alignLeft': '居左', 4 | 'alignRight': '居右', 5 | 'alternateText': '替代文本', 6 | 'bold': '粗体', 7 | 'both': '编辑 & 预览', 8 | 'check': '任务列表', 9 | 'close': '关闭', 10 | 'code': '代码块', 11 | 'code-theme': '代码块主题预览', 12 | 'column': '列', 13 | 'comment': '评论', 14 | 'confirm': '确定', 15 | 'content-theme': '内容主题预览', 16 | 'copied': '已复制', 17 | 'copy': '复制', 18 | 'delete-column': '删除列', 19 | 'delete-row': '删除行', 20 | 'devtools': '开发者工具', 21 | 'down': '下', 22 | 'downloadTip': '该浏览器不支持下载功能', 23 | 'edit': '编辑', 24 | 'edit-mode': '切换编辑模式', 25 | 'emoji': '表情', 26 | 'export': '导出', 27 | 'fileTypeError': '文件类型不允许上传,请压缩后再试', 28 | 'footnoteRef': '脚注标识', 29 | 'fullscreen': '全屏切换', 30 | 'generate': '生成中', 31 | 'headings': '标题', 32 | 'heading1': '一级标题', 33 | 'heading2': '二级标题', 34 | 'heading3': '三级标题', 35 | 'heading4': '四级标题', 36 | 'heading5': '五级标题', 37 | 'heading6': '六级标题', 38 | 'help': '帮助', 39 | 'imageURL': '图片地址', 40 | 'indent': '列表缩进', 41 | 'info': '关于', 42 | 'inline-code': '行内代码', 43 | 'insert-after': '末尾插入行', 44 | 'insert-before': '起始插入行', 45 | 'insertColumnLeft': '在左边插入一列', 46 | 'insertColumnRight': '在右边插入一列', 47 | 'insertRowAbove': '在上方插入一行', 48 | 'insertRowBelow': '在下方插入一行', 49 | 'instantRendering': '即时渲染', 50 | 'italic': '斜体', 51 | 'language': '语言', 52 | 'line': '分隔线', 53 | 'link': '链接', 54 | 'linkRef': '引用标识', 55 | 'list': '无序列表', 56 | 'more': '更多', 57 | 'nameEmpty': '文件名不能为空', 58 | 'ordered-list': '有序列表', 59 | 'outdent': '列表反向缩进', 60 | 'outline': '大纲', 61 | 'over': '超过', 62 | 'performanceTip': '实时预览需 ${x}ms,可点击编辑 & 预览按钮进行关闭', 63 | 'preview': '预览', 64 | 'quote': '引用', 65 | 'record': '开始录音/结束录音', 66 | 'record-tip': '该设备不支持录音功能', 67 | 'recording': '录音中...', 68 | 'redo': '重做', 69 | 'remove': '删除', 70 | 'row': '行', 71 | 'spin': '旋转', 72 | 'splitView': '分屏预览', 73 | 'strike': '删除线', 74 | 'table': '表格', 75 | 'textIsNotEmpty': '文本(不能为空)', 76 | 'title': '标题', 77 | 'tooltipText': '提示文本', 78 | 'undo': '撤销', 79 | 'up': '上', 80 | 'update': '更新', 81 | 'upload': '上传图片或文件', 82 | 'uploadError': '上传错误', 83 | 'uploading': '上传中...', 84 | 'wysiwyg': '所见即所得', 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/errorLog.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPublicInstance } from 'vue'; 2 | import dayjs from 'dayjs'; 3 | import fs from 'fs-extra'; 4 | import os from 'os'; 5 | import { remote } from 'electron'; 6 | import { join, dirname } from 'path'; 7 | import useMessage from '@/components/IMessage'; 8 | import { constErrorLogPath } from '@/config'; 9 | 10 | function getShortStack(stack?: string): string { 11 | const splitStack = stack?.split('\n '); 12 | if (!splitStack) return ''; 13 | const newStack: string[] = []; 14 | for (const line of splitStack) { 15 | // 其他信息 16 | if (line.includes('bundler')) continue; 17 | 18 | // 只保留错误文件信息 19 | if (line.includes('?!.')) { 20 | newStack.push(line.replace(/webpack-internal:\/\/\/\.\/node_modules\/.+\?!/, '')); 21 | } else { 22 | newStack.push(line); 23 | } 24 | } 25 | // 转换string 26 | return newStack.join('\n '); 27 | } 28 | 29 | export const errorLogPath = join(dirname(remote.app.getPath('exe')), constErrorLogPath); 30 | 31 | export default function(error: unknown, vm: ComponentPublicInstance | null, info: string): void { 32 | const { message, stack } = error as Error; 33 | const { electron, chrome, node, v8 } = process.versions; 34 | const { outerWidth, outerHeight, innerWidth, innerHeight } = window; 35 | const { width, height } = window.screen; 36 | 37 | // 报错信息 38 | const errorInfo = { 39 | errorInfo: info, 40 | errorMessage: message, 41 | errorStack: getShortStack(stack) 42 | }; 43 | 44 | // electron 45 | const electronInfo = { electron, chrome, node, v8 }; 46 | 47 | // 浏览器窗口信息 48 | const browserInfo = { outerWidth, outerHeight, innerWidth, innerHeight }; 49 | 50 | const errorLog = { 51 | versions: remote.app.getVersion(), 52 | date: dayjs().format('YYYY-MM-DD HH:mm'), 53 | error: errorInfo, 54 | electron: electronInfo, 55 | window: { 56 | type: os.type(), 57 | platform: os.platform() 58 | }, 59 | browser: browserInfo, 60 | screen: { width, height } 61 | }; 62 | 63 | useMessage('程序出现异常', 'error'); 64 | 65 | if (process.env.NODE_ENV === 'production') { 66 | fs.writeFileSync(errorLogPath, JSON.stringify(errorLog) + '\n', { flag: 'a' }); 67 | } else { 68 | console.log(errorInfo.errorStack); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Electron Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 执行分支on: 7 | workflow_dispatch: 8 | 9 | env: 10 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 11 | 12 | jobs: 13 | build_macos: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Node.js 16.x 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '16.x' 21 | - name: Install Dependencies 22 | run: npm install 23 | - name: Build macOS app 24 | env: 25 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 26 | run: | 27 | npm run build 28 | GH_TOKEN=${{ secrets.GH_TOKEN }} npm run pack:mac 29 | - name: Upload macOS Artifact 30 | uses: actions/upload-artifact@v2.2.4 31 | with: 32 | name: release-${{env.BUILD_TIME}}-mac 33 | path: ./dist_electron/*.dmg 34 | 35 | build_windows: 36 | runs-on: windows-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Set up Node.js 16.x 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: '16.x' 43 | - name: Install Dependencies 44 | run: npm install 45 | - name: Build Windows app 46 | env: 47 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 48 | run: | 49 | npm run build 50 | GH_TOKEN=${{ secrets.GH_TOKEN }} npm run pack:windows 51 | - name: Upload Windows Artifact 52 | uses: actions/upload-artifact@v2.2.4 53 | with: 54 | name: release-${{env.BUILD_TIME}}-win 55 | path: ./dist_electron/*.exe 56 | 57 | build_linux: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v2 61 | - name: Set up Node.js 16.x 62 | uses: actions/setup-node@v2 63 | with: 64 | node-version: '16.x' 65 | - name: Install Dependencies 66 | run: npm install 67 | - name: Build Linux app 68 | env: 69 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 70 | run: | 71 | npm run build 72 | GH_TOKEN=${{ secrets.GH_TOKEN }} npm run pack:linux 73 | - name: Upload Linux Artifact 74 | uses: actions/upload-artifact@v2.2.4 75 | with: 76 | name: release-${{env.BUILD_TIME}}-linux 77 | path: ./dist_electron/*.AppImage 78 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | lintOnSave: false, 6 | pages: { 7 | index: { 8 | entry: 'src/main.ts', 9 | template: 'public/index.html', 10 | filename: 'index.html', 11 | title: 'I便笺', 12 | // chunks: ['chunk-vendors', 'chunk-common', 'index'], 13 | platform: process.platform 14 | } 15 | }, 16 | // productionSourceMap: false, 17 | configureWebpack: config => { 18 | config.externals = { 19 | sqlite3: 'commonjs sqlite3' 20 | }; 21 | // if (process.env.NODE_ENV !== 'development') { 22 | // config.optimization.minimizer[0].options.terserOptions.warnings = false; 23 | // config.optimization.minimizer[0].options.terserOptions.compress = { 24 | // warnings: false, 25 | // drop_console: true, 26 | // drop_debugger: true, 27 | // pure_funcs: ['console.log'] 28 | // }; 29 | // } 30 | }, 31 | pluginOptions: { 32 | electronBuilder: { 33 | nodeIntegration: true, 34 | builderOptions: { 35 | productName: 'I便笺', 36 | appId: 'com.inotes.heiyehk', 37 | copyright: 'heiyehk', 38 | compression: 'store', // "store" | "normal"| "maximum" 打包压缩情况(store 相对较快),store 39749kb, maximum 39186kb 39 | // directories: { 40 | // output: 'build' // 输出文件夹 41 | // }, 42 | win: { 43 | // icon: 'xxx/icon.ico', 44 | target: ['nsis', 'zip'] 45 | }, 46 | nsis: { 47 | oneClick: false, // 一键安装 48 | // guid: 'xxxx', // 注册表名字,不推荐修改 49 | perMachine: true, // 是否开启安装时权限限制(此电脑或当前用户) 50 | allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。 51 | allowToChangeInstallationDirectory: true, // 允许修改安装目录 52 | // installerIcon: './build/icons/aaa.ico', // 安装图标 53 | // uninstallerIcon: './build/icons/bbb.ico', // 卸载图标 54 | // installerHeaderIcon: './build/icons/aaa.ico', // 安装时头部图标 55 | createDesktopShortcut: true, // 创建桌面图标 56 | createStartMenuShortcut: true, // 创建开始菜单图标 57 | shortcutName: 'I便笺' // 图标名称 58 | }, 59 | publish: ['github'] 60 | } 61 | }, 62 | 'style-resources-loader': { 63 | preProcessor: 'less', 64 | patterns: [path.resolve(__dirname, 'src/less/index.less')] // 引入全局样式变量 65 | } 66 | }, 67 | devServer: { 68 | port: 55225 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/config/electronConfig.ts: -------------------------------------------------------------------------------- 1 | import { Task } from 'electron'; 2 | 3 | const isDevelopment = process.env.NODE_ENV !== 'production'; 4 | 5 | /** 6 | * task事件 7 | */ 8 | export const userTasks: Task[] = [ 9 | { 10 | program: process.execPath, 11 | arguments: '--editor', 12 | iconPath: process.execPath, 13 | iconIndex: 0, 14 | title: '新建便笺', 15 | description: '创建新的便笺' 16 | }, 17 | { 18 | program: process.execPath, 19 | arguments: '--setting', 20 | iconPath: process.execPath, 21 | iconIndex: 0, 22 | title: '设置', 23 | description: '打开设置' 24 | } 25 | ]; 26 | 27 | /** 28 | * 主要禁用 29 | * - F11 禁用全屏放大 30 | * - CTRL+R 禁用刷新 31 | * - CTRL+SHIFT+R 禁用刷新 32 | */ 33 | export const disabledKeys = () => { 34 | const devShortcuts = ['F11', 'Ctrl+R', 'Ctrl+SHIFT+R']; 35 | 36 | const shortcuts = ['Ctrl+N', 'SHIFT+F10', 'Ctrl+SHIFT+I']; 37 | 38 | const exportKeys = isDevelopment ? shortcuts : [...devShortcuts, ...shortcuts]; 39 | return exportKeys; 40 | }; 41 | 42 | /** 43 | * BrowserWindow的配置项 44 | * @param type 单独给编辑窗口的配置 45 | */ 46 | export const browserWindowOption = (type?: 'editor'): Electron.BrowserWindowConstructorOptions => { 47 | const devWid = isDevelopment ? 950 : 0; 48 | const devHei = isDevelopment ? 600 : 0; 49 | 50 | // 底部icon: 40*40 51 | const editorWindowOptions = { 52 | width: devWid || 290, 53 | height: devHei || 320, 54 | minWidth: 290 55 | }; 56 | const commonOptions: Electron.BrowserWindowConstructorOptions = { 57 | minHeight: 48, 58 | frame: false, 59 | hasShadow: true, 60 | transparent: true, 61 | fullscreen: false, 62 | webPreferences: { 63 | enableRemoteModule: true, 64 | nodeIntegration: true, 65 | contextIsolation: false, 66 | webSecurity: false 67 | } 68 | }; 69 | // 兼容mac 70 | if (process.platform === 'darwin') { 71 | commonOptions.frame = true; 72 | commonOptions.transparent = false; 73 | commonOptions.backgroundColor = '#ffffff'; 74 | } 75 | if (!type) { 76 | return { 77 | width: devWid || 350, 78 | height: devHei || 600, 79 | minWidth: 320, 80 | ...commonOptions, 81 | resizable: isDevelopment ? true : false 82 | }; 83 | } 84 | return { 85 | ...editorWindowOptions, 86 | ...commonOptions 87 | }; 88 | }; 89 | 90 | /** 91 | * 开发环境: http://localhost:55225 92 | * 93 | * 正式环境: file://${__dirname}/index.html 94 | */ 95 | export const winURL = isDevelopment ? 'http://localhost:55225' : `file://${__dirname}/index.html`; 96 | -------------------------------------------------------------------------------- /public/3.8.10/dist/css/content-theme/light.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using clean-css v5.2.2. 3 | * Original file: /npm/vditor@3.8.10/dist/css/content-theme/light.css 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | /*! 8 | * Vditor - A markdown editor written in TypeScript. 9 | * 10 | * MIT License 11 | * 12 | * Copyright (c) 2018-present B3log 开源, b3log.org 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining a copy 15 | * of this software and associated documentation files (the "Software"), to deal 16 | * in the Software without restriction, including without limitation the rights 17 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | * copies of the Software, and to permit persons to whom the Software is 19 | * furnished to do so, subject to the following conditions: 20 | * 21 | * The above copyright notice and this permission notice shall be included in all 22 | * copies or substantial portions of the Software. 23 | * 24 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | * SOFTWARE. 31 | * 32 | */ 33 | .vditor-reset h1,.vditor-reset h2{padding-bottom:.3em;border-bottom:1px solid #eaecef}.vditor-reset hr{background-color:#eaecef}.vditor-reset blockquote{color:#6a737d;border-left:.25em solid #eaecef}.vditor-reset iframe{border:1px solid #d1d5da}.vditor-reset table tr{border-top:1px solid #c6cbd1;background-color:#fafbfc}.vditor-reset table td,.vditor-reset table th{border:1px solid #dfe2e5}.vditor-reset table tbody tr:nth-child(2n){background-color:#fff}.vditor-reset code:not(.hljs):not(.highlight-chroma){background-color:rgba(27,31,35,.05)}.vditor-reset kbd{color:#24292e;background-color:#fafbfc;border:solid 1px #d1d5da;box-shadow:inset 0 -1px 0 #d1d5da}.vditor-speech{background-color:#f6f8fa;border:1px solid #d1d5da;color:#586069}.vditor-speech--current,.vditor-speech:hover{color:#4285f4}.vditor-linkcard a{background-color:#f6f8fa}.vditor-linkcard a:visited .vditor-linkcard__abstract{color:rgba(88,96,105,.36)}.vditor-linkcard__title{color:#24292e}.vditor-linkcard__abstract{color:#586069}.vditor-linkcard__site{color:#4285f4}.vditor-linkcard__image{background-color:rgba(88,96,105,.36)} 34 | /*# sourceMappingURL=/sm/089ee1b23348ed2cbc928ec11193da90b974f1b065d2cdf85a1e81d521e5ae45.map */ -------------------------------------------------------------------------------- /src/components/IMessageBox.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 44 | 45 | 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i-notes", 3 | "version": "0.6.1", 4 | "private": true, 5 | "author": "heiyehk", 6 | "description": "I便笺拥有漂亮的过度效果,允许开启多个窗口方便在桌面端更方便的记录文字", 7 | "main": "background.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/heiyehk/electron-vue3-inote" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/heiyehk/electron-vue3-inote/issues", 15 | "email": "heiyehk@foxmail.com" 16 | }, 17 | "scripts": { 18 | "serve": "vue-cli-service electron:serve", 19 | "build": "node script/deleteBuild && vue-cli-service electron:build", 20 | "publish": "yarn build -p always", 21 | "lint": "vue-cli-service lint", 22 | "log": "conventional-changelog -p angular -i CHANGELOG.md -s", 23 | "commit": "yarn log && git add . && cz", 24 | "cz": "cz" 25 | }, 26 | "dependencies": { 27 | "conventional-changelog-cli": "^2.2.2", 28 | "core-js": "^3.15.2", 29 | "crypto-js": "^4.1.1", 30 | "dayjs": "^1.10.6", 31 | "electron-log": "^4.4.7", 32 | "electron-updater": "^5.0.1", 33 | "fs-extra": "^10.0.0", 34 | "pg-hstore": "^2.3.4", 35 | "sequelize": "^6.6.5", 36 | "sqlite3": "^5.1.7", 37 | "style-resources-loader": "^1.4.1", 38 | "vditor": "^3.9.0", 39 | "vue": "3.2.26", 40 | "vue-cli-plugin-electron-builder": "^2.1.1", 41 | "vue-router": "^4.0.12" 42 | }, 43 | "devDependencies": { 44 | "@types/crypto-js": "^4.0.2", 45 | "@types/sequelize": "^4.28.10", 46 | "@types/sqlite3": "^3.1.7", 47 | "@typescript-eslint/eslint-plugin": "^4.8.2", 48 | "@typescript-eslint/parser": "^4.8.2", 49 | "@vue/cli-plugin-babel": "~4.5.0", 50 | "@vue/cli-plugin-eslint": "~4.5.0", 51 | "@vue/cli-plugin-router": "~4.5.0", 52 | "@vue/cli-plugin-typescript": "~4.5.0", 53 | "@vue/cli-service": "~4.5.0", 54 | "@vue/compiler-sfc": "^3.2.26", 55 | "@vue/eslint-config-prettier": "^6.0.0", 56 | "@vue/eslint-config-typescript": "^5.0.2", 57 | "cz-conventional-changelog": "3.3.0", 58 | "electron": "11.5.0", 59 | "electron-builder": "22.14.5", 60 | "eslint": "^6.7.2", 61 | "eslint-config-prettier": "^6.15.0", 62 | "eslint-plugin-prettier": "^3.1.4", 63 | "eslint-plugin-vue": "^7.1.0", 64 | "less": "^3.0.4", 65 | "less-loader": "^5.0.0", 66 | "lint-staged": "^9.5.0", 67 | "prettier": "^1.19.1", 68 | "rimraf": "^3.0.2", 69 | "typescript": "4.4.4", 70 | "vue-cli-plugin-style-resources-loader": "~0.1.4" 71 | }, 72 | "husky": { 73 | "hooks": { 74 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 75 | } 76 | }, 77 | "config": { 78 | "commitizen": { 79 | "path": "cz-conventional-changelog" 80 | } 81 | }, 82 | "gitHooks": { 83 | "pre-commit": "lint-staged" 84 | }, 85 | "lint-staged": { 86 | "*.{js,jsx,vue,ts,tsx}": [ 87 | "vue-cli-service lint", 88 | "git add" 89 | ] 90 | }, 91 | "publish": [ 92 | "github" 93 | ], 94 | "engines": { 95 | "node": "<=16" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.6.1](https://github.com/heiyehk/electron-vue3-inote/compare/0.5.2...0.6.1) (2024-06-15) 2 | 3 | 4 | ### Features 5 | 6 | feature: [#11](https://github.com/heiyehk/electron-vue3-inote/issues/11) 调整储存位置为当前软件目录下 ([b44ace7](https://github.com/heiyehk/electron-vue3-inote/commit/b44ace721cdf28b3a327fd3fb6b56dea2ccf0392)) 7 | 8 | 9 | 10 | ## [0.5.2](https://github.com/heiyehk/electron-vue3-inote/compare/0.5.1...0.5.2) (2023-01-31) 11 | 12 | 13 | ### Features 14 | 15 | * **ieditor.vue:** 升级vditor版本,调整属性 ([c3d50d0](https://github.com/heiyehk/electron-vue3-inote/commit/c3d50d0f3411561c95124a50edfad9ed550b228a)) 16 | 17 | 18 | 19 | ## [0.5.1](https://github.com/heiyehk/electron-vue3-inote/compare/0.4.1...0.5.1) (2023-01-07) 20 | 21 | 22 | ### Features 23 | 24 | * 对编辑器的优化,增加右键功能以及本地图片的缓存,修复了部分bug ([c1c0c0f](https://github.com/heiyehk/electron-vue3-inote/commit/c1c0c0f676a29adce8dc46582f17f550d0c079ce)) 25 | 26 | 27 | 28 | ## [0.3.2](https://github.com/heiyehk/electron-vue3-inote/compare/0.3.1...0.3.2) (2021-11-18) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * 修复一些bug ([bd99c01](https://github.com/heiyehk/electron-vue3-inote/commit/bd99c0143a5867893d4a452558ee0e3b6f101ee9)) 34 | 35 | 36 | 37 | ## [0.3.1](https://github.com/heiyehk/electron-vue3-inote/compare/0.2.3...0.3.1) (2021-09-01)### Features 38 | 39 | * **沉浸模式**: 编辑框失去焦点后自动隐藏头部和编辑按钮 40 | * **纯净模式**: 在沉浸模式下不在自动显示头部和编辑按钮,需要按下`Esc`才能显示 41 | * **Input Component**: 输入框组件增加`disabled`功能 42 | * **Switch Component**: 开关组件增加`change`事件 43 | 44 | 45 | 46 | ## [0.2.3](https://github.com/heiyehk/electron-vue3-inote/compare/0.2.2...0.2.3) (2021-08-27) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * 文件大小写问题 ([40d14c4](https://github.com/heiyehk/electron-vue3-inote/commit/40d14c4e769de2ac34fc1458bf8fd8f408ac8684)) 52 | * 文件大小写问题 ([51e9542](https://github.com/heiyehk/electron-vue3-inote/commit/51e954229360d7a6f27c20f20e2bb7d40362402c)) 53 | * 修复编辑的时候光标错误 ([302683f](https://github.com/heiyehk/electron-vue3-inote/commit/302683f50f95d33ca697a9c8c36fac59a14ba8f5)) 54 | * 修复创建db时找不到文件错误 ([ba47627](https://github.com/heiyehk/electron-vue3-inote/commit/ba476279d73f6237cc7b327e44d0bf56149d1adc)) 55 | 56 | 57 | 58 | ## [0.2.2](https://github.com/heiyehk/electron-vue3-inote/compare/0.2.1...0.2.2) (2021-08-06) 59 | 60 | 61 | ### Features 62 | 63 | * 兼容mac以及暗黑模式 ([c1400cd](https://github.com/heiyehk/electron-vue3-inote/commit/c1400cdfe6dcd114f3ae90376ca0c090c70d8c82)) 64 | 65 | 66 | 67 | ## [0.2.1](https://github.com/heiyehk/electron-vue3-inote/compare/0.1.2...0.2.1) (2021-07-19) 68 | 69 | 70 | 71 | ## [0.1.2](https://github.com/heiyehk/electron-vue3-inote/compare/0.1.1...0.1.2) (2021-02-01) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * 首页双击重复点击导致打开多个一样的 ([0bd5cfe](https://github.com/heiyehk/electron-vue3-inote/commit/0bd5cfe240c85ed6909d24ed6e42d8a262bcbe9d)) 77 | 78 | 79 | 80 | ## [0.1.1](https://github.com/heiyehk/electron-vue3-inote/compare/0.1.0...0.1.1) (2021-01-11) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * 修复editorIcons.options文件命名问题 ([b0c2849](https://github.com/heiyehk/electron-vue3-inote/commit/b0c284994ce4656808fe080e78a17722be2af3fe)) 86 | 87 | 88 | 89 | # 0.1.0 (2020-12-25) 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import { app, protocol, BrowserWindow, globalShortcut } from 'electron'; 2 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; 3 | import path from 'path'; 4 | 5 | import { browserWindowOption, winURL, disabledKeys, userTasks } from './config'; 6 | import updater from './updater'; 7 | 8 | const startWindow = () => { 9 | const isDevelopment = process.env.NODE_ENV !== 'production'; 10 | let win: BrowserWindow | null; 11 | app.on('second-instance', () => { 12 | // 当运行第二个实例时,将会聚焦到win这个窗口 13 | if (win) { 14 | if (win.isMinimized()) win.restore(); 15 | win.focus(); 16 | } 17 | }); 18 | app.whenReady().then(() => { 19 | // 这个需要在app.ready触发之后使用 20 | protocol.registerFileProtocol('atom', (request, callback) => { 21 | const url = request.url.substring(7); 22 | callback(decodeURI(path.normalize(url))); 23 | }); 24 | }); 25 | 26 | // 将计划注册为标准将允许通过文件系统 API访问文件。否则,渲染器将为计划抛出一个安全错误。 27 | // 此方法只能在模块事件发出之前使用,并且只能调用一次。`ready app` 28 | protocol.registerSchemesAsPrivileged([ 29 | { 30 | scheme: 'app', 31 | privileges: { 32 | secure: true, 33 | standard: true 34 | } 35 | } 36 | ]); 37 | 38 | const createWindow = () => { 39 | // 如果有webpack启动的server 40 | if (process.env.WEBPACK_DEV_SERVER_URL) { 41 | win = new BrowserWindow(browserWindowOption()); 42 | // 默认打开webpack启动的serve 43 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 44 | if (!process.env.IS_TEST) win.webContents.openDevTools(); 45 | } else { 46 | const argv = process.argv[1]; 47 | // 注册一个协议 48 | createProtocol('app'); 49 | 50 | // 判断是否是新增 51 | if (argv === '--editor') { 52 | const editorWinOptions = browserWindowOption(); 53 | win = new BrowserWindow(editorWinOptions); 54 | win.loadURL(`${winURL}#/editor`); 55 | } else { 56 | win = new BrowserWindow(browserWindowOption()); 57 | win.loadURL(winURL); 58 | } 59 | } 60 | // win.webContents.openDevTools(); 61 | 62 | win.on('closed', () => { 63 | win = null; 64 | }); 65 | }; 66 | 67 | app.on('window-all-closed', () => { 68 | if (process.platform !== 'darwin') { 69 | app.quit(); 70 | } 71 | }); 72 | 73 | app.on('activate', () => { 74 | if (win === null) { 75 | createWindow(); 76 | } 77 | }); 78 | 79 | app.on('ready', async () => { 80 | // 快捷键禁用 81 | for (const key of disabledKeys()) { 82 | globalShortcut.register(key, () => void 0); 83 | } 84 | updater(); 85 | createWindow(); 86 | }); 87 | 88 | // TODO 待开发的内容 89 | app.setUserTasks(userTasks); 90 | 91 | if (isDevelopment) { 92 | if (process.platform === 'win32') { 93 | process.on('message', data => { 94 | if (data === 'graceful-exit') { 95 | app.quit(); 96 | } 97 | }); 98 | } else { 99 | process.on('SIGTERM', () => { 100 | app.quit(); 101 | }); 102 | } 103 | } 104 | }; 105 | 106 | export default startWindow; 107 | -------------------------------------------------------------------------------- /src/store/notes.state.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, remote } from 'electron'; 2 | import { ref, watch } from 'vue'; 3 | import { join, dirname } from 'path'; 4 | import { constImagesPath } from '@/config'; 5 | interface NotesState { 6 | [key: string]: any; 7 | syncDelay: number; 8 | serverAddress: string; 9 | serverToken: string; 10 | switchStatus: { 11 | [key: string]: any; 12 | /** 13 | * 开启提示 14 | */ 15 | textTip: boolean; 16 | 17 | /** 18 | * 删除确认 19 | */ 20 | deleteTip: boolean; 21 | 22 | /** 23 | * 自动沉浸 24 | */ 25 | autoNarrow: boolean; 26 | /** 27 | * 纯净模式 28 | */ 29 | autoNarrowPure: boolean; 30 | 31 | /** 32 | * 自动隐藏 33 | */ 34 | autoHide: boolean; 35 | 36 | /** 37 | * 打开同步 38 | */ 39 | openSync: boolean; 40 | }; 41 | /** 本地图片缓存地址 */ 42 | imagesCacheUrl: string; 43 | } 44 | 45 | const defaultNotesState: NotesState = { 46 | syncDelay: 100, 47 | serverAddress: '', 48 | serverToken: '', 49 | switchStatus: { 50 | /** 51 | * 开启提示 52 | */ 53 | textTip: true, 54 | 55 | /** 56 | * 删除确认 57 | */ 58 | deleteTip: false, 59 | 60 | /** 61 | * 自动缩小 62 | */ 63 | autoNarrow: false, 64 | /** 65 | * 缩小纯净模式 66 | */ 67 | autoNarrowPure: false, 68 | 69 | /** 70 | * 自动隐藏 71 | */ 72 | autoHide: false, 73 | 74 | /** 75 | * 打开同步 76 | */ 77 | openSync: false 78 | }, 79 | imagesCacheUrl: join(dirname(remote.app.getPath('exe')), constImagesPath) 80 | }; 81 | 82 | export const notesState = ref({} as NotesState); 83 | 84 | const getLocalValue = () => { 85 | if (localStorage.getItem('notesState')) { 86 | notesState.value = { ...notesState.value, ...JSON.parse(localStorage.getItem('notesState')!) }; 87 | } else { 88 | notesState.value = defaultNotesState; 89 | localStorage.setItem('notesState', JSON.stringify(defaultNotesState)); 90 | } 91 | }; 92 | getLocalValue(); 93 | const initialNotesStateLocal = () => { 94 | getLocalValue(); 95 | ipcRenderer.send('updateStorage'); 96 | }; 97 | 98 | const watchStateIPC = () => { 99 | const localJsonState = JSON.parse(localStorage.getItem('notesState')!) as NotesState; 100 | for (const keys of Object.keys(localJsonState)) { 101 | if (keys === 'switchStatus') { 102 | for (const item of Object.keys(localJsonState[keys])) { 103 | if (localJsonState.switchStatus[item] !== notesState.value.switchStatus[item]) { 104 | notesState.value.switchStatus[item] = localJsonState.switchStatus[item]; 105 | } 106 | } 107 | } 108 | } 109 | }; 110 | 111 | export const resetStore = () => { 112 | localStorage.clear(); 113 | localStorage.setItem('notesState', JSON.stringify(defaultNotesState)); 114 | ipcRenderer.send('updateStorage'); 115 | }; 116 | 117 | watch(() => localStorage.getItem('notesState'), initialNotesStateLocal); 118 | 119 | watch(notesState.value, e => { 120 | localStorage.setItem('notesState', JSON.stringify(e)); 121 | ipcRenderer.send('updateStorage'); 122 | }); 123 | 124 | remote.ipcMain.on('updateStorage', watchStateIPC); 125 | -------------------------------------------------------------------------------- /src/components/ISwitch.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 46 | 47 | 130 | -------------------------------------------------------------------------------- /public/font/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2233096 */ 3 | src: url('iconfont.woff2?t=1673061525635') format('woff2'), 4 | url('iconfont.woff?t=1673061525635') format('woff'), 5 | url('iconfont.ttf?t=1673061525635') format('truetype'), 6 | url('iconfont.svg?t=1673061525635#iconfont') format('svg'); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-tupian:before { 18 | content: "\e889"; 19 | } 20 | 21 | .icon-folderOpen:before { 22 | content: "\eabe"; 23 | } 24 | 25 | .icon-open:before { 26 | content: "\ea6b"; 27 | } 28 | 29 | .icon-qingchu:before { 30 | content: "\e747"; 31 | } 32 | 33 | .icon-narrow:before { 34 | content: "\e606"; 35 | } 36 | 37 | .icon-amplification:before { 38 | content: "\e607"; 39 | } 40 | 41 | .icon-copy1:before { 42 | content: "\e617"; 43 | } 44 | 45 | .icon-copy:before { 46 | content: "\e744"; 47 | } 48 | 49 | .icon-niantie:before { 50 | content: "\e610"; 51 | } 52 | 53 | .icon-code:before { 54 | content: "\e707"; 55 | } 56 | 57 | .icon-headline:before { 58 | content: "\e708"; 59 | } 60 | 61 | .icon-link:before { 62 | content: "\e701"; 63 | } 64 | 65 | .icon-arrow-up:before { 66 | content: "\e6f0"; 67 | } 68 | 69 | .icon-arrow-down:before { 70 | content: "\e6f1"; 71 | } 72 | 73 | .icon-kong_neirong:before { 74 | content: "\e6ce"; 75 | } 76 | 77 | .icon-newopen:before { 78 | content: "\e68d"; 79 | } 80 | 81 | .icon-delete:before { 82 | content: "\e605"; 83 | } 84 | 85 | .icon-editor-bold:before { 86 | content: "\e8ce"; 87 | } 88 | 89 | .icon-ol:before { 90 | content: "\e600"; 91 | } 92 | 93 | .icon-ul:before { 94 | content: "\e601"; 95 | } 96 | 97 | .icon-export:before { 98 | content: "\e802"; 99 | } 100 | 101 | .icon-import:before { 102 | content: "\e803"; 103 | } 104 | 105 | .icon-thepin-active:before { 106 | content: "\e715"; 107 | } 108 | 109 | .icon-italic:before { 110 | content: "\e6f7"; 111 | } 112 | 113 | .icon-underline:before { 114 | content: "\e6f8"; 115 | } 116 | 117 | .icon-strikethrough:before { 118 | content: "\e6fb"; 119 | } 120 | 121 | .icon-image:before { 122 | content: "\e702"; 123 | } 124 | 125 | .icon-unordered-list:before { 126 | content: "\e70e"; 127 | } 128 | 129 | .icon-ordered-list:before { 130 | content: "\e710"; 131 | } 132 | 133 | .icon-list:before { 134 | content: "\e61f"; 135 | } 136 | 137 | .icon-back:before { 138 | content: "\e636"; 139 | } 140 | 141 | .icon-thepin:before { 142 | content: "\e714"; 143 | } 144 | 145 | .icon-refresh:before { 146 | content: "\e602"; 147 | } 148 | 149 | .icon-close:before { 150 | content: "\e603"; 151 | } 152 | 153 | .icon-setting:before { 154 | content: "\e604"; 155 | } 156 | 157 | .icon-search:before { 158 | content: "\e60c"; 159 | } 160 | 161 | .icon-warning:before { 162 | content: "\e60e"; 163 | } 164 | 165 | .icon-mail:before { 166 | content: "\e60f"; 167 | } 168 | 169 | .icon-picture:before { 170 | content: "\e612"; 171 | } 172 | 173 | .icon-more:before { 174 | content: "\e62a"; 175 | } 176 | 177 | .icon-add:before { 178 | content: "\e62b"; 179 | } 180 | 181 | -------------------------------------------------------------------------------- /src/views/index/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 76 | 77 | 127 | -------------------------------------------------------------------------------- /src/components/IInput.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 88 | 89 | 151 | -------------------------------------------------------------------------------- /src/views/editor/components/ColorMask.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 74 | 75 | 177 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://juejin.cn/post/6953177188262740005 3 | */ 4 | const filetype: { [key: string]: string } = { 5 | '47494638': 'image/gif', 6 | '89504e47': 'image/png', 7 | ffd8ffe0: 'image/jpeg', 8 | ffd8ffe1: 'image/jpeg', 9 | ffd8ffe2: 'image/jpeg', 10 | ffd8ffe3: 'image/jpeg', 11 | ffd8ffe8: 'image/jpeg', 12 | '52494646{8,4}5745': 'image/webp', 13 | '52494646{8,4}4156': 'video/avi', 14 | '464C56': 'video/flv', 15 | '00000018': 'video/mp4', 16 | '00000020': 'video/mp4', 17 | '52494646,4143': 'ani', 18 | '52494646,4344': 'cda', 19 | '52494646,514c': 'qcp' 20 | }; 21 | 22 | /** 23 | * 文件转buffer 24 | * @param file 25 | * @returns 26 | */ 27 | export const fileToBuffer = async (file: File | Blob): Promise => { 28 | return new Promise(resolve => { 29 | const fr = new FileReader(); 30 | 31 | fr.readAsArrayBuffer(file); 32 | fr.onloadend = () => resolve(fr.result as ArrayBuffer); 33 | }); 34 | }; 35 | 36 | /** 37 | * 图片节点转base64 38 | * @param src 39 | * @param outputFormat 40 | * @returns 41 | */ 42 | export const convertImgToBase64 = async (src: string, outputFormat: string): Promise => { 43 | let canvas: HTMLCanvasElement | null = document.createElement('canvas'); 44 | const ctx = canvas.getContext('2d'); 45 | let img: HTMLImageElement | null = document.createElement('img'); 46 | img.crossOrigin = 'Anonymous'; 47 | img.src = src; 48 | return new Promise(resolve => { 49 | img!.onload = () => { 50 | canvas!.height = img!.height; 51 | canvas!.width = img!.width; 52 | ctx!.drawImage(img!, 0, 0); 53 | const dataURL = canvas!.toDataURL(outputFormat || 'image/png'); 54 | resolve(dataURL); 55 | canvas = null; 56 | img = null; 57 | }; 58 | }); 59 | }; 60 | 61 | /** 62 | * base64转blob 63 | * @param dataURL 64 | * @param type 65 | * @returns 66 | */ 67 | export const convertBase64UrlToBlob = (dataURL: string, type: string): Promise => { 68 | return new Promise(resolve => { 69 | let bytes = null; 70 | if (dataURL.split(',').length > 1) { 71 | // 是否带前缀 72 | bytes = window.atob(dataURL.split(',')[1]); // 去掉url的头,并转换为byte 73 | } else { 74 | bytes = window.atob(dataURL); 75 | } 76 | // 处理异常,将ascii码小于0的转换为大于0 77 | const ab = new ArrayBuffer(bytes.length); 78 | const ia = new Uint8Array(ab); 79 | for (let i = 0; i < bytes.length; i++) { 80 | ia[i] = bytes.charCodeAt(i); 81 | } 82 | resolve(new Blob([ab], { type })); 83 | }); 84 | }; 85 | 86 | // const handleFileType = (value16: string) => { 87 | // const header3 = value16.substring(0, 3 * 2); 88 | // const header4 = value16.substring(0, 4 * 2); 89 | // const header8 = value16.substring(0, 16 * 2); 90 | // let type = filetype[header4] || filetype[header3] || ''; 91 | // if (type === '') { 92 | // for (let key of Object.keys(filetype)) { 93 | // let arr = key.split(/\{\d+,\d+\}/, 2); 94 | // if (!arr[1]) { 95 | // continue; 96 | // } 97 | // if (header8.substring(0, arr[0].length) === arr[0]) { 98 | // const siteArr = key.match(/\{(\d+,\d+)\}/)[1].split(',').map(e => e | 0); 99 | // const startSite = arr[1].length + siteArr[0] + 2 100 | // if (header8.substring(startSite, startSite + siteArr[1]) === arr[1]) { 101 | // type = filetype[key]; 102 | // break; 103 | // } 104 | // } else { 105 | // continue; 106 | // } 107 | // } 108 | // } 109 | // return type; 110 | // } 111 | // const getHeaderValue = (value: ArrayBuffer) => { 112 | // const arr = new Uint8Array(value); 113 | // let header = ''; 114 | // for (let i = 0; i < arr.length; i++) { 115 | // const v = arr[i].toString(16); 116 | // header += v.length === 1 && v === '0' ? '0' + v : v; 117 | // } 118 | // return header; 119 | // } 120 | 121 | /** 122 | * 复制图片 123 | * @param dom 124 | */ 125 | export const copyImage = async (dom: HTMLImageElement) => { 126 | const base64Url = await convertImgToBase64(dom.src, 'image/png'); 127 | const blob = await convertBase64UrlToBlob(base64Url, 'image/png'); 128 | // 向剪切板写入流数据 129 | navigator.clipboard.write([ 130 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 131 | // @ts-ignore 132 | new ClipboardItem({ 133 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 134 | // @ts-ignore 135 | [blob.type]: blob 136 | }) 137 | ]); 138 | }; 139 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* font-family: 'PingFang SC Regular', 'sans-serif', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif; */ 3 | font-family: Avenir,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol',sans-serif; 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | html, 9 | body, 10 | div, 11 | object, 12 | iframe, 13 | applet, 14 | object, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | p, 22 | blockquote, 23 | pre, 24 | address, 25 | dl, 26 | dt, 27 | dd, 28 | ol, 29 | ul, 30 | li, 31 | table, 32 | caption, 33 | tbody, 34 | tfoot, 35 | thead, 36 | tr, 37 | th, 38 | td, 39 | article, 40 | aside, 41 | canvas, 42 | details, 43 | embed, 44 | figure, 45 | figcaption, 46 | footer, 47 | header, 48 | menu, 49 | nav, 50 | output, 51 | ruby, 52 | section, 53 | summary, 54 | time, 55 | mark, 56 | audio, 57 | video, 58 | progress { 59 | margin: 0; 60 | padding: 0; 61 | border: 0; 62 | vertical-align: baseline; 63 | } 64 | 65 | article, 66 | aside, 67 | details, 68 | figcaption, 69 | figure, 70 | footer, 71 | header, 72 | main, 73 | menu, 74 | nav, 75 | section, 76 | summary { 77 | display: block; 78 | } 79 | 80 | audio, 81 | canvas, 82 | progress, 83 | video { 84 | display: inline-block; 85 | } 86 | 87 | audio:not([controls]) { 88 | display: none; 89 | height: 0; 90 | } 91 | 92 | [hidden], 93 | template { 94 | display: none; 95 | } 96 | 97 | a { 98 | background-color: transparent; 99 | text-decoration: none; 100 | } 101 | 102 | a:active, 103 | a:hover { 104 | outline: 0; 105 | } 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | b, 112 | strong { 113 | font-weight: bold; 114 | } 115 | 116 | dfn { 117 | font-style: italic; 118 | } 119 | 120 | h1 { 121 | font-size: 2em; 122 | margin: 0.67em 0; 123 | } 124 | 125 | mark { 126 | background: #ff0; 127 | color: #000; 128 | } 129 | 130 | small { 131 | font-size: 80%; 132 | } 133 | 134 | sub, 135 | sup { 136 | font-size: 75%; 137 | line-height: 0; 138 | position: relative; 139 | vertical-align: baseline; 140 | } 141 | 142 | sup { 143 | top: -0.5em; 144 | } 145 | 146 | sub { 147 | bottom: -0.25em; 148 | } 149 | 150 | img { 151 | border: 0; 152 | } 153 | 154 | svg:not(:root) { 155 | overflow: hidden; 156 | } 157 | 158 | figure { 159 | margin: 1em 40px; 160 | } 161 | 162 | hr { 163 | -moz-box-sizing: content-box; 164 | box-sizing: content-box; 165 | height: 0; 166 | } 167 | 168 | pre { 169 | overflow: auto; 170 | } 171 | 172 | code, 173 | kbd, 174 | pre, 175 | samp { 176 | font-size: 1em; 177 | } 178 | 179 | button, 180 | input, 181 | optgroup, 182 | select, 183 | textarea { 184 | color: inherit; 185 | font: inherit; 186 | margin: 0; 187 | outline: none; 188 | line-height: normal; 189 | } 190 | 191 | button { 192 | overflow: visible; 193 | } 194 | 195 | button, 196 | select { 197 | text-transform: none; 198 | } 199 | 200 | button, 201 | html input[type='button'], 202 | input[type='reset'], 203 | input[type='submit'] { 204 | -webkit-appearance: button; 205 | cursor: pointer; 206 | } 207 | 208 | button[disabled], 209 | html input[disabled] { 210 | cursor: default; 211 | } 212 | 213 | button::-moz-focus-inner, 214 | input::-moz-focus-inner { 215 | border: 0; 216 | padding: 0; 217 | } 218 | 219 | input { 220 | line-height: normal; 221 | } 222 | 223 | input[type='checkbox'], 224 | input[type='radio'] { 225 | box-sizing: border-box; 226 | padding: 0; 227 | } 228 | 229 | input[type='number']::-webkit-inner-spin-button, 230 | input[type='number']::-webkit-outer-spin-button { 231 | height: auto; 232 | } 233 | 234 | input[type='search'] { 235 | -webkit-appearance: textfield; 236 | -moz-box-sizing: content-box; 237 | -webkit-box-sizing: content-box; 238 | box-sizing: content-box; 239 | } 240 | 241 | input[type='search']::-webkit-search-cancel-button, 242 | input[type='search']::-webkit-search-decoration { 243 | -webkit-appearance: none; 244 | } 245 | 246 | fieldset { 247 | border: 1px solid silver; 248 | margin: 0 2px; 249 | padding: 0.35em 0.625em 0.75em; 250 | } 251 | 252 | legend { 253 | border: 0; 254 | padding: 0; 255 | } 256 | 257 | textarea { 258 | overflow: auto; 259 | } 260 | 261 | optgroup { 262 | font-weight: bold; 263 | } 264 | 265 | table { 266 | border-collapse: collapse; 267 | border-spacing: 0; 268 | } 269 | 270 | td, 271 | th { 272 | padding: 0; 273 | } 274 | 275 | /* 清除ie表单自带icon*/ 276 | ::-ms-clear, 277 | ::-ms-reveal { 278 | display: none; 279 | } 280 | 281 | /* 选择历史记录的文字颜色和背景颜色 */ 282 | input:-webkit-autofill { 283 | -webkit-animation: autofill-fix 1s infinite !important; 284 | -webkit-text-fill-color: #666; 285 | -webkit-transition: background-color 50000s ease-in-out 0s !important; 286 | transition: background-color 50000s ease-in-out 0s !important; 287 | background-color: transparent !important; 288 | background-image: none !important; 289 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important; 290 | } 291 | 292 | [role='button'], 293 | a, 294 | area, 295 | button, 296 | input:not([type='range']), 297 | label, 298 | select, 299 | summary, 300 | textarea { 301 | -ms-touch-action: manipulation; 302 | touch-action: manipulation; 303 | } 304 | 305 | input[type='number'], 306 | input[type='password'], 307 | input[type='text'], 308 | textarea { 309 | -webkit-appearance: none; 310 | } 311 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { browserWindowOption, winURL } from '@/config'; 2 | import { BrowserWindow, remote } from 'electron'; 3 | import { enc, AES, mode, pad } from 'crypto-js'; 4 | 5 | type FunctionalControl = (this: any, fn: any, delay?: number) => (...args: any) => void; 6 | type DebounceEvent = FunctionalControl; 7 | type ThrottleEvent = FunctionalControl; 8 | 9 | // 防抖函数 10 | export const debounce: DebounceEvent = function(fn, delay = 1000) { 11 | let timer: NodeJS.Timeout | null = null; 12 | return (...args: any) => { 13 | if (timer) clearTimeout(timer); 14 | timer = setTimeout(() => { 15 | fn.apply(this, args); 16 | }, delay); 17 | }; 18 | }; 19 | 20 | // 节流函数 21 | export const throttle: ThrottleEvent = function(fn, delay = 500) { 22 | let flag = true; 23 | return (...args: any) => { 24 | if (!flag) return; 25 | flag = false; 26 | setTimeout(() => { 27 | fn.apply(this, args); 28 | flag = true; 29 | }, delay); 30 | }; 31 | }; 32 | 33 | // 创建窗口 34 | export const createBrowserWindow = ( 35 | bwopt = {} as Electron.BrowserWindowConstructorOptions, 36 | url = '/', 37 | devTools = false 38 | ): BrowserWindow | null => { 39 | let childrenWindow: BrowserWindow | null; 40 | childrenWindow = new remote.BrowserWindow(bwopt); 41 | 42 | if (process.env.NODE_ENV === 'development' && devTools) { 43 | childrenWindow.webContents.openDevTools(); 44 | } 45 | childrenWindow.loadURL(`${winURL}/#${url}`); 46 | childrenWindow.on('closed', () => { 47 | childrenWindow = null; 48 | }); 49 | // childrenWindow.webContents.openDevTools(); 50 | return childrenWindow; 51 | }; 52 | 53 | // 过渡关闭窗口 54 | export const transitCloseWindow = (): void => { 55 | document.querySelector('#app')?.classList.remove('app-show'); 56 | document.querySelector('#app')?.classList.add('app-hide'); 57 | remote.getCurrentWindow().close(); 58 | }; 59 | 60 | // uuid 61 | export const uuid = (): string => { 62 | const S4 = () => { 63 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 64 | }; 65 | return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); 66 | }; 67 | 68 | export const symbolKey = Symbol('key'); 69 | export const symbolIv = Symbol('iv'); 70 | export const symbolEncrypt = Symbol('encrypt'); 71 | export const symbolDecode = Symbol('decode'); 72 | export const algorithm = { 73 | [symbolKey]: enc.Utf8.parse('1234123412ABCDEF'), // 十六位十六进制数作为密钥 74 | [symbolIv]: enc.Utf8.parse('ABCDEF1234123412'), // 十六位十六进制数作为密钥偏移量 75 | // 加密 76 | [symbolEncrypt]: (word: string) => { 77 | const srcs = enc.Utf8.parse(word); 78 | const encrypted = AES.encrypt(srcs, algorithm[symbolKey], { 79 | iv: algorithm[symbolIv], 80 | mode: mode.CBC, 81 | padding: pad.Pkcs7 82 | }); 83 | return encrypted.ciphertext.toString().toUpperCase(); 84 | }, 85 | // 解密 86 | [symbolDecode]: (word: string) => { 87 | const encryptedHexStr = enc.Hex.parse(word); 88 | const srcs = enc.Base64.stringify(encryptedHexStr); 89 | const decrypt = AES.decrypt(srcs, algorithm[symbolKey], { 90 | iv: algorithm[symbolIv], 91 | mode: mode.CBC, 92 | padding: pad.Pkcs7 93 | }); 94 | const decryptedStr = decrypt.toString(enc.Utf8); 95 | return decryptedStr.toString(); 96 | } 97 | }; 98 | 99 | export interface TwiceHandle { 100 | keydownInterval: NodeJS.Timer | null; 101 | intervalCount: number; 102 | keydownCount: number; 103 | start: (fn: () => void) => void; 104 | } 105 | 106 | /** 107 | * 在300毫秒内触发2次事件的callback 108 | */ 109 | export const twiceHandle: TwiceHandle = { 110 | keydownInterval: null, 111 | intervalCount: 0, 112 | keydownCount: 0, 113 | start(fn) { 114 | if (!this.keydownInterval) { 115 | this.intervalCount += 1; 116 | this.keydownInterval = setInterval(() => { 117 | if (this.intervalCount > 5) { 118 | clearInterval(this.keydownInterval as NodeJS.Timer); 119 | this.keydownInterval = null; 120 | this.intervalCount = 0; 121 | this.keydownCount = 0; 122 | } else { 123 | this.intervalCount += 1; 124 | if (this.keydownCount >= 2) { 125 | clearInterval(this.keydownInterval as NodeJS.Timer); 126 | this.keydownInterval = null; 127 | this.intervalCount = 0; 128 | this.keydownCount = 0; 129 | fn(); 130 | } 131 | } 132 | }, 50); 133 | } 134 | 135 | if (this.keydownCount <= 2) { 136 | this.keydownCount += 1; 137 | } 138 | } 139 | }; 140 | 141 | export const openImageAsNewWindow = (img: Element) => { 142 | const devicePixelRatio = window.devicePixelRatio; 143 | const { availWidth, availHeight } = window.screen; 144 | const naturalWidth = (img as HTMLImageElement).naturalWidth / devicePixelRatio; 145 | const naturalHeight = (img as HTMLImageElement).naturalHeight / devicePixelRatio; 146 | const winWidth = naturalWidth < 500 ? 500 : naturalWidth; 147 | const winHeight = naturalHeight < 300 ? 300 : naturalHeight; 148 | const winOptWidth = winWidth > availWidth ? availWidth : winWidth; 149 | const winOptHeight = winHeight > availHeight ? availHeight : winHeight; 150 | 151 | createBrowserWindow( 152 | { 153 | ...browserWindowOption(), 154 | width: winOptWidth, 155 | height: winOptHeight, 156 | minWidth: winWidth, 157 | minHeight: winHeight 158 | }, 159 | `/image-preview?src=${(img as HTMLImageElement).src}`, 160 | false 161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /src/components/IRightClick/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h, App, VNode, RendererElement, RendererNode } from 'vue'; 2 | import './index.css'; 3 | 4 | type ClassName = string | string[] | (() => string | string[]); 5 | 6 | export interface MenuOptions { 7 | /** 文本 */ 8 | text: string; 9 | 10 | /** 是否在使用后就关闭 */ 11 | once?: boolean; 12 | 13 | /** 单独的样式名 */ 14 | className?: ClassName; 15 | 16 | disabled?: boolean | (() => boolean) | (() => Promise); 17 | 18 | /** 图标样式名 */ 19 | iconName?: ClassName; 20 | 21 | /** 函数 */ 22 | handler?: (e: Event) => void; 23 | 24 | render?: boolean | (() => boolean) | (() => Promise); 25 | } 26 | 27 | type RenderVNode = VNode< 28 | RendererNode, 29 | RendererElement, 30 | { 31 | [key: string]: any; 32 | } 33 | >; 34 | 35 | class CreateRightClick { 36 | rightClickEl?: App; 37 | rightClickElBox?: HTMLDivElement | null; 38 | 39 | constructor() { 40 | this.removeRightClickHandler(); 41 | } 42 | 43 | /** 44 | * 渲染dom 45 | * @param menu 46 | */ 47 | render(menu: MenuOptions[]): RenderVNode { 48 | const renderLiEl = (item: MenuOptions) => { 49 | // 判断是否禁用了 50 | let isDisabled = false; 51 | 52 | const isDisabledHandle = async () => { 53 | if (typeof item.disabled === 'function') { 54 | isDisabled = await (item.disabled() as Promise); 55 | } 56 | 57 | if (typeof item.disabled === 'boolean') { 58 | isDisabled = item.disabled; 59 | } 60 | }; 61 | 62 | isDisabledHandle(); 63 | 64 | const className = () => { 65 | let classNameStr = ''; 66 | if (isDisabled) classNameStr = 'disabled-item'; 67 | 68 | if (typeof item.className === 'function') classNameStr += ` ${item.className()}`; 69 | 70 | if (item.className) classNameStr += ` ${item.className}`; 71 | 72 | return classNameStr; 73 | }; 74 | 75 | return h( 76 | 'li', 77 | { 78 | class: className(), 79 | // vue3.x中简化了render,直接onclick即可,onClick也可以 80 | onclick: (e: Event) => { 81 | if (isDisabled) return; 82 | 83 | // 如果只是一次,那么点击之后直接关闭 84 | if (item.once) this.remove(); 85 | 86 | if (item.handler) { 87 | return item.handler(e); 88 | } 89 | } 90 | }, 91 | [ 92 | // icon 93 | h('i', { 94 | class: item.iconName 95 | }), 96 | // text 97 | h( 98 | 'span', 99 | { 100 | class: 'right-click-menu-text' 101 | }, 102 | item.text 103 | ) 104 | ] 105 | ); 106 | }; 107 | 108 | return h( 109 | 'ul', 110 | { 111 | class: ['right-click-menu-list'] 112 | }, 113 | [...menu.map(renderLiEl)] 114 | ); 115 | } 116 | 117 | /** 118 | * 给右键的样式 119 | * @param event 鼠标事件 120 | */ 121 | setRightClickElStyle(event: MouseEvent, len: number): void { 122 | if (!this.rightClickElBox) return; 123 | this.rightClickElBox.style.height = `${len * 36}px`; 124 | const { clientX, clientY } = event; 125 | const { innerWidth, innerHeight } = window; 126 | const { clientWidth, clientHeight } = this.rightClickElBox; 127 | let cssText = `height: ${len * 36}px;opacity: 1;transition: all 0.2s;`; 128 | if (clientX + clientWidth < innerWidth) { 129 | cssText += `left: ${clientX + 2}px;`; 130 | } else { 131 | cssText += `left: ${clientX - clientWidth}px;`; 132 | } 133 | if (clientY + clientHeight < innerHeight) { 134 | cssText += `top: ${clientY + 2}px;`; 135 | } else { 136 | cssText += `top: ${clientY - clientHeight}px;`; 137 | } 138 | cssText += `height: ${len * 36}px`; 139 | this.rightClickElBox.style.cssText = cssText; 140 | } 141 | 142 | remove(): void { 143 | if (this.rightClickElBox) { 144 | this.rightClickElBox.remove(); 145 | this.rightClickElBox = null; 146 | } 147 | } 148 | 149 | removeRightClickHandler(): void { 150 | document.addEventListener('click', e => { 151 | if (this.rightClickElBox) { 152 | const currentEl = e.target as Node; 153 | if (!currentEl || !this.rightClickElBox.contains(currentEl)) { 154 | this.remove(); 155 | } 156 | } 157 | }); 158 | } 159 | 160 | /** 161 | * 鼠标右键悬浮 162 | * @param event 163 | * @param menu 164 | */ 165 | useRightClick = (event: MouseEvent, menu: MenuOptions[] = []): void => { 166 | this.remove(); 167 | const filterMenuMap = menu.filter(item => { 168 | if (item.render !== undefined) { 169 | if (item.render === true) { 170 | return item; 171 | } 172 | if (typeof item.render === 'function' && item.render() === true) { 173 | return item; 174 | } 175 | } else { 176 | return item; 177 | } 178 | }); 179 | if (!this.rightClickElBox || !this.rightClickEl) { 180 | const createRender = this.render(filterMenuMap); 181 | this.rightClickEl = createApp({ 182 | setup() { 183 | return () => createRender; 184 | } 185 | }); 186 | } 187 | if (!this.rightClickElBox) { 188 | this.rightClickElBox = document.createElement('div'); 189 | this.rightClickElBox.id = 'rightClick'; 190 | document.body.appendChild(this.rightClickElBox); 191 | this.rightClickEl.mount('#rightClick'); 192 | } 193 | this.setRightClickElStyle(event, filterMenuMap.length); 194 | }; 195 | } 196 | 197 | export default CreateRightClick; 198 | -------------------------------------------------------------------------------- /src/components/IHeader.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 119 | 120 | 209 | -------------------------------------------------------------------------------- /public/css/common.css: -------------------------------------------------------------------------------- 1 | /* 空元素显示的内容 */ 2 | .empty-content:empty::before { 3 | /* content: attr(placeholder); */ 4 | content: '记笔记...'; 5 | font-size: 14px; 6 | color: #666; 7 | line-height: 21px; 8 | } 9 | 10 | /* 隐藏滚动条 */ 11 | ::-webkit-scrollbar { 12 | width: 0; 13 | height: 0; 14 | } 15 | 16 | /* 设置ol显示格式 */ 17 | .module-editor ol { 18 | counter-reset: sectioncounter; 19 | } 20 | 21 | .module-editor ol li { 22 | list-style: none; 23 | position: relative; 24 | } 25 | 26 | .module-editor ol li::before { 27 | content: counter(sectioncounter) '.'; 28 | counter-increment: sectioncounter; 29 | margin-right: 10px; 30 | } 31 | 32 | /* 使用自定义伪类会导致光标偏移向下 */ 33 | /* .module-editor ul { 34 | position: relative; 35 | } 36 | 37 | .module-editor ul li { 38 | list-style-type: none; 39 | word-break: break-all; 40 | } 41 | 42 | .module-editor ul li::before { 43 | content: ''; 44 | width: 5px; 45 | height: 5px; 46 | background-color: #000; 47 | margin-right: 6px; 48 | display: inline-block; 49 | border-radius: 100%; 50 | transform: translateY(-2px); 51 | margin-left: 1px; 52 | } */ 53 | 54 | .module-editor ul li { 55 | word-break: break-all; 56 | list-style: disc inside; 57 | } 58 | 59 | /* 常用flex布局 */ 60 | .flex { 61 | display: flex; 62 | } 63 | 64 | .flex-center { 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | } 69 | 70 | .flex-left { 71 | display: flex; 72 | justify-content: center; 73 | align-items: flex-start; 74 | } 75 | 76 | .flex-right { 77 | display: flex; 78 | justify-content: center; 79 | align-items: flex-end; 80 | } 81 | 82 | .flex-items { 83 | display: flex; 84 | align-items: center; 85 | } 86 | 87 | .flex-between { 88 | display: flex; 89 | justify-content: space-between; 90 | align-items: center; 91 | } 92 | 93 | .flex1 { 94 | flex: 1; 95 | } 96 | 97 | /* ellips */ 98 | .hidden { 99 | overflow: hidden; 100 | white-space: nowrap; 101 | text-overflow: ellipsis; 102 | } 103 | 104 | html, 105 | body, 106 | .app, 107 | .transition, 108 | .bg-white { 109 | width: 100%; 110 | height: 100%; 111 | box-sizing: border-box; 112 | position: relative; 113 | overflow: hidden; 114 | background-color: rgba(0, 0, 0, 0); 115 | outline: none; 116 | } 117 | 118 | .bg-white { 119 | background-color: #fff; 120 | } 121 | 122 | /* 软件阴影 */ 123 | .app { 124 | box-shadow: 0 0 4px rgb(185, 185, 185); 125 | } 126 | 127 | body { 128 | padding: 4px; 129 | user-select: none; 130 | outline: none; 131 | } 132 | 133 | @keyframes fadein { 134 | 0% { 135 | transform: scale(0.8); 136 | opacity: 0; 137 | } 138 | 100% { 139 | transform: scale(1); 140 | opacity: 1; 141 | } 142 | } 143 | 144 | @keyframes fadeout { 145 | 0% { 146 | transform: scale(1); 147 | opacity: 1; 148 | } 149 | 100% { 150 | transform: scale(0.9); 151 | opacity: 0; 152 | } 153 | } 154 | 155 | /* 进入和退出动效 */ 156 | .app-show { 157 | animation: fadein 0.4s forwards; 158 | transform: scale(1) !important; 159 | } 160 | 161 | .app-hide { 162 | animation: fadeout 0.2s forwards; 163 | transform: scale(0.9); 164 | } 165 | 166 | /* 颜色 */ 167 | .white-content { 168 | transition: background-color 0.4s; 169 | background-color: #ffffff !important; 170 | } 171 | 172 | .yellow-content { 173 | transition: background-color 0.4s; 174 | background-color: #fff7d1 !important; 175 | } 176 | 177 | .green-content { 178 | transition: background-color 0.4s; 179 | background-color: #e4f9e0 !important; 180 | } 181 | 182 | .pink-content { 183 | transition: background-color 0.4s; 184 | background-color: #ffe4f1 !important; 185 | } 186 | 187 | .purple-content { 188 | transition: background-color 0.4s; 189 | background-color: #f2e6ff !important; 190 | } 191 | 192 | .blue-content { 193 | transition: background-color 0.4s; 194 | background-color: #e2f1ff !important; 195 | } 196 | 197 | .gray-content { 198 | transition: background-color 0.4s; 199 | background-color: #f3f2f1 !important; 200 | } 201 | 202 | .black-content { 203 | transition: background-color 0.4s; 204 | background-color: #696969 !important; 205 | /* color: #fff; */ 206 | } 207 | 208 | 209 | /* 针对富文本做特定的样式 */ 210 | .black-content .vditor-copy { 211 | color: #24292D !important; 212 | } 213 | .black-content .vditor-copy span { 214 | color: #24292D !important; 215 | } 216 | .black-content .vditor-copy path { 217 | color: #24292D !important; 218 | } 219 | 220 | .black-content .vditor-ir__node--expand .vditor-ir__marker--heading { 221 | color: #fff !important; 222 | } 223 | 224 | .black-content * { 225 | color: #fff; 226 | } 227 | 228 | .window-blur-hide .vditor-toolbar { 229 | top: 40px !important; 230 | height: 0 !important; 231 | transition: top 0.4s, height 0.4s; 232 | } 233 | 234 | .vditor-toolbar { 235 | box-shadow: 0 0 2px #ccc; 236 | } 237 | 238 | .vditor-reset p { 239 | margin-bottom: 8px; 240 | } 241 | .vditor-reset img { 242 | cursor: pointer; 243 | } 244 | .page-editor:not(.white-content) .vditor-reset blockquote { 245 | border-left-color: #ababab !important; 246 | } 247 | .page-editor:not(.white-content) .vditor-reset h1, .vditor-reset h2 { 248 | border-bottom-color: #ababab !important; 249 | } 250 | .page-editor:not(.white-content) .vditor-reset hr { 251 | background-color: #d2d2d2 !important; 252 | } 253 | 254 | .vditor-img { 255 | top: 40px !important; 256 | max-width: 100% !important; 257 | max-height: 100% !important; 258 | } 259 | 260 | .vditor-img__img img { 261 | max-width: 100% !important; 262 | } 263 | 264 | 265 | @media (prefers-color-scheme: dark) { 266 | body, 267 | .header, 268 | body .bg-white, 269 | body .page-index, 270 | body .page-setting, 271 | body .page-editor, 272 | body .bottom-editor-tools, 273 | body .options-container .options-content, /* 遮罩列表 */ 274 | #rightClick { 275 | background-color: #333 !important; 276 | color: white !important; 277 | transition: background-color 0.4s; 278 | } 279 | body .options-container .options-content * { 280 | color: white !important; 281 | } 282 | /* 头部 */ 283 | .header a { 284 | color: white !important; 285 | } 286 | /* 编辑遮罩 */ 287 | body .options-container .options-cover { 288 | background-color: rgba(0, 0, 0, 0.6) !important; 289 | } 290 | /* 右键列表 */ 291 | .right-click-menu-list li:hover { 292 | background-color: rgba(0, 0, 0, 0.4) !important; 293 | } 294 | body .about-app-description, 295 | body .disabled-line { 296 | color: #828282; 297 | } 298 | } 299 | 300 | @media (prefers-color-scheme: light) { 301 | body { 302 | /* background-color: #fff !important; */ 303 | color: black !important; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/views/setting/index.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 154 | 155 | 220 | -------------------------------------------------------------------------------- /public/font/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2233096", 3 | "name": "notes", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "2076242", 10 | "name": " 图片", 11 | "font_class": "tupian", 12 | "unicode": "e889", 13 | "unicode_decimal": 59529 14 | }, 15 | { 16 | "icon_id": "7594806", 17 | "name": "24gl-folderOpen", 18 | "font_class": "folderOpen", 19 | "unicode": "eabe", 20 | "unicode_decimal": 60094 21 | }, 22 | { 23 | "icon_id": "7594037", 24 | "name": "24gl-minimize", 25 | "font_class": "open", 26 | "unicode": "ea6b", 27 | "unicode_decimal": 60011 28 | }, 29 | { 30 | "icon_id": "577352", 31 | "name": "清除", 32 | "font_class": "qingchu", 33 | "unicode": "e747", 34 | "unicode_decimal": 59207 35 | }, 36 | { 37 | "icon_id": "3217815", 38 | "name": "缩放", 39 | "font_class": "narrow", 40 | "unicode": "e606", 41 | "unicode_decimal": 58886 42 | }, 43 | { 44 | "icon_id": "3217822", 45 | "name": "全屏", 46 | "font_class": "amplification", 47 | "unicode": "e607", 48 | "unicode_decimal": 58887 49 | }, 50 | { 51 | "icon_id": "5772874", 52 | "name": "复制", 53 | "font_class": "copy1", 54 | "unicode": "e617", 55 | "unicode_decimal": 58903 56 | }, 57 | { 58 | "icon_id": "657583", 59 | "name": "copy", 60 | "font_class": "copy", 61 | "unicode": "e744", 62 | "unicode_decimal": 59204 63 | }, 64 | { 65 | "icon_id": "12719945", 66 | "name": "粘贴", 67 | "font_class": "niantie", 68 | "unicode": "e610", 69 | "unicode_decimal": 58896 70 | }, 71 | { 72 | "icon_id": "13479730", 73 | "name": "code", 74 | "font_class": "code", 75 | "unicode": "e707", 76 | "unicode_decimal": 59143 77 | }, 78 | { 79 | "icon_id": "13479755", 80 | "name": "headline", 81 | "font_class": "headline", 82 | "unicode": "e708", 83 | "unicode_decimal": 59144 84 | }, 85 | { 86 | "icon_id": "18531285", 87 | "name": "link", 88 | "font_class": "link", 89 | "unicode": "e701", 90 | "unicode_decimal": 59137 91 | }, 92 | { 93 | "icon_id": "18406194", 94 | "name": "arrow-up", 95 | "font_class": "arrow-up", 96 | "unicode": "e6f0", 97 | "unicode_decimal": 59120 98 | }, 99 | { 100 | "icon_id": "18406197", 101 | "name": "arrow-down", 102 | "font_class": "arrow-down", 103 | "unicode": "e6f1", 104 | "unicode_decimal": 59121 105 | }, 106 | { 107 | "icon_id": "1888665", 108 | "name": "空_内容", 109 | "font_class": "kong_neirong", 110 | "unicode": "e6ce", 111 | "unicode_decimal": 59086 112 | }, 113 | { 114 | "icon_id": "619187", 115 | "name": "图标1_新窗口打开", 116 | "font_class": "newopen", 117 | "unicode": "e68d", 118 | "unicode_decimal": 59021 119 | }, 120 | { 121 | "icon_id": "8144689", 122 | "name": "垃圾桶", 123 | "font_class": "delete", 124 | "unicode": "e605", 125 | "unicode_decimal": 58885 126 | }, 127 | { 128 | "icon_id": "16554097", 129 | "name": "editor-bold", 130 | "font_class": "editor-bold", 131 | "unicode": "e8ce", 132 | "unicode_decimal": 59598 133 | }, 134 | { 135 | "icon_id": "7617106", 136 | "name": "ol", 137 | "font_class": "ol", 138 | "unicode": "e600", 139 | "unicode_decimal": 58880 140 | }, 141 | { 142 | "icon_id": "7617111", 143 | "name": "ul", 144 | "font_class": "ul", 145 | "unicode": "e601", 146 | "unicode_decimal": 58881 147 | }, 148 | { 149 | "icon_id": "1917135", 150 | "name": "导出文件", 151 | "font_class": "export", 152 | "unicode": "e802", 153 | "unicode_decimal": 59394 154 | }, 155 | { 156 | "icon_id": "1917138", 157 | "name": "导入文件", 158 | "font_class": "import", 159 | "unicode": "e803", 160 | "unicode_decimal": 59395 161 | }, 162 | { 163 | "icon_id": "12567143", 164 | "name": "图钉选中", 165 | "font_class": "thepin-active", 166 | "unicode": "e715", 167 | "unicode_decimal": 59157 168 | }, 169 | { 170 | "icon_id": "13479053", 171 | "name": "italic", 172 | "font_class": "italic", 173 | "unicode": "e6f7", 174 | "unicode_decimal": 59127 175 | }, 176 | { 177 | "icon_id": "13479115", 178 | "name": "underline", 179 | "font_class": "underline", 180 | "unicode": "e6f8", 181 | "unicode_decimal": 59128 182 | }, 183 | { 184 | "icon_id": "13479356", 185 | "name": "strikethrough", 186 | "font_class": "strikethrough", 187 | "unicode": "e6fb", 188 | "unicode_decimal": 59131 189 | }, 190 | { 191 | "icon_id": "13479631", 192 | "name": "image", 193 | "font_class": "image", 194 | "unicode": "e702", 195 | "unicode_decimal": 59138 196 | }, 197 | { 198 | "icon_id": "13480098", 199 | "name": "unordered-list", 200 | "font_class": "unordered-list", 201 | "unicode": "e70e", 202 | "unicode_decimal": 59150 203 | }, 204 | { 205 | "icon_id": "13480129", 206 | "name": "ordered-list", 207 | "font_class": "ordered-list", 208 | "unicode": "e710", 209 | "unicode_decimal": 59152 210 | }, 211 | { 212 | "icon_id": "3217868", 213 | "name": "分类", 214 | "font_class": "list", 215 | "unicode": "e61f", 216 | "unicode_decimal": 58911 217 | }, 218 | { 219 | "icon_id": "4432162", 220 | "name": "上一步", 221 | "font_class": "back", 222 | "unicode": "e636", 223 | "unicode_decimal": 58934 224 | }, 225 | { 226 | "icon_id": "12567144", 227 | "name": "图钉", 228 | "font_class": "thepin", 229 | "unicode": "e714", 230 | "unicode_decimal": 59156 231 | }, 232 | { 233 | "icon_id": "3217800", 234 | "name": "加载中", 235 | "font_class": "refresh", 236 | "unicode": "e602", 237 | "unicode_decimal": 58882 238 | }, 239 | { 240 | "icon_id": "3217801", 241 | "name": "退出", 242 | "font_class": "close", 243 | "unicode": "e603", 244 | "unicode_decimal": 58883 245 | }, 246 | { 247 | "icon_id": "3217811", 248 | "name": "设置", 249 | "font_class": "setting", 250 | "unicode": "e604", 251 | "unicode_decimal": 58884 252 | }, 253 | { 254 | "icon_id": "3217829", 255 | "name": "放大镜", 256 | "font_class": "search", 257 | "unicode": "e60c", 258 | "unicode_decimal": 58892 259 | }, 260 | { 261 | "icon_id": "3217833", 262 | "name": "使用说明", 263 | "font_class": "warning", 264 | "unicode": "e60e", 265 | "unicode_decimal": 58894 266 | }, 267 | { 268 | "icon_id": "3217835", 269 | "name": "信封", 270 | "font_class": "mail", 271 | "unicode": "e60f", 272 | "unicode_decimal": 58895 273 | }, 274 | { 275 | "icon_id": "3217841", 276 | "name": "图片", 277 | "font_class": "picture", 278 | "unicode": "e612", 279 | "unicode_decimal": 58898 280 | }, 281 | { 282 | "icon_id": "3217895", 283 | "name": "更多", 284 | "font_class": "more", 285 | "unicode": "e62a", 286 | "unicode_decimal": 58922 287 | }, 288 | { 289 | "icon_id": "3217896", 290 | "name": "添加", 291 | "font_class": "add", 292 | "unicode": "e62b", 293 | "unicode_decimal": 58923 294 | } 295 | ] 296 | } 297 | -------------------------------------------------------------------------------- /src/views/editor/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 315 | 316 | 359 | -------------------------------------------------------------------------------- /src/assets/empty-content.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/editor/components/IEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 269 | 270 | 330 | -------------------------------------------------------------------------------- /public/font/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 heiyehk. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/views/index/components/List.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 261 | 262 | 449 | --------------------------------------------------------------------------------