1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

feat: 新增公告模块、数据加载hook与多项体验优化

- 新增公告全流程功能:首页公告轮播、公告列表页、公告详情页
- 新增统一数据加载状态hook useDataLoading与配套测试
- 重构配置加载逻辑,新增统一bootstrap静态配置拉取
- 新增维护模式、恋爱模块支持与验证码防刷功能
- 优化投票卡片、联系页、关于页UI样式
- 新增测试页面与全局玻璃卡片样式
- 修复markdown容器内边距与首页生命周期逻辑
This commit is contained in:
小莫唐尼
2026-09-04 02:57:25 +08:00
parent 902faa70e7
commit 5f22f4fa01
39 changed files with 4474 additions and 2244 deletions
@@ -1,77 +1,144 @@
<script lang="ts" setup>
/**
* 通知弹窗(源自旧项目 components/notify-dialog,新建复刻)
* wd-popup + mp-html 渲染 markdown 内容,可选跳转外链
* 通知/最新公告弹窗(源自旧项目 components/notify-dialog,2026-09-03 改造为
* plugin-uni-halo 最新公告自治宿主):挂载后自动拉取 GET /notices/latest,
* 「今日已看」命中(键 notice_latest_{name}_{yyyyMMdd})则不打扰;
* 「不再提醒/查看全文」写今日键;查看全文跳公告详情页。
* 放置:页面模板中直接 <uh-notify-dialog /> 即可(easycom)。
* UIUX 见 .docs/notice-module-client-design.md
*/
import { ref, watch } from 'vue'
import { checkIsUrl } from '@/utils/url'
import { markdownConfig } from '@/config/markdown'
import { onMounted, ref } from 'vue'
import { getNoticeLatest } from '@/api/uni-halo'
import { getCache, setCache } from '@/utils/storage'
import type { INoticeListVo } from '@/api/types/uni-halo'
const props = defineProps<{
show: boolean
title: string
content: string
url: string
}>()
const HIDDEN_KEY_PREFIX = 'notice_latest_hidden'
const emit = defineEmits<{
(e: 'on-change', show: boolean): void
}>()
const isShow = ref(false)
const notice = ref<INoticeListVo | null>(null)
const checking = ref(false)
const isShow = ref(props.show)
watch(() => props.show, (val) => {
isShow.value = val
})
function handleOnChange(e: unknown) {
isShow.value = false
emit('on-change', Boolean(e))
function todayKey(name: string): string {
const date = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${HIDDEN_KEY_PREFIX}_${name}_${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
}
function handleToWebview() {
if (checkIsUrl(props.url)) {
uni.navigateTo({
url: `/pages-blog/website/website?data=${JSON.stringify({
title: props.title,
url: encodeURIComponent(props.url),
})}`,
success: () => {
handleOnChange(false)
},
})
function formatDate(value?: string): string {
if (!value)
return ''
const date = new Date(value)
if (Number.isNaN(date.getTime()))
return ''
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
async function handleCheckLatest() {
if (checking.value)
return
checking.value = true
try {
const res = await getNoticeLatest()
const latest = res.data
if (!latest?.name || !latest.title)
return
// 今日已看过则不再打扰
if (getCache<string>(todayKey(latest.name)))
return
notice.value = latest
isShow.value = true
}
catch (err) {
console.error('获取最新公告失败', err)
}
finally {
checking.value = false
}
}
/** 仅关闭(不记录,下次进入可再弹) */
function handleClose() {
isShow.value = false
}
/** 今日不再提醒:记录后关闭 */
function handleDismissForever() {
if (notice.value?.name) {
setCache(todayKey(notice.value.name), '1')
}
isShow.value = false
}
/** 查看全文:记录今日已看并跳公告详情页 */
function handleViewAll() {
if (notice.value?.name) {
setCache(todayKey(notice.value.name), '1')
const detailName = notice.value.name
isShow.value = false
uni.navigateTo({ url: `/pages-blog/notice/detail?name=${detailName}` })
}
}
onMounted(() => {
handleCheckLatest()
})
</script>
<template>
<wd-popup v-model="isShow" width="80vw" position="center" custom-style="border-radius:12rpx;" @close="handleOnChange(false)">
<view class="uh-notify-dialog box-border w-[80vw] p-3">
<view class="text-overflow-2 mb-6 text-center font-bold">
{{ title }}
<wd-popup
v-model="isShow"
position="center"
width="80vw"
custom-style="border-radius:16rpx;"
@close="handleClose"
>
<view v-if="notice" class="uh-notify-dialog box-border w-[80vw] p-6">
<!-- 头部:标题 + 关闭 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<text class="text-[28rpx]">
📢
</text>
<text class="text-[30rpx] font-bold text-[#333]">
最新公告
</text>
<view
v-if="notice.typeDisplayName"
class="rounded px-1.5 py-0.5 text-[20rpx]"
:style="{
color: notice.typeColor || '#f83856',
backgroundColor: notice.typeColor ? `${notice.typeColor}1a` : '#fdeef1',
}"
>
{{ notice.typeDisplayName }}
</view>
</view>
<view class="flex h-8 w-8 items-center justify-center text-[#bbb]" @click="handleClose">
<wd-icon name="close" size="16px" />
</view>
</view>
<view class="content-box">
<scroll-view scroll-y :style="{ height: url ? '600rpx' : '680rpx' }">
<mp-html
class="evan-markdown"
lazy-load
:domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif"
scroll-table
selectable
:tag-style="markdownConfig.tagStyle"
:container-style="markdownConfig.containStyle"
:content="content"
:markdown="true"
:show-line-number="true"
:show-language-name="true"
copy-by-long-press
/>
</scroll-view>
<!-- 内容 -->
<view class="mt-4">
<view class="text-[32rpx] font-bold leading-snug text-[#222]">
{{ notice.title }}
</view>
<view v-if="notice.summary" class="mt-3 text-[26rpx] leading-relaxed text-[#666]">
{{ notice.summary }}
</view>
<view v-if="notice.publishTime" class="mt-3 text-[22rpx] text-[#bbb]">
{{ formatDate(notice.publishTime) }}
</view>
</view>
<view v-if="url" class="mt-6 flex justify-center">
<wd-button size="small" type="primary" @click="handleToWebview">
点击跳转内容链接
<!-- 操作 -->
<view class="mt-6 flex items-center justify-between border-t border-[#f5f5f5] pt-4">
<view class="text-[24rpx] text-[#999]" @click="handleDismissForever">
今日不再提醒
</view>
<wd-button size="small" type="primary" round @click="handleViewAll">
查看全文
</wd-button>
</view>
</view>
@@ -80,6 +147,6 @@ function handleToWebview() {
<style scoped lang="scss">
.uh-notify-dialog {
/* 无额外样式,布局全部由 UnoCSS 原子类实现 */
/* 布局全部由 UnoCSS 原子类实现 */
}
</style>