Files
momentry_studio/src/App.vue

237 lines
13 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import AgentTip from './components/AgentTip.vue'
import BuildVersion from './components/BuildVersion.vue'
import { loadUserBehavior } from '@/stores/agentStore'
import { useI18n } from 'vue-i18n'
import { changeLanguage, getCurrentLanguage } from './locales'
const { t } = useI18n()
const router = useRouter()
const appZoom = ref(100)
const loggedInUser = ref(localStorage.getItem('username') || '')
const currentLanguage = ref(getCurrentLanguage())
const sidebarWidth = ref(240)
const isDragging = ref(false)
const sidebarCollapsed = ref(localStorage.getItem('sidebar_collapsed') === 'true')
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem('sidebar_collapsed', String(sidebarCollapsed.value))
}
function refreshUser() {
const user = localStorage.getItem('username') || ''
loggedInUser.value = user
}
function handleLogout() {
localStorage.removeItem('token')
localStorage.removeItem('username')
localStorage.removeItem('expires_at')
loggedInUser.value = ''
router.push('/login')
}
function onChangeLanguage() {
changeLanguage(currentLanguage.value)
}
let systemMedia: MediaQueryList | null = null
function handleKeydown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey)) {
if (e.key === '=' || e.key === '+') {
e.preventDefault()
appZoom.value = Math.min(appZoom.value + 10, 300)
document.documentElement.style.zoom = appZoom.value + '%'
} else if (e.key === '-') {
e.preventDefault()
appZoom.value = Math.max(appZoom.value - 10, 50)
document.documentElement.style.zoom = appZoom.value + '%'
} else if (e.key === '0') {
e.preventDefault()
appZoom.value = 100
document.documentElement.style.zoom = '100%'
}
}
}
function startDrag(e: MouseEvent) {
isDragging.value = true
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}
function handleDrag(e: MouseEvent) {
if (!isDragging.value) return
const newWidth = e.clientX - 28
sidebarWidth.value = Math.max(180, Math.min(400, newWidth))
}
function stopDrag() {
isDragging.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', stopDrag)
refreshUser()
loadUserBehavior()
window.addEventListener('ms-login', () => {
refreshUser()
loadUserBehavior()
})
})
watch(() => router.currentRoute.value.path, () => {
refreshUser()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
})
</script>
<template>
<BuildVersion />
<div id="app" class="ms-app" :data-current-login="'guest'">
<aside class="ms-side" :class="{ collapsed: sidebarCollapsed }" :style="{ width: sidebarCollapsed ? '62px' : sidebarWidth + 'px', minWidth: sidebarCollapsed ? '62px' : sidebarWidth + 'px' }">
<div class="gs-sidebar-toggle" @click="toggleSidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline :points="sidebarCollapsed ? '9 18 15 12 9 6' : '15 18 9 12 15 6'"/>
</svg>
</div>
<div class="gs-logo" :class="{ 'gs-logo-collapsed': sidebarCollapsed }">{{ sidebarCollapsed ? 'WS' : 'Workspace' }}</div>
<nav class="gs-nav">
<router-link to="/search" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<span class="gs-nav-label">{{ t('nav.search') }}</span>
</router-link>
<router-link to="/library" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
<span class="gs-nav-label">{{ t('nav.library') }}</span>
</router-link>
<router-link to="/face" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="12" cy="10" r="4"/>
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
</svg>
<span class="gs-nav-label">Face</span>
</router-link>
<router-link to="/admin" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span class="gs-nav-label">{{ t('nav.admin') }}</span>
</router-link>
<router-link to="/client" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="gs-nav-label">{{ t('nav.client') }}</span>
</router-link>
</nav>
<div class="gs-divider gs-hidden"></div>
<div class="gs-footer gs-hidden">
<div class="gs-lang-switcher">
<select v-model="currentLanguage" @change="onChangeLanguage" class="gs-lang-select">
<option value="en">English</option>
<option value="zh-TW">繁體中文</option>
<option value="auto">Auto Detect</option>
</select>
</div>
<div class="gs-account-row">
<template v-if="loggedInUser">
<span class="gs-account-name">{{ loggedInUser }}</span>
<button class="gs-logout-btn" @click="handleLogout" title="Logout"> {{ t('common.logout') }}</button>
</template>
<template v-else>
<router-link to="/login" class="gs-login-link">{{ t('common.login') }}</router-link>
</template>
</div>
</div>
<div class="gs-footer-compact">
<template v-if="loggedInUser">
<button class="gs-logout-btn-compact" @click="handleLogout" title="Logout"></button>
</template>
<template v-else>
<router-link to="/login" class="gs-login-link-compact" title="Login"></router-link>
</template>
</div>
</aside>
<div class="ms-divider" :class="{ 'ms-divider-hidden': sidebarCollapsed }" @mousedown="startDrag"></div>
<main class="ms-main">
<router-view />
</main>
<AgentTip v-if="loggedInUser" />
</div>
</template>
<style>
@import './assets/wordpress-exact.css';
</style>
<style scoped>
* { margin: 0; padding: 0; box-sizing: border-box; }
#app, .ms-app { background: var(--background-color) !important; display: flex; max-width: 1400px; margin: 0 auto; padding: 28px; gap: 0; min-height: 100vh; box-sizing: border-box; align-items: stretch; }
.ms-side { width: 240px; min-width: 240px; flex-shrink: 0; background: var(--card-background) !important; border-radius: 18px; padding: 18px 14px; box-shadow: var(--shadow); display: flex; flex-direction: column; position: relative; overflow: visible; }
.ms-side.collapsed { align-items: center; padding: 18px 8px; }
.ms-side.collapsed .gs-nav { align-items: center; width: 100%; }
.ms-side.collapsed .gs-nav-item { justify-content: center; padding: 0; width: 44px; }
.ms-side.collapsed .gs-nav-label { display: none; }
.ms-side.collapsed .gs-nav-icon { width: 22px; height: 22px; }
.gs-footer-compact { display: none; }
.ms-side.collapsed .gs-footer-compact { display: flex; flex-direction: column; align-items: center; margin-top: auto; padding: 10px 0; }
.gs-logout-btn-compact, .gs-login-link-compact { background: none; border: 1px solid var(--border-color); cursor: pointer; font-size: 16px; color: var(--text-secondary); width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; text-decoration: none; transition: all .15s; }
.gs-logout-btn-compact:hover { color: var(--danger-color); border-color: var(--danger-color); background: var(--danger-background); }
.gs-login-link-compact:hover { color: var(--text-primary); border-color: var(--text-primary); }
.gs-hidden { display: none !important; }
.gs-sidebar-toggle { display: flex; align-items: center; justify-content: flex-end; padding: 0 4px 12px 4px; cursor: pointer; color: var(--text-secondary); flex-shrink: 0; }
.gs-sidebar-toggle svg { transition: transform 0.2s; }
.ms-side.collapsed .gs-sidebar-toggle { justify-content: center; width: 100%; }
.ms-side.collapsed .gs-sidebar-toggle svg { transform: rotate(180deg); }
.gs-logo-collapsed { text-align: center; padding: 8px 4px 16px 4px; font-size: 14px; }
.ms-divider { width: 28px; flex-shrink: 0; cursor: col-resize; display: flex; align-items: center; justify-content: center; }
.ms-divider-hidden { cursor: default; pointer-events: none; opacity: 0; }
.ms-divider::after { content: ''; width: 4px; height: 40px; background: var(--border-color); border-radius: 2px; transition: background 0.2s, height 0.2s; }
.ms-divider:hover::after { background: var(--primary-color); height: 60px; }
.gs-logo { font-size: 15px; font-weight: 600; color: var(--text-primary); padding: 8px 12px 16px 12px; flex-shrink: 0; }
.gs-nav { display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; }
.gs-nav-item { display: flex; align-items: center; gap: 12px; height: 48px; padding: 0 12px; border-radius: 10px; text-decoration: none; color: var(--text-primary) !important; font-weight: 500; font-size: 16px; line-height: 1; background: transparent !important; box-sizing: border-box; transition: all 0.15s; }
.gs-nav-item:hover { background: var(--hover-background) !important; color: var(--primary-color) !important; }
.gs-nav-item.active { background: var(--hover-background) !important; color: var(--primary-color) !important; font-weight: 600; }
.gs-nav-icon { width: 22px; height: 22px; flex-shrink: 0; }
.gs-divider { height: 1px; background: var(--border-light); margin: 14px 8px; flex-shrink: 0; }
.gs-footer { margin-top: auto; padding: 10px 8px; border-radius: 12px; background: var(--hover-background); flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; }
.gs-lang-switcher { width: 100%; }
.gs-lang-select { width: 100%; padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--card-background); font-size: 12px; font-family: inherit; color: var(--text-secondary); cursor: pointer; outline: none; }
.gs-lang-select:focus { border-color: var(--text-primary); }
.gs-account-row { display: flex; align-items: center; justify-content: center; gap: 8px; }
.gs-account-name { font-size: 12px; color: var(--text-secondary); }
.gs-logout-btn { background: none; border: 1px solid var(--border-color); cursor: pointer; font-size: 13px; color: var(--text-secondary); padding: 3px 8px; border-radius: 6px; transition: all .15s; margin-left: 6px; }
.gs-logout-btn:hover { color: var(--danger-color); border-color: var(--danger-color); background: var(--danger-background); }
.gs-login-link { font-size: 12px; color: var(--text-secondary); text-decoration: none; }
.gs-login-link:hover { color: var(--text-primary); }
.ms-main { flex: 1; min-width: 0; background: var(--card-background) !important; border-radius: 18px; padding: 36px 42px; overflow-y: auto; overflow-x: hidden; }
.ms-content { max-width: 1200px; }
</style>