diff --git a/package.json b/package.json index e38c806..68b1853 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aix-win", - "version": "1.1.1", + "version": "1.1.6", "description": "An Electron application with Vue", "main": "./out/main/index.js", "author": "example.com", diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000..45f6e65 Binary files /dev/null and b/resources/logo.png differ diff --git a/src/main/index.js b/src/main/index.js index 6381e86..30ef34e 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -3,15 +3,17 @@ import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import File from './file' -import chat from './chat' // chat封装 -import Store from './store' // Store封装 +import Logger from './logger' // 日志封装 +import chat from './chat' // chat封装 +import Store from './store' // Store封装 import updateInit from './update' - // 代理 electron/remote // 第一步:引入remote import remote from '@electron/remote/main' // 第二步: 初始化remote remote.initialize() +// 日志配置-初始化(日志直接绑定到console上) +if(!is.dev) Logger.initialize() // 持久化数据-初始化 Store.initialize() @@ -153,6 +155,7 @@ async function createLinkWin(data) { // 初始化完成 app.on('ready', () => { + appWatchError() // 监听app错误 process.env.LANG = 'en_US.UTF-8' // 设置应用程序用户模型标识符 electronApp.setAppUserModelId('com.electron') @@ -259,3 +262,34 @@ function handleAll() { win.webContents.send('pinia-state-set', storeName, jsonStr) }) } + +// app 崩溃监听器 +function appWatchError() { + // 渲染进程崩溃 + app.on('renderer-process-crashed', (event, webContents, killed) => { + console.error( + `APP-ERROR:renderer-process-crashed; event: ${JSON.stringify(event)}; webContents:${JSON.stringify( + webContents + )}; killed:${JSON.stringify(killed)}` + ) + }) + + // GPU进程崩溃 + app.on('gpu-process-crashed', (event, killed) => { + console.error(`APP-ERROR:gpu-process-crashed; event: ${JSON.stringify(event)}; killed: ${JSON.stringify(killed)}`) + }) + + // 渲染进程结束 + app.on('render-process-gone', async (event, webContents, details) => { + console.error( + `APP-ERROR:render-process-gone; event: ${JSON.stringify(event)}; webContents:${JSON.stringify( + webContents + )}; details:${JSON.stringify(details)}` + ) + }) + + // 子进程结束 + app.on('child-process-gone', async (event, details) => { + console.error(`APP-ERROR:child-process-gone; event: ${JSON.stringify(event)}; details:${JSON.stringify(details)}`) + }) +} \ No newline at end of file diff --git a/src/main/logger.js b/src/main/logger.js new file mode 100644 index 0000000..03b6db3 --- /dev/null +++ b/src/main/logger.js @@ -0,0 +1,52 @@ +/** + * @description 日志配置 + * @author zdg + * @date 2021-07-05 14:07:01 + */ +// import log from 'electron-log' +import log from 'electron-log/main' +import { app } from 'electron' +import path from 'path' + +// 关闭控制台打印 +// 日志控制台等级,默认值:false +log.transports.console.level = false +// log.transports.console.level = 'info' +// 日志文件等级,默认值:false +log.transports.file.level = 'info' +// 日志文件名,默认:main.log +// log.transports.file.fileName = 'main.log'; +// 日志大小,默认:1048576(1M),达到最大上限后,备份文件并重命名为:main.old.log,有且仅有一个备份文件 +log.transports.file.maxSize = 10 * 1024 * 1024; // 文件最大不超过 10M +// 自定义日志文件滚动策略 +log.transports.file.rollSize = 10 * 1024 * 1024; // 10MB +// 日志格式,默认:[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text} +log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}' +let date = new Date() +let dateStr = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() +// 文件位置及命名方式 +// 默认位置为:C:\Users\[user]\AppData\Roaming\[appname]\electron_log\ +// 文件名为:年-月-日.log +// 自定义文件保存位置为安装目录下 \log\年-月-日.log +// log.transports.file.resolvePathFn = () => 'logs\\' + dateStr+ '.log'; +log.transports.file.resolvePathFn = () => path.join(app.getPath('userData'), `logs/${dateStr}.log`) + +// 有六个日志级别error, warn, info, verbose, debug, silly。默认是silly +export const logger = { + error: (...args) => log.error(...args), + warn: (...args) => log.warn(...args), + info: (...args) => log.info(...args), + verbose: (...args) => log.verbose(...args), + debug: (...args) => log.debug(...args), + silly: (...args) => log.silly(...args) +} +export function initialize(bool = true, type = 'all') { + log.initialize() // 为渲染器进行初始化 + if (bool) { // 是否替换默认的console + if (type == 'all') Object.assign(console, log.functions) + else { // 替换指定类型 + console[type] = log[type] + } + } +} +export default { initialize } \ No newline at end of file diff --git a/src/main/store.js b/src/main/store.js index 9bb6fcf..c37f8f5 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -8,25 +8,54 @@ Store.initRenderer() // 默认共享数据 const defaultData = { - model: 'select', // 悬浮球-当前模式 - showBoardAll: false, // 全屏画板-是否显示 - isPdfWin: false, // pdf窗口是否打开 - isToolWin: false, // 工具窗口是否打开 - curSubjectNode: { - data: {}, // 当前教材节点 (包含当前教材 单元) - querySearch: {} // 查询资源所需参数 - } + session: { // 缓存(临时sessionStorage) + model: 'select', // 悬浮球-当前模式 + showBoardAll: false, // 全屏画板-是否显示 + isPdfWin: false, // pdf窗口是否打开 + isToolWin: false, // 工具窗口是否打开 + curSubjectNode: { + data: {}, // 当前教材节点 (包含当前教材 单元) + querySearch: {} // 查询资源所需参数 + } + }, + local: { // 本地(永久localStorage) + }, } // 初始化 export function initialize(){ - const store = new Store({ - name: 'cache-store', // 存储文件名 + // 缓存数据-sessionStore + const sessionStore = new Store({ + name: 'session-store', // 存储文件名 fileExtension: 'ini', // 文件后缀名 - encryptionKey: 'Eihrjwi7h104h2Kub423' // 数据加密-防止用户直接改配置 + encryptionKey: 'BvPLmgCC4DSIG0KkTec5', // 数据加密-防止用户直接改配置 + beforeEachMigration: (store, context) => { // 版本迁移回调 + console.log(`[session-store] 迁移从 ${context.fromVersion} → ${context.toVersion}`); + }, + migrations: { // 版本变化 + '0.0.0': store => { + // store.set('debugPhase', true); + } + } }) - store.clear() // 先清除-所有缓存数据 - store.set(defaultData) // 初始化-默认数据 - return store + sessionStore.clear() // 先清除-所有缓存数据 + sessionStore.set(defaultData.session) // 初始化-默认数据 + + // 缓存数据-localStore + const localStore = new Store({ + name: 'local-store', // 存储文件名 + fileExtension: 'ini', // 文件后缀名 + encryptionKey: '6CyoHQmUaPmLzvVsh', // 数据加密-防止用户直接改配置 + beforeEachMigration: (store, context) => { // 版本迁移回调 + console.log(`[local-store] 迁移从 ${context.fromVersion} → ${context.toVersion}`); + }, + migrations: { // 版本变化 + '0.0.0': store => { + // store.set('debugPhase', true); + } + } + }) + localStore.set(defaultData.local) // 初始化-默认数据 + return {sessionStore, localStore} } export default { initialize } \ No newline at end of file diff --git a/src/renderer/src/components/pdf/index.vue b/src/renderer/src/components/pdf/index.vue index ae43c6e..64a8671 100644 --- a/src/renderer/src/components/pdf/index.vue +++ b/src/renderer/src/components/pdf/index.vue @@ -383,16 +383,7 @@ defineExpose({ savaDataStore }) watchEffect(() => { - setTimeout(() => { - console.log(toolState,'监听') - - }, 300) if(toolState.isPdfWin){ - // if(toolState.isToolWin){ - // ispointer.value=false - // }else{ - // ispointer.value=true - // } watchToolState() //监听工具栏 } }) diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js index f3a9a67..da34555 100644 --- a/src/renderer/src/main.js +++ b/src/renderer/src/main.js @@ -12,10 +12,14 @@ import 'virtual:windi.css' import { store } from '@/store' import App from './App.vue' import router from './router' +import log from 'electron-log/renderer' // 渲染进程日志-文件记录 + +if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件 + Object.assign(console, log.functions) // 渲染进程日志-控制台替换 +} const app = createApp(App) - app.use(router) .use(store) .use(ElementPlus, { locale: zhLocale }).mount('#app') \ No newline at end of file diff --git a/src/renderer/src/plugins/shareStore.js b/src/renderer/src/plugins/shareStore.js index fee2151..7fc1392 100644 --- a/src/renderer/src/plugins/shareStore.js +++ b/src/renderer/src/plugins/shareStore.js @@ -3,13 +3,14 @@ */ const isNode = typeof require !== 'undefined' // 是否支持node函数 const { ipcRenderer } = isNode?require('electron'):{} // app使用 +import { sessionStore } from '@/utils/tool' // const Remote = isNode?require('@electron/remote'):{} // 远程模块 export function shareStorePlugin({store}) { store.$subscribe((mutation, state) => { // 自动同步 // mutation 变量包含了变化前后的状态 // mutation.events: key newValue target oldValue oldTarget // state 是变化后的状态 - // console.log('store.$subscribe', mutation) + // console.log('store.$subscribe', mutation, state, store) // 在存储变化的时候执行 // const storeName = store.$id const storeName = mutation.storeId @@ -19,13 +20,15 @@ export function shareStorePlugin({store}) { const { storeId: storeName, payload, events, type } = mutation // direct // if (!Object.keys(payload).length) return if (type != 'direct' || !events || Array.isArray(events) || !events.key) return - stateSync(storeName, events.key, events.newValue) // 需要同步 + stateSync(storeName, events.key, events.newValue, state) // 需要同步 } }) + // 暴露方法-手动同步 store.stateSync = (storeName, key, value) => { - if (!storeName && !!key && !!value) stateSync(storeName, key, value) - else stateSyncAll(store) + const state = store.$state + if (!storeName && !!key && !!value) stateSync(storeName, key, value, state) + else stateSyncAll(store, state) } // 暴露方法-发送当前状态-新窗口 store.stateSyncInit = wid => stateSyncInit(wid, store) @@ -34,14 +37,16 @@ export function shareStorePlugin({store}) { } // 同步数据-发送给主线程-单独 -function stateSync(storeName, key, value) { +function stateSync(storeName, key, value, state) { // console.log('state-change', storeName, key, value) try { - let jsonStr = '' - if (typeof key === 'string') jsonStr = JSON.stringify({[key]:value}) - else if (typeof value === 'object') jsonStr = JSON.stringify(key) + const { data, keystr } = filterByKey(state, key, value) + const jsonStr = JSON.stringify(data) // 从新组装-json数据 + // 更新本地数据-session + sessionStore.set(keystr, value) // 通知主线程更新 ipcRenderer?.invoke('pinia-state-change', storeName, jsonStr) + // console.log('======',keystr, data ) } catch (error) { console.log('state-change-error', error) } @@ -95,3 +100,27 @@ const circularSafeStringify = (obj) => { }); } +// 过滤对象 +const filterByKey = (obj, key, value) => { + let res = { data:{}, keystr:'' } + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + const isEqual = JSON.stringify(obj[k]) === JSON.stringify(value) // 值是否相同 + if (k === key && isEqual) { + // 如果匹配,则添加到新对象中 + res.data[k] = obj[k]; + res.keystr = k; + } else { + if (obj[k] !== null && typeof obj[k] === 'object') { + // 如果是对象,则递归处理 + const {data, keystr} = filterByKey(obj[k], key, value) + res.data[k] = data + res.keystr = keystr ? `${k}.${keystr}`: key + } + } + } + } + return res; +} +// 对象克隆 +const objClone = (obj) => JSON.parse(JSON.stringify(obj)) diff --git a/src/renderer/src/store/modules/tool.js b/src/renderer/src/store/modules/tool.js index ac4e440..e2af372 100644 --- a/src/renderer/src/store/modules/tool.js +++ b/src/renderer/src/store/modules/tool.js @@ -2,6 +2,10 @@ * 工具类-窗口-状态管理 */ import { defineStore } from 'pinia' +import { sessionStore } from '@/utils/tool' + +// 默认数据 +const defData = sessionStore.store || {} export const useToolState = defineStore('tool', { state: () => ({ @@ -10,9 +14,10 @@ export const useToolState = defineStore('tool', { isPdfWin: false, // pdf窗口是否打开 isToolWin: false, // 工具窗口是否打开 curSubjectNode: { - data: {}, // 当前教材节点 (包含当前教材 单元) + data: {}, // 当前教材节点 (包含当前教材 单元) querySearch: {} // 查询资源所需参数 - } + }, + ...defData // 默认数据-覆盖上面的配置(不要删除, 会导致新窗口-获取状态失败) }), actions: { } diff --git a/src/renderer/src/utils/tool.js b/src/renderer/src/utils/tool.js index 4476094..8520394 100644 --- a/src/renderer/src/utils/tool.js +++ b/src/renderer/src/utils/tool.js @@ -11,22 +11,31 @@ const path = isNode?require('path'):{} const Remote = isNode?require('@electron/remote'):{} const { ipcRenderer } = isNode?require('electron'):window.electron || {} const API = isNode?window.api:{} // preload-api -import { useToolState } from '@/store/modules/tool' // 获取store状态 +// import { useToolState } from '@/store/modules/tool' // 获取store状态 const Store = isNode?require('electron-store'):null // 持久化存储 // 常用变量 const BaseUrl = isNode?process.env['ELECTRON_RENDERER_URL']+'/#':'' const isDev = isNode?process.env.NODE_ENV !== 'production':'' -const toolState = useToolState() // 获取store状态 +// const toolState = useToolState() // 获取store状态 // 暴露Remote中的属性 export const ipcMain = Remote?.ipcMain || {} -// 暴露Store存储对象 -export const store = Store ? new Store({ - name: 'cache-store', // 存储文件名 - fileExtension: 'ini', // 文件后缀名 - encryptionKey: 'Eihrjwi7h104h2Kub423' // 数据加密-防止用户直接改配置 + +// 暴露sessionStore存储对象 +export const sessionStore = Store ? new Store({ + name: 'session-store', // 存储文件名 + fileExtension: 'ini', // 文件后缀名 + encryptionKey: 'BvPLmgCC4DSIG0KkTec5' // 数据加密-防止用户直接改配置 }) : {} + +// 暴露localStore存储对象 +export const localStore = Store ? new Store({ + name: 'local-store', // 存储文件名 + fileExtension: 'ini', // 文件后缀名 + encryptionKey: '6CyoHQmUaPmLzvVsh' // 数据加密-防止用户直接改配置 +}) : {} + /** * 获取静态资源,开发和生产环境 * @param {*} url @@ -128,7 +137,7 @@ export const createWindow = async (type, data) => { winPdf.restore(); } else{ winPdf.focus(); - toolState.isPdfWin=true + // toolState.isPdfWin=true } return @@ -195,9 +204,9 @@ export function toolWindow({url, isConsole, isWeb=true, option={}}) { mainWin.once('closed', () => { win.destroy()}) // 内部监听器 win.webContents.on('did-finish-load', () => { - setTimeout(() => { - toolState.stateSyncInit(win.id) // 同步状态 - }, 200); + // setTimeout(() => { + // toolState.stateSyncInit(win.id) // 同步状态 + // }, 200); }) // 内部监听器-是否打印 if (!!isConsole) { diff --git a/src/renderer/src/views/classBegins/index.vue b/src/renderer/src/views/classBegins/index.vue index a1b4ec3..afd9920 100644 --- a/src/renderer/src/views/classBegins/index.vue +++ b/src/renderer/src/views/classBegins/index.vue @@ -104,9 +104,12 @@ const switchPageMode = () => { } } onMounted(async () => { + const isDev = process.env.NODE_ENV == 'development' toolState.isPdfWin=true //设置打开pdf窗口 - pdfObj.pdfUrl = getStaticUrl(route.query.path, 'user', 'selfFile', true) //线上 - // pdfObj.pdfUrl = getStaticUrl('aaa.pdf', 'user', 'selfFile', true) //本地 + if (isDev) + pdfObj.pdfUrl = getStaticUrl('aaa.pdf', 'user', 'selfFile', true) //本地 + else + pdfObj.pdfUrl = getStaticUrl(route.query.path, 'user', 'selfFile', true) //线上 textbookId.value = route.query.textbookId pdfObj.bookId=textbookId.value //初始化获取接口数据 diff --git a/src/renderer/src/views/classManage/basicGroup.vue b/src/renderer/src/views/classManage/basicGroup.vue index 9fd48be..9e59f66 100644 --- a/src/renderer/src/views/classManage/basicGroup.vue +++ b/src/renderer/src/views/classManage/basicGroup.vue @@ -1,10 +1,10 @@