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.  重构偏好设置系统,实现站点默认与本地差异分层管理
4.  删除冗余的测试mock、插件模块与样式文件
5.  优化文章卡片与评论组件样式,更新全局主题色
6.  清理废弃的请求参数与配置项
This commit is contained in:
小莫唐尼
2026-09-03 22:36:26 +08:00
parent cf9c02cfc3
commit 902faa70e7
49 changed files with 3170 additions and 4288 deletions
+193 -384
View File
@@ -1,419 +1,228 @@
<script lang="ts" setup>
/**
* 首页(源自旧项目 pages/tabbar/home/home.vue,新建复刻)
* 功能:顶部栏 + 轮播 Banner + 快捷导航 + 精选分类 + 最新文章列表(分页) + 通知弹窗
*/
import { computed, ref, watch } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList, 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 { ICategory, IPost } from '@/api/types/halo'
import type { IBannerItem } from '@/components/uh-swiper/uh-swiper.vue'
import { computed, ref, watch } 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'
definePage({
style: {
navigationBarTitleText: '首页',
enablePullDownRefresh: true,
navigationStyle: 'custom',
backgroundColor: '#F8F8F8',
},
})
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 categoryList = ref<ICategory[]>([])
const result = ref<{ hasNext: boolean }>({ hasNext: false })
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading'))
const articleList = ref<IPost[]>([])
const queryParams = ref({
size: 5,
page: 1,
sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
})
const result = ref<{ hasNext : boolean }>({ hasNext: false })
/* ---------------- 计算属性 ---------------- */
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 queryParams = ref({
size: 5,
page: 1,
sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
})
const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, avatar?: string } | undefined
return {
nickname: blogger?.nickname || '',
avatar: checkAvatarUrl(blogger?.avatar),
}
})
/* ---------------- 计算属性 ---------------- */
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 calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname ?: string, avatar ?: string } | undefined
return {
nickname: blogger?.nickname || '',
avatar: checkAvatarUrl(blogger?.avatar),
}
})
const calcIsShowQuickNavigationEnabled = computed(() => haloConfigs.value.pageConfig?.homeConfig?.useQuickNavigation)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const calcIsShowCategory = computed(() => {
if (calcAuditModeEnabled.value)
return false
return !!haloConfigs.value.pageConfig?.homeConfig?.useCategory
})
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
const globalAppSettings = computed(() => settingStore.settings)
const bannerConfig = computed(() => haloConfigs.value.pageConfig?.homeConfig?.bannerConfig)
const globalAppSettings = computed(() => settingStore.settings)
/* ---------------- 数据加载 ---------------- */
async function handleQuery() {
handleGetArticleList()
}
/** 快捷导航列表(由配置控制显隐) */
const navList = computed(() => {
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean })?.loveEnabled
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled
return [
{
key: 'archives',
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
bgColor: 'rgba(3, 169, 244, 0.95)',
icon: 'news',
path: '/pages-blog/archives/archives',
show: true,
},
{
key: 'vote',
title: '投票中心',
bgColor: 'rgba(0, 188, 212, 0.95)',
icon: 'box',
path: '/pages-blog/votes/votes',
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
},
{
key: 'disclaimers',
title: '友情链接',
bgColor: 'rgba(0, 150, 136, 0.95)',
icon: 'link',
path: '/pages-blog/friend-links/friend-links',
show: calcLinksPluginEnabled.value,
},
{
key: 'love',
title: '恋爱日记',
bgColor: 'rgba(255, 76, 103, 0.95)',
icon: 'heart',
path: '/pages-blog/love/love',
show: loveEnabled,
},
{
key: 'contact-blogger',
title: '联系博主',
bgColor: 'rgba(255, 152, 0, 0.95)',
icon: 'message',
path: '/pages-blog/contact/contact',
show: socialEnabled,
},
]
})
/** 文章列表 */
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() {
// 轮播图数据由 uh-swiper 组件内部请求公开 banners 接口,页面不再组装
await Promise.all([handleGetArticleList(), handleGetCategoryList()])
}
if (!isLoadMore.value) {
loading.value = 'loading'
}
loadMoreText.value = t('common.loading')
/** 精选分类 */
async function handleGetCategoryList() {
if (calcAuditModeEnabled.value || !calcIsShowCategory.value) {
loading.value = 'success'
return
}
try {
const res = await getCategoryList({ fieldSelector: ['spec.hideFromList=false'], size: 10 })
categoryList.value = res.data.items
.map(item => ({ ...item, postCount: item.postCount ?? 0 }))
.sort((a, b) => (b.postCount || 0) - (a.postCount || 0))
loading.value = 'success'
}
catch (err) {
console.error('获取分类失败', err)
loading.value = 'error'
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 500)
}
}
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()
}
}
/** 文章列表 */
async function handleGetArticleList() {
if (calcAuditModeEnabled.value) {
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
const auditPostNames = appConfigStore.auditData.spec?.posts || []
try {
const res = await getPostList({ page: 1, size: 99999, sort: ['spec.publishTime,desc'] })
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
const orderMap = new Map(auditPostNames.map((name, index) => [name, index]))
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
articleList.value = filtered
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
}
/* ---------------- 跳转 ---------------- */
function handleToArticleDetail(article : IPost) {
uni.navigateTo({
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
animationType: 'slide-in-right',
})
}
if (!isLoadMore.value) {
loading.value = 'loading'
}
loadMoreText.value = t('common.loading')
function handleToSearch() {
uni.navigateTo({ url: '/pages-blog/search/search' })
}
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
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 handleOnLogoToPage() {
uni.switchTab({ url: '/pages/tabbar/about/about' })
}
/* ---------------- 跳转 ---------------- */
function handleToArticleDetail(article: IPost) {
if (calcAuditModeEnabled.value)
return
uni.navigateTo({
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
animationType: 'slide-in-right',
})
}
function handleToTopPage(duration = 500) {
uni.pageScrollTo({
scrollTop: 0,
duration,
fail: (err) => {
console.error('回顶失败', err)
},
})
}
function handleToCategoryPage() {
uni.switchTab({ url: '/pages/tabbar/category/category' })
}
function handleToCategoryBy(category: ICategory) {
if (calcAuditModeEnabled.value)
return
uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
function handleToSearch() {
uni.navigateTo({ url: '/pages-blog/search/search' })
}
/* ---------------- 生命周期 ---------------- */
onLoad(() => {
uni.setNavigationBarTitle({ title: t('page.home.title') })
})
function handleOnLogoToPage() {
uni.switchTab({ url: '/pages/tabbar/about/about' })
}
watch(haloConfigs, () => {
// 配置就绪后重新拉取(导航显隐依赖配置)
}, { deep: true })
function handleClickNav(item: { path: string }) {
uni.navigateTo({ url: item.path })
}
onPullDownRefresh(() => {
isLoadMore.value = false
queryParams.value.page = 1
handleQuery()
})
function handleToTopPage(duration = 500) {
uni.pageScrollTo({
scrollTop: 0,
duration,
fail: (err) => {
console.error('回顶失败', err)
},
})
}
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') })
}
})
function handleOnBannerClick(item: IBannerItem) {
// 审核模式下照常展示 Banner,点击分发不拦截(详情页自行处理审核限制)
if (item.type === 'custom') {
// 自定义条目:跳转 Banner 详情页,页面内调公开详情接口展示 content/外链
if (item.name) {
uni.navigateTo({
url: `/pages-blog/banner-detail/banner-detail?name=${item.name}`,
animationType: 'slide-in-right',
})
}
return
}
// 文章来源条目:用 postId 跳文章详情
const postId = item.postId || String(item.id || '')
if (!postId)
return
handleToArticleDetail({ metadata: { name: postId } } as IPost)
}
/* ---------------- 生命周期 ---------------- */
onLoad(() => {
uni.setNavigationBarTitle({ title: t('page.home.title') })
})
watch(haloConfigs, () => {
// 配置就绪后重新拉取(导航显隐依赖配置)
}, { deep: true })
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') })
}
})
// 首次加载
handleQuery()
// 首次加载
handleQuery()
</script>
<template>
<view class="min-h-screen w-screen flex flex-col">
<!-- 顶部栏 -->
<view class="header flex items-center gap-4 px-3 py-1.5">
<image class="logo h-[60rpx] w-[60rpx] rounded-3xl" :src="appInfo.logo" mode="scaleToFill" @click="handleOnLogoToPage" />
<view class="search-input h-[64rpx] flex flex-1 items-center rounded-3xl bg-[#f5f5f5] px-3" @click="handleToSearch">
<view class="search-icon flex items-center">
<wd-icon name="search" size="16px" color="#999" />
</view>
<text class="search-text text-grey ml-3 text-[26rpx] text-[#999]">搜索内容...</text>
</view>
<!-- #ifdef APP-PLUS || H5 -->
<view class="app-name max-w-[140rpx] overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#666]">
{{ appInfo.name }}
</view>
<!-- #endif -->
</view>
<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 v-if="loading !== 'success' && articleList.length === 0" class="loading-wrap px-3">
<wd-skeleton :row="3" :animated="true" />
</view>
<block v-else>
<!-- 轮播-->
<uh-home-banner />
<block v-else>
<!-- 轮播 Banner(数据由 uh-swiper 组件内部请求公开 banners 接口) -->
<view v-if="bannerConfig?.enabled" class="mb-4 bg-white">
<view class="banner mx-3 mt-3 overflow-hidden rounded-xl">
<uh-swiper
:height="bannerConfig.height"
:dot-position="bannerConfig.dotPosition"
:autoplay="true"
:use-dot="bannerConfig.showIndicator"
:use-title="bannerConfig.showTitle"
@on-click="handleOnBannerClick"
/>
</view>
</view>
<!-- 快捷导航 -->
<uh-home-quick-nav />
<!-- 快捷导航 -->
<view v-if="navList.filter(x => x.show).length" class="nav-box overflow-hidden rounded-xl bg-white p-3 px-4">
<view class="page-item-title font-bold">
快捷导航
</view>
<view class="nav-list grid grid-cols-5 mt-6 gap-6">
<template v-for="item in navList.filter(x => x.show)" :key="item.key">
<view class="nav-item flex flex-col items-center gap-3" @click="handleClickNav(item)">
<view class="nav-item-icon h-[88rpx] w-[88rpx] flex items-center justify-center rounded-3xl" :style="{ backgroundColor: item.bgColor }">
<wd-icon :name="item.icon" size="24px" color="#fff" />
</view>
<view class="nav-item-text text-[24rpx] text-[#303133]">
{{ item.title }}
</view>
</view>
</template>
</view>
</view>
<!-- 精选分类 -->
<uh-home-category />
<!-- 精选分类 -->
<block v-if="calcIsShowCategory">
<view class="mb-6 mt-6 flex items-center justify-between px-3">
<view class="page-item-title font-bold">
精选分类
</view>
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToCategoryPage">
<wd-icon name="arrow-right" size="12px" color="#909399" />
</view>
</view>
<scroll-view class="category mx-6 h-[200rpx] whitespace-nowrap" :scroll-x="true">
<view v-if="categoryList.length === 0" class="cate-empty text-grey h-[180rpx] w-full flex items-center justify-center">
还没有任何分类~
</view>
<view
v-for="category in categoryList"
v-else
:key="category.metadata.name"
class="category-item mr-4 inline-block"
@click="handleToCategoryBy(category)"
>
<uh-category-mini-card :category="category" />
</view>
</scroll-view>
</block>
<!-- 最新文章 -->
<view class="mb-6 mt-6 flex items-center justify-between px-3">
<view class="page-item-title font-bold">
最新列表
</view>
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToSearch">
<wd-icon name="arrow-right" size="12px" color="#909399" />
</view>
</view>
<view v-if="articleList.length === 0" class="article-empty py-10">
<wd-empty description="博主还没有发表任何内容~" />
</view>
<block v-else>
<view :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-[24rpx] text-[#999]">
{{ 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>
</template>
<!-- 最新文章 -->
<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>
<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>
</template>