feat: 初始化项目基础架构和核心功能
添加项目基础文件结构,包括Tauri配置、前端框架和核心功能模块 实现WebGPU渲染器基础、纹理管理和着色器 添加UI组件包括标题栏、确认对话框、帮助窗口等 设置状态管理存储和工具函数 配置构建工具和开发环境
This commit is contained in:
256
src/components/TitleBar.vue
Normal file
256
src/components/TitleBar.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* TitleBar Component
|
||||
* Shows when mouse hovers near the top of the window
|
||||
*/
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
alwaysOnTop?: boolean;
|
||||
isDirty?: boolean;
|
||||
hasContent?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'minimize'): void;
|
||||
(e: 'maximize'): void;
|
||||
(e: 'close'): void;
|
||||
(e: 'toggle-pin'): void;
|
||||
(e: 'start-drag'): void;
|
||||
}>();
|
||||
|
||||
const isVisible = ref(false);
|
||||
const isDragging = ref(false);
|
||||
const isMouseInTitleBar = ref(false);
|
||||
|
||||
// Show title bar when mouse is near top of window
|
||||
const TRIGGER_ZONE = 30; // pixels from top to trigger
|
||||
const TITLE_BAR_HEIGHT = 32; // height of title bar
|
||||
const HIDE_DELAY = 300; // ms delay before hiding
|
||||
|
||||
let hideTimeout: number | null = null;
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
// Show if mouse is in trigger zone
|
||||
if (e.clientY <= TRIGGER_ZONE) {
|
||||
showTitleBar();
|
||||
} else if (isVisible.value && !isMouseInTitleBar.value && !isDragging.value) {
|
||||
// Hide if mouse moved away from both trigger zone and title bar
|
||||
if (e.clientY > TITLE_BAR_HEIGHT) {
|
||||
scheduleHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showTitleBar() {
|
||||
cancelHide();
|
||||
isVisible.value = true;
|
||||
}
|
||||
|
||||
function scheduleHide() {
|
||||
// Don't hide while dragging
|
||||
if (isDragging.value) return;
|
||||
|
||||
if (hideTimeout === null) {
|
||||
hideTimeout = window.setTimeout(() => {
|
||||
if (!isMouseInTitleBar.value && !isDragging.value) {
|
||||
isVisible.value = false;
|
||||
}
|
||||
hideTimeout = null;
|
||||
}, HIDE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelHide() {
|
||||
if (hideTimeout !== null) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTitleBarMouseEnter() {
|
||||
isMouseInTitleBar.value = true;
|
||||
cancelHide();
|
||||
isVisible.value = true;
|
||||
}
|
||||
|
||||
function handleTitleBarMouseLeave() {
|
||||
isMouseInTitleBar.value = false;
|
||||
// Don't hide while dragging
|
||||
if (!isDragging.value) {
|
||||
scheduleHide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDoubleClick() {
|
||||
emit('maximize');
|
||||
}
|
||||
|
||||
async function handleDragStart(e: MouseEvent) {
|
||||
if (e.button === 0) {
|
||||
isDragging.value = true;
|
||||
emit('start-drag');
|
||||
|
||||
// Listen for mouseup to end dragging
|
||||
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||
isDragging.value = false;
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// Check if mouse is outside title bar area after drag ends
|
||||
if (upEvent.clientY > TITLE_BAR_HEIGHT) {
|
||||
isMouseInTitleBar.value = false;
|
||||
scheduleHide();
|
||||
}
|
||||
};
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="titleBarRef"
|
||||
class="title-bar"
|
||||
:class="{ 'title-bar--visible': isVisible }"
|
||||
@mouseenter="handleTitleBarMouseEnter"
|
||||
@mouseleave="handleTitleBarMouseLeave"
|
||||
>
|
||||
<div
|
||||
class="title-bar__drag-area"
|
||||
@dblclick="handleDoubleClick"
|
||||
@mousedown="handleDragStart"
|
||||
>
|
||||
<span class="title-bar__title">
|
||||
<template v-if="hasContent">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="title-bar__buttons">
|
||||
<!-- Pin icon: mdi:pin -->
|
||||
<button
|
||||
class="title-bar__btn title-bar__btn--pin"
|
||||
:class="{ 'title-bar__btn--active': alwaysOnTop }"
|
||||
@click="emit('toggle-pin')"
|
||||
title="置顶"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Minimize icon: mdi:minus -->
|
||||
<button
|
||||
class="title-bar__btn title-bar__btn--minimize"
|
||||
@click="emit('minimize')"
|
||||
title="最小化"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M19 13H5v-2h14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Maximize icon: mdi:checkbox-blank-outline -->
|
||||
<button
|
||||
class="title-bar__btn title-bar__btn--maximize"
|
||||
@click="emit('maximize')"
|
||||
title="最大化"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Close icon: mdi:close -->
|
||||
<button
|
||||
class="title-bar__btn title-bar__btn--close"
|
||||
@click="emit('close')"
|
||||
title="关闭"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border-bottom: 1px solid #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 9999;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.2s ease-out;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.title-bar--visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.title-bar__drag-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
-webkit-app-region: drag;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title-bar__title {
|
||||
color: #ccc;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.title-bar__buttons {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.title-bar__btn {
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.title-bar__btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.title-bar__btn--pin.title-bar__btn--active {
|
||||
color: #00c8ff;
|
||||
}
|
||||
|
||||
.title-bar__btn--close:hover {
|
||||
background: #e81123;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user