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
+130 -569
View File
@@ -1,590 +1,151 @@
<template>
<div class="messages-page">
<!-- 头部 -->
<header class="page-header">
<div class="header-content">
<div class="header-left">
<a-button type="text" @click="$router.back()" class="back-btn">
<ArrowLeftOutlined />
</a-button>
<h1 class="page-title">消息中心</h1>
<div class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased">
<!-- App Header -->
<header class="fixed top-0 left-0 right-0 z-40 bg-white/90 backdrop-blur-md border-b border-gray-200/80">
<div class="container mx-auto px-4 h-16 flex items-center justify-between relative">
<button @click="goBack" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="chevron-left" class="w-6 h-6"></i>
</button>
<h1 class="text-lg font-semibold text-text-dark absolute left-1/2 -translate-x-1/2">消息中心</h1>
<router-link to="/" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</router-link>
</div>
<div class="header-actions">
<a-button type="text" @click="markAllAsRead" :disabled="unreadCount === 0">
全部已读
</a-button>
<a-button type="text" @click="clearAllMessages" danger>
<DeleteOutlined />
清空
</a-button>
</div>
</div>
</header>
</header>
<!-- 主要内容 -->
<main class="page-main">
<div class="container">
<!-- 筛选标签 -->
<div class="filter-tabs">
<a-radio-group v-model:value="activeTab" @change="handleTabChange">
<a-radio-button value="all">
全部 <a-badge :count="messages.length" :show-zero="false" />
</a-radio-button>
<a-radio-button value="unread">
未读 <a-badge :count="unreadCount" :show-zero="false" />
</a-radio-button>
<a-radio-button value="system">
系统消息 <a-badge :count="systemCount" :show-zero="false" />
</a-radio-button>
<a-radio-button value="notification">
通知 <a-badge :count="notificationCount" :show-zero="false" />
</a-radio-button>
<a-radio-button value="reminder">
提醒 <a-badge :count="reminderCount" :show-zero="false" />
</a-radio-button>
</a-radio-group>
</div>
<!-- 消息列表 -->
<div class="messages-list">
<div
v-for="message in filteredMessages"
:key="message.id"
class="message-item"
:class="{ 'unread': message.status === 'unread' }"
@click="handleMessageClick(message)"
>
<div class="message-icon">
<div class="icon-wrapper" :class="`type-${message.type}`">
<component :is="getMessageIcon(message.type)" />
<main class="pt-20 pb-8 bg-light-gray min-h-screen">
<div class="container mx-auto px-6">
<div id="message-list" class="max-w-3xl mx-auto space-y-4">
<div
v-for="(msg, index) in messages"
:key="index"
class="bg-white p-5 rounded-xl shadow-sm border border-gray-200/80 flex items-start space-x-4 hover:shadow-md hover:border-tech-blue/30 transition-all duration-300 animate-fade-in-up"
:style="{ animationDelay: `${index * 0.1}s` }"
>
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-light-gray flex items-center justify-center border">
<i :data-lucide="msg.icon" class="w-5 h-5" :class="msg.color"></i>
</div>
</div>
<div class="message-content">
<div class="message-header">
<h3 class="message-title">{{ message.title }}</h3>
<div class="message-meta">
<a-tag :color="getTypeColor(message.type)" size="small">
{{ getTypeText(message.type) }}
</a-tag>
<span class="message-time">{{ formatTime.friendly(message.createTime) }}</span>
<div class="flex-grow">
<div class="flex justify-between items-center">
<h3 class="font-bold text-text-dark">{{ msg.title }}</h3>
<span class="text-xs text-text-medium whitespace-nowrap">{{ msg.timestamp }}</span>
</div>
<p class="text-text-medium mt-1 pr-4">{{ msg.content }}</p>
</div>
<p class="message-text">{{ message.content }}</p>
<div class="message-actions" v-if="message.actionUrl">
<a-button type="link" size="small" @click.stop="handleAction(message)">
查看详情
<RightOutlined />
</a-button>
</div>
</div>
<div class="message-controls">
<a-button
type="text"
size="small"
@click.stop="toggleReadStatus(message)"
:title="message.status === 'read' ? '标记为未读' : '标记为已读'"
>
<EyeOutlined v-if="message.status === 'unread'" />
<EyeInvisibleOutlined v-else />
</a-button>
<a-button
type="text"
size="small"
danger
@click.stop="deleteMessage(message.id)"
title="删除消息"
>
<DeleteOutlined />
</a-button>
<button class="flex-shrink-0 text-text-medium hover:text-tech-blue self-center">
<i data-lucide="chevron-right" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredMessages.length === 0" class="empty-state">
<a-empty
:description="getEmptyDescription()"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
/>
</div>
<!-- 加载更多 -->
<div class="load-more" v-if="hasMore">
<a-button @click="loadMore" :loading="isLoading" block>
加载更多
</a-button>
</div>
</div>
</main>
<!-- 消息详情模态框 -->
<a-modal
v-model:open="showDetailModal"
:title="selectedMessage?.title"
:footer="null"
width="600px"
>
<div v-if="selectedMessage" class="message-detail">
<div class="detail-header">
<a-tag :color="getTypeColor(selectedMessage.type)">
{{ getTypeText(selectedMessage.type) }}
</a-tag>
<span class="detail-time">{{ formatTime.standard(selectedMessage.createTime) }}</span>
</div>
<div class="detail-content">
<p>{{ selectedMessage.content }}</p>
</div>
<div class="detail-actions" v-if="selectedMessage.actionUrl">
<a-button type="primary" @click="handleAction(selectedMessage)">
查看详情
</a-button>
</div>
</div>
</a-modal>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import {
ArrowLeftOutlined,
DeleteOutlined,
RightOutlined,
EyeOutlined,
EyeInvisibleOutlined,
BellOutlined,
InfoCircleOutlined,
ClockCircleOutlined,
SettingOutlined,
} from '@ant-design/icons-vue'
import { Empty, message } from 'ant-design-vue'
import { formatTime } from '@/utils'
import type { Message } from '@/types'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
// 响应式数据
const activeTab = ref('all')
const showDetailModal = ref(false)
const selectedMessage = ref<Message | null>(null)
const isLoading = ref(false)
const hasMore = ref(false)
const router = useRouter()
// 消息数据
const messages = ref<Message[]>([
{
id: '1',
title: '欢迎使用开心APP',
content: '感谢您注册开心APP!我是您的情绪陪伴使者开开,很高兴认识您。让我们一起开始这段美好的情绪陪伴之旅吧!',
type: 'system',
status: 'unread',
createTime: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
actionUrl: '/chat'
},
{
id: '2',
title: '每日心情记录提醒',
content: '今天还没有记录心情哦~花几分钟写下今天的感受,让开开更好地了解您的情绪变化。',
type: 'reminder',
status: 'unread',
createTime: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
actionUrl: '/diary'
},
{
id: '3',
title: '新功能上线通知',
content: '话题追踪功能已上线!现在您可以创建和管理感兴趣的话题,让开开帮您更好地整理思路。',
type: 'notification',
status: 'read',
createTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
actionUrl: '/topic-tracker'
},
{
id: '4',
title: '系统维护通知',
content: '系统将于今晚23:00-24:00进行例行维护,期间可能会影响部分功能的使用,请您谅解。',
type: 'system',
status: 'read',
createTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: '5',
title: '个人展板完善提醒',
content: '完善您的个人展板信息,让开开更好地了解您的兴趣爱好和生活技能,提供更个性化的陪伴。',
type: 'reminder',
status: 'read',
createTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
actionUrl: '/dashboard'
// 消息数据
const messages = ref([
{
type: 'ai',
icon: 'sparkles',
color: 'text-warm-orange',
title: '开开的每周心情总结',
content: '你好呀!上周我们聊了很多关于"新工作的挑战",你表现出了很棒的适应能力和积极心态。记得给自己一些放松的时间哦,比如看看你喜欢的电影。',
timestamp: '2025年7月15日 09:30'
},
{
type: 'system',
icon: 'bell',
color: 'text-tech-blue',
title: '系统通知:欢迎使用日记功能',
content: '现在,你可以在日记区记录下你的生活点滴,开开会阅读你的日记并给你温暖的回复和鼓励哦。',
timestamp: '2025年7月14日 18:00'
},
{
type: 'ai',
icon: 'sparkles',
color: 'text-warm-orange',
title: '开开的话题追踪提醒',
content: '我发现你最近经常提到"学吉他",我已经为你创建了一个话题追踪卡片,帮你记录学习进度和心得。一起加油吧!',
timestamp: '2025年7月12日 11:25'
},
{
type: 'system',
icon: 'award',
color: 'text-green-500',
title: '成就解锁:初次见面',
content: '恭喜你完成了与开开的第一次对话,这是共同成长的第一步。',
timestamp: '2025年7月10日 20:45'
}
])
// 返回上一页
const goBack = () => {
router.back()
}
// 生命周期
onMounted(() => {
// 初始化Lucide图标
if (window.lucide) {
window.lucide.createIcons()
}
// 延迟初始化图标
setTimeout(() => {
if (window.lucide) {
window.lucide.createIcons()
}
])
// 计算属性
const filteredMessages = computed(() => {
switch (activeTab.value) {
case 'unread':
return messages.value.filter(msg => msg.status === 'unread')
case 'system':
return messages.value.filter(msg => msg.type === 'system')
case 'notification':
return messages.value.filter(msg => msg.type === 'notification')
case 'reminder':
return messages.value.filter(msg => msg.type === 'reminder')
default:
return messages.value
}
})
const unreadCount = computed(() =>
messages.value.filter(msg => msg.status === 'unread').length
)
const systemCount = computed(() =>
messages.value.filter(msg => msg.type === 'system').length
)
const notificationCount = computed(() =>
messages.value.filter(msg => msg.type === 'notification').length
)
const reminderCount = computed(() =>
messages.value.filter(msg => msg.type === 'reminder').length
)
// 方法
const getMessageIcon = (type: Message['type']) => {
const icons = {
system: SettingOutlined,
notification: BellOutlined,
reminder: ClockCircleOutlined
}
return icons[type] || InfoCircleOutlined
}
const getTypeColor = (type: Message['type']) => {
const colors = {
system: 'blue',
notification: 'green',
reminder: 'orange'
}
return colors[type] || 'default'
}
const getTypeText = (type: Message['type']) => {
const texts = {
system: '系统消息',
notification: '通知',
reminder: '提醒'
}
return texts[type] || type
}
const getEmptyDescription = () => {
switch (activeTab.value) {
case 'unread':
return '暂无未读消息'
case 'system':
return '暂无系统消息'
case 'notification':
return '暂无通知消息'
case 'reminder':
return '暂无提醒消息'
default:
return '暂无消息'
}
}
const handleTabChange = () => {
// 标签切换逻辑已在计算属性中处理
}
const handleMessageClick = (msg: Message) => {
// 标记为已读
if (msg.status === 'unread') {
msg.status = 'read'
}
// 显示详情
selectedMessage.value = msg
showDetailModal.value = true
}
const toggleReadStatus = (msg: Message) => {
msg.status = msg.status === 'read' ? 'unread' : 'read'
message.success(`${msg.status === 'read' ? '标记为已读' : '标记为未读'}`)
}
const deleteMessage = (id: string) => {
const index = messages.value.findIndex(msg => msg.id === id)
if (index > -1) {
messages.value.splice(index, 1)
message.success('消息删除成功')
}
}
const markAllAsRead = () => {
messages.value.forEach(msg => {
if (msg.status === 'unread') {
msg.status = 'read'
}
})
message.success('所有消息已标记为已读')
}
const clearAllMessages = () => {
messages.value.length = 0
message.success('所有消息已清空')
}
const handleAction = (msg: Message) => {
if (msg.actionUrl) {
// 这里可以使用路由跳转
message.info(`跳转到:${msg.actionUrl}`)
}
}
const loadMore = () => {
isLoading.value = true
// 模拟加载更多
setTimeout(() => {
isLoading.value = false
hasMore.value = false
}, 1000)
}
// 组件挂载
onMounted(() => {
// 初始化消息数据
})
}, 100)
})
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.messages-page {
min-height: 100vh;
background: $light-gray;
<style scoped>
/* 导入原始样式变量 */
:root {
--tech-blue: #4A90E2;
--warm-orange: #F5A623;
--white: #FFFFFF;
--light-gray: #F7F8FA;
--text-dark: #333333;
--text-medium: #888888;
}
/* 应用原始样式类 */
.bg-tech-blue { background-color: var(--tech-blue); }
.bg-warm-orange { background-color: var(--warm-orange); }
.bg-light-gray { background-color: var(--light-gray); }
.text-tech-blue { color: var(--tech-blue); }
.text-warm-orange { color: var(--warm-orange); }
.text-text-dark { color: var(--text-dark); }
.text-text-medium { color: var(--text-medium); }
.border-tech-blue { border-color: var(--tech-blue); }
.animate-fade-in-up {
animation: fade-in-up 0.8s ease-out forwards;
opacity: 0;
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
.page-header {
background: white;
box-shadow: $shadow-sm;
position: sticky;
top: 0;
z-index: 10;
to {
opacity: 1;
transform: translateY(0);
}
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-md $spacing-lg;
max-width: 1000px;
margin: 0 auto;
}
.header-left {
display: flex;
align-items: center;
gap: $spacing-md;
}
.back-btn {
color: $text-medium;
&:hover {
color: $tech-blue;
}
}
.page-title {
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $text-dark;
margin: 0;
}
.header-actions {
display: flex;
gap: $spacing-sm;
}
.page-main {
padding: $spacing-lg;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.filter-tabs {
margin-bottom: $spacing-xl;
:deep(.ant-radio-group) {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
}
}
.messages-list {
display: flex;
flex-direction: column;
gap: $spacing-md;
}
.message-item {
background: white;
border-radius: $border-radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
display: flex;
gap: $spacing-md;
cursor: pointer;
transition: all $transition-normal;
border-left: 4px solid transparent;
&:hover {
box-shadow: $shadow-md;
transform: translateY(-1px);
}
&.unread {
border-left-color: $tech-blue;
background: linear-gradient(90deg, rgba(74, 144, 226, 0.02) 0%, white 100%);
}
.message-icon {
flex-shrink: 0;
.icon-wrapper {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: $font-size-lg;
&.type-system {
background: #1890ff;
}
&.type-notification {
background: #52c41a;
}
&.type-reminder {
background: #fa8c16;
}
}
}
.message-content {
flex: 1;
min-width: 0;
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-sm;
gap: $spacing-md;
.message-title {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-dark;
margin: 0;
line-height: 1.4;
}
.message-meta {
display: flex;
align-items: center;
gap: $spacing-sm;
flex-shrink: 0;
.message-time {
font-size: $font-size-sm;
color: $text-medium;
white-space: nowrap;
}
}
}
.message-text {
color: $text-medium;
line-height: 1.5;
margin: 0 0 $spacing-sm 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.message-actions {
.ant-btn-link {
padding: 0;
height: auto;
font-size: $font-size-sm;
}
}
}
.message-controls {
display: flex;
flex-direction: column;
gap: $spacing-xs;
flex-shrink: 0;
}
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: $spacing-xxl;
}
.load-more {
margin-top: $spacing-xl;
}
// 消息详情模态框
.message-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-lg;
padding-bottom: $spacing-md;
border-bottom: 1px solid #f0f0f0;
.detail-time {
color: $text-medium;
font-size: $font-size-sm;
}
}
.detail-content {
margin-bottom: $spacing-lg;
p {
color: $text-dark;
line-height: 1.6;
margin: 0;
}
}
.detail-actions {
padding-top: $spacing-md;
border-top: 1px solid #f0f0f0;
}
}
</style>
/* 全局样式 */
body {
font-family: 'Noto Sans SC', sans-serif;
}
</style>