桌面倒计时

基于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渲染进程” 进行状态实时传递。不知道有没有更好的办法,还需在研究研究。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值