fix: 系统公告弹窗前端

This commit is contained in:
Gsh
2025-11-05 23:12:23 +08:00
parent 09fb43ee14
commit 17337b8d78
21 changed files with 2729 additions and 17 deletions

View File

@@ -0,0 +1,598 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import type { Activity, Announcement } from '@/api'
import { useAnnouncementStore } from '@/stores'
import type { CloseType } from '@/stores/modules/announcement'
const router = useRouter()
const announcementStore = useAnnouncementStore()
const activeTab = ref('activity')
// 窗口宽度响应式状态
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1920)
// 监听窗口大小变化
onMounted(() => {
const handleResize = () => {
windowWidth.value = window.innerWidth
}
window.addEventListener('resize', handleResize)
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
})
// 响应式弹窗宽度
const dialogWidth = computed(() => {
if (windowWidth.value < 768) return '95%'
if (windowWidth.value < 1024) return '90%'
return '700px'
})
// 从store获取数据
const { carousels, activities, announcements, isDialogVisible } = storeToRefs(announcementStore)
// 分离最新公告和历史公告
const latestAnnouncements = computed(() =>
announcements.value.filter(a => a.type === 'latest'),
)
const historyAnnouncements = computed(() =>
announcements.value.filter(a => a.type === 'history'),
)
// 处理关闭弹窗
function handleClose(type: CloseType) {
announcementStore.closeDialog(type)
const messages = {
today: '今日内不再显示',
week: '本周内不再显示',
permanent: '公告已关闭',
}
ElMessage.success(messages[type])
}
// 查看活动详情
function viewActivityDetail(activity: Activity) {
router.push({
name: 'activityDetail',
params: { id: activity.id },
})
announcementStore.isDialogVisible = false
}
// 查看公告详情
function viewAnnouncementDetail(announcement: Announcement) {
router.push({
name: 'announcementDetail',
params: { id: announcement.id },
})
announcementStore.isDialogVisible = false
}
// 格式化时间
function formatTime(time: string) {
const date = new Date(time)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
</script>
<template>
<el-dialog
v-model="isDialogVisible"
title="系统公告"
:width="dialogWidth"
:close-on-click-modal="false"
class="announcement-dialog"
>
<div class="announcement-dialog-content">
<el-tabs v-model="activeTab" class="announcement-tabs">
<!-- 活动Tab -->
<el-tab-pane label="活动" name="activity">
<div class="tab-content-wrapper">
<div class="activity-content">
<!-- 轮播图 -->
<el-carousel
v-if="carousels.length > 0"
height="250px"
class="carousel-container"
>
<el-carousel-item v-for="item in carousels" :key="item.id">
<img
:src="item.imageUrl"
:alt="item.title"
class="carousel-image"
>
<div v-if="item.title" class="carousel-title">
{{ item.title }}
</div>
</el-carousel-item>
</el-carousel>
<!-- 活动列表 -->
<div class="activity-list">
<div
v-for="activity in activities"
:key="activity.id"
class="activity-item"
>
<div class="activity-header">
<h3 class="activity-title">{{ activity.title }}</h3>
<el-tag
v-if="activity.status === 'active'"
type="success"
size="small"
>
进行中
</el-tag>
<el-tag
v-else-if="activity.status === 'expired'"
type="info"
size="small"
>
已结束
</el-tag>
</div>
<p class="activity-description">{{ activity.description }}</p>
<div class="activity-footer">
<span class="activity-time">{{ formatTime(activity.createdAt) }}</span>
<el-button
type="primary"
link
@click="viewActivityDetail(activity)"
>
查看详情
</el-button>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<!-- 公告Tab -->
<el-tab-pane label="公告" name="announcement">
<div class="tab-content-wrapper">
<div class="announcement-content">
<!-- 最新公告 -->
<div v-if="latestAnnouncements.length > 0" class="latest-section">
<h3 class="section-title">最新公告</h3>
<div
v-for="announcement in latestAnnouncements"
:key="announcement.id"
class="announcement-item latest"
>
<div class="announcement-header">
<h4 class="announcement-title">
<el-icon v-if="announcement.isImportant" color="#f56c6c">
<i-ep-warning />
</el-icon>
{{ announcement.title }}
</h4>
<span class="announcement-time">{{ formatTime(announcement.publishTime) }}</span>
</div>
<p class="announcement-summary">{{ announcement.content.substring(0, 100) }}...</p>
<el-button
type="primary"
link
@click="viewAnnouncementDetail(announcement)"
>
查看详情
</el-button>
</div>
</div>
<!-- 历史公告时间线 -->
<div v-if="historyAnnouncements.length > 0" class="history-section">
<h3 class="section-title">历史公告</h3>
<el-timeline>
<el-timeline-item
v-for="announcement in historyAnnouncements"
:key="announcement.id"
:timestamp="formatTime(announcement.publishTime)"
placement="top"
>
<div class="timeline-item-content">
<h4 class="timeline-title">{{ announcement.title }}</h4>
<p class="timeline-summary">{{ announcement.content.substring(0, 80) }}...</p>
<el-button
type="primary"
link
size="small"
@click="viewAnnouncementDetail(announcement)"
>
查看详情
</el-button>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose('week')">本周关闭</el-button>
<el-button @click="handleClose('today')">今日关闭</el-button>
<el-button type="primary" @click="handleClose('permanent')">
关闭公告
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
// 确保弹窗本身不会溢出
:deep(.el-dialog) {
margin-top: 5vh !important;
margin-bottom: 5vh !important;
max-height: 90vh;
display: flex;
flex-direction: column;
}
:deep(.el-dialog__body) {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
padding: 0 !important;
}
.announcement-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.announcement-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.el-tabs__header) {
flex-shrink: 0;
margin: 0 20px;
}
:deep(.el-tabs__content) {
flex: 1;
overflow: hidden;
}
:deep(.el-tab-pane) {
height: auto;
display: flex;
flex-direction: column;
}
}
.tab-content-wrapper {
height: 60vh;
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
padding: 0 20px 20px;
flex-shrink: 0;
// 确保滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.activity-content {
min-height: min-content;
.carousel-container {
margin-bottom: 24px;
border-radius: 8px;
overflow: hidden;
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.carousel-title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: #fff;
font-size: 16px;
font-weight: 500;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
transition: all 0.3s;
&:hover {
background: #e8eaed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.activity-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.activity-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
flex: 1;
}
.activity-description {
margin: 0 0 12px 0;
color: #606266;
font-size: 14px;
line-height: 1.5;
}
.activity-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.activity-time {
font-size: 12px;
color: #909399;
}
}
.announcement-content {
min-height: min-content;
.section-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
.latest-section {
margin-bottom: 32px;
}
.announcement-item {
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
margin-bottom: 12px;
transition: all 0.3s;
&:hover {
background: #e8eaed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&.latest {
border-left: 3px solid #409eff;
}
}
.announcement-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.announcement-title {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 6px;
}
.announcement-time {
font-size: 12px;
color: #909399;
}
.announcement-summary {
margin: 0 0 12px 0;
color: #606266;
font-size: 14px;
line-height: 1.5;
}
.history-section {
:deep(.el-timeline) {
padding-left: 0;
}
.timeline-item-content {
padding-left: 8px;
}
.timeline-title {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.timeline-summary {
margin: 0 0 8px 0;
color: #606266;
font-size: 13px;
line-height: 1.5;
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
// 移动端适配
@media screen and (max-width: 768px) {
:deep(.el-dialog) {
margin: 5vh auto !important;
max-height: 90vh;
}
:deep(.el-dialog__header) {
padding: 16px;
flex-shrink: 0;
}
:deep(.el-dialog__footer) {
padding: 12px;
flex-shrink: 0;
}
.announcement-tabs {
:deep(.el-tabs__header) {
margin: 0 12px;
}
}
.tab-content-wrapper {
height: calc(90vh - 230px);
max-height: calc(90vh - 230px);
padding: 0 12px 12px;
flex-shrink: 0;
-webkit-overflow-scrolling: touch;
// 移动端滚动条样式
&::-webkit-scrollbar {
width: 4px;
}
}
.activity-content {
.carousel-container {
margin-bottom: 16px;
}
.carousel-title {
padding: 8px 12px;
font-size: 14px;
}
.activity-item {
padding: 12px;
}
.activity-title {
font-size: 14px;
}
.activity-description {
font-size: 13px;
}
.activity-footer {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
.announcement-content {
.section-title {
font-size: 14px;
margin-bottom: 12px;
}
.announcement-item {
padding: 12px;
margin-bottom: 8px;
}
.announcement-title {
font-size: 14px;
}
.announcement-summary {
font-size: 13px;
}
.history-section {
.timeline-title {
font-size: 13px;
}
.timeline-summary {
font-size: 12px;
}
}
}
.dialog-footer {
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
margin: 0;
}
}
}
// 平板适配 (768px - 1024px)
@media screen and (min-width: 768px) and (max-width: 1024px) {
:deep(.el-dialog) {
margin: 5vh auto !important;
max-height: 90vh;
}
.announcement-tabs {
max-height: calc(90vh - 200px);
}
}
</style>