feat: 添加公告系统,支持动态显示和确认功能
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
339
frontend/src/components/NoticeModal.vue
Normal file
339
frontend/src/components/NoticeModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user