添加项目基础文件结构,包括Tauri配置、前端框架和核心功能模块 实现WebGPU渲染器基础、纹理管理和着色器 添加UI组件包括标题栏、确认对话框、帮助窗口等 设置状态管理存储和工具函数 配置构建工具和开发环境
257 lines
6.1 KiB
Vue
257 lines
6.1 KiB
Vue
<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>
|