238 lines
13 KiB
Vue
238 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" :class="{ 'gs-hidden': sidebarCollapsed }"></div>
|
|
<div class="gs-footer" :class="{ 'gs-hidden': sidebarCollapsed }">
|
|
<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';
|
|
html, body { height: 100%; margin: 0; }
|
|
</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; height: 100vh; box-sizing: border-box; }
|
|
.ms-side { width: 240px; min-width: 240px; background: var(--card-background) !important; border-radius: 18px; padding: 18px 14px; box-shadow: var(--shadow); display: flex; flex-direction: column; position: relative; overflow: visible; height: 100%; }
|
|
.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; height: 100%; }
|
|
.ms-content { max-width: 1200px; }
|
|
</style>
|