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'