1st_version
This commit is contained in:
612
vue/src/App.vue
612
vue/src/App.vue
@@ -13,6 +13,8 @@ import {
|
||||
Trophy,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue'
|
||||
import { isRectVisible } from './lighting'
|
||||
import { calculateRevealRadius, resolveInitialTheme, toggleTheme, type Theme } from './theme'
|
||||
|
||||
type SectionId = 'overview' | 'explorer' | 'games' | 'school'
|
||||
type ExplorerItemKind = 'folder' | 'file'
|
||||
@@ -47,6 +49,19 @@ interface ExplorerFolderTreeItem {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
interface SectionMeta {
|
||||
eyebrow: string
|
||||
title: string
|
||||
description: string
|
||||
badge: string
|
||||
}
|
||||
|
||||
interface LightingTarget {
|
||||
element: HTMLElement
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
|
||||
const text = {
|
||||
skip: '跳到主要内容',
|
||||
loginTitle: 'Workspace Login',
|
||||
@@ -59,6 +74,14 @@ const text = {
|
||||
welcome: '欢迎回来',
|
||||
}
|
||||
|
||||
const THEME_STORAGE_KEY = 'workspace-theme'
|
||||
const THEME_REVEAL_DURATION_MS = 520
|
||||
const THEME_APPLY_OFFSET_MS = 320
|
||||
const THEME_CANVAS_COLORS: Record<Theme, string> = {
|
||||
light: '#f5f8fc',
|
||||
dark: '#070d16',
|
||||
}
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loginError = ref('')
|
||||
@@ -81,12 +104,30 @@ const sidebarIndicatorStyle = ref({
|
||||
opacity: '0',
|
||||
})
|
||||
const sidebarIndicatorJelly = ref(false)
|
||||
const theme = ref<Theme>('light')
|
||||
const followsSystemTheme = ref(true)
|
||||
const themeRevealActive = ref(false)
|
||||
const themeRevealStyle = ref<Record<string, string>>({
|
||||
'--reveal-x': '50vw',
|
||||
'--reveal-y': '50vh',
|
||||
'--reveal-radius': '0px',
|
||||
'--reveal-color': THEME_CANVAS_COLORS.light,
|
||||
})
|
||||
let sidebarJellyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let glowFrameId: number | null = null
|
||||
let lightingTargetRefreshFrameId: number | null = null
|
||||
let themeApplyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let themeRevealCleanupTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let systemThemeMediaQuery: MediaQueryList | null = null
|
||||
let systemThemeChangeHandler: ((event: MediaQueryListEvent) => void) | null = null
|
||||
let latestPointer: { x: number; y: number } | null = null
|
||||
let latestPointerTarget: 'workspace' | 'login' | null = null
|
||||
let workspaceLightTargets: LightingTarget[] = []
|
||||
let loginLightTargets: LightingTarget[] = []
|
||||
let workspaceLightObserver: MutationObserver | null = null
|
||||
let loginLightObserver: MutationObserver | null = null
|
||||
const lightTargetSelector =
|
||||
'.nav-item, .ghost-btn, .primary-btn, .icon-btn, .game-card, .file-main, .folder-item, .topbar, .sidebar, .panel, .hero-card, .metric-card, .explorer-toolbar, .folder-list, .file-list, .file-card, .game-player, .study-card, .path-segment, .status'
|
||||
'.nav-item, .ghost-btn, .primary-btn, .icon-btn, .game-card, .file-main, .folder-item, .path-segment'
|
||||
const loginLightTargetSelector = '.login-card, .login-form button, .login-input-shell, .login-card h1'
|
||||
|
||||
const navItems: SectionNavItem[] = [
|
||||
@@ -113,6 +154,74 @@ const gameOptions: GameOption[] = [
|
||||
]
|
||||
|
||||
const currentUser = computed(() => username.value.trim() || 'Guest')
|
||||
const isDarkTheme = computed(() => theme.value === 'dark')
|
||||
const themeToggleAriaLabel = computed(() => (isDarkTheme.value ? '切换为浅色主题' : '切换为深色主题'))
|
||||
const sectionMeta = computed<SectionMeta>(() => {
|
||||
switch (activeSection.value) {
|
||||
case 'explorer':
|
||||
return {
|
||||
eyebrow: 'Explorer',
|
||||
title: '文件编排',
|
||||
description: '用更清晰的目录树和文件卡片处理工作区结构。',
|
||||
badge: `${explorerCurrentFolder.value?.name ?? 'Workspace'} · ${explorerChildItems.value.length} 项`,
|
||||
}
|
||||
case 'games':
|
||||
return {
|
||||
eyebrow: 'Arcade',
|
||||
title: '游戏中心',
|
||||
description: '切换到内置小游戏,支持沉浸式全屏与快速返回。',
|
||||
badge: activeGame.value ? `Now Playing · ${activeGame.value.label}` : `${gameOptions.length} 个可玩项目`,
|
||||
}
|
||||
case 'school':
|
||||
return {
|
||||
eyebrow: 'Learning',
|
||||
title: '学习路径',
|
||||
description: '把阶段任务、知识结构与部署能力集中到同一视图。',
|
||||
badge: '3 条核心进阶路线',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
eyebrow: 'Overview',
|
||||
title: '总览驾驶舱',
|
||||
description: '把常用入口、当前状态与近期重点放在同一块主画布里。',
|
||||
badge: `${navItems.length} 个模块 · ${explorerFolders.value.length} 个目录`,
|
||||
}
|
||||
}
|
||||
})
|
||||
const overviewSignals = computed(() => [
|
||||
{
|
||||
label: '当前模块',
|
||||
value: sectionMeta.value.title,
|
||||
note: '主工作流状态',
|
||||
},
|
||||
{
|
||||
label: '路径深度',
|
||||
value: `${explorerPath.value.length} 层`,
|
||||
note: '当前文件位置',
|
||||
},
|
||||
{
|
||||
label: '主题模式',
|
||||
value: isDarkTheme.value ? 'Dark' : 'Light',
|
||||
note: followsSystemTheme.value ? '跟随系统' : '手动切换',
|
||||
},
|
||||
])
|
||||
const studyTracks = [
|
||||
{
|
||||
title: 'Frontend Systems',
|
||||
description: 'Vue + TypeScript 组件拆分、状态设计、可访问性。',
|
||||
meta: '组件化 · 可访问性 · 工程化',
|
||||
},
|
||||
{
|
||||
title: 'Graphics',
|
||||
description: '游戏渲染循环、碰撞检测、输入系统与性能优化。',
|
||||
meta: 'Canvas · 循环调度 · 性能',
|
||||
},
|
||||
{
|
||||
title: 'Deployment',
|
||||
description: '构建产物、缓存策略、静态资源托管与监控。',
|
||||
meta: '构建 · 托管 · 监控',
|
||||
},
|
||||
]
|
||||
|
||||
const explorerCurrentFolder = computed(
|
||||
() => explorerItems.value.find((item) => item.id === explorerCurrentFolderId.value) ?? explorerItems.value[0],
|
||||
@@ -196,38 +305,181 @@ function setSection(nextSection: SectionId) {
|
||||
statusMessage.value = `已切换到${navItems.find((item) => item.id === nextSection)?.title ?? ''}视图。`
|
||||
}
|
||||
|
||||
function applyMouseLighting(clientX: number, clientY: number) {
|
||||
const workspace = workspaceRef.value
|
||||
if (!workspace) return
|
||||
|
||||
const targets = workspace.querySelectorAll<HTMLElement>(lightTargetSelector)
|
||||
for (const target of targets) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
target.style.setProperty('--lx', `${clientX - rect.left}px`)
|
||||
target.style.setProperty('--ly', `${clientY - rect.top}px`)
|
||||
function readStoredTheme(): Theme | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(THEME_STORAGE_KEY)
|
||||
return raw === 'light' || raw === 'dark' ? raw : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredTheme(nextTheme: Theme) {
|
||||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, nextTheme)
|
||||
} catch {
|
||||
// Ignore storage write errors (private mode, quota, etc.)
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeMeta(nextTheme: Theme) {
|
||||
let meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta')
|
||||
meta.name = 'theme-color'
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
meta.content = THEME_CANVAS_COLORS[nextTheme]
|
||||
}
|
||||
|
||||
function applyTheme(nextTheme: Theme) {
|
||||
theme.value = nextTheme
|
||||
document.documentElement.setAttribute('data-theme', nextTheme)
|
||||
updateThemeMeta(nextTheme)
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
function clearThemeTimers() {
|
||||
if (themeApplyTimer) {
|
||||
clearTimeout(themeApplyTimer)
|
||||
themeApplyTimer = null
|
||||
}
|
||||
if (themeRevealCleanupTimer) {
|
||||
clearTimeout(themeRevealCleanupTimer)
|
||||
themeRevealCleanupTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeToggle(event: MouseEvent) {
|
||||
const nextTheme = toggleTheme(theme.value)
|
||||
followsSystemTheme.value = false
|
||||
writeStoredTheme(nextTheme)
|
||||
|
||||
const toggleButton = event.currentTarget instanceof HTMLElement ? event.currentTarget : null
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (reduceMotion || !toggleButton) {
|
||||
themeRevealActive.value = false
|
||||
clearThemeTimers()
|
||||
applyTheme(nextTheme)
|
||||
return
|
||||
}
|
||||
|
||||
clearThemeTimers()
|
||||
|
||||
const rect = toggleButton.getBoundingClientRect()
|
||||
const originX = rect.left + rect.width / 2
|
||||
const originY = rect.top + rect.height / 2
|
||||
const radius = calculateRevealRadius(originX, originY, window.innerWidth, window.innerHeight)
|
||||
|
||||
themeRevealStyle.value = {
|
||||
'--reveal-x': `${originX}px`,
|
||||
'--reveal-y': `${originY}px`,
|
||||
'--reveal-radius': `${radius}px`,
|
||||
'--reveal-color': THEME_CANVAS_COLORS[nextTheme],
|
||||
}
|
||||
|
||||
themeRevealActive.value = false
|
||||
requestAnimationFrame(() => {
|
||||
themeRevealActive.value = true
|
||||
})
|
||||
|
||||
themeApplyTimer = setTimeout(() => {
|
||||
applyTheme(nextTheme)
|
||||
themeApplyTimer = null
|
||||
}, THEME_APPLY_OFFSET_MS)
|
||||
|
||||
themeRevealCleanupTimer = setTimeout(() => {
|
||||
themeRevealActive.value = false
|
||||
themeRevealCleanupTimer = null
|
||||
}, THEME_REVEAL_DURATION_MS)
|
||||
}
|
||||
|
||||
function initializeTheme() {
|
||||
const storedTheme = readStoredTheme()
|
||||
followsSystemTheme.value = storedTheme === null
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
applyTheme(resolveInitialTheme(storedTheme, prefersDark))
|
||||
}
|
||||
|
||||
function buildLightingTargets(root: HTMLElement, selector: string) {
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const targets: LightingTarget[] = []
|
||||
for (const element of root.querySelectorAll<HTMLElement>(selector)) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (!isRectVisible(rect, viewportWidth, viewportHeight)) continue
|
||||
const target: LightingTarget = {
|
||||
element,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
}
|
||||
element.style.setProperty('--target-left', `${target.left}px`)
|
||||
element.style.setProperty('--target-top', `${target.top}px`)
|
||||
targets.push(target)
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
function refreshLightingTargets() {
|
||||
lightingTargetRefreshFrameId = null
|
||||
workspaceLightTargets = workspaceRef.value ? buildLightingTargets(workspaceRef.value, lightTargetSelector) : []
|
||||
loginLightTargets = loginRef.value ? buildLightingTargets(loginRef.value, loginLightTargetSelector) : []
|
||||
}
|
||||
|
||||
function scheduleLightingTargetsRefresh() {
|
||||
if (lightingTargetRefreshFrameId !== null) return
|
||||
lightingTargetRefreshFrameId = requestAnimationFrame(refreshLightingTargets)
|
||||
}
|
||||
|
||||
function setContainerPointer(container: HTMLElement | null, clientX: number, clientY: number) {
|
||||
if (!container) return
|
||||
container.style.setProperty('--pointer-x', `${clientX}px`)
|
||||
container.style.setProperty('--pointer-y', `${clientY}px`)
|
||||
}
|
||||
|
||||
function clearContainerPointer(container: HTMLElement | null) {
|
||||
if (!container) return
|
||||
container.style.setProperty('--pointer-x', '-9999px')
|
||||
container.style.setProperty('--pointer-y', '-9999px')
|
||||
}
|
||||
|
||||
function reconnectLightingObservers() {
|
||||
if (workspaceLightObserver) {
|
||||
workspaceLightObserver.disconnect()
|
||||
workspaceLightObserver = null
|
||||
}
|
||||
if (loginLightObserver) {
|
||||
loginLightObserver.disconnect()
|
||||
loginLightObserver = null
|
||||
}
|
||||
|
||||
if (workspaceRef.value) {
|
||||
workspaceLightObserver = new MutationObserver(scheduleLightingTargetsRefresh)
|
||||
workspaceLightObserver.observe(workspaceRef.value, { childList: true, subtree: true })
|
||||
}
|
||||
if (loginRef.value) {
|
||||
loginLightObserver = new MutationObserver(scheduleLightingTargetsRefresh)
|
||||
loginLightObserver.observe(loginRef.value, { childList: true, subtree: true })
|
||||
}
|
||||
}
|
||||
|
||||
function applyMouseLighting(clientX: number, clientY: number) {
|
||||
setContainerPointer(workspaceRef.value, clientX, clientY)
|
||||
}
|
||||
|
||||
function applyLoginLighting(clientX: number, clientY: number) {
|
||||
const login = loginRef.value
|
||||
if (!login) return
|
||||
login.classList.add('lighting-active')
|
||||
|
||||
const targets = login.querySelectorAll<HTMLElement>(loginLightTargetSelector)
|
||||
for (const target of targets) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const x = `${clientX - rect.left}px`
|
||||
const y = `${clientY - rect.top}px`
|
||||
target.style.setProperty('--lx', x)
|
||||
target.style.setProperty('--ly', y)
|
||||
target.style.setProperty('--mx', x)
|
||||
target.style.setProperty('--my', y)
|
||||
if (login) {
|
||||
login.classList.add('lighting-active')
|
||||
}
|
||||
setContainerPointer(login, clientX, clientY)
|
||||
}
|
||||
|
||||
function flushMouseLighting() {
|
||||
glowFrameId = null
|
||||
if (document.hidden) return
|
||||
if (!latestPointer) return
|
||||
|
||||
if (latestPointerTarget === 'workspace') {
|
||||
applyMouseLighting(latestPointer.x, latestPointer.y)
|
||||
} else if (latestPointerTarget === 'login') {
|
||||
@@ -236,6 +488,9 @@ function flushMouseLighting() {
|
||||
}
|
||||
|
||||
function onWorkspacePointerMove(event: PointerEvent) {
|
||||
if (!workspaceLightTargets.length) {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
latestPointerTarget = 'workspace'
|
||||
latestPointer = { x: event.clientX, y: event.clientY }
|
||||
if (glowFrameId !== null) return
|
||||
@@ -243,17 +498,14 @@ function onWorkspacePointerMove(event: PointerEvent) {
|
||||
}
|
||||
|
||||
function onWorkspacePointerLeave() {
|
||||
const workspace = workspaceRef.value
|
||||
if (!workspace) return
|
||||
const targets = workspace.querySelectorAll<HTMLElement>(lightTargetSelector)
|
||||
for (const target of targets) {
|
||||
target.style.setProperty('--lx', '-9999px')
|
||||
target.style.setProperty('--ly', '-9999px')
|
||||
}
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
latestPointerTarget = null
|
||||
}
|
||||
|
||||
function onLoginPointerMove(event: PointerEvent) {
|
||||
if (!loginLightTargets.length) {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
latestPointerTarget = 'login'
|
||||
latestPointer = { x: event.clientX, y: event.clientY }
|
||||
if (glowFrameId !== null) return
|
||||
@@ -264,13 +516,7 @@ function onLoginPointerLeave() {
|
||||
const login = loginRef.value
|
||||
if (!login) return
|
||||
login.classList.remove('lighting-active')
|
||||
const targets = login.querySelectorAll<HTMLElement>(loginLightTargetSelector)
|
||||
for (const target of targets) {
|
||||
target.style.setProperty('--lx', '-9999px')
|
||||
target.style.setProperty('--ly', '-9999px')
|
||||
target.style.setProperty('--mx', '-9999px')
|
||||
target.style.setProperty('--my', '-9999px')
|
||||
}
|
||||
clearContainerPointer(login)
|
||||
latestPointerTarget = null
|
||||
}
|
||||
|
||||
@@ -506,6 +752,15 @@ function onFullscreenChange() {
|
||||
|
||||
function onWindowResize() {
|
||||
updateSidebarIndicator(false)
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
function onAnyScroll() {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initializeTheme()
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -513,98 +768,165 @@ watch(
|
||||
async () => {
|
||||
await nextTick()
|
||||
updateSidebarIndicator(true)
|
||||
scheduleLightingTargetsRefresh()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
watch(isLoggedIn, async (loggedIn) => {
|
||||
if (!loggedIn) return
|
||||
await nextTick()
|
||||
updateSidebarIndicator(false)
|
||||
if (loggedIn) {
|
||||
updateSidebarIndicator(false)
|
||||
}
|
||||
refreshLightingTargets()
|
||||
reconnectLightingObservers()
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
clearContainerPointer(loginRef.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange)
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
document.addEventListener('scroll', onAnyScroll, true)
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
systemThemeChangeHandler = (event) => {
|
||||
if (!followsSystemTheme.value) return
|
||||
applyTheme(event.matches ? 'dark' : 'light')
|
||||
}
|
||||
systemThemeMediaQuery.addEventListener('change', systemThemeChangeHandler)
|
||||
nextTick(() => {
|
||||
updateSidebarIndicator(false)
|
||||
refreshLightingTargets()
|
||||
reconnectLightingObservers()
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
clearContainerPointer(loginRef.value)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('fullscreenchange', onFullscreenChange)
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
document.removeEventListener('scroll', onAnyScroll, true)
|
||||
if (systemThemeMediaQuery && systemThemeChangeHandler) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeChangeHandler)
|
||||
}
|
||||
clearThemeTimers()
|
||||
if (sidebarJellyTimer) {
|
||||
clearTimeout(sidebarJellyTimer)
|
||||
}
|
||||
if (glowFrameId !== null) {
|
||||
cancelAnimationFrame(glowFrameId)
|
||||
}
|
||||
if (lightingTargetRefreshFrameId !== null) {
|
||||
cancelAnimationFrame(lightingTargetRefreshFrameId)
|
||||
}
|
||||
if (workspaceLightObserver) {
|
||||
workspaceLightObserver.disconnect()
|
||||
}
|
||||
if (loginLightObserver) {
|
||||
loginLightObserver.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="skip-link" href="#main-content">{{ text.skip }}</a>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="theme-reveal-layer"
|
||||
:class="{ active: themeRevealActive }"
|
||||
:style="themeRevealStyle"
|
||||
></div>
|
||||
|
||||
<main
|
||||
v-if="!isLoggedIn"
|
||||
id="main-content"
|
||||
ref="loginRef"
|
||||
class="login-view"
|
||||
@pointermove="onLoginPointerMove"
|
||||
@pointerleave="onLoginPointerLeave"
|
||||
>
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
<p class="eyebrow">Workspace Console</p>
|
||||
<h1 id="login-title">{{ text.loginTitle }}</h1>
|
||||
<p class="subtitle">{{ text.loginSubtitle }}</p>
|
||||
<Transition name="view-swap" mode="out-in">
|
||||
<main
|
||||
v-if="!isLoggedIn"
|
||||
id="main-content"
|
||||
key="login"
|
||||
ref="loginRef"
|
||||
class="login-view"
|
||||
@pointermove="onLoginPointerMove"
|
||||
@pointerleave="onLoginPointerLeave"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle theme-toggle-floating"
|
||||
:class="{ 'is-dark': isDarkTheme }"
|
||||
:aria-label="themeToggleAriaLabel"
|
||||
@click="handleThemeToggle"
|
||||
>
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-sun">浅</span>
|
||||
<span class="theme-toggle-moon">深</span>
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<label for="username">{{ text.username }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
:placeholder="text.usernamePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
<p class="eyebrow">Workspace Console</p>
|
||||
<h1 id="login-title">{{ text.loginTitle }}</h1>
|
||||
<p class="subtitle">{{ text.loginSubtitle }}</p>
|
||||
|
||||
<label for="password">{{ text.password }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
type="password"
|
||||
:placeholder="text.passwordPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<label for="username">{{ text.username }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
:placeholder="text.usernamePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="loginError" class="form-error" aria-live="polite">{{ loginError }}</p>
|
||||
<button type="submit">{{ text.loginButton }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<label for="password">{{ text.password }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
type="password"
|
||||
:placeholder="text.passwordPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main
|
||||
v-else
|
||||
id="main-content"
|
||||
ref="workspaceRef"
|
||||
class="workspace-view"
|
||||
@pointermove="onWorkspacePointerMove"
|
||||
@pointerleave="onWorkspacePointerLeave"
|
||||
>
|
||||
<p v-if="loginError" class="form-error" aria-live="polite">{{ loginError }}</p>
|
||||
<button type="submit">{{ text.loginButton }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main
|
||||
v-else
|
||||
id="main-content"
|
||||
key="workspace"
|
||||
ref="workspaceRef"
|
||||
class="workspace-view"
|
||||
@pointermove="onWorkspacePointerMove"
|
||||
@pointerleave="onWorkspacePointerLeave"
|
||||
>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace</p>
|
||||
<h1>Personal Command Center</h1>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
:class="{ 'is-dark': isDarkTheme }"
|
||||
:aria-label="themeToggleAriaLabel"
|
||||
@click="handleThemeToggle"
|
||||
>
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-sun">浅</span>
|
||||
<span class="theme-toggle-moon">深</span>
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="user-chip">{{ currentUser }}</p>
|
||||
<button type="button" class="ghost-btn" @click="logout">退出</button>
|
||||
</div>
|
||||
@@ -638,33 +960,73 @@ onUnmounted(() => {
|
||||
</aside>
|
||||
|
||||
<section class="panel" aria-live="polite">
|
||||
<div v-if="activeSection === 'overview'" class="panel-body overview-panel">
|
||||
<article class="hero-card">
|
||||
<h2>一眼进入高频任务</h2>
|
||||
<p>从这里切换到文件、游戏或学习模块。相比原先桌面拖拽窗口模式,这里改为稳定导航 + 单主工作区,减少操作成本。</p>
|
||||
<div class="hero-actions">
|
||||
<button type="button" class="primary-btn" @click="setSection('explorer')">打开文件管理</button>
|
||||
<button type="button" class="ghost-btn" @click="setSection('games')">打开游戏中心</button>
|
||||
<div class="panel-body">
|
||||
<header class="panel-intro">
|
||||
<div>
|
||||
<p class="eyebrow">{{ sectionMeta.eyebrow }}</p>
|
||||
<h2>{{ sectionMeta.title }}</h2>
|
||||
<p>{{ sectionMeta.description }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<span class="panel-intro-badge">{{ sectionMeta.badge }}</span>
|
||||
</header>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<p>Folders</p>
|
||||
<strong>{{ explorerFolders.length }}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Items In Current Folder</p>
|
||||
<strong>{{ explorerChildItems.length }}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Games</p>
|
||||
<strong>{{ gameOptions.length }}</strong>
|
||||
</article>
|
||||
<div v-if="activeSection === 'overview'" class="panel-section overview-panel">
|
||||
<div class="overview-hero-grid">
|
||||
<article class="hero-card hero-card-featured">
|
||||
<div class="hero-copy">
|
||||
<span class="hero-kicker">Workspace Flow</span>
|
||||
<h3>一眼进入高频任务</h3>
|
||||
<p>
|
||||
从这里切换到文件、游戏或学习模块。界面改为稳定导航 + 单主工作区,并用更强的层次、光感与状态反馈降低切换成本。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button type="button" class="primary-btn" @click="setSection('explorer')">打开文件管理</button>
|
||||
<button type="button" class="ghost-btn" @click="setSection('games')">打开游戏中心</button>
|
||||
</div>
|
||||
<div class="hero-stat-strip" aria-label="总览状态">
|
||||
<div v-for="signal in overviewSignals" :key="signal.label" class="hero-stat">
|
||||
<small>{{ signal.label }}</small>
|
||||
<strong>{{ signal.value }}</strong>
|
||||
<span>{{ signal.note }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="insight-rail" aria-label="工作区重点">
|
||||
<article class="insight-card ambient">
|
||||
<span class="insight-pill">Live Pulse</span>
|
||||
<strong>主工作区在线</strong>
|
||||
<p>导航、主题切换与局部高光已统一到同一套交互语言。</p>
|
||||
</article>
|
||||
<article class="insight-card">
|
||||
<span class="insight-pill neutral">Focus</span>
|
||||
<strong>{{ explorerCurrentFolder?.name ?? 'Workspace' }}</strong>
|
||||
<p>当前目录可直接新建、重命名与回退,减少跳转路径。</p>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<p>Folders</p>
|
||||
<strong>{{ explorerFolders.length }}</strong>
|
||||
<span>工作区目录总数</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Items In Current Folder</p>
|
||||
<strong>{{ explorerChildItems.length }}</strong>
|
||||
<span>当前层级可操作项</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Games</p>
|
||||
<strong>{{ gameOptions.length }}</strong>
|
||||
<span>支持快速进入沉浸模式</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'explorer'" class="panel-body explorer-panel">
|
||||
<div v-else-if="activeSection === 'explorer'" class="panel-section explorer-panel">
|
||||
<header class="explorer-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@@ -773,7 +1135,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'games'" class="panel-body games-panel">
|
||||
<div v-else-if="activeSection === 'games'" class="panel-section games-panel">
|
||||
<div v-if="!activeGame" class="game-grid">
|
||||
<button
|
||||
v-for="game in gameOptions"
|
||||
@@ -787,6 +1149,7 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<strong>{{ game.label }}</strong>
|
||||
<small>{{ game.subtitle }}</small>
|
||||
<span class="game-meta">Launch Experience</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -816,31 +1179,26 @@ onUnmounted(() => {
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="panel-body school-panel">
|
||||
<div v-else class="panel-section school-panel">
|
||||
<article class="hero-card compact">
|
||||
<h2>学习路径</h2>
|
||||
<p>你可以把课程链接、阶段任务、周计划集中放在这里。</p>
|
||||
<h3>学习路径</h3>
|
||||
<p>把课程链接、阶段任务和周计划集中到一个高可读性的学习面板里。</p>
|
||||
</article>
|
||||
<div class="study-grid">
|
||||
<article class="study-card">
|
||||
<h3>Frontend</h3>
|
||||
<p>Vue + TypeScript 组件拆分、状态设计、可访问性。</p>
|
||||
</article>
|
||||
<article class="study-card">
|
||||
<h3>Graphics</h3>
|
||||
<p>游戏渲染循环、碰撞检测、输入系统与性能优化。</p>
|
||||
</article>
|
||||
<article class="study-card">
|
||||
<h3>Deployment</h3>
|
||||
<p>构建产物、缓存策略、静态资源托管与监控。</p>
|
||||
<article v-for="track in studyTracks" :key="track.title" class="study-card">
|
||||
<span class="study-meta">{{ track.meta }}</span>
|
||||
<h3>{{ track.title }}</h3>
|
||||
<p>{{ track.description }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="status" aria-live="polite">{{ statusMessage }}</p>
|
||||
</main>
|
||||
</main>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user