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.  移除冗余的qs导入与注释代码,简化请求参数处理
3.  修复瞬间详情页悬浮操作的渲染问题与收藏按钮样式
4.  优化文章卡片的头像处理与分类跳转逻辑
5.  重构数据加载组件,新增尺寸配置与样式优化
6.  为分类文章页添加排序切换功能,支持默认/置顶/最新/最旧排序
7.  优化首页分类模块的加载状态与数据获取逻辑,添加延迟模拟
8.  统一API文件的导入格式与代码风格
This commit is contained in:
小莫唐尼
2026-09-09 20:41:00 +08:00
parent 4c623be4c0
commit eb44980484
8 changed files with 307 additions and 252 deletions
+74 -80
View File
@@ -1,11 +1,11 @@
/**
* Halo 官方 API 接口定义
*/
import { http } from '@/http/alova'
import { RequestFrom } from '@/http/tools/enum'
import type { IResponse } from '@/http/types'
import { getCache } from '@/utils/storage'
import { getNologinEmail, getOpenid } from '@/utils/auth'
import { http } from '@/http/alova';
import { RequestFrom } from '@/http/tools/enum';
import type { IResponse } from '@/http/types';
import { getCache } from '@/utils/storage';
import { getNologinEmail, getOpenid } from '@/utils/auth';
import type {
IBlogStats,
ICategory,
@@ -31,11 +31,11 @@ import type {
ISearchRes,
ITagListRes,
ITrackerCounterReq,
IUpvoteReq,
} from './types/halo'
IUpvoteReq
} from './types/halo';
/** 评论验证码 cookie key */
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha';
/* ==================== 文章 ==================== */
@@ -45,8 +45,8 @@ const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
export function getPostList(params: IPostListReq) {
return http.Get<IResponse<IPostListRes>>('/apis/api.content.halo.run/v1alpha1/posts', {
query: params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -56,10 +56,10 @@ export function getPostByName(name: string) {
return http.Get<IResponse<IPost>>(`/apis/api.content.halo.run/v1alpha1/posts/${name}`, {
headers: {
'Wechat-Session-Id': getOpenid(),
'nologin-email': getNologinEmail(),
'nologin-email': getNologinEmail()
},
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -67,22 +67,20 @@ export function getPostByName(name: string) {
*/
export function getPostListByKeyword(params: ISearchReq) {
return http.Post<IResponse<ISearchRes>>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 分类 / 标签 ==================== */
/**
* 分类列表
* 注:旧代码用 qs.stringify 特殊序列化(arrayFormat:'repeat'、allowDots),
* alova 的 params 对数组默认即 repeat 形式(a=1&a=2),已等价;如遇嵌套对象场景再单独处理
*/
export function getCategoryList(params: ICategoryListReq) {
return http.Get<IResponse<ICategoryListRes>>('/apis/api.content.halo.run/v1alpha1/categories', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -91,8 +89,8 @@ export function getCategoryList(params: ICategoryListReq) {
export function getCategoryPostList(name: string, params: IPostListReq) {
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/categories/${name}/posts`, {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -101,8 +99,8 @@ export function getCategoryPostList(name: string, params: IPostListReq) {
export function getTagList(params: ICategoryListReq) {
return http.Get<IResponse<ITagListRes>>('/apis/api.content.halo.run/v1alpha1/tags', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -111,8 +109,8 @@ export function getTagList(params: ICategoryListReq) {
export function getPostByTagName(tagName: string, params: IPostListReq) {
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/tags/${tagName}/posts`, {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 评论(含验证码 cookie 链路) ==================== */
@@ -124,8 +122,8 @@ export function getPostCommentList(params: ICommentListReq) {
return http.Get<IResponse<ICommentListRes>>('/apis/api.halo.run/v1alpha1/comments', {
params,
cacheFor: 0,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -135,63 +133,59 @@ export function getPostCommentReplyList(commentName: string, params: ICommentLis
return http.Get<IResponse<ICommentListRes>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
params,
cacheFor: 0,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/** 新增评论(带验证码,captchaCode 转入请求头) */
export interface IAddCommentReq {
allowNotification: boolean
raw: string
content?: string
owner?: Record<string, unknown>
allowNotification: boolean;
raw: string;
content?: string;
owner?: Record<string, unknown>;
/** 评论目标引用(subjectRef: group/kind/name/version) */
subjectRef?: {
group: string
kind: string
name: string
version?: string
}
group: string;
kind: string;
name: string;
version?: string;
};
/** 验证码,提交时转入 X-Captcha-Code 头 */
captchaCode?: string
captchaCode?: string;
}
/**
* 新增评论(captchaCode 拆出转 X-Captcha-Code 头 + Cookie)
*/
export function addPostComment(data: IAddCommentReq) {
const { captchaCode, ...rest } = data
const { captchaCode, ...rest } = data;
const headers: Record<string, string> = {
Accept: 'application/json',
}
if (captchaCode)
headers['X-Captcha-Code'] = captchaCode
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES)
if (cookie)
headers.Cookie = cookie
Accept: 'application/json'
};
if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES);
if (cookie) headers.Cookie = cookie;
return http.Post<IResponse<IComment>>('/apis/api.halo.run/v1alpha1/comments', rest, {
headers,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
* 新增评论回复(同上,验证码逻辑)
*/
export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
const { captchaCode, ...rest } = data
const { captchaCode, ...rest } = data;
const headers: Record<string, string> = {
Accept: 'application/json',
}
if (captchaCode)
headers['X-Captcha-Code'] = captchaCode
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES)
if (cookie)
headers.Cookie = cookie
Accept: 'application/json'
};
if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES);
if (cookie) headers.Cookie = cookie;
return http.Post<IResponse<IComment>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
headers,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 瞬间 ==================== */
@@ -202,8 +196,8 @@ export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
export function getMomentList(params: IMomentListReq) {
return http.Get<IResponse<IMomentListRes>>('/apis/api.moment.halo.run/v1alpha1/moments', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -211,8 +205,8 @@ export function getMomentList(params: IMomentListReq) {
*/
export function getMomentByName(name: string) {
return http.Get<IResponse<IMoment>>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 图库 ==================== */
@@ -223,8 +217,8 @@ export function getMomentByName(name: string) {
export function getPhotoGroupList(params: IPhotoGroupListReq) {
return http.Get<IResponse<IPhotoGroupListRes>>('/apis/api.photo.halo.run/v1alpha1/photogroups', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -233,8 +227,8 @@ export function getPhotoGroupList(params: IPhotoGroupListReq) {
export function getPhotoListByGroupName(params: IPhotoListReq) {
return http.Get<IResponse<IPhotoListRes>>('/apis/api.photo.halo.run/v1alpha1/photos', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 友链 ==================== */
@@ -245,8 +239,8 @@ export function getPhotoListByGroupName(params: IPhotoListReq) {
export function getFriendLinkGroupList(params: ICategoryListReq) {
return http.Get<IResponse<Array<ILinkGroup>>>('/apis/api.link.halo.run/v1alpha1/linkgroups', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -255,8 +249,8 @@ export function getFriendLinkGroupList(params: ICategoryListReq) {
export function getFriendLinkList(params: ICategoryListReq) {
return http.Get<IResponse<ILinkListRes>>('/apis/api.link.halo.run/v1alpha1/links', {
params,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/* ==================== 统计 / 埋点 / 插件 ==================== */
@@ -266,8 +260,8 @@ export function getFriendLinkList(params: ICategoryListReq) {
*/
export function getBlogStatistics() {
return http.Get<IResponse<IBlogStats>>('/apis/api.halo.run/v1alpha1/stats/-', {
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -275,8 +269,8 @@ export function getBlogStatistics() {
*/
export function submitUpvote(data: IUpvoteReq) {
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -284,8 +278,8 @@ export function submitUpvote(data: IUpvoteReq) {
*/
export function postTrackersCounter(data: ITrackerCounterReq) {
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/**
@@ -294,9 +288,9 @@ export function postTrackersCounter(data: ITrackerCounterReq) {
export function checkPluginAvailable(name: string) {
return http.Get<IResponse<boolean>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
cacheFor: 0,
meta: { requestFrom: RequestFrom.Halo },
})
meta: { requestFrom: RequestFrom.Halo }
});
}
/** 分类资源(供加密分类判断等场景) */
export type { ICategory, ILink, IMoment, IPost }
export type { ICategory, ILink, IMoment, IPost };
+6
View File
@@ -63,6 +63,12 @@ export interface IPageConfig {
/** 是否显示快捷导航(首页) */
useQuickNavigation?: boolean
bannerConfig?: IBannerConfig
/** 首页精选分类引用(插件端「通用配置 → 页面设置 → 首页」配置,固定最多 3 个,
* 数组顺序 = 展示顺序;未配置/为空时客户端回退默认取数) */
categories?: Array<{
name: string
displayName?: string
}>
}
categoryConfig?: { type?: string }
momentConfig?: { useTagRandomColor?: boolean }
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { checkThumbnailUrl } from '@/utils/url'
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import { useSettingStore } from '@/store/setting'
import { formatTime } from '@/utils/formatTime'
import type { ICategory, IPost } from '@/api/types/halo'
@@ -140,15 +140,6 @@
animationType: 'slide-in-right',
})
}
function handleToCategory(category : ICategory) {
if (props.auditMode) {
return
}
uni.navigateTo({
url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
</script>
<template>
@@ -171,7 +162,7 @@
<view v-if="!isGrid" class="my-1 box-border flex flex-wrap gap-2" :class="cardLayout.tagCategory">
<template v-if="article.categories && article.categories.length !== 0">
<text v-for="cate in article.categories" :key="cate.metadata.name"
class="box-border uh-global-card-glass border uh-shadow-xs rounded-xl bg-secondary px-2 py-0.5 text-xs" @click.stop="handleToCategory(cate)">
class="box-border uh-global-card-glass border uh-shadow-xs rounded-xl bg-secondary px-2 py-0.5 text-xs">
{{ cate.spec.displayName }}
</text>
</template>
@@ -184,7 +175,7 @@
</view>
<view class="flex items-center text-xs text-gray-500" :class="cardLayout.footer">
<view class="flex items-center" :class="cardLayout.authorGroup">
<image :src="article.owner.avatar" class="uh-global-card-glass h-5 w-5 rounded-full"
<image :src="checkAvatarUrl(article.owner?.avatar || '')" class="uh-global-card-glass h-5 w-5 rounded-full"
:class="cardLayout.avatar" mode="aspectFill" />
<template v-if="isSocialCard">
<view :class="cardLayout.infoCol">
@@ -1,18 +1,15 @@
<script setup lang="ts">
import { computed } from 'vue'
import { checkUrl } from '@/utils/url'
import { useAppConfigStore } from '@/store/appConfig'
import type { DataLoadingStatus } from '@/hooks/useDataLoading'
interface IProps {
/** 加载状态(取值同 useDataLoading 返回的 status) */
loadingStatus ?: DataLoadingStatus
/** 占位区最小高度 */
size ?: 'mini' | 'small' | 'large'
minHeight ?: string
loadingText ?: string
errorText ?: string
emptyText ?: string
/** 各态副文案(留空则不显示副行) */
loadingSubText ?: string
errorSubText ?: string
emptySubText ?: string
@@ -21,6 +18,7 @@
const props = withDefaults(defineProps<IProps>(), {
loadingStatus: 'loading',
size: 'large',
minHeight: '80vh',
loadingText: '稍等,正在加载中哦',
errorText: '哎呀,加载失败了呢~',
@@ -33,14 +31,26 @@
const emit = defineEmits<{ (e : 'refresh') : void }>()
const appConfigStore = useAppConfigStore()
const appInfo = computed(() => {
const _appInfo = (appConfigStore.configs?.appConfig?.appInfo as any)
return {
name: _appInfo.name ?? 'UniHalo',
logo: checkUrl(_appInfo?.logo ?? 'https://uni-halo.925i.cn/logo.png')
const SizeClasses = {
mini: {
icon: '60rpx',
stage: 'h-16 w-16',
glow: 'h-12 w-12'
},
small: {
icon: '100rpx',
stage: 'h-22 w-22',
glow: 'h-18 w-18'
},
large: {
icon: '120rpx',
stage: 'h-32 w-32',
glow: 'h-28 w-28'
},
}
const sizeClasses = computed(() => {
return SizeClasses[props.size] ?? SizeClasses.large
})
const isLoading = computed(() => props.loadingStatus === 'loading')
@@ -76,20 +86,17 @@
</script>
<template>
<view class="relative w-full flex flex-col items-center justify-center gap-y-4 text-sm"
<view class="relative w-full flex flex-col items-center justify-center gap-y-3 text-sm"
:style="{ minHeight: props.minHeight }">
<!-- logo背景 -->
<image v-if="false" :src="appInfo.logo" class="absolute left-1/2 top-1/2 -translate-1/2 z-0 opacity-10 w-46 h-46"></image>
<view class="scene relative z-1 h-[250rpx] w-[250rpx] flex items-center justify-center"
:class="statusScene.stageClass">
<view class="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" />
<view class="scene relative z-1 flex items-center justify-center"
:class="[statusScene.stageClass,sizeClasses.stage]">
<view class="glow absolute inset-0 m-auto rounded-full" :class="sizeClasses.glow" />
<view class="deco-dot dot-a absolute rounded-full" />
<view class="deco-dot dot-b absolute rounded-full" />
<view class="bubble relative h-[150rpx] w-[150rpx] flex items-center justify-center rounded-full">
<view class="bubble">
<text class="bubble-icon">
<wd-icon class-prefix="uhemoji-icon" :name="statusScene.icon" size="120rpx" />
<wd-icon class-prefix="uhemoji-icon" :name="statusScene.icon" :size="sizeClasses.icon" />
</text>
</view>
</view>
@@ -103,11 +110,12 @@
:style="{ animationDelay: `${(n - 1) * 0.15}s` }" />
</view>
</view>
<text v-if="statusScene.subText" class="mt-3 text-xs text-gray-500">
<text v-if="statusScene.subText" class="mt-2 text-xs text-gray-500">
{{ statusScene.subText }}
</text>
<view v-if="props.useRefreshButton" class="mt-5">
<uh-button custom-class="!rounded-lg uh-global-card-glass uh-shadow-xs border rounded-lg" @click="emit('refresh')">
<view v-if="props.useRefreshButton" class="mt-4">
<uh-button custom-class="!rounded-lg uh-global-card-glass uh-shadow-xs border"
@click="emit('refresh')">
刷新试试
</uh-button>
</view>
@@ -4,6 +4,7 @@
import { checkThumbnailUrl } from '@/utils/url'
import { useAppConfigStore } from '@/store/appConfig'
import type { ICategory } from '@/api/types/halo'
import { sleep } from '@/utils/common'
const appConfigStore = useAppConfigStore()
@@ -14,23 +15,34 @@
const loading = ref<'loading' | 'success' | 'error'>('loading')
const categoryList = ref<ICategory[]>([])
const calcIsShowCategory = computed(() => {
if (calcAuditModeEnabled.value) {
return false;
}
const isEnableCategoryModule = computed(() => {
return !!haloConfigs.value.pageConfig?.homeConfig?.useCategory
})
/** 精选分类 */
async function handleGetCategoryList() {
if (calcAuditModeEnabled.value || !calcIsShowCategory.value) {
loading.value = 'success'
return
}
try {
loading.value = 'loading'
const configured = haloConfigs.value.pageConfig?.homeConfig?.categories
let categoryListRaw : ICategory[] = []
if (configured && configured.length) {
// 配置模式:按 name 用 in 查询(Halo fieldSelector 数组为 AND 语义,多 name 需 in 语法)
const names = configured.map(c => c.name)
const res = await getCategoryList({
fieldSelector: [`metadata.name in (${names.join(',')})`],
size: 3,
})
// 按配置顺序排列;配置的 name 查不到(分类已删除)则跳过
const byName = new Map(res.data.items.map(item => [item.metadata.name, item]))
categoryListRaw = names
.map(name => byName.get(name))
.filter((item) : item is ICategory => !!item)
}
else {
// 默认模式(老部署无配置):保持原有取数与排序
const res = await getCategoryList({ fieldSelector: ['spec.hideFromList=false'], size: 3 })
categoryList.value = res.data.items
categoryListRaw = res.data.items
}
categoryList.value = categoryListRaw
.map(item => {
item.spec.cover = checkThumbnailUrl(item.spec.cover)
return {
@@ -38,7 +50,14 @@
postCount: item.postCount ?? 0
}
})
.sort((a, b) => (b.postCount || 0) - (a.postCount || 0))
.sort((a, b) => {
if (configured && configured.length) {
return 0
}
return (b.postCount || 0) - (a.postCount || 0)
})
await sleep(600)
loading.value = 'success'
}
catch (err) {
@@ -54,8 +73,6 @@
function handleToCategoryBy(category : ICategory) {
if (calcAuditModeEnabled.value)
return
uni.navigateTo({
url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
@@ -67,28 +84,30 @@
</script>
<template>
<view v-if="calcIsShowCategory" class="mb-6">
<view v-if="isEnableCategoryModule" class="mb-6">
<uh-section-title class="mb-4 px-3 box-border">
精选分类
<template #right>
<view class="flex items-center justify-center rounded-md bg-white p-1.5 text-gray-400" @click="handleToCategoryPage">
<wd-icon name="arrow-right" size="12px" />
<view class="box-border flex items-center justify-center rounded-md bg-white p-1.5 text-gray-400"
@click="handleToCategoryPage">
<wd-icon name="arrow-right" size="24rpx" />
</view>
</template>
</uh-section-title>
<view class="w-full grid grid-cols-2 grid-rows-auto h-42 box-border px-3 gap-2">
<view v-if="categoryList.length === 0"
class="cate-empty text-grey w-full flex items-center justify-center">
还没有任何分类~
<view v-if="loading!=='success'" class="box-border px-3">
<view class="uh-global-card-glass shadow-none rounded-xl">
<uh-data-loading :loading-status="loading" min-height="28vh" size="small" :use-refresh-button="true"
@refresh="handleGetCategoryList()" />
</view>
<block v-else>
</view>
<view v-else class="w-full grid grid-cols-2 grid-rows-auto h-42 box-border px-3 gap-2">
<view v-for="(category,index) in categoryList" :key="category.metadata.name"
class="uh-global-card-glass relative w-full h-full overflow-hidden rounded-xl text-center text-white"
:class="{'grid-row-span-2':index===0 }" @click="handleToCategoryBy(category)">
<image :src="category.spec.cover" class="w-full h-full" mode="aspectFill" lazy-load />
<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 h-[140rpx] w-full bg-gradient-to-b from-black/0 to-black/30" />
<view class="absolute left-2 bottom-2 flex z-2 flex-col text-left">
<text class="text-sm font-bold">
{{ category.spec.displayName }}
@@ -96,7 +115,6 @@
<text class="mt-1 text-xs text-gray-200"> {{ category.postCount ?? 0 }} </text>
</view>
</view>
</block>
</view>
</view>
</template>
+11
View File
@@ -2,6 +2,7 @@ import type { CustomRequestOptions } from '@/http/types';
import { useTokenStore } from '@/store';
import { getEnvBaseUrl } from '@/utils';
import { stringifyQuery } from './tools/queryString';
// import qs from 'qs'
// 请求基准地址
const baseUrl = getEnvBaseUrl();
@@ -16,7 +17,17 @@ const httpInterceptor = {
// 非 alova 请求,正常执行
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
// const queryStr = qs.stringify(options.query, {
// allowDots: true,
// encodeValuesOnly: true,
// skipNulls: true,
// encode: true,
// arrayFormat: 'repeat'
// })
const queryStr = stringifyQuery(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`;
@@ -3,7 +3,7 @@ import { ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getCategoryPostList } from '@/api/halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IPost } from '@/api/types/halo'
import type { IPost, IPostListReq } from '@/api/types/halo'
definePage({
style: {
@@ -23,6 +23,24 @@ const dataList = ref<IPost[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref('')
/* ---------------- 排序切换(sort 参数由接口透传,见 IPostListReq.sort) ---------------- */
const sortOptions: { key: string; label: string; sort?: string[] }[] = [
{ key: 'default', label: '默认' },
{ key: 'pinned', label: '按置顶', sort: ['spec.pinned,desc'] },
{ key: 'latest', label: '按最新', sort: ['metadata.creationTimestamp,desc'] },
{ key: 'oldest', label: '按最旧', sort: ['metadata.creationTimestamp,asc'] },
]
const activeSort = ref('default')
function handleSortChange(key: string) {
if (activeSort.value === key)
return
activeSort.value = key
isLoadMore.value = false
queryParams.value.page = 0
handleGetData()
}
async function handleGetData() {
if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
@@ -30,7 +48,15 @@ async function handleGetData() {
loadMoreText.value = '加载中...'
try {
const res = await getCategoryPostList(name.value, { ...queryParams.value })
const reqParams: Record<string, unknown> = { ...queryParams.value }
const currentSort = sortOptions.find(opt => opt.key === activeSort.value)?.sort
if (currentSort) {
reqParams.sort = currentSort
}
else {
delete reqParams.sort
}
const res = await getCategoryPostList(name.value, reqParams as IPostListReq)
navbarTitle.value = `${pageTitle.value} (共${res.data.total}篇)`
hasNext.value = res.data.hasNext
dataList.value = isLoadMore.value
@@ -62,16 +88,6 @@ function handleToArticleDetail(article: IPost) {
})
}
function handleToTopPage(duration = 500) {
uni.pageScrollTo({
scrollTop: 0,
duration,
fail: (err) => {
console.error('回顶失败', err)
},
})
}
onLoad((options) => {
name.value = options?.name || ''
pageTitle.value = options?.title || '分类详情'
@@ -112,6 +128,19 @@ onShareTimeline(() => ({
<!-- 自定义导航 -->
<uh-navbar :default-title="navbarTitle" title-color="text-gray-900" />
<!-- 排序切换:默认 / 按置顶 / 按最新 / 按最旧 -->
<view class="box-border flex items-center gap-2 px-3 py-2">
<view
v-for="opt in sortOptions"
:key="opt.key"
class="rounded-full px-3 py-1 text-xs"
:class="activeSort === opt.key ? 'bg-secondary font-bold' : 'uh-global-card-glass shadow-none border text-gray-500'"
@click="handleSortChange(opt.key)"
>
{{ opt.label }}
</view>
</view>
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
@@ -122,6 +151,7 @@ onShareTimeline(() => ({
</view>
<block v-else>
<view class="box-border flex flex-col gap-y-3 p-3">
<uh-article-card
v-for="(article, index) in dataList"
:key="index"
@@ -131,9 +161,6 @@ onShareTimeline(() => ({
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
{{ loadMoreText }}
</view>
<view class="to-top-btn fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
</view>
</block>
</view>
@@ -362,7 +362,7 @@
kind="Moment" :disallow-comment="!moment.spec.allowComment" @on-comment="handleOnComment" />
</view>
<!-- 悬浮操作(与文章详情一致:点赞/评论/收藏) -->
<view class="fixed bottom-8 left-1/2 z-10 flex items-center justify-center pb-safe -translate-x-1/2">
<view v-if="moment" class="fixed bottom-8 left-1/2 z-10 flex items-center justify-center pb-safe -translate-x-1/2">
<view
class="uh-global-card-glass box-border flex items-center justify-center gap-2 border rounded-full p-1 text-primary">
<!-- 点赞 -->
@@ -384,8 +384,8 @@
class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none"
@click="handleToggleMomentFavorite">
<wd-icon class-prefix="uhemoji-icon" name="-smile-" size="36rpx" />
<text class="shrink-0 text-sm text-gray-900 font-semibold"
:style="momentFavorited ? { color: '#ffb300' } : ''">{{ momentFavorited ? '已收藏' : '收藏' }}</text>
<text class="shrink-0 text-sm font-semibold"
:class="momentFavorited ? 'text-primary' : 'text-gray-900'">{{ momentFavorited ? '已收藏' : '收藏' }}</text>
</view>
</view>
</view>