feat: 添加公告系统,支持动态显示和确认功能

This commit is contained in:
2025-08-31 22:50:28 +08:00
parent fab4645132
commit fe35e37371
4 changed files with 454 additions and 2 deletions

View File

@@ -0,0 +1,339 @@
<template>
<a-modal
v-model:open="visible"
title="系统公告"
:width="800"
:footer="null"
:closable="false"
:mask-closable="false"
class="notice-modal"
>
<div v-if="notices.length > 0" class="notice-container">
<!-- 公告标签页 - 竖直布局 -->
<a-tabs
v-model:activeKey="activeNoticeKey"
tab-position="left"
class="notice-tabs"
:tabBarStyle="{ width: '200px' }"
>
<a-tab-pane
v-for="(content, title) in noticeData"
:key="title"
:tab="title"
class="notice-tab-pane"
>
<div class="notice-content">
<div class="markdown-content" v-html="renderMarkdown(content)"></div>
</div>
</a-tab-pane>
</a-tabs>
<!-- 底部操作按钮 -->
<div class="notice-footer">
<div class="notice-pagination">
<span class="pagination-text"> {{ notices.length }} 个公告 </span>
</div>
<div class="notice-actions">
<a-button
type="primary"
@click="confirmNotices"
:loading="confirming"
class="confirm-button"
>
我知道了
</a-button>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import MarkdownIt from 'markdown-it'
import { Service } from '@/api/services/Service'
interface Props {
visible: boolean
noticeData: Record<string, string>
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'confirmed'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.visible,
set: value => emit('update:visible', value),
})
const confirming = ref(false)
const activeNoticeKey = ref('')
// 初始化 markdown 解析器
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
})
// 获取公告标题列表
const notices = computed(() => Object.keys(props.noticeData))
// 当前公告索引
const currentNoticeIndex = computed(() => {
return notices.value.findIndex(title => title === activeNoticeKey.value)
})
// 渲染 markdown
const renderMarkdown = (content: string) => {
return md.render(content)
}
// 确认所有公告
const confirmNotices = async () => {
confirming.value = true
try {
const response = await Service.confirmNoticeApiInfoNoticeConfirmPost()
if (response.code === 200) {
// message.success('公告已确认')
visible.value = false
emit('confirmed')
} else {
message.error(response.message || '确认公告失败')
}
} catch (error) {
console.error('确认公告失败:', error)
message.error('确认公告失败,请重试')
} finally {
confirming.value = false
}
}
// 监听公告数据变化,设置默认选中第一个公告
watch(
() => props.noticeData,
newData => {
const titles = Object.keys(newData)
if (titles.length > 0 && !activeNoticeKey.value) {
activeNoticeKey.value = titles[0]
}
},
{ immediate: true }
)
// 监听弹窗显示状态,重置到第一个公告
watch(visible, newVisible => {
if (newVisible && notices.value.length > 0) {
activeNoticeKey.value = notices.value[0]
}
})
</script>
<style scoped>
.notice-modal :deep(.ant-modal-body) {
padding: 16px 24px;
max-height: 70vh;
overflow: hidden;
}
.notice-container {
display: flex;
flex-direction: column;
height: 60vh;
}
.notice-tabs {
flex: 1;
min-height: 0;
}
.notice-tabs :deep(.ant-tabs-tab-list) {
width: 200px;
}
.notice-tabs :deep(.ant-tabs-tab) {
text-align: left;
padding: 8px 16px;
}
.notice-tabs :deep(.ant-tabs-content-holder) {
max-height: 50vh;
overflow-y: auto;
padding-left: 16px;
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
/* 隐藏 WebKit 浏览器的滚动条 */
.notice-tabs :deep(.ant-tabs-content-holder)::-webkit-scrollbar {
display: none;
}
.notice-tab-pane {
height: 100%;
}
.notice-content {
padding: 0;
}
.markdown-content {
line-height: 1.6;
color: var(--ant-color-text);
}
.markdown-content :deep(h1) {
font-size: 24px;
font-weight: 600;
margin: 16px 0 12px 0;
color: var(--ant-color-text);
border-bottom: 2px solid var(--ant-color-border);
padding-bottom: 8px;
}
.markdown-content :deep(h2) {
font-size: 20px;
font-weight: 600;
margin: 16px 0 10px 0;
color: var(--ant-color-text);
}
.markdown-content :deep(h3) {
font-size: 16px;
font-weight: 600;
margin: 12px 0 8px 0;
color: var(--ant-color-text);
}
.markdown-content :deep(p) {
margin: 8px 0;
color: var(--ant-color-text);
}
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
margin: 8px 0;
padding-left: 24px;
}
.markdown-content :deep(li) {
margin: 4px 0;
color: var(--ant-color-text);
}
.markdown-content :deep(strong) {
font-weight: 600;
color: var(--ant-color-text);
}
.markdown-content :deep(code) {
padding: 3px 8px;
border-radius: 6px;
font-size: 1em;
color: var(--ant-color-primary);
border: 1px solid var(--ant-color-border-secondary);
font-weight: 600;
letter-spacing: 0.5px;
}
.markdown-content :deep(pre) {
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
}
.markdown-content :deep(pre code) {
background: none;
padding: 0;
border-radius: 0;
}
.markdown-content :deep(a) {
color: var(--ant-color-primary);
text-decoration: none;
}
.markdown-content :deep(a:hover) {
text-decoration: underline;
}
.markdown-content :deep(blockquote) {
border-left: 4px solid var(--ant-color-primary);
margin: 12px 0;
padding: 8px 16px;
color: var(--ant-color-text-secondary);
}
.notice-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ant-color-border);
}
.notice-pagination {
flex: 1;
}
.pagination-text {
color: var(--ant-color-text-tertiary);
font-size: 14px;
}
.notice-actions {
display: flex;
gap: 8px;
}
.confirm-button {
min-width: 100px;
height: 36px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.notice-modal :deep(.ant-modal) {
width: 95vw !important;
margin: 10px;
}
.notice-modal :deep(.ant-modal-body) {
padding: 12px 16px;
max-height: 60vh;
}
.notice-container {
height: 50vh;
}
.notice-tabs :deep(.ant-tabs-tab-list) {
width: 120px;
}
.notice-tabs :deep(.ant-tabs-content-holder) {
max-height: 40vh;
padding-left: 8px;
}
.notice-footer {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.notice-actions {
justify-content: center;
}
}
</style>

View File

@@ -3,6 +3,13 @@
<a-typography-title>{{ greeting }}</a-typography-title>
</div>
<!-- 公告模态框 -->
<NoticeModal
v-model:visible="noticeVisible"
:notice-data="noticeData"
@confirmed="onNoticeConfirmed"
/>
<div class="content">
<!-- 当期活动关卡 -->
<a-card
@@ -225,6 +232,7 @@ import { ref, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import { ReloadOutlined, ClockCircleOutlined, UserOutlined } from '@ant-design/icons-vue'
import { Service } from '@/api/services/Service'
import NoticeModal from '@/components/NoticeModal.vue'
import dayjs from 'dayjs'
interface ActivityInfo {
@@ -275,6 +283,10 @@ const activityData = ref<ActivityItem[]>([])
const resourceData = ref<ResourceItem[]>([])
const proxyData = ref<Record<string, ProxyInfo>>({})
// 公告系统相关状态
const noticeVisible = ref(false)
const noticeData = ref<Record<string, string>>({})
// 获取当前活动信息
const currentActivity = computed(() => {
if (!activityData.value.length) return null
@@ -318,7 +330,7 @@ const isLessThanTwoDays = (expireTime: string) => {
const expire = new Date(expireTime)
const now = new Date()
const remaining = expire.getTime() - now.getTime()
const twoDaysInMs = 20 * 24 * 60 * 60 * 1000
const twoDaysInMs = 2 * 24 * 60 * 60 * 1000
return remaining <= twoDaysInMs
} catch {
return false
@@ -442,8 +454,35 @@ const greeting = computed(() => {
}
})
// 获取公告信息
const fetchNoticeData = async () => {
try {
const response = await Service.getNoticeInfoApiInfoNoticeGetPost()
if (response.code === 200) {
// 检查是否需要显示公告
if (response.if_need_show && response.data && Object.keys(response.data).length > 0) {
// if (response.data && Object.keys(response.data).length > 0) {
noticeData.value = response.data
noticeVisible.value = true
}
} else {
console.warn('获取公告失败:', response.message)
}
} catch (error) {
console.error('获取公告失败:', error)
}
}
// 公告确认回调
const onNoticeConfirmed = () => {
noticeVisible.value = false
// message.success('公告已确认')
}
onMounted(() => {
fetchActivityData()
fetchNoticeData()
})
</script>