181 lines
4.2 KiB
Vue
181 lines
4.2 KiB
Vue
|
<template>
|
||
|
<div
|
||
|
v-if="windowData.isVisible && !windowData.isMinimized"
|
||
|
class="window"
|
||
|
:style="windowStyle"
|
||
|
@mousedown="bringToFront"
|
||
|
>
|
||
|
<div class="title-bar" @mousedown="startDrag">
|
||
|
<div class="title-bar-text">{{ t(windowData.title) }}</div>
|
||
|
<div class="title-bar-controls">
|
||
|
<button aria-label="Minimize" @click.stop="minimizeWindow"></button>
|
||
|
<button aria-label="Close" @click.stop="closeWindow"></button>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="window-body">
|
||
|
<p>{{ t('hello_developer') }}</p>
|
||
|
<p>{{ t('demo_ui') }}</p>
|
||
|
<p>{{ t('coming_soon') }}</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
|
||
|
import { useI18n } from 'vue-i18n';
|
||
|
|
||
|
const { t } = useI18n();
|
||
|
|
||
|
const props = defineProps({
|
||
|
windowData: {
|
||
|
type: Object,
|
||
|
required: true,
|
||
|
default: () => ({}),
|
||
|
},
|
||
|
});
|
||
|
|
||
|
const emit = defineEmits([
|
||
|
'close',
|
||
|
'minimize',
|
||
|
'restore',
|
||
|
'bring-to-front',
|
||
|
'update-position',
|
||
|
]);
|
||
|
|
||
|
// Local refs for dragging, updated by watch for initial position from props
|
||
|
const x = ref(props.windowData.x);
|
||
|
const y = ref(props.windowData.y);
|
||
|
|
||
|
let isDragging = false;
|
||
|
let dragOffsetX = 0;
|
||
|
let dragOffsetY = 0;
|
||
|
let animationFrameId = null; // To store requestAnimationFrame ID
|
||
|
|
||
|
// Watch for changes in windowData.x and windowData.y (e.g., from localStorage load or app.vue updates)
|
||
|
watch(() => props.windowData.x, (newX) => { x.value = newX; });
|
||
|
watch(() => props.windowData.y, (newY) => { y.value = newY; });
|
||
|
|
||
|
const windowStyle = computed(() => ({
|
||
|
left: `${x.value}px`,
|
||
|
top: `${y.value}px`,
|
||
|
width: `${props.windowData.width}px`,
|
||
|
height: `${props.windowData.height}px`,
|
||
|
zIndex: props.windowData.zIndex,
|
||
|
}));
|
||
|
|
||
|
const startDrag = (event) => {
|
||
|
isDragging = true;
|
||
|
dragOffsetX = event.clientX - x.value;
|
||
|
dragOffsetY = event.clientY - y.value;
|
||
|
window.addEventListener('mousemove', doDrag);
|
||
|
window.addEventListener('mouseup', stopDrag);
|
||
|
bringToFront(); // Bring to front when dragging starts
|
||
|
};
|
||
|
|
||
|
const doDrag = (event) => {
|
||
|
if (isDragging) {
|
||
|
const newX = event.clientX - dragOffsetX;
|
||
|
const newY = event.clientY - dragOffsetY;
|
||
|
|
||
|
if (animationFrameId) {
|
||
|
cancelAnimationFrame(animationFrameId);
|
||
|
}
|
||
|
animationFrameId = requestAnimationFrame(() => {
|
||
|
x.value = newX;
|
||
|
y.value = newY;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const stopDrag = () => {
|
||
|
isDragging = false;
|
||
|
window.removeEventListener('mousemove', doDrag);
|
||
|
window.removeEventListener('mouseup', stopDrag);
|
||
|
if (animationFrameId) {
|
||
|
cancelAnimationFrame(animationFrameId);
|
||
|
animationFrameId = null;
|
||
|
}
|
||
|
// Emit the final updated position
|
||
|
emit('update-position', { x: x.value, y: y.value });
|
||
|
};
|
||
|
|
||
|
const closeWindow = () => {
|
||
|
emit('close');
|
||
|
};
|
||
|
|
||
|
const minimizeWindow = () => {
|
||
|
emit('minimize');
|
||
|
};
|
||
|
|
||
|
const bringToFront = () => {
|
||
|
emit('bring-to-front');
|
||
|
};
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.window {
|
||
|
position: absolute;
|
||
|
background-color: #c0c0c0; /* Classic Windows gray */
|
||
|
border: 2px solid;
|
||
|
border-color: #ffffff #808080 #808080 #ffffff; /* 3D effect */
|
||
|
box-shadow: 4px 4px 0px #000; /* Simple black shadow */
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
/* z-index, top, left, width, height are set dynamically via style binding */
|
||
|
}
|
||
|
|
||
|
.title-bar {
|
||
|
background: linear-gradient(to right, #0a246a, #a6caf0); /* Classic blue gradient */
|
||
|
padding: 3px 2px 3px 3px;
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
align-items: center;
|
||
|
font-weight: bold;
|
||
|
color: white;
|
||
|
font-size: 0.9rem;
|
||
|
cursor: grab;
|
||
|
}
|
||
|
|
||
|
.title-bar-text {
|
||
|
margin-left: 3px;
|
||
|
}
|
||
|
|
||
|
.title-bar-controls button {
|
||
|
background-color: #c0c0c0;
|
||
|
border: 2px solid;
|
||
|
border-color: #ffffff #000 #000 #ffffff;
|
||
|
width: 16px;
|
||
|
height: 14px;
|
||
|
font-size: 0.8rem;
|
||
|
line-height: 1;
|
||
|
padding: 0;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.title-bar-controls button:active {
|
||
|
border-color: #000 #ffffff #ffffff #000;
|
||
|
}
|
||
|
|
||
|
.title-bar-controls button[aria-label="Minimize"]::before {
|
||
|
content: '_';
|
||
|
display: block;
|
||
|
position: relative;
|
||
|
top: -2px;
|
||
|
}
|
||
|
|
||
|
.title-bar-controls button[aria-label="Close"]::before {
|
||
|
content: 'X';
|
||
|
display: block;
|
||
|
position: relative;
|
||
|
top: -2px;
|
||
|
}
|
||
|
|
||
|
.window-body {
|
||
|
padding: 1rem;
|
||
|
flex-grow: 1;
|
||
|
overflow-y: auto;
|
||
|
border-top: 2px solid #000;
|
||
|
background-color: #c0c0c0; /* Ensure classic gray background for body */
|
||
|
}
|
||
|
</style>
|