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

refactor: 重构项目页面与组件,优化整体结构与体验

1.  重构分类页面路由与实现,替换旧分类详情页为文章列表页
2.  统一使用自定义导航栏,替换原有原生导航配置
3.  重构加载状态管理,统一使用useDataLoadingStatus hook
4.  优化插件可用性检查逻辑与组件展示
5.  调整数据加载占位与空状态样式
6.  优化文章卡片样式与交互,新增分类跳转功能
7.  重构设置页枚举选择弹窗,使用wd-picker替换自定义实现
8.  删除废弃的IPluginAvailable类型与相关代码
This commit is contained in:
小莫唐尼
2026-09-04 20:31:46 +08:00
parent 56ce29e69e
commit 02fa9e4294
27 changed files with 727 additions and 630 deletions
+1 -2
View File
@@ -24,7 +24,6 @@ import type {
IPhotoGroupListRes, IPhotoGroupListRes,
IPhotoListReq, IPhotoListReq,
IPhotoListRes, IPhotoListRes,
IPluginAvailable,
IPost, IPost,
IPostListReq, IPostListReq,
IPostListRes, IPostListRes,
@@ -293,7 +292,7 @@ export function postTrackersCounter(data: ITrackerCounterReq) {
* 检查插件是否可用 * 检查插件是否可用
*/ */
export function checkPluginAvailable(name: string) { export function checkPluginAvailable(name: string) {
return http.Get<IResponse<IPluginAvailable>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, { return http.Get<IResponse<boolean>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
cacheFor: 0, cacheFor: 0,
meta: { requestFrom: RequestFrom.Halo }, meta: { requestFrom: RequestFrom.Halo },
}) })
-7
View File
@@ -456,10 +456,3 @@ export interface IUpvoteReq {
name: string name: string
plural?: string plural?: string
} }
/* ---------- 插件可用性 ---------- */
export interface IPluginAvailable {
available: boolean
reason?: string
}
@@ -1,79 +1,105 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { checkThumbnailUrl } from '@/utils/url' import { checkThumbnailUrl } from '@/utils/url'
import { useSettingStore } from '@/store/setting' import { useSettingStore } from '@/store/setting'
import { formatTime} from '@/utils/formatTime' import { formatTime } from '@/utils/formatTime'
import type { IPost } from '@/api/types/halo' import type { IPost,ICategory } from '@/api/types/halo'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
/** 来源页面(home 时跟随首页布局) */ from ?: string
from?: string auditMode?: boolean
article: IPost article : IPost
}>(), { }>(), {
from: '', auditMode: false,
}) from: '',
})
const emit = defineEmits<{ const settingStore = useSettingStore()
(e: 'on-click', article: IPost): void
}>()
const settingStore = useSettingStore() /** 卡片布局 class(由全局设置 layout 决定) */
const cardType = computed(() => {
const layout = settingStore.settings.layout
// 首页双列时强制上图下文布局,除非显式指定其他
if (props.from === 'home' && layout.home === 'h_row_col2') {
if (!['tb_image_text', 'tb_text_image', 'only_text'].includes(layout.cardType)) {
return [props.from, layout.home, 'tb_image_text']
}
return [props.from, layout.home, layout.cardType]
}
return [layout.home, layout.cardType]
})
/** 卡片布局 class(由全局设置 layout 决定) */ /** 发布时间格式化 yyyy-MM-dd */
const cardType = computed(() => { const publishTimeText = computed(() => {
const layout = settingStore.settings.layout const time = props.article.spec.publishTime
// 首页双列时强制上图下文布局,除非显式指定其他 return time ? formatTime({ d: time, f: 'yyyy-MM-dd' }) : ''
if (props.from === 'home' && layout.home === 'h_row_col2') { })
if (!['tb_image_text', 'tb_text_image', 'only_text'].includes(layout.cardType)) {
return [props.from, layout.home, 'tb_image_text']
}
return [props.from, layout.home, layout.cardType]
}
return [layout.home, layout.cardType]
})
/** 发布时间格式化 yyyy-MM-dd */ /** 阅读数(兼容 status.stats.visits 与旧版顶层 stats.visit) */
const publishTimeText = computed(() => { const visitCount = computed(() => {
const time = props.article.spec.publishTime return props.article.status?.stats?.visits ?? props.article.stats?.visit ?? 0
return time ? formatTime({d:time,f:'yyyy-MM-dd'}) : '' })
})
/** 阅读数(兼容 status.stats.visits 与旧版顶层 stats.visit) */ function handleToArticleDetail() {
const visitCount = computed(() => { uni.navigateTo({
return props.article.status?.stats?.visits ?? props.article.stats?.visit ?? 0 url: `/pages-blog/article-detail/article-detail?name=${props.article.metadata.name}`,
}) animationType: 'slide-in-right',
})
}
function handleClick() { function handleToCategory(category : ICategory) {
emit('on-click', props.article) if (props.auditMode) {
} return
}
uni.navigateTo({
url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
</script> </script>
<template> <template>
<view class="uh-global-card-glass overflow-hidden relative rounded-xl p-3" @click="handleClick"> <view class="uh-global-card-glass uh-shadow-xs overflow-hidden relative rounded-xl p-3"
<!-- v-if="article.spec.pinned" --> @click.stop="handleToArticleDetail()">
<text class="absolute right-6 top-6 z-1 bg-secondary text-gray-60 text-xs px-2 py-1 rounded-lg"> 置顶 </text> <text v-if="article.spec.pinned"
<image class="w-full h-36 rounded-lg" :src="checkThumbnailUrl(article.spec.cover)" mode="aspectFill" lazy-load /> class="absolute right-6 top-6 z-1 bg-secondary text-gray-60 text-xs px-2 py-1 rounded-lg"> 置顶 </text>
<view class="flex flex-col w-full gap-y-2 text-sm"> <image class="w-full h-36 rounded-lg" :src="checkThumbnailUrl(article.spec.cover)" mode="aspectFill"
<view class="mt-2 font-bold truncate"> lazy-load />
{{ article.spec.title }} <view class="flex flex-col w-full gap-y-2 text-sm">
</view> <view class="mt-2 font-bold truncate">
<view class="content line-clamp-2 text-gray-600"> {{ article.spec.title }}
{{ article.status?.excerpt }} </view>
</view> <view class="content line-clamp-2 text-gray-600">
<view class="flex items-center justify-between text-xs text-gray-500"> {{ article.status?.excerpt }}
<view class="flex items-center gap-x-1"> </view>
<image :src="article.owner.avatar" class="uh-global-card-glass rounded-full w-5 h-5" mode="aspectFill"></image> <view class="my-1 box-border flex flex-wrap gap-2">
<text>{{article.owner.displayName}}</text> <template v-if="article.categories && article.categories.length !== 0">
<text v-for="cate in article.categories" :key="cate.metadata.name"
class="py-1 px-2 text-xs rounded-xl bg-secondary" @click.stop="handleToCategory(cate)">
{{ cate.spec.displayName }}
</text>
</template>
<template v-if="article.tags && article.tags.length !== 0">
<text v-for="tag in article.tags" :key="tag.metadata.name"
class="py-1 px-2 text-xs rounded-xl bg-secondary">
# {{ tag.spec.displayName }}
</text>
</template>
</view>
<view class="flex items-center justify-between text-xs text-gray-500">
<view class="flex items-center gap-x-1">
<image :src="article.owner.avatar" class="uh-global-card-glass rounded-full w-5 h-5"
mode="aspectFill"></image>
<text>{{article.owner.displayName}}</text>
</view>
<view class="flex items-center gap-x-2">
{{ publishTimeText }}
</view>
<view class="visits">
浏览
<text class="number">{{ visitCount }}</text>
</view>
</view>
</view> </view>
<view class="flex items-center gap-x-2"> </view>
{{ publishTimeText }}
</view>
<view class="visits">
浏览
<text class="number">{{ visitCount }}</text>
</view>
</view>
</view>
</view>
</template> </template>
@@ -18,7 +18,7 @@ interface IProps {
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
loadingStatus: 'loading', loadingStatus: 'loading',
minHeight: '75vh', minHeight: '80vh',
loadingText: '稍等,正在加载中哦', loadingText: '稍等,正在加载中哦',
errorText: '哎呀,加载失败了呢~', errorText: '哎呀,加载失败了呢~',
emptyText: '啊偶,暂时没有数据呢~', emptyText: '啊偶,暂时没有数据呢~',
@@ -57,7 +57,7 @@
if (calcAuditModeEnabled.value) if (calcAuditModeEnabled.value)
return return
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`, url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
}) })
} }
@@ -3,17 +3,11 @@
import { NeedPlugins } from '@/hooks/usePluginAvailable' import { NeedPlugins } from '@/hooks/usePluginAvailable'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
/** 插件名称(与 NeedPlugins 中的 id 对应) */
pluginId : string pluginId : string
errorText ?: string errorText ?: string
useDecoration ?: boolean checking : boolean
useBorder ?: boolean
customStyle ?: Record<string, string>
}>(), { }>(), {
errorText: '', errorText: '',
useDecoration: true,
useBorder: true,
customStyle: () => ({}),
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -25,75 +19,40 @@
const info = NeedPlugins.get(props.pluginId) const info = NeedPlugins.get(props.pluginId)
return info || { return info || {
id: props.pluginId, id: props.pluginId,
name: props.pluginId, name: props.name,
desc: '', desc: '',
logo: '',
url: '', url: '',
} }
}) })
const defaultStyle = { function handleRefresh() {
width: '80vw', if (props.checking) { return }
borderRadius: '24rpx', emit('on-refresh')
} }
const calcCustomStyle = computed(() => ({
...defaultStyle,
...props.customStyle,
}))
</script> </script>
<template> <template>
<view v-if="pluginInfo" <view v-if="pluginInfo" class="mx-auto my-auto box-border flex flex-col items-center justify-center gap-6 text-sm">
class="uh-plugin-unavailable mx-auto my-auto box-border flex flex-col gap-6 p-10 text-[28rpx]"
:class="{ border: useBorder, decoration: useDecoration }" :style="[calcCustomStyle]"> <wd-icon class-prefix="uhemoji-icon" name="-cry" size="160rpx"></wd-icon>
<!-- 图标 -->
<image class="plugin-logo box-border h-[120rpx] w-[120rpx] rounded-3xl" :src="pluginInfo.logo" <view class="box-border text-lg text-gray-900 font-bold">
mode="scaleToFill" />
<!-- 名称 -->
<view class="plugin-name box-border text-[32rpx] text-[#333] font-bold">
{{ pluginInfo.name }} {{ pluginInfo.name }}
</view> </view>
<!-- 自定义错误提示 --> <view v-if="errorText" class=" text-yellow-500 text-sm">
<view v-if="errorText" class="plugin-tip box-border border-2 rounded-xl border-dashed px-5 py-2.5 text-[24rpx]"
style="border-color: #f2c97d; color: #f0a020;">
{{ errorText }} {{ errorText }}
</view> </view>
<!-- 反馈按钮/复制地址 --> <view class="w-full flex flex-col gap-y-4">
<view class="plugin-btns box-border w-full"> <uh-button custom-class="!rounded-full py-2 !uh-shadow-xs" @click="handleRefresh()">
{{props.checking?'正在刷新':'刷新试试'}}
</uh-button>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<wd-button type="warning" plain block size="medium" open-type="contact"> <uh-button custom-class="bg-white py-2 !rounded-full" open-type="contact">
提交反馈 提交反馈
</wd-button> </uh-button>
<!-- #endif --> <!-- #endif -->
</view> </view>
<!-- 刷新按钮 -->
<view class="flex justify-center">
<wd-button size="small" plain type="info" @click="emit('on-refresh')">
刷新试试
</wd-button>
</view>
<view class="plugin-copyright text-[20rpx] text-[#999]" style="transform: scale(0.9) translateY(20px);">
提示请确保 Halo 博客已安装相关插件
</view>
</view> </view>
</template> </template>
<style scoped lang="scss">
.uh-plugin-unavailable {
&.border {
border: 2rpx solid #eee;
}
&.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);
}
}
</style>
+50 -49
View File
@@ -1,27 +1,19 @@
/** import { useAppConfigStore } from '@/store/appConfig';
* 维护拦截 hook(2026-09-04)
* 统一「主插件未激活 / 维护模式开启」两项检查与维护页跳转,供入口页与首页等
* 页面复用(auto-import 已配置 src/hooks,页面直接调用无需 import)。
* 拦截规则:任一命中即跳转 /pages/maintenance/maintenance?from=reason,
* 维护页按 reason 展示默认(未配置维护信息)或配置文案。设计见插件
* .docs/maintenance-config-design.md §8。
*/
import { useAppConfigStore } from '@/store/appConfig'
/** 维护拦截原因:plugin 主插件未激活 / maintenance 维护模式开启 */ /** 维护拦截原因:plugin 主插件未激活 / maintenance 维护模式开启 */
export type MaintenanceInterceptReason = 'plugin' | 'maintenance' export type MaintenanceInterceptReason = 'plugin' | 'maintenance';
export interface IMaintenanceInterceptResult { export interface IMaintenanceInterceptResult {
/** 是否命中拦截(需要跳转维护页) */ /** 是否命中拦截(需要跳转维护页) */
intercepted: boolean intercepted: boolean;
/** 命中原因;未命中为 null */ /** 命中原因;未命中为 null */
reason: MaintenanceInterceptReason | null reason: MaintenanceInterceptReason | null;
} }
/** 维护页路径 */ /** 维护页路径 */
export const MAINTENANCE_PAGE_PATH = '/pages/maintenance/maintenance' export const MAINTENANCE_PAGE_PATH = '/pages/maintenance/maintenance';
/** 主插件 ID(与 utils/plugin NeedPluginIds.PluginUniHalo 一致) */ /** 主插件 ID(与 utils/plugin NeedPluginIds.PluginUniHalo 一致) */
export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo' export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo';
/** /**
* 维护拦截能力:检查 + 跳转封装 * 维护拦截能力:检查 + 跳转封装
@@ -31,43 +23,52 @@ export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo'
* onLoad(async () => { if (await interceptOrContinue()) return }) * onLoad(async () => { if (await interceptOrContinue()) return })
*/ */
export function useMaintenanceIntercept() { export function useMaintenanceIntercept() {
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore();
/** 主插件可用性 hook(checkIntercept 内 await check 后读取 available) */ const reason = ref<MaintenanceInterceptReason | null>(null);
const { available: pluginAvailable, check: checkPluginAvailable } = usePluginAvailable(MAINTENANCE_PLUGIN_ID) /** 主插件可用性 hook(checkIntercept 内 await check 后读取 available) */
const { available: pluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: MAINTENANCE_PLUGIN_ID
});
/** /**
* 检查是否命中拦截(插件可用性 + 维护模式)。 * 检查是否命中拦截(插件可用性 + 维护模式)。
* @param force 是否强制刷新配置(默认 false 走 bootstrap TTL 缓存) * @param force 是否强制刷新配置(默认 false 走 bootstrap TTL 缓存)
*/ */
async function checkIntercept(force = false): Promise<IMaintenanceInterceptResult> { async function checkIntercept(force = false): Promise<IMaintenanceInterceptResult> {
await checkPluginAvailable() await checkPluginAvailable();
if (!pluginAvailable.value) if (!pluginAvailable.value) {
return { intercepted: true, reason: 'plugin' } reason.value = 'plugin';
return { intercepted: true, reason: 'plugin' };
}
const { ok } = await appConfigStore.bootstrap({ force }) const { ok } = await appConfigStore.bootstrap({ force });
if (!ok) if (!ok) {
return { intercepted: false, reason: null } reason.value = null;
if (appConfigStore.configs.maintenance) return { intercepted: false, reason: null };
return { intercepted: true, reason: 'maintenance' } }
return { intercepted: false, reason: null } if (appConfigStore.configs.maintenance) {
} reason.value = 'maintenance';
return { intercepted: true, reason: 'maintenance' };
}
/** 跳转维护页(带原因参数,供维护页区分默认/配置文案) */ return { intercepted: false, reason: null };
function redirectToMaintenance(reason: MaintenanceInterceptReason) { }
uni.redirectTo({ url: `${MAINTENANCE_PAGE_PATH}?from=${reason}` })
}
/** /** 跳转维护页(带原因参数,供维护页区分默认/配置文案) */
* 一站式:检查并跳转维护页。 function redirectToMaintenance(reason: MaintenanceInterceptReason) {
* @returns true = 已命中并跳转,调用方应中断后续逻辑;false = 放行 uni.redirectTo({ url: `${MAINTENANCE_PAGE_PATH}?from=${reason}` });
*/ }
async function interceptOrContinue(force = false): Promise<boolean> {
const { intercepted, reason } = await checkIntercept(force)
if (intercepted && reason)
redirectToMaintenance(reason)
return intercepted
}
return { checkIntercept, redirectToMaintenance, interceptOrContinue } /**
* 一站式:检查并跳转维护页。
* @returns true = 已命中并跳转,调用方应中断后续逻辑;false = 放行
*/
async function interceptOrContinue(force = false): Promise<boolean> {
const { intercepted, reason } = await checkIntercept(force);
if (intercepted && reason) redirectToMaintenance(reason);
return intercepted;
}
return { reason, checkIntercept, redirectToMaintenance, interceptOrContinue };
} }
+138 -136
View File
@@ -15,150 +15,152 @@
* handleGetData() * handleGetData()
* }) * })
*/ */
import { ref } from 'vue' import { ref } from 'vue';
import { checkPluginAvailable } from '@/api/halo' import { checkPluginAvailable } from '@/api/halo';
import { checkUrl } from '@/utils/url' import { checkUrl } from '@/utils/url';
/** 依赖插件 ID 常量 */ /** 依赖插件 ID 常量 */
export const NeedPluginIds = Object.freeze({ export const NeedPluginIds = Object.freeze({
PluginUniHalo: 'plugin-uni-halo', PluginUniHalo: 'plugin-uni-halo',
PluginPhotos: 'PluginPhotos', PluginPhotos: 'PluginPhotos',
PluginLinks: 'PluginLinks', PluginLinks: 'PluginLinks',
PluginMoments: 'PluginMoments', PluginMoments: 'PluginMoments',
PluginSearchWidget: 'PluginSearchWidget', PluginSearchWidget: 'PluginSearchWidget',
PluginCommentWidget: 'PluginCommentWidget', PluginCommentWidget: 'PluginCommentWidget',
PluginVote: 'vote', PluginVote: 'vote',
PluginDataStatistics: 'data-statistics', PluginDataStatistics: 'data-statistics'
}) });
interface IPluginAvailableOption {
pluginId: string;
// 用于提示用户插件未安装或未启用时显示的提示信息
tips?: string;
// 是否直接隐藏UI,不显示 uh-plugin-unavailable 组件
hideUI?: boolean;
// 是否默认启用插件
initalAvailable?: boolean;
// 检查完成后的回调函数
callback?: (available: boolean) => void;
}
export interface IPluginInfo { export interface IPluginInfo {
id: string id: string;
name: string name: string;
desc: string desc: string;
logo: string logo: string;
url: string url: string;
} }
/** 依赖插件清单 */ /** 依赖插件清单 */
export const NeedPlugins = new Map<string, IPluginInfo>([ export const NeedPlugins = new Map<string, IPluginInfo>([
[ [
NeedPluginIds.PluginUniHalo, NeedPluginIds.PluginUniHalo,
{ {
id: 'plugin-uni-halo', id: 'plugin-uni-halo',
name: 'UniHalo配置', name: 'UniHalo配置',
desc: 'uni-halo 核心插件,未安装和启用的情况下,将无法使用 uni-halo,请检查是否已安装和启用', desc: 'uni-halo 核心插件,未安装和启用的情况下,将无法使用 uni-halo,请检查是否已安装和启用',
logo: checkUrl('/plugins/plugin-uni-halo/assets/logo.png'), logo: checkUrl('/plugins/plugin-uni-halo/assets/logo.png'),
url: 'https://www.halo.run/store/apps/app-ryemX', url: 'https://www.halo.run/store/apps/app-ryemX'
}, }
], ],
[ [
NeedPluginIds.PluginPhotos, NeedPluginIds.PluginPhotos,
{ {
id: 'PluginPhotos', id: 'PluginPhotos',
name: '图库管理', name: '图库管理',
desc: '图库功能模块所需要的插件', desc: '图库功能模块所需要的插件',
logo: checkUrl('/plugins/PluginPhotos/assets/logo.svg'), logo: checkUrl('/plugins/PluginPhotos/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-BmQJW', url: 'https://www.halo.run/store/apps/app-BmQJW'
}, }
], ],
[ [
NeedPluginIds.PluginLinks, NeedPluginIds.PluginLinks,
{ {
id: 'PluginLinks', id: 'PluginLinks',
name: '链接管理', name: '链接管理',
desc: '链接管理模块,用于网站友情链接功能模块', desc: '链接管理模块,用于网站友情链接功能模块',
logo: checkUrl('/plugins/PluginLinks/assets/logo.svg'), logo: checkUrl('/plugins/PluginLinks/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-hfbQg', url: 'https://www.halo.run/store/apps/app-hfbQg'
}, }
], ],
[ [
NeedPluginIds.PluginMoments, NeedPluginIds.PluginMoments,
{ {
id: 'PluginMoments', id: 'PluginMoments',
name: '瞬间', name: '瞬间',
desc: '提供一个轻量级的内容图文、视频、音频等内容展示', desc: '提供一个轻量级的内容图文、视频、音频等内容展示',
logo: checkUrl('/plugins/PluginMoments/assets/logo.svg'), logo: checkUrl('/plugins/PluginMoments/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-SnwWD', url: 'https://www.halo.run/store/apps/app-SnwWD'
}, }
], ],
[ [
NeedPluginIds.PluginSearchWidget, NeedPluginIds.PluginSearchWidget,
{ {
id: 'PluginSearchWidget', id: 'PluginSearchWidget',
name: '搜索组件', name: '搜索组件',
desc: '为应用提供统一的搜索组件', desc: '为应用提供统一的搜索组件',
logo: checkUrl('/plugins/PluginSearchWidget/assets/logo.svg'), logo: checkUrl('/plugins/PluginSearchWidget/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-DlacW', url: 'https://www.halo.run/store/apps/app-DlacW'
}, }
], ],
[ [
NeedPluginIds.PluginCommentWidget, NeedPluginIds.PluginCommentWidget,
{ {
id: 'PluginCommentWidget', id: 'PluginCommentWidget',
name: '评论组件', name: '评论组件',
desc: '为用户前台提供完整的评论解决方案', desc: '为用户前台提供完整的评论解决方案',
logo: checkUrl('/plugins/PluginCommentWidget/assets/logo.svg'), logo: checkUrl('/plugins/PluginCommentWidget/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-YXyaD', url: 'https://www.halo.run/store/apps/app-YXyaD'
}, }
], ],
[ [
NeedPluginIds.PluginVote, NeedPluginIds.PluginVote,
{ {
id: 'vote', id: 'vote',
name: '投票管理', name: '投票管理',
desc: '投票模块所需要的插件,用于展示投票和提交投票', desc: '投票模块所需要的插件,用于展示投票和提交投票',
logo: checkUrl('/plugins/vote/assets/logo.png'), logo: checkUrl('/plugins/vote/assets/logo.png'),
url: 'https://www.halo.run/store/apps/app-veyvzyhv', url: 'https://www.halo.run/store/apps/app-veyvzyhv'
}, }
], ],
[ [
NeedPluginIds.PluginDataStatistics, NeedPluginIds.PluginDataStatistics,
{ {
id: 'data-statistics', id: 'data-statistics',
name: '数据看板', name: '数据看板',
desc: '为 Halo2 提供强大的数据可视化统计功能,支持 Umami 流量统计、uptime、网站内部数据图表(标签、分类、文章趋势、评论排行、热门文章等)', desc: '为 Halo2 提供强大的数据可视化统计功能,支持 Umami 流量统计、uptime、网站内部数据图表(标签、分类、文章趋势、评论排行、热门文章等)',
logo: checkUrl('/plugins/data-statistics/assets/logo.svg'), logo: checkUrl('/plugins/data-statistics/assets/logo.svg'),
url: 'https://www.halo.run/store/apps/app-rtnbbgfk', url: 'https://www.halo.run/store/apps/app-rtnbbgfk'
}, }
], ]
]) ]);
/** export function usePluginAvailable(option: IPluginAvailableOption) {
* 检查插件是否启用、安装 const { pluginId, callback, tips = '功能正在开发中...', initalAvailable = false } = option;
* @param pluginId 插件 id const checking = ref(false);
* @returns true = 安装、启用;false = 未安装启用 const available = ref(initalAvailable ?? false);
*/
export async function checkNeedPluginAvailable(pluginId: string): Promise<boolean> { /**
try { * 检查插件是否启用、安装
const available = await checkPluginAvailable(pluginId) */
return available?.data?.available !== false function check(): Promise<boolean> {
} return new Promise<boolean>(async (resolve) => {
catch (err) { try {
console.error(`检查插件 ${pluginId} 可用性失败`, err) checking.value = true;
return false const result = await checkPluginAvailable(pluginId);
} console.log(`检查插件 ${pluginId} 可用性成功`, result);
} available.value = result.data;
resolve(result.data);
export function usePluginAvailable(pluginId: string, initial = true) { } catch (err) {
/** 插件是否可用(默认 true,避免首帧闪现插件不可用占位;需要先置 false 的页面传 initial=false) */ console.error(`检查插件 ${pluginId} 可用性失败`, err);
const available = ref(initial) available.value = false;
/** 是否校验中 */ resolve(false);
const checking = ref(false) } finally {
typeof callback === 'function' && callback(available.value);
/** checking.value = false;
* 执行插件可用性校验(刷新 available) }
* @returns 当前是否可用(与 available.value 一致,便于一次性调用方直接取返回值) });
*/ }
async function check(): Promise<boolean> {
checking.value = true return { pluginId, tips, checking, available, check };
try {
available.value = await checkNeedPluginAvailable(pluginId)
return available.value
}
finally {
checking.value = false
}
}
return { available, checking, check }
} }
+5 -1
View File
@@ -8,6 +8,7 @@ import { onLoad } from '@dcloudio/uni-app'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '关于项目', navigationBarTitleText: '关于项目',
navigationStyle: 'custom',
}, },
}) })
@@ -37,6 +38,9 @@ onLoad(() => {
<template> <template>
<view class="app-page box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-6"> <view class="app-page box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-6">
<!-- 自定义导航 -->
<uh-navbar default-title="关于项目" title-color="text-gray-900" />
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) --> <!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
<view class="hero-wrap relative"> <view class="hero-wrap relative">
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" /> <view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
@@ -68,7 +72,7 @@ onLoad(() => {
:class="index < links.length - 1 ? 'border-b border-black/5' : ''" :class="index < links.length - 1 ? 'border-b border-black/5' : ''"
@click="copyText(link.copy, link.tip)" @click="copyText(link.copy, link.tip)"
> >
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5" :style="{ backgroundColor: link.tileColor + '1A' }"> <view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5" :style="{ backgroundColor: `${link.tileColor}1A` }">
<text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text> <text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text>
</view> </view>
<view class="min-w-0 flex flex-1 flex-col"> <view class="min-w-0 flex flex-1 flex-col">
+44 -21
View File
@@ -8,6 +8,7 @@ import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPostList } from '@/api/halo' import { getPostList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting' import { useSettingStore } from '@/store/setting'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkThumbnailUrl } from '@/utils/url' import { checkThumbnailUrl } from '@/utils/url'
import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IPost } from '@/api/types/halo' import type { IPost } from '@/api/types/halo'
@@ -16,6 +17,7 @@ definePage({
style: { style: {
navigationBarTitleText: '归档', navigationBarTitleText: '归档',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
@@ -26,7 +28,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const globalAppSettings = computed(() => settingStore.settings) const globalAppSettings = computed(() => settingStore.settings)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const activeTabIndex = ref(0) const activeTabIndex = ref(0)
const queryParams = ref({ size: 10, page: 1 }) const queryParams = ref({ size: 10, page: 1 })
const result = ref<{ hasNext: boolean }>({ hasNext: false }) const result = ref<{ hasNext: boolean }>({ hasNext: false })
@@ -125,14 +127,16 @@ async function handleGetData() {
const posts = handleGetPosts(filtered) const posts = handleGetPosts(filtered)
dataList.value = handleGetShowDataList(posts) dataList.value = handleGetShowDataList(posts)
cacheDataList.value = filtered cacheDataList.value = filtered
loading.value = 'success' updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = '呜呜,没有更多数据啦~' loadMoreText.value = '呜呜,没有更多数据啦~'
uni.hideLoading() uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
return return
@@ -142,7 +146,7 @@ async function handleGetData() {
uni.showLoading({ title: '加载中...' }) uni.showLoading({ title: '加载中...' })
} }
else { else {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
loadMoreText.value = '加载中...' loadMoreText.value = '加载中...'
@@ -178,12 +182,14 @@ async function handleGetData() {
cacheDataList.value = res.data.items cacheDataList.value = res.data.items
} }
loading.value = 'success' updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
@@ -192,6 +198,12 @@ async function handleGetData() {
} }
} }
/** 顶部 tab 定义(同收藏页胶囊 chip) */
const archiveTabs = [
{ key: 'month', label: '按月份查看' },
{ key: 'year', label: '按年份查看' },
]
function handleOnTabChange(e: { index: number }) { function handleOnTabChange(e: { index: number }) {
activeTabIndex.value = e.index activeTabIndex.value = e.index
queryParams.value.page = 1 queryParams.value.page = 1
@@ -250,27 +262,38 @@ onReachBottom(() => {
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page"> <view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 顶部 tab(玻璃吸顶,wd-tabs 需用 wd-tab 子组件声明页签) --> <!-- 自定义导航 -->
<view class="archive-tabs uh-global-card-glass sticky top-0 z-10"> <uh-navbar default-title="归档" title-color="text-gray-900" />
<wd-tabs v-model="activeTabIndex" align="center" custom-style="background: transparent;" @change="handleOnTabChange">
<wd-tab title="按月份查看" />
<wd-tab title="按年份查看" />
</wd-tabs>
</view>
<!-- 加载/错误占位 --> <!-- 顶部 tab(吸顶玻璃胶囊 chip,同收藏页) -->
<view v-if="loading !== 'success'"> <wd-sticky>
<uh-data-loading :loading-status="loading" @refresh="handleGetData" /> <scroll-view scroll-x class="w-full whitespace-nowrap">
<view class="flex gap-2 px-3 pb-1 pt-3">
<view
v-for="(tab, index) in archiveTabs" :key="tab.key"
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
@click="handleOnTabChange({ index })"
>
{{ tab.label }}
</view>
</view>
</scroll-view>
</wd-sticky>
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
:loading-status="loadingStatus"
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
@refresh="handleGetData"
/>
</view> </view>
<!-- 内容区域 --> <!-- 内容区域 -->
<block v-else> <block v-else>
<view v-if="dataList.length === 0" class="list-empty min-h-[60vh] flex items-center justify-center">
<wd-empty :description="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'" />
</view>
<!-- 时间线 --> <!-- 时间线 -->
<view v-else class="timeline px-4 pt-3"> <view class="timeline px-4 pt-3">
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex"> <view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
<view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center"> <view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center">
<view class="timeline-dot mt-2 h-4 w-4 rounded-full bg-secondary shadow-[0_0_0_8rpx_rgba(215,249,76,0.3)]" /> <view class="timeline-dot mt-2 h-4 w-4 rounded-full bg-secondary shadow-[0_0_0_8rpx_rgba(215,249,76,0.3)]" />
@@ -387,7 +387,7 @@
/* ---------------- 跳转 ---------------- */ /* ---------------- 跳转 ---------------- */
function handleToCate(category : { metadata : { name : string }, spec : { displayName : string } }) { function handleToCate(category : { metadata : { name : string }, spec : { displayName : string } }) {
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`, url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
}) })
} }
+33
View File
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
definePage({
style: {
navigationBarTitleText: '文章列表',
navigationStyle: 'custom',
},
})
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
setTimeout(() => {
updateLoadingStatus(DataLoadingStatusEnum.Success)
}, 3000)
</script>
<template>
<view class="w-full min-h-screen bg-page">
<uh-navbar default-title="文章列表" :need-placeholder="true" title-color="text-gray-900"></uh-navbar>
<!-- 内容区由于 uh-navbar 内置有占位所以我们的页面的主要内容应该从这里开始比如这里就可以设置内边距或者其他样式最外层的 <view class="w-full min-h-screen bg-page"> 仅作为容器-->
<view class="box-border px-3">
<!-- 加载状态 -->
<uh-data-loading v-if="loadingStatus!==DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="80vh"></uh-data-loading>
<!-- 实际内容 -->
<view v-else>
请求成功啦
</view>
</view>
</view>
</template>
@@ -1,24 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 分类详情页(源自旧项目 pagesA/category-detail,新建复刻)
* 展示某分类下的文章列表,分页加载
*/
import { ref } from 'vue' import { ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getCategoryPostList } from '@/api/halo' import { getCategoryPostList } from '@/api/halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IPost } from '@/api/types/halo' import type { IPost } from '@/api/types/halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '分类详情', navigationBarTitleText: '分类详情',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({ size: 10, page: 0 }) const queryParams = ref({ size: 10, page: 0 })
const name = ref('') const name = ref('')
const pageTitle = ref('加载中...') const pageTitle = ref('加载中...')
const navbarTitle = ref('分类详情')
const hasNext = ref(false) const hasNext = ref(false)
const dataList = ref<IPost[]>([]) const dataList = ref<IPost[]>([])
const isLoadMore = ref(false) const isLoadMore = ref(false)
@@ -26,25 +25,27 @@ const loadMoreText = ref('')
async function handleGetData() { async function handleGetData() {
if (!isLoadMore.value) { if (!isLoadMore.value) {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
loadMoreText.value = '加载中...' loadMoreText.value = '加载中...'
try { try {
const res = await getCategoryPostList(name.value, { ...queryParams.value }) const res = await getCategoryPostList(name.value, { ...queryParams.value })
uni.setNavigationBarTitle({ title: `${pageTitle.value} (共${res.data.total}篇)` }) navbarTitle.value = `${pageTitle.value} (共${res.data.total}篇)`
hasNext.value = res.data.hasNext hasNext.value = res.data.hasNext
dataList.value = isLoadMore.value dataList.value = isLoadMore.value
? dataList.value.concat(res.data.items) ? dataList.value.concat(res.data.items)
: res.data.items : res.data.items
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
setTimeout(() => { setTimeout(() => {
loading.value = 'success' updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
}, 500) }, 500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
@@ -74,6 +75,7 @@ function handleToTopPage(duration = 500) {
onLoad((options) => { onLoad((options) => {
name.value = options?.name || '' name.value = options?.name || ''
pageTitle.value = options?.title || '分类详情' pageTitle.value = options?.title || '分类详情'
navbarTitle.value = pageTitle.value
handleGetData() handleGetData()
}) })
@@ -96,39 +98,40 @@ onReachBottom(() => {
onShareAppMessage(() => ({ onShareAppMessage(() => ({
title: pageTitle.value, title: pageTitle.value,
path: `/pages-blog/category-detail/category-detail?name=${name.value}&title=${pageTitle.value}`, path: `/pages-blog/category-articles/category-articles?name=${name.value}&title=${pageTitle.value}`,
})) }))
onShareTimeline(() => ({ onShareTimeline(() => ({
title: pageTitle.value, title: pageTitle.value,
path: `/pages-blog/category-detail/category-detail?name=${name.value}&title=${pageTitle.value}`, path: `/pages-blog/category-articles/category-articles?name=${name.value}&title=${pageTitle.value}`,
})) }))
</script> </script>
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col py-6" style="background-color: #fafafd;"> <view class="app-page min-h-screen w-screen flex flex-col" style="background-color: #fafafd;">
<!-- 加载/错误占位 --> <!-- 自定义导航 -->
<view v-if="loading !== 'success'"> <uh-navbar :default-title="navbarTitle" title-color="text-gray-900" />
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
:loading-status="loadingStatus"
empty-text="该分类下暂无文章"
@refresh="handleGetData"
/>
</view> </view>
<block v-else> <block v-else>
<view v-if="dataList.length === 0" class="empty h-[60vh] flex items-center justify-center"> <uh-article-card
<wd-empty description="该分类下暂无文章" /> v-for="(article, index) in dataList"
:key="index"
:article="article"
@on-click="handleToArticleDetail"
/>
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
{{ loadMoreText }}
</view> </view>
<block v-else>
<uh-article-card
v-for="(article, index) in dataList"
:key="index"
:article="article"
@on-click="handleToArticleDetail"
/>
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
{{ loadMoreText }}
</view>
</block>
<view class="to-top-btn fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()"> <view class="to-top-btn fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
<wd-icon name="arrow-up" size="20px" color="#03a9f4" /> <wd-icon name="arrow-up" size="20px" color="#03a9f4" />
</view> </view>
+19 -12
View File
@@ -3,14 +3,15 @@
* 联系博主页(源自旧项目 pagesA/contact,新建复刻) * 联系博主页(源自旧项目 pagesA/contact,新建复刻)
* 数字名片式设计:Hero 名片卡(渐变光斑透卡) + 品牌色字母瓦片联系方式列表,点击复制 * 数字名片式设计:Hero 名片卡(渐变光斑透卡) + 品牌色字母瓦片联系方式列表,点击复制
*/ */
import { computed, ref, watch } from 'vue' import { computed, ref, watch, watchEffect } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkAvatarUrl } from '@/utils/url' import { checkAvatarUrl } from '@/utils/url'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '联系博主', navigationBarTitleText: '联系博主',
navigationStyle: 'custom',
}, },
}) })
@@ -58,6 +59,12 @@ const platformMeta: Record<string, { color: string, letter: string }> = {
const calcIsNotEmpty = computed(() => result.value.some(item => item.value !== '')) const calcIsNotEmpty = computed(() => result.value.some(item => item.value !== ''))
/* ---------------- 加载状态机(本地配置,状态直接推导) ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
watchEffect(() => {
updateLoadingStatus(calcIsNotEmpty.value ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
})
function handleGetData() { function handleGetData() {
for (const key in socialConfig.value) { for (const key in socialConfig.value) {
if (key === 'enabled') if (key === 'enabled')
@@ -84,14 +91,13 @@ function handleOnClick(item: { value: string, name: string }) {
watch(socialConfig, () => { watch(socialConfig, () => {
handleGetData() handleGetData()
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
onLoad(() => {
uni.setNavigationBarTitle({ title: '联系博主' })
})
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-6"> <view class="app-page box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-6">
<!-- 自定义导航 -->
<uh-navbar default-title="联系博主" title-color="text-gray-900" />
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) --> <!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
<view class="hero-wrap relative"> <view class="hero-wrap relative">
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" /> <view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
@@ -113,8 +119,12 @@ onLoad(() => {
</view> </view>
</view> </view>
<!-- 联系方式 --> <!-- 联系方式(状态机:无联系方式 empty ) -->
<block v-if="calcIsNotEmpty"> <uh-data-loading
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="30vh"
empty-text="暂无联系方式" empty-sub-text="" @refresh="handleGetData"
/>
<template v-else>
<uh-section-title class="mb-3 mt-6 text-[30rpx]"> <uh-section-title class="mb-3 mt-6 text-[30rpx]">
联系方式 联系方式
</uh-section-title> </uh-section-title>
@@ -137,9 +147,6 @@ onLoad(() => {
<wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" /> <wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" />
</view> </view>
</view> </view>
</block> </template>
<view v-else class="pt-12">
<wd-empty description="暂无联系方式" />
</view>
</view> </view>
</template> </template>
+13 -5
View File
@@ -2,12 +2,14 @@
import { ref } from 'vue' import { ref } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh } from '@dcloudio/uni-app'
import { getChartData } from '@/api/uni-halo' import { getChartData } from '@/api/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IDataStatistics } from '@/api/uni-halo' import type { IDataStatistics } from '@/api/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '数据看板', navigationBarTitleText: '数据看板',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
@@ -16,7 +18,7 @@
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const statistics = ref<IDataStatistics>({ const statistics = ref<IDataStatistics>({
tags: [], tags: [],
categories: [], categories: [],
@@ -109,7 +111,7 @@
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
uni.showLoading({ mask: true, title: '加载中...' }) uni.showLoading({ mask: true, title: '加载中...' })
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getChartData() const res = await getChartData()
statistics.value = res.data statistics.value = res.data
@@ -118,11 +120,14 @@
handleTrendArticlesChart() handleTrendArticlesChart()
handleUserCommentsChart() handleUserCommentsChart()
handleTop10ArticlesChart() handleTop10ArticlesChart()
loading.value = 'success' // 五类统计数据全部为空 → 空态
const hasData = [res.data.tags, res.data.categories, res.data.articles, res.data.comments, res.data.top10Articles]
.some(list => list.length > 0)
updateLoadingStatus(hasData ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
@@ -155,10 +160,13 @@
<template> <template>
<view class="bg-page box-border min-h-screen w-screen p-3"> <view class="bg-page box-border min-h-screen w-screen p-3">
<!-- 自定义导航 -->
<uh-navbar default-title="数据看板" title-color="text-gray-900" />
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId" <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
error-text="阿偶检测到当前插件没有安装或者启用无法使用功能哦请联系管理员" @on-refresh="handleGetData" /> error-text="阿偶检测到当前插件没有安装或者启用无法使用功能哦请联系管理员" @on-refresh="handleGetData" />
<template v-else> <template v-else>
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" @refresh="handleGetData" /> <uh-data-loading v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" empty-text="暂无统计数据" @refresh="handleGetData" />
<!-- 内容区域 --> <!-- 内容区域 -->
<view v-else class="content flex flex-col gap-3"> <view v-else class="content flex flex-col gap-3">
@@ -8,6 +8,7 @@ import { useAppConfigStore } from '@/store/appConfig'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '免责声明', navigationBarTitleText: '免责声明',
navigationStyle: 'custom',
}, },
}) })
@@ -40,6 +41,9 @@ function copyText(content: string, tips = '复制成功') {
<template> <template>
<view class="app-page box-border min-h-screen bg-white p-12 text-[30rpx] text-[#303133] leading-[1.65]"> <view class="app-page box-border min-h-screen bg-white p-12 text-[30rpx] text-[#303133] leading-[1.65]">
<!-- 自定义导航 -->
<uh-navbar default-title="免责声明" title-color="text-gray-900" />
<!-- 通过配置 --> <!-- 通过配置 -->
<view v-if="disclaimersContent" style="min-height: 100%;" v-html="disclaimersContent" /> <view v-if="disclaimersContent" style="min-height: 100%;" v-html="disclaimersContent" />
+55 -33
View File
@@ -11,6 +11,7 @@ import { getFriendLinkGroupList, getFriendLinkList } from '@/api/halo'
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo' import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting' import { useSettingStore } from '@/store/setting'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { NeedPluginIds } from '@/hooks/usePluginAvailable' import { NeedPluginIds } from '@/hooks/usePluginAvailable'
import type { ILink, ILinkGroup } from '@/api/types/halo' import type { ILink, ILinkGroup } from '@/api/types/halo'
@@ -20,6 +21,7 @@ definePage({
style: { style: {
navigationBarTitleText: '友情链接', navigationBarTitleText: '友情链接',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
@@ -40,6 +42,12 @@ const { available: miniPluginAvailable, check: checkMiniPluginAvailable } = useP
/* ---------------- tabs ---------------- */ /* ---------------- tabs ---------------- */
const activeTabIndex = ref(0) const activeTabIndex = ref(0)
/** 顶部 tab 定义(同收藏页胶囊 chip;审核模式下小程序 tab 隐藏) */
const friendLinkTabs = computed(() => [
{ key: 'site', label: '站点' },
...(appConfigStore.auditModeEnabled ? [] : [{ key: 'mini', label: '小程序' }]),
])
function handleOnTabChange(e: { index: number }) { function handleOnTabChange(e: { index: number }) {
activeTabIndex.value = e.index activeTabIndex.value = e.index
} }
@@ -52,7 +60,7 @@ watch(() => appConfigStore.auditModeEnabled, (enabled) => {
/* ==================== 站点 tab(plugin-links) ==================== */ /* ==================== 站点 tab(plugin-links) ==================== */
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus: siteLoadingStatus, updateLoadingStatus: updateSiteLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({ size: 10, page: 1 }) const queryParams = ref({ size: 10, page: 1 })
const detail = ref<{ show: boolean, data: ILink | null }>({ show: false, data: null }) const detail = ref<{ show: boolean, data: ILink | null }>({ show: false, data: null })
const hasNext = ref(false) const hasNext = ref(false)
@@ -77,13 +85,13 @@ async function handleGetLinkGroupData() {
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
} }
} }
async function handleGetData() { async function handleGetData() {
if (!isLoadMore.value) { if (!isLoadMore.value) {
loading.value = 'loading' updateSiteLoadingStatus(DataLoadingStatusEnum.Loading)
} }
loadMoreText.value = '' loadMoreText.value = ''
@@ -106,13 +114,15 @@ async function handleGetData() {
})) }))
dataList.value = dataList.value.concat(list) dataList.value = dataList.value.concat(list)
setTimeout(() => { setTimeout(() => {
loading.value = 'success' updateSiteLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}, 500) }, 500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
@@ -163,24 +173,26 @@ function calcSiteThumbnail(val?: string): string {
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */ /* ==================== 小程序 tab(plugin-uni-halo) ==================== */
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const miniLoading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([]) const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null }) const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null })
const applyShow = ref(false) const applyShow = ref(false)
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetMiniProgramLinks() { async function handleGetMiniProgramLinks() {
miniLoading.value = 'loading' updateMiniLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getMiniProgramLinkGroupedList() const res = await getMiniProgramLinkGroupedList()
miniGroups.value = res.data || [] miniGroups.value = res.data || []
setTimeout(() => { setTimeout(() => {
miniLoading.value = 'success' updateMiniLoadingStatus(
miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
}, 500) }, 500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
miniLoading.value = 'error' updateMiniLoadingStatus(DataLoadingStatusEnum.Error)
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
@@ -321,13 +333,24 @@ onReachBottom(() => {
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page"> <view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 顶部 tabs --> <!-- 自定义导航 -->
<view class="tabs-wrap uh-global-card-glass sticky top-0 z-10 px-6"> <uh-navbar default-title="友情链接" title-color="text-gray-900" />
<wd-tabs v-model="activeTabIndex" align="left" custom-style="background: transparent;" @change="handleOnTabChange">
<wd-tab title="站点" /> <!-- 顶部 tab(吸顶玻璃胶囊 chip,同收藏页) -->
<wd-tab v-if="!appConfigStore.auditModeEnabled" title="小程序" /> <wd-sticky>
</wd-tabs> <scroll-view scroll-x class="w-full whitespace-nowrap">
</view> <view class="flex gap-2 px-3 pb-1 pt-3">
<view
v-for="(tab, index) in friendLinkTabs" :key="tab.key"
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
@click="handleOnTabChange({ index })"
>
{{ tab.label }}
</view>
</view>
</scroll-view>
</wd-sticky>
<!-- ==================== 站点 tab ==================== --> <!-- ==================== 站点 tab ==================== -->
<template v-if="activeTabIndex === 0"> <template v-if="activeTabIndex === 0">
@@ -338,18 +361,18 @@ onReachBottom(() => {
@on-refresh="handleGetLinkGroupData" @on-refresh="handleGetLinkGroupData"
/> />
<template v-else> <template v-else>
<!-- 加载/错误占位 --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="loading !== 'success'"> <view v-if="siteLoadingStatus !== 'success'">
<uh-data-loading :loading-status="loading" @refresh="handleGetData" /> <uh-data-loading
:loading-status="siteLoadingStatus"
empty-text="啊偶,博主还没有朋友呢~"
@refresh="handleGetData"
/>
</view> </view>
<view v-else class="content pt-4"> <view v-else class="content pt-4">
<view v-if="dataList.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
<wd-empty description="啊偶,博主还没有朋友呢~" />
</view>
<!-- 友链列表 --> <!-- 友链列表 -->
<view v-else class="link-list flex flex-col gap-4 px-4 pb-4"> <view class="link-list flex flex-col gap-4 px-4 pb-4">
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName"> <view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName">
<!-- 色彩版 --> <!-- 色彩版 -->
<view <view
@@ -436,19 +459,18 @@ onReachBottom(() => {
@on-refresh="handleGetMiniProgramLinks" @on-refresh="handleGetMiniProgramLinks"
/> />
<template v-else> <template v-else>
<!-- 加载/错误占位 --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="miniLoading !== 'success'"> <view v-if="miniLoadingStatus !== 'success'">
<uh-data-loading :loading-status="miniLoading" @refresh="handleGetMiniProgramLinks" /> <uh-data-loading
:loading-status="miniLoadingStatus"
empty-text="还没有收录的小程序呢~"
@refresh="handleGetMiniProgramLinks"
/>
</view> </view>
<view v-else class="content flex flex-1 flex-col"> <view v-else class="content flex flex-1 flex-col">
<!-- 空态 -->
<view v-if="miniGroups.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
<wd-empty description="还没有收录的小程序呢~" />
</view>
<!-- 分组列表 --> <!-- 分组列表 -->
<view v-else class="mini-link-list flex-1 px-6 py-4"> <view class="mini-link-list flex-1 px-6 py-4">
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8"> <view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
<view class="group-title mb-4 flex items-center"> <view class="group-title mb-4 flex items-center">
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" /> <text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
+27 -19
View File
@@ -16,13 +16,14 @@ import { generateUUID } from '@/utils/uuid'
import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import { randomTagColor } from '@/utils/random' import { randomTagColor } from '@/utils/random'
import { markdownConfig } from '@/config/markdown' import { markdownConfig } from '@/config/markdown'
import { useDataLoading } from '@/hooks/useDataLoading' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IMoment } from '@/api/types/halo' import type { IMoment } from '@/api/types/halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '瞬间详情', navigationBarTitleText: '瞬间详情',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
// 下拉/回弹露出的窗口底色对齐页面底色 // 下拉/回弹露出的窗口底色对齐页面底色
backgroundColor: '#f6f3ee', backgroundColor: '#f6f3ee',
}, },
@@ -88,24 +89,29 @@ function buildMomentCard(res: IMoment): MomentCard {
} }
} }
/* ---------------- 数据加载(useDataLoading 试点:状态由 hook 接管) ---------------- */ /* ---------------- 数据加载(useDataLoadingStatus 状态机接管) ---------------- */
const { data: moment, status, run: loadMoment } = useDataLoading( const moment = ref<MomentCard | null>(null)
async (): Promise<MomentCard> => { const { loadingStatus: status, updateLoadingStatus } = useDataLoadingStatus()
async function loadMoment() {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
try {
const res = await getMomentByName(queryName.value) const res = await getMomentByName(queryName.value)
uni.setNavigationBarTitle({ title: '瞬间详情' }) const card = buildMomentCard(res.data)
return buildMomentCard(res.data) moment.value = card
}, // 对象无键视为空 → 空态(与原 useDataLoading 默认判空一致)
{ updateLoadingStatus(
onSuccess: (card) => { Object.keys(card).length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
nextTick(() => { )
createVideoContexts(card.videos || []) nextTick(() => {
}) createVideoContexts(card.videos || [])
}, })
onError: () => { }
uni.setNavigationBarTitle({ title: '瞬间详情' }) catch (err) {
}, console.error('[moment-detail] 加载失败', err)
}, updateLoadingStatus(DataLoadingStatusEnum.Error)
) }
}
/** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */ /** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */
const calcTagColors = computed(() => { const calcTagColors = computed(() => {
@@ -255,7 +261,6 @@ function handleToTopPage(duration = 500) {
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad((options) => { onLoad((options) => {
uni.setNavigationBarTitle({ title: '瞬间加载中...' })
queryName.value = options?.name || '' queryName.value = options?.name || ''
loadMoment() loadMoment()
}) })
@@ -280,6 +285,9 @@ onShareTimeline(() => ({
<template> <template>
<view class="app-page box-border min-h-screen w-screen bg-page px-4 pb-8 pt-4"> <view class="app-page box-border min-h-screen w-screen bg-page px-4 pb-8 pt-4">
<!-- 自定义导航 -->
<uh-navbar default-title="瞬间详情" title-color="text-gray-900" />
<!-- 状态区(加载中/失败可重试/) --> <!-- 状态区(加载中/失败可重试/) -->
<uh-data-loading <uh-data-loading
v-if="status !== 'success'" v-if="status !== 'success'"
+27 -49
View File
@@ -8,6 +8,7 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { getNoticeDetail } from '@/api/uni-halo' import { getNoticeDetail } from '@/api/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkIsUrl } from '@/utils/url' import { checkIsUrl } from '@/utils/url'
import { markdownConfig } from '@/config/markdown' import { markdownConfig } from '@/config/markdown'
import type { INoticeDetail } from '@/api/types/uni-halo' import type { INoticeDetail } from '@/api/types/uni-halo'
@@ -15,11 +16,12 @@ import type { INoticeDetail } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '公告详情', navigationBarTitleText: '公告详情',
navigationStyle: 'custom',
}, },
}) })
const loading = ref(true) const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const notFound = ref(false) const name = ref('')
const detail = ref<INoticeDetail | null>(null) const detail = ref<INoticeDetail | null>(null)
const spec = computed(() => detail.value?.spec) const spec = computed(() => detail.value?.spec)
@@ -41,17 +43,6 @@ function formatDate(value?: string): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
} }
function handleBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
}
else {
uni.switchTab({ url: '/pages/tabbar/home/home' })
}
}
/** 外链 → 复用 website 承载页 */
function handleToExternal() { function handleToExternal() {
if (!spec.value?.link) if (!spec.value?.link)
return return
@@ -63,57 +54,44 @@ function handleToExternal() {
}) })
} }
onLoad(async (options) => { /** 加载公告详情(状态机;404/无数据 → 空态,其余错误 → error 态,可重试) */
const name = options?.name || '' async function loadDetail() {
if (!name) { updateLoadingStatus(DataLoadingStatusEnum.Loading)
notFound.value = true if (!name.value) {
loading.value = false updateLoadingStatus(DataLoadingStatusEnum.Empty)
return return
} }
try { try {
const res = await getNoticeDetail(name) const res = await getNoticeDetail(name.value)
detail.value = res.data || null detail.value = res.data || null
if (!detail.value?.spec) { updateLoadingStatus(
notFound.value = true detail.value?.spec ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
} )
} }
catch (err) { catch (err) {
console.error('公告详情加载失败', err) console.error('公告详情加载失败', err)
const code = (err as { code?: number }).code const code = (err as { code?: number }).code
notFound.value = code === 404 updateLoadingStatus(code === 404 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Error)
if (code !== 404) {
uni.showToast({ icon: 'none', title: '公告加载失败' })
}
}
finally {
loading.value = false
} }
}
onLoad((options) => {
name.value = options?.name || ''
loadDetail()
}) })
</script> </script>
<template> <template>
<view class="notice-detail min-h-screen w-screen bg-white pb-12"> <view class="notice-detail min-h-screen w-screen bg-white pb-12">
<!-- 加载中 --> <!-- 自定义导航 -->
<view v-if="loading" class="flex flex-col items-center justify-center py-40"> <uh-navbar default-title="公告详情" title-color="text-gray-900" />
<text class="text-[26rpx] text-[#999]">
加载中...
</text>
</view>
<!-- 不存在/已下线 --> <!-- 加载/错误/空态(状态机) -->
<view v-else-if="notFound" class="flex flex-col items-center justify-center py-40"> <uh-data-loading
<text class="text-[60rpx]"> v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="55vh"
🕳 error-text="公告加载失败" empty-text="公告不存在或已下线" empty-sub-text=""
</text> @refresh="loadDetail"
<text class="mt-6 text-[26rpx] text-[#999]"> />
公告不存在或已下线
</text>
<view class="mt-10">
<wd-button size="small" type="primary" plain @click="handleBack">
返回
</wd-button>
</view>
</view>
<!-- 正文 --> <!-- 正文 -->
<view v-else class="px-6 py-6"> <view v-else class="px-6 py-6">
+54 -49
View File
@@ -7,6 +7,7 @@ import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getPostListByKeyword } from '@/api/halo' import { getPostListByKeyword } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { markdownConfig } from '@/config/markdown' import { markdownConfig } from '@/config/markdown'
import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import { debounce } from '@/utils/debounce' import { debounce } from '@/utils/debounce'
@@ -15,6 +16,7 @@ definePage({
style: { style: {
navigationBarTitleText: '内容搜索', navigationBarTitleText: '内容搜索',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
@@ -26,7 +28,7 @@ const uniHaloPluginId = 'plugin-search-widget'
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({ const queryParams = ref({
keyword: '', keyword: '',
limit: 50, limit: 50,
@@ -52,19 +54,26 @@ const calcAniDelays = computed(() => {
}) })
}) })
/** 空态文案(无关键词提示输入;有关键词提示未搜到) */
const emptyText = computed(() =>
queryParams.value.keyword ? `未搜到 ${queryParams.value.keyword} 相关内容` : '请输入关键词搜索',
)
/* ---------------- 搜索 ---------------- */ /* ---------------- 搜索 ---------------- */
async function handleGetData() { async function handleGetData() {
if (calcAuditModeEnabled.value) if (calcAuditModeEnabled.value)
return return
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getPostListByKeyword({ ...queryParams.value }) const res = await getPostListByKeyword({ ...queryParams.value })
loading.value = 'success'
dataList.value = (res.data as unknown as { hits?: typeof dataList.value }).hits || [] dataList.value = (res.data as unknown as { hits?: typeof dataList.value }).hits || []
updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
@@ -76,7 +85,7 @@ async function handleGetData() {
function handleOnSearch() { function handleOnSearch() {
if (!queryParams.value.keyword) { if (!queryParams.value.keyword) {
dataList.value = [] dataList.value = []
loading.value = 'success' updateLoadingStatus(DataLoadingStatusEnum.Empty)
} }
else { else {
handleGetData() handleGetData()
@@ -128,7 +137,7 @@ onLoad(async () => {
} }
// 关键词非空(如带参进入)时自动搜索,否则展示空态 // 关键词非空(如带参进入)时自动搜索,否则展示空态
if (!queryParams.value.keyword) { if (!queryParams.value.keyword) {
loading.value = 'success' updateLoadingStatus(DataLoadingStatusEnum.Empty)
} }
else { else {
handleGetData() handleGetData()
@@ -145,64 +154,67 @@ onPullDownRefresh(() => {
</script> </script>
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page pb-6"> <view class="box-border min-h-screen w-screen flex flex-col bg-page pb-6">
<!-- 自定义导航 -->
<uh-navbar default-title="内容搜索" title-color="text-gray-900" />
<uh-plugin-unavailable <uh-plugin-unavailable
v-if="!uniHaloPluginAvailable" v-if="!uniHaloPluginAvailable"
:plugin-id="uniHaloPluginId" :plugin-id="uniHaloPluginId"
error-text="检测到当前插件没有安装或者启用无法使用搜索功能哦请联系管理员" error-text="检测到当前插件没有安装或者启用无法使用搜索功能哦请联系管理员"
@on-refresh="handleOnSearch" @on-refresh="handleOnSearch"
/> />
<template v-else> <template v-else>
<!-- 顶部搜索框(玻璃吸顶,呼应首页搜索条) --> <!-- 顶部搜索框-->
<view class="search-bar uh-global-card-glass sticky top-0 z-10 px-3 py-2"> <wd-sticky class="">
<view class="search-input h-[72rpx] flex items-center gap-3 rounded-full bg-[#f6f3ee] px-5"> <view class="w-screen box-border px-3 py-2">
<wd-icon name="search" size="16px" color="#a8a294" /> <view class="uh-global-card-glass h-9 flex items-center gap-3 rounded-full px-5">
<input <wd-icon name="search" size="16px" />
v-model="queryParams.keyword" <input
class="search-field flex-1 text-[26rpx] text-gray-900" v-model="queryParams.keyword"
placeholder="哈喽,想看些什么呢~" class="flex-1 text-[26rpx] text-gray-900"
placeholder-class="text-gray-400" placeholder="哈喽,想看些什么呢~"
confirm-type="search" placeholder-class="text-gray-400"
@input="handleOnInput" confirm-type="search"
@confirm="handleOnSearch" @input="handleOnInput"
> @confirm="handleOnSearch"
<view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()"> >
<wd-icon name="close" size="14px" color="#a8a294" /> <view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()">
<wd-icon name="close" size="14px" />
</view>
</view> </view>
</view> </view>
</view> </wd-sticky>
<!-- 骨架屏 --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="loading === 'loading'" class="loading-wrap p-3"> <uh-data-loading
<wd-skeleton :row="4" :animated="true" /> v-if="loadingStatus !== 'success'"
</view> :loading-status="loadingStatus"
<view v-else-if="loading === 'error'" class="min-h-[60vh] flex items-center justify-center content-empty"> min-height="65vh"
<wd-empty description="搜索异常" /> error-text="搜索异常"
</view> :empty-text="emptyText"
@refresh="handleOnSearch"
/>
<!-- 内容区域 --> <!-- 内容区域(成功态) -->
<view v-else class="content pt-4"> <view v-else class="box-border pt-2 px-3 flex flex-col gap-y-3">
<view v-if="dataList.length === 0" class="min-h-[60vh] flex items-center justify-center content-empty"> <block v-if="dataList.length !== 0">
<wd-empty v-if="!queryParams.keyword" description="请输入关键词搜索" />
<wd-empty v-else :description="`未搜到 ${queryParams.keyword} 相关内容`" />
</view>
<block v-else>
<view <view
v-for="(item, index) in dataList" v-for="(item, index) in dataList"
:key="index" :key="index"
class="article-card fade-up uh-global-card-glass mx-4 mb-4 flex flex-col overflow-hidden rounded-2xl p-4" class="uh-global-card-glass uh-shadow-xs border flex flex-col overflow-hidden rounded-2xl p-4"
:style="{ animationDelay: `${calcAniDelays[index]}ms` }" :style="{ animationDelay: `${calcAniDelays[index]}ms` }"
@click="handleToDetail(item)" @click="handleToDetail(item)"
> >
<view class="card-head mb-3 flex items-center"> <view class="card-head mb-3 flex items-center">
<view <view
class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-0.5 text-[20rpx] leading-none" class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-1 text-xs leading-none"
:class="isArticle(item) ? 'bg-secondary text-[#4d7c0f]' : 'bg-[#e8e3d8] text-gray-600'" :class="isArticle(item) ? 'bg-secondary text-gray-900' : 'bg-blue-500 text-gray-50'"
> >
{{ isArticle(item) ? '文章' : '瞬间' }} {{ isArticle(item) ? '文章' : '瞬间' }}
</view> </view>
<text class="card-title flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[28rpx] text-gray-900 font-bold">{{ item.title }}</text> <text class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-gray-900 font-bold">{{ item.title }}</text>
</view> </view>
<mp-html <mp-html
class="evan-markdown" class="evan-markdown"
@@ -219,13 +231,6 @@ onPullDownRefresh(() => {
:show-language-name="true" :show-language-name="true"
copy-by-long-press copy-by-long-press
/> />
<view class="card-foot mt-3 flex items-center">
<text class="text-[24rpx] text-gray-400">{{ item.updateTimestamp ? `最近更新:${formatTimeUtil({ d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒' })}` : '' }}</text>
</view>
</view>
<view class="to-top-btn uh-global-card-glass fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full" @click="handleToTopPage()">
<wd-icon name="arrow-up" size="20px" color="#6b7280" />
</view> </view>
</block> </block>
</view> </view>
+35 -26
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { DefaultAppSettings } from '@/config/appSettings' import { DefaultAppSettings } from '@/config/appSettings'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
@@ -157,6 +157,32 @@
handleCloseEnum() handleCloseEnum()
} }
/* ---------------- wd-picker 弹层数据 ---------------- */
/** 枚举弹层列(首项「跟随站点默认」,空串哨兵映射 null) */
const enumColumns = computed(() => {
const def = enumSheet.value.def
if (!def)
return []
return [
{ label: '跟随站点默认', value: '' },
...(def.options || []).map(opt => ({ label: opt.label, value: opt.value })),
]
})
/** 当前选中列值(单列;跟随站点默认时为空串) */
const enumValue = computed(() => {
const def = enumSheet.value.def
if (!def)
return ['']
return [isFollowing(def) ? '' : String(valueOf(def.path) ?? '')]
})
/** wd-picker 确认:空串哨兵还原为「跟随站点默认」 */
function handlePickerConfirm(payload: { value: (string | number)[] }) {
const picked = String(payload.value[0] ?? '')
handleChooseEnum(picked === '' ? null : picked)
}
/** 当前枚举项是否处于「跟随站点默认」 */ /** 当前枚举项是否处于「跟随站点默认」 */
function isFollowing(def : PrefDef) : boolean { function isFollowing(def : PrefDef) : boolean {
return !isOverridden(def.path) return !isOverridden(def.path)
@@ -247,7 +273,7 @@
</template> </template>
</view> </view>
</view> </view>
<wd-switch :model-value="def.path" @change="handleSwitchChange(def, $event)" /> <wd-switch :model-value="valueOf(def.path) === true" @change="handleSwitchChange(def, $event)" />
</view> </view>
<!-- 枚举选择(指示器位置) --> <!-- 枚举选择(指示器位置) -->
<view v-else class="pick-row flex items-center justify-between px-4 py-4" <view v-else class="pick-row flex items-center justify-between px-4 py-4"
@@ -276,34 +302,17 @@
</template> </template>
</view> </view>
</view> </view>
<!-- 底部操作栏(玻璃悬浮--> <!-- 底部操作栏(玻璃悬浮) -->
<view class="box-border w-full px-2"> <view class="box-border w-full px-2">
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full" <uh-button custom-class="uh-global-card-glass py-2 !rounded-full"
@click="handleResetAll">恢复默认</uh-button> @click="handleResetAll">恢复默认</uh-button>
</view> </view>
</view> </view>
<!-- 枚举选择底部弹层 --> <!-- 枚举选择弹层(wd-picker 自带底部弹层与工具栏) -->
<wd-popup v-model="enumSheet.show" position="bottom" closable custom-style="border-radius: 24rpx 24rpx 0 0;" <wd-picker
@close="handleCloseEnum"> v-model:visible="enumSheet.show" :title="enumSheet.def?.label || ''" :columns="enumColumns"
<view v-if="enumSheet.def" class="enum-sheet box-border w-full pb-[env(safe-area-inset-bottom)]"> :model-value="enumValue" confirm-button-text="确定" cancel-button-text="取消"
<view class="enum-title py-6 text-center text-[30rpx] text-gray-900 font-bold"> @confirm="handlePickerConfirm"
{{ enumSheet.def.label }} />
</view>
<view class="enum-item flex items-center justify-between px-6 py-5"
:class="isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
@click="handleChooseEnum(null)">
<text class="text-[28rpx]">跟随站点默认</text>
<wd-icon v-if="isFollowing(enumSheet.def)" name="check" size="16px" color="#4d7c0f" />
</view>
<view v-for="opt in enumSheet.def.options" :key="opt.value"
class="enum-item flex items-center justify-between border-t border-[#f0ece2] px-6 py-5"
:class="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
@click="handleChooseEnum(opt.value)">
<text class="text-[28rpx]">{{ opt.label }}</text>
<wd-icon v-if="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def)"
name="check" size="16px" color="#4d7c0f" />
</view>
</view>
</wd-popup>
</view> </view>
</template> </template>
+21 -15
View File
@@ -6,6 +6,7 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getVoteDetail, submitVote } from '@/api/uni-halo' import { getVoteDetail, submitVote } from '@/api/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { calcVotePercent, VOTE_TYPES, voteCacheUtil } from '@/utils/vote' import { calcVotePercent, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo' import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
@@ -14,11 +15,12 @@ definePage({
style: { style: {
navigationBarTitleText: '投票详情', navigationBarTitleText: '投票详情',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const submitLoading = ref(false) const submitLoading = ref(false)
const pageTitle = ref('加载中...') const pageTitle = ref('加载中...')
const safeAreaBottom = ref(24) const safeAreaBottom = ref(24)
@@ -76,7 +78,7 @@ function handleCalcIsChecked(option: { id?: string }): boolean {
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
pageTitle.value = '加载中...' pageTitle.value = '加载中...'
try { try {
const res = await getVoteDetail(name.value) const res = await getVoteDetail(name.value)
@@ -133,19 +135,20 @@ async function handleGetData() {
vote.value = tempVote vote.value = tempVote
detail.value = res detail.value = res
setTimeout(() => { setTimeout(() => {
loading.value = 'success' updateLoadingStatus(
tempVote ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
)
}, 200) }, 200)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
pageTitle.value = '加载失败,请重试...' pageTitle.value = '加载失败,请重试...'
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
uni.hideLoading() uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
uni.setNavigationBarTitle({ title: pageTitle.value })
}, 200) }, 200)
} }
} }
@@ -269,18 +272,21 @@ onShareTimeline(() => ({
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen flex flex-col py-6 pb-[160rpx]" style="background-color: #fafafd;"> <view class="app-page box-border min-h-screen w-screen flex flex-col pb-[160rpx]" style="background-color: #fafafd;">
<!-- 加载/错误占位 --> <!-- 自定义导航 -->
<view v-if="loading !== 'success'"> <uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
:loading-status="loadingStatus"
empty-text="未查询到数据"
@refresh="handleGetData"
/>
</view> </view>
<block v-else> <block v-else>
<view v-if="!vote" class="empty h-[60vh] flex items-center justify-center"> <template v-if="vote">
<wd-empty description="未查询到数据" />
</view>
<block v-else>
<!-- 投票信息 --> <!-- 投票信息 -->
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm"> <view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
<view class="sub-title relative box-border pl-6 text-[30rpx]"> <view class="sub-title relative box-border pl-6 text-[30rpx]">
@@ -392,7 +398,7 @@ onShareTimeline(() => ({
提交投票 提交投票
</wd-button> </wd-button>
</view> </view>
</block> </template>
</block> </block>
</view> </view>
</template> </template>
+22 -12
View File
@@ -6,6 +6,7 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getVoteList } from '@/api/uni-halo' import { getVoteList } from '@/api/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import type { IVoteItem } from '@/api/types/uni-halo' import type { IVoteItem } from '@/api/types/uni-halo'
@@ -13,6 +14,7 @@ definePage({
style: { style: {
navigationBarTitleText: '投票中心', navigationBarTitleText: '投票中心',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom',
}, },
}) })
@@ -24,7 +26,7 @@ const uniHaloPluginId = 'plugin-vote'
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId) const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const dataList = ref<IVoteItem[]>([]) const dataList = ref<IVoteItem[]>([])
const hasNext = ref(false) const hasNext = ref(false)
const queryParams = ref({ page: 1, size: 10 }) const queryParams = ref({ page: 1, size: 10 })
@@ -33,7 +35,9 @@ const loadMoreText = ref('加载中...')
async function handleGetData() { async function handleGetData() {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
loading.value = 'success' updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = '呜呜,没有更多数据啦~' loadMoreText.value = '呜呜,没有更多数据啦~'
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
@@ -41,22 +45,24 @@ async function handleGetData() {
uni.showLoading({ mask: true, title: '加载中...' }) uni.showLoading({ mask: true, title: '加载中...' })
if (!isLoadMore.value) { if (!isLoadMore.value) {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
loadMoreText.value = '加载中...' loadMoreText.value = '加载中...'
try { try {
const res = await getVoteList({ ...queryParams.value }) const res = await getVoteList({ ...queryParams.value })
loading.value = 'success'
hasNext.value = res.data.hasNext || false hasNext.value = res.data.hasNext || false
dataList.value = isLoadMore.value dataList.value = isLoadMore.value
? dataList.value.concat(res.data.items) ? dataList.value.concat(res.data.items)
: res.data.items : res.data.items
updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~'
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
@@ -117,6 +123,9 @@ onReachBottom(() => {
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page"> <view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 自定义导航 -->
<uh-navbar default-title="投票中心" title-color="text-gray-900" />
<uh-plugin-unavailable <uh-plugin-unavailable
v-if="!uniHaloPluginAvailable" v-if="!uniHaloPluginAvailable"
:plugin-id="uniHaloPluginId" :plugin-id="uniHaloPluginId"
@@ -124,16 +133,17 @@ onReachBottom(() => {
@on-refresh="handleGetData" @on-refresh="handleGetData"
/> />
<template v-else> <template v-else>
<!-- 加载/错误占位 --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="loading !== 'success'"> <view v-if="loadingStatus !== 'success'">
<uh-data-loading :loading-status="loading" @refresh="handleGetData" /> <uh-data-loading
:loading-status="loadingStatus"
empty-text="博主还未发布投票~"
@refresh="handleGetData"
/>
</view> </view>
<view v-else class="content flex flex-col gap-4 p-3"> <view v-else class="content flex flex-col gap-4 p-3">
<view v-if="dataList.length === 0" class="min-h-[60vh] flex items-center justify-center content-empty"> <block v-if="dataList.length !== 0">
<wd-empty description="博主还未发布投票~" />
</view>
<block v-else>
<uh-vote-card <uh-vote-card
v-for="vote in dataList" v-for="vote in dataList"
:key="vote.metadata?.name" :key="vote.metadata?.name"
+2 -2
View File
@@ -28,7 +28,7 @@
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
// 维护拦截 // 维护拦截
const { interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept() const { reason, interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept()
/** 通过二维码 scene 获取文章 id */ /** 通过二维码 scene 获取文章 id */
async function getPostIdByQRCode(key : string) : Promise<string | null> { async function getPostIdByQRCode(key : string) : Promise<string | null> {
@@ -84,7 +84,7 @@
} }
catch (err) { catch (err) {
console.error('入口页初始化失败', err) console.error('入口页初始化失败', err)
redirectToMaintenance() redirectToMaintenance(reason.value)
} }
}) })
</script> </script>
+1 -2
View File
@@ -121,11 +121,10 @@
return return
} }
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`, url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
}) })
} }
onMounted(() => { onMounted(() => {
handleInitPage() handleInitPage()
}) })
+16 -13
View File
@@ -1,8 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
*/
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo' import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
@@ -25,9 +21,19 @@
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig) const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
/** 依赖插件(plugin-photos) */ /** 依赖插件(PluginPhotos) */
const uniHaloPluginId = 'plugin-photos' const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId, false) pluginId: 'PluginPhotos',
tips: '很抱歉,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetCategory()
}
})
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
@@ -152,8 +158,6 @@
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
// 开始正常数据请求
handleGetCategory() handleGetCategory()
}) })
@@ -169,8 +173,7 @@
}) })
onReachBottom(() => { onReachBottom(() => {
if (!uniHaloPluginAvailable.value) if (!uniHaloPluginAvailable.value) { return }
return
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') }) uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return return
@@ -188,8 +191,8 @@
<template> <template>
<view class="min-h-screen w-screen flex flex-col bg-page pb-6"> <view class="min-h-screen w-screen flex flex-col bg-page pb-6">
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId" <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
error-text="检测到当前插件没有安装或者启用无法使用图库功能哦请联系管理员" @on-refresh="handleGetCategory" /> :checking="checking" @on-refresh="checkPluginAvailable" />
<template v-else> <template v-else>
<wd-sticky v-if="category.list.length!==0"> <wd-sticky v-if="category.list.length!==0">
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3"> <scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
+8 -13
View File
@@ -124,15 +124,9 @@
} }
/* ---------------- 跳转 ---------------- */ /* ---------------- 跳转 ---------------- */
function handleToArticleDetail(article : IPost) {
uni.navigateTo({
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
animationType: 'slide-in-right',
})
}
function handleToSearch() { function handleToArticles() {
uni.navigateTo({ url: '/pages-blog/search/search' }) uni.navigateTo({ url: '/pages-blog/articles/articles' })
} }
function handleOnLogoToPage() { function handleOnLogoToPage() {
@@ -202,10 +196,10 @@
<!-- 最新文章 --> <!-- 最新文章 -->
<uh-section-title class="mb-4 box-border px-3"> <uh-section-title class="mb-4 box-border px-3">
最新内容 最新推荐
<template #right> <template #right>
<view class="uh-global-card-glass flex items-center justify-center rounded-md p-1 text-gray-400" <view class="uh-global-card-glass flex items-center justify-center gap-x-1 rounded-md p-1 text-gray-400"
@click="handleToSearch()"> @click="handleToArticles()">
<wd-icon name="arrow-right" size="12px" /> <wd-icon name="arrow-right" size="12px" />
</view> </view>
</template> </template>
@@ -217,8 +211,9 @@
<block v-else> <block v-else>
<view class="flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home"> <view class="flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home">
<uh-article-card v-for="(article, index) in articleList" :key="index" from="home" :article="article" <uh-article-card v-for="(article, index) in articleList" :key="index"
@on-click="handleToArticleDetail" /> from="home" :article="article" :audit-mode="calcAuditModeEnabled"
/>
</view> </view>
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400"> <view class="load-text mt-3 pb-5 text-center text-xs text-gray-400">
{{ loadMoreText }} {{ loadMoreText }}