重命名前端项目目录:web-flowith -> web
- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁 - 保持所有前端代码和配置文件不变 - 统一项目目录命名规范
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<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)" />
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('all')
|
||||
const showDetailModal = ref(false)
|
||||
const selectedMessage = ref<Message | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const hasMore = ref(false)
|
||||
|
||||
// 消息数据
|
||||
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 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(() => {
|
||||
// 初始化消息数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.messages-page {
|
||||
min-height: 100vh;
|
||||
background: $light-gray;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
box-shadow: $shadow-sm;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user