library-picturebook-activity/java-frontend/src/layouts/PublicLayout.vue
En 48fc71b41d fix: 前后端接口对齐修复
- 修复 sys-config 接口参数对齐(configKey/configValue)
- 添加 dict 字典项管理 API
- 修复 logs 接口参数格式(批量删除/清理日志)
- 添加 request.ts postForm/putForm 方法支持

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 18:53:24 +08:00

273 lines
5.9 KiB
Vue

<template>
<div class="public-layout">
<!-- 顶部导航 -->
<header class="public-header">
<div class="header-inner">
<div class="header-brand" @click="goHome">
<img src="@/assets/images/logo-icon.png" alt="乐绘世界" class="header-logo" />
<span class="header-title">乐绘世界</span>
</div>
<div class="header-actions">
<template v-if="isLoggedIn">
<div class="user-menu" @click="goMine">
<a-avatar :size="28" :src="userAvatar">
{{ user?.nickname?.charAt(0) }}
</a-avatar>
<span class="user-name hidden-mobile">{{ user?.nickname }}</span>
</div>
</template>
<template v-else>
<a-button type="primary" size="small" shape="round" @click="goLogin">
登录
</a-button>
</template>
</div>
</div>
</header>
<!-- 主内容 -->
<main class="public-main">
<router-view />
</main>
<!-- 移动端底部导航 -->
<nav class="public-tabbar">
<div
class="tabbar-item"
:class="{ active: currentTab === 'home' }"
@click="goHome"
>
<home-outlined />
<span>发现</span>
</div>
<div
class="tabbar-item"
:class="{ active: currentTab === 'create' }"
@click="goCreate"
>
<plus-circle-outlined />
<span>创作</span>
</div>
<div
class="tabbar-item"
:class="{ active: currentTab === 'activity' }"
@click="goActivity"
>
<trophy-outlined />
<span>活动</span>
</div>
<div
class="tabbar-item"
:class="{ active: currentTab === 'works' }"
@click="goWorks"
>
<appstore-outlined />
<span>作品库</span>
</div>
<div
class="tabbar-item"
:class="{ active: currentTab === 'mine' }"
@click="goMine"
>
<user-outlined />
<span>我的</span>
</div>
</nav>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { useRouter, useRoute } from "vue-router"
import { HomeOutlined, UserOutlined, PlusCircleOutlined, AppstoreOutlined, TrophyOutlined } from "@ant-design/icons-vue"
const router = useRouter()
const route = useRoute()
const isLoggedIn = computed(() => !!localStorage.getItem("public_token"))
const user = computed(() => {
const data = localStorage.getItem("public_user")
return data ? JSON.parse(data) : null
})
const userAvatar = computed(() => user.value?.avatar || undefined)
const currentTab = computed(() => {
const path = route.path
if (path.includes("/mine")) return "mine"
if (path.includes("/create")) return "create"
if (path.startsWith("/p/works")) return "works"
if (path.includes("/activities")) return "activity"
return "home"
})
const goHome = () => router.push("/p/gallery")
const goActivity = () => router.push("/p/activities")
const goCreate = () => {
if (!isLoggedIn.value) { router.push("/p/login"); return }
router.push("/p/create")
}
const goWorks = () => {
if (!isLoggedIn.value) { router.push("/p/login"); return }
router.push("/p/works")
}
const goMine = () => {
if (!isLoggedIn.value) { router.push("/p/login"); return }
router.push("/p/mine")
}
const goLogin = () => router.push("/p/login")
</script>
<style scoped lang="scss">
$primary: #6366f1;
.public-layout {
min-height: 100vh;
background: #f8f7fc;
display: flex;
flex-direction: column;
}
// ========== 顶部导航 ==========
.public-header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(99, 102, 241, 0.06);
box-shadow: 0 1px 8px rgba(99, 102, 241, 0.04);
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-brand {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
.header-logo {
width: 32px;
height: 32px;
object-fit: contain;
}
.header-title {
font-size: 16px;
font-weight: 800;
background: linear-gradient(135deg, $primary 0%, #ec4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.user-menu {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px 4px 4px;
border-radius: 20px;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba($primary, 0.06);
}
.user-name {
font-size: 13px;
font-weight: 600;
color: #374151;
}
}
// ========== 主内容 ==========
.public-main {
flex: 1;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;
padding-bottom: 80px; // 为底部 tabbar 留空间
}
// ========== 底部导航(移动端) ==========
.public-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(16px);
border-top: 1px solid rgba(99, 102, 241, 0.06);
display: flex;
padding: 6px 0;
padding-bottom: calc(6px + env(safe-area-inset-bottom));
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 4px 0;
font-size: 20px;
color: #9ca3af;
cursor: pointer;
transition: color 0.2s;
span {
font-size: 11px;
font-weight: 500;
}
&.active {
color: $primary;
}
}
}
// ========== 响应式 ==========
@media (min-width: 768px) {
.public-tabbar {
display: none;
}
.public-main {
padding-bottom: 40px;
}
}
@media (max-width: 767px) {
.hidden-mobile {
display: none;
}
.public-main {
padding: 16px;
padding-bottom: 80px;
}
.header-inner {
padding: 0 16px;
height: 50px;
}
}
</style>