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
+158 -359
View File
@@ -1,384 +1,183 @@
<script lang="ts" setup>
/**
* 分类页(源自旧项目 pages/tabbar/category/category.vue,新建复刻)
* 两种视图:list(分类卡片网格)/ list-post(左侧分类导航 + 右侧文章列表)
*/
import { computed, ref, watch } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList, getCategoryPostList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkThumbnailUrl } from '@/utils/url'
import { t } from '@/locale'
import type { ICategory, IPost } from '@/api/types/halo'
import { computed, ref, watch } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList, getCategoryPostList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkThumbnailUrl } from '@/utils/url'
import { t } from '@/locale'
import { useDataLoadingStatus, DataLoadingStatusEnum } from '@/hooks/useDataLoadingStatus'
import type { ICategory, IPost } from '@/api/types/halo'
definePage({
style: {
navigationBarTitleText: '分类',
enablePullDownRefresh: true,
},
})
definePage({
style: {
navigationBarTitleText: '分类',
enablePullDownRefresh: true,
backgroundColor: '#f6f3ee',
},
})
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 categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
const queryParams = ref({
size: 20,
page: 1,
fieldSelector: ['spec.hideFromList=false'],
})
const hasNext = ref(false)
const dataList = ref<ICategory[]>([])
const categoryList = ref<ICategory[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading'))
const currentCategoryConfig = ref<{ type?: string }>({ type: 'list' })
const currentCategoryName = ref('')
const postQueryParams = ref({ size: 10, page: 0 })
const postList = ref<IPost[]>([])
/* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({
size: 20,
page: 1,
fieldSelector: ['spec.hideFromList=false'],
})
const hasNext = ref(false)
const dataList = ref<ICategory[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading'))
/* ---------------- 计算属性 ---------------- */
const calcShowType = computed(() => currentCategoryConfig.value.type)
function handleResetInit() {
dataList.value = []
queryParams.value.page = 1
hasNext.value = false
isLoadMore.value = false
loadMoreText.value = t('common.loading')
}
/* ---------------- 视图切换 ---------------- */
function handleChangeShowType() {
currentCategoryConfig.value.type = calcShowType.value === 'list-post' ? 'list' : 'list-post'
handleInitPage()
}
function handleInitPage() {
handleResetInit()
handleGetData()
}
function handleResetInit() {
postList.value = []
dataList.value = []
categoryList.value = []
queryParams.value.page = 1
postQueryParams.value.page = 0
hasNext.value = false
isLoadMore.value = false
loadMoreText.value = t('common.loading')
currentCategoryName.value = ''
}
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
// 审核模式
if (calcAuditModeEnabled.value) {
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
try {
const res = await getCategoryList({ page: 1, size: 99999 })
const filtered = res.data.items
.filter(item => auditCategoryNames.includes(item.metadata.name))
.map(item => ({
...item,
postCount: item.postCount ?? 0,
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
}))
const orderMap = new Map(auditCategoryNames.map((name, index) => [name, index]))
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
dataList.value = filtered
updateLoadingStatus(DataLoadingStatusEnum.Success)
loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh()
}
catch (err) {
console.error(err)
updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = t('common.loadFailed')
}
return
}
function handleInitPage() {
handleResetInit()
if (calcShowType.value === 'list-post') {
queryParams.value.size = 99999
}
handleGetData()
}
if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
}
loadMoreText.value = t('common.loading')
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
if (calcAuditModeEnabled.value) {
// 审核模式:真实分类按 audit-data categories 过滤(数组顺序即展示顺序)
currentCategoryConfig.value.type = 'list'
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
try {
const res = await getCategoryList({ page: 1, size: 99999 })
const filtered = res.data.items
.filter(item => auditCategoryNames.includes(item.metadata.name))
.map(item => ({
...item,
postCount: item.postCount ?? 0,
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
}))
const orderMap = new Map(auditCategoryNames.map((name, index) => [name, index]))
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
dataList.value = filtered
loading.value = 'success'
loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh()
}
catch (err) {
console.error(err)
loading.value = 'error'
loadMoreText.value = t('common.loadFailed')
}
return
}
try {
const res = await getCategoryList({ ...queryParams.value })
uni.showLoading({ mask: true, title: t('common.loading') })
if (!isLoadMore.value) {
loading.value = 'loading'
}
loadMoreText.value = t('common.loading')
updateLoadingStatus(DataLoadingStatusEnum.Success)
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
hasNext.value = res.data.hasNext
try {
const res = await getCategoryList({ ...queryParams.value })
const tempItems = res.data.items.map(item => ({
...item,
postCount: item.postCount ?? 0,
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
}))
if (calcShowType.value === 'list') {
loading.value = 'success'
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
hasNext.value = res.data.hasNext
dataList.value = isLoadMore.value
? dataList.value.concat(tempItems)
: tempItems
const tempItems = res.data.items.map(item => ({
...item,
postCount: item.postCount ?? 0,
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
}))
}
catch (err) {
console.error(err)
updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = t('common.loadFailed')
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 500)
}
}
dataList.value = isLoadMore.value
? dataList.value.concat(tempItems)
: tempItems
}
else {
dataList.value = res.data.items
categoryList.value = res.data.items.map(item => ({
...item,
postCount: item.postCount ?? 0,
}))
loading.value = 'success'
if (dataList.value.length !== 0) {
currentCategoryName.value = dataList.value[0].metadata.name
handleGetPostByCategory()
}
}
}
catch (err) {
console.error(err)
loading.value = 'error'
loadMoreText.value = t('common.loadFailed')
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 500)
}
}
function handleToCategory(category : ICategory) {
if (calcAuditModeEnabled.value) {
return
}
uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
/** 获取当前分类下的文章 */
async function handleGetPostByCategory(isPulldownRefresh = true) {
if (!isPulldownRefresh) {
if (hasNext.value) {
postQueryParams.value.page += 1
}
else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
}
}
else {
postQueryParams.value.page = 0
}
try {
const res = await getCategoryPostList(currentCategoryName.value, postQueryParams.value)
hasNext.value = res.data.hasNext
postList.value = isPulldownRefresh
? res.data.items
: postList.value.concat(res.data.items)
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
}
catch (err) {
loadMoreText.value = t('common.loadFailedShort')
console.error(err)
}
}
/* ---------------- 生命周期 ---------------- */
/* ---------------- 交互 ---------------- */
function handleOnCategoryChange(e: { detail: { current: number } }) {
const index = e.detail.current
if (!dataList.value[index])
return
currentCategoryName.value = dataList.value[index].metadata.name
postList.value = []
handleGetPostByCategory()
}
onMounted(() => {
handleInitPage()
})
function handleToCategory(category: ICategory) {
if (calcAuditModeEnabled.value)
return
uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
onPullDownRefresh(() => {
handleResetInit()
handleGetData()
})
function handleToArticleDetail(post: IPost) {
uni.navigateTo({
url: `/pages-blog/article-detail/article-detail?name=${post.metadata.name}`,
animationType: 'slide-in-right',
})
}
function handleScrollTop() {
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
}
/* ---------------- 生命周期 ---------------- */
watch(categoryConfig, (newVal) => {
if (!newVal)
return
currentCategoryConfig.value = newVal
uni.setNavigationBarTitle({ title: t('page.category.title') })
handleInitPage()
}, { deep: true, immediate: true })
onPullDownRefresh(() => {
isLoadMore.value = false
queryParams.value.page = 1
handleGetData()
})
onReachBottom(() => {
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
}
if (hasNext.value) {
if (calcShowType.value === 'list') {
queryParams.value.page += 1
isLoadMore.value = true
handleGetData()
}
else {
postQueryParams.value.page += 1
handleGetPostByCategory(false)
}
}
else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
}
})
onReachBottom(() => {
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
}
if (hasNext.value) {
queryParams.value.page += 1
isLoadMore.value = true
handleGetData()
}
else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
}
})
</script>
<template>
<view class="app-page min-h-screen w-screen flex flex-col" :style="{ padding: calcShowType === 'list-post' ? '0' : '24rpx 0' }">
<!-- 骨架屏 -->
<view v-if="loading !== 'success'" class="loading-wrap px-3">
<wd-skeleton :row="3" :animated="true" />
</view>
<view class="bg-page min-h-screen w-screen flex flex-col p-3 box-border">
<!-- 骨架屏 -->
<view v-if="loadingStatus !== DataLoadingStatusEnum.Success">
<uh-data-loading :loading-status="loadingStatus" />
</view>
<!-- 内容区域 -->
<view v-else class="app-page-content flex flex-wrap gap-y-5 px-1.5" :class="[calcShowType === 'list-post' ? 'list-post' : '']">
<view v-if="dataList.length === 0" class="h-[70vh] flex items-center justify-center content-empty">
<wd-empty :description="t('common.empty')" />
</view>
<block v-else>
<!-- list 视图:分类卡片网格 -->
<block v-if="calcAuditModeEnabled || calcShowType === 'list'">
<view
v-for="(item, index) in dataList"
:key="index"
class="catgory-card box-border w-1/2 p-1"
:style="{ backgroundImage: `url(${item.spec.cover})` }"
>
<view class="catgory-card-content h-[200rpx] flex flex-col items-center justify-center overflow-hidden rounded-xl shadow-sm" @click="handleToCategory(item)">
<view class="catgory-name z-2 text-[32rpx] text-white">
{{ item.spec.displayName }}
</view>
<view v-if="!calcAuditModeEnabled" class="catgory-count z-2 mt-1 text-[24rpx] text-white">
{{ item.postCount }} 篇文章
</view>
</view>
</view>
<view class="load-text w-full py-5 text-center text-[24rpx] text-[#999]">
{{ loadMoreText }}
</view>
</block>
<!-- list-post 视图:左侧分类 + 右侧文章 -->
<view v-else class="list-post-wrapper min-h-screen w-screen flex">
<scroll-view class="left-nav w-[180rpx] shrink-0 bg-white" :scroll-y="true">
<view
v-for="(item, index) in categoryList"
:key="item.metadata.name"
class="left-nav-item border-l-4 px-4 py-8 text-center text-[26rpx] text-[#606266]"
:class="{ active: currentCategoryName === item.metadata.name }"
@click="handleOnCategoryChange({ detail: { current: index } })"
>
{{ item.spec.displayName }}
</view>
</scroll-view>
<scroll-view class="right-content box-border h-screen flex-1" :scroll-y="true">
<view v-if="postList.length === 0" class="article-empty flex items-center justify-center py-10">
<wd-empty description="该分类下暂无文章~" />
</view>
<block v-else>
<uh-article-min-card
v-for="(post, index) in postList"
:key="index"
:article="post"
@on-click="handleToArticleDetail"
/>
<view class="load-text w-full py-5 text-center text-[24rpx] text-[#999]">
{{ loadMoreText }}
</view>
</block>
</scroll-view>
</view>
</block>
</view>
<!-- 悬浮按钮 -->
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleScrollTop">
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
</view>
<view v-if="!calcAuditModeEnabled" class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleChangeShowType">
<wd-icon :name="calcShowType === 'list' ? 'list' : 'grid'" size="20px" color="#03a9f4" />
</view>
</view>
</view>
<block v-else>
<view class="grid grid-cols-2 gap-3">
<view v-for="(item, index) in dataList" :key="index"
class="relative w-full box-border rounded-xl overflow-hidden uh-global-card-glass"
@click="handleToCategory(item)">
<image v-if="item.spec.cover" class="block h-32 w-full" :src="item.spec.cover" mode="aspectFill" />
<view class="absolute bottom-0 left-0 h-[140rpx] w-full bg-gradient-to-b from-black/0 to-black/30" />
<view class="absolute bottom-0 left-0 box-border w-full p-2.5 flex flex-col gap-1">
<text class="text-sm text-white font-bold truncate">
{{ item.spec.displayName }}
</text>
<text class="text-xs text-white opacity-80">
{{ item.postCount }} 篇文章
</text>
</view>
</view>
</view>
<view class="w-full py-5 text-center text-xs text-gray-400">
{{ loadMoreText }}
</view>
</block>
</view>
</template>
<style scoped lang="scss">
.app-page {
width: 100vw;
}
.app-page-content {
&.list-post {
padding: 0;
gap: 0;
}
}
.catgory-card {
> view {
position: relative;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.catgory-card-content::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgb(0 0 0 / 15%);
backdrop-filter: blur(3rpx);
z-index: 1;
}
}
.list-post-wrapper {
.left-nav {
.left-nav-item {
border-left-color: transparent;
&.active {
color: #03a9f4;
border-left-color: #03a9f4;
background-color: #f5f7fa;
font-weight: bold;
}
}
}
}
.flot-buttons {
.fab-btn {
box-shadow: 0 4rpx 16rpx rgb(0 0 0 / 10%);
}
}
</style>