feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置

This commit is contained in:
2025-07-27 10:05:59 +08:00
parent 6903ac1c0d
commit cc886cd4d5
126 changed files with 21179 additions and 15734 deletions
+302
View File
@@ -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>
+94
View File
@@ -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>
+188
View File
@@ -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>
-34
View File
@@ -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>
-321
View File
@@ -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>