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:
+144
-150
@@ -1,41 +1,41 @@
|
||||
/**
|
||||
* 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,
|
||||
ICategoryListReq,
|
||||
ICategoryListRes,
|
||||
IComment,
|
||||
ICommentListReq,
|
||||
ICommentListRes,
|
||||
ILink,
|
||||
ILinkGroup,
|
||||
ILinkListRes,
|
||||
IMoment,
|
||||
IMomentListReq,
|
||||
IMomentListRes,
|
||||
IPhotoGroupListReq,
|
||||
IPhotoGroupListRes,
|
||||
IPhotoListReq,
|
||||
IPhotoListRes,
|
||||
IPost,
|
||||
IPostListReq,
|
||||
IPostListRes,
|
||||
ISearchReq,
|
||||
ISearchRes,
|
||||
ITagListRes,
|
||||
ITrackerCounterReq,
|
||||
IUpvoteReq,
|
||||
} from './types/halo'
|
||||
IBlogStats,
|
||||
ICategory,
|
||||
ICategoryListReq,
|
||||
ICategoryListRes,
|
||||
IComment,
|
||||
ICommentListReq,
|
||||
ICommentListRes,
|
||||
ILink,
|
||||
ILinkGroup,
|
||||
ILinkListRes,
|
||||
IMoment,
|
||||
IMomentListReq,
|
||||
IMomentListRes,
|
||||
IPhotoGroupListReq,
|
||||
IPhotoGroupListRes,
|
||||
IPhotoListReq,
|
||||
IPhotoListRes,
|
||||
IPost,
|
||||
IPostListReq,
|
||||
IPostListRes,
|
||||
ISearchReq,
|
||||
ISearchRes,
|
||||
ITagListRes,
|
||||
ITrackerCounterReq,
|
||||
IUpvoteReq
|
||||
} from './types/halo';
|
||||
|
||||
/** 评论验证码 cookie key */
|
||||
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
|
||||
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha';
|
||||
|
||||
/* ==================== 文章 ==================== */
|
||||
|
||||
@@ -43,76 +43,74 @@ 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 },
|
||||
})
|
||||
return http.Get<IResponse<IPostListRes>>('/apis/api.content.halo.run/v1alpha1/posts', {
|
||||
query: params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情(带访客标识头)
|
||||
*/
|
||||
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(),
|
||||
},
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<IPost>>(`/apis/api.content.halo.run/v1alpha1/posts/${name}`, {
|
||||
headers: {
|
||||
'Wechat-Session-Id': getOpenid(),
|
||||
'nologin-email': getNologinEmail()
|
||||
},
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键词搜索文章
|
||||
*/
|
||||
export function getPostListByKeyword(params: ISearchReq) {
|
||||
return http.Post<IResponse<ISearchRes>>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Post<IResponse<ISearchRes>>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
|
||||
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 },
|
||||
})
|
||||
return http.Get<IResponse<ICategoryListRes>>('/apis/api.content.halo.run/v1alpha1/categories', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类下文章列表
|
||||
*/
|
||||
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 },
|
||||
})
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/categories/${name}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签列表
|
||||
*/
|
||||
export function getTagList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ITagListRes>>('/apis/api.content.halo.run/v1alpha1/tags', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<ITagListRes>>('/apis/api.content.halo.run/v1alpha1/tags', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签下文章列表
|
||||
*/
|
||||
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 },
|
||||
})
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/tags/${tagName}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================== 评论(含验证码 cookie 链路) ==================== */
|
||||
@@ -121,77 +119,73 @@ export function getPostByTagName(tagName: string, params: IPostListReq) {
|
||||
* 评论列表
|
||||
*/
|
||||
export function getPostCommentList(params: ICommentListReq) {
|
||||
return http.Get<IResponse<ICommentListRes>>('/apis/api.halo.run/v1alpha1/comments', {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<ICommentListRes>>('/apis/api.halo.run/v1alpha1/comments', {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论回复列表
|
||||
*/
|
||||
export function getPostCommentReplyList(commentName: string, params: ICommentListReq) {
|
||||
return http.Get<IResponse<ICommentListRes>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<ICommentListRes>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增评论(带验证码,captchaCode 转入请求头) */
|
||||
export interface IAddCommentReq {
|
||||
allowNotification: boolean
|
||||
raw: string
|
||||
content?: string
|
||||
owner?: Record<string, unknown>
|
||||
/** 评论目标引用(subjectRef: group/kind/name/version) */
|
||||
subjectRef?: {
|
||||
group: string
|
||||
kind: string
|
||||
name: string
|
||||
version?: string
|
||||
}
|
||||
/** 验证码,提交时转入 X-Captcha-Code 头 */
|
||||
captchaCode?: string
|
||||
allowNotification: boolean;
|
||||
raw: string;
|
||||
content?: string;
|
||||
owner?: Record<string, unknown>;
|
||||
/** 评论目标引用(subjectRef: group/kind/name/version) */
|
||||
subjectRef?: {
|
||||
group: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
};
|
||||
/** 验证码,提交时转入 X-Captcha-Code 头 */
|
||||
captchaCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论(captchaCode 拆出转 X-Captcha-Code 头 + Cookie)
|
||||
*/
|
||||
export function addPostComment(data: IAddCommentReq) {
|
||||
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
|
||||
return http.Post<IResponse<IComment>>('/apis/api.halo.run/v1alpha1/comments', rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
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;
|
||||
return http.Post<IResponse<IComment>>('/apis/api.halo.run/v1alpha1/comments', rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论回复(同上,验证码逻辑)
|
||||
*/
|
||||
export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
|
||||
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
|
||||
return http.Post<IResponse<IComment>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
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;
|
||||
return http.Post<IResponse<IComment>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================== 瞬间 ==================== */
|
||||
@@ -200,19 +194,19 @@ 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 },
|
||||
})
|
||||
return http.Get<IResponse<IMomentListRes>>('/apis/api.moment.halo.run/v1alpha1/moments', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 瞬间详情
|
||||
*/
|
||||
export function getMomentByName(name: string) {
|
||||
return http.Get<IResponse<IMoment>>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<IMoment>>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================== 图库 ==================== */
|
||||
@@ -221,20 +215,20 @@ 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 },
|
||||
})
|
||||
return http.Get<IResponse<IPhotoGroupListRes>>('/apis/api.photo.halo.run/v1alpha1/photogroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 照片列表(按相册)
|
||||
*/
|
||||
export function getPhotoListByGroupName(params: IPhotoListReq) {
|
||||
return http.Get<IResponse<IPhotoListRes>>('/apis/api.photo.halo.run/v1alpha1/photos', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<IPhotoListRes>>('/apis/api.photo.halo.run/v1alpha1/photos', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================== 友链 ==================== */
|
||||
@@ -243,20 +237,20 @@ 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 },
|
||||
})
|
||||
return http.Get<IResponse<Array<ILinkGroup>>>('/apis/api.link.halo.run/v1alpha1/linkgroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 友链列表
|
||||
*/
|
||||
export function getFriendLinkList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ILinkListRes>>('/apis/api.link.halo.run/v1alpha1/links', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<ILinkListRes>>('/apis/api.link.halo.run/v1alpha1/links', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================== 统计 / 埋点 / 插件 ==================== */
|
||||
@@ -265,38 +259,38 @@ export function getFriendLinkList(params: ICategoryListReq) {
|
||||
* 博客统计信息
|
||||
*/
|
||||
export function getBlogStatistics() {
|
||||
return http.Get<IResponse<IBlogStats>>('/apis/api.halo.run/v1alpha1/stats/-', {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Get<IResponse<IBlogStats>>('/apis/api.halo.run/v1alpha1/stats/-', {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交点赞
|
||||
*/
|
||||
export function submitUpvote(data: IUpvoteReq) {
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交计数埋点
|
||||
*/
|
||||
export function postTrackersCounter(data: ITrackerCounterReq) {
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否可用
|
||||
*/
|
||||
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 },
|
||||
})
|
||||
return http.Get<IResponse<boolean>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
}
|
||||
|
||||
/** 分类资源(供加密分类判断等场景) */
|
||||
export type { ICategory, ILink, IMoment, IPost }
|
||||
export type { ICategory, ILink, IMoment, IPost };
|
||||
|
||||
@@ -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'
|
||||
@@ -139,16 +139,7 @@
|
||||
url: `/pages-blog/article-detail/article-detail?name=${props.article.metadata.name}`,
|
||||
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,16 +31,28 @@
|
||||
|
||||
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')
|
||||
|
||||
const statusScene = computed(() => {
|
||||
@@ -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 res = await getCategoryList({ fieldSelector: ['spec.hideFromList=false'], size: 3 })
|
||||
categoryList.value = res.data.items
|
||||
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 })
|
||||
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,36 +84,37 @@
|
||||
</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 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 left-2 bottom-2 flex z-2 flex-col text-left">
|
||||
<text class="text-sm font-bold">
|
||||
{{ category.spec.displayName }}
|
||||
</text>
|
||||
<text class="mt-1 text-xs text-gray-200">共 {{ category.postCount ?? 0 }} 篇</text>
|
||||
</view>
|
||||
</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 left-2 bottom-2 flex z-2 flex-col text-left">
|
||||
<text class="text-sm font-bold">
|
||||
{{ category.spec.displayName }}
|
||||
</text>
|
||||
<text class="mt-1 text-xs text-gray-200">共 {{ category.postCount ?? 0 }} 篇</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -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,18 +151,16 @@ onShareTimeline(() => ({
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<uh-article-card
|
||||
v-for="(article, index) in dataList"
|
||||
:key="index"
|
||||
:article="article"
|
||||
@on-click="handleToArticleDetail"
|
||||
/>
|
||||
<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 class="box-border flex flex-col gap-y-3 p-3">
|
||||
<uh-article-card
|
||||
v-for="(article, index) in dataList"
|
||||
:key="index"
|
||||
:article="article"
|
||||
@on-click="handleToArticleDetail"
|
||||
/>
|
||||
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user