feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<!-- 弹框头部 -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900">聊天历史记录</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">查看和搜索您的所有对话记录</p>
|
||||
</div>
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-full hover:bg-gray-100"
|
||||
>
|
||||
<i data-lucide="x" class="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选区域 -->
|
||||
<div class="p-6 border-b border-gray-200 bg-gray-50">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- 关键词搜索 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">搜索关键词</label>
|
||||
<div class="relative">
|
||||
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
placeholder="输入关键词..."
|
||||
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期范围 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">开始日期</label>
|
||||
<input
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
@change="handleDateFilter"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">结束日期</label>
|
||||
<input
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
@change="handleDateFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-sm text-gray-600">
|
||||
共找到 {{ totalCount }} 条记录
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="clearFilters"
|
||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||
>
|
||||
清除筛选
|
||||
</button>
|
||||
<button
|
||||
@click="refreshData"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? '刷新中...' : '刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p class="mt-2 text-gray-600">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="messages.length === 0" class="text-center py-8 text-gray-500">
|
||||
<i data-lucide="message-circle" class="w-12 h-12 mx-auto mb-4 text-gray-300"></i>
|
||||
<p>暂无聊天记录</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
||||
:class="message.sender === 'user' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'"
|
||||
>
|
||||
{{ message.sender === 'user' ? '我' : '开开' }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ formatDateTime(message.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-900 leading-relaxed">{{ message.content }}</p>
|
||||
<div v-if="message.aiReply" class="mt-2 p-3 bg-white rounded border-l-4 border-green-400">
|
||||
<p class="text-sm text-gray-700">{{ message.aiReply }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="totalPages > 1" class="p-6 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-600">
|
||||
第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MessageService, { type MessageResponse, type PageResult } from '@/services/message'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const messages = ref<MessageResponse[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalCount = ref(0)
|
||||
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value))
|
||||
|
||||
// 搜索防抖
|
||||
let searchTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
// 方法
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const loadMessages = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
if (searchKeyword.value.trim()) {
|
||||
// 搜索模式
|
||||
const searchResult = await MessageService.searchUserMessages({
|
||||
keyword: searchKeyword.value.trim(),
|
||||
limit: 100,
|
||||
startTime: startDate.value ? `${startDate.value} 00:00:00` : undefined,
|
||||
endTime: endDate.value ? `${endDate.value} 23:59:59` : undefined
|
||||
})
|
||||
messages.value = searchResult
|
||||
totalCount.value = searchResult.length
|
||||
currentPage.value = 1
|
||||
} else {
|
||||
// 分页模式
|
||||
const pageResult = await MessageService.getUserMessages(currentPage.value, pageSize.value)
|
||||
messages.value = pageResult.records
|
||||
totalCount.value = pageResult.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
ElMessage.error('加载聊天记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
searchTimeout = setTimeout(() => {
|
||||
currentPage.value = 1
|
||||
loadMessages()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleDateFilter = () => {
|
||||
currentPage.value = 1
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
searchKeyword.value = ''
|
||||
startDate.value = ''
|
||||
endDate.value = ''
|
||||
currentPage.value = 1
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
loadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
return new Date(dateTime).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 监听弹框显示状态
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadMessages()
|
||||
// 初始化图标
|
||||
setTimeout(() => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条 */
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="user-avatar" :class="sizeClass">
|
||||
<img
|
||||
v-if="avatar"
|
||||
:src="avatar"
|
||||
:alt="nickname"
|
||||
class="avatar-image"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ avatarText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
/** 头像URL */
|
||||
avatar?: string
|
||||
/** 昵称 */
|
||||
nickname: string
|
||||
/** 尺寸 */
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
// 图片加载失败标记
|
||||
const imageError = ref(false)
|
||||
|
||||
// 尺寸类名
|
||||
const sizeClass = computed(() => `avatar-${props.size}`)
|
||||
|
||||
// 头像文字(取昵称首字符)
|
||||
const avatarText = computed(() => {
|
||||
if (!props.nickname) return '用'
|
||||
return props.nickname.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
// 处理图片加载失败
|
||||
const handleImageError = () => {
|
||||
imageError.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.avatar-medium {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.avatar-large {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<div class="user-info-trigger">
|
||||
<UserAvatar :avatar="userInfo?.avatar" :nickname="userInfo?.nickname || '用户'" size="medium" />
|
||||
<div class="user-text">
|
||||
<div class="user-nickname">{{ userInfo?.nickname || '用户' }}</div>
|
||||
<div class="user-level">{{ userInfo?.memberLevel || 'Lv.1' }}</div>
|
||||
</div>
|
||||
<el-icon class="dropdown-icon">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="settings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
账号设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="dashboard">
|
||||
<el-icon><DataBoard /></el-icon>
|
||||
个人仪表盘
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowDown,
|
||||
User,
|
||||
Setting,
|
||||
DataBoard,
|
||||
SwitchButton
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import UserAvatar from './UserAvatar.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => authStore.userInfo)
|
||||
|
||||
// 处理下拉菜单命令
|
||||
const handleCommand = async (command: string) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'dashboard':
|
||||
router.push('/personal-dashboard')
|
||||
break
|
||||
case 'logout':
|
||||
await handleLogout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要退出登录吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await authStore.logout()
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-info-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-info-trigger:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.user-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-nickname {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.user-info-trigger:hover .dropdown-icon {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu) {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item:hover) {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-divided) {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 640px) {
|
||||
.user-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-info-trigger {
|
||||
padding: 6px;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<div style="background: white; padding: 40px 20px; text-align: center; border-top: 1px solid #e8e8e8;">
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3 style="color: #4A90E2; font-size: 20px; margin-bottom: 8px;">开心APP</h3>
|
||||
<p style="color: #888; margin: 0;">陪伴、理解、记录、共同成长。</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; gap: 40px; margin-bottom: 20px; flex-wrap: wrap;">
|
||||
<router-link to="/chat" style="color: #888; text-decoration: none;">聊天</router-link>
|
||||
<router-link to="/diary" style="color: #888; text-decoration: none;">日记</router-link>
|
||||
<router-link to="/dashboard" style="color: #888; text-decoration: none;">展板</router-link>
|
||||
<router-link to="/settings" style="color: #888; text-decoration: none;">设置</router-link>
|
||||
</div>
|
||||
|
||||
<p style="color: #888; font-size: 14px; margin: 0;">
|
||||
© 2025 开心APP. All Rights Reserved. 来自"开心"星球的温柔科技。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 简化版Footer组件
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/assets/styles/variables.scss" as *;
|
||||
.app-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,321 +0,0 @@
|
||||
<template>
|
||||
<header class="app-header" :class="{ 'scrolled': isScrolled }">
|
||||
<div class="header-content">
|
||||
<!-- Logo -->
|
||||
<router-link to="/" class="logo">
|
||||
<svg width="32" height="32" viewBox="0 0 100 100" class="logo-icon">
|
||||
<path fill="currentColor" d="M85.4,37.3C85.4,37.3,85.4,37.3,85.4,37.3c-2.8-9.9-10-17.7-19.1-21.2c-0.2-0.1-0.5-0.1-0.7-0.2c-0.1,0-0.2-0.1-0.3-0.1 c-1.2-0.4-2.5-0.8-3.7-1.1c-1-0.2-2-0.4-3-0.6c-1.1-0.2-2.1-0.3-3.2-0.4c-1.2-0.1-2.4-0.2-3.6-0.2c-0.1,0-0.2,0-0.3,0h-0.1 c-0.1,0-0.2,0-0.3,0c-1.2,0-2.4,0.1-3.6,0.2c-1.1,0.1-2.1,0.2-3.2,0.4c-1,0.2-2,0.4-3,0.6c-1.3,0.3-2.5,0.6-3.7,1.1 c-0.1,0-0.2,0.1-0.3,0.1c-0.2,0.1-0.5,0.1-0.7,0.2C21.6,19.6,14.4,27.4,11.6,37.3c0,0,0,0.1-0.1,0.1C8,47.7,8,58.8,11.5,69.2 c0,0.1,0.1,0.1,0.1,0.2c2.8,9.9,10,17.7,19.1,21.2c0.2,0.1,0.5,0.1,0.7,0.2c0.1,0,0.2,0.1,0.3,0.1c1.2,0.4,2.5,0.8,3.7,1.1 c1,0.2,2,0.4,3,0.6c1.1,0.2,2.1,0.3,3.2,0.4c1.2,0.1,2.4,0.2,3.6,0.2c0.1,0,0.2,0,0.3,0h0.1c0.1,0,0.2,0,0.3,0 c1.2,0,2.4-0.1,3.6-0.2c-1.1-0.1-2.1-0.2-3.2-0.4c1-0.2,2-0.4,3-0.6c1.3-0.3,2.5-0.6,3.7-1.1c0.1,0,0.2-0.1,0.3-0.1 c0.2-0.1,0.5-0.1,0.7-0.2c9.1-3.5,16.3-11.3,19.1-21.2c0-0.1,0.1-0.1,0.1-0.2C89,58.8,89,47.7,85.4,37.3z M50,77.9 c-15.4,0-27.9-12.5-27.9-27.9S34.6,22.1,50,22.1s27.9,12.5,27.9,27.9S65.4,77.9,50,77.9z"></path>
|
||||
<path fill="#F5A623" d="M50,88.8c-21.4,0-38.8-17.4-38.8-38.8S28.6,11.2,50,11.2s38.8,17.4,38.8,38.8S71.4,88.8,50,88.8z M50,16.2 c-18.7,0-33.8,15.1-33.8,33.8S31.3,83.8,50,83.8s33.8-15.1,33.8-33.8S68.7,16.2,50,16.2z"></path>
|
||||
</svg>
|
||||
<span class="logo-text">开心APP</span>
|
||||
</router-link>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu" :class="{ 'mobile-hidden': !mobileMenuVisible }">
|
||||
<router-link to="/chat" class="nav-link" @click="closeMobileMenu">聊天</router-link>
|
||||
<router-link to="/diary" class="nav-link" @click="closeMobileMenu">日记</router-link>
|
||||
<router-link to="/dashboard" class="nav-link" @click="closeMobileMenu">展板</router-link>
|
||||
<router-link to="/topic-tracker" class="nav-link" @click="closeMobileMenu">话题追踪</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="header-actions">
|
||||
<!-- 未登录状态 -->
|
||||
<template v-if="!userStore.isLoggedIn">
|
||||
<a-button type="text" @click="$router.push('/login')" class="login-btn">
|
||||
登录
|
||||
</a-button>
|
||||
<a-button type="primary" @click="$router.push('/chat')" class="start-btn">
|
||||
免费开始
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 已登录状态 -->
|
||||
<template v-else>
|
||||
<a-dropdown>
|
||||
<div class="user-info-section">
|
||||
<a-avatar
|
||||
:size="32"
|
||||
:src="userStore.userInfo?.avatar"
|
||||
class="user-avatar"
|
||||
>
|
||||
<template #icon v-if="!userStore.userInfo?.avatar">
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="user-nickname">
|
||||
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}
|
||||
</span>
|
||||
<DownOutlined class="dropdown-icon" />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile" @click="$router.push('/dashboard')">
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings" @click="$router.push('/settings')">
|
||||
<SettingOutlined />
|
||||
设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" @click="handleLogout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<a-button
|
||||
type="text"
|
||||
class="mobile-menu-btn"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
<MenuOutlined v-if="!mobileMenuVisible" />
|
||||
<CloseOutlined v-else />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div v-if="mobileMenuVisible" class="mobile-menu">
|
||||
<nav class="mobile-nav">
|
||||
<router-link to="/chat" class="mobile-nav-link" @click="closeMobileMenu">聊天</router-link>
|
||||
<router-link to="/diary" class="mobile-nav-link" @click="closeMobileMenu">日记</router-link>
|
||||
<router-link to="/dashboard" class="mobile-nav-link" @click="closeMobileMenu">展板</router-link>
|
||||
<router-link to="/topic-tracker" class="mobile-nav-link" @click="closeMobileMenu">话题追踪</router-link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
DownOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
MenuOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 响应式状态
|
||||
const isScrolled = ref(false)
|
||||
const mobileMenuVisible = ref(false)
|
||||
|
||||
// 滚动监听
|
||||
const handleScroll = () => {
|
||||
isScrolled.value = window.scrollY > 50
|
||||
}
|
||||
|
||||
// 移动端菜单控制
|
||||
const toggleMobileMenu = () => {
|
||||
mobileMenuVisible.value = !mobileMenuVisible.value
|
||||
}
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
mobileMenuVisible.value = false
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await userStore.logout()
|
||||
message.success('退出登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
message.error('退出登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
handleScroll() // 初始检查
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/assets/styles/variables.scss" as *;
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.scrolled {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
border-bottom-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: $tech-blue;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: $tech-blue;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #888888;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: #4A90E2;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
color: #4A90E2;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: #4A90E2;
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.user-nickname {
|
||||
font-weight: 500;
|
||||
color: #4A90E2;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
color: #4A90E2;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: #4A90E2;
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
.user-nickname {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<nav class="fixed bottom-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm shadow-[0_-2px_10px_rgba(0,0,0,0.05)] flex justify-around py-2 border-t border-gray-200/80">
|
||||
<router-link
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:to="item.href"
|
||||
class="flex flex-col items-center justify-center text-xs p-2 rounded-md transition-colors w-20"
|
||||
:class="isActive(item.href) ? 'text-tech-blue bg-tech-blue/10 font-semibold' : 'text-text-medium hover:bg-gray-100 hover:text-tech-blue'"
|
||||
>
|
||||
<i :data-lucide="item.icon" class="w-5 h-5 mb-1"></i>
|
||||
<span>{{ item.text }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const navItems = [
|
||||
{ icon: 'message-square', text: '聊天', href: '/chat', name: 'Chat' },
|
||||
{ icon: 'book-open', text: '日记', href: '/diary', name: 'Diary' },
|
||||
{ icon: 'crosshair', text: '话题', href: '/topic-tracker', name: 'TopicTracker' },
|
||||
{ icon: 'milestone', text: '人生轨迹', href: '/life-milestones', name: 'LifeMilestones' },
|
||||
{ icon: 'layout-dashboard', text: '个人展板', href: '/personal-dashboard', name: 'PersonalDashboard' }
|
||||
]
|
||||
|
||||
const isActive = (href: string) => {
|
||||
return route.path === href
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化Lucide图标
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:root {
|
||||
--tech-blue: #4A90E2;
|
||||
--text-medium: #888888;
|
||||
}
|
||||
|
||||
.text-tech-blue { color: var(--tech-blue); }
|
||||
.text-text-medium { color: var(--text-medium); }
|
||||
.bg-tech-blue\/10 { background-color: rgba(74, 144, 226, 0.1); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user