Files
douyin_cookie_yunsya/frontend/src/components/LayoutShell.vue
T
travel 9e0644095f 重构为 HTTP SSO 扫码方案并引入 Vue3 前端
移除 Playwright 浏览器自动化,改用 passport/SSO HTTP 接口获取二维码与轮询登录;后端模块化拆分,前端替换为 Vue3 SPA。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 10:47:55 +08:00

350 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="layout-cloud" :class="{ 'is-nav-open': sidebarOpen }">
<div class="nav-backdrop" aria-hidden="true" @click="sidebarOpen = false" />
<aside class="sidebar" role="navigation" :aria-hidden="desktopNav ? undefined : !sidebarOpen">
<div class="sidebar-brand">
<button type="button" class="sidebar-close" aria-label="关闭菜单" @click="sidebarOpen = false">
×
</button>
<div class="brand-title">抖音 Cookie 提取</div>
<div class="brand-sub">扫码登录 · JSON 导出</div>
</div>
<nav class="sidebar-nav">
<router-link
v-for="item in navItems"
:key="item.name"
:to="{ name: item.name }"
class="nav-item"
exact-active-class="nav-item-active"
@click="onNavClick"
>
{{ item.label }}
</router-link>
</nav>
<div class="sidebar-footer">
<button type="button" class="btn btn-soft full-width" @click="refreshAll">刷新状态</button>
</div>
</aside>
<div class="main-wrap">
<header class="mobile-topbar">
<button
type="button"
class="nav-toggle"
aria-label="打开菜单"
:aria-expanded="sidebarOpen"
@click="sidebarOpen = true"
>
<span class="nav-toggle-bar"></span>
<span class="nav-toggle-bar"></span>
<span class="nav-toggle-bar"></span>
</button>
<span class="mobile-title">Cookie 提取</span>
</header>
<main class="main-inner">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const sidebarOpen = ref(false)
const desktopNav = ref(true)
const navItems = [
{ name: 'home', label: '扫码提取' },
{ name: 'debug', label: 'SSO 调试' },
]
let mq
function syncMq() {
desktopNav.value = typeof window !== 'undefined' && window.matchMedia('(min-width: 900px)').matches
if (desktopNav.value) sidebarOpen.value = false
}
function refreshAll() {
window.dispatchEvent(new CustomEvent('douyin-refresh-all'))
}
function onNavClick() {
if (!desktopNav.value) sidebarOpen.value = false
}
function onKeydown(e) {
if (e.key === 'Escape' && sidebarOpen.value) sidebarOpen.value = false
}
watch(
() => route.fullPath,
() => {
if (!desktopNav.value) sidebarOpen.value = false
},
)
watch(sidebarOpen, (open) => {
if (typeof document === 'undefined') return
document.body.style.overflow = open && !desktopNav.value ? 'hidden' : ''
})
onMounted(() => {
syncMq()
mq = window.matchMedia('(min-width: 900px)')
mq.addEventListener('change', syncMq)
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
if (mq) mq.removeEventListener('change', syncMq)
window.removeEventListener('keydown', onKeydown)
document.body.style.overflow = ''
})
</script>
<style scoped>
.layout-cloud {
display: flex;
min-height: 100vh;
min-height: 100dvh;
height: 100vh;
height: 100dvh;
max-height: 100vh;
max-height: 100dvh;
overflow: hidden;
}
.nav-backdrop {
display: none;
}
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
align-self: stretch;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-light);
display: flex;
flex-direction: column;
box-shadow: 1px 0 0 rgba(0, 0, 0, 0.04);
min-height: 0;
}
.sidebar-brand {
position: relative;
flex-shrink: 0;
padding: 1rem 0.75rem 0.85rem;
border-bottom: 1px solid var(--border-light);
}
.sidebar-close {
display: none;
position: absolute;
top: 0.75rem;
right: 0.65rem;
width: var(--tap-min, 44px);
height: var(--tap-min, 44px);
margin: 0;
padding: 0;
border: none;
background: #f5f7fa;
border-radius: var(--radius-sm);
font-size: 1.5rem;
line-height: 1;
color: var(--muted);
cursor: pointer;
}
.brand-title {
font-size: 1.05rem;
font-weight: 700;
color: var(--text-strong);
letter-spacing: -0.02em;
padding-right: 2.5rem;
}
.brand-sub {
font-size: 0.75rem;
color: var(--muted);
margin-top: 0.25rem;
}
.sidebar-nav {
flex: 1;
min-height: 0;
padding: 0.55rem 0.5rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
min-height: var(--tap-min, 44px);
padding: 0.45rem 0.6rem;
border-radius: var(--radius-sm);
color: var(--text-strong);
font-size: 0.875rem;
border: 1px solid #e8e8e8;
background: #ffffff;
text-decoration: none;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.nav-item:hover {
background: #f7f8fa;
border-color: #dcdfe6;
}
.nav-item-active {
background: #eef5fe !important;
color: #3b82f6 !important;
font-weight: 600;
border-color: #bfdbfe !important;
}
.sidebar-footer {
flex-shrink: 0;
padding: 0.55rem 0.5rem;
border-top: 1px solid var(--border-light);
padding-bottom: max(0.55rem, env(safe-area-inset-bottom));
}
.full-width {
width: 100%;
}
.sidebar-footer .btn-soft {
background: #f5f5f5;
border-color: #e8e8e8;
color: #303133;
}
.mobile-topbar {
display: none;
}
.main-wrap {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-inner {
flex: 1;
width: 100%;
min-width: 0;
min-height: 0;
box-sizing: border-box;
padding-top: var(--main-pad-y);
padding-bottom: max(var(--main-pad-y), env(safe-area-inset-bottom));
padding-left: var(--main-pad-start);
padding-right: var(--main-pad-end);
overflow: auto;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.12s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@media (max-width: 899px) {
.sidebar-close {
display: block;
}
.nav-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 250;
background: rgba(0, 0, 0, 0.45);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s;
}
.layout-cloud.is-nav-open .nav-backdrop {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(260px, 86vw);
z-index: 300;
transform: translateX(-100%);
transition: transform 0.22s ease;
}
.layout-cloud.is-nav-open .sidebar {
transform: translateX(0);
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.12);
}
.mobile-topbar {
display: flex;
align-items: center;
gap: 0.75rem;
flex-shrink: 0;
padding: 0.5rem var(--main-pad-end, var(--main-pad-x));
padding-top: max(0.5rem, env(safe-area-inset-top));
background: var(--surface);
border-bottom: 1px solid var(--border-light);
position: sticky;
top: 0;
z-index: 100;
}
.nav-toggle {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
width: var(--tap-min, 44px);
height: var(--tap-min, 44px);
padding: 0 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
cursor: pointer;
}
.nav-toggle-bar {
display: block;
height: 2px;
background: var(--text-strong);
border-radius: 1px;
}
.mobile-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-strong);
}
}
</style>