From 5f22f4fa019e466763e8ef53f92f3b39b0e20bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8E=AB=E5=94=90=E5=B0=BC?= Date: Fri, 4 Sep 2026 02:57:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=85=AC=E5=91=8A?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E3=80=81=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?hook=E4=B8=8E=E5=A4=9A=E9=A1=B9=E4=BD=93=E9=AA=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增公告全流程功能:首页公告轮播、公告列表页、公告详情页 - 新增统一数据加载状态hook useDataLoading与配套测试 - 重构配置加载逻辑,新增统一bootstrap静态配置拉取 - 新增维护模式、恋爱模块支持与验证码防刷功能 - 优化投票卡片、联系页、关于页UI样式 - 新增测试页面与全局玻璃卡片样式 - 修复markdown容器内边距与首页生命周期逻辑 --- .trae/rules/git-commit-message.md | 5 + src/App.vue | 91 +- src/api/types/uni-halo.ts | 71 + src/api/uni-halo.ts | 76 +- .../uh-album-unlock-modal.vue | 98 +- src/components/uh-button/uh-button.vue | 16 + .../uh-data-loading/uh-data-loading.vue | 93 +- .../uh-home-notify/uh-home-notify.vue | 107 ++ .../uh-mini-link-apply/uh-mini-link-apply.vue | 83 +- .../uh-notify-dialog/uh-notify-dialog.vue | 187 ++- src/components/uh-vote-card/uh-vote-card.vue | 29 +- src/config/markdown.ts | 2 +- src/hooks/useDataLoading.test.ts | 84 ++ src/hooks/useDataLoading.ts | 69 + src/hooks/useDataLoadingStatus.ts | 25 +- src/pages-blog/about/about.vue | 68 +- src/pages-blog/archives/archives.vue | 141 +- .../article-detail/article-detail.vue | 1185 +++++++++-------- src/pages-blog/contact/contact.vue | 81 +- src/pages-blog/friend-links/friend-links.vue | 103 +- src/pages-blog/love/love.vue | 80 +- .../moment-detail/moment-detail.vue | 353 ++--- .../moment-detail/moment-detail.vue.glass.bak | 337 +++++ src/pages-blog/notice/detail.vue | 176 +++ src/pages-blog/notice/notice.vue | 261 ++++ src/pages-blog/search/search.vue | 81 +- src/pages-blog/setting/setting.vue | 178 +-- src/pages-blog/test/test.vue | 14 + src/pages-blog/votes/votes.vue | 16 +- src/pages/index/index.vue | 22 +- src/pages/maintenance/maintenance.vue | 247 ++++ src/pages/tabbar/about/about.vue | 612 +++++---- src/pages/tabbar/category/category.vue | 320 ++--- src/pages/tabbar/home/home.vue | 23 +- src/pages/tabbar/moments/moments.vue | 795 +++++------ .../tabbar/moments/moments.vue.glass.bak | 423 ++++++ src/store/appConfig.ts | 71 +- src/style/index.scss | 1 + src/style/uhemoji-iconfont.css | 94 ++ 39 files changed, 4474 insertions(+), 2244 deletions(-) create mode 100644 .trae/rules/git-commit-message.md create mode 100644 src/components/uh-button/uh-button.vue create mode 100644 src/components/uh-home-notify/uh-home-notify.vue create mode 100644 src/hooks/useDataLoading.test.ts create mode 100644 src/hooks/useDataLoading.ts create mode 100644 src/pages-blog/moment-detail/moment-detail.vue.glass.bak create mode 100644 src/pages-blog/notice/detail.vue create mode 100644 src/pages-blog/notice/notice.vue create mode 100644 src/pages-blog/test/test.vue create mode 100644 src/pages/maintenance/maintenance.vue create mode 100644 src/pages/tabbar/moments/moments.vue.glass.bak create mode 100644 src/style/uhemoji-iconfont.css diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..f8be550 --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,5 @@ +--- +alwaysApply: true +scene: git_message +--- +使用中文编写提交信息 diff --git a/src/App.vue b/src/App.vue index a5c4e4b..a18ce98 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,57 +1,56 @@ + \ No newline at end of file diff --git a/src/api/types/uni-halo.ts b/src/api/types/uni-halo.ts index 159fef3..8f7fe77 100644 --- a/src/api/types/uni-halo.ts +++ b/src/api/types/uni-halo.ts @@ -124,6 +124,29 @@ export interface IAppConfig { articleCardType?: string avatarRadius?: boolean } + /** + * 维护模式(additive,2026-09-04 插件端 GeneralConfig.spec.maintenance 经 getConfigs + * 下发;仅 scheduled/active 时存在,键缺失=未维护或已到点自动结束) + */ + maintenance?: IPublicMaintenance + [key: string]: unknown +} + +/** + * getConfigs 顶层 maintenance 键(插件端输出;status 判定权威在服务端,客户端仅据此 + * 展示维护页与倒计时,设计见插件 .docs/maintenance-config-design.md) + */ +export interface IPublicMaintenance { + /** scheduled 维护预告(倒计时至 startTime)/ active 维护中(倒计时至 endTime) */ + status: 'scheduled' | 'active' + /** 维护页标题 */ + title?: string + /** 维护说明(富文本 HTML,mp-html 渲染) */ + description?: string + /** 维护开始时间(RFC3339 UTC 字符串) */ + startTime?: string + /** 预计恢复时间(RFC3339 UTC 字符串) */ + endTime?: string [key: string]: unknown } @@ -269,6 +292,54 @@ export interface ICommentCaptcha { [key: string]: unknown } +/* ---------- 通知公告(plugin-uni-halo notice,2026-09-03 客户端接入) ---------- */ + +/** 公告公开列表项(脱敏,不含 content;内嵌类型信息 typeDisplayName/typeColor) */ +export interface INoticeListVo { + name?: string + title?: string + summary?: string + cover?: string + link?: string + typeName?: string + typeDisplayName?: string + typeColor?: string + priority?: number + /** 发布时间(spec.publishTime,ISO-8601) */ + publishTime?: string +} + +/** 公告公开分页列表响应(ListResult 形态) */ +export interface INoticeListRes { + page?: number + size?: number + total?: number + items: INoticeListVo[] +} + +/** 公告详情 spec(完整 Notice + 内嵌 typeDisplayName/typeColor) */ +export interface INoticeDetailSpec { + title?: string + /** 富文本 HTML(仅详情返回) */ + content?: string + summary?: string + cover?: string + link?: string + typeName?: string + status?: string + priority?: number + publishTime?: string + typeDisplayName?: string + typeColor?: string +} + +/** 公告详情(完整 extension 对象: metadata + spec) */ +export interface INoticeDetail { + metadata?: { name?: string, [key: string]: unknown } + spec?: INoticeDetailSpec + [key: string]: unknown +} + /* ---------- 恋爱模块(plugin-uni-halo love) ---------- */ export interface ILoveConfig { diff --git a/src/api/uni-halo.ts b/src/api/uni-halo.ts index ef99dfd..7f93a10 100644 --- a/src/api/uni-halo.ts +++ b/src/api/uni-halo.ts @@ -34,6 +34,9 @@ import type { IMiniProgramLinkGroupedRes, IMiniProgramLinkGroupOption, IMiniProgramLinkSubmissionForm, + INoticeDetail, + INoticeListRes, + INoticeListVo, IQRCodeInfo, IRestrictReadCheckReq, IRestrictReadCheckRes, @@ -109,7 +112,7 @@ export function getCommentWidgetConfig() { * 检查更新(uhalo-upgrade 插件对接,插件本体引入后业务侧入口) */ export function checkVersion(baseUrl: string) { - return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/checkVersion', { + return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/upgrade/checkVersion', { params: { baseUrl }, meta: { requestFrom: RequestFrom.Halo }, }) @@ -124,6 +127,68 @@ export function getQRCodeInfo(key: string) { }) } +/* ==================== 防刷验证码(plugin-uni-halo,2026-09-03 客户端接入) ==================== */ + +/** 防刷验证码(服务端 captcha/generate 返回;受保护写接口 403 时响应体亦附带新验证码) */ +export interface IPluginCaptcha { + id: string + imageBase64: string +} + +/** 受保护写接口携带的验证码 query(未触发时省略,触发后每次提交都需携带,一次性) */ +export interface ICaptchaQuery { + captchaId?: string + captchaCode?: string +} + +/** + * 获取防刷验证码(公开;适用 POST /mini-program-links/submissions 与 + * /love-albums/{name}/unlock;站点关闭验证码或该功能不在生效范围时无需携带) + */ +export function getPluginCaptcha() { + return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/captcha/generate', { + meta: { requestFrom: RequestFrom.Halo }, + }) +} + +/** 仅当携带有效验证码时拼接 query 参数 */ +export function buildCaptchaQuery(captcha?: ICaptchaQuery | null): ICaptchaQuery | undefined { + if (!captcha || !captcha.captchaId || !captcha.captchaCode) + return undefined + return { captchaId: captcha.captchaId, captchaCode: captcha.captchaCode } +} + +/* ==================== 通知公告(plugin-uni-halo,2026-09-03 客户端接入) ==================== */ + +/** + * 公告分页列表(公开,仅已发布;脱敏不含 content,内嵌类型信息)。 + * 注意:公开接口当前仅支持分页,类型筛选/排序由页面本地聚合处理(公告量小)。 + */ +export function getNotices(params: { page?: number, size?: number }) { + return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/notices', { + params, + meta: { requestFrom: RequestFrom.Halo }, + }) +} + +/** + * 最新一条已发布公告(公开;无数据返回 null,供首页/我的弹窗) + */ +export function getNoticeLatest() { + return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/notices/latest', { + meta: { requestFrom: RequestFrom.Halo }, + }) +} + +/** + * 公告详情(公开;含 content 富文本 HTML 与内嵌类型信息;不存在/删除中返回 404) + */ +export function getNoticeDetail(name: string) { + return http.Get>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/notices/${name}`, { + meta: { requestFrom: RequestFrom.Halo }, + }) +} + /* ==================== 恋爱模块 ==================== */ /** @@ -158,13 +223,14 @@ export function getLoveAlbumByName(name: string, params: ILoveAlbumListReq) { /** * 密码解锁相册 */ -export function unlockAlbum(name: string, password: string) { +export function unlockAlbum(name: string, password: string, captcha?: ICaptchaQuery | null) { return http.Post>( `/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}/unlock`, { password, }, { + params: buildCaptchaQuery(captcha), meta: { requestFrom: RequestFrom.Halo }, }, ) @@ -233,15 +299,17 @@ export function getMiniProgramLinkDetail(name: string) { } /** - * 提交小程序链接申请(公开接口,落库为待审核;受 linkConfig.submissionEnabled 开关控制) + * 提交小程序链接申请(公开接口,落库为待审核;受 linkConfig.submissionEnabled 开关 + * 与防刷验证码 captchaConfig 控制) */ -export function submitMiniProgramLinkApplication(data: IMiniProgramLinkSubmissionForm) { +export function submitMiniProgramLinkApplication(data: IMiniProgramLinkSubmissionForm, captcha?: ICaptchaQuery | null) { return http.Post>( '/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/mini-program-links/submissions', { spec: data, }, { + params: buildCaptchaQuery(captcha), meta: { requestFrom: RequestFrom.Halo }, }, ) diff --git a/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue b/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue index 18125d0..26b58dc 100644 --- a/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue +++ b/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue @@ -1,10 +1,12 @@ + + \ No newline at end of file diff --git a/src/components/uh-data-loading/uh-data-loading.vue b/src/components/uh-data-loading/uh-data-loading.vue index a6a68da..84257af 100644 --- a/src/components/uh-data-loading/uh-data-loading.vue +++ b/src/components/uh-data-loading/uh-data-loading.vue @@ -1,41 +1,84 @@ + + diff --git a/src/components/uh-home-notify/uh-home-notify.vue b/src/components/uh-home-notify/uh-home-notify.vue new file mode 100644 index 0000000..a9d134b --- /dev/null +++ b/src/components/uh-home-notify/uh-home-notify.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/components/uh-mini-link-apply/uh-mini-link-apply.vue b/src/components/uh-mini-link-apply/uh-mini-link-apply.vue index 0ed62f9..28eec0f 100644 --- a/src/components/uh-mini-link-apply/uh-mini-link-apply.vue +++ b/src/components/uh-mini-link-apply/uh-mini-link-apply.vue @@ -1,10 +1,12 @@ diff --git a/src/pages-blog/contact/contact.vue b/src/pages-blog/contact/contact.vue index 8ee73b9..3d43285 100644 --- a/src/pages-blog/contact/contact.vue +++ b/src/pages-blog/contact/contact.vue @@ -1,7 +1,7 @@ - - diff --git a/src/pages-blog/friend-links/friend-links.vue b/src/pages-blog/friend-links/friend-links.vue index a35a379..36b0fa3 100644 --- a/src/pages-blog/friend-links/friend-links.vue +++ b/src/pages-blog/friend-links/friend-links.vue @@ -320,10 +320,10 @@ onReachBottom(() => { - - diff --git a/src/pages-blog/love/love.vue b/src/pages-blog/love/love.vue index 80d6093..17392aa 100644 --- a/src/pages-blog/love/love.vue +++ b/src/pages-blog/love/love.vue @@ -4,8 +4,7 @@ * 情侣信息 + 恋爱计时 + 功能导航(恋爱故事/相册/清单) */ import { computed, onBeforeUnmount, ref } from 'vue' -import { onLoad } from '@dcloudio/uni-app' -import { getLoveConfig } from '@/api/uni-halo' +import { onLoad, onShow } from '@dcloudio/uni-app' import { useAppConfigStore } from '@/store/appConfig' import { checkAvatarUrl, checkImageUrl } from '@/utils/url' @@ -70,41 +69,45 @@ const loveWrapStyle = computed(() => ({ })) /* ---------------- 数据加载 ---------------- */ -async function handleGetLoveConfig() { - try { - const loveConfigRes = await getLoveConfig() - if (loveConfigRes) { - loveConfig.value = { - ...loveConfig.value, - ...loveConfigRes, - } - } - // 同时从 getConfigs 获取模块开关和图片配置 - const appConfigs = appConfigStore.configs - const loveModuleConfig = appConfigs.loveConfig as Partial | undefined - if (loveModuleConfig) { - loveConfig.value = { - ...loveConfig.value, - pageImages: loveModuleConfig.pageImages || loveConfig.value.pageImages, - ourStory: loveModuleConfig.ourStory || loveConfig.value.ourStory, - lovePhoto: loveModuleConfig.lovePhoto || loveConfig.value.lovePhoto, - loveDaily: loveModuleConfig.loveDaily || loveConfig.value.loveDaily, - } - } - initList() - handleInitLoveDayCount() +/** + * 恋爱配置数据统一来自 appConfig store(静态配置一次性拉取,设计见 + * .docs/static-config-unified-fetch-design.md): + * - loveConfig(公开 /love-config 内容: enabled/纪念日/恋人信息, bootstrap 已拉取) + * - configs.loveConfig(模块开关与页面图片, getConfigs 合成下发) + * 页面不再自行发起 /love-config 请求,也不再需要接口失败降级分支(store 兜底默认)。 + */ +function syncLoveConfigFromStore() { + const storeLove = appConfigStore.loveConfig + const appConfigs = appConfigStore.configs + + loveConfig.value = { + ...loveConfig.value, + ...(storeLove.loveDateTitle ? { loveDateTitle: storeLove.loveDateTitle } : {}), + ...(storeLove.loveDate ? { loveDate: storeLove.loveDate } : {}), + ...(storeLove.enabled !== undefined ? { enabled: storeLove.enabled } : {}), + loveInfo: { + ...loveConfig.value.loveInfo, + ...(storeLove.loveInfo || {}), + }, } - catch (e) { - console.error('获取恋爱配置失败', e) - // 降级:从旧配置读取 - const appConfigs = appConfigStore.configs - const loveModuleConfig = appConfigs.loveConfig as ILoveConfigPage | undefined - if (loveModuleConfig) { - loveConfig.value = loveModuleConfig - initList() - handleInitLoveDayCount() + + // 从 getConfigs.loveConfig 取模块开关与页面图片(缺省保留默认) + const loveModuleConfig = appConfigs.loveConfig as Partial | undefined + if (loveModuleConfig) { + loveConfig.value = { + ...loveConfig.value, + pageImages: loveModuleConfig.pageImages || loveConfig.value.pageImages, + ourStory: loveModuleConfig.ourStory || loveConfig.value.ourStory, + lovePhoto: loveModuleConfig.lovePhoto || loveConfig.value.lovePhoto, + loveDaily: loveModuleConfig.loveDaily || loveConfig.value.loveDaily, } } + + initList() + // 未配置纪念日(loveDate 为空)不启动倒计时,避免 NaN + if (loveConfig.value.loveDate) { + handleInitLoveDayCount() + } } function initList() { @@ -169,7 +172,14 @@ function handleToPage(pageName: string) { /* ---------------- 生命周期 ---------------- */ onLoad(() => { uni.setNavigationBarTitle({ title: '恋爱日记' }) - handleGetLoveConfig() + // 先用 store 已有数据立即渲染(index 启动已 bootstrap 或持久化缓存恢复) + syncLoveConfigFromStore() +}) + +// 每次进入页面:静态配置 TTL 内不重复请求(bootstrap 内部判定),刷新后重新合成视图 +onShow(async () => { + await appConfigStore.bootstrap() + syncLoveConfigFromStore() }) onBeforeUnmount(() => { diff --git a/src/pages-blog/moment-detail/moment-detail.vue b/src/pages-blog/moment-detail/moment-detail.vue index 7821709..3758a8c 100644 --- a/src/pages-blog/moment-detail/moment-detail.vue +++ b/src/pages-blog/moment-detail/moment-detail.vue @@ -2,10 +2,11 @@ /** * 瞬间详情页(源自旧项目 pagesA/moment-detail,新建复刻) * 展示瞬间内容(mp-html) + 图片/音频/视频附件 + * 设计:日记页(大字日期刊头 + 宽松留白),与列表页的信息流形成两种性格 + * 试点:useDataLoading 状态机接管加载四态 */ import { computed, ref } from 'vue' import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' -import dayjs from 'dayjs' import { getMomentByName } from '@/api/halo' import { useAppConfigStore } from '@/store/appConfig' import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url' @@ -13,12 +14,15 @@ import { generateUUID } from '@/utils/uuid' import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { randomTagColor } from '@/utils/random' import { markdownConfig } from '@/config/markdown' +import { useDataLoading } from '@/hooks/useDataLoading' import type { IMoment } from '@/api/types/halo' definePage({ style: { navigationBarTitleText: '瞬间详情', enablePullDownRefresh: true, + // 下拉/回弹露出的窗口底色对齐页面底色 + backgroundColor: '#f6f3ee', }, }) @@ -42,16 +46,17 @@ const siteName = computed(() => { }) /* ---------------- 状态 ---------------- */ -const loading = ref<'loading' | 'success' | 'error'>('loading') const queryName = ref('') -const moment = ref<(IMoment & { +const videoContexts = ref>({}) +const currentVideoId = ref(null) + +/** 瞬间卡片(medium 按类型拆为 images/videos/audios + 正文 tag 清理) */ +type MomentCard = IMoment & { images?: { type?: string, url: string }[] videos?: { id?: string, url: string }[] audios?: { type?: string, url: string }[] spec: IMoment['spec'] & { newHtml?: string } -}) | null>(null) -const videoContexts = ref>({}) -const currentVideoId = ref(null) +} /** 移除 tag 链接 */ function removeTagLinksCompletely(htmlString: string): string { @@ -59,56 +64,63 @@ function removeTagLinksCompletely(htmlString: string): string { return htmlString.replace(regex, '') } -function tagColor(): string { - if (!calcUseTagRandomColor.value) - return 'blue' - return randomTagColor() +/** 瞬间详情 → 展示卡片(作者兜底 + medium 拆分 + tag 清理) */ +function buildMomentCard(res: IMoment): MomentCard { + const medium = (res.spec.content?.medium || []) + .map(x => ({ ...x, url: x.url || '' })) + const owner = res.owner + return { + ...res, + // 无顶层 owner(如个别历史接口)时兜底为博主信息 + owner: owner?.displayName + ? owner + : { displayName: bloggerInfo.value.nickname || '', name: bloggerInfo.value.nickname || '', avatar: bloggerInfo.value.avatar }, + spec: { + ...res.spec, + newHtml: removeTagLinksCompletely(res.spec.content?.html || ''), + }, + images: medium.filter(x => x.type === 'PHOTO').map(x => ({ ...x, url: checkThumbnailUrl(x.url, true) })), + videos: medium.filter(x => x.type === 'VIDEO').map(x => ({ ...x, id: generateUUID() })), + audios: medium.filter(x => x.type === 'AUDIO'), + } } -function formatTime(time?: string): string { - // 与旧项目一致:yyyy年MM月dd日 星期w - return time ? formatTimeUtil({ d: time, f: 'yyyy年MM月dd日 星期w' }) : '' -} - -/* ---------------- 数据加载 ---------------- */ -async function handleGetData() { - loading.value = 'loading' - try { +/* ---------------- 数据加载(useDataLoading 试点:状态由 hook 接管) ---------------- */ +const { data: moment, status, run: loadMoment } = useDataLoading( + async (): Promise => { const res = await getMomentByName(queryName.value) uni.setNavigationBarTitle({ title: '瞬间详情' }) + return buildMomentCard(res.data) + }, + { + onSuccess: (card) => { + nextTick(() => { + createVideoContexts(card.videos || []) + }) + }, + onError: () => { + uni.setNavigationBarTitle({ title: '瞬间详情' }) + }, + }, +) - const medium = (res.data.spec.content?.medium || []) - .map(x => ({ ...x, url: x.url || '' })) - const owner = res.data.owner - const tempResult = { - ...res.data, - // 无顶层 owner(如个别历史接口)时兜底为博主信息 - owner: owner?.displayName - ? owner - : { displayName: bloggerInfo.value.nickname || '', name: bloggerInfo.value.nickname || '', avatar: bloggerInfo.value.avatar }, - spec: { - ...res.data.spec, - newHtml: removeTagLinksCompletely(res.data.spec.content?.html || ''), - }, - images: medium.filter(x => x.type === 'PHOTO').map(x => ({ ...x, url: checkThumbnailUrl(x.url, true) })), - videos: medium.filter(x => x.type === 'VIDEO').map(x => ({ ...x, id: generateUUID() })), - audios: medium.filter(x => x.type === 'AUDIO'), - } - moment.value = tempResult - loading.value = 'success' +/** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */ +const calcTagColors = computed(() => { + const tags = moment.value?.spec.tags || [] + return tags.map(() => (calcUseTagRandomColor.value ? randomTagColor() : '#4d7c0f')) +}) - nextTick(() => { - createVideoContexts(tempResult.videos || []) - }) - } - catch (err) { - console.error('获取瞬间详情失败', err) - loading.value = 'error' - } - finally { - uni.stopPullDownRefresh() - } -} +/** 刊头大字日期(如 09月03日) */ +const calcMastheadDate = computed(() => { + const time = moment.value?.spec.releaseTime + return time ? formatTimeUtil({ d: time, f: 'MM月dd日' }) : '' +}) + +/** 刊头辅助信息(如 2026年 · 星期四) */ +const calcMastheadMeta = computed(() => { + const time = moment.value?.spec.releaseTime + return time ? formatTimeUtil({ d: time, f: 'yyyy年 · 星期w' }) : '' +}) /* ---------------- 视频互斥 ---------------- */ function createVideoContexts(videos: { id?: string }[]) { @@ -165,13 +177,14 @@ function handleToTopPage(duration = 500) { onLoad((options) => { uni.setNavigationBarTitle({ title: '瞬间加载中...' }) queryName.value = options?.name || '' - handleGetData() + loadMoment() }) -onPullDownRefresh(() => { +onPullDownRefresh(async () => { videoContexts.value = {} currentVideoId.value = null - handleGetData() + await loadMoment() + uni.stopPullDownRefresh() }) onShareAppMessage(() => ({ @@ -186,131 +199,139 @@ onShareTimeline(() => ({ - - diff --git a/src/pages-blog/moment-detail/moment-detail.vue.glass.bak b/src/pages-blog/moment-detail/moment-detail.vue.glass.bak new file mode 100644 index 0000000..2257916 --- /dev/null +++ b/src/pages-blog/moment-detail/moment-detail.vue.glass.bak @@ -0,0 +1,337 @@ + + + diff --git a/src/pages-blog/notice/detail.vue b/src/pages-blog/notice/detail.vue new file mode 100644 index 0000000..5d0611a --- /dev/null +++ b/src/pages-blog/notice/detail.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/pages-blog/notice/notice.vue b/src/pages-blog/notice/notice.vue new file mode 100644 index 0000000..72939da --- /dev/null +++ b/src/pages-blog/notice/notice.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/pages-blog/search/search.vue b/src/pages-blog/search/search.vue index 6ca92ff..64f7df9 100644 --- a/src/pages-blog/search/search.vue +++ b/src/pages-blog/search/search.vue @@ -4,7 +4,7 @@ * 功能:关键词搜索文章/瞬间,结果列表展示 */ import { computed, ref } from 'vue' -import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app' +import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' import { getPostListByKeyword } from '@/api/halo' import { useAppConfigStore } from '@/store/appConfig' import { usePluginAvailable } from '@/utils/plugin' @@ -43,23 +43,15 @@ const dataList = ref<{ updateTimestamp?: string }[]>([]) -/* ---------------- 动画(对应旧版 mixin calcAniWait/fnResetSetAniWaitIndex) ---------------- */ -const aniWaitIndex = ref(0) - -function resetAniWaitIndex() { - aniWaitIndex.value = 0 -} - -/** 计算列表项动画等待(每 10 项重置一轮,每项递增 50ms) */ -function calcAniWait(index: number): number { - if ((index + 1) % 10 === 0) { - aniWaitIndex.value = 1 - } - else { - aniWaitIndex.value += 1 - } - return aniWaitIndex.value * 50 -} +/* ---------------- 动画(对应旧版 mixin calcAniWait) ---------------- */ +/** 预计算列表项入场延迟(每 10 项重置一轮,每项递增 50ms);必须在渲染外算好,渲染中修改响应式状态会导致递归更新 */ +const calcAniDelays = computed(() => { + let wait = 0 + return dataList.value.map((_, index) => { + wait = (index + 1) % 10 === 0 ? 1 : wait + 1 + return wait * 50 + }) +}) /* ---------------- 搜索 ---------------- */ async function handleGetData() { @@ -83,7 +75,6 @@ async function handleGetData() { } function handleOnSearch() { - resetAniWaitIndex() if (!queryParams.value.keyword) { dataList.value = [] loading.value = 'success' @@ -131,7 +122,6 @@ function handleToTopPage(duration = 500) { /* ---------------- 生命周期 ---------------- */ onLoad(async () => { - resetAniWaitIndex() uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId) if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() @@ -146,10 +136,6 @@ onLoad(async () => { } }) -onShow(() => { - resetAniWaitIndex() -}) - onPullDownRefresh(() => { if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() @@ -160,7 +146,7 @@ onPullDownRefresh(() => { - - diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 0f58470..bc8dd67 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -26,9 +26,9 @@ const homePagePath = '/pages/tabbar/home/home' const articleDetailPath = '/pages-blog/article-detail/article-detail' // 本地开发快速跳转页面,发布请置为 false -const DEV_MODE = false +const DEV_MODE = true const DEV_TO_TYPE = 'page' as 'page' | 'tabbar' -const DEV_TO_PATH = `${articleDetailPath}?name=01a057b2-3200-74af-8afe-28a054092e82` +const DEV_TO_PATH = `/pages-blog/test/test` /* ---------------- 状态 ---------------- */ const appConfigStore = useAppConfigStore() @@ -57,11 +57,6 @@ async function getPostIdByQRCode(key: string): Promise { return null } -/** 获取审核模式数据(公开接口 /audit-data,enabled 联动设置页开关) */ -async function handleAuditMode() { - await appConfigStore.fetchAuditData() -} - onLoad(async (options) => { // 本地开发,快速跳转页面,发布请设置 DEV_MODE = false if (DEV_MODE && DEV_TO_PATH) { @@ -78,10 +73,11 @@ onLoad(async (options) => { if (!(await handleCheckPluginAvailable())) return - // 获取配置 + // 获取配置(统一 bootstrap: getConfigs + audit-data + love-config 并行一次; + // TTL 内直接返回缓存。设计见 .docs/static-config-unified-fetch-design.md) try { - const res = await appConfigStore.fetchConfigs() - if (!res) { + const { ok } = await appConfigStore.bootstrap() + if (!ok) { uni.switchTab({ url: homePagePath }) return } @@ -98,10 +94,8 @@ onLoad(async (options) => { } } - // 审计模式数据(公开接口 /audit-data) - await handleAuditMode() - - // 两层偏好合并:应用站点默认(L0)到 setting store(内部合并本地差异,含旧数据迁移) + // 两层偏好合并:应用站点默认(L0)到 setting store(内部合并本地差异,含旧数据迁移); + // 审核模式数据已随 bootstrap 拉取(auditData/auditModeEnabled 即可用) settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs)) // 启动页已下线(v2.2 ⑤):直接进首页 diff --git a/src/pages/maintenance/maintenance.vue b/src/pages/maintenance/maintenance.vue new file mode 100644 index 0000000..8c0648c --- /dev/null +++ b/src/pages/maintenance/maintenance.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/src/pages/tabbar/about/about.vue b/src/pages/tabbar/about/about.vue index 2896028..c61cb8c 100644 --- a/src/pages/tabbar/about/about.vue +++ b/src/pages/tabbar/about/about.vue @@ -1,343 +1,329 @@ - - + + + {{ copyrightConfig.content }} + + + \ No newline at end of file diff --git a/src/pages/tabbar/category/category.vue b/src/pages/tabbar/category/category.vue index e57f189..78639b2 100644 --- a/src/pages/tabbar/category/category.vue +++ b/src/pages/tabbar/category/category.vue @@ -1,183 +1,183 @@ diff --git a/src/pages/tabbar/home/home.vue b/src/pages/tabbar/home/home.vue index 284f6a5..25dcdd7 100644 --- a/src/pages/tabbar/home/home.vue +++ b/src/pages/tabbar/home/home.vue @@ -1,5 +1,5 @@ - + @@ -225,4 +223,5 @@ + \ No newline at end of file diff --git a/src/pages/tabbar/moments/moments.vue b/src/pages/tabbar/moments/moments.vue index 5f57c3e..e4e6a98 100644 --- a/src/pages/tabbar/moments/moments.vue +++ b/src/pages/tabbar/moments/moments.vue @@ -1,495 +1,390 @@ + + \ No newline at end of file diff --git a/src/pages/tabbar/moments/moments.vue.glass.bak b/src/pages/tabbar/moments/moments.vue.glass.bak new file mode 100644 index 0000000..e96a4e7 --- /dev/null +++ b/src/pages/tabbar/moments/moments.vue.glass.bak @@ -0,0 +1,423 @@ + + + diff --git a/src/store/appConfig.ts b/src/store/appConfig.ts index 3a919e3..c303812 100644 --- a/src/store/appConfig.ts +++ b/src/store/appConfig.ts @@ -4,7 +4,7 @@ */ import { defineStore } from 'pinia' import { computed, ref } from 'vue' -import { getAppConfigs, getAuditData } from '@/api/uni-halo' +import { getAppConfigs, getAuditData, getLoveConfig } from '@/api/uni-halo' import { DefaultAppConfigs } from '@/config/appConfig' import { deepMerge } from '@/utils/merge' import { setCache } from '@/utils/storage' @@ -14,6 +14,26 @@ import type { IAppConfig, IAuditDataResult } from '@/api/types/uni-halo' const APP_TOKENS_KEY = 'APP_TOKENS' /** 合并后配置缓存 key(与 utils/url.ts / api/uni-halo.ts 的 APP_GLOBAL_CONFIGS 读取保持一致) */ const APP_GLOBAL_CONFIGS_KEY = 'APP_GLOBAL_CONFIGS' +/** + * 恋爱配置内容(公开 GET /love-config 返回: enabled + 纪念日/恋人信息; + * 恋爱开关与页面图片仍在 configs.loveConfig, 由 love.vue 组合读取) + */ +export interface IStaticLoveConfig { + enabled?: boolean + loveDateTitle?: string + loveDate?: string + loveInfo?: { + boyNickname?: string + boyAvatar?: string + girlNickname?: string + girlAvatar?: string + } +} +/** + * 静态配置后台刷新间隔(ms): onShow 距上次拉取在 TTL 内则不再重复请求(设计见 + * .docs/static-config-unified-fetch-design.md) + */ +const STATIC_TTL = 5 * 60 * 1000 export const useAppConfigStore = defineStore( 'appConfig', @@ -23,11 +43,18 @@ export const useAppConfigStore = defineStore( const auditData = ref({ enabled: false }) /** 审核模式开关(单一数据源:公开接口返回的 enabled) */ const auditModeEnabled = computed(() => !!auditData.value.enabled) + /** 维护模式信息(configs 顶层 additive maintenance 键;undefined=未维护/已到点自动结束) */ + const maintenance = computed(() => configs.value.maintenance) + /** 恋爱配置内容(公开 /love-config;bootstrap 统一拉取,随 store persist 持久化) */ + const loveConfig = ref({ enabled: false }) + /** 最近一次静态配置拉取时间戳(ms,0=从未拉取;TTL 判定依据) */ + const fetchedAt = ref(0) /** 重置为默认配置 */ const setDefaultAppSettings = () => { configs.value = JSON.parse(JSON.stringify(DefaultAppConfigs)) auditData.value = { enabled: false } + loveConfig.value = { enabled: false } } /** 获取应用配置(与默认值深合并,并存储 token) */ @@ -71,13 +98,55 @@ export const useAppConfigStore = defineStore( } } + /** 获取恋爱配置内容(公开 /love-config) */ + const fetchLoveConfig = async () => { + try { + const res = await getLoveConfig() + const body = res.data as unknown as IStaticLoveConfig | undefined + loveConfig.value = body && Object.keys(body).length > 0 + ? body + : { enabled: false } + return loveConfig.value + } + catch (err) { + console.error('获取恋爱配置失败', err) + loveConfig.value = { enabled: false } + return loveConfig.value + } + } + + /** + * 统一拉取静态配置(设计见 .docs/static-config-unified-fetch-design.md): + * getConfigs + audit-data + love-config 并行一次;TTL 内(默认 5 分钟,persist 恢复后 + * 亦生效)直接返回缓存,避免每次冷启动/onShow 重复请求;force=true 强制刷新。 + * 返回 ok = getConfigs 是否成功(失败时走内置默认/旧缓存,由调用方决定后续)。 + */ + const bootstrap = async (options?: { force?: boolean }): Promise<{ ok: boolean, fromCache: boolean }> => { + const force = options?.force ?? false + if (!force && fetchedAt.value > 0 && Date.now() - fetchedAt.value < STATIC_TTL) { + return { ok: true, fromCache: true } + } + const [cfg] = await Promise.all([fetchConfigs(), fetchAuditData(), fetchLoveConfig()]) + fetchedAt.value = Date.now() + return { ok: !!cfg, fromCache: false } + } + + /** 强制刷新静态配置(下拉刷新/设置页手动刷新入口) */ + const refreshStatic = () => bootstrap({ force: true }) + return { configs, auditData, auditModeEnabled, + maintenance, + loveConfig, + fetchedAt, fetchConfigs, setDefaultAppSettings, fetchAuditData, + fetchLoveConfig, + bootstrap, + refreshStatic, } }, { diff --git a/src/style/index.scss b/src/style/index.scss index bf44221..bd0dfc6 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -1,5 +1,6 @@ // 测试用的 iconfont,可生效 @import './iconfont.css'; +@import './uhemoji-iconfont.css'; :root, page { diff --git a/src/style/uhemoji-iconfont.css b/src/style/uhemoji-iconfont.css new file mode 100644 index 0000000..de23efa --- /dev/null +++ b/src/style/uhemoji-iconfont.css @@ -0,0 +1,94 @@ +@font-face { + font-family: "uhemoji-icon"; /* Project id 5230124 */ + /* Color fonts */ + src: + url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'); +} + +.uhemoji-icon { + font-family: "uhemoji-icon" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.uhemoji-icon--cool:before { + content: "\e6e9"; +} + +.uhemoji-icon--confused:before { + content: "\e6ea"; +} + +.uhemoji-icon--angry:before { + content: "\e6eb"; +} + +.uhemoji-icon--dizzy:before { + content: "\e6ec"; +} + +.uhemoji-icon--cry:before { + content: "\e6ed"; +} + +.uhemoji-icon--expressionless:before { + content: "\e6ee"; +} + +.uhemoji-icon--flushed:before { + content: "\e6ef"; +} + +.uhemoji-icon--happy-:before { + content: "\e6f0"; +} + +.uhemoji-icon--happy-1:before { + content: "\e6f1"; +} + +.uhemoji-icon--injury:before { + content: "\e6f2"; +} + +.uhemoji-icon--joy:before { + content: "\e6f3"; +} + +.uhemoji-icon--kiss-:before { + content: "\e6f4"; +} + +.uhemoji-icon--scared:before { + content: "\e6f5"; +} + +.uhemoji-icon--shocked:before { + content: "\e6f6"; +} + +.uhemoji-icon--smile-:before { + content: "\e6f7"; +} + +.uhemoji-icon--sleeping:before { + content: "\e6f8"; +} + +.uhemoji-icon--smiling-:before { + content: "\e6f9"; +} + +.uhemoji-icon--sweat:before { + content: "\e6fa"; +} + +.uhemoji-icon--thinking:before { + content: "\e6fb"; +} + +.uhemoji-icon--tired:before { + content: "\e6fc"; +}