- 修复 sys-config 接口参数对齐(configKey/configValue) - 添加 dict 字典项管理 API - 修复 logs 接口参数格式(批量删除/清理日志) - 添加 request.ts postForm/putForm 方法支持 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
273 lines
5.9 KiB
Vue
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>
|