基于electron + vue + pinia 数据持久化本地缓存做了一个桌面倒计时的demo,随手记录一下,哪里不对,希望可以帮忙指出来。
主要就两个页面,一个为编辑时间列表窗口,一个为无边框展示窗口。
主进程窗口创建:
main/index.ts
function createWindow(data, option): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
...option,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
enableRemoteModule: true, // 允许渲染进程使用remote模块
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
// 官网似乎说是默认false,但是这里必须设置contextIsolation
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '/#/' + (data.path ? data.path : 'main')
)
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html/'), {
hash: data.path ? data.path : 'main'
})
}
mainWindow.setIgnoreMouseEvents(false) // 设置窗口不接收鼠标事件
require('@electron/remote/main').enable(mainWindow.webContents)
// 主进程 渲染进程 通信
ipcMain.on('store-send', (_, message) => {
mainWindow.webContents.send('store-to', message)
})
}
// 初步准备页面
app.whenReady().then(() => {
...
const option = {
width: 500,
height: 500
}
createWindow({ path: '' }, option)
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow({ path: '' }, option)
})
})
// 创建新窗口
ipcMain.on('createOtherWindow', (_, data) => {
const option = {
width: 300,
minHeight: 200,
show: false,
autoHideMenuBar: true,
frame: false, // 是否显示边缘框
fullscreen: false, // 是否全屏显示
transparent: true, // 是否透明
resizable: true // 是否可以改变窗口大小
// type: 'toolbar' // 设置窗口类型为工具窗口,则不会在任务栏出现缩略图
}
createWindow(data, option)
})
// 本地存储 electron-store
ipcMain.on('electron-store-get', async (event, val) => {
event.returnValue = store.get(val)
})
ipcMain.on('electron-store-set', async (_event, key, val) => {
store.set(key, val)
})
ipcMain.on('electron-store-clear', () => {
store.clear()
})
preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
const api = {}
const store = {
get(val) {
return ipcRenderer.sendSync('electron-store-get', val)
},
set(property, val) {
return ipcRenderer.send('electron-store-set', property, val)
},
clear() {
return ipcRenderer.send('electron-store-clear')
}
}
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', { ...electronAPI, store })
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
window.electron = { ...electronAPI, store }
window.api = api
}
pinia 状态管理
main.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const app = createApp(App)
const store = createPinia()
store.use(piniaPluginPersist).use(Router).mount('#app')
store/index.ts
import { defineStore } from 'pinia'
const electronStorage: Storage = {
setItem(key, state): any {
return window.electron.store.set(key, state)
},
getItem(key): string {
return window.electron.store.get(key)
},
clear() {
return window.electron.store.clear()
},
removeItem(key) {
return key
},
key(key): any {
return key
},
length
}
export const useGlobalStore = defineStore({
id: 'global',
// 数据
state: () => ({
timeList: [] as object[]
//
}),
// 计算
getters: {
updateTime: (state) => {
return state.timeList
}
},
// 方法
actions: {
add_time_item(item) {
this.timeList.push(item)
},
update_time_item(item) {
const index = this.timeList.findIndex((i: { id?: string }) => i.id === item.id)
if (index >= 0) {
this.timeList[index] = item
}
},
delete_time_item(item) {
const index = this.timeList.findIndex((i: { id?: string }) => i.id === item.id)
if (index >= 0) {
this.timeList.splice(index, 1)
}
},
clear_time_item() {
this.timeList = []
}
},
// 默认存储到 sessionStorage ,key 为 store 的 id
persist: {
enabled: true,
strategies: [
{
key: 'user',
storage: electronStorage
}
]
}
})
主窗口
small-time.vue
<template>
<div class="layout">
<header class="layout-header flex-row-center">
<a-button class="_add-btn" @click="handleAddTime"> 新增日历 </a-button>
<a-button class="_add-btn" @click="renderWindow"> 生成桌面组件 </a-button>
</header>
<main class="layout-main">
<div class="l-list">
<div
v-for="(item, index) in globalStore.timeList"
:key="index"
class="list-item flex-row-center"
>
<div class="list-item-name">{{ item.label }}</div>
<div class="list-item-time">{{ dayjs(item.time).format(dateFormat) }}</div>
<div class="list-item-action flex-row-center">
<p class="_edit-btn" @click="() => handleEditFrom(item)">编辑</p>
<p class="_del-btn" @click="() => handleDelItem(item)">删除</p>
</div>
</div>
</div>
</main>
<a-modal class="modal-form" v-model:visible="visible" title="日历" :footer="false">
<div class="form-action">
<a-form
ref="formRef"
layout="horizontal"
:model="formState"
autocomplete="off"
@finish="handleFormFinish"
>
<a-form-item label="标题" name="label">
<a-input v-model:value="formState.label" />
</a-form-item>
<a-form-item label="日期" name="time">
<a-date-picker v-model:value="formState.time" :format="dateFormat" />
</a-form-item>
<div class="flex-row-center modal-form-footer-btn">
<a-button @click="handleClearFrom">取消</a-button>
<a-button type="primary" html-type="submit">确定</a-button>
</div>
</a-form>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { useGlobalStore } from '@renderer/store'
import { ref, watch } from 'vue'
import dayjs from 'dayjs'
import { uuid } from '@renderer/until/until'
const globalStore: any = useGlobalStore()
const dateFormat = 'YYYY-MM-DD'
const visible = ref(false)
const modalType = ref({ type: 'add', id: '' })
const formRef = ref()
const formState = ref<any>({
label: '',
time: ''
})
// 列表发生变化,通知给主进程再由主进程发给无边框窗口
watch(globalStore.timeList, (n) => {
// 发送给主进程
window.electron.ipcRenderer.send('store-send', {
label: 'timeList',
data: JSON.stringify(n)
})
})
const renderWindow = () => {
window.electron.ipcRenderer.send('createOtherWindow', {
path: 'countdown',
windowId: 2
})
}
const handleAddTime = () => {
visible.value = true
modalType.value = { type: 'add', id: '' }
formState.value = {
label: '',
time: ''
}
}
const handleEditFrom = (item) => {
modalType.value = { type: 'edit', id: item.id }
formState.value = {
label: item.label,
time: dayjs(item.time, dateFormat)
}
visible.value = true
}
const handleClearFrom = () => {
formRef.value.resetFields()
visible.value = false
}
const handleDelItem = (item) => {
globalStore.delete_time_item(item)
}
const handleFormFinish = (values) => {
const time = dayjs(values.time, dateFormat)
if (modalType.value.type === 'edit') {
globalStore.update_time_item({ id: modalType.value.id, ...values, time })
} else if (modalType.value.type === 'add') {
globalStore.add_time_item({ id: uuid(), ...values, time })
}
formRef.value.resetFields()
visible.value = false
}
</script>
大概是这样展示的
无边框窗口
此窗口只负责展示,是可以拖动则需要给添加属性 -webkit-app-region: drag; 如果添加了按钮则需要对按钮属性添加-webkit-app-region: no-drag;
countdown.vue
<template>
<div class="countdown">
<a-card v-for="(ite, inx) in state.list" :key="inx" :title="`距离 ${ite.label} 还有 `">
<a-statistic-countdown :value="ite.time" format="D 天 H 时 m 分 s 秒" />
</a-card>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import { useGlobalStore } from '@renderer/store/index'
const globalStore: any = useGlobalStore()
const { ipcRenderer } = window.electron
const state = reactive({
list: globalStore.timeList as { label?: string; time?: string }[]
})
ipcRenderer.on('store-to', (_, msg) => {
if (msg.label === 'timeList') {
state.list = JSON.parse(msg.data)
}
})
</script>
<style lang="less">
.countdown {
width: calc(100% - 40px);
margin: auto;
-webkit-app-region: drag;
}
</style>
当store发生更改,无边框窗口如不刷新页面就监听不到改变后状态,所有才想着通过
“A渲染进程——主进程——B渲染进程” 进行状态实时传递。不知道有没有更好的办法,还需在研究研究。