1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

feat: 新增收藏功能与多项体验优化

- 新增本地收藏store与工具函数,支持文章/瞬间收藏持久化
- 重构插件可用性检查hook,统一管理依赖插件状态
- 替换旧数据加载hook为新的状态管理方案
- 优化首页布局与加载逻辑,调整公告组件样式
- 为瞬间详情页添加点赞、评论与收藏功能
- 新增文本处理工具,支持HTML/Markdown转纯文本与摘要截断
- 修复评论弹窗适配瞬间类型的参数问题
This commit is contained in:
小莫唐尼
2026-09-04 17:17:18 +08:00
parent 629b1f9219
commit 62b604644c
28 changed files with 1548 additions and 1387 deletions
+35 -30
View File
@@ -8,12 +8,14 @@
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getMomentList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { useFavoritesStore } from '@/store/favorites'
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import { buildMomentFavoriteItem } from '@/utils/favorite'
import { generateUUID } from '@/utils/uuid'
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import { formatTime } from '@/utils/formatTime'
import { randomTagColor } from '@/utils/random'
import { t } from '@/locale'
import { usePluginAvailable } from '@/utils/plugin'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { markdownConfig } from '@/config/markdown'
import type { IMoment } from '@/api/types/halo'
@@ -21,12 +23,11 @@
style: {
navigationBarTitleText: '瞬间',
enablePullDownRefresh: true,
// 下拉/回弹露出的窗口底色对齐页面底色
backgroundColor: '#f6f3ee',
},
})
const appConfigStore = useAppConfigStore()
const favoritesStore = useFavoritesStore()
const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const calcUseTagRandomColor = computed(() => !!haloConfigs.value.pageConfig?.momentConfig?.useTagRandomColor)
@@ -47,13 +48,13 @@
/** 依赖插件(plugin-moments) */
const uniHaloPluginId = 'plugin-moments'
const uniHaloPluginAvailable = ref(true)
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({ size: 10, page: 1 })
const hasNext = ref(false)
/** 列表卡片:medium 已按类型拆为 images/videos/audios + 正文 tag 清理 */
/** 列表卡片 */
type MomentCard = IMoment & {
images ?: { type ?: string, url : string }[]
videos ?: { id ?: string, url : string }[]
@@ -66,13 +67,6 @@
const videoContexts = ref<Record<string, UniApp.VideoContext | undefined>>({})
const currentVideoId = ref<string | null>(null)
/** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */
const calcTagColors = computed(() => {
return dataList.value.map(moment =>
(moment.spec.tags || []).map(() => (calcUseTagRandomColor.value ? randomTagColor() : '#4d7c0f')),
)
})
/** 移除内容中的 tag 链接 */
function removeTagLinksCompletely(htmlString : string) : string {
const regex = /<a\b[^>]+class=(['"])[^'"]*\btag\b[^'"]*\1[^>]*>[\s\S]*?<\/a>/gi
@@ -116,14 +110,14 @@
nextTick(() => {
createVideoContexts(tempItems)
})
loading.value = 'success'
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh()
}
catch (err) {
console.error(err)
loading.value = 'error'
updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = t('common.loadFailed')
}
return
@@ -131,13 +125,12 @@
uni.showLoading({ mask: true, title: t('common.loading') })
if (!isLoadMore.value) {
loading.value = 'loading'
updateLoadingStatus(DataLoadingStatusEnum.Loading)
}
loadMoreText.value = t('common.loading')
try {
const res = await getMomentList({ ...queryParams.value })
loading.value = 'success'
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
hasNext.value = res.data.hasNext
@@ -148,6 +141,7 @@
dataList.value = isLoadMore.value
? dataList.value.concat(tempItems)
: tempItems
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
nextTick(() => {
createVideoContexts(tempItems)
@@ -155,7 +149,7 @@
}
catch (err) {
console.error(err)
loading.value = 'error'
updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = t('common.loadFailed')
}
finally {
@@ -216,6 +210,18 @@
})
}
/** 是否已收藏该瞬间(卡片收藏格高亮) */
function isMomentFavorite(moment : IMoment) : boolean {
return favoritesStore.isFavorite('moment', moment.metadata.name)
}
/** 切换收藏(收藏/取消),收藏时按当前卡片内容生成快照入库 */
function handleToggleMomentFavorite(moment : MomentCard) {
if (!moment) { return }
const favorited = favoritesStore.toggle(buildMomentFavoriteItem(moment))
uni.showToast({ icon: 'none', title: favorited ? '收藏成功' : '已取消收藏' })
}
function handleToTopPage(duration = 500) {
uni.pageScrollTo({
scrollTop: 0,
@@ -228,14 +234,13 @@
/** 格式化瞬间时间 */
function formatMomentTime(time ?: string) : string {
// 与旧项目一致:yyyy年MM月dd日 星期w
return time ? formatTimeUtil({ d: time, f: 'yyyy年MM月dd日 星期w' }) : ''
return time ? formatTime({ d: time, f: 'yyyy年MM月dd日 星期w' }) : ''
}
/* ---------------- 生命周期 ---------------- */
onLoad(async () => {
uni.setNavigationBarTitle({ title: t('page.moments.title') })
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
await checkPluginAvailable()
if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh()
return
@@ -256,8 +261,7 @@
})
onReachBottom(() => {
if (!uniHaloPluginAvailable.value)
return
if (!uniHaloPluginAvailable.value) { return }
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
@@ -279,8 +283,8 @@
error-text="检测到当前插件没有安装或者启用无法使用瞬间功能哦请联系管理员" @on-refresh="handleGetData" />
<template v-else>
<!-- 加载失败(可重试) -->
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
@refresh="handleGetData" />
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="60vh" @refresh="handleGetData" />
<view v-else class="flex flex-col gap-3 px-3">
<view v-if="dataList.length === 0"
@@ -289,7 +293,7 @@
</view>
<block v-else>
<!-- 瞬间卡片(社交信息流:着色昵称 + 朋友圈式不缩进正文 + 媒体九宫格 + 内嵌互动脚注) -->
<!-- 瞬间卡片-->
<view v-for="moment in dataList" :key="moment.metadata.name"
class="moment-card uh-shadow-xs overflow-hidden rounded-[24rpx] bg-white">
<!-- 作者 -->
@@ -372,9 +376,10 @@
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
<text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text>
</view>
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
<text class="text-sm text-gray-600">收藏</text>
<view class="flex items-center gap-x-1" @click.stop="handleToggleMomentFavorite(moment)">
<wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
<text class="text-sm text-gray-600"
:style="isMomentFavorite(moment) ? { color: '#ffb300' } : ''">{{ isMomentFavorite(moment) ? '已收藏' : '收藏' }}</text>
</view>
</view>
</view>