mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-13 00:50:40 +08:00
refactor: 优化页面加载状态与组件样式,统一数据加载逻辑
1. 新增通用sleep工具函数 2. 统一使用uh-data-loading组件处理加载/错误状态 3. 优化回到顶部按钮z-index与分类页面加载延迟 4. 重构关于页导航图标样式,优化视觉效果 5. 格式化代码缩进与排版
This commit is contained in:
+314
-295
@@ -1,329 +1,348 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
/**
|
||||
* 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻)
|
||||
* 功能:博主信息 + 站点统计 + 功能导航 + 版权
|
||||
* 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块)
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getBlogStatistics } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { checkHasAdminLogin } from '@/utils/auth'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IBlogStats } from '@/api/types/halo'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getBlogStatistics } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { checkHasAdminLogin } from '@/utils/auth'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IBlogStats } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as
|
||||
| { nickname ?: string, avatar ?: string, description ?: string }
|
||||
| undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
description: blogger?.description || '',
|
||||
}
|
||||
})
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as
|
||||
| { nickname?: string, avatar?: string, description?: string }
|
||||
| undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
description: blogger?.description || '',
|
||||
}
|
||||
})
|
||||
|
||||
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
|
||||
| { bgImageUrl ?: string, waveImageUrl ?: string }
|
||||
| undefined)
|
||||
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
|
||||
| { bgImageUrl?: string, waveImageUrl?: string }
|
||||
| undefined)
|
||||
|
||||
const calcProfileStyle = computed(() => ({
|
||||
backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`,
|
||||
}))
|
||||
const calcProfileStyle = computed(() => ({
|
||||
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
|
||||
| {
|
||||
copyrightConfig ?: { enabled ?: boolean, content ?: string }
|
||||
disclaimers ?: { enabled ?: boolean }
|
||||
showAboutSystem ?: boolean
|
||||
}
|
||||
| undefined)
|
||||
const basicConfig = computed(() => haloConfigs.value.basicConfig as
|
||||
| {
|
||||
copyrightConfig?: { enabled?: boolean, content?: string }
|
||||
disclaimers?: { enabled?: boolean }
|
||||
showAboutSystem?: boolean
|
||||
}
|
||||
| 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 socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled)
|
||||
const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean } | undefined)?.loveEnabled)
|
||||
const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const statisticsShowMore = ref(false)
|
||||
const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 })
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const statisticsShowMore = ref(false)
|
||||
const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 })
|
||||
|
||||
/** 主行统计(常驻展示) */
|
||||
const allStats = computed(() => [
|
||||
{ key: 'post', label: '内容', value: statistics.value.post },
|
||||
{ key: 'visit', label: '访客', value: statistics.value.visit },
|
||||
{ key: 'category', label: '分类', value: statistics.value.category },
|
||||
{ key: 'comment', label: '评论', value: statistics.value.comment },
|
||||
{ key: 'upvote', label: '点赞', value: statistics.value.upvote },
|
||||
])
|
||||
/** 主行统计(常驻展示) */
|
||||
const allStats = computed(() => [
|
||||
{ key: 'post', label: '内容', value: statistics.value.post },
|
||||
{ key: 'visit', label: '访客', value: statistics.value.visit },
|
||||
{ key: 'category', label: '分类', value: statistics.value.category },
|
||||
{ key: 'comment', label: '评论', value: statistics.value.comment },
|
||||
{ key: 'upvote', label: '点赞', value: statistics.value.upvote },
|
||||
])
|
||||
|
||||
interface INavItem {
|
||||
key : string
|
||||
title : string
|
||||
icon : string
|
||||
/** 图标块背景色(与首页快捷导航同色板,同一功能同色) */
|
||||
bgColor : string
|
||||
rightText : string
|
||||
path : string | null
|
||||
isAdmin ?: boolean
|
||||
openType ?: string
|
||||
show : boolean
|
||||
/** 分组:blog=博客功能 more=更多信息 */
|
||||
group : 'blog' | 'more'
|
||||
}
|
||||
interface INavItem {
|
||||
key: string
|
||||
title: string
|
||||
icon: string
|
||||
/** 图标块背景色(与首页快捷导航同色板,同一功能同色) */
|
||||
bgColor: string
|
||||
rightText: string
|
||||
path: string | null
|
||||
isAdmin?: boolean
|
||||
openType?: string
|
||||
show: boolean
|
||||
/** 分组:blog=博客功能 more=更多信息 */
|
||||
group: 'blog' | 'more'
|
||||
}
|
||||
|
||||
const navList = ref<INavItem[]>([])
|
||||
const navList = ref<INavItem[]>([])
|
||||
|
||||
/** 分组渲染(过滤后空组整组隐藏) */
|
||||
const calcNavGroups = computed(() => {
|
||||
const visible = navList.value.filter(n => n.show)
|
||||
const groupDefs : { key : 'blog' | 'more', title : string }[] = [
|
||||
{ key: 'blog', title: '博客功能' },
|
||||
{ key: 'more', title: '其他功能' },
|
||||
]
|
||||
return groupDefs
|
||||
.map(def => ({ ...def, items: visible.filter(n => n.group === def.key) }))
|
||||
.filter(group => group.items.length > 0)
|
||||
})
|
||||
/** 分组渲染(过滤后空组整组隐藏) */
|
||||
const calcNavGroups = computed(() => {
|
||||
const visible = navList.value.filter(n => n.show)
|
||||
const groupDefs: { key: 'blog' | 'more', title: string }[] = [
|
||||
{ key: 'blog', title: '博客功能' },
|
||||
{ key: 'more', title: '其他功能' },
|
||||
]
|
||||
return groupDefs
|
||||
.map(def => ({ ...def, items: visible.filter(n => n.group === def.key) }))
|
||||
.filter(group => group.items.length > 0)
|
||||
})
|
||||
|
||||
/* ---------------- 功能导航 ---------------- */
|
||||
async function handleGetNavList() {
|
||||
const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics')
|
||||
/* ---------------- 功能导航 ---------------- */
|
||||
/** 图标块浅色背景:品牌深色 rgba 降透明度 → 轻量底色 */
|
||||
function toLightBg(rgba: string) {
|
||||
return rgba.replace('0.95)', '0.15)')
|
||||
}
|
||||
|
||||
navList.value = [
|
||||
{
|
||||
key: 'data-visual',
|
||||
title: '数据看板',
|
||||
icon: 'chart',
|
||||
bgColor: 'rgba(102, 60, 201, 0.95)',
|
||||
rightText: '站点数据可视化',
|
||||
path: '/pages-blog/data-visual/data-visual',
|
||||
show: dataVisualAvailable,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'archives',
|
||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
||||
icon: 'folder',
|
||||
bgColor: 'rgba(3, 169, 244, 0.95)',
|
||||
rightText: calcAuditModeEnabled.value ? '全部已归档内容' : '全部已归档文章',
|
||||
path: '/pages-blog/archives/archives',
|
||||
show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
icon: 'heart',
|
||||
bgColor: 'rgba(255, 76, 103, 0.95)',
|
||||
rightText: '博主的恋爱日记',
|
||||
path: '/pages-blog/love/love',
|
||||
show: loveEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
icon: 'box',
|
||||
bgColor: 'rgba(0, 188, 212, 0.95)',
|
||||
rightText: '查看和进行投票',
|
||||
path: '/pages-blog/votes/votes',
|
||||
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'friend-links',
|
||||
title: '友情链接',
|
||||
icon: 'link',
|
||||
bgColor: 'rgba(0, 150, 136, 0.95)',
|
||||
rightText: '看看博主朋友们吧',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
show: calcLinksPluginEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '免责声明',
|
||||
icon: 'map',
|
||||
bgColor: 'rgba(121, 85, 72, 0.95)',
|
||||
rightText: '博客内容免责声明',
|
||||
path: '/pages-blog/disclaimers/disclaimers',
|
||||
show: !!basicConfig.value?.disclaimers?.enabled,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
icon: 'message',
|
||||
bgColor: 'rgba(255, 152, 0, 0.95)',
|
||||
rightText: '博主常用联系方式',
|
||||
path: '/pages-blog/contact/contact',
|
||||
show: socialEnabled.value,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: '关于项目',
|
||||
icon: 'info',
|
||||
bgColor: 'rgba(96, 125, 139, 0.95)',
|
||||
rightText: '小莫唐尼开源项目',
|
||||
path: '/pages-blog/about/about',
|
||||
show: !!basicConfig.value?.showAboutSystem,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'setting',
|
||||
title: '偏好设置',
|
||||
icon: 'settings',
|
||||
bgColor: 'rgba(121, 134, 203, 0.95)',
|
||||
rightText: '首页布局、卡片样式等本地偏好',
|
||||
path: '/pages-blog/setting/setting',
|
||||
show: true,
|
||||
group: 'more',
|
||||
},
|
||||
]
|
||||
}
|
||||
/** 图标颜色:品牌深色实色 */
|
||||
function toSolidColor(rgba: string) {
|
||||
return rgba.replace('0.95)', '1)')
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
try {
|
||||
const res = await getBlogStatistics()
|
||||
statistics.value = res.data
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取统计失败', err)
|
||||
uni.showToast({ icon: 'none', title: t('common.loadFailedRetry') })
|
||||
}
|
||||
finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
async function handleGetNavList() {
|
||||
const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics')
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handleOnNav(data : { path : string | null, isAdmin ?: boolean }) {
|
||||
const { path, isAdmin } = data
|
||||
if (!path)
|
||||
return
|
||||
navList.value = [
|
||||
{
|
||||
key: 'data-visual',
|
||||
title: '数据看板',
|
||||
icon: 'chart',
|
||||
bgColor: 'rgba(102, 60, 201, 0.95)',
|
||||
rightText: '站点数据可视化',
|
||||
path: '/pages-blog/data-visual/data-visual',
|
||||
show: dataVisualAvailable,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'archives',
|
||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
||||
icon: 'folder',
|
||||
bgColor: 'rgba(3, 169, 244, 0.95)',
|
||||
rightText: calcAuditModeEnabled.value ? '全部已归档内容' : '全部已归档文章',
|
||||
path: '/pages-blog/archives/archives',
|
||||
show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
icon: 'heart',
|
||||
bgColor: 'rgba(255, 76, 103, 0.95)',
|
||||
rightText: '博主的恋爱日记',
|
||||
path: '/pages-blog/love/love',
|
||||
show: loveEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
icon: 'box',
|
||||
bgColor: 'rgba(0, 188, 212, 0.95)',
|
||||
rightText: '查看和进行投票',
|
||||
path: '/pages-blog/votes/votes',
|
||||
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'friend-links',
|
||||
title: '友情链接',
|
||||
icon: 'link',
|
||||
bgColor: 'rgba(0, 150, 136, 0.95)',
|
||||
rightText: '看看博主朋友们吧',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
show: calcLinksPluginEnabled.value,
|
||||
// show: true,
|
||||
group: 'blog',
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '免责声明',
|
||||
icon: 'map',
|
||||
bgColor: 'rgba(121, 85, 72, 0.95)',
|
||||
rightText: '博客内容免责声明',
|
||||
path: '/pages-blog/disclaimers/disclaimers',
|
||||
show: !!basicConfig.value?.disclaimers?.enabled,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
icon: 'message',
|
||||
bgColor: 'rgba(255, 152, 0, 0.95)',
|
||||
rightText: '博主常用联系方式',
|
||||
path: '/pages-blog/contact/contact',
|
||||
show: socialEnabled.value,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: '关于项目',
|
||||
icon: 'info',
|
||||
bgColor: 'rgba(96, 125, 139, 0.95)',
|
||||
rightText: '小莫唐尼开源项目',
|
||||
path: '/pages-blog/about/about',
|
||||
show: !!basicConfig.value?.showAboutSystem,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
{
|
||||
key: 'setting',
|
||||
title: '偏好设置',
|
||||
icon: 'settings',
|
||||
bgColor: 'rgba(121, 134, 203, 0.95)',
|
||||
rightText: '首页布局、卡片样式等本地偏好',
|
||||
path: '/pages-blog/setting/setting',
|
||||
show: true,
|
||||
group: 'more',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 拦截后台管理页面(需超管登录)
|
||||
if (isAdmin && !checkHasAdminLogin()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '未登录超管账号或登录状态已过期,是否立即登录?',
|
||||
showCancel: true,
|
||||
cancelText: '否',
|
||||
cancelColor: '#999999',
|
||||
confirmText: '是',
|
||||
confirmColor: '#03a9f4',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
try {
|
||||
const res = await getBlogStatistics()
|
||||
statistics.value = res.data
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取统计失败', err)
|
||||
uni.showToast({ icon: 'none', title: t('common.loadFailedRetry') })
|
||||
}
|
||||
finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handleOnNav(data: { path: string | null, isAdmin?: boolean }) {
|
||||
const { path, isAdmin } = data
|
||||
if (!path)
|
||||
return
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
watch(haloConfigs, () => {
|
||||
handleGetNavList()
|
||||
}, { deep: true, immediate: true })
|
||||
// 拦截后台管理页面(需超管登录)
|
||||
if (isAdmin && !checkHasAdminLogin()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '未登录超管账号或登录状态已过期,是否立即登录?',
|
||||
showCancel: true,
|
||||
cancelText: '否',
|
||||
cancelColor: '#999999',
|
||||
confirmText: '是',
|
||||
confirmColor: '#03a9f4',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleGetData()
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
handleGetData()
|
||||
})
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
watch(haloConfigs, () => {
|
||||
handleGetNavList()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
handleGetData()
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
handleGetData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border bg-page min-h-screen w-screen pb-8">
|
||||
<!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) -->
|
||||
<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="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"
|
||||
mode="aspectFill" />
|
||||
<view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]">
|
||||
{{ bloggerInfo.nickname }}
|
||||
</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)]">
|
||||
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
|
||||
</view>
|
||||
</view>
|
||||
<image 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 class="box-border min-h-screen w-screen bg-page pb-8">
|
||||
<!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) -->
|
||||
<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 bg-black/30 backdrop-blur-[2rpx]" />
|
||||
<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"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]">
|
||||
{{ bloggerInfo.nickname }}
|
||||
</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)]"
|
||||
>
|
||||
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
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 class="uh-global-card-glass border relative flex z-100 mx-4 rounded-2xl -mt-12">
|
||||
<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">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
<view class="mt-1 text-xs text-gray-500">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 站点统计(上浮玻璃卡,与头部衔接) -->
|
||||
<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 class="text-lg text-gray-900 font-bold">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
<view class="mt-1 text-xs text-gray-500">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能导航(分组玻璃卡) -->
|
||||
<template v-for="group in calcNavGroups" :key="group.key">
|
||||
<uh-section-title class="mx-4 mb-3 mt-8">
|
||||
{{ group.title }}
|
||||
</uh-section-title>
|
||||
<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"
|
||||
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)">
|
||||
<view class="nav-left flex items-center gap-3 py-3">
|
||||
<view class="h-9 w-9 flex items-center justify-center rounded-xl"
|
||||
:style="{ backgroundColor: nav.bgColor }">
|
||||
<wd-icon :name="nav.icon" size="20px" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text>
|
||||
</view>
|
||||
<view class="nav-right flex items-center gap-2">
|
||||
<text class="nav-right-text text-xs text-gray-400">{{ nav.rightText }}</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 功能导航(分组玻璃卡) -->
|
||||
<template v-for="group in calcNavGroups" :key="group.key">
|
||||
<uh-section-title class="mx-4 mb-3 mt-8">
|
||||
{{ group.title }}
|
||||
</uh-section-title>
|
||||
<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"
|
||||
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)"
|
||||
>
|
||||
<view class="nav-left flex items-center gap-3 py-3">
|
||||
<view
|
||||
class="h-9 w-9 flex items-center justify-center border border-black/5 rounded-xl"
|
||||
:style="{ backgroundColor: toLightBg(nav.bgColor) }"
|
||||
>
|
||||
<wd-icon :name="nav.icon" size="20px" :color="toSolidColor(nav.bgColor)" />
|
||||
</view>
|
||||
<text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text>
|
||||
</view>
|
||||
<view class="nav-right flex items-center gap-2">
|
||||
<text class="nav-right-text text-xs text-gray-400">{{ nav.rightText }}</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 版权 -->
|
||||
<view v-if="copyrightConfig?.enabled" class="mt-6 px-6 text-center text-xs text-gray-400">
|
||||
<view>{{ copyrightConfig.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 版权 -->
|
||||
<view v-if="copyrightConfig?.enabled" class="mt-6 px-6 text-center text-xs text-gray-400">
|
||||
<view>{{ copyrightConfig.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getCategoryList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkThumbnailUrl } from '@/utils/url'
|
||||
import { sleep } from '@/utils/common'
|
||||
import { t } from '@/locale'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import type { ICategory } from '@/api/types/halo'
|
||||
@@ -50,6 +51,8 @@ function handleInitPage() {
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||
// 增加延迟,提升用户体验
|
||||
await sleep(800)
|
||||
// 审核模式
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
|
||||
|
||||
@@ -1,250 +1,250 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
/**
|
||||
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
|
||||
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { ICategory, IPhoto, IPhotoGroup } from '@/api/types/halo'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '图库',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '图库',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||
|
||||
/** 依赖插件(plugin-photos) */
|
||||
const uniHaloPluginId = 'plugin-photos'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
/** 依赖插件(plugin-photos) */
|
||||
const uniHaloPluginId = 'plugin-photos'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const category = ref<{ activeIndex : number, list : IPhotoGroup[] }>({
|
||||
activeIndex: 0,
|
||||
list: [],
|
||||
})
|
||||
const queryParams = ref({ size: 10, page: 1, group: '' })
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref('')
|
||||
const hasNext = ref(false)
|
||||
const dataList = ref<IPhoto[]>([])
|
||||
const lock = ref(false)
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const category = ref<{ activeIndex: number, list: IPhotoGroup[] }>({
|
||||
activeIndex: 0,
|
||||
list: [],
|
||||
})
|
||||
const queryParams = ref({ size: 10, page: 1, group: '' })
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref('')
|
||||
const hasNext = ref(false)
|
||||
const dataList = ref<IPhoto[]>([])
|
||||
const lock = ref(false)
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetCategory() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
|
||||
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
const filtered = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.filter(item => auditGroupNames.includes(item.metadata.name))
|
||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||
category.value.list = filtered
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
else {
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
category.value.list = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||
category.value.list.unshift({ metadata: { name: undefined }, spec: { displayName: '全部', priority: 0 } })
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
}
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetCategory() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
|
||||
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
const filtered = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.filter(item => auditGroupNames.includes(item.metadata.name))
|
||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||
category.value.list = filtered
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
else {
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
category.value.list = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||
category.value.list.unshift({ metadata: { name: undefined }, spec: { displayName: '全部', priority: 0 } })
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetData(isClearList = false) {
|
||||
if (isClearList) {
|
||||
dataList.value = []
|
||||
queryParams.value.page = 1
|
||||
}
|
||||
async function handleGetData(isClearList = false) {
|
||||
if (isClearList) {
|
||||
dataList.value = []
|
||||
queryParams.value.page = 1
|
||||
}
|
||||
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = ''
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = ''
|
||||
|
||||
try {
|
||||
const res = await getPhotoListByGroupName({ ...queryParams.value })
|
||||
hasNext.value = res.data.hasNext
|
||||
loading.value = 'success'
|
||||
if (res.data.items.length !== 0) {
|
||||
const list = res.data.items.map(item => ({
|
||||
...item,
|
||||
spec: { ...item.spec, url: checkImageUrl(item.spec.url || item.spec.cover) },
|
||||
}))
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(list)
|
||||
: list
|
||||
}
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
lock.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await getPhotoListByGroupName({ ...queryParams.value })
|
||||
hasNext.value = res.data.hasNext
|
||||
loading.value = 'success'
|
||||
if (res.data.items.length !== 0) {
|
||||
const list = res.data.items.map(item => ({
|
||||
...item,
|
||||
spec: { ...item.spec, url: checkImageUrl(item.spec.url || item.spec.cover) },
|
||||
}))
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(list)
|
||||
: list
|
||||
}
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
lock.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function handleGetDataByCategory(index : number, cate : IPhotoGroup) {
|
||||
queryParams.value.group = cate.metadata.name || ''
|
||||
queryParams.value.page = 1
|
||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
||||
dataList.value = []
|
||||
category.value.activeIndex = index
|
||||
handleGetData(true)
|
||||
}
|
||||
function handleGetDataByCategory(index: number, cate: IPhotoGroup) {
|
||||
queryParams.value.group = cate.metadata.name || ''
|
||||
queryParams.value.page = 1
|
||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
||||
dataList.value = []
|
||||
category.value.activeIndex = index
|
||||
handleGetData(true)
|
||||
}
|
||||
|
||||
/* ---------------- 图片预览 ---------------- */
|
||||
function handlePreview(data: IPhoto) {
|
||||
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls: dataList.value.map(x => x.spec.url),
|
||||
indicator: 'number',
|
||||
loop: true,
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------- 图片预览 ---------------- */
|
||||
function handlePreview(data : IPhoto) {
|
||||
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls: dataList.value.map(x => x.spec.url),
|
||||
indicator: 'number',
|
||||
loop: true,
|
||||
})
|
||||
}
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
// 检查插件可用性
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
// 检查插件可用性
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
})
|
||||
watch(galleryConfig, (newVal) => {
|
||||
if (!newVal)
|
||||
return
|
||||
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
|
||||
handleGetCategory()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(galleryConfig, (newVal) => {
|
||||
if (!newVal)
|
||||
return
|
||||
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
|
||||
handleGetCategory()
|
||||
}, { deep: true, immediate: true })
|
||||
onPullDownRefresh(() => {
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
dataList.value = []
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleGetData(true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
dataList.value = []
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleGetData(true)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!uniHaloPluginAvailable.value)
|
||||
return
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData(false)
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
onReachBottom(() => {
|
||||
if (!uniHaloPluginAvailable.value)
|
||||
return
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData(false)
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-page min-h-screen w-screen flex flex-col pb-6">
|
||||
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员" @on-refresh="handleGetCategory" />
|
||||
<template v-else>
|
||||
<wd-sticky>
|
||||
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
||||
<view v-for="(cate,index) in category.list" :key="cate.spec.displayName"
|
||||
class="uh-global-card-glass border ml-3 mb-1 px-4 py-1 uh-shadow-xs text-sm rounded-2xl inline-block"
|
||||
:class="{
|
||||
'bg-primary text-gray-900 font-bold': index === category.activeIndex,
|
||||
}" @click="handleGetDataByCategory(index,cate)">
|
||||
{{ cate.spec.displayName }}({{cate.status?.photoCount??0}})
|
||||
</view>
|
||||
</scroll-view>
|
||||
</wd-sticky>
|
||||
<view class="min-h-screen w-screen flex flex-col bg-page pb-6">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员" @on-refresh="handleGetCategory"
|
||||
/>
|
||||
<template v-else>
|
||||
<wd-sticky>
|
||||
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
||||
<view
|
||||
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="{
|
||||
'bg-primary text-gray-900 font-bold': index === category.activeIndex,
|
||||
}" @click="handleGetDataByCategory(index, cate)"
|
||||
>
|
||||
{{ cate.spec.displayName }}({{ cate.status?.photoCount ?? 0 }})
|
||||
</view>
|
||||
</scroll-view>
|
||||
</wd-sticky>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3">
|
||||
<wd-skeleton :row="4" :animated="true" />
|
||||
</view>
|
||||
<!-- 加载/错误占位(统一 uh-data-loading,错误可重试) -->
|
||||
<view v-if="loading !== 'success'" class="box-border p-3">
|
||||
<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 v-else class="box-border w-full p-3">
|
||||
<view v-if="dataList.length === 0"
|
||||
class="h-[70vh] w-full flex items-center justify-center content-empty">
|
||||
<wd-empty description="博主还没有分享图片~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<!-- 瀑布流(双列) -->
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view v-for="(item, index) in dataList" :key="index"
|
||||
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl">
|
||||
<image class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load
|
||||
@click="handlePreview(item)" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-text w-full py-4 text-center text-xs text-gray-500">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="box-border w-full p-3">
|
||||
<view
|
||||
v-if="dataList.length === 0"
|
||||
class="h-[70vh] w-full flex items-center justify-center content-empty"
|
||||
>
|
||||
<wd-empty description="博主还没有分享图片~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<!-- 瀑布流(双列) -->
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="(item, index) in dataList" :key="index"
|
||||
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl"
|
||||
>
|
||||
<image
|
||||
class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load
|
||||
@click="handlePreview(item)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-text w-full py-4 text-center text-xs text-gray-500">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
+196
-196
@@ -1,227 +1,227 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import type { IPost } from '@/api/types/halo'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import type { IPost } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref(t('common.loading'))
|
||||
const articleList = ref<IPost[]>([])
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref(t('common.loading'))
|
||||
const articleList = ref<IPost[]>([])
|
||||
|
||||
const result = ref<{ hasNext : boolean }>({ hasNext: false })
|
||||
const result = ref<{ hasNext: boolean }>({ hasNext: false })
|
||||
|
||||
const queryParams = ref({
|
||||
size: 5,
|
||||
page: 1,
|
||||
sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
|
||||
})
|
||||
const queryParams = ref({
|
||||
size: 5,
|
||||
page: 1,
|
||||
sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
|
||||
})
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const appInfo = computed(() => {
|
||||
const appInfoData = haloConfigs.value.appConfig?.appInfo as { name ?: string, logo ?: string } | undefined
|
||||
return {
|
||||
name: appInfoData?.name || 'uni-halo',
|
||||
logo: checkImageUrl(appInfoData?.logo),
|
||||
}
|
||||
})
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const appInfo = computed(() => {
|
||||
const appInfoData = haloConfigs.value.appConfig?.appInfo as { name?: string, logo?: string } | undefined
|
||||
return {
|
||||
name: appInfoData?.name || 'uni-halo',
|
||||
logo: checkImageUrl(appInfoData?.logo),
|
||||
}
|
||||
})
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname ?: string, avatar ?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
}
|
||||
})
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, avatar?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
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() {
|
||||
handleGetArticleList()
|
||||
}
|
||||
|
||||
/** 文章列表 */
|
||||
async function handleGetArticleList() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
|
||||
const auditPostNames = appConfigStore.auditData.spec?.posts || []
|
||||
try {
|
||||
const res = await getPostList({ page: 1, size: 0, sort: ['spec.publishTime,desc'] })
|
||||
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
|
||||
articleList.value = filtered.map((item) => {
|
||||
item.owner.avatar = checkAvatarUrl(item.owner.avatar)
|
||||
return item
|
||||
})
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取审核文章失败', err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleQuery() {
|
||||
handleGetArticleList()
|
||||
}
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = t('common.loading')
|
||||
|
||||
/** 文章列表 */
|
||||
async function handleGetArticleList() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
|
||||
const auditPostNames = appConfigStore.auditData.spec?.posts || []
|
||||
try {
|
||||
const res = await getPostList({ page: 1, size: 0, sort: ['spec.publishTime,desc'] })
|
||||
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
|
||||
articleList.value = filtered.map((item) => {
|
||||
item.owner.avatar = checkAvatarUrl(item.owner.avatar);
|
||||
return item;
|
||||
})
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取审核文章失败', err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getPostList({ ...toRaw(queryParams.value) })
|
||||
result.value.hasNext = res.data.hasNext
|
||||
articleList.value = (isLoadMore.value
|
||||
? articleList.value.concat(res.data.items)
|
||||
: res.data.items).map((item) => {
|
||||
item.owner.avatar = checkAvatarUrl(item.owner.avatar)
|
||||
return item
|
||||
})
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
console.error('获取文章失败', err)
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = t('common.loading')
|
||||
/* ---------------- 跳转 ---------------- */
|
||||
function handleToArticleDetail(article: IPost) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getPostList({ ...toRaw(queryParams.value) })
|
||||
result.value.hasNext = res.data.hasNext
|
||||
articleList.value = (isLoadMore.value
|
||||
? articleList.value.concat(res.data.items)
|
||||
: res.data.items).map((item) => {
|
||||
item.owner.avatar = checkAvatarUrl(item.owner.avatar);
|
||||
return item;
|
||||
})
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
console.error('获取文章失败', err)
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
function handleToSearch() {
|
||||
uni.navigateTo({ url: '/pages-blog/search/search' })
|
||||
}
|
||||
|
||||
/* ---------------- 跳转 ---------------- */
|
||||
function handleToArticleDetail(article : IPost) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
function handleOnLogoToPage() {
|
||||
uni.switchTab({ url: '/pages/tabbar/about/about' })
|
||||
}
|
||||
|
||||
function handleToSearch() {
|
||||
uni.navigateTo({ url: '/pages-blog/search/search' })
|
||||
}
|
||||
function handleToTopPage(duration = 500) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration,
|
||||
fail: (err) => {
|
||||
console.error('回顶失败', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOnLogoToPage() {
|
||||
uni.switchTab({ url: '/pages/tabbar/about/about' })
|
||||
}
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
|
||||
function handleToTopPage(duration = 500) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration,
|
||||
fail: (err) => {
|
||||
console.error('回顶失败', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
onPullDownRefresh(() => {
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleQuery()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (result.value.hasNext) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetArticleList()
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleQuery()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (result.value.hasNext) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetArticleList()
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
|
||||
// 首次加载
|
||||
onMounted(() => {
|
||||
handleQuery()
|
||||
})
|
||||
// 首次加载
|
||||
onMounted(() => {
|
||||
handleQuery()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-page min-h-screen w-screen flex flex-col">
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading !== 'success' && articleList.length === 0" class="loading-wrap px-3">
|
||||
<wd-skeleton :row="3" :animated="true" />
|
||||
</view>
|
||||
<view class="min-h-screen w-screen flex flex-col bg-page">
|
||||
<!-- 加载/错误占位(列表为空时展示,避免覆盖下拉刷新的旧内容) -->
|
||||
<view v-if="loading !== 'success' && articleList.length === 0">
|
||||
<uh-data-loading :loading-status="loading" @refresh="handleQuery" />
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<!-- 轮播-->
|
||||
<uh-home-banner />
|
||||
<block v-else>
|
||||
<!-- 轮播 -->
|
||||
<uh-home-banner />
|
||||
|
||||
<!-- 公告 -->
|
||||
<uh-home-notify />
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<uh-home-quick-nav />
|
||||
<!-- 公告 -->
|
||||
<uh-home-notify />
|
||||
|
||||
<!-- 精选分类 -->
|
||||
<uh-home-category />
|
||||
<!-- 快捷导航 -->
|
||||
<uh-home-quick-nav />
|
||||
|
||||
<!-- 最新文章 -->
|
||||
<uh-section-title class="mb-4 px-3 box-border">
|
||||
最新内容
|
||||
<template #right>
|
||||
<view 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" />
|
||||
</view>
|
||||
</template>
|
||||
</uh-section-title>
|
||||
<!-- 精选分类 -->
|
||||
<uh-home-category />
|
||||
|
||||
<view v-if="articleList.length === 0" class="article-empty py-10">
|
||||
<wd-empty description="博主还没有发表任何内容~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<view class="p-3 pt-0 flex flex-col gap-y-3" :class="globalAppSettings.layout.home">
|
||||
<uh-article-card v-for="(article, index) in articleList" :key="index" from="home" :article="article"
|
||||
@on-click="handleToArticleDetail" />
|
||||
</view>
|
||||
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
<view v-if="articleList.length > 10" class="to-top-btn" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<uh-notify-dialog />
|
||||
</template>
|
||||
<!-- 最新文章 -->
|
||||
<uh-section-title class="mb-4 box-border px-3">
|
||||
最新内容
|
||||
<template #right>
|
||||
<view
|
||||
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" />
|
||||
</view>
|
||||
</template>
|
||||
</uh-section-title>
|
||||
|
||||
<view v-if="articleList.length === 0" class="article-empty py-10">
|
||||
<wd-empty description="博主还没有发表任何内容~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<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"
|
||||
@on-click="handleToArticleDetail"
|
||||
/>
|
||||
</view>
|
||||
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
<view v-if="articleList.length > 10" class="to-top-btn" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<uh-notify-dialog />
|
||||
</template>
|
||||
|
||||
@@ -278,14 +278,9 @@
|
||||
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员" @on-refresh="handleGetData" />
|
||||
<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"
|
||||
error-text="瞬间加载失败,请点击重试" @refresh="handleGetData" />
|
||||
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
|
||||
@refresh="handleGetData" />
|
||||
|
||||
<view v-else class="flex flex-col gap-3 px-4">
|
||||
<view v-if="dataList.length === 0"
|
||||
|
||||
Reference in New Issue
Block a user