Add sidebar layout for authenticated homepage users
This commit is contained in:
parent
4370f1a8d7
commit
0a23ee2587
@ -172,7 +172,7 @@ const handleMenuSelect = (key: string) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onUnmounted, nextTick, reactive } from 'vue'
|
||||
import { NButton, NSpace, useMessage, NModal, NForm, NFormItem, NInput, NText } from 'naive-ui'
|
||||
import { NButton, NSpace, useMessage, NModal, NForm, NFormItem, NInput, NText, NLayout, NLayoutSider, NLayoutHeader, NLayoutContent } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useSEO } from '@/shared/composables/useSEO'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { useAuthStore } from '@/shared/stores/auth'
|
||||
import { signupApi } from '@/shared/api/auth'
|
||||
import UserMenu from '@/shared/components/UserMenu.vue'
|
||||
import AppHeader from '@/app/layouts/AppHeader.vue'
|
||||
import AppSidebar from '@/app/layouts/AppSidebar.vue'
|
||||
import { compressImageFile } from '@/shared/utils/imageResize'
|
||||
import axios from 'axios'
|
||||
|
||||
@ -190,6 +191,51 @@ const switchToLogin = () => {
|
||||
// 登录状态
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
|
||||
// Sidebar 折叠状态
|
||||
const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed'
|
||||
const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true')
|
||||
const isMobile = ref(false)
|
||||
|
||||
// 检测屏幕尺寸
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
if (isMobile.value) {
|
||||
collapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
// 侧边栏折叠事件
|
||||
const onSidebarCollapse = () => {
|
||||
collapsed.value = true
|
||||
}
|
||||
|
||||
// 侧边栏展开事件
|
||||
const onSidebarExpand = () => {
|
||||
collapsed.value = false
|
||||
}
|
||||
|
||||
// 菜单选择事件 - 移动端自动关闭
|
||||
const onMenuSelect = () => {
|
||||
if (isMobile.value) {
|
||||
collapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleWindowResize = () => {
|
||||
checkIsMobile()
|
||||
adjustContainerSize()
|
||||
}
|
||||
|
||||
watch(collapsed, (val) => {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSE_KEY, String(val))
|
||||
})
|
||||
|
||||
useSEO({
|
||||
title: t('Remove Background - Free AI Background Removal Tool'),
|
||||
description: t('Remove background from images using AI technology. Free online tool to remove image backgrounds instantly. Supports JPG, PNG, WebP formats.'),
|
||||
@ -667,10 +713,6 @@ const adjustContainerSize = async () => {
|
||||
|
||||
watch([uploadedImageUrl, resultImageUrl], adjustContainerSize, { immediate: true })
|
||||
|
||||
const handleResize = () => {
|
||||
adjustContainerSize()
|
||||
}
|
||||
|
||||
const handleSplitLineMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
isDraggingSplitLine.value = true
|
||||
@ -860,7 +902,7 @@ const removeHistoryItem = (index: number) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
window.addEventListener('paste', handlePaste)
|
||||
|
||||
// 初始化认证状态
|
||||
@ -878,10 +920,13 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to get server config:', error)
|
||||
}
|
||||
|
||||
// 检测移动端
|
||||
checkIsMobile()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
window.removeEventListener('paste', handlePaste)
|
||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||
@ -895,33 +940,28 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="home-page" @dragenter="handleDragEnter" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop">
|
||||
<!-- Header -->
|
||||
<header class="marketing-header">
|
||||
<div class="header-container">
|
||||
<div class="header-left">
|
||||
<router-link to="/" class="logo-link">
|
||||
<img v-if="logoUrl" :src="logoUrl" :alt="appName" class="logo-img" />
|
||||
<span class="logo-text">{{ appName }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-space :size="12">
|
||||
<!-- 未登录状态:显示登录/注册按钮 -->
|
||||
<template v-if="!isLoggedIn">
|
||||
<!-- 未登录状态:显示营销页面布局 -->
|
||||
<template v-if="!isLoggedIn">
|
||||
<!-- Header -->
|
||||
<header class="marketing-header">
|
||||
<div class="header-container">
|
||||
<div class="header-left">
|
||||
<router-link to="/" class="logo-link">
|
||||
<img v-if="logoUrl" :src="logoUrl" :alt="appName" class="logo-img" />
|
||||
<span class="logo-text">{{ appName }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-space :size="12">
|
||||
<n-button quaternary @click="handleSignup">注册</n-button>
|
||||
<n-button type="primary" @click="handleLogin" class="login-btn">登录</n-button>
|
||||
</template>
|
||||
<!-- 已登录状态:显示用户菜单 -->
|
||||
<template v-else>
|
||||
<UserMenu />
|
||||
</template>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div v-if="isDragging" class="global-drag-overlay">
|
||||
<div class="overlay-content">
|
||||
<p>拖放图片到任意位置去除背景</p>
|
||||
@ -1073,10 +1113,10 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="marketing-footer">
|
||||
<!-- Footer -->
|
||||
<footer class="marketing-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-grid">
|
||||
<div class="footer-column">
|
||||
@ -1120,7 +1160,201 @@ onUnmounted(() => {
|
||||
<p class="copyright">© {{ currentYear }} {{ appName }}. 版权所有。</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<!-- 已登录状态:显示应用布局(带 sidebar 和 header) -->
|
||||
<template v-else>
|
||||
<n-layout has-sider class="app-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
v-model:collapsed="collapsed"
|
||||
:show-trigger="!isMobile"
|
||||
:responsive="true"
|
||||
:breakpoint="768"
|
||||
@collapse="onSidebarCollapse"
|
||||
@expand="onSidebarExpand"
|
||||
>
|
||||
<AppSidebar :collapsed="collapsed" @menu-select="onMenuSelect" />
|
||||
</n-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<n-layout>
|
||||
<!-- 顶部导航 -->
|
||||
<n-layout-header bordered>
|
||||
<AppHeader @toggle-sidebar="toggleSidebar" />
|
||||
</n-layout-header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<n-layout-content>
|
||||
<div class="content-wrapper">
|
||||
<div v-if="isDragging" class="global-drag-overlay">
|
||||
<div class="overlay-content">
|
||||
<p>拖放图片到任意位置去除背景</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="fileInputRef" type="file" accept="image/*" style="display: none" @change="handleFileSelect" />
|
||||
|
||||
<div class="tool-page">
|
||||
<div class="page-header">
|
||||
<h2>去除背景</h2>
|
||||
<div v-if="uploadedImage" class="toolbar-actions">
|
||||
<button v-if="resultImage" class="toolbar-btn" @click="handleDownload" title="下载">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" @click="resetUpload" title="更换图片">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="tool-container">
|
||||
<div class="upload-section">
|
||||
<div v-if="!uploadedImage" class="upload-area" :class="{ dragging: isDragging }">
|
||||
<div class="upload-content">
|
||||
<button type="button" class="upload-btn" @click="triggerFileInput" :disabled="processing">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span>上传图片</span>
|
||||
</button>
|
||||
|
||||
<div class="divider">
|
||||
<span>或</span>
|
||||
</div>
|
||||
|
||||
<div class="url-input-wrapper" @click.stop>
|
||||
<input
|
||||
ref="urlInputRef"
|
||||
v-model="imageUrl"
|
||||
type="text"
|
||||
class="url-input"
|
||||
placeholder="粘贴图片URL"
|
||||
@keyup.enter="handleUrlSubmit"
|
||||
@paste="handleUrlPaste"
|
||||
:disabled="processing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="upload-hint">拖放图片到任意位置,或直接粘贴图片</p>
|
||||
<p class="upload-format-hint">支持 JPG、PNG、WebP 格式</p>
|
||||
</div>
|
||||
<div v-if="processing" class="upload-processing-overlay">
|
||||
<div class="spinner"></div>
|
||||
<p>加载图片URL中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview-section">
|
||||
<div class="comparison-view" v-if="resultImage">
|
||||
<div class="comparison-container" ref="comparisonContainerRef">
|
||||
<div class="comparison-image original-image" :style="{ clipPath: `inset(0 ${100 - splitPosition}% 0 0)` }">
|
||||
<img ref="originalImageRef" :src="uploadedImageUrl" alt="原图" @load="adjustContainerSize" />
|
||||
</div>
|
||||
<div class="comparison-image result-image" :style="{ clipPath: `inset(0 0 0 ${splitPosition}%)` }">
|
||||
<img ref="resultImageRef" :src="resultImageUrl" alt="结果" @load="adjustContainerSize" />
|
||||
</div>
|
||||
<div class="split-line" :class="{ dragging: isDraggingSplitLine }" :style="{ left: `${splitPosition}%` }" @mousedown.prevent.stop="handleSplitLineMouseDown">
|
||||
<div class="split-line-handle">
|
||||
<i class="fa fa-arrows-h"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="single-image-view">
|
||||
<div class="image-wrapper" ref="singleImageWrapperRef">
|
||||
<div class="single-image-container">
|
||||
<img ref="singleImageRef" :src="uploadedImageUrl" alt="原图" @load="adjustContainerSize" />
|
||||
</div>
|
||||
<div v-if="processing" class="processing-overlay">
|
||||
<div class="spinner"></div>
|
||||
<p>处理中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="historyList.length > 0 || uploadedImage" class="history-bar">
|
||||
<div class="history-scroll-container">
|
||||
<button
|
||||
type="button"
|
||||
class="history-item add-button"
|
||||
@click.stop="triggerFileInput"
|
||||
title="添加新图片"
|
||||
>
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in historyList"
|
||||
:key="item.id"
|
||||
class="history-item"
|
||||
:class="{ 'active': currentHistoryIndex === index }"
|
||||
@click="selectHistoryItem(index)"
|
||||
>
|
||||
<div class="history-thumbnail">
|
||||
<img :src="getHistoryThumbnailUrl(item)" alt="History" />
|
||||
<button
|
||||
type="button"
|
||||
class="history-delete-btn"
|
||||
@click.stop="removeHistoryItem(index)"
|
||||
title="删除"
|
||||
:aria-label="'删除'"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="uploadedImage && resultImage && currentHistoryIndex === -1"
|
||||
class="history-item active"
|
||||
>
|
||||
<div class="history-thumbnail">
|
||||
<img :src="resultImageUrl" alt="Current" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例图片区块 -->
|
||||
<div v-if="!uploadedImage" class="sample-images-section">
|
||||
<p class="sample-images-title">{{ t('Click image to try') }}</p>
|
||||
<div class="sample-images-container">
|
||||
<div
|
||||
v-for="sample in sampleImages"
|
||||
:key="sample.id"
|
||||
class="sample-image-item"
|
||||
@click="handleSampleImageClick(sample.url)"
|
||||
:class="{ loading: processing }"
|
||||
>
|
||||
<div class="sample-image-wrapper">
|
||||
<img :src="sample.url" :alt="sample.name" loading="lazy" />
|
||||
<div class="sample-image-overlay">
|
||||
<Icon icon="tabler:wand" class="overlay-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
|
||||
<!-- 移动端遮罩层 -->
|
||||
<div
|
||||
v-if="isMobile && !collapsed"
|
||||
class="mobile-overlay"
|
||||
@click="toggleSidebar"
|
||||
></div>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<n-modal
|
||||
@ -1310,6 +1544,69 @@ onUnmounted(() => {
|
||||
overflow: visible; /* 改为 visible 允许内容溢出 */
|
||||
}
|
||||
|
||||
/* 应用布局样式(登录后) */
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 使用Naive UI内置的sticky功能 */
|
||||
:deep(.n-layout-header) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 移动端遮罩层 */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
@media (max-width: 767px) {
|
||||
/* 移动端时完全隐藏侧边栏 */
|
||||
:deep(.n-layout-sider) {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
width: 280px !important;
|
||||
max-width: 80vw;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 移动端侧边栏打开时的样式 */
|
||||
:deep(.n-layout-sider:not(.n-layout-sider--collapsed)) {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
/* 移动端主内容区域占满全宽 */
|
||||
:deep(.n-layout) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端保持原有样式 */
|
||||
@media (min-width: 768px) {
|
||||
:deep(.n-layout-sider) {
|
||||
position: relative !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.marketing-header {
|
||||
flex-shrink: 0;
|
||||
@ -1400,6 +1697,13 @@ onUnmounted(() => {
|
||||
height: 0; /* 强制 flex 子元素计算高度 */
|
||||
}
|
||||
|
||||
/* 登录后的 main-content 样式覆盖 */
|
||||
.content-wrapper .main-content {
|
||||
height: auto; /* 登录后恢复自动高度 */
|
||||
min-height: auto;
|
||||
overflow: visible; /* 登录后允许内容显示 */
|
||||
}
|
||||
|
||||
.global-drag-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user