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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, onUnmounted, nextTick, reactive } from 'vue'
|
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 { Icon } from '@iconify/vue'
|
||||||
import { useSEO } from '@/shared/composables/useSEO'
|
import { useSEO } from '@/shared/composables/useSEO'
|
||||||
import { t } from '@/shared/i18n'
|
import { t } from '@/shared/i18n'
|
||||||
import { useAuthStore } from '@/shared/stores/auth'
|
import { useAuthStore } from '@/shared/stores/auth'
|
||||||
import { signupApi } from '@/shared/api/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 { compressImageFile } from '@/shared/utils/imageResize'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@ -190,6 +191,51 @@ const switchToLogin = () => {
|
|||||||
// 登录状态
|
// 登录状态
|
||||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
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({
|
useSEO({
|
||||||
title: t('Remove Background - Free AI Background Removal Tool'),
|
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.'),
|
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 })
|
watch([uploadedImageUrl, resultImageUrl], adjustContainerSize, { immediate: true })
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
adjustContainerSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSplitLineMouseDown = (e: MouseEvent) => {
|
const handleSplitLineMouseDown = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
isDraggingSplitLine.value = true
|
isDraggingSplitLine.value = true
|
||||||
@ -860,7 +902,7 @@ const removeHistoryItem = (index: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleWindowResize)
|
||||||
window.addEventListener('paste', handlePaste)
|
window.addEventListener('paste', handlePaste)
|
||||||
|
|
||||||
// 初始化认证状态
|
// 初始化认证状态
|
||||||
@ -878,10 +920,13 @@ onMounted(async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get server config:', error)
|
console.error('Failed to get server config:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测移动端
|
||||||
|
checkIsMobile()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleWindowResize)
|
||||||
window.removeEventListener('paste', handlePaste)
|
window.removeEventListener('paste', handlePaste)
|
||||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||||
@ -895,6 +940,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="home-page" @dragenter="handleDragEnter" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop">
|
<div class="home-page" @dragenter="handleDragEnter" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop">
|
||||||
|
<!-- 未登录状态:显示营销页面布局 -->
|
||||||
|
<template v-if="!isLoggedIn">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="marketing-header">
|
<header class="marketing-header">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
@ -906,15 +953,8 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<n-space :size="12">
|
<n-space :size="12">
|
||||||
<!-- 未登录状态:显示登录/注册按钮 -->
|
|
||||||
<template v-if="!isLoggedIn">
|
|
||||||
<n-button quaternary @click="handleSignup">注册</n-button>
|
<n-button quaternary @click="handleSignup">注册</n-button>
|
||||||
<n-button type="primary" @click="handleLogin" class="login-btn">登录</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>
|
</div>
|
||||||
@ -1121,6 +1161,200 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<n-modal
|
||||||
@ -1310,6 +1544,69 @@ onUnmounted(() => {
|
|||||||
overflow: visible; /* 改为 visible 允许内容溢出 */
|
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 */
|
/* Header */
|
||||||
.marketing-header {
|
.marketing-header {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -1400,6 +1697,13 @@ onUnmounted(() => {
|
|||||||
height: 0; /* 强制 flex 子元素计算高度 */
|
height: 0; /* 强制 flex 子元素计算高度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 登录后的 main-content 样式覆盖 */
|
||||||
|
.content-wrapper .main-content {
|
||||||
|
height: auto; /* 登录后恢复自动高度 */
|
||||||
|
min-height: auto;
|
||||||
|
overflow: visible; /* 登录后允许内容显示 */
|
||||||
|
}
|
||||||
|
|
||||||
.global-drag-overlay {
|
.global-drag-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user