fix: deploy auto exec

This commit is contained in:
王性驊 2025-09-11 07:43:41 +08:00
commit 66bdc2a2bc
15 changed files with 18081 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

201
app/app.vue Normal file
View File

@ -0,0 +1,201 @@
<script setup>
import Taskbar from '~/components/Taskbar.vue';
import AboutMeWindow from '~/components/AboutMeWindow.vue';
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';
// Reactive array to hold all window states
const windows = reactive([]);
// Keep track of the highest z-index to ensure new/focused windows are on top
const currentZIndex = ref(100);
// Function to generate a unique ID for each window instance
const generateUniqueId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
// Load window states from localStorage on mount
onMounted(() => {
const savedWindows = localStorage.getItem('windows');
if (savedWindows) {
const parsedWindows = JSON.parse(savedWindows);
parsedWindows.forEach(win => {
// 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
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 = 400, initialHeight = 300) => {
const existingWindow = windows.find(w => w.type === type);
if (existingWindow) {
bringWindowToFront(existingWindow.id);
if (existingWindow.isMinimized) {
restoreWindow(existingWindow.id);
}
existingWindow.isVisible = true; // Ensure it's visible if it was closed
} else {
currentZIndex.value++;
// Calculate initial position to center the window within the desktop content area
const desktopWidth = window.innerWidth;
const desktopHeight = window.innerHeight - 40; // Minus taskbar height
const initialX = (desktopWidth - initialWidth) / 2;
const initialY = (desktopHeight - initialHeight) / 2 + 40; // Add taskbar height
const newWindow = {
id: generateUniqueId(),
type,
title: titleKey, // Store the translation key here
isVisible: true,
isMinimized: false,
x: initialX,
y: initialY,
width: initialWidth,
height: initialHeight,
zIndex: currentZIndex.value,
};
windows.push(newWindow);
saveWindowsToLocalStorage();
}
};
// Function to close a window
const closeWindow = (id) => {
const index = windows.findIndex(w => w.id === id);
if (index !== -1) {
windows.splice(index, 1);
saveWindowsToLocalStorage();
}
};
// Function to close all windows
const closeAllWindows = () => {
windows.splice(0, windows.length); // Clear the array
saveWindowsToLocalStorage();
};
// Function to minimize a window
const minimizeWindow = (id) => {
const windowToMinimize = windows.find(w => w.id === id);
if (windowToMinimize) {
windowToMinimize.isMinimized = true;
saveWindowsToLocalStorage();
}
};
// Function to restore a minimized window
const restoreWindow = (id) => {
const windowToRestore = windows.find(w => w.id === id);
if (windowToRestore) {
windowToRestore.isMinimized = false;
bringWindowToFront(id);
saveWindowsToLocalStorage();
}
};
// Function to bring a window to the front (update z-index)
const bringWindowToFront = (id) => {
const windowToFront = windows.find(w => w.id === id);
if (windowToFront) {
currentZIndex.value++;
windowToFront.zIndex = currentZIndex.value;
saveWindowsToLocalStorage();
}
};
// Function to update window position (for dragging)
const updateWindowPosition = (id, newX, newY) => {
const windowToUpdate = windows.find(w => w.id === id);
if (windowToUpdate) {
windowToUpdate.x = newX;
windowToUpdate.y = newY;
saveWindowsToLocalStorage();
}
};
// Handle opening About Me window from Taskbar
const openAboutMeWindow = () => {
openWindow('about-me', 'about_me'); // Pass the translation key
};
// Computed property for minimized windows
const minimizedWindows = computed(() => {
return windows.filter(win => win.isMinimized);
});
// Clean up localStorage on component unmount (optional, for development)
onUnmounted(() => {
// localStorage.removeItem('windows');
});
</script>
<template>
<div>
<Taskbar
@open-about-me="openAboutMeWindow"
@close-all-windows="closeAllWindows"
:minimized-windows="minimizedWindows"
@restore-window="restoreWindow"
/>
<div class="desktop-content">
<NuxtPage />
</div>
<!-- Dynamically render windows -->
<template v-for="window in windows" :key="window.id">
<AboutMeWindow
v-if="window.type === 'about-me' && window.isVisible && !window.isMinimized"
:window-data="window"
@close="closeWindow(window.id)"
@minimize="minimizeWindow(window.id)"
@restore="restoreWindow(window.id)"
@bring-to-front="bringWindowToFront(window.id)"
@update-position="updateWindowPosition(window.id, $event.x, $event.y)"
/>
</template>
</div>
</template>
<style>
body {
background-color: #008080; /* Windows 95 teal */
background-image: url('https://picsum.photos/1920/1080'); /* Placeholder wallpaper */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
font-family: 'Courier New', Courier, monospace;
margin: 0;
overflow: hidden; /* Hide scrollbars from the body */
}
/* Adjust Taskbar position */
.taskbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 40px;
z-index: 1000; /* Ensure it's on top of other content */
}
.desktop-content {
/* Push content down by the height of the taskbar */
margin-top: 40px;
height: calc(100vh - 40px); /* Fill remaining height */
width: 100%;
box-sizing: border-box;
overflow-y: auto; /* Allow scrolling for desktop content if needed */
background-color: transparent; /* Changed to transparent */
border: none; /* Removed border */
box-shadow: none; /* Removed shadow */
}
</style>

View File

@ -0,0 +1,180 @@
<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>

216
app/components/Taskbar.vue Normal file
View File

@ -0,0 +1,216 @@
<template>
<div class="taskbar">
<div class="start-section">
<button class="start-button" @click="toggleStartMenu">
<span class="start-icon">🪟</span>
<span>{{ t('start') }}</span>
</button>
<div v-if="showStartMenu" class="start-menu">
<div class="menu-item" @click="openAboutMe">
{{ t('about_me') }}
</div>
<!-- More menu items can go here -->
<div class="menu-item" @click="closeAllWindows">
{{ t('close_all_windows') }}
</div>
</div>
</div>
<div class="task-buttons">
<button
v-for="window in minimizedWindows"
:key="window.id"
class="task-button"
@click="restoreMinimizedWindow(window.id)"
>
{{ t(window.title) }}
</button>
</div>
<div class="right-section">
<div class="language-toggle" @click="toggleLanguage">
<span>{{ locale === 'zh' ? '注音' : 'ABC' }}</span>
</div>
<div class="clock">
<span>{{ currentTime }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const props = defineProps({
minimizedWindows: {
type: Array,
default: () => [],
},
});
const currentTime = ref(new Date().toLocaleTimeString());
let timerId = null;
const showStartMenu = ref(false);
const emit = defineEmits(['open-about-me', 'restore-window', 'close-all-windows']);
const toggleStartMenu = () => {
showStartMenu.value = !showStartMenu.value;
};
const openAboutMe = () => {
emit('open-about-me');
showStartMenu.value = false;
};
const closeAllWindows = () => {
emit('close-all-windows');
showStartMenu.value = false;
};
const toggleLanguage = () => {
locale.value = locale.value === 'zh' ? 'en' : 'zh';
};
const restoreMinimizedWindow = (id) => {
emit('restore-window', id);
};
onMounted(() => {
timerId = setInterval(() => {
currentTime.value = new Date().toLocaleTimeString();
}, 1000);
});
onUnmounted(() => {
if (timerId) {
clearInterval(timerId);
}
});
</script>
<style scoped>
.taskbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #c0c0c0; /* Classic Windows gray */
padding: 4px 6px;
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff; /* 3D effect */
width: 100%;
box-sizing: border-box;
height: 40px;
position: relative; /* For positioning the start menu */
}
.start-section {
position: relative;
display: flex;
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 {
display: flex;
align-items: center;
padding: 4px 8px;
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff;
background-color: #c0c0c0;
font-weight: bold;
font-size: 1rem;
cursor: pointer;
color: black;
}
.start-button:active {
border-color: #808080 #ffffff #ffffff #808080;
}
.start-icon {
margin-right: 8px;
}
.start-menu {
position: absolute;
top: 100%;
left: 0;
background-color: #c0c0c0;
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff;
padding: 4px;
min-width: 160px;
z-index: 1001;
}
.menu-item {
padding: 4px 8px;
cursor: pointer;
white-space: nowrap;
border: 1px solid transparent;
color: black;
}
.menu-item:hover {
background-color: #000080;
color: white;
border-color: #000080;
}
.language-toggle {
padding: 4px 8px;
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
font-size: 0.9rem;
color: black;
cursor: pointer;
background-color: #c0c0c0;
}
.language-toggle:active {
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>

65
app/pages/index.vue Normal file
View File

@ -0,0 +1,65 @@
<template>
<div class="desktop">
<div class="icon">
<div class="icon-image">[PC]</div>
<div class="icon-label">{{ t('my_computer') }}</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>
</template>
<script setup>
import { useI18n } from 'vue-i18n'; // Import useI18n
const { t } = useI18n(); // Use useI18n composable
</script>
<style scoped>
.desktop {
display: flex;
flex-wrap: wrap;
gap: 2rem;
padding: 2rem;
align-content: flex-start;
background-color: transparent; /* Ensure desktop background is transparent */
}
.icon {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
text-align: center;
cursor: pointer;
}
.icon:hover .icon-image {
background-color: #e0e0e0;
}
.icon-image {
width: 64px;
height: 64px;
border: 2px solid black;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.icon-label {
background-color: white;
padding: 2px 4px;
font-size: 0.8rem;
}
</style>

6
eslint.config.mjs Normal file
View File

@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

11
i18n/locales/en.json Normal file
View File

@ -0,0 +1,11 @@
{
"start": "Start",
"about_me": "About Me",
"hello_developer": "Hello! I'm a developer passionate about creating unique web experiences.",
"demo_ui": "This is a Windows 95 inspired UI.",
"coming_soon": "More content coming soon!",
"my_computer": "My Computer",
"recycle_bin": "Recycle Bin",
"terminal": "Terminal",
"close_all_windows": "Close All Windows"
}

12
i18n/locales/zh.json Normal file
View File

@ -0,0 +1,12 @@
{
"start": "開始",
"about_me": "關於我",
"hello_developer": "哈囉!我是一位熱衷於創造獨特網路體驗的開發者。",
"demo_ui": "這是一個 Windows 95 風格的使用者介面展示。",
"coming_soon": "更多內容即將推出!",
"my_computer": "我的電腦",
"recycle_bin": "資源回收筒",
"terminal": "終端機",
"close_all_windows": "關閉全視窗"
}

28
nuxt.config.ts Normal file
View File

@ -0,0 +1,28 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
modules: [
'@nuxt/ui',
'@nuxt/test-utils',
'@nuxt/scripts',
'@nuxt/image',
'@nuxt/eslint',
'@nuxt/content',
'@nuxtjs/i18n',
],
i18n: {
locales: [
{ code: 'en', file: 'en.json', name: 'English' },
{ code: 'zh', file: 'zh.json', name: '繁體中文' }
],
langDir: 'locales', // 你的語言文件夾名稱
defaultLocale: 'zh', // 預設語言
strategy: 'no_prefix', // 或者 'prefix_except_default'
},
build: {
transpile: ['@nuxtjs/i18n']
}
})

17216
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "nuxt-app",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "^2.7.2",
"@nuxt/eslint": "^1.9.0",
"@nuxt/image": "^1.11.0",
"@nuxt/scripts": "^0.11.13",
"@nuxt/test-utils": "^3.19.2",
"@nuxt/ui": "^3.3.3",
"@nuxtjs/i18n": "^10.1.0",
"@unhead/vue": "^2.0.14",
"eslint": "^9.35.0",
"nuxt": "^4.1.1",
"typescript": "^5.9.2",
"vue": "^3.5.21",
"vue-router": "^4.5.1"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-Agent: *
Disallow:

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}