feat: 新增窗口头部组件
This commit is contained in:
7
env.d.ts
vendored
7
env.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
declare module "@store/*";
|
||||
declare module "@modules/*";
|
||||
declare module "@utils/*";
|
||||
declare module "@assets/images/*";
|
||||
declare module "@constant/*";
|
||||
declare module "@remixicon/vue";
|
||||
declare module "vue-router";
|
||||
37
global.d.ts
vendored
37
global.d.ts
vendored
@@ -49,11 +49,12 @@ declare global {
|
||||
external: {
|
||||
open: (url: string) => void
|
||||
},
|
||||
window: {
|
||||
minimize: () => void,
|
||||
maximize: () => void,
|
||||
close: () => void
|
||||
},
|
||||
minimizeWindow: () => void,
|
||||
maximizeWindow: () => void,
|
||||
closeWindow: () => void,
|
||||
onWindowMaximized: (callback: (isMaximized: boolean) => void) => void,
|
||||
isWindowMaximized: () => Promise<boolean>,
|
||||
viewIsReady: () => void
|
||||
app: {
|
||||
setFrameless: (route?: string) => void
|
||||
},
|
||||
@@ -69,10 +70,34 @@ declare global {
|
||||
on: (event: 'tab-updated' | 'tab-created' | 'tab-closed' | 'tab-switched', handler: (payload: any) => void) => void
|
||||
},
|
||||
readFile: (filePath: string) => Promise<{success: boolean, data?: string, error?: string}>,
|
||||
logToMain: (logLevel: string, message: string) => void,
|
||||
logger: {
|
||||
debug: (message: string, ...meta?: any[]) => void;
|
||||
info: (message: string, ...meta?: any[]) => void;
|
||||
warn: (message: string, ...meta?: any[]) => void;
|
||||
error: (message: string, ...meta?: any[]) => void;
|
||||
},
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
api: WindowApi;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@store/*";
|
||||
declare module "@modules/*";
|
||||
declare module "@utils/*";
|
||||
declare module "@assets/images/*";
|
||||
declare module "@constant/*";
|
||||
declare module "@remixicon/vue";
|
||||
declare module "vue-router";
|
||||
declare module '@iconify/vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
export const Icon: DefineComponent<{
|
||||
icon: string
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
color?: string
|
||||
flip?: string
|
||||
rotate?: number
|
||||
}>
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141; connect-src 'self' http://8.138.234.141"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141; connect-src 'self' http://8.138.234.141 https://api.iconify.design"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -9,6 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@remixicon/vue": "^4.7.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
@@ -1533,6 +1535,36 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify-json/material-symbols": {
|
||||
"version": "1.2.50",
|
||||
"resolved": "https://registry.npmmirror.com/@iconify-json/material-symbols/-/material-symbols-1.2.50.tgz",
|
||||
"integrity": "sha512-71tjHR70h46LHtBFab3fAd2V/wPTO7JMV5lKnRn3IcF303LaFgAlO0BZeTJDcmCv9d0snRZmnoLZAJVD7/eisw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@iconify/types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz",
|
||||
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/vue": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@iconify/vue/-/vue-5.0.0.tgz",
|
||||
"integrity": "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iconify/types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyberalien"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/checkbox": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz",
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
"vite": "^7.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@remixicon/vue": "^4.7.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
|
||||
@@ -25,7 +25,14 @@ export enum IPC_EVENTS {
|
||||
FILE_WRITE = 'file:write',
|
||||
GET_WINDOW_ID='get-window-id',
|
||||
CUSTOM_EVENT ='custom:event',
|
||||
TIME_UPDATE = 'time:update'
|
||||
TIME_UPDATE = 'time:update',
|
||||
RENDERER_IS_READY = 'renderer-ready',
|
||||
|
||||
// 发送日志
|
||||
LOG_DEBUG = 'log-debug',
|
||||
LOG_INFO = 'log-info',
|
||||
LOG_WARN = 'log-warn',
|
||||
LOG_ERROR = 'log-error',
|
||||
}
|
||||
|
||||
export const MAIN_WIN_SIZE = {
|
||||
|
||||
@@ -30,6 +30,8 @@ class AppMain {
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
titleBarStyle: 'hidden',
|
||||
title: 'NIANXX',
|
||||
webPreferences: {
|
||||
devTools: this.isDev,
|
||||
nodeIntegration: false,
|
||||
|
||||
@@ -8,11 +8,12 @@ const api: WindowApi = {
|
||||
open: (url: string) => ipcRenderer.invoke('external-open', url)
|
||||
},
|
||||
|
||||
window: {
|
||||
minimize: () => ipcRenderer.send(IPC_EVENTS.WINDOW_MINIMIZE),
|
||||
maximize: () => ipcRenderer.send(IPC_EVENTS.WINDOW_MAXIMIZE),
|
||||
close: () => ipcRenderer.send(IPC_EVENTS.WINDOW_CLOSE)
|
||||
},
|
||||
closeWindow: () => ipcRenderer.send(IPC_EVENTS.WINDOW_CLOSE),
|
||||
minimizeWindow: () => ipcRenderer.send(IPC_EVENTS.WINDOW_MINIMIZE),
|
||||
maximizeWindow: () => ipcRenderer.send(IPC_EVENTS.WINDOW_MAXIMIZE),
|
||||
onWindowMaximized: (callback: (isMaximized: boolean) => void) => ipcRenderer.on(IPC_EVENTS.WINDOW_MAXIMIZE + 'back', (_, isMaximized) => callback(isMaximized)),
|
||||
isWindowMaximized: () => ipcRenderer.invoke(IPC_EVENTS.WINDOW_MAXIMIZE),
|
||||
viewIsReady: () => ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY),
|
||||
|
||||
app: {
|
||||
setFrameless: (route?: string) => ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route)
|
||||
@@ -63,7 +64,12 @@ const api: WindowApi = {
|
||||
getCurrentWindowId: () => ipcRenderer.sendSync(IPC_EVENTS.GET_WINDOW_ID),
|
||||
|
||||
// 发送日志
|
||||
logToMain: (logLevel: string, message: string) => ipcRenderer.send(IPC_EVENTS.LOG_TO_MAIN, logLevel, message),
|
||||
logger: {
|
||||
debug: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta),
|
||||
info: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta),
|
||||
warn: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta),
|
||||
error: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta),
|
||||
}
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
76
src/renderer/components/HeaderBar/index.vue
Normal file
76
src/renderer/components/HeaderBar/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<header class="title-bar flex items-start justify-between h-[40px]">
|
||||
<div class="title-bar-main flex-auto">
|
||||
<slot>{{ title ?? '' }}</slot>
|
||||
</div>
|
||||
|
||||
<div class="title-bar-controls w-[168px] flex items-center justify-end text-tx-secondary">
|
||||
<native-tooltip content="最小化">
|
||||
<button v-show="isMinimizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||
@click="minimizeWindow">
|
||||
<iconify-icon icon="material-symbols:check-indeterminate-small" color="#ffffff" :width="btnSize"
|
||||
:height="btnSize" />
|
||||
</button>
|
||||
</native-tooltip>
|
||||
<native-tooltip :content="isMaximized ? '还原' : '最大化'">
|
||||
<button v-show="isMaximizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||
@click="maximizeWindow">
|
||||
<iconify-icon icon="material-symbols:chrome-maximize-outline-sharp" color="#ffffff" :width="btnSize"
|
||||
:height="btnSize" v-show="!isMaximized" />
|
||||
<iconify-icon icon="material-symbols:chrome-restore-outline-sharp" color="#ffffff" :width="btnSize"
|
||||
:height="btnSize" v-show="isMaximized" />
|
||||
</button>
|
||||
</native-tooltip>
|
||||
<native-tooltip content="关闭">
|
||||
<button v-show="isClosable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||
@click="handleClose">
|
||||
<iconify-icon icon="material-symbols:close" color="#ffffff" :width="btnSize" :height="btnSize" />
|
||||
</button>
|
||||
</native-tooltip>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon as IconifyIcon } from '@iconify/vue'
|
||||
import { useWinManager } from '@hooks/useWinManager'
|
||||
|
||||
import NativeTooltip from '@components/NativeTooltip/index.vue'
|
||||
|
||||
interface HeaderBarProps {
|
||||
title?: string;
|
||||
isMaximizable?: boolean;
|
||||
isMinimizable?: boolean;
|
||||
isClosable?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'HeaderBar' })
|
||||
|
||||
withDefaults(defineProps<HeaderBarProps>(), {
|
||||
isMaximizable: true,
|
||||
isMinimizable: true,
|
||||
isClosable: true,
|
||||
})
|
||||
const emit = defineEmits(['close']);
|
||||
// const { t } = useI18n();
|
||||
|
||||
const btnSize = 16;
|
||||
|
||||
const {
|
||||
isMaximized,
|
||||
closeWindow,
|
||||
minimizeWindow,
|
||||
maximizeWindow
|
||||
} = useWinManager();
|
||||
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
closeWindow();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-bar {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
43
src/renderer/components/NativeTooltip/index.vue
Normal file
43
src/renderer/components/NativeTooltip/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<template v-if="slots.default()[0].el">
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :title="content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { logger } from '@utils/logger'
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'NativeTooltip' });
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const slots = defineSlots()
|
||||
|
||||
if (slots?.default?.().length > 1) {
|
||||
logger.warn('NativeTooltip only support one slot.')
|
||||
}
|
||||
|
||||
const updateTooltipContent = (content: string) => {
|
||||
const defaultSlot = slots?.default?.();
|
||||
|
||||
if (defaultSlot) {
|
||||
const slotElement = defaultSlot[0]?.el
|
||||
|
||||
if (slotElement && slotElement instanceof HTMLElement) {
|
||||
slotElement.title = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => updateTooltipContent(props.content))
|
||||
|
||||
watch(() => props.content, (val: string) => updateTooltipContent(val));
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
export function useWinManager() {
|
||||
const isMaximized = ref(false)
|
||||
|
||||
function closeWindow() {
|
||||
window.api.closeWindow();
|
||||
}
|
||||
|
||||
function minimizeWindow() {
|
||||
window.api.minimizeWindow();
|
||||
}
|
||||
|
||||
function maximizeWindow() {
|
||||
window.api.maximizeWindow();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
window.api.viewIsReady();
|
||||
isMaximized.value = await window.api.isWindowMaximized();
|
||||
window.api.onWindowMaximized((_isMaximized: boolean) => isMaximized.value = _isMaximized);
|
||||
})
|
||||
|
||||
return {
|
||||
isMaximized,
|
||||
closeWindow,
|
||||
minimizeWindow,
|
||||
maximizeWindow
|
||||
}
|
||||
};
|
||||
|
||||
export default useWinManager;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import { createApp, type Plugin } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import router from "./router";
|
||||
import App from "./App.vue";
|
||||
@@ -10,6 +10,15 @@ import locale from 'element-plus/es/locale/lang/zh-cn'
|
||||
import "./styles/index.css";
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
// 引入全局组件
|
||||
import HeaderBar from './components/HeaderBar/index.vue'
|
||||
import DragRegion from './components/DragRegion/index.vue'
|
||||
|
||||
const components: Plugin = (app) => {
|
||||
app.component('HeaderBar', HeaderBar);
|
||||
app.component('DragRegion', DragRegion);
|
||||
}
|
||||
|
||||
// 创建 Vue 应用实例
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -20,6 +29,7 @@ app.use(createPinia());
|
||||
// 使用 Vue Router
|
||||
app.use(router);
|
||||
app.use(ElementPlus, { locale })
|
||||
app.use(components)
|
||||
|
||||
// 挂载应用到 DOM
|
||||
app.mount("#app");
|
||||
|
||||
12
src/renderer/utils/logger.ts
Normal file
12
src/renderer/utils/logger.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export const logger = window.api.logger ?? console;
|
||||
|
||||
if (window.api.logger) {
|
||||
console.debug = logger.debug;
|
||||
console.log = logger.info;
|
||||
console.info = logger.info;
|
||||
console.warn = logger.warn;
|
||||
console.error = logger.error;
|
||||
}
|
||||
|
||||
export default logger;
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="h-screen box-border p-[8px] login-bg flex items-center justify-center">
|
||||
<div class="h-screen login-bg flex flex-col">
|
||||
<header-bar>
|
||||
<drag-region class="w-full" />
|
||||
</header-bar>
|
||||
|
||||
<main class="box-border p-[8px] flex flex-auto items-center justify-center">
|
||||
|
||||
<div class="w-[836px] h-full bg-white rounded-2xl p-[32px] flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<img class="w-[48px] h-[48px]" src="@assets/images/login/blue_logo.png" />
|
||||
@@ -75,12 +81,12 @@
|
||||
</div>
|
||||
|
||||
<img class="w-[570px]" src="@assets/images/login/logo.png" />
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { authOauth2TokenUsingPost } from "@renderer/api";
|
||||
import { RiUser3Fill, RiKey2Fill } from '@remixicon/vue'
|
||||
import { generateUUID } from "@utils/generateUUID";
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
"@utils/*": ["src/renderer/utils/*"],
|
||||
"@common/*": ["src/common/*"],
|
||||
"@modules/*": ["src/main/modules/*"],
|
||||
"@locales/*": ["locales/*"]
|
||||
"@locales/*": ["locales/*"],
|
||||
"@hooks/*": ["src/renderer/hooks/*"],
|
||||
"@components/*": ["src/renderer/components/*"],
|
||||
},
|
||||
"types": ["element-plus/global", "vue"]
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"forge.env.d.ts",
|
||||
|
||||
@@ -25,6 +25,8 @@ export default defineConfig(async () => {
|
||||
"@assets": resolve(__dirname, "./src/assets"),
|
||||
'@common': resolve(__dirname, './src/common'),
|
||||
"@constant": resolve(__dirname, "./src/renderer/constant"),
|
||||
"@components": resolve(__dirname, "./src/renderer/components"),
|
||||
"@hooks": resolve(__dirname, "./src/renderer/hooks"),
|
||||
"@store": resolve(__dirname, "./src/renderer/store"),
|
||||
"@utils": resolve(__dirname, "./src/renderer/utils"),
|
||||
"@shared": resolve(__dirname, "./src/renderer/shared"),
|
||||
|
||||
Reference in New Issue
Block a user