fix: language

This commit is contained in:
王性驊 2025-09-12 00:50:28 +08:00
parent 33835376ba
commit cfa76cab17
8 changed files with 743 additions and 441 deletions

View File

@ -2,76 +2,76 @@
import Taskbar from '~/components/Taskbar.vue'; import Taskbar from '~/components/Taskbar.vue';
import AboutMeWindow from '~/components/AboutMeWindow.vue'; import AboutMeWindow from '~/components/AboutMeWindow.vue';
import TerminalWindow from '~/components/TerminalWindow.vue'; import TerminalWindow from '~/components/TerminalWindow.vue';
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'; import WinampWindow from '~/components/WinampWindow.vue';
import { useI18n } from 'vue-i18n'; import { ref, reactive, onMounted, computed, provide, onUnmounted } from 'vue';
// --- Data Definitions ---
// Reactive array to hold all window states // Reactive array to hold all window states
const windows = reactive([]); const windows = reactive([]);
// Keep track of the highest z-index to ensure new/focused windows are on top // Keep track of the highest z-index
const currentZIndex = ref(100); const currentZIndex = ref(100);
// Function to generate a unique ID for each window instance // Desktop icons container position
const generateUniqueId = () => { const iconsContainerX = ref(0);
return Date.now().toString(36) + Math.random().toString(36).substr(2); const iconsContainerY = ref(0);
};
// Load window states from localStorage on mount // Dragging state for desktop icons container
onMounted(() => { const isIconsContainerDragging = ref(false);
const savedWindows = localStorage.getItem('windows'); const iconsContainerStartX = ref(0);
if (savedWindows) { const iconsContainerStartY = ref(0);
const parsedWindows = JSON.parse(savedWindows); const initialIconsContainerX = ref(0);
parsedWindows.forEach(win => { const initialIconsContainerY = ref(0);
// Ensure z-index is properly managed on load
if (win.zIndex > currentZIndex.value) {
currentZIndex.value = win.zIndex;
}
windows.push(win);
});
}
});
// Save window states to localStorage whenever windows array changes // Define Start Menu Items with icons and actions
const saveWindowsToLocalStorage = () => { const startMenuItems = ref([
localStorage.setItem('windows', JSON.stringify(windows)); {
}; labelKey: 'terminal',
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0yMiAxNlY4QzIyIDYuODk1NDMgMjEuMTA0NiA2IDIwIDZIMTRMMTIgNEg0QzIuODk1NDMgNCAyIDQuODk1NDMgMiA2VjE4QzIgMTkuMTA0NiAyLjg5NTQzIDIwIDQgMjBIMjBDMjEuMTA0NiAyMCAyMiAxOS4xMDQ2IDIyIDE4VjE2WiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0iTTggMTJMMTAgMTQuNUwxMiAxMkwxMCA5LjVMOCAxMloiIGZpbGw9IndoaXRlIi8+CiAgICA8cGF0aCBkPSJNMTAgMTZIMTRWMThIMTBWMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4=',
action: 'openTerminal',
},
{
labelKey: 'about_me',
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIiBmaWxsPSIjRkZGRjAwIi8+CiAgICA8cGF0aCBkPSJNMTEgN0gxM1Y5SDExVjdaTTExIDExSDEzVjE3SDExVjExWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg==',
action: 'openAboutMe',
},
{
labelKey: 'close_all_windows',
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0xOC41IDUuNUw1LjUgMTguNU01LjUgNS41TDE4LjUgMTguNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
action: 'closeAllWindows',
},
]);
// --- Window Management Functions ---
const generateUniqueId = () => Date.now().toString(36) + Math.random().toString(36).substr(2);
const saveWindowsToLocalStorage = () => localStorage.setItem('windows', JSON.stringify(windows));
// Function to open a new window or bring an existing one to front
const openWindow = (type, titleKey, initialWidth = 600, initialHeight = 400) => { const openWindow = (type, titleKey, initialWidth = 600, initialHeight = 400) => {
const existingWindow = windows.find(w => w.type === type); const existingWindow = windows.find(w => w.type === type);
if (existingWindow) { if (existingWindow) {
bringWindowToFront(existingWindow.id); bringWindowToFront(existingWindow.id);
if (existingWindow.isMinimized) { if (existingWindow.isMinimized) restoreWindow(existingWindow.id);
restoreWindow(existingWindow.id); existingWindow.isVisible = true;
}
existingWindow.isVisible = true; // Ensure it's visible if it was closed
} else { } else {
currentZIndex.value++; currentZIndex.value++;
// Calculate initial position to center the window within the desktop content area
const desktopWidth = window.innerWidth; const desktopWidth = window.innerWidth;
const desktopHeight = window.innerHeight - 40; // Minus taskbar height const desktopHeight = window.innerHeight - 40;
const initialX = (desktopWidth - initialWidth) / 2; const initialX = (desktopWidth - initialWidth) / 2;
const initialY = (desktopHeight - initialHeight) / 2 + 40; // Offset by taskbar height const initialY = (desktopHeight - initialHeight) / 2 + 40;
const newWindow = { windows.push({
id: generateUniqueId(), id: generateUniqueId(),
type, type, title: titleKey, isVisible: true, isMinimized: false,
title: titleKey, // Store the translation key here x: initialX, y: initialY, width: initialWidth, height: initialHeight,
isVisible: true,
isMinimized: false,
x: initialX,
y: initialY,
width: initialWidth,
height: initialHeight,
zIndex: currentZIndex.value, zIndex: currentZIndex.value,
}; });
windows.push(newWindow);
saveWindowsToLocalStorage();
} }
saveWindowsToLocalStorage();
}; };
// Function to close a window
const closeWindow = (id) => { const closeWindow = (id) => {
const index = windows.findIndex(w => w.id === id); const index = windows.findIndex(w => w.id === id);
if (index !== -1) { if (index !== -1) {
@ -80,138 +80,256 @@ const closeWindow = (id) => {
} }
}; };
// Function to close all windows
const closeAllWindows = () => { const closeAllWindows = () => {
windows.splice(0, windows.length); // Clear the array windows.length = 0;
saveWindowsToLocalStorage(); saveWindowsToLocalStorage();
}; };
// Function to minimize a window
const minimizeWindow = (id) => { const minimizeWindow = (id) => {
const windowToMinimize = windows.find(w => w.id === id); const win = windows.find(w => w.id === id);
if (windowToMinimize) { if (win) {
windowToMinimize.isMinimized = true; console.log(`Minimizing window ${id}. Before: isMinimized=${win.isMinimized}`);
win.isMinimized = true;
console.log(`Minimizing window ${id}. After: isMinimized=${win.isMinimized}`);
saveWindowsToLocalStorage(); saveWindowsToLocalStorage();
} }
}; };
// Function to restore a minimized window
const restoreWindow = (id) => { const restoreWindow = (id) => {
const windowToRestore = windows.find(w => w.id === id); const win = windows.find(w => w.id === id);
if (windowToRestore) { if (win) {
windowToRestore.isMinimized = false; console.log(`Restoring window ${id}. Before: isMinimized=${win.isMinimized}`);
win.isMinimized = false;
console.log(`Restoring window ${id}. After: isMinimized=${win.isMinimized}`);
bringWindowToFront(id); bringWindowToFront(id);
saveWindowsToLocalStorage(); saveWindowsToLocalStorage();
} }
}; };
// Function to bring a window to the front (update z-index)
const bringWindowToFront = (id) => { const bringWindowToFront = (id) => {
const windowToFront = windows.find(w => w.id === id); const win = windows.find(w => w.id === id);
if (windowToFront) { if (win) {
currentZIndex.value++; currentZIndex.value++;
windowToFront.zIndex = currentZIndex.value; win.zIndex = currentZIndex.value;
console.log(`Bringing window ${id} to front. New zIndex: ${win.zIndex}`);
saveWindowsToLocalStorage(); saveWindowsToLocalStorage();
} }
}; };
// Function to update window position (for dragging) const updateWindowPosition = (id, { x, y }) => {
const updateWindowPosition = (id, newX, newY) => { const win = windows.find(w => w.id === id);
const windowToUpdate = windows.find(w => w.id === id); if (win) {
if (windowToUpdate) { win.x = x;
windowToUpdate.x = newX; win.y = y;
windowToUpdate.y = newY;
saveWindowsToLocalStorage(); saveWindowsToLocalStorage();
} }
}; };
// Handle opening About Me window from Taskbar const updateWindowDimensions = (id, { width, height }) => {
const openAboutMeWindow = () => { const win = windows.find(w => w.id === id);
openWindow('about-me', 'about_me'); // Pass the translation key if (win) {
win.width = width;
win.height = height;
saveWindowsToLocalStorage();
}
}; };
const openTerminalWindow = () => { // --- Action Handlers ---
openWindow('terminal', 'terminal');
const openAboutMeWindow = () => openWindow('about-me', 'about_me');
const openTerminalWindow = () => openWindow('terminal', 'terminal');
const openWinampWindow = () => {
console.log('Opening Winamp window via provide/inject.');
openWindow('winamp', 'Winamp', 400, 200);
}; };
// Computed property for minimized windows // Provide the openWinampWindow function
const minimizedWindows = computed(() => { provide('openWinamp', openWinampWindow);
return windows.filter(win => win.isMinimized);
const menuActions = {
openTerminal: openTerminalWindow,
openAboutMe: openAboutMeWindow,
closeAllWindows: closeAllWindows,
};
const handleMenuAction = (action) => {
const func = menuActions[action];
if (func) func();
};
// --- Desktop Icons Container Dragging Functions ---
const handleIconsContainerMouseDown = (e) => {
// Only drag if not clicking on a window or taskbar
if (e.target.closest('.window-wrapper') || e.target.closest('.taskbar')) {
console.log('Clicked on a window or taskbar, not dragging desktop icons container.');
return;
}
isIconsContainerDragging.value = true;
iconsContainerStartX.value = e.clientX;
iconsContainerStartY.value = e.clientY;
initialIconsContainerX.value = iconsContainerX.value;
initialIconsContainerY.value = iconsContainerY.value;
console.log(`Icons container drag started. Initial pos: (${initialIconsContainerX.value}, ${initialIconsContainerY.value})`);
};
const handleIconsContainerMouseMove = (e) => {
if (!isIconsContainerDragging.value) return;
const dx = e.clientX - iconsContainerStartX.value;
const dy = e.clientY - iconsContainerStartY.value;
let newX = initialIconsContainerX.value + dx;
let newY = initialIconsContainerY.value + dy;
// Get desktop content dimensions
const desktopContent = document.querySelector('.desktop-content');
const desktopWidth = desktopContent.offsetWidth;
const desktopHeight = desktopContent.offsetHeight;
// Get NuxtPage (icons container) dimensions
const nuxtPage = document.querySelector('.desktop-icons-container');
const nuxtPageWidth = nuxtPage.offsetWidth;
const nuxtPageHeight = nuxtPage.offsetHeight;
// Define boundaries
const minX = -(nuxtPageWidth - desktopWidth);
const minY = -(nuxtPageHeight - desktopHeight);
const maxX = 0;
const maxY = 0;
// Clamp newX and newY within boundaries
iconsContainerX.value = Math.max(minX, Math.min(maxX, newX));
iconsContainerY.value = Math.max(minY, Math.min(maxY, newY));
console.log(`Icons container dragging. Current pos: (${iconsContainerX.value}, ${iconsContainerY.value})`);
};
const handleIconsContainerMouseUp = () => {
if (isIconsContainerDragging.value) {
isIconsContainerDragging.value = false;
console.log(`Icons container drag ended. Final pos: (${iconsContainerX.value}, ${iconsContainerY.value})`);
}
};
// --- Lifecycle Hooks ---
onMounted(() => {
const savedWindows = localStorage.getItem('windows');
if (savedWindows) {
const parsedWindows = JSON.parse(savedWindows);
parsedWindows.forEach(win => {
if (win.zIndex > currentZIndex.value) currentZIndex.value = win.zIndex;
windows.push(win);
});
}
window.addEventListener('mousemove', handleIconsContainerMouseMove);
window.addEventListener('mouseup', handleIconsContainerMouseUp);
}); });
// Clean up localStorage on component unmount (optional, for development)
onUnmounted(() => { onUnmounted(() => {
// localStorage.removeItem('windows'); window.removeEventListener('mousemove', handleIconsContainerMouseMove);
window.removeEventListener('mouseup', handleIconsContainerMouseUp);
}); });
const minimizedWindows = computed(() => windows.filter(win => win.isMinimized));
</script> </script>
<template> <template>
<div> <div>
<Taskbar <Taskbar
@open-about-me="openAboutMeWindow" :menu-items="startMenuItems"
@close-all-windows="closeAllWindows"
@open-terminal="openTerminalWindow"
:minimized-windows="minimizedWindows" :minimized-windows="minimizedWindows"
@menu-action="handleMenuAction"
@restore-window="restoreWindow" @restore-window="restoreWindow"
/> />
<div class="desktop-content"> <div class="desktop-content">
<div
class="desktop-icons-container"
:style="{ left: `${iconsContainerX}px`, top: `${iconsContainerY}px`, cursor: isIconsContainerDragging ? 'grabbing' : 'grab' }"
@mousedown="handleIconsContainerMouseDown"
>
<NuxtPage /> <NuxtPage />
</div> </div>
</div>
<!-- Dynamically render windows -->
<template v-for="window in windows" :key="window.id"> <template v-for="window in windows" :key="window.id">
<AboutMeWindow <AboutMeWindow
v-if="window.type === 'about-me' && window.isVisible && !window.isMinimized" v-if="window.type === 'about-me' && window.isVisible && !window.isMinimized"
:window-data="window" :window-data="window"
@close="closeWindow(window.id)" @close="closeWindow(window.id)"
@minimize="minimizeWindow(window.id)" @minimize="minimizeWindow(window.id)"
@restore="restoreWindow(window.id)"
@bring-to-front="bringWindowToFront(window.id)" @bring-to-front="bringWindowToFront(window.id)"
@update-position="updateWindowPosition(window.id, $event.x, $event.y)" @update-position="updateWindowPosition(window.id, $event)"
@update-dimensions="updateWindowDimensions(window.id, $event)"
/> />
<TerminalWindow <TerminalWindow
v-if="window.type === 'terminal' && window.isVisible && !window.isMinimized" v-if="window.type === 'terminal' && window.isVisible && !window.isMinimized"
:window-data="window" :window-data="window"
@close="closeWindow(window.id)" @close="closeWindow(window.id)"
@minimize="minimizeWindow(window.id)" @minimize="minimizeWindow(window.id)"
@restore="restoreWindow(window.id)"
@bring-to-front="bringWindowToFront(window.id)" @bring-to-front="bringWindowToFront(window.id)"
@update-position="updateWindowPosition(window.id, $event.x, $event.y)" @update-position="updateWindowPosition(window.id, $event)"
@update-dimensions="updateWindowDimensions(window.id, $event)"
/>
<WinampWindow
v-if="window.type === 'winamp' && window.isVisible && !window.isMinimized"
:window-data="window"
@close="closeWindow(window.id)"
@minimize="minimizeWindow(window.id)"
@bring-to-front="bringWindowToFront(window.id)"
@update-position="updateWindowPosition(window.id, $event)"
@update-dimensions="updateWindowDimensions(window.id, $event)"
/> />
</template> </template>
</div> </div>
</template> </template>
<style> <style>
body { html {
background-color: #008080; /* Windows 95 teal */ background-color: #008080;
background-image: url('https://picsum.photos/1920/1080'); /* Placeholder wallpaper */ background-image: url('https://picsum.photos/1920/1080');
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
font-family: 'Courier New', Courier, monospace; height: 100vh; /* Use vh for full viewport height */
margin: 0; }
overflow: hidden; /* Hide scrollbars from the body */
html, body {
margin: 0;
padding: 0;
}
body {
height: 100vh; /* Use vh for full viewport height */
font-family: 'Courier New', Courier, monospace;
} }
/* Adjust Taskbar position */
.taskbar { .taskbar {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 40px; height: 40px;
z-index: 1000; /* Ensure it's on top of other content */ z-index: 1000;
} }
.desktop-content { .desktop-content {
margin-top: 40px; /* Push content down by the height of the taskbar */ position: absolute;
height: calc(100vh - 40px); /* Fill remaining height */ top: 40px;
left: 0;
width: 100%; width: 100%;
height: calc(100vh - 40px); /* Changed to 100vh - 40px */
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto; /* Allow scrolling for desktop content if needed */ background-color: transparent;
background-color: transparent; /* Changed to transparent */ border: none;
border: none; /* Removed border */ box-shadow: none;
box-shadow: none; /* Removed shadow */ overflow: hidden; /* Prevent scrollbars on the desktop content itself */
}
.desktop-icons-container {
position: relative; /* Allow positioning of children (NuxtPage) */
width: 100%;
height: 100%;
cursor: grab;
} }
</style> </style>

