1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00
Files
uni-halo/src/pages-blog/archives/archives.vue
T
小莫唐尼 938c09ad4e feat: 接入小程序链接与审核配置公开接口
- 友链接入:friend-links 双 tab(站点/小程序)、小程序详情弹窗(太阳码长按保存/地址复制/预览图轮播/作者信息)、申请收录弹窗(POST /submissions)
- 审核配置接入:新增 getAuditData,移除旧 mockJson,9 页审核模式改为真实数据按 name 过滤,友链站点 tab 按 LinkGroup 过滤、小程序 tab 隐藏
- 修复:补 uni.getLocale mock、pages.json vitest alias、getI18nText 无占位符容错、getPostList 参数类型

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
2026-09-02 01:26:45 +08:00

361 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
/**
* 归档页(源自旧项目 pagesA/archives,新建复刻)
* 按月份/年份分组展示文章时间线
*/
import { computed, ref } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPostList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { checkThumbnailUrl } from '@/utils/url'
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IPost } from '@/api/types/halo'
definePage({
style: {
navigationBarTitleText: '归档',
enablePullDownRefresh: true,
},
})
const appConfigStore = useAppConfigStore()
const settingStore = useSettingStore()
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const globalAppSettings = computed(() => settingStore.settings)
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
const tab = ref({ activeIndex: 0, list: ['按月份查看', '按年份查看'] })
const queryParams = ref({ size: 10, page: 1 })
const result = ref<{ hasNext: boolean }>({ hasNext: false })
const cacheDataList = ref<IPost[]>([])
const dataList = ref<{
sort: number
key: string
year: string
month: string
posts: IPost[]
}[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref('加载中...')
const postLabelYearKey = 'content.halo.run/archive-year'
const postLabelMonthKey = 'content.halo.run/archive-month'
/* ---------------- 数据处理 ---------------- */
/** 按 tab 分组文章 */
function handleGetPosts(list: IPost[]): Record<string, IPost[]> {
const posts: Record<string, IPost[]> = {}
list.forEach((item) => {
const labels = item.metadata.labels || {}
let postItemKey = ''
if (tab.value.activeIndex === 0) {
postItemKey = `${labels[postLabelYearKey]}-${labels[postLabelMonthKey]}`
}
else {
postItemKey = `${labels[postLabelYearKey]}`
}
if (posts[postItemKey]) {
posts[postItemKey].push(item)
}
else {
posts[postItemKey] = [item]
}
})
return posts
}
/** 处理成显示数据(分组 + 排序) */
function handleGetShowDataList(posts: Record<string, IPost[]>): typeof dataList.value {
const listResult: typeof dataList.value = []
Object.keys(posts).forEach((key) => {
const postData = {
sort: 0,
key,
year: key,
month: '',
posts: posts[key],
}
if (tab.value.activeIndex === 0) {
const splitDate = key.split('-')
postData.year = splitDate[0]
postData.month = splitDate[1]
postData.sort = Number(key.replace('-', ''))
}
else {
postData.sort = Number(key)
}
listResult.push(postData)
})
listResult.sort((a, b) => Number(b.sort) - Number(a.sort))
return listResult
}
/** 去重缓存列表 */
function handleUniqueCacheDatalist(list: IPost[]): IPost[] {
const seen = new Set<string>()
return list.filter((item) => {
return seen.has(item.metadata.name) ? false : (seen.add(item.metadata.name), true)
})
}
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
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))
const posts = handleGetPosts(filtered)
dataList.value = handleGetShowDataList(posts)
cacheDataList.value = filtered
loading.value = 'success'
loadMoreText.value = '呜呜,没有更多数据啦~'
uni.hideLoading()
uni.stopPullDownRefresh()
}
catch (err) {
console.error(err)
loading.value = 'error'
loadMoreText.value = '加载失败,请下拉刷新!'
}
return
}
if (isLoadMore.value) {
uni.showLoading({ title: '加载中...' })
}
else {
loading.value = 'loading'
}
loadMoreText.value = '加载中...'
try {
const data = await getPostList({ ...queryParams.value })
result.value = { hasNext: data.hasNext }
const posts = handleGetPosts(data.items)
const showDataList = handleGetShowDataList(posts)
if (isLoadMore.value) {
cacheDataList.value = handleUniqueCacheDatalist([...cacheDataList.value, ...data.items])
// 合并增量数据
showDataList.forEach((item) => {
const find = dataList.value.find(x => x.key === item.key)
if (find) {
item.posts.forEach((post) => {
if (!find.posts.some(x => x.metadata.name === post.metadata.name)) {
find.posts.push(post)
}
})
}
})
showDataList.forEach((post) => {
if (!dataList.value.some(x => x.key === post.key)) {
dataList.value.push(post)
}
})
dataList.value.sort((a, b) => Number(b.sort) - Number(a.sort))
}
else {
dataList.value = showDataList
cacheDataList.value = data.items
}
loading.value = 'success'
loadMoreText.value = data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}
catch (err) {
console.error(err)
loading.value = 'error'
loadMoreText.value = '加载失败,请下拉刷新!'
}
finally {
uni.hideLoading()
uni.stopPullDownRefresh()
}
}
function handleOnTabChange(index: number) {
tab.value.activeIndex = index
queryParams.value.page = 1
dataList.value = handleGetShowDataList(handleGetPosts(cacheDataList.value))
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
}
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 formatTime(time?: string): string {
// 与旧项目一致:yyyy年MM月dd日 星期w
return time ? formatTimeUtil({ d: time, f: 'yyyy年MM月dd日 星期w' }) : ''
}
/* ---------------- 生命周期 ---------------- */
handleGetData()
onPullDownRefresh(() => {
isLoadMore.value = false
queryParams.value.page = 1
handleGetData()
})
onReachBottom(() => {
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: '没有更多数据了' })
return
}
if (result.value.hasNext) {
queryParams.value.page += 1
isLoadMore.value = true
handleGetData()
}
else {
uni.showToast({ icon: 'none', title: '没有更多数据了' })
}
})
</script>
<template>
<view class="app-page min-h-screen w-screen flex flex-col" style="background-color: #fafafd;">
<!-- 顶部 tab -->
<view class="archive-tabs fixed inset-x-0 top-0 z-6 bg-white">
<wd-tabs
v-model="tab.activeIndex"
:tabs="tab.list.map(title => ({ title }))"
align="center"
@change="handleOnTabChange"
/>
</view>
<view class="h-[90rpx] w-screen" />
<!-- 骨架屏 -->
<view v-if="loading !== 'success'" class="loading-wrap p-3">
<wd-skeleton :row="3" :animated="true" />
</view>
<!-- 内容区域 -->
<block v-else>
<view v-if="dataList.length === 0" class="list-empty h-screen w-screen flex items-center justify-center">
<wd-empty :description="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'" />
</view>
<view v-else class="timeline mt-6 px-6">
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
<view class="timeline-left w-[160rpx] flex shrink-0 flex-col items-center">
<view class="timeline-dot mt-1 h-6 w-6 rounded-full" style="background-color: #64b5f6; box-shadow: 0 4rpx 12rpx rgb(100 181 246 / 40%);" />
<view v-if="index !== dataList.length - 1" class="timeline-line mt-1 w-0.5 flex-1 bg-[#e0e0e0]" />
</view>
<view class="timeline-content flex-1 pb-12 pl-6">
<view class="time mb-6 flex items-center font-bold">
<text class="time-text text-[30rpx]">{{ item.year }}</text>
<text v-if="tab.activeIndex === 0" class="time-text text-[30rpx]">{{ item.month }}</text>
<text class="time-count ml-3 text-[22rpx] text-[#999] font-normal"> {{ item.posts.length }} {{ calcAuditModeEnabled ? '内容' : '文章' }}</text>
</view>
<view v-if="item.posts.length !== 0">
<view
v-for="post in item.posts"
:key="post.metadata.name"
class="post mb-6 flex rounded-xl bg-white p-6 shadow-sm"
:class="[globalAppSettings.layout.cardType]"
@click="handleToArticleDetail(post)"
>
<image class="post-thumbnail h-[170rpx] w-[200rpx] shrink-0 rounded-lg" :src="checkThumbnailUrl(post.spec.cover)" mode="aspectFill" lazy-load />
<view class="post-info w-0 flex-1 pl-5">
<view class="post-info-title text-overflow text-[28rpx] text-[#303133] font-bold">
{{ post.spec.title }}
</view>
<view class="post-info-summary line-clamp-2 mt-3 text-[24rpx] text-[#909399]">
{{ post.status?.excerpt }}
</view>
<view class="post-info-time mt-3 text-[24rpx] text-[#909399]">
发布时间{{ formatTime(post.spec.publishTime) }}
</view>
</view>
</view>
</view>
<view v-else class="post-empty py-6 text-[26rpx] text-[#909399]">
该日期下暂无归档文章
</view>
</view>
</view>
</view>
<view class="load-text pb-6 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>
</template>
<style scoped lang="scss">
.app-page {
/* 布局全部由 UnoCSS 原子类实现 */
}
.timeline {
.post {
&.tb_image_text,
&.tb_text_image {
flex-direction: column;
.post-thumbnail {
width: 100%;
height: 220rpx;
}
.post-info {
width: 100%;
padding-left: 0;
}
}
&.lr_text_image {
.post-thumbnail {
order: 2;
}
.post-info {
order: 1;
padding-left: 0;
padding-right: 24rpx;
}
}
&.only_text {
.post-thumbnail {
display: none;
}
.post-info {
padding: 6rpx;
}
}
}
}
</style>