diff --git a/app/general/ipcEvent.js b/app/general/ipcEvent.js deleted file mode 100644 index a2a88e27a84870e02def634987e5b62e8c71afad..0000000000000000000000000000000000000000 --- a/app/general/ipcEvent.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * @Author: wuruipeng - * @Date: 2024-04-08 09:57:33 - * @LastEditors: wuruipeng - * @LastEditTime: 2024-04-08 10:02:41 - * @description: 监听渲染进程交互主进程方法 - */ - -const { app, ipcMain } = require('electron') - -// 关闭应用方法 -ipcMain.on('close-app', () => { - // 调用 app.quit() 方法来关闭应用程序 - app.quit() -}) diff --git a/app/main.js b/app/main.js index 6abb4b69033f417294fe5a84b27aa1d10b529269..7f4ca0087532e4b95f94a853feb7445060d8d0e1 100644 --- a/app/main.js +++ b/app/main.js @@ -2,110 +2,45 @@ * @Author: wuruipeng * @Date: 2024-03-06 17:39:06 * @LastEditors: wuruipeng - * @LastEditTime: 2024-04-11 14:47:50 + * @LastEditTime: 2024-04-26 14:37:53 * @description: 主进程配置 */ -const { app, screen, BrowserWindow, Menu, dialog } = require('electron') +global.config = require('./utils/config.js') +const { app, dialog, BrowserWindow } = require('electron') const path = require('path') -const { confirmExit } = require('./general/common.js') -require('./general/ipcEvent.js') // 配置监听ipc信息事件 +const { confirmExit } = require('./utils/common.js') +require('./render/mainMenu.js') // 菜单配置 // 初始化主进程 function initApp(rootPath) { - // 菜单模板 - const template = [ - { - label: 'Setting', - submenu: [ - { label: 'Change Serve/Local', accelerator: 'CmdOrCtrl+S' }, - { label: 'Toggle DevTools', role: 'toggleDevTools' }, - { type: 'separator' }, - { label: 'Quit', role: 'quit' } - ] - }, - { - label: 'Window', - submenu: [ - { label: 'Reload', role: 'reload' }, - { label: 'Zoom In', role: 'zoomIn' }, - { label: 'Zoom Out', role: 'zoomOut' }, - { label: 'Toggle Fullscreen', role: 'togglefullscreen' } - ] - }, - { - label: 'Help', - submenu: [{ label: `About ${app.name}`, role: 'about' }] - } - ] // 基础窗口信息配置 const createWindow = function () { - const { width, height } = screen.getPrimaryDisplay().workAreaSize // 自适应默认尺寸 - const mainWindow = new BrowserWindow({ - width, - height, - x: 0, - y: 0, - webPreferences: { - devTools: true, // 打开开发者工具 - nodeIntegration: true, // 使用nodejs整合模块 - contextIsolation: true, // 上下文隔离 - preload: path.join(__dirname, 'preload.js') // 设定html内预加载script文件 - } - }) - // 切换应用功能方法 - const changePage = (type = null) => { - switch (type) { - case 'web': - mainWindow.loadURL('http://localhost:3000') // 在线功能加载 React 服务的 URL - // 需要启动服务的项目,同时监听加载失败事件 - mainWindow.webContents.on( - 'did-fail-load', - (event, errorCode, errorDescription) => { - // 如果errorCode为-105(net::ERR_NAME_NOT_RESOLVED),则表示服务不可用 - if (errorCode === -105) { - dialog.showErrorBox('Can not use this server') - } else { - dialog.showErrorBox(`Load Failed:${errorDescription}`) - } - } - ) - break - default: - mainWindow.loadFile( - path.join(__dirname, '..', rootPath, 'index.html') - ) // 使用相对路径加载 HTML 文件 - break - } - } - // 自定义应用切换事件 - template[0].submenu[0].submenu = [ - { - label: 'Online Web', - click: () => { - changePage('web') - } - }, - { - label: 'Local Page', - click: () => { - changePage() + const { mainWindow } = require('./render/mainWindow.js') + mainWindow.loadURL('http://localhost:3000') // 在线功能加载 React 服务的 URL + // 需要启动服务的项目,同时监听加载失败事件 + mainWindow.webContents.on( + 'did-fail-load', + (event, errorCode, errorDescription) => { + // 如果errorCode为-105(net::ERR_NAME_NOT_RESOLVED),则表示服务不可用 + if (errorCode === -105) { + dialog.showErrorBox('Can not use this server') + } else { + dialog.showErrorBox(`Load Failed:${errorDescription}`) } + mainWindow.loadFile(path.join(__dirname, '..', rootPath, 'index.html')) // 使用相对路径加载 HTML 文件 } - ] - // 定义并执行菜单配置 - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) - changePage('web') + ) // 窗口关闭前进行确认验证 mainWindow.on('close', event => { confirmExit(event) }) } // 应用加载完成配置 - app.on('ready', createWindow) - app.on('activate', () => { + app.whenReady().then(() => { createWindow() + if (!BrowserWindow.getAllWindows().length) createWindow() + require('./utils/ipcEvent.js') // 配置监听ipc信息事件 }) // 退出程序提示 app.on('before-quit', event => { diff --git a/app/render/mainMenu.js b/app/render/mainMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..33269f2c6e44c5fc8236c23bbd21d095f13454f6 --- /dev/null +++ b/app/render/mainMenu.js @@ -0,0 +1,68 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-25 09:55:08 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-05-06 10:36:52 + * @description: 菜单配置 + */ + +const { app, Menu } = require('electron') +const isMac = process.platform === 'darwin' + +// 菜单模板 +const template = [ + // 操作配置 + { + label: 'Setting', + submenu: [ + { + label: 'File', + accelerator: 'CmdOrCtrl+S', + submenu: [ + { + label: 'Import File' + }, + { + label: 'Export File' + } + ] + }, + { + label: 'Toggle DevTools', + role: 'toggleDevTools', + accelerator: 'F12' + }, + { type: 'separator' }, + { + label: 'Quit', + role: isMac ? 'close' : 'quit', + accelerator: 'Ctrl+Shift+A' + } + ] + }, + // 窗口配置 + { + label: 'Window', + submenu: [ + { label: 'Reload', role: 'reload', accelerator: 'Ctrl+R' }, + // { label: 'Zoom In', role: 'zoomIn', accelerator: 'Ctrl+=' }, + // { label: 'Zoom Out', role: 'zoomOut', accelerator: 'Ctrl+-' }, + { + label: 'Toggle Fullscreen', + role: 'togglefullscreen', + accelerator: 'F11' + } + ] + }, + // 帮助配置 + { + label: 'Help', + submenu: [{ label: `About ${app.name}`, role: 'about' }] + } +] + +global.config.appMenus = template + +// 定义并执行菜单配置 +const menu = Menu.buildFromTemplate(template) +Menu.setApplicationMenu(menu) diff --git a/app/render/mainWindow.js b/app/render/mainWindow.js new file mode 100644 index 0000000000000000000000000000000000000000..6caa04ed4c3b5f499a513858d6c74a1c15a67183 --- /dev/null +++ b/app/render/mainWindow.js @@ -0,0 +1,29 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-25 09:52:38 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-05-06 09:31:01 + * @description: 主应用窗口配置 + */ + +const { screen, BrowserWindow } = require('electron') +const path = require('path') + +const { width, height } = screen.getPrimaryDisplay().workAreaSize // 自适应默认尺寸 +const mainWindow = new BrowserWindow({ + width, + height, + minWidth: 800, + minHeight: 600, + frame: false, + x: 0, + y: 0, + webPreferences: { + devTools: true, // 打开开发者工具 + nodeIntegration: true, // 使用nodejs整合模块 + contextIsolation: true, // 上下文隔离 + preload: path.join(__dirname, 'preload.js') // 设定html内预加载script文件 + } +}) + +module.exports = { mainWindow } diff --git a/app/preload.js b/app/render/preload.js similarity index 86% rename from app/preload.js rename to app/render/preload.js index 8da3fdf96543808745e697251b98ab37e8ae067d..84bffeb729380c41edad0e0005201fc01f9ab1ca 100644 --- a/app/preload.js +++ b/app/render/preload.js @@ -2,12 +2,12 @@ * @Author: wuruipeng * @Date: 2024-04-08 09:55:51 * @LastEditors: wuruipeng - * @LastEditTime: 2024-04-08 16:06:34 + * @LastEditTime: 2024-04-28 10:06:26 * @description: 渲染进程预加载脚本(用于搭建传递主进程与渲染进程的属性使用和方法) */ -const { contextBridge, ipcRenderer } = require('electron') -const common = require('./general/common.js') +const { app, contextBridge, ipcRenderer } = require('electron') +const common = require('../utils/common.js') // 可发送通信消息至主进程控制调用 contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) diff --git a/app/general/common.js b/app/utils/common.js similarity index 100% rename from app/general/common.js rename to app/utils/common.js diff --git a/app/utils/config.js b/app/utils/config.js new file mode 100644 index 0000000000000000000000000000000000000000..60bb5d33057fd8ee1e992553e4e7937d89824248 --- /dev/null +++ b/app/utils/config.js @@ -0,0 +1,17 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-25 10:52:47 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-04-28 11:07:19 + * @description:全局应用配置属性 + */ + +const { app } = require('electron') + +module.exports = { + appName: app.name, // 应用名称 + appMenus: [], //应用操作菜单 + windowBorder: true, // 是否展示窗口边框 + fullScreen: false, // 是否全屏 + maximize: false // 是否窗口最大化 +} diff --git a/app/utils/ipcEvent.js b/app/utils/ipcEvent.js new file mode 100644 index 0000000000000000000000000000000000000000..a168e8e4af2889b5c80cd415998ae3cfa5e406c4 --- /dev/null +++ b/app/utils/ipcEvent.js @@ -0,0 +1,102 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-08 09:57:33 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-05-06 10:27:21 + * @description: 监听渲染进程交互主进程方法 + */ + +const { app, ipcMain, Menu } = require('electron') +const { mainWindow } = require('../render/mainWindow') + +// 关闭应用方法 +ipcMain.on('close-app', () => { + app.quit() +}) +// 最小化应用方法 +ipcMain.on('minimize-app', () => { + mainWindow.minimize() +}) +// 切换展示应用窗口方法 +ipcMain.on('maximize-app', event => { + // 检查窗口的最大化状态 + const isMaximized = mainWindow.isMaximized() + if (isMaximized) { + mainWindow.restore() + } else { + mainWindow.maximize() + } + // 将窗口状态发送给渲染进程改变前端展示图标 + event.sender.send('isMaximized', mainWindow.isMaximized()) +}) +// 切换全屏展示方法 +ipcMain.handle('full-screen-app', () => { + // 将全屏状态发送给渲染进程改变页面组件展示 + return { status: mainWindow.isFullScreen() } +}) +// 获取应用相关信息(名称,菜单,窗口状态) +ipcMain.handle('get-app-info', event => { + const info = { + name: app.name, + menus: global.config.appMenus, + restored: mainWindow.isMaximized(), + fullScreen: mainWindow.isFullScreen() + } + event.sender.send('set-app-info', info) +}) +// 触发应用菜单系统权限菜单项操作 +ipcMain.on('choice-role-menu', (event, role) => { + if (!role) return + // 不同类型事件触发方式 + const types = [ + 'toggleDevTools', + 'reload', + 'zoomIn', + 'zoomOut', + 'togglefullscreen' + ] + if (types.includes(role)) { + switch (role) { + case 'toggleDevTools': + mainWindow.webContents.toggleDevTools() + break + case 'reload': + mainWindow.reload() + break + case 'togglefullscreen': + mainWindow.setFullScreen(!mainWindow.isFullScreen()) + break + case 'zoomIn': + mainWindow.webContents.on('zoom-changed', () => { + mainWindow.webContents.setZoomFactor(+0.1) + }) + break + case 'zoomOut': + mainWindow.webContents.on('zoom-changed', () => { + mainWindow.webContents.setZoomFactor(-0.1) + }) + break + default: + break + } + return + } + // 获取菜单项列表 + const { items } = Menu.getApplicationMenu() + // 触发click事件 + const findRole = menus => { + menus.forEach(item => { + if (item.role) { + if (item.role === role) { + item.click() + return + } + } + if (!item.role && item.submenu) { + findRole(item.submenu.items) + return + } + }) + } + findRole(items) +}) diff --git a/package.json b/package.json index e85a921dd1b7415b13dac5e678c41a42316f2eb2..806d29b0853afeaa404289f1dd43796a41e537b4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "WuRuipeng", "license": "ISC", - "name": "mtlh-application", + "name": "MTLH", "version": "1.0.0", "description": "electron-project", "main": "app/main.js", diff --git a/public/index.html b/public/index.html index cb1889536387249582de68204d830237100b4674..a35db9c7214db797db0919421abe116d9c203829 100644 --- a/public/index.html +++ b/public/index.html @@ -2,16 +2,16 @@ - + - - - Application + + + MTLH diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..faa67488e7a8f555c0740027eceeafc01d28a37d --- /dev/null +++ b/src/components/TitleBar.tsx @@ -0,0 +1,209 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-25 14:07:08 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-04-30 17:21:14 + * @description:自定义应用操作栏组件 + */ + +import React, { ReactElement } from 'react' +import { useEffect, useState } from 'react' +import '@/styles/components/TitleBar.less' +import { keyboardKey } from '@testing-library/user-event' + +interface ControlsObject { + context: string + role: string +} +interface MenusObject { + type?: string + label?: string + accelerator?: string + role?: string + submenu?: any[] +} +type TitleBarType = HTMLElement | null +type ControlsType = Array +type MenusType = Array | [] +type MenuElementType = React.JSX.Element[] | ReactElement | null + +const TitleBar: React.FC = () => { + const [appName, setAppName] = useState('') + const [menus, setMenus] = useState([]) + const [menuIndex, setMenuIndex] = useState(-1) + const [subMenuElements, setSubMenuElements] = useState() + const [isMax, setMax] = useState('') + const [controls] = useState([ + { context: '-', role: 'minimize' }, + { context: '◻', role: 'maximize' }, + { context: '×', role: 'close' } + ]) + // 更新应用当前信息和状态 + const updateAppInfo = (): void => { + ;(window as any).resultAPI.invoke('get-app-info') + ;(window as any).resultAPI.on('set-app-info', (_: any, info: any) => { + const { name, menus, restored, fullScreen } = info + setAppName(name) + setMenus(menus) + setMax(String(restored)) + changeFullScreen(fullScreen) + }) + } + // 菜单操作 + const menuEvent = (info: MenusObject, index: number): void => { + setTimeout(() => { + if (menuIndex !== index) { + setMenuIndex(index) + } else { + setMenuIndex(-1) + return + } + setSubMenuElements(subMenuRender(info)) + }, 130) + } + // 创建子菜单结构模板 + const subTemplate = (submenus: MenusType): MenuElementType => { + return submenus.map((sub: MenusObject, index: number) => { + return ( +
{ + e.stopPropagation() + subMenuEvent(sub) + }} + key={index} + > +
{sub.label ?? ''}
+
+ {sub.accelerator ?? ''} + +
+ {sub.submenu ? ( +
{subTemplate(sub.submenu)}
+ ) : ( + '' + )} +
+ ) + }) + } + // 子菜单渲染 + const subMenuRender = (info: MenusObject): MenuElementType => { + if (!info.submenu) return null + const subElement = subTemplate(info.submenu) + return
{subElement}
+ } + // 子菜单操作事件 + const subMenuEvent = (sub: MenusObject): void => { + if (!sub.role) { + return + } + // 对拥有系统权限的菜单项进行事件绑定 + ;(window as any).ipcRenderer.send('choice-role-menu', sub.role) + } + // 窗口操作 + const controlEvent = (role: string): void => { + ;(window as any).ipcRenderer.send(`${role}-app`) + if (role === 'maximize') { + // 更新应用窗口化状态 + ;(window as any).resultAPI.on( + 'isMaximized', + (_: any, status: boolean) => { + setMax(String(status)) + } + ) + } + } + // 更改全屏状态操作 + const changeFullScreen = (status: boolean): void => { + // 如果为全屏状态,应隐藏边框和操作栏 + const titleBar: TitleBarType = document.getElementById('title-bar') + const app: TitleBarType = document.getElementById('app') + if (!titleBar || !app) return + // 获取根元素(:root)并修改CSS变量 + const _root = document.documentElement + if (status) { + _root.style.setProperty('--title-bar-display', 'none') + _root.style.setProperty('--title-bar-height', '0px') + } else { + _root.style.setProperty('--title-bar-display', 'grid') + _root.style.setProperty('--title-bar-height', '40px') + } + } + useEffect(() => { + // 当快捷键全屏时 + const onKeyDown = (event: keyboardKey): void => { + if (event.key === 'F11') { + ;(window as any).resultAPI + .invoke('full-screen-app') + .then((res: any) => { + changeFullScreen(res.status) + }) + } + } + // 当窗口大小变化时 + const onSizeChange = (): void => { + updateAppInfo() + } + updateAppInfo() + document.addEventListener('keyup', onKeyDown) + window.addEventListener('resize', onSizeChange) + return () => { + document.removeEventListener('keyup', onKeyDown) + window.removeEventListener('resize', onSizeChange) + } + }, []) + return ( +
+
+
+
+
+
+
    + {controls.map((item, index) => ( +
  • { + controlEvent(item.role) + }} + > + + {item.context} + +
  • + ))} +
