feat: add taskbar

This commit is contained in:
王性驊 2025-09-24 17:45:36 +08:00
parent e43449f8b4
commit baecaa2e2d
11 changed files with 1542 additions and 122 deletions

View File

@ -19,6 +19,9 @@
--taskbar-item-text-color: #ffffff;
--taskbar-item-border-color: rgba(255, 255, 255, 0.2);
--start-menu-background: var(--window-background);
--start-menu-border-color: var(--window-border-color);
/* Shadows */
--shadow-window: 0 10px 30px rgba(0, 0, 0, 0.2);
--shadow-button: 0 4px 10px rgba(0, 0, 0, 0.2);
@ -31,6 +34,7 @@
/* Z-Indexes */
--z-window-base: 100;
--z-taskbar: 9999;
--z-start-menu: 10000;
--z-resizer: 1;
}
@ -50,6 +54,9 @@
--taskbar-item-background-active: rgba(0, 0, 0, 0.15);
--taskbar-item-text-color: #000000;
--taskbar-item-border-color: rgba(0, 0, 0, 0.1);
--start-menu-background: var(--window-background);
--start-menu-border-color: var(--window-border-color);
}

View File

@ -2,8 +2,8 @@
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useWindowsStore } from '../stores/windows';
import { useSettingsStore } from '../stores/settings';
import { useUIStore } from '../stores/ui';
import { useSettingsStore } from '../stores/settings'; // Import settings store
import Window from './Window.vue';
import Taskbar from './Taskbar.vue';
import SnapPreview from './SnapPreview.vue';
@ -11,23 +11,37 @@ import StartMenu from './StartMenu.vue';
import type { SnapType } from '../composables/useDraggable';
const windowsStore = useWindowsStore();
const settingsStore = useSettingsStore();
const uiStore = useUIStore();
const settingsStore = useSettingsStore();
const { orderedWindows } = storeToRefs(windowsStore);
const { createWindow, snapWindow } = windowsStore;
const { toggleTheme } = settingsStore;
const { isStartMenuOpen } = storeToRefs(uiStore); // Get start menu state
const { createWindow, snapWindow, closeAllWindows } = windowsStore;
const { closeStartMenu } = uiStore;
const { toggleTheme } = settingsStore;
const snapPreview = ref<{ x: number; y: number; width: number; height: number; } | null>(null);
// --- Start Menu Event Handlers ---
function handleMenuAction(action: () => void) {
action();
closeStartMenu();
}
function handleAbout() { handleMenuAction(() => console.log('About clicked')); }
function handleSettings() { handleMenuAction(() => console.log('Settings clicked')); }
function handleSignOut() { handleMenuAction(() => console.log('Sign Out clicked')); }
function handleToggleTheme() { handleMenuAction(toggleTheme); }
function handleCloseAllWindows() { handleMenuAction(closeAllWindows); }
// --------------------------------
function handleSnapPreview(snapType: SnapType) {
if (!snapType) {
snapPreview.value = null;
return;
}
const taskbarHeight = 48;
const taskbarHeight = 22;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight - taskbarHeight;
@ -68,15 +82,19 @@ function handleDesktopClick() {
/>
<SnapPreview v-if="snapPreview" :preview="snapPreview" />
<StartMenu />
<StartMenu
v-if="isStartMenuOpen"
@about="handleAbout"
@settings="handleSettings"
@toggle-theme="handleToggleTheme"
@sign-out="handleSignOut"
@close-all-windows="handleCloseAllWindows"
/>
<div class="fixed top-14 left-4 z-[9999] flex flex-col gap-2">
<button @click="() => createWindow('New App')" class="bg-white/20 backdrop-blur-md text-white font-bold py-2 px-4 rounded-lg shadow-lg">
+ Create Window
</button>
<button @click="toggleTheme" class="bg-white/20 backdrop-blur-md text-white font-bold py-2 px-4 rounded-lg shadow-lg">
Toggle Theme
</button>
</div>
<Taskbar />
@ -90,6 +108,6 @@ function handleDesktopClick() {
height: 100vh;
background: var(--background-desktop);
overflow: hidden;
padding-top: 48px;
padding-top: 22px;
}
</style>

View File

@ -1,122 +1,67 @@
<script setup lang="ts">
import { useUIStore } from '../stores/ui';
import { storeToRefs } from 'pinia';
const uiStore = useUIStore();
const { isStartMenuOpen } = storeToRefs(uiStore);
// Mock data for app list
const apps = [
{ name: 'File Explorer', icon: '📁' },
{ name: 'Web Browser', icon: '🌐' },
{ name: 'Settings', icon: '⚙️' },
{ name: 'Calculator', icon: '🧮' },
{ name: 'Notepad', icon: '📝' },
];
// This component now only emits events and has no internal logic.
const emit = defineEmits([
'about',
'settings',
'toggle-theme',
'sign-out',
'close-all-windows'
]);
</script>
<template>
<transition name="start-menu-fade">
<div v-if="isStartMenuOpen" class="start-menu">
<div class="user-profile">
<div class="avatar"></div>
<span>Daniel</span>
</div>
<ul class="app-list">
<li v-for="app in apps" :key="app.name" class="app-item">
<span class="icon">{{ app.icon }}</span>
<span class="name">{{ app.name }}</span>
</li>
</ul>
<div class="power-controls">
<button class="power-btn">Power Off</button>
</div>
</div>
</transition>
<div class="start-menu">
<ul>
<li @click="emit('about')">About This Project</li>
<li class="separator"></li>
<li @click="emit('settings')">System Settings...</li>
<li @click="emit('toggle-theme')">Toggle Theme</li>
<li class="separator"></li>
<li @click="emit('sign-out')">Sign Out</li>
<li @click="emit('close-all-windows')">Close All Windows</li>
</ul>
</div>
</template>
<style scoped>
.start-menu-fade-enter-active,
.start-menu-fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.start-menu-fade-enter-from,
.start-menu-fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.start-menu {
position: fixed;
top: 52px; /* Taskbar height + a small gap */
left: 4px;
width: 320px;
height: 450px;
background: var(--taskbar-background);
backdrop-filter: blur(10px);
border-radius: 8px;
border: 1px solid var(--taskbar-item-border-color);
z-index: calc(var(--z-taskbar) - 1); /* Below taskbar, but above other UI */
display: flex;
flex-direction: column;
color: var(--taskbar-item-text-color);
top: 26px; /* Adjusted for new 22px taskbar height */
left: 8px;
width: 240px;
background-color: var(--start-menu-background);
border: 1px solid var(--start-menu-border-color);
border-radius: 12px;
z-index: 10000;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
padding: 6px;
color: var(--content-text-color);
}
.user-profile {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--taskbar-item-border-color);
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ccc;
margin-right: 12px;
}
.app-list {
ul {
list-style: none;
padding: 8px;
padding: 0;
margin: 0;
flex-grow: 1;
overflow-y: auto;
}
.app-item {
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 6px;
li {
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
border-radius: 6px;
transition: background-color 0.15s ease-in-out;
white-space: nowrap;
}
.app-item:hover {
li:not(.separator):hover {
background-color: var(--taskbar-item-background-hover);
}
.app-item .icon {
font-size: 20px;
margin-right: 12px;
}
.power-controls {
padding: 12px;
border-top: 1px solid var(--taskbar-item-border-color);
display: flex;
justify-content: flex-end;
}
.power-btn {
background: var(--taskbar-item-background);
color: var(--taskbar-item-text-color);
border: 1px solid var(--taskbar-item-border-color);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
.separator {
height: 1px;
background-color: var(--start-menu-border-color);
margin: 6px 0;
padding: 0;
cursor: default;
}
</style>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useWindowsStore } from '../stores/windows';
import { useUIStore } from '../stores/ui';
@ -10,6 +11,59 @@ const { windows } = storeToRefs(windowsStore);
const { focusWindow } = windowsStore;
const { toggleStartMenu } = uiStore;
// --- Datetime Logic ---
const dateString = ref('');
const timeString = ref('');
let timer: number;
const updateTime = () => {
const now = new Date();
dateString.value = now.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
timeString.value = now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
};
onMounted(() => {
updateTime();
timer = window.setInterval(updateTime, 1000);
});
// --- Input Mode Logic ---
const inputMode = ref('A');
const isInputMenuOpen = ref(false);
const inputSwitcherWrapper = ref<HTMLElement | null>(null);
const availableInputModes = [
{ key: '注', label: '注音' },
{ key: 'A', label: 'English (US)' },
];
function toggleInputMenu() {
isInputMenuOpen.value = !isInputMenuOpen.value;
}
function selectInputMode(mode: 'A' | '注') {
inputMode.value = mode;
isInputMenuOpen.value = false;
}
const handleClickOutside = (event: MouseEvent) => {
if (inputSwitcherWrapper.value && !inputSwitcherWrapper.value.contains(event.target as Node)) {
isInputMenuOpen.value = false;
}
};
watch(isInputMenuOpen, (isOpen) => {
if (isOpen) {
document.addEventListener('click', handleClickOutside);
} else {
document.removeEventListener('click', handleClickOutside);
}
});
onUnmounted(() => {
clearInterval(timer);
document.removeEventListener('click', handleClickOutside);
});
function handleTaskbarButtonClick(windowId: string) {
focusWindow(windowId);
}
@ -29,47 +83,85 @@ function handleTaskbarButtonClick(windowId: string) {
{{ window.title }}
</button>
</div>
<div class="taskbar-right-controls">
<div class="input-switcher-wrapper" ref="inputSwitcherWrapper">
<button @click="toggleInputMenu" class="input-switcher">
{{ inputMode }}
</button>
<div v-if="isInputMenuOpen" class="input-menu">
<ul>
<li v-for="mode in availableInputModes" :key="mode.key" @click="selectInputMode(mode.key as 'A' | '注')">
<span class="checkmark" :style="{ visibility: inputMode === mode.key ? 'visible' : 'hidden' }"></span>
<span>{{ mode.label }}</span>
</li>
</ul>
</div>
</div>
<div class="datetime">
<span>{{ dateString }}</span>
<span>{{ timeString }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.taskbar {
position: fixed;
top: 0; /* Changed from bottom to top */
top: 0;
left: 0;
right: 0;
height: 48px;
height: 22px;
background-color: var(--taskbar-background);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
padding: 0 16px;
padding: 0 8px;
z-index: var(--z-taskbar);
color: var(--taskbar-item-text-color);
font-size: 12px;
}
.start-button {
background: none;
border: none;
font-size: 24px;
font-size: 14px;
cursor: pointer;
padding: 0 12px;
color: var(--taskbar-item-text-color);
margin-right: 8px;
padding: 0 8px;
color: inherit;
margin-right: 4px;
line-height: 22px;
}
.window-list {
flex: 1 1 0;
min-width: 0;
display: flex;
gap: 8px;
gap: 4px;
overflow-x: auto;
}
/* Subtle scrollbar styling */
.window-list::-webkit-scrollbar {
height: 2px;
}
.window-list::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 1px;
}
.taskbar-item {
background-color: var(--taskbar-item-background);
color: var(--taskbar-item-text-color);
color: inherit;
border: 1px solid var(--taskbar-item-border-color);
border-radius: var(--rounded-button);
padding: 6px 12px;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s ease;
white-space: nowrap;
}
.taskbar-item:hover {
@ -80,4 +172,71 @@ function handleTaskbarButtonClick(windowId: string) {
background-color: var(--taskbar-item-background-active);
font-weight: bold;
}
.taskbar-right-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; /* Prevent right controls from shrinking */
}
.input-switcher-wrapper {
position: relative;
}
.input-switcher {
background: none;
border: none;
color: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
padding: 0 4px;
}
.datetime {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
}
.input-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
width: 160px;
background-color: var(--start-menu-background);
border: 1px solid var(--start-menu-border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
padding: 4px;
z-index: 10001;
}
.input-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.input-menu li {
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.input-menu li:hover {
background-color: var(--taskbar-item-background-hover);
}
.checkmark {
width: 16px;
text-align: center;
margin-right: 4px;
}
</style>

11
i18n.config.ts Normal file
View File

@ -0,0 +1,11 @@
import en from './lang/en.json';
import zhTW from './lang/zh-TW.json';
export default defineI18nConfig(() => ({
legacy: false,
locale: 'zh-TW',
messages: {
en,
'zh-TW': zhTW,
}
}))

9
lang/en.json Normal file
View File

@ -0,0 +1,9 @@
{
"startMenu": {
"about": "About This Project",
"systemSettings": "System Settings...",
"toggleTheme": "Toggle Theme",
"signOut": "Sign Out",
"closeAllWindows": "Close All Windows"
}
}

9
lang/zh-TW.json Normal file
View File

@ -0,0 +1,9 @@
{
"startMenu": {
"about": "About This Project",
"systemSettings": "System Settings...",
"toggleTheme": "Toggle Theme",
"signOut": "Sign Out",
"closeAllWindows": "Close All Windows"
}
}

View File

@ -2,11 +2,29 @@
export default defineNuxtConfig({
devtools: { enabled: true },
// Register the Pinia module
// Register modules
modules: [
'@pinia/nuxt',
'@nuxtjs/i18n',
],
// i18n configuration
i18n: {
strategy: 'no_prefix', // No URL prefix for locales
locales: [
{
code: 'en',
name: 'English'
},
{
code: 'zh-TW',
name: '繁體中文'
}
],
defaultLocale: 'zh-TW', // Set default language to Traditional Chinese
vueI18n: './i18n.config.ts', // Point to a separate config file for cleaner setup
},
// Global CSS - We will import this in app.vue instead
css: [],
})

1235
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,5 +25,8 @@
"typescript": "^5.9.2",
"vue": "^3.5.21",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@nuxtjs/i18n": "^10.1.0"
}
}

View File

@ -82,6 +82,11 @@ export const useWindowsStore = defineStore('windows', () => {
}
}
function closeAllWindows() {
windows.value = [];
focusedWindowId.value = null;
}
function minimizeWindow(id: string) {
const windowToMinimize = windows.value.find(w => w.id === id);
if (windowToMinimize) {
@ -108,7 +113,7 @@ export const useWindowsStore = defineStore('windows', () => {
const windowToSnap = windows.value.find(w => w.id === id);
if (!windowToSnap) return;
const taskbarHeight = 48;
const taskbarHeight = 22; // Updated taskbar height
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight - taskbarHeight;
@ -162,6 +167,7 @@ export const useWindowsStore = defineStore('windows', () => {
createWindow,
focusWindow,
closeWindow,
closeAllWindows,
minimizeWindow,
toggleMaximize,
snapWindow,