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

@@ -48,11 +48,13 @@
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@types/adm-zip": "^0.5.7",
"@types/markdown-it": "^14.1.2",
"adm-zip": "^0.5.16",
"ant-design-vue": "4.x",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"form-data": "^4.0.4",
"markdown-it": "^14.1.0",
"vue": "^3.5.17",
"vue-router": "4"
},

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>

View File

@@ -1055,6 +1055,30 @@ __metadata:
languageName: node
linkType: hard
"@types/linkify-it@npm:^5":
version: 5.0.0
resolution: "@types/linkify-it@npm:5.0.0"
checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
languageName: node
linkType: hard
"@types/markdown-it@npm:^14.1.2":
version: 14.1.2
resolution: "@types/markdown-it@npm:14.1.2"
dependencies:
"@types/linkify-it": "npm:^5"
"@types/mdurl": "npm:^2"
checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
languageName: node
linkType: hard
"@types/mdurl@npm:^2":
version: 2.0.0
resolution: "@types/mdurl@npm:2.0.0"
checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
languageName: node
linkType: hard
"@types/ms@npm:*":
version: 2.1.0
resolution: "@types/ms@npm:2.1.0"
@@ -2486,7 +2510,7 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.5.0":
"entities@npm:^4.4.0, entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
@@ -3023,6 +3047,7 @@ __metadata:
dependencies:
"@ant-design/icons-vue": "npm:^7.0.1"
"@types/adm-zip": "npm:^0.5.7"
"@types/markdown-it": "npm:^14.1.2"
"@types/node": "npm:22.17.1"
"@typescript-eslint/eslint-plugin": "npm:^8.38.0"
"@typescript-eslint/parser": "npm:^8.38.0"
@@ -3041,6 +3066,7 @@ __metadata:
eslint-plugin-prettier: "npm:^5.5.3"
eslint-plugin-vue: "npm:^10.4.0"
form-data: "npm:^4.0.4"
markdown-it: "npm:^14.1.0"
openapi-typescript-codegen: "npm:^0.29.0"
prettier: "npm:^3.6.2"
typescript: "npm:^5.9.2"
@@ -3824,6 +3850,15 @@ __metadata:
languageName: node
linkType: hard
"linkify-it@npm:^5.0.0":
version: 5.0.0
resolution: "linkify-it@npm:5.0.0"
dependencies:
uc.micro: "npm:^2.0.0"
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
languageName: node
linkType: hard
"locate-path@npm:^6.0.0":
version: 6.0.0
resolution: "locate-path@npm:6.0.0"
@@ -3957,6 +3992,22 @@ __metadata:
languageName: node
linkType: hard
"markdown-it@npm:^14.1.0":
version: 14.1.0
resolution: "markdown-it@npm:14.1.0"
dependencies:
argparse: "npm:^2.0.1"
entities: "npm:^4.4.0"
linkify-it: "npm:^5.0.0"
mdurl: "npm:^2.0.0"
punycode.js: "npm:^2.3.1"
uc.micro: "npm:^2.1.0"
bin:
markdown-it: bin/markdown-it.mjs
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
languageName: node
linkType: hard
"matcher@npm:^3.0.0":
version: 3.0.0
resolution: "matcher@npm:3.0.0"
@@ -3973,6 +4024,13 @@ __metadata:
languageName: node
linkType: hard
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
languageName: node
linkType: hard
"merge2@npm:^1.3.0":
version: 1.4.1
resolution: "merge2@npm:1.4.1"
@@ -4670,6 +4728,13 @@ __metadata:
languageName: node
linkType: hard
"punycode.js@npm:^2.3.1":
version: 2.3.1
resolution: "punycode.js@npm:2.3.1"
checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
languageName: node
linkType: hard
"punycode@npm:^2.1.0":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@@ -5424,6 +5489,13 @@ __metadata:
languageName: node
linkType: hard
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
version: 2.1.0
resolution: "uc.micro@npm:2.1.0"
checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
languageName: node
linkType: hard
"uglify-js@npm:^3.1.4":
version: 3.19.3
resolution: "uglify-js@npm:3.19.3"