+
+
+
    + {menus?.map((item, index) => ( +
  • + + {menuIndex === index ? subMenuElements : ''} +
  • + ))} +
+
+
+ ) +} + +export default TitleBar diff --git a/src/index.tsx b/src/index.tsx index 21d01243aea03386d5da65c8cf4d14a3444fdc7e..a6c5f9d76e0e0b9b6798b3c85ccfd3496ad2d202 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,24 @@ +/* + * @Author: wuruipeng + * @Date: 2024-04-19 16:29:24 + * @LastEditors: wuruipeng + * @LastEditTime: 2024-04-28 17:21:58 + * @description:渲染进程入口组件 + */ + import React from 'react' -import '@/styles/index.less' import { Root, createRoot } from 'react-dom/client' +import TitleBar from 'components/TitleBar' +import '@/styles/index.less' const rootElement = document.getElementById('root') if (rootElement) { const root: Root = createRoot(rootElement) root.render( - Main Theme +
+ +
) } else { diff --git a/src/styles/components/TitleBar.less b/src/styles/components/TitleBar.less new file mode 100644 index 0000000000000000000000000000000000000000..2cf3a60ffa7d30c5ce06785226abd61ecae1d454 --- /dev/null +++ b/src/styles/components/TitleBar.less @@ -0,0 +1,182 @@ +@import '../variables.less'; + +@transprop: transition; +@transvalue:all 0.1s; + +#title-bar { + display: @title-bar-display; + grid-template-columns: 108px calc(100% - 108px - 160px) 160px; + justify-content: space-between; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: @title-bar-height; + padding: 8px; + background: @title-bar-bg; + z-index: 999; + li { + text-align: center; + cursor: context-menu; + user-select: none; + } + .title-bar-left, + .title-bar-right, + .title-bar-center { + background-color: transparent; + } + .title-bar-bottom { + position: relative; + top: 5px; + left: -@theme-border-width; + width: calc(100% + (8 * 2px) - @theme-border-width * 2); + height: calc(@title-bar-height / 2); + grid-area: 2/4/2/1; + background: #ddd; + .application-menus { + display: grid; + grid-template-columns: repeat(3, 10rch); + font-size: 12px; + li { + button { + width: 100%; + height: calc(@title-bar-height / 2); + border: none; + &:hover { + background-color: #ccc; + @{transprop}: @transvalue; + } + &:active { + transform: scale(0.95); + @{transprop}: @transvalue; + } + &:not(:hover), + &:not(:active) { + @{transprop}: @transvalue; + } + } + } + &-submenu, + .item-submenu { + position: absolute; + width: auto; + min-width: 18rch; + padding: 6px 0; + background-color: #fff; + text-align: left; + box-shadow: 1px 1px 4px 2px @shadow-color; + border-radius: @theme-border-width; + .item-submenu-item { + height: calc(@title-bar-height / 2); + line-height: calc(@title-bar-height / 4); + display: grid; + grid-template-columns: minmax(4rlh, auto) auto; + gap: 10px; + padding: 6px 12px; + cursor: pointer; + [class~='item-submenu'] { + display: none; + left: 95%; + } + &:hover { + background-color: #ddd; + @{transprop}: @transvalue; + > [class~='item-submenu'] { + display: inline-block; + } + } + &:not(:hover) { + @{transprop}: @transvalue; + } + .item-name { + width: auto; + white-space: nowrap; + font-weight: 500; + } + .item-hot-key { + text-align: right; + span { + color: #777474; + &:first-child { + margin-right: 6px; + } + } + } + } + } + } + } + .window-icon { + position: relative; + width: calc(@title-bar-height - 8px); + height: calc(@title-bar-height - (8 * 1.7px)); + margin-right: 8px; + background: radial-gradient(red, orange, yellow, green, skyblue, blue); + &::before { + content: attr(app-name); + position: absolute; + left: 40px; + top: -2px; + font: 24px blod; + color: @title-bar-color; + } + } + .window-controls { + display: grid; + grid-template-columns: repeat(3, 36px); + gap: 20px; + padding-right: 8px; + li { + height: calc(@title-bar-height / 1.6); + line-height: calc(@title-bar-height / 1.7); + background: @title-bar-item-bg; + box-shadow: 1px 0px 6px 0px @shadow-color; + border: 1px solid @title-bar-color; + border-radius: @theme-border-width; + &:hover { + box-shadow: 1px 1px 10px 1px @shadow-color; + @{transprop}: @transvalue; + } + &:active { + transform: scale(0.95); + @{transprop}: @transvalue; + } + &:not(:hover), + &:not(:active) { + @{transprop}: @transvalue; + } + span { + display: inline-block; + color: @title-bar-color; + } + } + .window-minimize { + span { + transform: scaleX(2.5) translateY(-1px); + } + } + .window-maximize { + span { + transform: scale(1.3); + } + } + .window-close { + background: @title-bar-close-bg; + span { + transform: scale(1.5); + } + &:hover { + @{transprop}: @transvalue; + } + &:not(:hover) { + @{transprop}: @transvalue; + } + } + } + .restore-square::before { + content: '◻'; + position: absolute; + top: -2px; + right: -2px; + } +} diff --git a/src/styles/index.less b/src/styles/index.less index bf6d0e8ef8ea1fb0f46fdd5f6bc0aa52aae159bb..b1336ab35ff27104c95b349d91e02e9edb9d545d 100644 --- a/src/styles/index.less +++ b/src/styles/index.less @@ -1,3 +1,5 @@ +@import './variables.less'; + /* Typography */ html, body, @@ -8,6 +10,7 @@ body, margin: 0; font: inherit; font-family: Arial, sans-serif; + overflow: hidden; } /* Box sizing */ @@ -33,6 +36,7 @@ ul, ol { margin: 0; padding: 0; + list-style: none; } /* Form elements */ @@ -49,3 +53,12 @@ img { max-width: 100%; height: auto; } + +#app { + position: relative; + top: @title-bar-height; + height: calc(100% - @title-bar-height); + padding-top: calc(@title-bar-height / 2); + overflow: hidden auto; + border: @theme-border-width solid @theme-color; +} diff --git a/src/styles/variables.less b/src/styles/variables.less new file mode 100644 index 0000000000000000000000000000000000000000..cda01bbd661b67dc42e949287f7a265b5dafede9 --- /dev/null +++ b/src/styles/variables.less @@ -0,0 +1,25 @@ +:root { + --title-bar-display: grid; + --title-bar-height: 40px; +} + +// 主题色变量 +@theme-color: #6262e8; +@theme-border-width: 4px; + +// 窗口操作栏边框 +@title-bar-display: var(--title-bar-display); +@title-bar-height: var(--title-bar-height); +@title-bar-color: #fff; +@title-bar-bg: linear-gradient(to top, #1e5be9, #4374e6, #fff); +@title-bar-item-bg: linear-gradient(to top, #77b7ed, #1e5be9, #4374e6, #fff); +@title-bar-close-bg: linear-gradient(to top, #ecbfbf, #ff0000, #f06e6e, #fff); +@shadow-color: #0000006d; + +// 全局类变量 +.drag-window { + -webkit-app-region: drag; +} +.no-drag-window { + -webkit-app-region: no-drag; +} diff --git a/tsconfig.json b/tsconfig.json index f0ccea136ca281099843bd4be24e1ebd8a2d855b..894d3b5566a5c6e7ab6ab16c0d9217f343319113 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,9 +28,10 @@ "isolatedModules": true, "jsx": "react" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/components/TitleBar.jsx"], "exclude": ["app", "node_modules", "build", "dist", "public", "server"], "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "components*": ["src/components*"] } } diff --git a/webpack.config.js b/webpack.config.js index fadbdb625bc34eaaeef5403113ba8ef573b1a209..3b0ceed95010fbabf4104489d7f91851a034ad70 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,10 +1,12 @@ const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') +const { DefinePlugin } = require('webpack') module.exports = { - mode: 'development', - entry: './src/index.tsx', + mode: 'development', // 开发模式 + entry: './src/index.tsx', // 入口文件 + // 开发配置 devServer: { static: { directory: path.join(__dirname, 'public') @@ -18,6 +20,7 @@ module.exports = { overlay: true, progress: true }, + // 请求代理配置 proxy: [ { context: ['/api'], @@ -29,11 +32,21 @@ module.exports = { ] }, resolve: { + // 路径别名配置 alias: { - '@': path.resolve(__dirname, 'src/') + '@': path.resolve(__dirname, 'src/'), + components: path.resolve(__dirname, 'src/components/'), + app: path.resolve(__dirname, 'app/'), + server: path.resolve(__dirname, 'server/') }, - extensions: ['.tsx', '.ts', '.jsx', '.js'] + // 支持的扩展类型文件 + extensions: ['.tsx', '.ts', '.jsx', '.js'], + // 其他解析配置... + fallback: { + fs: false // 禁用 fs 模块 + } }, + // loader规则配置 module: { rules: [ { @@ -64,9 +77,11 @@ module.exports = { ] }, plugins: [ + // 配置模板静态html文件 new HtmlWebpackPlugin({ template: './public/index.html' }), + // 配置public目录下其他静态资源的打包构建选项 new CopyWebpackPlugin({ patterns: [ { @@ -77,8 +92,12 @@ module.exports = { } } ] + }), + new DefinePlugin({ + BASE_URL: JSON.stringify('/') // 设置BASE_URL为根路径 }) ], + // 输出配置 output: { path: path.resolve(__dirname, 'build'), filename: 'bundle.js'