View File

@ -1,180 +1,33 @@
<template> <template>
<div <BaseWindow
v-if="windowData.isVisible && !windowData.isMinimized" :window-data="windowData"
class="window" @close="$emit('close')"
:style="windowStyle" @minimize="$emit('minimize')"
@mousedown="bringToFront" @bring-to-front="$emit('bring-to-front')"
@update-position="$emit('update-position', $event)"
@update-dimensions="$emit('update-dimensions', $event)"
> >
<div class="title-bar" @mousedown="startDrag"> <div class="about-content">
<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('hello_developer') }}</p>
<p>{{ t('demo_ui') }}</p> <p>{{ t('demo_ui') }}</p>
<p>{{ t('coming_soon') }}</p> <p>{{ t('coming_soon') }}</p>
</div> </div>
</div> </BaseWindow>
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits, ref, computed, watch } from 'vue'; import BaseWindow from './BaseWindow.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ defineProps({ windowData: Object });
windowData: { defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
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> </script>
<style scoped> <style scoped>
.window { .about-content {
position: absolute; padding: 10px;
background-color: #c0c0c0; /* Classic Windows gray */ color: black;
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> </style>

View File

@ -0,0 +1,207 @@
<template>
<div
class="window-wrapper"
:style="{ top: windowData.y + 'px', left: windowData.x + 'px', width: windowData.width + 'px', height: windowData.height + 'px', zIndex: windowData.zIndex }"
@mousedown="bringToFront"
>
<div class="title-bar" @mousedown.prevent="startDrag">
<span class="title">{{ t(windowData.title) }}</span>
<div class="window-controls">
<button @click.stop="minimize">_</button>
<button @click.stop="close">X</button>
</div>
</div>
<div class="content">
<slot></slot> <!-- Unique content will be injected here -->
</div>
<!-- Resize Handles -->
<div class="resize-handle top" @mousedown.prevent="startResize('top')"></div>
<div class="resize-handle bottom" @mousedown.prevent="startResize('bottom')"></div>
<div class="resize-handle left" @mousedown.prevent="startResize('left')"></div>
<div class="resize-handle right" @mousedown.prevent="startResize('right')"></div>
<div class="resize-handle top-left" @mousedown.prevent="startResize('top-left')"></div>
<div class="resize-handle top-right" @mousedown.prevent="startResize('top-right')"></div>
<div class="resize-handle bottom-left" @mousedown.prevent="startResize('bottom-left')"></div>
<div class="resize-handle bottom-right" @mousedown.prevent="startResize('bottom-right')"></div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ windowData: Object });
const emit = defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
const close = () => emit('close');
const minimize = () => emit('minimize');
const bringToFront = () => emit('bring-to-front');
const startDrag = (event) => {
const startX = event.clientX;
const startY = event.clientY;
const initialX = props.windowData.x;
const initialY = props.windowData.y;
const onMouseMove = (moveEvent) => {
let newX = initialX + (moveEvent.clientX - startX);
let newY = initialY + (moveEvent.clientY - startY);
// Get window dimensions
const windowWidth = props.windowData.width;
const windowHeight = props.windowData.height;
// Define boundaries
const minX = 0;
const minY = 40; // Taskbar height
const maxX = window.innerWidth - windowWidth;
const maxY = window.innerHeight - windowHeight;
// Clamp newX and newY within boundaries
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
emit('update-position', { x: newX, y: newY });
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const startResize = (handle) => {
const startX = event.clientX;
const startY = event.clientY;
const initialX = props.windowData.x;
const initialY = props.windowData.y;
const initialWidth = props.windowData.width;
const initialHeight = props.windowData.height;
const minWidth = 200;
const minHeight = 150;
const onMouseMove = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
let newX = initialX;
let newY = initialY;
let newWidth = initialWidth;
let newHeight = initialHeight;
if (handle.includes('right')) {
newWidth = Math.max(minWidth, initialWidth + deltaX);
}
if (handle.includes('left')) {
const calculatedWidth = initialWidth - deltaX;
if (calculatedWidth >= minWidth) {
newWidth = calculatedWidth;
newX = initialX + deltaX;
}
}
if (handle.includes('bottom')) {
newHeight = Math.max(minHeight, initialHeight + deltaY);
}
if (handle.includes('top')) {
const calculatedHeight = initialHeight - deltaY;
if (calculatedHeight >= minHeight) {
newHeight = calculatedHeight;
newY = initialY + deltaY;
}
}
// Clamp position and dimensions during resize
const currentWindowRight = newX + newWidth;
const currentWindowBottom = newY + newHeight;
const maxAllowedX = window.innerWidth - newWidth;
const maxAllowedY = window.innerHeight - newHeight;
newX = Math.max(0, Math.min(maxAllowedX, newX));
newY = Math.max(40, Math.min(maxAllowedY, newY)); // 40px for taskbar
emit('update-position', { x: newX, y: newY });
emit('update-dimensions', { width: newWidth, height: newHeight });
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
</script>
<style scoped>
.window-wrapper {
position: absolute;
background-color: #c0c0c0;
border: 1px solid;
border-color: #ffffff #808080 #808080 #ffffff;
box-shadow: 1px 1px 4px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
min-width: 200px;
min-height: 150px;
}
.title-bar {
background-color: #000080;
color: white;
padding: 4px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
font-weight: bold;
}
.window-controls button {
background-color: #c0c0c0;
border: 1px solid;
border-color: #ffffff #808080 #808080 #ffffff;
font-weight: bold;
margin-left: 2px;
min-width: 20px;
}
.content {
/* padding: 10px; */ /* REMOVED PADDING */
flex-grow: 1;
overflow: auto;
}
.resize-handle {
position: absolute;
z-index: 10;
}
.resize-handle.top {
top: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize;
}
.resize-handle.bottom {
bottom: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize;
}
.resize-handle.left {
left: -4px; top: 4px; bottom: 4px; width: 8px; cursor: ew-resize;
}
.resize-handle.right {
right: -4px; top: 4px; bottom: 4px; width: 8px; cursor: ew-resize;
}
.resize-handle.top-left {
top: -4px; left: -4px; width: 16px; height: 16px; cursor: nwse-resize;
}
.resize-handle.top-right {
top: -4px; right: -4px; width: 16px; height: 16px; cursor: nesw-resize;
}
.resize-handle.bottom-left {
bottom: -4px; left: -4px; width: 16px; height: 16px; cursor: nesw-resize;
}
.resize-handle.bottom-right {
bottom: -4px; right: -4px; width: 16px; height: 16px; cursor: nwse-resize;
}
</style>

View File

@ -2,19 +2,18 @@
<div class="taskbar"> <div class="taskbar">
<div class="start-section"> <div class="start-section">
<button class="start-button" @click="toggleStartMenu"> <button class="start-button" @click="toggleStartMenu">
<span class="start-icon">🪟</span> <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIGZpbGw9IiNjMGMwYzAiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiBmaWxsPSIjMDAwMDgwIi8+PHJlY3QgeD0iMiIgeT0iMTEiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIGZpbGw9IiMwMDAwODAiLz48cmVjdCB4PSIxMSIgeT0iMiIgd2lkdGg9IjciIGhlaWdodD0iNyIgZmlsbD0iIzAwMDA4MCIvPjxyZWN0IHg9IjExIiB5PSIxMSIgd2lkdGg9IjciIGhlaWdodD0iNyIgZmlsbD0iIzAwMDA4MCIvPjwvc3ZnPg==" alt="Start" class="start-icon" />
<span>{{ t('start') }}</span> <span>{{ t('start') }}</span>
</button> </button>
<div v-if="showStartMenu" class="start-menu"> <div v-if="showStartMenu" class="start-menu">
<div class="menu-item" @click="openTerminal"> <div
{{ t('terminal') }} v-for="item in menuItems"
</div> :key="item.labelKey"
<div class="menu-item" @click="openAboutMe"> class="menu-item"
{{ t('about_me') }} @click="handleMenuClick(item.action)"
</div> >
<!-- More menu items can go here --> <img :src="item.icon" class="menu-icon" />
<div class="menu-item" @click="closeAllWindows"> <span>{{ t(item.labelKey) }}</span>
{{ t('close_all_windows') }}
</div> </div>
</div> </div>
</div> </div>
@ -44,36 +43,25 @@ import { ref, onMounted, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t, locale, setLocale } = useI18n(); const { t, locale, setLocale } = useI18n();
defineProps({ defineProps({
minimizedWindows: { minimizedWindows: { type: Array, default: () => [] },
type: Array, menuItems: { type: Array, default: () => [] },
default: () => [],
},
}); });
const emit = defineEmits(['restore-window', 'menu-action']);
const currentTime = ref(''); const currentTime = ref('');
const isMounted = ref(false); const isMounted = ref(false);
let timerId = null; let timerId = null;
const showStartMenu = ref(false); const showStartMenu = ref(false);
const emit = defineEmits(['open-about-me', 'restore-window', 'close-all-windows', 'open-terminal']);
const toggleStartMenu = () => { const toggleStartMenu = () => {
showStartMenu.value = !showStartMenu.value; showStartMenu.value = !showStartMenu.value;
}; };
const openTerminal = () => { const handleMenuClick = (action) => {
emit('open-terminal'); emit('menu-action', action);
showStartMenu.value = false;
};
const openAboutMe = () => {
emit('open-about-me');
showStartMenu.value = false;
};
const closeAllWindows = () => {
emit('close-all-windows');
showStartMenu.value = false; showStartMenu.value = false;
}; };
@ -88,16 +76,15 @@ const restoreMinimizedWindow = (id) => {
}; };
const updateCurrentTime = () => { const updateCurrentTime = () => {
// Map i18n locale to a specific locale for time formatting
const timeLocale = locale.value === 'zh-tw' ? 'zh-TW' : 'en-US'; const timeLocale = locale.value === 'zh-tw' ? 'zh-TW' : 'en-US';
currentTime.value = new Date().toLocaleTimeString(timeLocale); currentTime.value = new Date().toLocaleTimeString(timeLocale);
}; };
// Watch for locale changes to update the time format immediately
watch(locale, updateCurrentTime); watch(locale, updateCurrentTime);
onMounted(() => { onMounted(() => {
isMounted.value = true; isMounted.value = true;
updateCurrentTime(); // Set initial time based on current locale updateCurrentTime();
timerId = setInterval(updateCurrentTime, 1000); timerId = setInterval(updateCurrentTime, 1000);
}); });
@ -113,53 +100,20 @@ onUnmounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: #c0c0c0; /* Classic Windows gray */ background-color: #c0c0c0;
padding: 4px 6px; padding: 4px 6px;
border-top: 2px solid #ffffff; border-top: 2px solid #ffffff;
border-bottom: 2px solid #808080; border-bottom: 2px solid #808080;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
height: 40px; height: 40px;
position: relative; /* For positioning the start menu */ position: relative;
} }
.start-section { .start-section {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
flex-grow: 0; /* Don't let start section grow */
}
.right-section {
display: flex;
align-items: center;
gap: 6px;
}
.task-buttons {
display: flex;
flex-grow: 1; /* Allow task buttons to take available space */
margin-left: 10px; /* Space between start button and task buttons */
gap: 6px;
overflow-x: auto; /* Allow horizontal scrolling if many windows */
padding-bottom: 2px; /* Prevent scrollbar from overlapping border */
}
.task-button {
padding: 4px 8px;
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
background-color: #c0c0c0;
font-weight: bold;
font-size: 0.9rem;
cursor: pointer;
color: black;
white-space: nowrap; /* Prevent text wrapping */
flex-shrink: 0; /* Prevent buttons from shrinking */
}
.task-button:active {
border-color: #ffffff #808080 #808080 #ffffff;
} }
.start-button { .start-button {
@ -181,26 +135,31 @@ onUnmounted(() => {
.start-icon { .start-icon {
margin-right: 8px; margin-right: 8px;
width: 20px;
height: 20px;
} }
.start-menu { .start-menu {
position: absolute; position: absolute;
top: 100%; /* Opens downwards from the taskbar */ top: 100%;
left: 0; left: 0;
background-color: #c0c0c0; background-color: #c0c0c0;
border: 2px solid; border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff; border-color: #ffffff #808080 #808080 #ffffff;
padding: 4px; padding: 4px;
min-width: 160px; min-width: 200px;
z-index: 1001; z-index: 1001;
} }
.menu-item { .menu-item {
padding: 4px 8px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
border: 1px solid transparent; border: 1px solid transparent;
color: black; color: black;
display: flex;
align-items: center;
gap: 10px;
} }
.menu-item:hover { .menu-item:hover {
@ -209,7 +168,43 @@ onUnmounted(() => {
border-color: #000080; border-color: #000080;
} }
.language-toggle { .menu-icon {
width: 24px;
height: 24px;
}
.task-buttons {
display: flex;
flex-grow: 1;
margin-left: 10px;
gap: 6px;
overflow-x: auto;
}
.task-button {
padding: 4px 8px;
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
background-color: #c0c0c0;
font-weight: bold;
font-size: 0.9rem;
cursor: pointer;
color: black;
white-space: nowrap;
flex-shrink: 0;
}
.task-button:active {
border-color: #ffffff #808080 #808080 #ffffff;
}
.right-section {
display: flex;
align-items: center;
gap: 6px;
}
.language-toggle, .clock {
padding: 4px 8px; padding: 4px 8px;
border: 2px solid; border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080; border-color: #808080 #ffffff #ffffff #808080;
@ -222,13 +217,4 @@ onUnmounted(() => {
.language-toggle:active { .language-toggle:active {
border-color: #ffffff #808080 #808080 #ffffff; border-color: #ffffff #808080 #808080 #ffffff;
} }
.clock {
padding: 4px 8px;
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
font-size: 0.9rem;
color: black;
background-color: #c0c0c0;
}
</style> </style>

View File

@ -1,17 +1,13 @@
<template> <template>
<div <BaseWindow
class="window-wrapper" :window-data="windowData"
:style="{ top: windowData.y + 'px', left: windowData.x + 'px', width: windowData.width + 'px', height: windowData.height + 'px', zIndex: windowData.zIndex }" @close="$emit('close')"
@mousedown="bringToFront" @minimize="$emit('minimize')"
@bring-to-front="$emit('bring-to-front')"
@update-position="$emit('update-position', $event)"
@update-dimensions="$emit('update-dimensions', $event)"
> >
<div class="title-bar" @mousedown.prevent="startDrag"> <div class="terminal-content">
<span class="title">{{ t(windowData.title) }}</span>
<div class="window-controls">
<button @click.stop="minimize">_</button>
<button @click.stop="close">X</button>
</div>
</div>
<div class="content">
<div class="terminal-section"> <div class="terminal-section">
<span class="prompt">guest@daniels-mac:~$</span> <span class="prompt">guest@daniels-mac:~$</span>
<input <input
@ -24,92 +20,38 @@
/> />
</div> </div>
</div> </div>
</div> </BaseWindow>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import BaseWindow from './BaseWindow.vue';
const { t } = useI18n(); defineProps({ windowData: Object });
const props = defineProps({ windowData: Object }); defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
const emit = defineEmits(['close', 'minimize', 'bring-to-front', 'update-position']);
const command = ref(''); const command = ref('');
const runCommand = () => { const runCommand = () => {
console.log(`Command executed: ${command.value}`);
command.value = ''; command.value = '';
}; };
const close = () => emit('close');
const minimize = () => emit('minimize');
const bringToFront = () => emit('bring-to-front');
const startDrag = (event) => {
const startX = event.clientX;
const startY = event.clientY;
const initialX = props.windowData.x;
const initialY = props.windowData.y;
const onMouseMove = (moveEvent) => {
const newX = initialX + (moveEvent.clientX - startX);
const newY = initialY + (moveEvent.clientY - startY);
emit('update-position', { x: newX, y: newY });
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
</script> </script>
<style scoped> <style scoped>
.window-wrapper { .terminal-content {
position: absolute;
background-color: #c0c0c0; /* Windows 95 gray */
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
}
.title-bar {
background-color: #000080; /* Windows 95 blue */
color: white;
padding: 4px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
font-weight: bold;
}
.window-controls button {
background-color: #c0c0c0;
border: 1px solid;
border-color: #ffffff #808080 #808080 #ffffff;
font-weight: bold;
margin-left: 2px;
min-width: 20px;
}
.content {
padding: 10px;
flex-grow: 1;
background-color: black; background-color: black;
color: #34d399; /* green-400 */ color: #34d399; /* green-400 */
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
height: 100%;
display: flex;
flex-direction: column;
padding: 5px; /* Added small padding */
} }
.terminal-section { .terminal-section {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
} }
.prompt { .prompt {

View File

@ -0,0 +1,48 @@
<script setup>
import BaseWindow from '~/components/BaseWindow.vue';
const emit = defineEmits([
'close',
'minimize',
'bring-to-front',
'update-position',
'update-dimensions',
]);
const handleClose = () => emit('close');
const handleMinimize = () => emit('minimize');
const handleBringToFront = () => emit('bring-to-front');
const handleUpdatePosition = (newPosition) => emit('update-position', newPosition);
const handleUpdateDimensions = (newDimensions) => emit('update-dimensions', newDimensions);
</script>
<template>
<BaseWindow
:window-data="windowData"
@close="handleClose"
@minimize="handleMinimize"
@bring-to-front="handleBringToFront"
@update-position="handleUpdatePosition"
@update-dimensions="handleUpdateDimensions"
>
<div class="winamp-content">
<!-- Winamp content goes here -->
<p>This is the Winamp player.</p>
<p>You can add your music player UI here.</p>
</div>
</BaseWindow>
</template>
<style scoped>
.winamp-content {
padding: 10px;
background-color: #C0C0C0;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'MS Sans Serif', 'Arial', sans-serif;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,149 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps({
windowData: Object,
});
const emit = defineEmits([
'close',
'minimize',
'bring-to-front',
'update-position',
'update-dimensions',
]);
const isDragging = ref(false);
const startX = ref(0);
const startY = ref(0);
const initialX = ref(0); // Store initial x position when drag starts
const initialY = ref(0); // Store initial y position when drag starts
const currentX = ref(0); // New ref for current X during drag
const currentY = ref(0); // New ref for current Y during drag
const windowRef = ref(null);
const handleMouseDown = (e) => {
if (e.target.classList.contains('window-header') || e.target.classList.contains('window-title')) {
isDragging.value = true;
startX.value = e.clientX;
startY.value = e.clientY;
initialX.value = props.windowData.x; // Initialize with prop value at drag start
initialY.value = props.windowData.y; // Initialize with prop value at drag start
currentX.value = props.windowData.x; // Initialize current with prop value
currentY.value = props.windowData.y; // Initialize current with prop value
emit('bring-to-front');
}
};
const handleMouseMove = (e) => {
if (!isDragging.value) return;
const dx = e.clientX - startX.value;
const dy = e.clientY - startY.value;
// Update local position during drag based on initial position
currentX.value = initialX.value + dx; // Use initialX, not props.windowData.x
currentY.value = initialY.value + dy; // Use initialY, not props.windowData.y
};
const handleMouseUp = () => {
if (isDragging.value) {
isDragging.value = false;
// Emit final position only once after drag ends
emit('update-position', { x: currentX.value, y: currentY.value });
}
};
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
});
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
});
const closeWindow = () => {
emit('close');
};
const minimizeWindow = () => {
emit('minimize');
};
const bringToFront = () => {
emit('bring-to-front');
};
</script>
<template>
<div
ref="windowRef"
class="window"
:style="{
// Use currentX and currentY for positioning during drag
left: `${isDragging ? currentX : windowData.x}px`,
top: `${isDragging ? currentY : windowData.y}px`,
width: `${windowData.width}px`,
height: `${windowData.height}px`,
zIndex: windowData.zIndex,
}"
@mousedown="bringToFront"
>
<div class="window-header" @mousedown="handleMouseDown">
<span class="window-title">{{ windowData.title }}</span>
<div class="window-controls">
<button @click="minimizeWindow">_</button>
<button @click="closeWindow">X</button>
</div>
</div>
<div class="window-content">
<slot></slot>
</div>
</div>
</template>
<style scoped>
.window {
position: absolute;
border: 2px solid #000;
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
background-color: #C0C0C0;
display: flex;
flex-direction: column;
resize: both;
overflow: auto;
}
.window-header {
background-color: #000080;
color: white;
padding: 4px;
cursor: grab;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'MS Sans Serif', 'Arial', sans-serif;
font-size: 14px;
}
.window-title {
font-weight: bold;
}
.window-controls button {
background-color: #C0C0C0;
border: 1px solid #000;
padding: 1px 6px;
margin-left: 4px;
cursor: pointer;
font-family: 'MS Sans Serif', 'Arial', sans-serif;
font-size: 12px;
}
.window-content {
flex-grow: 1;
background-color: #C0C0C0;
border-top: 2px solid #000;
}
</style>

View File

@ -1,16 +1,15 @@
<script setup>
import { inject } from 'vue';
const openWinamp = inject('openWinamp');
</script>
<template> <template>
<div class="desktop"> <div class="desktop">
<div class="icon"> <div class="icon" @click="openWinamp">
<div class="icon-image">[PC]</div> <div class="icon-image">[W]</div>
<div class="icon-label">{{ $t('my_computer') }}</div> <div class="icon-label">Winamp</div>
</div>
<div class="icon">
<div class="icon-image">[BIN]</div>
<div class="icon-label">{{ $t('recycle_bin') }}</div>
</div>
<div class="icon">
<div class="icon-image">[>]_</div>
<div class="icon-label">{{ $t('terminal') }}</div>
</div> </div>
</div> </div>
</template> </template>