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

refactor: 优化页面加载状态与组件样式,统一数据加载逻辑

1. 新增通用sleep工具函数
2. 统一使用uh-data-loading组件处理加载/错误状态
3. 优化回到顶部按钮z-index与分类页面加载延迟
4. 重构关于页导航图标样式,优化视觉效果
5. 格式化代码缩进与排版
This commit is contained in:
小莫唐尼
2026-09-04 03:36:21 +08:00
parent 3c84661d0e
commit d0dd7d7a46
7 changed files with 745 additions and 725 deletions
@@ -15,7 +15,7 @@
<template> <template>
<view <view
class="uh-global-card-glass border fixed bottom-24 right-4 z-90 h-10 w-10 flex items-center justify-center rounded-full text-primary" class="uh-global-card-glass border fixed bottom-24 right-4 z-50 h-10 w-10 flex items-center justify-center rounded-full text-primary"
:class="props.customClass" @click="handleScrollTop"> :class="props.customClass" @click="handleScrollTop">
<wd-icon name="arrow-up" size="20px" /> <wd-icon name="arrow-up" size="20px" />
</view> </view>
+103 -84
View File
@@ -1,112 +1,122 @@
<script lang="ts" setup> <script lang="ts" setup>
/** /**
* 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻) * 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻)
* 功能:博主信息 + 站点统计 + 功能导航 + 版权 * 功能:博主信息 + 站点统计 + 功能导航 + 版权
* 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块) * 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块)
*/ */
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh } from '@dcloudio/uni-app'
import { getBlogStatistics } from '@/api/halo' import { getBlogStatistics } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { checkHasAdminLogin } from '@/utils/auth' import { checkHasAdminLogin } from '@/utils/auth'
import { t } from '@/locale' import { t } from '@/locale'
import { usePluginAvailable } from '@/utils/plugin' import { usePluginAvailable } from '@/utils/plugin'
import type { IBlogStats } from '@/api/types/halo' import type { IBlogStats } from '@/api/types/halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '关于', navigationBarTitleText: '关于',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs) const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled) const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled) const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
/* ---------------- 计算属性 ---------------- */ /* ---------------- 计算属性 ---------------- */
const bloggerInfo = computed(() => { const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as const blogger = haloConfigs.value.authorConfig?.blogger as
| { nickname ?: string, avatar ?: string, description ?: string } | { nickname?: string, avatar?: string, description?: string }
| undefined | undefined
return { return {
nickname: blogger?.nickname || '', nickname: blogger?.nickname || '',
avatar: checkAvatarUrl(blogger?.avatar), avatar: checkAvatarUrl(blogger?.avatar),
description: blogger?.description || '', description: blogger?.description || '',
} }
}) })
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
| { bgImageUrl ?: string, waveImageUrl ?: string } | { bgImageUrl?: string, waveImageUrl?: string }
| undefined) | undefined)
const calcProfileStyle = computed(() => ({ const calcProfileStyle = computed(() => ({
backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`, backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`,
})) }))
const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl)) const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl))
const basicConfig = computed(() => haloConfigs.value.basicConfig as const basicConfig = computed(() => haloConfigs.value.basicConfig as
| { | {
copyrightConfig ?: { enabled ?: boolean, content ?: string } copyrightConfig?: { enabled?: boolean, content?: string }
disclaimers ?: { enabled ?: boolean } disclaimers?: { enabled?: boolean }
showAboutSystem ?: boolean showAboutSystem?: boolean
} }
| undefined) | undefined)
const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig) const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig)
const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean } | undefined)?.loveEnabled) const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean } | undefined)?.loveEnabled)
const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled) const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const statisticsShowMore = ref(false) const statisticsShowMore = ref(false)
const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 }) const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 })
/** 主行统计(常驻展示) */ /** 主行统计(常驻展示) */
const allStats = computed(() => [ const allStats = computed(() => [
{ key: 'post', label: '内容', value: statistics.value.post }, { key: 'post', label: '内容', value: statistics.value.post },
{ key: 'visit', label: '访客', value: statistics.value.visit }, { key: 'visit', label: '访客', value: statistics.value.visit },
{ key: 'category', label: '分类', value: statistics.value.category }, { key: 'category', label: '分类', value: statistics.value.category },
{ key: 'comment', label: '评论', value: statistics.value.comment }, { key: 'comment', label: '评论', value: statistics.value.comment },
{ key: 'upvote', label: '点赞', value: statistics.value.upvote }, { key: 'upvote', label: '点赞', value: statistics.value.upvote },
]) ])
interface INavItem { interface INavItem {
key : string key: string
title : string title: string
icon : string icon: string
/** 图标块背景色(与首页快捷导航同色板,同一功能同色) */ /** 图标块背景色(与首页快捷导航同色板,同一功能同色) */
bgColor : string bgColor: string
rightText : string rightText: string
path : string | null path: string | null
isAdmin ?: boolean isAdmin?: boolean
openType ?: string openType?: string
show : boolean show: boolean
/** 分组:blog=博客功能 more=更多信息 */ /** 分组:blog=博客功能 more=更多信息 */
group : 'blog' | 'more' group: 'blog' | 'more'
} }
const navList = ref<INavItem[]>([]) const navList = ref<INavItem[]>([])
/** 分组渲染(过滤后空组整组隐藏) */ /** 分组渲染(过滤后空组整组隐藏) */
const calcNavGroups = computed(() => { const calcNavGroups = computed(() => {
const visible = navList.value.filter(n => n.show) const visible = navList.value.filter(n => n.show)
const groupDefs : { key : 'blog' | 'more', title : string }[] = [ const groupDefs: { key: 'blog' | 'more', title: string }[] = [
{ key: 'blog', title: '博客功能' }, { key: 'blog', title: '博客功能' },
{ key: 'more', title: '其他功能' }, { key: 'more', title: '其他功能' },
] ]
return groupDefs return groupDefs
.map(def => ({ ...def, items: visible.filter(n => n.group === def.key) })) .map(def => ({ ...def, items: visible.filter(n => n.group === def.key) }))
.filter(group => group.items.length > 0) .filter(group => group.items.length > 0)
}) })
/* ---------------- 功能导航 ---------------- */ /* ---------------- 功能导航 ---------------- */
async function handleGetNavList() { /** 图标块浅色背景:品牌深色 rgba 降透明度 → 轻量底色 */
function toLightBg(rgba: string) {
return rgba.replace('0.95)', '0.15)')
}
/** 图标颜色:品牌深色实色 */
function toSolidColor(rgba: string) {
return rgba.replace('0.95)', '1)')
}
async function handleGetNavList() {
const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics') const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics')
navList.value = [ navList.value = [
@@ -207,10 +217,10 @@
group: 'more', group: 'more',
}, },
] ]
} }
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
try { try {
const res = await getBlogStatistics() const res = await getBlogStatistics()
statistics.value = res.data statistics.value = res.data
@@ -222,10 +232,10 @@
finally { finally {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
} }
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleOnNav(data : { path : string | null, isAdmin ?: boolean }) { function handleOnNav(data: { path: string | null, isAdmin?: boolean }) {
const { path, isAdmin } = data const { path, isAdmin } = data
if (!path) if (!path)
return return
@@ -250,43 +260,48 @@
} }
uni.navigateTo({ url: path }) uni.navigateTo({ url: path })
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
watch(haloConfigs, () => { watch(haloConfigs, () => {
handleGetNavList() handleGetNavList()
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
handleGetData() handleGetData()
onPullDownRefresh(() => { onPullDownRefresh(() => {
handleGetData() handleGetData()
}) })
</script> </script>
<template> <template>
<view class="box-border bg-page min-h-screen w-screen pb-8"> <view class="box-border min-h-screen w-screen bg-page pb-8">
<!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) --> <!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) -->
<view class="blogger-info relative h-76 w-full bg-cover bg-no-repeat" :style="[calcProfileStyle]"> <view class="blogger-info relative h-76 w-full bg-cover bg-no-repeat" :style="[calcProfileStyle]">
<!-- 背景遮罩 --> <!-- 背景遮罩 -->
<view class="absolute left-0 top-0 z-0 h-full w-full backdrop-blur-[2rpx] bg-black/30" /> <view class="absolute left-0 top-0 z-0 h-full w-full bg-black/30 backdrop-blur-[2rpx]" />
<view class="relative z-6 h-full flex flex-col items-center justify-center pb-[140rpx] pt-safe"> <view class="relative z-6 h-full flex flex-col items-center justify-center pb-[140rpx] pt-safe">
<image class="uh-global-card-glass h-20 w-20 rounded-full" :src="bloggerInfo.avatar" <image
mode="aspectFill" /> class="uh-global-card-glass h-20 w-20 rounded-full" :src="bloggerInfo.avatar"
mode="aspectFill"
/>
<view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]"> <view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]">
{{ bloggerInfo.nickname }} {{ bloggerInfo.nickname }}
</view> </view>
<view <view
class="desc mt-2 px-10 text-center text-[26rpx] text-white/90 leading-relaxed text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]"> class="desc mt-2 px-10 text-center text-[26rpx] text-white/90 leading-relaxed text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]"
>
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }} {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
</view> </view>
</view> </view>
<image v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill" <image
class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;" /> v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill"
class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;"
/>
</view> </view>
<!-- 站点统计(上浮玻璃卡,与头部衔接) --> <!-- 站点统计(上浮玻璃卡,与头部衔接) -->
<view class="uh-global-card-glass border relative flex z-100 mx-4 rounded-2xl -mt-12"> <view class="uh-global-card-glass relative z-100 mx-4 flex border rounded-2xl -mt-12">
<view v-for="item in allStats" :key="item.key" class="flex-1 py-6 text-center"> <view v-for="item in allStats" :key="item.key" class="flex-1 py-6 text-center">
<view class="text-lg text-gray-900 font-bold"> <view class="text-lg text-gray-900 font-bold">
{{ item.value }} {{ item.value }}
@@ -303,13 +318,17 @@
{{ group.title }} {{ group.title }}
</uh-section-title> </uh-section-title>
<view class="nav-wrap uh-global-card-glass mx-4 overflow-hidden rounded-2xl"> <view class="nav-wrap uh-global-card-glass mx-4 overflow-hidden rounded-2xl">
<view v-for="(nav, index) in group.items" :key="nav.key" <view
v-for="(nav, index) in group.items" :key="nav.key"
class="nav-item flex items-center justify-between px-4" class="nav-item flex items-center justify-between px-4"
:class="index < group.items.length - 1 ? 'border-b border-b-solid border-black/5' : ''" @click="handleOnNav(nav)"> :class="index < group.items.length - 1 ? 'border-b border-b-solid border-black/5' : ''" @click="handleOnNav(nav)"
>
<view class="nav-left flex items-center gap-3 py-3"> <view class="nav-left flex items-center gap-3 py-3">
<view class="h-9 w-9 flex items-center justify-center rounded-xl" <view
:style="{ backgroundColor: nav.bgColor }"> class="h-9 w-9 flex items-center justify-center border border-black/5 rounded-xl"
<wd-icon :name="nav.icon" size="20px" color="#ffffff" /> :style="{ backgroundColor: toLightBg(nav.bgColor) }"
>
<wd-icon :name="nav.icon" size="20px" :color="toSolidColor(nav.bgColor)" />
</view> </view>
<text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text> <text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text>
</view> </view>
+3
View File
@@ -4,6 +4,7 @@ import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList } from '@/api/halo' import { getCategoryList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkThumbnailUrl } from '@/utils/url' import { checkThumbnailUrl } from '@/utils/url'
import { sleep } from '@/utils/common'
import { t } from '@/locale' import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ICategory } from '@/api/types/halo' import type { ICategory } from '@/api/types/halo'
@@ -50,6 +51,8 @@ function handleInitPage() {
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Loading) updateLoadingStatus(DataLoadingStatusEnum.Loading)
// 增加延迟,提升用户体验
await sleep(800)
// 审核模式 // 审核模式
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
const auditCategoryNames = appConfigStore.auditData.spec?.categories || [] const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
+73 -73
View File
@@ -1,49 +1,49 @@
<script lang="ts" setup> <script lang="ts" setup>
/** /**
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻) * 图库页(源自旧项目 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'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url' import { checkImageUrl } from '@/utils/url'
import { t } from '@/locale' import { t } from '@/locale'
import { usePluginAvailable } from '@/utils/plugin' import { usePluginAvailable } from '@/utils/plugin'
import type { ICategory, IPhoto, IPhotoGroup } from '@/api/types/halo' import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '图库', navigationBarTitleText: '图库',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs) const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig) const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
/** 依赖插件(plugin-photos) */ /** 依赖插件(plugin-photos) */
const uniHaloPluginId = 'plugin-photos' const uniHaloPluginId = 'plugin-photos'
const uniHaloPluginAvailable = ref(true) const uniHaloPluginAvailable = ref(true)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const loading = ref<'loading' | 'success' | 'error'>('loading')
const category = ref<{ activeIndex : number, list : IPhotoGroup[] }>({ const category = ref<{ activeIndex: number, list: IPhotoGroup[] }>({
activeIndex: 0, activeIndex: 0,
list: [], list: [],
}) })
const queryParams = ref({ size: 10, page: 1, group: '' }) const queryParams = ref({ size: 10, page: 1, group: '' })
const isLoadMore = ref(false) const isLoadMore = ref(false)
const loadMoreText = ref('') const loadMoreText = ref('')
const hasNext = ref(false) const hasNext = ref(false)
const dataList = ref<IPhoto[]>([]) const dataList = ref<IPhoto[]>([])
const lock = ref(false) const lock = ref(false)
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetCategory() { async function handleGetCategory() {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示 // 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || [] const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
@@ -85,9 +85,9 @@
loading.value = 'error' loading.value = 'error'
category.value = { activeIndex: 0, list: [] } category.value = { activeIndex: 0, list: [] }
} }
} }
async function handleGetData(isClearList = false) { async function handleGetData(isClearList = false) {
if (isClearList) { if (isClearList) {
dataList.value = [] dataList.value = []
queryParams.value.page = 1 queryParams.value.page = 1
@@ -125,20 +125,19 @@
lock.value = false lock.value = false
}, 500) }, 500)
} }
} }
function handleGetDataByCategory(index : number, cate : IPhotoGroup) { function handleGetDataByCategory(index: number, cate: IPhotoGroup) {
queryParams.value.group = cate.metadata.name || '' queryParams.value.group = cate.metadata.name || ''
queryParams.value.page = 1 queryParams.value.page = 1
uni.pageScrollTo({ scrollTop: 0, duration: 500 }) uni.pageScrollTo({ scrollTop: 0, duration: 500 })
dataList.value = [] dataList.value = []
category.value.activeIndex = index category.value.activeIndex = index
handleGetData(true) handleGetData(true)
} }
/* ---------------- 图片预览 ---------------- */
/* ---------------- 图片预览 ---------------- */ function handlePreview(data: IPhoto) {
function handlePreview(data : IPhoto) {
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name) const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
uni.previewImage({ uni.previewImage({
current, current,
@@ -146,26 +145,26 @@
indicator: 'number', indicator: 'number',
loop: true, loop: true,
}) })
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad(async () => { onLoad(async () => {
// 检查插件可用性 // 检查插件可用性
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId) uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
if (!uniHaloPluginAvailable.value) { if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
}) })
watch(galleryConfig, (newVal) => { watch(galleryConfig, (newVal) => {
if (!newVal) if (!newVal)
return return
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') }) uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
handleGetCategory() handleGetCategory()
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
onPullDownRefresh(() => { onPullDownRefresh(() => {
if (!uniHaloPluginAvailable.value) { if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
@@ -174,9 +173,9 @@
isLoadMore.value = false isLoadMore.value = false
queryParams.value.page = 1 queryParams.value.page = 1
handleGetData(true) handleGetData(true)
}) })
onReachBottom(() => { onReachBottom(() => {
if (!uniHaloPluginAvailable.value) if (!uniHaloPluginAvailable.value)
return return
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
@@ -191,53 +190,54 @@
else { else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') }) uni.showToast({ icon: 'none', title: t('common.noMoreData') })
} }
}) })
</script> </script>
<template> <template>
<view class="bg-page min-h-screen w-screen flex flex-col 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
error-text="检测到当前插件没有安装或者启用无法使用图库功能哦请联系管理员" @on-refresh="handleGetCategory" /> v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
error-text="检测到当前插件没有安装或者启用无法使用图库功能哦请联系管理员" @on-refresh="handleGetCategory"
/>
<template v-else> <template v-else>
<wd-sticky> <wd-sticky>
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3"> <scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
<view v-for="(cate,index) in category.list" :key="cate.spec.displayName" <view
class="uh-global-card-glass border ml-3 mb-1 px-4 py-1 uh-shadow-xs text-sm rounded-2xl inline-block" v-for="(cate, index) in category.list" :key="cate.spec.displayName"
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-block border rounded-2xl px-4 py-1 text-sm"
:class="{ :class="{
'bg-primary text-gray-900 font-bold': index === category.activeIndex, 'bg-primary text-gray-900 font-bold': index === category.activeIndex,
}" @click="handleGetDataByCategory(index,cate)"> }" @click="handleGetDataByCategory(index, cate)"
{{ cate.spec.displayName }}({{cate.status?.photoCount??0}}) >
{{ cate.spec.displayName }}({{ cate.status?.photoCount ?? 0 }})
</view> </view>
</scroll-view> </scroll-view>
</wd-sticky> </wd-sticky>
<!-- 骨架屏 --> <!-- 加载/错误占位(统一 uh-data-loading,错误可重试) -->
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3"> <view v-if="loading !== 'success'" class="box-border p-3">
<wd-skeleton :row="4" :animated="true" /> <uh-data-loading :loading-status="loading" @refresh="handleGetCategory" />
</view>
<!-- 错误态 -->
<view v-else-if="loading === 'error'"
class="error-wrap h-[60vh] w-full flex flex-col items-center justify-center gap-6">
<wd-empty description="阿偶,获取数据失败了~" />
<wd-button size="small" plain type="primary" @click="handleGetCategory()">
刷新试试
</wd-button>
</view> </view>
<!-- 内容区域 --> <!-- 内容区域 -->
<view v-else class="box-border w-full p-3"> <view v-else class="box-border w-full p-3">
<view v-if="dataList.length === 0" <view
class="h-[70vh] w-full flex items-center justify-center content-empty"> v-if="dataList.length === 0"
class="h-[70vh] w-full flex items-center justify-center content-empty"
>
<wd-empty description="博主还没有分享图片~" /> <wd-empty description="博主还没有分享图片~" />
</view> </view>
<block v-else> <block v-else>
<!-- 瀑布流(双列) --> <!-- 瀑布流(双列) -->
<view class="grid grid-cols-2 gap-3"> <view class="grid grid-cols-2 gap-3">
<view v-for="(item, index) in dataList" :key="index" <view
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl"> v-for="(item, index) in dataList" :key="index"
<image class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl"
@click="handlePreview(item)" /> >
<image
class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load
@click="handlePreview(item)"
/>
</view> </view>
</view> </view>
<view class="load-text w-full py-4 text-center text-xs text-gray-500"> <view class="load-text w-full py-4 text-center text-xs text-gray-500">
+72 -72
View File
@@ -1,70 +1,68 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch, onMounted } from 'vue' import { computed, onMounted, ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' 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 { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { t } from '@/locale' import { t } from '@/locale'
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', navigationStyle: 'custom',
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const haloConfigs = computed(() => appConfigStore.configs) const haloConfigs = computed(() => appConfigStore.configs)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const loading = ref<'loading' | 'success' | 'error'>('loading')
const isLoadMore = ref(false) const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading')) const loadMoreText = ref(t('common.loading'))
const articleList = ref<IPost[]>([]) const articleList = ref<IPost[]>([])
const result = ref<{ hasNext : boolean }>({ hasNext: false }) const result = ref<{ hasNext: boolean }>({ hasNext: false })
const queryParams = ref({ const queryParams = ref({
size: 5, size: 5,
page: 1, page: 1,
sort: ['spec.pinned,desc', 'spec.publishTime,desc'], sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
}) })
/* ---------------- 计算属性 ---------------- */ /* ---------------- 计算属性 ---------------- */
const appInfo = computed(() => { const appInfo = computed(() => {
const appInfoData = haloConfigs.value.appConfig?.appInfo as { name ?: string, logo ?: string } | undefined const appInfoData = haloConfigs.value.appConfig?.appInfo as { name?: string, logo?: string } | undefined
return { return {
name: appInfoData?.name || 'uni-halo', name: appInfoData?.name || 'uni-halo',
logo: checkImageUrl(appInfoData?.logo), logo: checkImageUrl(appInfoData?.logo),
} }
}) })
const bloggerInfo = computed(() => { const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname ?: string, avatar ?: string } | undefined const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, avatar?: string } | undefined
return { return {
nickname: blogger?.nickname || '', nickname: blogger?.nickname || '',
avatar: checkAvatarUrl(blogger?.avatar), avatar: checkAvatarUrl(blogger?.avatar),
} }
}) })
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const globalAppSettings = computed(() => settingStore.settings)
const globalAppSettings = computed(() => settingStore.settings) /* ---------------- 数据加载 ---------------- */
async function handleQuery() {
/* ---------------- 数据加载 ---------------- */
async function handleQuery() {
handleGetArticleList() handleGetArticleList()
} }
/** 文章列表 */ /** 文章列表 */
async function handleGetArticleList() { async function handleGetArticleList() {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序) // 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
const auditPostNames = appConfigStore.auditData.spec?.posts || [] const auditPostNames = appConfigStore.auditData.spec?.posts || []
@@ -72,8 +70,8 @@
const res = await getPostList({ page: 1, size: 0, sort: ['spec.publishTime,desc'] }) const res = await getPostList({ page: 1, size: 0, sort: ['spec.publishTime,desc'] })
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name)) const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
articleList.value = filtered.map((item) => { articleList.value = filtered.map((item) => {
item.owner.avatar = checkAvatarUrl(item.owner.avatar); item.owner.avatar = checkAvatarUrl(item.owner.avatar)
return item; return item
}) })
loading.value = 'success' loading.value = 'success'
loadMoreText.value = t('common.noMore') loadMoreText.value = t('common.noMore')
@@ -101,8 +99,8 @@
articleList.value = (isLoadMore.value articleList.value = (isLoadMore.value
? articleList.value.concat(res.data.items) ? articleList.value.concat(res.data.items)
: res.data.items).map((item) => { : res.data.items).map((item) => {
item.owner.avatar = checkAvatarUrl(item.owner.avatar); item.owner.avatar = checkAvatarUrl(item.owner.avatar)
return item; return item
}) })
loading.value = 'success' loading.value = 'success'
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore') loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
@@ -116,25 +114,25 @@
uni.hideLoading() uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
} }
} }
/* ---------------- 跳转 ---------------- */ /* ---------------- 跳转 ---------------- */
function handleToArticleDetail(article : IPost) { function handleToArticleDetail(article: IPost) {
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`, url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
animationType: 'slide-in-right', animationType: 'slide-in-right',
}) })
} }
function handleToSearch() { function handleToSearch() {
uni.navigateTo({ url: '/pages-blog/search/search' }) uni.navigateTo({ url: '/pages-blog/search/search' })
} }
function handleOnLogoToPage() { function handleOnLogoToPage() {
uni.switchTab({ url: '/pages/tabbar/about/about' }) uni.switchTab({ url: '/pages/tabbar/about/about' })
} }
function handleToTopPage(duration = 500) { function handleToTopPage(duration = 500) {
uni.pageScrollTo({ uni.pageScrollTo({
scrollTop: 0, scrollTop: 0,
duration, duration,
@@ -142,19 +140,17 @@
console.error('回顶失败', err) console.error('回顶失败', err)
}, },
}) })
} }
/* ---------------- 生命周期 ---------------- */
onPullDownRefresh(() => {
/* ---------------- 生命周期 ---------------- */
onPullDownRefresh(() => {
isLoadMore.value = false isLoadMore.value = false
queryParams.value.page = 1 queryParams.value.page = 1
handleQuery() handleQuery()
}) })
onReachBottom(() => { onReachBottom(() => {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') }) uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return return
@@ -167,23 +163,23 @@
else { else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') }) uni.showToast({ icon: 'none', title: t('common.noMoreData') })
} }
}) })
// 首次加载 // 首次加载
onMounted(() => { onMounted(() => {
handleQuery() handleQuery()
}) })
</script> </script>
<template> <template>
<view class="bg-page min-h-screen w-screen flex flex-col"> <view class="min-h-screen w-screen flex flex-col bg-page">
<!-- 骨架屏 --> <!-- 加载/错误占位(列表为空时展示,避免覆盖下拉刷新的旧内容) -->
<view v-if="loading !== 'success' && articleList.length === 0" class="loading-wrap px-3"> <view v-if="loading !== 'success' && articleList.length === 0">
<wd-skeleton :row="3" :animated="true" /> <uh-data-loading :loading-status="loading" @refresh="handleQuery" />
</view> </view>
<block v-else> <block v-else>
<!-- 轮播--> <!-- 轮播 -->
<uh-home-banner /> <uh-home-banner />
<!-- 公告 --> <!-- 公告 -->
@@ -196,11 +192,13 @@
<uh-home-category /> <uh-home-category />
<!-- 最新文章 --> <!-- 最新文章 -->
<uh-section-title class="mb-4 px-3 box-border"> <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
@click="handleToSearch()"> class="uh-global-card-glass flex items-center justify-center rounded-md p-1 text-gray-400"
@click="handleToSearch()"
>
<wd-icon name="arrow-right" size="12px" /> <wd-icon name="arrow-right" size="12px" />
</view> </view>
</template> </template>
@@ -210,9 +208,11 @@
<wd-empty description="博主还没有发表任何内容~" /> <wd-empty description="博主还没有发表任何内容~" />
</view> </view>
<block v-else> <block v-else>
<view class="p-3 pt-0 flex flex-col gap-y-3" :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
@on-click="handleToArticleDetail" /> v-for="(article, index) in articleList" :key="index" from="home" :article="article"
@on-click="handleToArticleDetail"
/>
</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 }}
+2 -7
View File
@@ -278,14 +278,9 @@
<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>
<!-- 加载中 -->
<view v-if="loading === 'loading'" class="loading-wrap p-3">
<wd-skeleton :row="3" :animated="true" />
</view>
<!-- 加载失败(可重试) --> <!-- 加载失败(可重试) -->
<uh-data-loading v-else-if="loading === 'error'" :loading-status="loading" min-height="60vh" <uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
error-text="瞬间加载失败请点击重试" @refresh="handleGetData" /> @refresh="handleGetData" />
<view v-else class="flex flex-col gap-3 px-4"> <view v-else class="flex flex-col gap-3 px-4">
<view v-if="dataList.length === 0" <view v-if="dataList.length === 0"
+3
View File
@@ -0,0 +1,3 @@
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}