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 @@
-
+
-
- 加载中...
-
+
+
+
+
+
+
+ {{ props.loadingText }}
+
+
-
-
- 加载失败
-
+
+
+
+
+
+
+
+ {{ props.errorText }}
+
+
刷新试试
-
-
+
+
-
-
- 无数据
-
- 刷新试试
-
-
+
+
+
+
+
+
+
+ {{ props.emptyText }}
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ 📢
+
+
+ 公告
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+ {{ showList[0]?.title }}
+
+
+
+
+
+
+ 更多
+
+
+
+
+
+
+
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 @@
-
-
-
- {{ title }}
+
+
+
+
+
+
+ 📢
+
+
+ 最新公告
+
+
+ {{ notice.typeDisplayName }}
+
+
+
+
+
-
-
-
-
+
+
+
+
+ {{ notice.title }}
+
+
+ {{ notice.summary }}
+
+
+ {{ formatDate(notice.publishTime) }}
+
-
-
- 点击跳转内容链接
+
+
+
+
+ 今日不再提醒
+
+
+ 查看全文 →
@@ -80,6 +147,6 @@ function handleToWebview() {
diff --git a/src/components/uh-vote-card/uh-vote-card.vue b/src/components/uh-vote-card/uh-vote-card.vue
index dd7a528..712969b 100644
--- a/src/components/uh-vote-card/uh-vote-card.vue
+++ b/src/components/uh-vote-card/uh-vote-card.vue
@@ -91,16 +91,16 @@ defineExpose({ refresh: handleGetData })
-
+
-
+
{{ voteData.title }}
-
+
{{ voteData.description }}
@@ -108,15 +108,15 @@ defineExpose({ refresh: handleGetData })
-
+
{{ option.label }}
-
-
+
+
@@ -126,20 +126,9 @@ defineExpose({ refresh: handleGetData })
{{ voteLabel }}
-
+
{{ voteLabel }}{{ voteResultLabel }}
-
-
diff --git a/src/config/markdown.ts b/src/config/markdown.ts
index 5985a22..29d6f88 100644
--- a/src/config/markdown.ts
+++ b/src/config/markdown.ts
@@ -100,7 +100,7 @@ export const markdownConfig = {
video: 'width: 100%',
},
/** 容器样式 */
- containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;padding:12px;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px;border-radius: 0px;background-color:transparent;',
+ containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;padding:0;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px;border-radius: 0px;background-color:transparent;',
/** 加载图 / 空图(来自应用配置 imagesConfig) */
loadingGif: checkImageUrl(undefined),
emptyGif: checkImageUrl(undefined),
diff --git a/src/hooks/useDataLoading.test.ts b/src/hooks/useDataLoading.test.ts
new file mode 100644
index 0000000..10145a2
--- /dev/null
+++ b/src/hooks/useDataLoading.test.ts
@@ -0,0 +1,84 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+import { defineComponent, h } from 'vue'
+import { useDataLoading } from './useDataLoading'
+
+/**
+ * 在 Vue 应用上下文中运行 composable。
+ * composable 的 ref/computed 只能在 setup() 内使用,
+ * withSetup 通过挂载一个临时组件来提供这个上下文。
+ */
+function withSetup(composableFn: () => T): T {
+ let result!: T
+ const Comp = defineComponent({
+ setup() {
+ result = composableFn()
+ return () => h('div')
+ },
+ })
+ const wrapper = mount(Comp)
+ wrapper.unmount()
+ return result
+}
+
+describe('useDataLoading', () => {
+ it('初始状态:status=loading, data=undefined', () => {
+ const fetcher = vi.fn().mockResolvedValue('data')
+ const { status, data } = withSetup(() => useDataLoading(fetcher))
+
+ expect(status.value).toBe('loading')
+ expect(data.value).toBeUndefined()
+ })
+
+ it('run 成功:status 从 loading 流转到 success,data 更新为返回值并触发 onSuccess', async () => {
+ const fetcher = vi.fn().mockResolvedValue('result')
+ const onSuccess = vi.fn()
+ const { status, data, run } = withSetup(() => useDataLoading(fetcher, { onSuccess }))
+
+ const promise = run()
+ expect(status.value).toBe('loading')
+
+ await promise
+ expect(status.value).toBe('success')
+ expect(data.value).toBe('result')
+ expect(onSuccess).toHaveBeenCalledWith('result')
+ })
+
+ it('run 返回空数组:status 流转到 empty', async () => {
+ const fetcher = vi.fn().mockResolvedValue([])
+ const { status, run } = withSetup(() => useDataLoading(fetcher))
+
+ await run()
+ expect(status.value).toBe('empty')
+ })
+
+ it('isEmpty 自定义判空:返回 true 时 status 为 empty', async () => {
+ const fetcher = vi.fn(async (): Promise<{ total: number, items: number[] }> => ({ total: 0, items: [1] }))
+ const { status, run } = withSetup(() => useDataLoading(fetcher, { isEmpty: data => data.total === 0 }))
+
+ await run()
+ expect(status.value).toBe('empty')
+ })
+
+ it('run 失败:不向外抛出异常,status 流转到 error 并触发 onError', async () => {
+ const err = new Error('network error')
+ const onError = vi.fn()
+ const fetcher = vi.fn().mockRejectedValue(err)
+ const { status, data, run } = withSetup(() => useDataLoading(fetcher, { onError }))
+
+ await expect(run()).resolves.toBeUndefined()
+
+ expect(status.value).toBe('error')
+ expect(data.value).toBeUndefined()
+ expect(onError).toHaveBeenCalledWith(err)
+ })
+
+ it('带参数 run:参数透传给 fetcher', async () => {
+ const fetcher = vi.fn(async (name: string) => `hi ${name}`)
+ const { data, run } = withSetup(() => useDataLoading(fetcher))
+
+ await run('world')
+ expect(fetcher).toHaveBeenCalledWith('world')
+ expect(data.value).toBe('hi world')
+ })
+})
diff --git a/src/hooks/useDataLoading.ts b/src/hooks/useDataLoading.ts
new file mode 100644
index 0000000..8fb7520
--- /dev/null
+++ b/src/hooks/useDataLoading.ts
@@ -0,0 +1,69 @@
+import type { Ref } from 'vue'
+import { ref } from 'vue'
+
+/** 数据加载四态(取值与旧 DataLoadingStatusEnum 一致,二者可互相兼容) */
+export type DataLoadingStatus = 'loading' | 'error' | 'empty' | 'success'
+
+export interface IUseDataLoadingOptions {
+ /** 自定义判空;默认:数组看长度、对象看键数、空值视为空 */
+ isEmpty?: (data: T) => boolean
+ /** 成功回调(数据就绪) */
+ onSuccess?: (data: T) => void
+ /** 失败回调(run 已捕获异常,不会向外抛出) */
+ onError?: (err: unknown) => void
+}
+
+export interface IUseDataLoadingReturn {
+ data: Ref
+ status: Ref
+ run: (...args: P) => Promise
+}
+
+/**
+ * 数据加载状态机
+ * 页面只提供请求函数,loading/empty/success/error 状态由 hook 接管,
+ * 取代「手工 ref + try/catch 里逐个搬运状态」的写法。
+ *
+ * @example
+ * const { data, status, run } = useDataLoading(() => getMomentByName(name))
+ * onLoad(() => run())
+ * onPullDownRefresh(async () => { await run(); uni.stopPullDownRefresh() })
+ */
+export function useDataLoading(
+ fetcher: (...args: P) => Promise,
+ options: IUseDataLoadingOptions = {},
+): IUseDataLoadingReturn {
+ const { isEmpty, onSuccess, onError } = options
+
+ const data = ref(undefined) as Ref
+ const status = ref('loading')
+
+ function calcEmpty(value: T): boolean {
+ if (isEmpty)
+ return isEmpty(value)
+ if (Array.isArray(value))
+ return value.length === 0
+ if (value !== null && typeof value === 'object')
+ return Object.keys(value).length === 0
+ return value === undefined || value === null || value === ''
+ }
+
+ async function run(...args: P): Promise {
+ status.value = 'loading'
+ try {
+ const res = await fetcher(...args)
+ data.value = res
+ status.value = calcEmpty(res) ? 'empty' : 'success'
+ onSuccess?.(res)
+ return res
+ }
+ catch (err) {
+ console.error('[useDataLoading] 请求失败', err)
+ status.value = 'error'
+ onError?.(err)
+ return undefined
+ }
+ }
+
+ return { data, status, run }
+}
diff --git a/src/hooks/useDataLoadingStatus.ts b/src/hooks/useDataLoadingStatus.ts
index ff5f8c5..bbf4f5e 100644
--- a/src/hooks/useDataLoadingStatus.ts
+++ b/src/hooks/useDataLoadingStatus.ts
@@ -1,18 +1,25 @@
-export enum DataLoadingStatusEnum {
- Loading = 'loading',
- Error = 'error',
- Empty = 'empty',
- Success = 'success',
-}
+import type { DataLoadingStatus } from './useDataLoading'
-export function useDataLoadingStatus(status?:DataLoadingStatusEnum) {
- const loadingStatus = ref(status??DataLoadingStatusEnum.Loading)
+/**
+ * @deprecated 请改用 useDataLoading:状态由 hook 内部流转,
+ * 无需再在 try/catch 里手工调用 updateLoadingStatus 搬运状态。
+ * 本文件保留仅为兼容存量页面(category/article-detail 等),后续逐步迁移。
+ */
+export const DataLoadingStatusEnum = {
+ Loading: 'loading',
+ Error: 'error',
+ Empty: 'empty',
+ Success: 'success',
+} as const
+
+export function useDataLoadingStatus(status?: DataLoadingStatus) {
+ const loadingStatus = ref(status ?? DataLoadingStatusEnum.Loading)
function resetLoadingStatus() {
loadingStatus.value = DataLoadingStatusEnum.Loading
}
- function updateLoadingStatus(newStatus: DataLoadingStatusEnum) {
+ function updateLoadingStatus(newStatus: DataLoadingStatus) {
loadingStatus.value = newStatus
}
diff --git a/src/pages-blog/about/about.vue b/src/pages-blog/about/about.vue
index 9108c08..af0477b 100644
--- a/src/pages-blog/about/about.vue
+++ b/src/pages-blog/about/about.vue
@@ -1,6 +1,7 @@
-
-
-
-
-
- uni-halo
+
+
+
+
+
+
+
+
+ uni-halo
+
+
+ AGPL-3.0 开源协议
+ uni-app × Halo
+
+
+ 基于 uni-app 打造的 Halo 博客跨端客户端
+
+
-
+
+
+ 相关链接
+
+
- {{ link.title }}
- {{ link.value }}
+
+ {{ link.tileLetter }}
+
+
+ {{ link.title }}
+
+ {{ link.value }}
+
+
+
-
- 根据 AGPL-3.0 协议开源
-
- 「 2022 uni-halo 丨 开源项目丨巷子工坊@小莫唐尼 」
-
+
+
+ 「 2022 uni-halo 丨 开源项目丨巷子工坊@小莫唐尼 」
diff --git a/src/pages-blog/archives/archives.vue b/src/pages-blog/archives/archives.vue
index 90ba647..646049a 100644
--- a/src/pages-blog/archives/archives.vue
+++ b/src/pages-blog/archives/archives.vue
@@ -27,7 +27,7 @@ const globalAppSettings = computed(() => settingStore.settings)
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
-const tab = ref({ activeIndex: 0, list: ['按月份查看', '按年份查看'] })
+const activeTabIndex = ref(0)
const queryParams = ref({ size: 10, page: 1 })
const result = ref<{ hasNext: boolean }>({ hasNext: false })
const cacheDataList = ref([])
@@ -44,6 +44,17 @@ const loadMoreText = ref('加载中...')
const postLabelYearKey = 'content.halo.run/archive-year'
const postLabelMonthKey = 'content.halo.run/archive-month'
+/** 卡片布局偏好 → 原子类(对应旧版 cardType 样式变体) */
+const CARD_LAYOUTS: Record = {
+ lr_image_text: { card: '', thumb: 'h-[170rpx] w-[200rpx]', info: 'w-0 flex-1 pl-5' },
+ lr_text_image: { card: '', thumb: 'order-2 h-[170rpx] w-[200rpx]', info: 'order-1 w-0 flex-1 pr-5' },
+ tb_image_text: { card: 'flex-col', thumb: 'h-[220rpx] w-full', info: 'w-full pt-3' },
+ tb_text_image: { card: 'flex-col', thumb: 'order-2 h-[220rpx] w-full', info: 'order-1 w-full pb-3' },
+ only_text: { card: '', thumb: 'hidden', info: 'py-1' },
+}
+
+const calcCardLayout = computed(() => CARD_LAYOUTS[globalAppSettings.value.layout.cardType] || CARD_LAYOUTS.lr_image_text)
+
/* ---------------- 数据处理 ---------------- */
/** 按 tab 分组文章 */
function handleGetPosts(list: IPost[]): Record {
@@ -51,7 +62,7 @@ function handleGetPosts(list: IPost[]): Record {
list.forEach((item) => {
const labels = item.metadata.labels || {}
let postItemKey = ''
- if (tab.value.activeIndex === 0) {
+ if (activeTabIndex.value === 0) {
postItemKey = `${labels[postLabelYearKey]}-${labels[postLabelMonthKey]}`
}
else {
@@ -78,7 +89,7 @@ function handleGetShowDataList(posts: Record): typeof dataList.
month: '',
posts: posts[key],
}
- if (tab.value.activeIndex === 0) {
+ if (activeTabIndex.value === 0) {
const splitDate = key.split('-')
postData.year = splitDate[0]
postData.month = splitDate[1]
@@ -136,13 +147,14 @@ async function handleGetData() {
loadMoreText.value = '加载中...'
try {
- const data = await getPostList({ ...queryParams.value })
- result.value = { hasNext: data.hasNext }
- const posts = handleGetPosts(data.items)
+ // getPostList 返回 IResponse,数据在 .data 下
+ const res = await getPostList({ ...queryParams.value })
+ result.value = { hasNext: res.data.hasNext }
+ const posts = handleGetPosts(res.data.items)
const showDataList = handleGetShowDataList(posts)
if (isLoadMore.value) {
- cacheDataList.value = handleUniqueCacheDatalist([...cacheDataList.value, ...data.items])
+ cacheDataList.value = handleUniqueCacheDatalist([...cacheDataList.value, ...res.data.items])
// 合并增量数据
showDataList.forEach((item) => {
const find = dataList.value.find(x => x.key === item.key)
@@ -163,11 +175,11 @@ async function handleGetData() {
}
else {
dataList.value = showDataList
- cacheDataList.value = data.items
+ cacheDataList.value = res.data.items
}
loading.value = 'success'
- loadMoreText.value = data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
+ loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}
catch (err) {
console.error(err)
@@ -180,8 +192,8 @@ async function handleGetData() {
}
}
-function handleOnTabChange(index: number) {
- tab.value.activeIndex = index
+function handleOnTabChange(e: { index: number }) {
+ activeTabIndex.value = e.index
queryParams.value.page = 1
dataList.value = handleGetShowDataList(handleGetPosts(cacheDataList.value))
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
@@ -237,17 +249,14 @@ onReachBottom(() => {
-
-
-
-
+
+
+
+
+
+
+
-
@@ -256,105 +265,59 @@ onReachBottom(() => {
-
+
-
+
+
-
-
-
+
+
+
-
-
- {{ item.year }}年
- {{ item.month }}月
- (共 {{ item.posts.length }} 篇{{ calcAuditModeEnabled ? '内容' : '文章' }})
+
+
+ {{ item.year }}年
+ {{ item.month }}月
+ 共 {{ item.posts.length }} 篇{{ calcAuditModeEnabled ? '内容' : '文章' }}
-
-
-
+
+
+
{{ post.spec.title }}
-
+
{{ post.status?.excerpt }}
-
+
发布时间:{{ formatTime(post.spec.publishTime) }}
-
+
该日期下暂无归档文章!
-
+
{{ loadMoreText }}
-
-
+
+
-
-
diff --git a/src/pages-blog/article-detail/article-detail.vue b/src/pages-blog/article-detail/article-detail.vue
index 661d2ce..8fcb1df 100644
--- a/src/pages-blog/article-detail/article-detail.vue
+++ b/src/pages-blog/article-detail/article-detail.vue
@@ -1,626 +1,655 @@
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ result?.owner?.displayName }}
+
+
+ {{ result?.spec.title }}
+
+
+
+ {{ item.spec.displayName }}
+
+
+ #{{ item.spec.displayName }}
+
+
+
+
+
+ 原文
+
+ {{ originalURL }}
+
+
+
+ 日期
+ {{ formatPublishTime(result?.spec.publishTime) }}
+
+
+
+ 阅读
+ {{ result?.stats?.visit ?? 0 }}
+
+
+ 喜欢
+ {{ result?.stats?.upvote ?? 0 }}
+
+
+ 评论
+ {{ result?.stats?.comment ?? 0 }}
+
+
+ 字数
+ {{ result?.content?.raw.length || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 版权声明
+
+
+ 版权归属:{{ postDetailConfig.copyrightAuthor }}
+
+
+ 版权说明:{{ postDetailConfig.copyrightDesc }}
+
+
+ 侵权处理:{{ postDetailConfig.copyrightViolation }}
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+ 点赞
+
+
+
+ 评论
+
+
+
+ 收藏
+
+
+
+
-
-
-
-
-
- {{ result?.owner?.displayName }}
-
-
- {{ result?.spec.title }}
-
-
-
- {{ item.spec.displayName }}
-
-
- #{{ item.spec.displayName }}
-
-
+
+
+
+
+
+
-
-
- 原文
-
- {{originalURL}}
-
-
-
- 日期
- {{ formatPublishTime(result?.spec.publishTime) }}
-
-
-
- 阅读
- {{ result?.stats?.visit ?? 0 }}
-
-
- 喜欢
- {{ result?.stats?.upvote ?? 0 }}
-
-
- 评论
- {{ result?.stats?.comment ?? 0 }}
-
-
- 字数
- {{ result?.content?.raw.length || 0 }}
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 版权声明
-
-
- 版权归属:{{ postDetailConfig.copyrightAuthor }}
-
-
- 版权说明:{{ postDetailConfig.copyrightDesc }}
-
-
- 侵权处理:{{ postDetailConfig.copyrightViolation }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 点赞
-
-
-
- 评论
-
-
-
- 收藏
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
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 @@
-
-
-
-
-
-
-
- {{ bloggerInfo.nickname }}
-
-
- {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+
+
+
+
+
+
+
+
+
+
+ {{ bloggerInfo.nickname }}
+
+
+ {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+
+
+
+ 点击卡片即可复制对应内容
+
-
-
-
+
+
+
+ 联系方式
+
+
-
- {{ item.name }}
+
+ {{ platformMeta[item.key]?.letter || item.name.slice(0, 1) }}
-
- {{ item.value }}
+
+ {{ item.name }}
+
+ {{ item.value }}
+
+
-
-
-
+
+
+
-
-
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(() => {
-
+
-
-
+
+
@@ -342,43 +342,42 @@ onReachBottom(() => {
-
+
-
-
+
+
-
-
- {{ link.spec.groupName || '暂未分组' }}
+
+
+ {{ link.spec.groupName || '暂未分组' }}
{{ link.spec.displayName }}
-
+
站点地址:{{ link.spec.url }}
-
+
博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
-
-
-
-
+
+
+
+
{{ link.spec.displayName }}
-
+
{{ link.spec.description }}
@@ -388,39 +387,39 @@ onReachBottom(() => {
-
-
+
+
-
-
+
+
-
+
-
+
{{ detail.data.spec.displayName }}
-
+
{{ detail.data.spec.groupName }}
- {{ detail.data.spec.url }}
+ {{ detail.data.spec.url }}
-
+
博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
-
+
-
+
{{ loadMoreText }}
@@ -450,15 +449,15 @@ onReachBottom(() => {
-
- {{ group.displayName || '未分组' }}
- ({{ group.links.length }})
+
+ {{ group.displayName || '未分组' }}
+ ({{ group.links.length }})
{
mode="aspectFill"
/>
-
+
{{ link.spec?.displayName }}
-
+
{{ link.spec.authorName }}
-
+
{{ link.spec?.description || '暂无简介~' }}
-
+
@@ -485,7 +484,7 @@ onReachBottom(() => {
-
+
申请收录
@@ -504,42 +503,42 @@ onReachBottom(() => {
@click="handlePreviewMiniProgramCode(miniDetail.data)"
@longpress="handleSaveMiniProgramCode(miniDetail.data)"
/>
-
-
+
+
点击预览,长按保存太阳码
- {{ miniDetail.data.spec?.displayName }}
-
+ {{ miniDetail.data.spec?.displayName }}
+
{{ miniDetail.data.spec.groupName }}
-
+
{{ miniDetail.data.spec.description }}
-
+
- {{ miniDetail.data.spec.authorName }}
-
+ {{ miniDetail.data.spec.authorName }}
+
网站:{{ miniDetail.data.spec.website }}
-
-
+
+
{{ miniDetail.data.spec.link }}
- 复制
+ 复制
@@ -559,9 +558,3 @@ 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(() => ({
-
-
-
-
+
+
+
-
-
-
-
-
-
-
- {{ moment.owner?.displayName || bloggerInfo.nickname }}
-
-
- {{ formatTime(moment.spec.releaseTime) }}
-
-
-
-
-
- {{ moment.stats.upvote || 0 }}
-
-
-
- {{ moment.stats.totalComment || 0 }}
-
-
+
+
+
+
+
+ {{ calcMastheadDate }}
+
+
+ {{ calcMastheadMeta }}
+
+
+
+
+
+
+
+
+
+ {{ moment.owner?.displayName || bloggerInfo.nickname }}
+
+
+ 记录了这美好的一刻
+
-
-
- 标签列表:
-
- {{ tag }}
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
- 图片附件:
-
-
-
-
-
-
-
-
-
-
-
- 音频附件:
-
-
-
-
-
-
-
-
-
- 视频附件:
-
-
-
-
+
+
+
+
-
-
+
+
+
+ # {{ tag }}
+
-
+
+
+
+
+
+ {{ moment.stats.upvote || 0 }}
+
+
+
+ {{ moment.stats.totalComment || 0 }}
+
+
+
+
+
+
+
+
+
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ moment.owner?.displayName || bloggerInfo.nickname }}
+
+
+ {{ formatTime(moment.spec.releaseTime) }}
+
+
+
+
+
+
+ {{ moment.stats.upvote || 0 }}
+
+
+
+ {{ moment.stats.totalComment || 0 }}
+
+
+
+
+
+
+
+ # {{ tag }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 图片附件
+
+
+
+
+
+
+
+
+
+
+
+
+ 音频附件
+
+
+
+
+
+
+
+
+
+
+ 视频附件
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+ 🕳️
+
+
+ 公告不存在或已下线
+
+
+
+ 返回
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+ {{ typeDisplayName }}
+
+
+ {{ publishTime }}
+
+
+
+
+
+
+
+
+
+ 查看原文 ↗
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ 全部
+
+
+
+ {{ type.typeDisplayName }}
+
+
+
+
+
+
+
+ {{ opt.label }}
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+ {{ item.summary }}
+
+
+
+ {{ item.typeDisplayName }}
+
+
+ {{ formatDate(item.publishTime) }}
+
+
+
+
+
+
+ 加载中...
+
+
+ 没有更多了
+
+
+
+
+
+ 📢
+
+ {{ loading ? '加载中...' : '暂无公告' }}
+
+
+
+
+
+
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(() => {
-
+
{
@on-refresh="handleOnSearch"
/>
-
-
-
-
+
+
+
+
-
+
-
-
+
-
-
+
+
@@ -206,15 +192,18 @@ onPullDownRefresh(() => {
-
+
{{ isArticle(item) ? '文章' : '瞬间' }}
- {{ item.title }}
+ {{ item.title }}
{
copy-by-long-press
/>
-
-
+
+
@@ -246,11 +235,7 @@ onPullDownRefresh(() => {
diff --git a/src/pages-blog/test/test.vue b/src/pages-blog/test/test.vue
new file mode 100644
index 0000000..d6b6352
--- /dev/null
+++ b/src/pages-blog/test/test.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages-blog/votes/votes.vue b/src/pages-blog/votes/votes.vue
index 70799c9..10e68bc 100644
--- a/src/pages-blog/votes/votes.vue
+++ b/src/pages-blog/votes/votes.vue
@@ -117,7 +117,7 @@ onReachBottom(() => {
-
+
{
-
+
@@ -140,20 +140,14 @@ onReachBottom(() => {
:vote-name="vote.name"
@on-vote-success="handleOnVoteSuccess"
/>
-
+
{{ loadMoreText }}
-
-
+
+
-
-
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 @@
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+ ✅
+
+
+ 当前服务正常,无需维护
+
+
+ 如果仍然无法访问,请稍后重试或联系站长
+
+
+
+ 返回首页
+
+
+
+
+
+
+
+ ⚠️
+
+
+ 服务暂时无法访问
+
+
+ 站点可能正在维护中,请稍后重试
+
+
+
+ 重新加载
+
+
+
+
+
+
+
+
+
+
+ {{ isScheduled ? '维护预告' : '维护中' }}
+
+
+
+
+ {{ title }}
+
+
+
+
+ {{ countdownPrefix }}
+
+
+ {{ countdownText }}
+
+
+
+
+
+
+
+
+ 请耐心等待,维护完成后将自动恢复访问
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
- {{ bloggerInfo.nickname }}
-
-
- {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ bloggerInfo.nickname }}
+
+
+ {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+
+
+
+
-
-
-
-
-
- {{ statistics.post }}
-
-
- 内容数量
-
-
-
-
- {{ statistics.visit }}
-
-
- 访客数量
-
-
-
-
- {{ statistics.category }}
-
-
- 分类总数
-
-
-
-
-
-
- {{ statistics.comment }}
-
-
- 评论数量
-
-
-
-
- {{ statistics.upvote }}
-
-
- 点赞数量
-
-
-
-
- {{ statisticsShowMore ? '收起' : '展开' }}
-
-
+
+
+
+
+ {{ item.value }}
+
+
+ {{ item.label }}
+
+
+
-
-
-
-
-
-
- {{ nav.title }}
-
-
- {{ nav.rightText }}
-
-
-
-
-
+
+
+
+ {{ group.title }}
+
+
+
+
+
+
+
+ {{ nav.title }}
+
+
+ {{ nav.rightText }}
+
+
+
+
+
-
-
- {{ copyrightConfig.content }}
-
-
-
-
-
+
+
+ {{ 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 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
- {{ item.spec.displayName }}
-
-
- 共 {{ item.postCount }} 篇文章
-
-
-
-
-
- {{ loadMoreText }}
-
-
-
+
+
+
+
+
+
+
+ {{ item.spec.displayName }}
+
+
+ 共 {{ item.postCount }} 篇文章
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
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 @@
@@ -191,6 +186,9 @@
+
+
+
@@ -207,7 +205,7 @@
-
+
@@ -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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
- {{ moment.owner?.displayName || bloggerInfo.nickname }}
-
-
- {{ formatMomentTime(moment.spec.releaseTime) }}
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ moment.owner?.displayName || bloggerInfo.nickname }}
+
+
+ {{ formatMomentTime(moment.spec.releaseTime) }}
+
+
+
+
+ 详情
+
+
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+ # {{ tag }}
+
+
-
-
-
-
+
+
-
-
-
- {{ tag }}
-
-
+
+
+
+
+
+
-
-
-
-
- {{ moment.stats.upvote || 0 }}
-
-
-
- {{ moment.stats.totalComment || 0 }}
-
-
-
+
+
+
+
-
-
-
-
- {{ loadMoreText }}
-
-
-
-
-
-
+
+
+
+
-
+
+ {{ loadMoreText }}
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ moment.owner?.displayName || bloggerInfo.nickname }}
+
+
+ {{ formatMomentTime(moment.spec.releaseTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ # {{ tag }}
+
+
+
+
+
+
+
+ {{ moment.stats.upvote || 0 }}
+
+
+
+ {{ moment.stats.totalComment || 0 }}
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
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";
+}