From 62b604644c3ff93913770188c2a5bd994314f4b6 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 17:17:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=B8=8E=E5=A4=9A=E9=A1=B9=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增本地收藏store与工具函数,支持文章/瞬间收藏持久化 - 重构插件可用性检查hook,统一管理依赖插件状态 - 替换旧数据加载hook为新的状态管理方案 - 优化首页布局与加载逻辑,调整公告组件样式 - 为瞬间详情页添加点赞、评论与收藏功能 - 新增文本处理工具,支持HTML/Markdown转纯文本与摘要截断 - 修复评论弹窗适配瞬间类型的参数问题 --- .agents/skills/uni-halo/SKILL.md | 143 ++++-- .../uh-comment-modal/uh-comment-modal.vue | 5 +- .../uh-data-loading/uh-data-loading.vue | 18 +- .../uh-home-banner - 副本/uh-home-banner.vue | 269 ---------- .../uh-home-notify/uh-home-notify.vue | 15 +- .../uh-notify-dialog/uh-notify-dialog.vue | 233 ++++----- .../uh-plugin-unavailable.vue | 213 ++++---- .../uh-plugin-unavailable.vue.bak | 135 +++++ src/hooks/useMaintenanceIntercept.ts | 7 +- .../plugin.ts => hooks/usePluginAvailable.ts} | 45 +- .../article-detail/article-detail.vue | 24 +- src/pages-blog/data-visual/data-visual.vue | 482 +++++++++--------- src/pages-blog/favorites/favorites.vue | 196 +++++++ src/pages-blog/friend-links/friend-links.vue | 12 +- .../moment-detail/moment-detail.vue | 122 ++++- src/pages-blog/search/search.vue | 5 +- src/pages-blog/votes/votes.vue | 5 +- src/pages/maintenance/maintenance.vue | 5 +- src/pages/tabbar/about/about.vue | 33 +- src/pages/tabbar/gallery/gallery.vue | 36 +- src/pages/tabbar/home/home.vue | 94 ++-- src/pages/tabbar/moments/moments.vue | 65 +-- .../tabbar/moments/moments.vue.glass.bak | 423 --------------- src/store/favorites.test.ts | 111 ++++ src/store/favorites.ts | 83 +++ src/store/index.ts | 1 + src/utils/favorite.ts | 104 ++++ src/utils/text.ts | 51 ++ 28 files changed, 1548 insertions(+), 1387 deletions(-) delete mode 100644 src/components/uh-home-banner - 副本/uh-home-banner.vue create mode 100644 src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue.bak rename src/{utils/plugin.ts => hooks/usePluginAvailable.ts} (73%) create mode 100644 src/pages-blog/favorites/favorites.vue delete mode 100644 src/pages/tabbar/moments/moments.vue.glass.bak create mode 100644 src/store/favorites.test.ts create mode 100644 src/store/favorites.ts create mode 100644 src/utils/favorite.ts create mode 100644 src/utils/text.ts diff --git a/.agents/skills/uni-halo/SKILL.md b/.agents/skills/uni-halo/SKILL.md index 6cf6b0b..0955744 100644 --- a/.agents/skills/uni-halo/SKILL.md +++ b/.agents/skills/uni-halo/SKILL.md @@ -16,7 +16,7 @@ license: Complete terms in LICENSE.txt - **定义或修改 API**(约定使用 alova,格式、`meta` 传参、类型文件位置) - **定义或修改类型**(`src/api/types/` 下的接口与请求/响应类型) - **使用通用配置**(`src/config/` 的 appConfig / appSettings / haloGlobal / markdown) -- **页面数据请求**(`useDataLoading` / `useRequest` hooks + `uh-data-loading` 组件) +- **页面数据请求**(`useDataLoadingStatus` + `updateLoadingStatus` 管理四态 + `uh-data-loading` 组件) - **处理多平台差异**(H5 / 微信小程序 / APP,条件编译) ## 三条铁律(先记住) @@ -295,81 +295,128 @@ onLoad(() => { - 页面根节点用 `app-page` 类 + 主题底色:`` - 页面标题 `navigationBarTitleText` 写中文;下拉刷新 `enablePullDownRefresh: true` -### 5.4 页面数据请求(统一 hooks + uh-data-loading 组件) +### 5.4 页面数据请求(统一 updateLoadingStatus + uh-data-loading 组件) -**数据加载四态**:`loading / error / empty / success`,由 `useDataLoading` hook 接管, -页面只提供请求函数,**取代「手工 ref + try/catch 逐个搬运状态」的写法**。 +**数据加载四态**:`loading / error / empty / success`,统一用 +`useDataLoadingStatus`(`src/hooks/useDataLoadingStatus.ts`)的 +`updateLoadingStatus(DataLoadingStatusEnum.Xxx)` 管理,**取代「手工 ref + try/catch 逐个搬运状态」的写法**。 +首页、图库、瞬间、分类等 tabbar 页面均为此写法(参考 `src/pages/tabbar/category/category.vue`), +**后续所有页面请求状态一律照此管理**。 ```vue ``` **要点**: -- `useDataLoading(fetcher, { isEmpty, onSuccess, onError })` 返回 `{ data, status, run }` - - `isEmpty` 自定义判空(默认:数组看长度、对象看键数、空值视为空) - - `run` 已捕获异常,失败置 `status='error'`,不会向外抛出 -- 模板里 `v-if="status !== 'success'"` 显示 ``,否则渲染数据 +- `useDataLoadingStatus()` 返回 `{ loadingStatus, updateLoadingStatus }`;`DataLoadingStatusEnum` 四态: + `Loading / Error / Empty / Success` +- 请求开始置 `DataLoadingStatusEnum.Loading`;成功按数据是否为空置 `Success` / `Empty`;失败置 `Error` +- 模板里 `v-if="loadingStatus !== DataLoadingStatusEnum.Success"` 显示 ``,否则渲染数据 - `` 常用 props:`loading-status`(必传)、`min-height`、`loading-text`、 `error-text`、`empty-text`(留空显示默认文案);`@refresh` 绑重试函数 -- **旧页面**用的 `useDataLoadingStatus`(`DataLoadingStatusEnum`)已标记 `@deprecated`, - 新代码一律用 `useDataLoading` +- 首页特例:入口拦截 + 文章列表空时用 `v-if="loadingStatus !== Success && articleList.length === 0"`, + 避免轮播/公告区被占位组件顶掉 +- 详情页等单数据场景(如 `moment-detail`)可用 `useDataLoading` 状态机(返回 `{ data, status, run }`), + 列表页/四态展示优先 `updateLoadingStatus` 写法 - 简单场景也可用 `useRequest(fn, { immediate })`:返回 `{ loading, error, data, run }` ### 5.5 列表页(分页加载) @@ -640,7 +687,7 @@ loadMoreText.value = t('common.loadMore') - 新 hooks 放 `src/hooks/`,auto-import(`unplugin-auto-import` 已配 `dirs: ['src/hooks']`),页面**免 import 直接调用** - 命名 `useXxx`;有配套单测的写 `xxx.test.ts`(如 `useDataLoading.test.ts`、`useRequest.test.ts`) -- 参考既有实现风格:`useDataLoading`(状态机四态)、`useScroll`、`useUpload`(平台条件编译处理) +- 参考既有实现风格:`useDataLoadingStatus`(页面四态管理,见 §5.4)、`useDataLoading`(详情页单数据状态机)、`useScroll`、`useUpload`(平台条件编译处理) ### 8.6 Git 提交与合入 diff --git a/src/components/uh-comment-modal/uh-comment-modal.vue b/src/components/uh-comment-modal/uh-comment-modal.vue index 6fdbfe3..d28c4cd 100644 --- a/src/components/uh-comment-modal/uh-comment-modal.vue +++ b/src/components/uh-comment-modal/uh-comment-modal.vue @@ -10,9 +10,12 @@ isComment ?: boolean title ?: string postName : string + /** 评论目标 kind(文章 Post / 瞬间 Moment) */ + subjectKind ?: string }>(), { isComment: false, title: '', + subjectKind: 'Post', }) const emit = defineEmits<{ @@ -189,7 +192,7 @@ }, subjectRef: { group: 'content.halo.run', - kind: 'Post', + kind: props.subjectKind, name: form.value.postName, version: 'v1alpha1', }, diff --git a/src/components/uh-data-loading/uh-data-loading.vue b/src/components/uh-data-loading/uh-data-loading.vue index 2ceb7b5..a4baa37 100644 --- a/src/components/uh-data-loading/uh-data-loading.vue +++ b/src/components/uh-data-loading/uh-data-loading.vue @@ -18,8 +18,8 @@ interface IProps { const props = withDefaults(defineProps(), { loadingStatus: 'loading', - minHeight: '60vh', - loadingText: '稍等,正在加载中哦...', + minHeight: '75vh', + loadingText: '稍等,正在加载中哦', errorText: '哎呀,加载失败了呢~', emptyText: '啊偶,暂时没有数据呢~', loadingSubText: '', @@ -63,7 +63,7 @@ const statusScene = computed(() => { + &.decoration { + background-color: rgb(255 255 255 / 95%); + box-shadow: 0 0 12rpx rgb(226 232 240 / 35%); + backdrop-filter: blur(6rpx); + border-top: 12rpx solid rgb(3 169 244); + } + } + \ No newline at end of file diff --git a/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue.bak b/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue.bak new file mode 100644 index 0000000..03dd947 --- /dev/null +++ b/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue.bak @@ -0,0 +1,135 @@ + + + + + diff --git a/src/hooks/useMaintenanceIntercept.ts b/src/hooks/useMaintenanceIntercept.ts index 9fd919c..ee4ee44 100644 --- a/src/hooks/useMaintenanceIntercept.ts +++ b/src/hooks/useMaintenanceIntercept.ts @@ -6,7 +6,6 @@ * 维护页按 reason 展示默认(未配置维护信息)或配置文案。设计见插件 * .docs/maintenance-config-design.md §8。 */ -import { usePluginAvailable } from '@/utils/plugin' import { useAppConfigStore } from '@/store/appConfig' /** 维护拦截原因:plugin 主插件未激活 / maintenance 维护模式开启 */ @@ -33,14 +32,16 @@ export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo' */ export function useMaintenanceIntercept() { const appConfigStore = useAppConfigStore() + /** 主插件可用性 hook(checkIntercept 内 await check 后读取 available) */ + const { available: pluginAvailable, check: checkPluginAvailable } = usePluginAvailable(MAINTENANCE_PLUGIN_ID) /** * 检查是否命中拦截(插件可用性 + 维护模式)。 * @param force 是否强制刷新配置(默认 false 走 bootstrap TTL 缓存) */ async function checkIntercept(force = false): Promise { - const pluginAvailable = await usePluginAvailable(MAINTENANCE_PLUGIN_ID) - if (!pluginAvailable) + await checkPluginAvailable() + if (!pluginAvailable.value) return { intercepted: true, reason: 'plugin' } const { ok } = await appConfigStore.bootstrap({ force }) diff --git a/src/utils/plugin.ts b/src/hooks/usePluginAvailable.ts similarity index 73% rename from src/utils/plugin.ts rename to src/hooks/usePluginAvailable.ts index b765f97..d461079 100644 --- a/src/utils/plugin.ts +++ b/src/hooks/usePluginAvailable.ts @@ -1,6 +1,21 @@ /** - * 插件清单与可用性检查(源自旧项目 utils/plugin.js,TS 化) + * 插件清单与可用性 hook + * 源自旧项目 utils/plugin.js(TS 化)与 utils/plugin.ts,整体迁移至 hooks 目录。 + * 提供: + * - 依赖插件 ID 常量(NeedPluginIds)与插件清单(NeedPlugins) + * - 底层可用性检查(checkNeedPluginAvailable) + * - 响应式 hook(usePluginAvailable):available 状态 + check 校验函数,页面免 import 直接调用 + * (auto-import 已配置 src/hooks,无需手写 import) + * + * @example + * const { available, check } = usePluginAvailable('plugin-vote') + * onLoad(async () => { + * await check() + * if (!available.value) return + * handleGetData() + * }) */ +import { ref } from 'vue' import { checkPluginAvailable } from '@/api/halo' import { checkUrl } from '@/utils/url' @@ -124,10 +139,26 @@ export async function checkNeedPluginAvailable(pluginId: string): Promise { - return checkNeedPluginAvailable(pluginId) +export function usePluginAvailable(pluginId: string, initial = true) { + /** 插件是否可用(默认 true,避免首帧闪现插件不可用占位;需要先置 false 的页面传 initial=false) */ + const available = ref(initial) + /** 是否校验中 */ + const checking = ref(false) + + /** + * 执行插件可用性校验(刷新 available) + * @returns 当前是否可用(与 available.value 一致,便于一次性调用方直接取返回值) + */ + async function check(): Promise { + checking.value = true + try { + available.value = await checkNeedPluginAvailable(pluginId) + return available.value + } + finally { + checking.value = false + } + } + + return { available, checking, check } } diff --git a/src/pages-blog/article-detail/article-detail.vue b/src/pages-blog/article-detail/article-detail.vue index 8fcb1df..19d425f 100644 --- a/src/pages-blog/article-detail/article-detail.vue +++ b/src/pages-blog/article-detail/article-detail.vue @@ -5,8 +5,10 @@ import { getPostByName, getPostCommentReplyList, postTrackersCounter, submitUpvo import { createVerificationCode, requestRestrictReadCheck } from '@/api/uni-halo' import { formatTime } from '@/utils/formatTime' import { useAppConfigStore } from '@/store/appConfig' +import { useFavoritesStore } from '@/store/favorites' import { useSettingStore } from '@/store/setting' import { checkAvatarUrl, checkImageUrl } from '@/utils/url' +import { buildPostFavoriteItem } from '@/utils/favorite' import { checkPostRestrictRead, copyToClipboard, getRestrictReadTypeName, getShowableContent } from '@/utils/restrictRead' import { getDomainOnly } from '@/utils/urlParams' import { markdownConfig } from '@/config/markdown' @@ -23,6 +25,7 @@ definePage({ }) const appConfigStore = useAppConfigStore() +const favoritesStore = useFavoritesStore() const settingStore = useSettingStore() const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() @@ -217,6 +220,22 @@ async function handleDoLikes() { } } +/* ---------------- 收藏 ---------------- */ +/** 当前文章是否已收藏 */ +function hasFavorited(): boolean { + const name = result.value?.metadata.name + return !!name && favoritesStore.isFavorite('post', name) +} + +/** 切换收藏(收藏/取消),收藏时按当前文章内容生成快照入库 */ +function handleTogglePostFavorite() { + const post = result.value + if (!post) + return + const favorited = favoritesStore.toggle(buildPostFavoriteItem(post)) + uni.showToast({ icon: 'none', title: favorited ? '收藏成功' : '已取消收藏' }) +} + /* ---------------- 受限阅读 ---------------- */ function readMore() { const annotations = result.value?.metadata?.annotations @@ -613,10 +632,11 @@ const globalAppSettings = computed(() => settingStore.settings) - 收藏 + {{ hasFavorited() ? '已收藏' : '收藏' }} diff --git a/src/pages-blog/data-visual/data-visual.vue b/src/pages-blog/data-visual/data-visual.vue index cf0f387..d37ec37 100644 --- a/src/pages-blog/data-visual/data-visual.vue +++ b/src/pages-blog/data-visual/data-visual.vue @@ -1,280 +1,256 @@ + + \ No newline at end of file diff --git a/src/pages-blog/favorites/favorites.vue b/src/pages-blog/favorites/favorites.vue new file mode 100644 index 0000000..09631a6 --- /dev/null +++ b/src/pages-blog/favorites/favorites.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/pages-blog/friend-links/friend-links.vue b/src/pages-blog/friend-links/friend-links.vue index 9d15fb2..c60c6e0 100644 --- a/src/pages-blog/friend-links/friend-links.vue +++ b/src/pages-blog/friend-links/friend-links.vue @@ -12,7 +12,7 @@ import { getMiniProgramLinkGroupedList } from '@/api/uni-halo' import { useAppConfigStore } from '@/store/appConfig' import { useSettingStore } from '@/store/setting' import { checkAvatarUrl, checkImageUrl } from '@/utils/url' -import { NeedPluginIds, usePluginAvailable } from '@/utils/plugin' +import { NeedPluginIds } from '@/hooks/usePluginAvailable' import type { ILink, ILinkGroup } from '@/api/types/halo' import type { IMiniProgramLink, IMiniProgramLinkGroupVo } from '@/api/types/uni-halo' @@ -32,10 +32,10 @@ const globalAppSettings = computed(() => settingStore.settings) /* ---------------- 依赖插件 ---------------- */ /** 站点 tab:plugin-links */ const sitePluginId = NeedPluginIds.PluginLinks -const sitePluginAvailable = ref(true) +const { available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable(sitePluginId) /** 小程序 tab:plugin-uni-halo */ const miniPluginId = NeedPluginIds.PluginUniHalo -const miniPluginAvailable = ref(true) +const { available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable(miniPluginId) /* ---------------- tabs ---------------- */ const activeTabIndex = ref(0) @@ -268,9 +268,9 @@ function handleSaveMiniProgramCode(link: IMiniProgramLink) { /* ---------------- 生命周期 ---------------- */ onLoad(async () => { - ;[sitePluginAvailable.value, miniPluginAvailable.value] = await Promise.all([ - usePluginAvailable(sitePluginId), - usePluginAvailable(miniPluginId), + await Promise.all([ + checkSitePluginAvailable(), + checkMiniPluginAvailable(), ]) if (sitePluginAvailable.value) handleGetLinkGroupData() diff --git a/src/pages-blog/moment-detail/moment-detail.vue b/src/pages-blog/moment-detail/moment-detail.vue index 3758a8c..dbfed16 100644 --- a/src/pages-blog/moment-detail/moment-detail.vue +++ b/src/pages-blog/moment-detail/moment-detail.vue @@ -7,9 +7,11 @@ */ import { computed, ref } from 'vue' import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' -import { getMomentByName } from '@/api/halo' +import { getMomentByName, submitUpvote } from '@/api/halo' import { useAppConfigStore } from '@/store/appConfig' +import { useFavoritesStore } from '@/store/favorites' import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url' +import { buildMomentFavoriteItem } from '@/utils/favorite' import { generateUUID } from '@/utils/uuid' import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { randomTagColor } from '@/utils/random' @@ -27,6 +29,7 @@ definePage({ }) const appConfigStore = useAppConfigStore() +const favoritesStore = useFavoritesStore() const haloConfigs = computed(() => appConfigStore.configs) const bloggerInfo = computed(() => { @@ -122,6 +125,83 @@ const calcMastheadMeta = computed(() => { return time ? formatTimeUtil({ d: time, f: 'yyyy年 · 星期w' }) : '' }) +/* ---------------- 收藏 ---------------- */ +/** 当前瞬间是否已收藏(悬浮胶囊高亮) */ +const momentFavorited = computed(() => { + const name = moment.value?.metadata.name + return !!name && favoritesStore.isFavorite('moment', name) +}) + +/** 切换收藏(收藏/取消),收藏时按当前详情内容生成快照入库 */ +function handleToggleMomentFavorite() { + const card = moment.value + if (!card) + return + const favorited = favoritesStore.toggle(buildMomentFavoriteItem(card)) + uni.showToast({ icon: 'none', title: favorited ? '收藏成功' : '已取消收藏' }) +} + +/* ---------------- 点赞 ---------------- */ +const upvotedNames = ref([]) + +function hasUpvoted(): boolean { + return upvotedNames.value.includes(moment.value?.metadata.name || '') +} + +async function handleDoLikes() { + const current = moment.value + if (!current) + return + if (hasUpvoted()) { + uni.showToast({ icon: 'none', title: '已经点过赞啦!' }) + return + } + try { + await submitUpvote({ + group: 'content.halo.run', + plural: 'moments', + name: current.metadata.name, + }) + uni.showToast({ icon: 'none', title: '点赞成功!' }) + upvotedNames.value.push(current.metadata.name) + if (current.stats) { + current.stats.upvote = (current.stats.upvote || 0) + 1 + } + } + catch (err) { + console.error('点赞失败', err) + uni.showToast({ icon: 'none', title: '点赞失败' }) + } +} + +/* ---------------- 评论 ---------------- */ +const commentModal = ref({ + show: false, + isComment: false, + postName: '', + title: '', +}) + +function handleToComment() { + const current = moment.value + if (!current) + return + if (!current.spec.allowComment) { + uni.showToast({ icon: 'none', title: '瞬间已开启禁止评论!' }) + return + } + commentModal.value = { + show: true, + isComment: true, + postName: current.metadata.name, + title: '新增评论', + } +} + +function handleOnCommentModalClose(data: { refresh: boolean, isSubmit: boolean }) { + commentModal.value.show = false +} + /* ---------------- 视频互斥 ---------------- */ function createVideoContexts(videos: { id?: string }[]) { stopAllVideos() @@ -329,9 +409,49 @@ onShareTimeline(() => ({ + + + + + + + 点赞 + + + + + 评论 + + + + + {{ momentFavorited ? '已收藏' : '收藏' }} + + + + + + + diff --git a/src/pages-blog/search/search.vue b/src/pages-blog/search/search.vue index 64f7df9..cf7823f 100644 --- a/src/pages-blog/search/search.vue +++ b/src/pages-blog/search/search.vue @@ -7,7 +7,6 @@ import { computed, ref } from 'vue' import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' import { getPostListByKeyword } from '@/api/halo' import { useAppConfigStore } from '@/store/appConfig' -import { usePluginAvailable } from '@/utils/plugin' import { markdownConfig } from '@/config/markdown' import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { debounce } from '@/utils/debounce' @@ -24,7 +23,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) /** 依赖插件(plugin-search-widget) */ const uniHaloPluginId = 'plugin-search-widget' -const uniHaloPluginAvailable = ref(true) +const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) /* ---------------- 状态 ---------------- */ const loading = ref<'loading' | 'success' | 'error'>('loading') @@ -122,7 +121,7 @@ function handleToTopPage(duration = 500) { /* ---------------- 生命周期 ---------------- */ onLoad(async () => { - uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId) + await checkPluginAvailable() if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() return diff --git a/src/pages-blog/votes/votes.vue b/src/pages-blog/votes/votes.vue index 870a3e2..e5fd71f 100644 --- a/src/pages-blog/votes/votes.vue +++ b/src/pages-blog/votes/votes.vue @@ -7,7 +7,6 @@ import { computed, ref } from 'vue' import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { getVoteList } from '@/api/uni-halo' import { useAppConfigStore } from '@/store/appConfig' -import { usePluginAvailable } from '@/utils/plugin' import type { IVoteItem } from '@/api/types/uni-halo' definePage({ @@ -22,7 +21,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) /** 依赖插件(plugin-vote) */ const uniHaloPluginId = 'plugin-vote' -const uniHaloPluginAvailable = ref(true) +const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) /* ---------------- 状态 ---------------- */ const loading = ref<'loading' | 'success' | 'error'>('loading') @@ -84,7 +83,7 @@ function handleToTopPage(duration = 500) { /* ---------------- 生命周期 ---------------- */ onLoad(async () => { - uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId) + await checkPluginAvailable() if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() return diff --git a/src/pages/maintenance/maintenance.vue b/src/pages/maintenance/maintenance.vue index a7e8ec2..1cd2d94 100644 --- a/src/pages/maintenance/maintenance.vue +++ b/src/pages/maintenance/maintenance.vue @@ -3,7 +3,6 @@ import { onLoad, onUnload } from '@dcloudio/uni-app' import { useAppConfigStore } from '@/store/appConfig' import { checkUrl } from '@/utils/url' - import { usePluginAvailable } from '@/utils/plugin' import type { IPublicMaintenance } from '@/api/types/uni-halo' definePage({ @@ -23,6 +22,8 @@ const RECOVERY_POLL_INTERVAL = 30 * 1000 const store = useAppConfigStore() + /** 插件可用性(拦截恢复检测用) */ + const { check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) const viewState = ref('loading') const maintenance = ref(null) @@ -146,7 +147,7 @@ try { await store.bootstrap({ force: true }) if (fromReason.value === 'plugin') { - const available = await usePluginAvailable(uniHaloPluginId) + const available = await checkPluginAvailable() if (!available) { const info = store.configs.maintenance if (info) { diff --git a/src/pages/tabbar/about/about.vue b/src/pages/tabbar/about/about.vue index 872ec13..2c9a28d 100644 --- a/src/pages/tabbar/about/about.vue +++ b/src/pages/tabbar/about/about.vue @@ -5,13 +5,13 @@ * 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块) */ import { computed, ref, watch } from 'vue' -import { onPullDownRefresh } from '@dcloudio/uni-app' +import { onPullDownRefresh, onShow } from '@dcloudio/uni-app' import { getBlogStatistics } from '@/api/halo' import { useAppConfigStore } from '@/store/appConfig' +import { useFavoritesStore } from '@/store/favorites' import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkHasAdminLogin } from '@/utils/auth' import { t } from '@/locale' -import { usePluginAvailable } from '@/utils/plugin' import type { IBlogStats } from '@/api/types/halo' definePage({ @@ -23,10 +23,13 @@ definePage({ }) const appConfigStore = useAppConfigStore() +const favoritesStore = useFavoritesStore() const haloConfigs = computed(() => appConfigStore.configs) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled) const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled) +/** 数据看板插件可用性(供导航项显隐判断) */ +const { check: checkDataVisualPlugin } = usePluginAvailable('plugin-data-statistics') /* ---------------- 计算属性 ---------------- */ const bloggerInfo = computed(() => { @@ -116,10 +119,28 @@ function toSolidColor(rgba: string) { return rgba.replace('0.95)', '1)') } +/** 收藏导航项右侧文案跟随收藏总数(收藏页返回/切回时刷新) */ +function syncFavoritesNavText() { + const nav = navList.value.find(n => n.key === 'favorites') + if (nav) { + nav.rightText = `共 ${favoritesStore.counts.total} 条收藏` + } +} + async function handleGetNavList() { - const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics') + const dataVisualAvailable = await checkDataVisualPlugin() navList.value = [ + { + key: 'favorites', + title: '我的收藏', + icon: 'star', + bgColor: 'rgba(255, 179, 0, 0.95)', + rightText: '', + path: '/pages-blog/favorites/favorites', + show: true, + group: 'blog', + }, { key: 'data-visual', title: '数据看板', @@ -217,6 +238,7 @@ async function handleGetNavList() { group: 'more', }, ] + syncFavoritesNavText() } /* ---------------- 数据加载 ---------------- */ @@ -269,6 +291,11 @@ watch(haloConfigs, () => { handleGetData() +// 从收藏页返回/切回时刷新收藏数文案 +onShow(() => { + syncFavoritesNavText() +}) + onPullDownRefresh(() => { handleGetData() }) diff --git a/src/pages/tabbar/gallery/gallery.vue b/src/pages/tabbar/gallery/gallery.vue index 50d8576..a759c50 100644 --- a/src/pages/tabbar/gallery/gallery.vue +++ b/src/pages/tabbar/gallery/gallery.vue @@ -9,7 +9,7 @@ import { useAppConfigStore } from '@/store/appConfig' import { checkImageUrl } from '@/utils/url' import { t } from '@/locale' - import { usePluginAvailable } from '@/utils/plugin' + import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import type { IPhoto, IPhotoGroup } from '@/api/types/halo' definePage({ @@ -27,10 +27,10 @@ /** 依赖插件(plugin-photos) */ const uniHaloPluginId = 'plugin-photos' - const uniHaloPluginAvailable = ref(true) + const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId, false) /* ---------------- 状态 ---------------- */ - const loading = ref<'loading' | 'success' | 'error' | 'empty'>('loading') + const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() const category = ref<{ activeIndex : number, list : IPhotoGroup[] }>({ activeIndex: 0, list: [], @@ -58,14 +58,12 @@ handleGetData(true) } else { - loading.value = 'success' loadMoreText.value = t('common.noMore') uni.stopPullDownRefresh() } } catch (e) { console.error(e) - loading.value = 'error' category.value = { activeIndex: 0, list: [] } } return @@ -82,7 +80,6 @@ } catch (e) { console.error(e) - loading.value = 'error' category.value = { activeIndex: 0, list: [] } } } @@ -94,7 +91,7 @@ } if (!isLoadMore.value) { - loading.value = 'loading' + updateLoadingStatus(DataLoadingStatusEnum.Loading) } loadMoreText.value = '' @@ -110,12 +107,12 @@ ? dataList.value.concat(list) : list } - loading.value = dataList.value.length !== 0 ? 'success' : 'empty' + updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success) loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore') } catch (err) { console.error(err) - loading.value = 'error' + updateLoadingStatus(DataLoadingStatusEnum.Error) loadMoreText.value = t('common.loadFailed') } finally { @@ -150,20 +147,18 @@ /* ---------------- 生命周期 ---------------- */ onLoad(async () => { // 检查插件可用性 - uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId) + await checkPluginAvailable() + console.log('uniHaloPluginAvailable',uniHaloPluginAvailable.value) if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() return } - }) - - watch(galleryConfig, (newVal) => { - if (!newVal) - return - uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') }) + + + // 开始正常数据请求 handleGetCategory() - }, { deep: true, immediate: true }) - + }) + onPullDownRefresh(() => { if (!uniHaloPluginAvailable.value) { uni.stopPullDownRefresh() @@ -211,9 +206,8 @@ - - - + diff --git a/src/pages/tabbar/home/home.vue b/src/pages/tabbar/home/home.vue index e1f6b4b..a222181 100644 --- a/src/pages/tabbar/home/home.vue +++ b/src/pages/tabbar/home/home.vue @@ -7,6 +7,7 @@ import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { t } from '@/locale' import { useMaintenanceIntercept } from '@/hooks/useMaintenanceIntercept' + import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import type { IPost } from '@/api/types/halo' definePage({ @@ -28,7 +29,7 @@ const haloConfigs = computed(() => appConfigStore.configs) /* ---------------- 状态 ---------------- */ - const loading = ref<'loading' | 'success' | 'error'>('loading') + const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() const isLoadMore = ref(false) const loadMoreText = ref(t('common.loading')) const articleList = ref([]) @@ -79,12 +80,12 @@ item.owner.avatar = checkAvatarUrl(item.owner.avatar) return item }) - loading.value = 'success' + updateLoadingStatus(articleList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success) loadMoreText.value = t('common.noMore') } catch (err) { console.error('获取审核文章失败', err) - loading.value = 'error' + updateLoadingStatus(DataLoadingStatusEnum.Error) loadMoreText.value = t('common.loadFailed') } finally { @@ -95,7 +96,7 @@ } if (!isLoadMore.value) { - loading.value = 'loading' + updateLoadingStatus(DataLoadingStatusEnum.Loading) } loadMoreText.value = t('common.loading') @@ -108,11 +109,11 @@ item.owner.avatar = checkAvatarUrl(item.owner.avatar) return item }) - loading.value = 'success' + updateLoadingStatus(articleList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success) loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore') } catch (err) { - loading.value = 'error' + updateLoadingStatus(DataLoadingStatusEnum.Error) loadMoreText.value = t('common.loadFailed') console.error('获取文章失败', err) } @@ -147,14 +148,14 @@ }, }) } - - function init(){ + + function init() { if (!intercepted.value) { handleQuery() } } init() - + /* ---------------- 生命周期 ---------------- */ // 维护检查 @@ -183,55 +184,48 @@ uni.showToast({ icon: 'none', title: t('common.noMoreData') }) } }) -