前端实战:electron+vue3+ts开发桌面端便签应用
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端实战:electron+vue3+ts开发桌面端便签应用相关的知识,希望对你有一定的参考价值。
前端时间我的一个朋友为了快速熟悉 Vue3 开发, 特意使用 electron+vue3+ts 开发了一个桌面端应用, 并在
github
上开源了, 接下来我就带大家一起了解一下这个项目, 在文章末尾我会放 github
的地址, 大家如果想学习vue3 + ts + electron 开发, 可以本地 clone
学习参考一下.
image.png
技术栈
以上是我们看到的便签软件使用界面, 整体技术选型如下:
脚手架 vue-cli
前端框架和语言规范 vue + typescript
桌面端开发框架 electron
electron支持插件 vue-cli-plugin-electron-builder
数据库 NeDB | 一款NoSQL嵌入式数据库
代码格式规范 eslint
接下来我们来看看具体的演示效果:
具体实现过程, 内容很长, 建议先点赞收藏, 再一步步学习, 接下来会就该项目的每一个重点细节做详细的分析.
开发思路
页面:
列表页index.vue
头部、搜索、内容部分,只能有一个列表页存在
设置页setting.vue
设置内容和软件信息,和列表页一样只能有一个存在
编辑页 editor.vue
icons功能和背景颜色功能,可以多个编辑页同时存在
动效:
打开动效,有一个放大、透明度的过渡,放不了动图这里暂时不演示了。
标题过渡效果
切换index
和setting
时头部不变,内容过渡
数据储存:数据的创建和更新都在编辑页editor.vue
进行,这个过程中在储存进nedb
之后才通信列表页index.vue
更新内容,考虑到性能问题,这里使用了防抖
防止连续性的更新而导致卡顿(不过貌似没有这个必要。。也算是一个小功能吧,然后可以设置这个更新速度)
错误采集:采集在使用中的错误并弹窗提示
编辑显示:document
暴露 execCommand
方法,该方法允许运行命令来操纵可编辑内容区域的元素。
在开发的时候还遇到过好多坑,这些都是在electron
环境中才有,比如
@input
触发2次,加上v-model
触发3次。包括创建一个新的electron框架也是这样,别人电脑上不会出现这个问题,猜测是electron缓存
问题
vue3碰到空属性
报错时无限报错,在普通浏览器(edge和chrome)是正常一次
组件无法正常渲染不报错,只在控制台报异常
打包后由于electron
的缓存导致打开需要10秒左右,清除c盘软件缓存后正常
其他的不记得了。。
这里暂时不提供vue3和electron介绍,有需要的可以先看看社区其他的有关文章或者后期再详细专门提供。软件命名为i-notes
。
vue3中文教程 vue3js.cn/docs/zh/gui…[1] electron教程 www.electronjs.org/[2]
typescript教程 www.typescriptlang.org/[3]
electron-vue
里面的包环境太低了,所以是手动配置electron+vue3(虽然说是手动。。其实就两个步骤)
目录结构
electron-vue-notes
├── public
│ ├── css
│ ├── font
│ └── index.html
├── src
│ ├── assets
│ │ └── empty-content.svg
│ ├── components
│ │ ├── message
│ │ ├── rightClick
│ │ ├── editor.vue
│ │ ├── header.vue
│ │ ├── input.vue
│ │ ├── messageBox.vue
│ │ ├── switch.vue
│ │ └── tick.vue
│ ├── config
│ │ ├── browser.options.ts
│ │ ├── classNames.options.ts
│ │ ├── editorIcons.options.ts
│ │ ├── index.ts
│ │ └── shortcuts.keys.ts
│ ├── inotedb
│ │ └── index.ts
│ ├── less
│ │ └── index.less
│ ├── router
│ │ └── index.ts
│ ├── script
│ │ └── deleteBuild.js
│ ├── store
│ │ ├── exeConfig.state.ts
│ │ └── index.ts
│ ├── utils
│ │ ├── errorLog.ts
│ │ └── index.ts
│ ├── views
│ │ ├── editor.vue
│ │ ├── index.vue
│ │ ├── main.vue
│ │ └── setting.vue
│ ├── App.vue
│ ├── background.ts
│ ├── main.ts
│ └── shims-vue.d.ts
├── .browserslistrc
├── .eslintrc.js
├── .prettierrc.js
├── babel.config.js
├── inoteError.log
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.json
├── vue.config.js
└── yarn.lock
使用脚手架搭建vue3环境
没有脚手架的可以先安装脚手架
npm install -g @vue/cli
创建vue3项目
vue create electron-vue-notes
# 后续
? Please pick a preset: (Use arrow keys)
2] babel, eslint)
3 Preview) ([Vue 3] babel, eslint)
> Manually select features
# 手动选择配置
# 后续所有配置
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, CSS Pre-processors, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) n
创建完之后的目录是这样的
electron-vue-notes
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ ├── router
│ │ └── index.ts
│ ├── views
│ │ ├── About.vue
│ │ └── Home.vue
│ ├── App.vue
│ ├── main.ts
│ └── shims-vue.d.ts
├── .browserslistrc
├── .eslintrc.js
├── babel.config.js
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock
安装electron的依赖
# yarn
yarn add vue-cli-plugin-electron-builder electron
# npm 或 cnpm
npm i vue-cli-plugin-electron-builder electron
安装完之后完善一些配置,比如别名
、eslint
、prettier
等等基础配置,还有一些颜色
、icons
等等具体可以看下面
项目的一些基础配置
eslint
使用eslint主要是规范代码风格,不推荐tslint是因为tslint已经不更新了,tslint也推荐使用eslint 安装eslint
npm i eslint -g
进入项目之后初始化eslint
eslint --init
# 后续配置
? How would you like to use ESLint? To check syntax and find problems
? What type of modules does your project use? javascript modules (import/export)
? Which framework does your project use? Vue.js
? Does your project use TypeScript? Yes
? Where does your code run? Browser, Node
? What format do you want your config file to be in? JavaScript
The config that youve selected requires the following dependencies:
eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
? Would you like to install them now with npm? (Y/n) y
修改eslint配置,·.eslintrc.js
,规则rules
可以根据自己的喜欢配置 eslint.org/docs/user-g…[4]
module.exports =
true,
env:
true
,
extends: [
plugin:vue/vue3-essential,
eslint:recommended,
plugin:prettier/recommended,
plugin:@typescript-eslint/eslint-recommended,
@vue/typescript/recommended,
@vue/prettier,
@vue/prettier/@typescript-eslint
],
parserOptions:
2020
,
rules:
1, single],
1,
@typescript-eslint/camelcase: 0,
@typescript-eslint/no-explicit-any: 0,
no-irregular-whitespace: 2,
no-case-declarations: 0,
no-undef: 0,
eol-last: 1,
block-scoped-var: 2,
comma-dangle: [2, never],
no-dupe-keys: 2,
no-empty: 1,
no-extra-semi: 2,
no-multiple-empty-lines: [1, max: 1, maxEOF: 1 ],
no-trailing-spaces: 1,
semi-spacing: [2, before: false, after: true ],
no-unreachable: 1,
space-infix-ops: 1,
spaced-comment: 1,
no-var: 2,
no-multi-spaces: 2,
comma-spacing: 1
;
prettier
在根目录增加.prettierrc.js
配置,根据自己的喜好进行配置,单行多少个字符、单引号、分号、逗号结尾等等
module.exports =
120,
true,
true,
none
;
tsconfig.json
如果这里没有配置识别@/
路径的话,在项目中使用会报错
"paths":
"@/*": [
]
package.json
"author": "heiyehk",
"description": "I便笺个人开发者heiyehk独立开发,在Windows中更方便的记录文字。",
"main": "background.js",
"scripts":
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve"
配置入口文件background.ts
因为需要做一些打开和关闭的动效,因此我们需要配置electron
为frame无边框
和透明transparent
的属性
/* eslint-disable @typescript-eslint/no-empty-function */
use strict;
import app, protocol, BrowserWindow, globalShortcut from electron;
import
createProtocol
// installVueDevtools
from vue-cli-plugin-electron-builder/lib;
const isDevelopment = process.env.NODE_ENV !== production;
let win: BrowserWindow | null;
protocol.registerSchemesAsPrivileged([
app,
privileges:
true,
true
]);
function createWindow()
new BrowserWindow(
false, // 无边框
false,
true, // 透明
950,
600,
webPreferences:
true,
true
);
if (process.env.WEBPACK_DEV_SERVER_URL)
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) win.webContents.openDevTools();
else
app);
http://localhost:8080);
closed, () =>
win = null;
);
app.on(window-all-closed, () =>
if (process.platform !== darwin)
app.quit();
);
app.on(activate, () =>
if (win === null)
createWindow();
);
app.on(ready, async () =>
// 这里注释掉是因为会安装tools插件,需要屏蔽掉,有能力的话可以打开注释
// if (isDevelopment && !process.env.IS_TEST)
// try
// await installVueDevtools();
// catch (e)
// console.error(Vue Devtools failed to install:, e.toString());
//
//
createWindow();
);
if (isDevelopment)
if (process.platform === win32)
message, data =>
if (data === graceful-exit)
app.quit();
);
else
SIGTERM, () =>
app.quit();
);
启动
yarn electron:serve
到这里配置就算是成功搭建好这个窗口了,但是还有一些其他细节需要进行配置,比如electron打包
配置,模块化
的配置等等
常规配置
这里配置一些常用的开发内容和一些轮子代码, 大家可以参考 reset.csss
和 common.css
这两个文件.
config
这个对应项目中的config文件夹
config
├── browser.options.ts # 窗口的配置
├── classNames.options.ts # 样式名的配置,背景样式都通过这个文件渲染
├── editorIcons.options.ts # 编辑页面的一些editor图标
├── index.ts # 导出
└── shortcuts.keys.ts # 禁用的一些快捷键,electron是基于chromium浏览器,所以也存在一些浏览器快捷键比如F5
browser.options
这个文件的主要作用就是配置主窗口和编辑窗口区分开发正式的配置,宽高等等,以及要显示的主页面
/**
* 软件数据和配置
* C:\\Users\\用户名\\AppData\\Roaming
* 共享
* C:\\ProgramData\\Intel\\ShaderCache\\i-notesxx
* 快捷方式
* C:\\Users\\用户名\\AppData\\Roaming\\Microsoft\\Windows\\Recent
* 电脑自动创建缓存
* C:\\Windows\\Prefetch\\I-NOTES.EXExx
*/
/** */
const globalEnv = process.env.NODE_ENV;
const devWid = globalEnv === development ? 950 : 0;
const devHei = globalEnv === development ? 600 : 0;
// 底部icon: 40*40
const editorWindowOptions =
290,
350,
250
;
/**
* BrowserWindow的配置项
* @param type 单独给编辑窗口的配置
*/
const browserWindowOption = (type?: editor): Electron.BrowserWindowConstructorOptions =>
const commonOptions =
48,
false,
true,
true,
webPreferences:
true,
true
;
if (!type)
return
350,
600,
320,
...commonOptions
;
return
...editorWindowOptions,
...commonOptions
;
;
/**
* 开发环境: http://localhost:8080
* 正式环境: file://$__dirname/index.html
*/
const winURL = globalEnv === development ? http://localhost:8080 : `file://$__dirname/index.html`;
export browserWindowOption, winURL ;
router
增加meta中的title属性,显示在软件上方头部
import createRouter, createWebHashHistory from vue-router;
import RouteRecordRaw from vue-router;
import main from ../views/main.vue;
const routes: Array<RouteRecordRaw> = [
/,
main,
component: main,
children: [
/,
index,
import(../views/index.vue),
meta:
I便笺
,
/editor,
editor,
import(../views/editor.vue),
meta:
,
/setting,
setting,
import(../views/setting.vue),
meta:
设置
]
];
const router = createRouter(
history: createWebHashHistory(process.env.BASE_URL),
routes
);
export default router;
main.vue
main.vue
文件主要是作为一个整体框架,考虑到页面切换时候的动效,分为头部和主体部分,头部作为一个单独的组件处理,内容区域使用router-view
渲染。html部分,这里和vue2.x有点区别的是,在vue2.x中可以直接
// bad
<transition name="fade">
<keep-alive>
<router-view />
</keep-alive>
</transition>
上面的这种写法在vue3中会在控制台报异常,记不住写法的可以看看控制台????????
<router-view v-slot=" Component ">
"main-fade">
"transition" :key="routeName">
<keep-alive>
"Component" />
</keep-alive>
</div>
</transition>
</router-view>
然后就是ts部分了,使用vue3的写法去写,script
标签注意需要写上lang="ts"
代表是ts语法。router
的写法也不一样,虽然在vue3中还能写vue2的格式,但是不推荐使用。这里是获取route
的name
属性,来进行一个页面过渡的效果。
<script lang="ts">
import defineComponent, ref, onBeforeUpdate from vue;
import useRoute from vue-router;
import Header from @/components/header.vue;
export default defineComponent(
components:
Header
,
setup()
const routeName = ref(useRoute().name);
onBeforeUpdate(() =>
routeName.value = useRoute().name;
);
return
routeName
;
);
</script>
less部分
<style lang="less" scoped>
.main-fade-enter,
.main-fade-leave-to
display: none;
opacity: 0;
animation: main-fade 0.4s reverse;
.main-fade-enter-active,
.main-fade-leave-active
opacity: 0;
animation: main-fade 0.4s;
@keyframes main-fade
from
opacity: 0;
transform: scale(0.96);
to
opacity: 1;
transform: scale(1);
</style>
以上就是main.vue
的内容,在页面刷新或者进入的时候根据useRouter().name
的切换进行放大的过渡效果
,后面的内容会更简洁一点。
header.vue
onBeforeRouteUpdate
头部组件还有一个标题过渡的效果,根据路由导航获取当前路由的mate.title
变化进行过渡效果。vue3中路由守卫需要从vue-route
导入使用。
import onBeforeRouteUpdate, useRoute from vue-router;
...
onBeforeRouteUpdate((to, from, next) =>
title.value = to.meta.title;
currentRouteName.value = to.name;
next();
);
computed
这里是计算不同的路由下标题内边距的不同,首页是有个设置入口的按钮,而设置页面是只有两个按钮,computed
会返回一个你需要的新的值
// 获取首页的内边距
const computedPaddingLeft = computed(() =>
return currentRouteName.value === index ? padding-left: 40px; : ;
);
emit子传父和props父传子
vue3没有了this
,那么要使用emit
怎么办呢?在入口setup
中有2个参数
setup(props, content)
props
是父组件传给子组件的内容,props
常用的emit
和props
都在content
中。
????这里需要注意的是,使用
props
和emit
需要先定义,才能去使用,并且会在vscode
中直接调用时辅助弹窗显示
props示例
emit示例
export default defineComponent(
props:
test: String
,
option-click, on-close],
// 如果只用emit的话可以使用es6解构
// 如:setup(props, emit )
setup(props, content)
option-click));
)
electron打开窗口
import browserWindowOption from @/config;
import createBrowserWindow, transitCloseWindow from @/utils;
...
const editorWinOptions = browserWindowOption(editor);
// 打开新窗口
const openNewWindow = () =>
/editor);
;
electron图钉固定屏幕前面
先获取当前屏幕实例
????这里需要注意的是,需要从
remote
获取当前窗口信息
判断当前窗口是否在最前面isAlwaysOnTop()
,然后通过setAlwaysOnTop()
属性设置当前窗口最前面。
import remote from electron;
...
// 获取窗口固定状态
let isAlwaysOnTop = ref(false);
const currentWindow = remote.getCurrentWindow();
isAlwaysOnTop.value = currentWindow.isAlwaysOnTop();
// 固定前面
const drawingPin = () =>
if (isAlwaysOnTop.value)
false);
false;
else
true);
true;
;
electron关闭窗口
这里是在utils封装了通过对dom的样式名操作,达到一个退出的过渡效果,然后再关闭。
// 过渡关闭窗口
export const transitCloseWindow = (): void =>
#app)?.classList.remove(app-show);
#app)?.classList.add(app-hide);
close();
;
noteDb数据库
安装nedb数据库,文档: www.w3cschool.cn/nedbintro/n…[5]
yarn add nedb @types/nedb
数据储存在nedb
中,定义字段,并在根目录的shims-vue.d.ts
加入类型
/**
* 储存数据库的
*/
interface DBNotes
string; // 样式名
string; // 内容
// 创建时间,这个时间是nedb自动生成的
string; // uid,utils中的方法生成
// update,自动创建的
string; // 自动创建的
对nedb的封装
自我感觉这里写的有点烂。。。勿喷,持续学习中
这里的QueryDB
是shims-vue.d.ts
定义好的类型
这里的意思是QueryDB<T>
是一个对象,然后这个对象传入一个泛型T
,这里keyof T
获取这个对象的key
(属性)值,?:
代表这个key
可以是undefined
,表示可以不存在。T[K]
表示从这个对象中获取这个K
的值。
type QueryDB<T> =
[K in keyof T]?: T[K];
;
import Datastore from nedb;
import path from path;
import remote from electron;
/**
* @see https://www.npmjs.com/package/nedb
*/
class INoteDB<G = any>
/**
* 默认储存位置
* C:\\Users\\Windows User Name\\AppData\\Roaming\\i-notes
*/
// dbPath = path.join(remote.app.getPath(userData), db/inote.db);
// dbPath = ./db/inote.db;
dbPath = this.path;
_db: Datastore<Datastore.DataStoreOptions> = this.backDatastore;
get path()
if (process.env.NODE_ENV === development)
return path.join(__dirname, db/inote.db);
return path.join(remote.app.getPath(userData), db/inote.db);
get backDatastore()
return new Datastore(
/**
* autoload
* default: false
* 当数据存储被创建时,数据将自动从文件中加载到内存,不必去调用loadDatabase
* 注意所有命令操作只有在数据加载完成后才会被执行
*/
true,
filename: this.dbPath,
true
);
refreshDB()
this._db = this.backDatastore;
insert<T extends G>(doc: T)
return new Promise((resolve: (value: T) => void) =>
this._db.insert(doc, (error: Error | null, document: T) =>
if (!error) resolve(document);
);
);
/**
* db.find(query)
* @param Query<T> query:object类型,查询条件,可以使用空对象。
* 支持使用比较运算符($lt, $lte, $gt, $gte, $in, $nin, $ne)
* 逻辑运算符($or, $and, $not, $where)
* 正则表达式进行查询。
*/
find(query: QueryDB<DBNotes>)
return new Promise((resolve: (value: DBNotes[]) => void) =>
this._db.find(query, (error: Error | null, document: DBNotes[]) =>
if (!error) resolve(document as DBNotes[]);
);
);
/**
* db.findOne(query)
* @param query
*/
findOne(query: QueryDB<DBNotes>)
return new Promise((resolve: (value: DBNotes) => void) =>
this._db.findOne(query, (error: Error | null, document) =>
if (!error) resolve(document as DBNotes);
);
);
/**
* db.remove(query, options)
* @param Record<keyof DBNotes, any> query
* @param Nedb.RemoveOptions options
* @return BackPromise<number>
*/
remove(query: QueryDB<DBNotes>, options?: Nedb.RemoveOptions)
return new Promise((resolve: (value: number) => void) =>
if (options)
this._db.remove(query, options, (error: Error | null, n: number) =>
if (!error) resolve(n);
);
else
this._db.remove(query, (error: Error | null, n: number) =>
if (!error) resolve(n);
);
);
update<T extends G>(query: T, updateQuery: T, options: Nedb.UpdateOptions = )
return new Promise((resolve: (value: T) => void) =>
this._db.update(
query,
updateQuery,
options,
(error: Error | null, numberOfUpdated: number, affectedDocuments: T) =>
if (!error) resolve(affectedDocuments);
);
);
export default new INoteDB();
使用ref
和reactive
代替vuex,并用watch
监听
创建exeConfig.state.ts
用ref
和reactive
引入的方式就可以达到vuex
的state
效果,这样就可以完全舍弃掉vuex
。比如软件配置,创建exeConfig.state.ts
在store
中,这样在外部.vue
文件中进行更改也能去更新视图。
import reactive, watch from vue;
const exeConfigLocal = localStorage.getItem(exeConfig);
export let exeConfig = reactive(
1000,
...
switchStatus:
/**
* 开启提示
*/
true
);
if (exeConfigLocal)
exeConfig = reactive(JSON.parse(exeConfigLocal));
else
exeConfig, JSON.stringify(exeConfig));
watch(exeConfig, e =>
exeConfig, JSON.stringify(e));
);
vuex番外
vuex的使用是直接在项目中引入useStore
,但是是没有state
类型提示的,所以需要手动去推导state
的内容。这里的S
代表state
的类型,然后传入vuex
中export declare class Store<S> readonly state: S;
想要查看某个值的类型的时候在vscode中
ctrl+鼠标左键
点进去就能看到,或者鼠标悬浮该值
declare module vuex
type StoreStateType = typeof store.state;
export function useStore<S = StoreStateType>(): Store<S>;
index.vue
这里在防止没有数据的时候页面空白闪烁,使用一个图片和列表区域去控制显示,拿到数据之后就显示列表,否则就只显示图片。
在这个页面对editor.vue
进行了createNewNote
创建便笺笔记、updateNoteItem_className
更新类型更改颜色、updateNoteItem_content
更新内容、removeEmptyNoteItem
删除、whetherToOpen
是否打开(在editor中需要打开列表的操作)通信操作
以及对软件失去焦点进行监听getCurrentWindow().on(blur)
,如果失去焦点,那么在右键弹窗打开的情况下进行去除。
deleteActiveItem_uid
删除便笺笔记内容,这里在component
封装了一个弹窗组件messageBox
,然后在弹窗的时候提示是否删除
和不在询问
的功能操作。
????如果勾选不在询问
,那么在store=>exeConfig.state
中做相应的更改
这里在设置中会进行详细的介绍
开发一个vue3右键弹窗插件
vue3也发布了有段时间了,虽然还没有完全稳定,但后面的时间出现的插件开发方式说不定也会多起来。插件开发思路
定义好插件类型,比如需要哪些属性MenuOptions
判断是否需要在触发之后立即关闭还是继续显示
在插入body
时判断是否存在,否则就删除重新显示
import createApp, h, App, VNode, RendererElement, RendererNode from vue;
import ./index.css;
type ClassName = string | string[];
interface MenuOptions
/**
* 文本
*/
string;
/**
* 是否在使用后就关闭
*/
once?: boolean;
/**
* 单独的样式名
*/
className?: ClassName;
/**
* 图标样式名
*/
iconName?: ClassName;
/**
* 函数
*/
handler(): void;
type RenderVNode = VNode<
RendererNode,
RendererElement,
string]: any;
>;
class CreateRightClick
rightClickEl?: App<Element>;
rightClickElBox?: HTMLDivElement | null;
constructor()
this.removeRightClickHandler();
/**
* 渲染dom
* @param menu
*/
render(menu: MenuOptions[]): RenderVNode
return h(
ul,
right-click-menu-list]
,
[
map(item =>
return h(
li,
class: item.className,
// vue3.x中简化了render,直接onclick即可,onClick也可以
onclick: () =>
// 如果只是一次,那么点击之后直接关闭
if (item.once) this.remove();
return item.handler();
,
[
// icon
i,
class: item.iconName
),
// text
h(
span,
right-click-menu-text
,
item.text
)
]
);
)
]
);
/**
* 给右键的样式
* @param event 鼠标事件
*/
len: number): void
if (!this.rightClickElBox) return;
`$len * 36px`;
const clientX, clientY = event;
const innerWidth, innerHeight = window;
const clientWidth, clientHeight = this.rightClickElBox;
`height: $len * 36px;opacity: 1;transition: all 0.2s;`;
if (clientX + clientWidth < innerWidth)
`left: $clientX + 2px;`;
else
`left: $clientX - clientWidthpx;`;
if (clientY + clientHeight < innerHeight)
`top: $clientY + 2px;`;
<以上是关于前端实战:electron+vue3+ts开发桌面端便签应用的主要内容,如果未能解决你的问题,请参考以下文章
Electron-Vite2-MacUI桌面管理框架|electron13+vue3.x仿mac桌面UI
vite+vue3+ts实战项目,教你实现一个网页版的typora!(前端篇)