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. 优化uh-glass-popup组件,新增lazyRender和hideWhenClose属性并设置默认值
3. 重构友链、瞬间等页面的加载逻辑,移除多余的setTimeout,统一使用sleep延迟
4. 重写偏好设置页面的枚举选择弹窗,替换为自定义弹窗组件
5. 优化瞬间页面的UI布局,改为时间轴样式
6. 修复多个组件的样式细节与代码规范问题
This commit is contained in:
小莫唐尼
2026-09-09 01:45:23 +08:00
parent 1ca79b3ad5
commit bcb91bf58f
11 changed files with 425 additions and 410 deletions
@@ -0,0 +1,118 @@
<script setup lang="ts">
import { computed } from 'vue'
/** 单列选项(wd-picker PickerOption 的宽松形态,label/value 与 wot 一致) */
interface IPickerOption {
label?: string | number
value?: string | number
disabled?: boolean
children?: IPickerOption[]
[key: string]: unknown
}
interface IProps {
/** v-model:visible 是否显示 */
visible: boolean
/** 弹层标题 */
title?: string
/** 选择器数据(单列/多列;与 wd-picker columns 一致) */
columns?: Array<IPickerOption | IPickerOption[]>
/** 选中项(单列如 ['value']) */
modelValue?: (string | number)[]
/** 确认/取消按钮文案 */
confirmButtonText?: string
cancelButtonText?: string
/** 自定义层级 */
zIndex?: number
/** 点击遮罩是否关闭 */
closeOnClickModal?: boolean
/** 底部安全距离适配 */
safeAreaInsetBottom?: boolean
/** 追加到选择器根元素的自定义类 */
customClass?: string
}
interface IEmits {
(e: 'update:visible', value: boolean): void
(e: 'update:modelValue', value: (string | number)[]): void
(e: 'confirm', payload: { value: (string | number)[] }): void
(e: 'open'): void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<IProps>(), {
title: '',
columns: () => [],
modelValue: () => [],
confirmButtonText: '确定',
cancelButtonText: '取消',
zIndex: 15,
closeOnClickModal: true,
safeAreaInsetBottom: true,
customClass: '',
})
const emit = defineEmits<IEmits>()
const pickerClass = computed(() => `uh-picker ${props.customClass}`.trim())
/* ---------------- wd-picker 事件转发(在 script 处理,模板只做绑定) ---------------- */
function handleUpdateVisible(value: boolean) {
emit('update:visible', value)
}
function handleUpdateModelValue(value: (string | number)[]) {
emit('update:modelValue', value)
}
function handleConfirm(payload: { value: (string | number)[] }) {
emit('confirm', payload)
}
function handleOpen() {
emit('open')
}
function handleCancel() {
emit('cancel')
}
</script>
<template>
<wd-picker
:visible="visible" :title="title" :columns="columns" :model-value="modelValue"
:confirm-button-text="confirmButtonText" :cancel-button-text="cancelButtonText" :z-index="zIndex"
:close-on-click-modal="closeOnClickModal" :safe-area-inset-bottom="safeAreaInsetBottom"
:custom-class="pickerClass" @update:visible="handleUpdateVisible" @update:model-value="handleUpdateModelValue"
@confirm="handleConfirm" @open="handleOpen" @cancel="handleCancel"
>
<slot />
</wd-picker>
</template>
<style scoped lang="scss">
/* 弹层面板玻璃质感(与 uh-glass-popup 同款;wd-picker 内层 popup 挂 .wd-picker__popup) */
:deep(.wd-picker__popup .wd-popup) {
box-sizing: border-box;
background-color: rgb(255 255 255 / 85%);
border: 4rpx solid rgb(255 255 255 / 90%);
box-shadow:
inset 0 1rpx 0 rgb(255 255 255 / 75%),
0 8rpx 32rpx rgb(90 105 200 / 14%);
backdrop-filter: blur(24rpx) saturate(160%);
-webkit-backdrop-filter: blur(24rpx) saturate(160%);
}
/* 低端 WebView 不支持 backdrop-filter 的兜底:提高不透明度保证可读性 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
:deep(.wd-picker__popup .wd-popup) {
background-color: rgb(255 255 255 / 88%);
}
}
:deep(.wd-popup--bottom) {
left: 24rpx;
bottom: 24rpx;
right: 24rpx;
}
</style>
@@ -23,6 +23,8 @@
radius ?: string
/** 是否开启 wot 位置自适应圆角(bottom→上圆角 / center→四圆角等),与 radius 二选一 */
round ?: boolean
lazyRender ?: boolean
hideWhenClose?: boolean
/** 追加到面板的内联样式,如宽度:width:640rpx; */
customStyle ?: string
customClass? :string
@@ -43,6 +45,8 @@
safeAreaInsetBottom: false,
radius: '',
round: false,
lazyRender: true,
hideWhenClose: true,
customStyle: '',
customClass:''
})
@@ -66,10 +70,12 @@
:closable="closable"
:modal="modal"
:close-on-click-modal="closeOnClickModal"
:hide-when-close="hideWhenClose"
:safe-area-inset-bottom="safeAreaInsetBottom"
:round="round"
:custom-style="panelStyle"
:custom-class="panelClass"
:lazy-render="lazyRender"
@update:model-value="(value: boolean) => emit('update:modelValue', value)"
@close="emit('close')"
@click-modal="emit('click-modal')"
@@ -201,7 +201,7 @@
<template>
<uh-glass-popup v-model="isShow" :z-index="100" position="bottom"
custom-class="!border rounded-lt-2xl rounded-rt-2xl" @close="handleClose(false)">
custom-class="!border rounded-xl" @close="handleClose(false)">
<view class="mb-4 relative w-full flex items-center justify-around box-border px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">申请收录小程序</text>
@@ -6,6 +6,7 @@
pluginId : string
errorText ?: string
checking : boolean
customClass ?: string
}>(), {
errorText: '',
})
@@ -16,7 +17,7 @@
/** 插件信息(未在清单中时兜底) */
const pluginInfo = computed(() => {
return NeedPlugins.get(props.pluginId) ?? {pluginId:props.pluginId, name: '未找到插件' }
return NeedPlugins.get(props.pluginId) ?? { pluginId: props.pluginId, name: '未找到插件' }
})
function handleRefresh() {
@@ -26,7 +27,9 @@
</script>
<template>
<view v-if="pluginInfo" class="max-w-3/5 mx-auto my-auto box-border flex flex-col items-center justify-center gap-6 text-sm">
<view v-if="pluginInfo"
class="max-w-3/5 mx-auto my-auto box-border flex flex-col items-center justify-center gap-6 text-sm"
:class="props.customClass">
<wd-icon class-prefix="uhemoji-icon" name="-cry" size="160rpx"></wd-icon>
@@ -46,8 +49,7 @@
<!-- 微信端客服会话只能由原生 button open-type="contact" 唤起,故此处不用 uh-button(view 实现) -->
<button
class="uh-contact-btn bg-white py-2 px-4 !rounded-full text-black text-sm leading-none flex items-center justify-center"
open-type="contact"
hover-class="none">提交反馈</button>
open-type="contact" hover-class="none">提交反馈</button>
<!-- #endif -->
</view>
</view>
@@ -59,5 +61,6 @@
.uh-contact-btn::after {
border: none;
}
/* #endif */
</style>
@@ -1,135 +0,0 @@
<script lang="ts" setup>
/**
* 插件不可用提示(源自旧项目 components/plugin-unavailable,新建复刻)
* 当依赖的 Halo 插件未安装/未启用时展示:插件 logo、名称、错误标签、描述、插件地址、复制/反馈按钮
*/
import { computed } from 'vue'
import { NeedPlugins } from '@/utils/plugin'
const props = withDefaults(defineProps<{
/** 插件名称(与 NeedPlugins 中的 id 对应) */
pluginId: string
errorText?: string
useDecoration?: boolean
useBorder?: boolean
customStyle?: Record<string, string>
}>(), {
errorText: '',
useDecoration: true,
useBorder: true,
customStyle: () => ({}),
})
const emit = defineEmits<{
(e: 'on-refresh'): void
}>()
/** 插件信息(未在清单中时兜底) */
const pluginInfo = computed(() => {
const info = NeedPlugins.get(props.pluginId)
return info || {
id: props.pluginId,
name: props.pluginId,
desc: '',
logo: '',
url: '',
}
})
const defaultStyle = {
width: '80vw',
borderRadius: '24rpx',
}
const calcCustomStyle = computed(() => ({
...defaultStyle,
...props.customStyle,
}))
function copy() {
if (!pluginInfo.value.url)
return
uni.setClipboardData({
data: pluginInfo.value.url,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: '插件地址已复制' })
},
})
}
</script>
<template>
<view
v-if="pluginInfo"
class="uh-plugin-unavailable mx-auto my-auto box-border flex flex-col gap-6 p-10 text-[28rpx]"
:class="{ border: useBorder, decoration: useDecoration }"
:style="[calcCustomStyle]"
>
<!-- 图标 -->
<image class="plugin-logo box-border h-[120rpx] w-[120rpx] rounded-3xl" :src="pluginInfo.logo" mode="scaleToFill" />
<!-- 名称 -->
<view class="plugin-name box-border text-[32rpx] text-[#333] font-bold">
{{ pluginInfo.name }}
</view>
<!-- 错误标签 -->
<view class="plugin-error box-border rounded-[36rpx] px-4 py-1.5 text-[24rpx] font-bold" style="background-color: rgb(255 61 49 / 7.5%); color: rgb(255 61 49);">
未安装/启用插件
</view>
<!-- 描述 -->
<view class="plugin-desc box-border w-[60vw] text-center text-[24rpx] text-[#64748b]">
{{ pluginInfo.desc }}
</view>
<!-- 自定义错误提示 -->
<view v-if="errorText" class="plugin-tip box-border border-2 rounded-xl border-dashed px-5 py-2.5 text-[24rpx]" style="border-color: #f2c97d; color: #f0a020;">
{{ errorText }}
</view>
<!-- 插件地址 -->
<view class="plugin-url box-border w-full overflow-hidden text-ellipsis whitespace-nowrap rounded-xl bg-[#f1f5f9] px-6 py-4 text-[24rpx] text-[#666]">
插件地址:{{ pluginInfo.url }}
</view>
<!-- 反馈按钮/复制地址 -->
<view class="plugin-btns box-border w-full">
<!-- #ifndef MP-WEIXIN -->
<wd-button type="primary" block size="medium" @click="copy">
复制地址
</wd-button>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="flex gap-3">
<wd-button type="primary" plain block size="medium" @click="copy">
复制地址
</wd-button>
<wd-button type="warning" plain block size="medium" open-type="contact">
提交反馈
</wd-button>
</view>
<!-- #endif -->
</view>
<!-- 刷新按钮 -->
<view class="flex justify-center">
<wd-button size="small" plain type="info" @click="emit('on-refresh')">
刷新试试
</wd-button>
</view>
<view class="plugin-copyright text-[20rpx] text-[#999]" style="transform: scale(0.9) translateY(20px);">
提示:请确保 Halo 博客已安装相关插件
</view>
</view>
</template>
<style scoped lang="scss">
.uh-plugin-unavailable {
&.border {
border: 2rpx solid #eee;
}
&.decoration {
background-color: rgb(255 255 255 / 95%);
box-shadow: 0 0 12rpx rgb(226 232 240 / 35%);
backdrop-filter: blur(6rpx);
border-top: 12rpx solid rgb(3 169 244);
}
}
</style>
+52 -49
View File
@@ -5,6 +5,7 @@
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { sleep } from '@/utils/common'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
@@ -29,24 +30,37 @@
/** 站点 tab:PluginLinks */
const { pluginId: sitePluginId, checking: siteChecking, tips: siteTips, available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginLinks,
tips: '检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员',
tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetLinkGroupData()
}
})
/** 小程序 tab:plugin-uni-halo */
const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginUniHalo,
tips: '检测到当前插件没有安装或者启用,无法使用小程序链接功能哦,请联系管理员',
tips: '啊偶,功能正在维护中...', callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetMiniProgramLinks()
}
})
/** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleSitePluginRefresh() {
if (await checkSitePluginAvailable())
handleGetLinkGroupData()
if (await checkSitePluginAvailable()) { handleGetLinkGroupData() }
}
/** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleMiniPluginRefresh() {
if (await checkMiniPluginAvailable())
handleGetMiniProgramLinks()
if (await checkMiniPluginAvailable()) { handleGetMiniProgramLinks() }
}
/* ---------------- tabs ---------------- */
@@ -64,8 +78,7 @@
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab
watch(() => appConfigStore.auditModeEnabled, (enabled) => {
if (enabled)
activeTabIndex.value = 0
if (enabled) { activeTabIndex.value = 0 }
})
/* ==================== 站点 tab(plugin-links) ==================== */
@@ -122,12 +135,11 @@
},
}))
dataList.value = dataList.value.concat(list)
setTimeout(() => {
updateSiteLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}, 500)
await sleep(600)
updateSiteLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}
catch (err) {
console.error(err)
@@ -135,9 +147,7 @@
loadMoreText.value = '加载失败,请下拉刷新!'
}
finally {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
uni.stopPullDownRefresh()
}
}
@@ -173,13 +183,6 @@
})
}
function calcSiteThumbnail(val ?: string) : string {
if (!val)
return ''
const _val = val.endsWith('/') ? val : `${val}/`
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
}
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
/* ---------------- 状态 ---------------- */
const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
@@ -193,20 +196,17 @@
try {
const res = await getMiniProgramLinkGroupedList()
miniGroups.value = res.data || []
setTimeout(() => {
updateMiniLoadingStatus(
miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
}, 500)
await sleep(600)
updateMiniLoadingStatus(
miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
}
catch (err) {
console.error(err)
updateMiniLoadingStatus(DataLoadingStatusEnum.Error)
}
finally {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
uni.stopPullDownRefresh()
}
}
@@ -393,16 +393,17 @@
</view>
<!-- 悬浮按钮 -->
<view class="flot-buttons fixed bottom-10 right-4 z-999 flex flex-col gap-1.5">
<view class="flot-buttons fixed bottom-8 right-3 z-999 flex flex-col gap-1.5">
<view v-if="!haloPluginConfigs?.linksSubmitPlugin?.enabled"
class="fab-btn uh-global-card-glass h-10 w-10 flex items-center justify-center rounded-full"
class="fab-btn uh-global-card-glass h-11 w-11 flex items-center justify-center rounded-full"
@click="toSubmitLinkPage">
<wd-icon name="edit" size="20px" color="#6b7280" />
</view>
</view>
<!-- 详情弹窗 -->
<uh-glass-popup v-model="detail.show" position="bottom" :z-index="999" custom-class="rounded-xl !border">
<uh-glass-popup v-model="detail.show" position="bottom" :z-index="999"
custom-class="rounded-xl !border">
<view class="relative w-full flex items-center justify-around box-border px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">站点详情</text>
@@ -418,23 +419,24 @@
<view class="flex">
<image class="h-20 w-20 shrink-0 rounded-2xl uh-global-card-glass"
:src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
<view class="ml-4 flex flex-1 flex-col gap-y-1 justify-center">
<view class="text-lg text-gray-900 font-bold">
<view class="ml-4 flex flex-1 flex-col gap-y-1.5 justify-center">
<view class="text-md text-gray-900 font-bold">
{{ detail.data.spec.displayName }}
</view>
<view class="flex items-center gap-x-2">
<text class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900">{{ detail.data.spec.groupName }}</text>
<text class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900">
<text
class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900">{{ detail.data.spec.groupName }}</text>
<text
class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900">
复制地址
</text>
</view>
<view @click="handleCopyLink(detail.data)">
<text
class="text-xs text-gray-900">{{ detail.data.spec.url }}</text>
<text class="text-xs text-gray-900">{{ detail.data.spec.url }}</text>
</view>
</view>
</view>
<view class="poup-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]">
<view class="poup-desc mt-4 text-xs text-gray-600 leading-5">
{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
</view>
</scroll-view>
@@ -457,11 +459,11 @@
<view v-else class="content flex flex-1 flex-col">
<!-- 分组列表 -->
<view class="box-border flex-1 p-3">
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
<view class="mb-4 flex items-center">
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-4">
<view class="mb-3 flex items-center">
<text class="mr-2 inline-block h-4 w-1 rounded-full bg-secondary" />
<text
class="text-[30rpx] text-gray-900 font-bold">{{ group.displayName || '未分组' }}</text>
class="text-sm text-gray-900 font-bold">{{ group.displayName || '未分组' }}</text>
<text class="ml-3 text-xs text-gray-400">{{ group.links.length }}</text>
</view>
<view class="group-cards flex flex-col gap-4">
@@ -491,9 +493,9 @@
</view>
<!-- 申请收录悬浮按钮 -->
<view class="fixed bottom-10 right-4 z-50">
<view class="fixed bottom-8 right-3 z-50">
<view
class="box-border flex flex-col w-10 h-10 items-center justify-center rounded-full bg-gray-900"
class="box-border flex flex-col w-11 h-11 items-center justify-center rounded-full bg-gray-900"
@click="handleOpenApply">
<text class="text-xs text-white">申请</text>
</view>
@@ -501,7 +503,8 @@
</view>
<!-- 小程序详情弹窗 -->
<uh-glass-popup v-model="miniDetail.show" :z-index="999" position="bottom" custom-class="rounded-xl !border">
<uh-glass-popup v-model="miniDetail.show" :z-index="999" position="bottom"
custom-class="!rounded-xl !border">
<view class="relative w-full flex items-center justify-around box-border px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">小程序详情</text>
+2 -10
View File
@@ -241,10 +241,8 @@
<template>
<view class="uh-global-love-page box-border min-h-screen w-screen flex flex-col">
<!-- 自定义导航 -->
<uh-navbar default-title="恋爱清单" title-color="text-love" back-class="text-love"/>
<!-- 粘性筛选区:参考投票页顶部胶囊设计,每个维度独立状态 -->
<wd-sticky>
<view class="box-border px-3 pb-1 pt-2">
<view class="box-border flex items-center justify-between gap-x-2">
@@ -263,16 +261,10 @@
</view>
</wd-sticky>
<!-- 加载/错误/空占位(状态机) -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="60vh" empty-text="暂时还没有恋爱清单快去制定你们的恋爱清单吧~" @refresh="handleGetList" />
<!-- 清单列表 -->
<view v-else class="box-border flex flex-1 flex-col gap-y-3 p-3 pb-safe">
<view
class="uh-global-card-glass uh-shadow-xs box-border w-full rounded-xl p-3 text-center text-xs text-love">
看看我们的恋爱清单都完成了哪些吧
</view>
<block v-for="(item, index) in showList" :key="item.name">
<view
class="uh-global-card-glass uh-shadow-xs box-border w-full flex flex-col items-center rounded-xl p-3">
@@ -298,10 +290,10 @@
<view v-if="item.open"
class="uh-global-card-glass mt-4 box-border w-full rounded-xl p-3 text-xs shadow-none">
<view v-if="item.content" class="desc mb-3 flex">
<view class="desc-label w-16 shrink-0 text-gray-500">
<view class="desc-label w-16 shrink-0 text-gray-500 ">
计划内容
</view>
<view class="desc-value w-0 flex-1 text-gray-900 leading-4">
<view class="desc-value w-0 flex-1 text-gray-900 leading-5">
{{ item.content || '-' }}
</view>
</view>
+9 -1
View File
@@ -22,7 +22,15 @@
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginSearchWidget,
tips: '啊偶,当前功能未开放!',
tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleOnSearch()
}
})
async function handlePluginRefresh() {
+101 -51
View File
@@ -10,7 +10,7 @@
definePage({
style: {
navigationBarTitleText: '偏好设置',
navigationStyle: 'custom'
navigationStyle: 'custom',
},
})
@@ -87,7 +87,7 @@
]
/* ---------------- 状态读取 ---------------- */
function valueOf(path : Path) : unknown {
function prefValueOf(path : Path) : unknown {
return getByPath(settingStore.settings, path)
}
@@ -133,11 +133,17 @@
settingStore.savePreference(buildPatch(path, null))
}
/* ---------------- 枚举底部弹层 ---------------- */
/* ---------------- 枚举底部弹层(uh-glass-popup + wd-picker-view) ---------------- */
const enumSheet = ref<{ show : boolean, def : PrefDef | null }>({ show: false, def: null })
/** 弹层内滚动中的临时选中值(单列;确认时才落库,取消不生效) */
const pickerValue = ref<(string | number)[]>([''])
function handleOpenEnum(def : PrefDef) {
console.log('handleOpenEnum', def)
enumSheet.value = { show: true, def }
// 打开时同步当前值(跟随站点默认 → 空串哨兵)
pickerValue.value = [isFollowing(def) ? '' : String(prefValueOf(def.path) ?? '')]
console.log('pickerValue', pickerValue.value)
}
function handleCloseEnum() {
@@ -157,37 +163,59 @@
handleCloseEnum()
}
/* ---------------- wd-picker 弹层数据 ---------------- */
/* ---------------- wd-picker-view 弹层数据 ---------------- */
/** 枚举弹层列(首项「跟随站点默认」,空串哨兵映射 null) */
const enumColumns = computed(() => {
const def = enumSheet.value.def
if (!def)
return []
if (!def) { return [] }
return [
{ label: '跟随站点默认', value: '' },
...(def.options || []).map(opt => ({ label: opt.label, value: opt.value })),
]
})
/** 当前选中值(单列;跟随站点默认时为空串) */
const enumValue = computed(() => {
const def = enumSheet.value.def
if (!def)
return ['']
return [isFollowing(def) ? '' : String(valueOf(def.path) ?? '')]
})
/** wd-picker-view 滚动变化:更新临时选中值(未确认不落库) */
function handlePickerChange(payload : { selectedValues : (string | number)[] }) {
pickerValue.value = payload.selectedValues
}
/** wd-picker 确认:空串哨兵还原为「跟随站点默认」 */
function handlePickerConfirm(payload: { value: (string | number)[] }) {
const picked = String(payload.value[0] ?? '')
/** 确认:空串哨兵还原为「跟随站点默认」 */
function handlePickerConfirm() {
const picked = String(pickerValue.value[0] ?? '')
handleChooseEnum(picked === '' ? null : picked)
}
/** 取消:不落库,直接关闭 */
function handlePickerCancel() {
handleCloseEnum()
}
/** 当前枚举项是否处于「跟随站点默认」 */
function isFollowing(def : PrefDef) : boolean {
return !isOverridden(def.path)
}
/* ---------------- 展示行(预计算,避免模板渲染期函数调用) ---------------- */
/** 偏好展示行:展示文本/跟随态/开关值在数据层算好,模板只做属性访问 */
interface PrefRow extends PrefDef {
displayValue : string
following : boolean
/** 仅 kind==='bool' 使用 */
boolValue : boolean
}
function buildRows(defs : PrefDef[]) : PrefRow[] {
return defs.map(def => ({
...def,
displayValue: enumLabelOf(def, prefValueOf(def.path)),
following: isFollowing(def),
boolValue: def.kind === 'bool' ? prefValueOf(def.path) === true : false,
}))
}
const layoutRows = computed(() => buildRows(layoutPrefs))
const featureRows = computed(() => buildRows(featurePrefs))
/* ---------------- 重置全部 ---------------- */
function handleResetAll() {
uni.showModal({
@@ -211,10 +239,10 @@
<template>
<view class="box-border min-h-screen bg-page">
<!-- 自定义标题 -->
<uh-navbar default-title="偏好设置" title-color="text-gray-900" :need-placeholder="true"></uh-navbar>
<uh-navbar default-title="偏好设置" title-color="text-gray-900" :need-placeholder="true" />
<!-- 内容区域 -->
<view class="box-border p-3 flex flex-col gap-y-6">
<view class="box-border flex flex-col gap-y-6 p-3">
<!-- 布局设置 -->
<view class="flex flex-col gap-y-3">
<uh-section-title>
@@ -224,14 +252,14 @@
</template>
</uh-section-title>
<view class="uh-global-card-glass overflow-hidden rounded-2xl">
<view v-for="(def, index) in layoutPrefs" :key="def.key"
<view v-for="(row, index) in layoutRows" :key="row.key"
class="pick-row flex items-center justify-between px-4 py-4"
:class="index < layoutPrefs.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(def)">
:class="index < layoutRows.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(row)">
<view class="row-left flex flex-col gap-1">
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ row.label }}</text>
<view class="flex items-center gap-2">
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<text v-if="row.following" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<view v-else
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义
@@ -239,8 +267,7 @@
</view>
</view>
<view class="row-value flex items-center gap-2">
<text
class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
<text class="value-text text-[26rpx] text-gray-400">{{ row.displayValue }}</text>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
</view>
</view>
@@ -255,64 +282,87 @@
</template>
</uh-section-title>
<view class="setting-sheet uh-global-card-glass overflow-hidden rounded-2xl">
<template v-for="(def, index) in featurePrefs" :key="def.key">
<template v-for="(row, index) in featureRows" :key="row.key">
<!-- 布尔开关 -->
<view v-if="def.kind === 'bool'" class="switch-row flex items-center justify-between px-4 py-4"
:class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''">
<view v-if="row.kind === 'bool'" class="switch-row flex items-center justify-between px-4 py-4"
:class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''">
<view class="row-left flex flex-col gap-1">
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ row.label }}</text>
<view class="flex items-center gap-2">
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<text v-if="row.following" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<template v-else>
<view
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义
</view>
<text class="revert-text text-2xs text-gray-400 underline"
@click.stop="handleRevert(def.path)">恢复默认</text>
@click.stop="handleRevert(row.path)">
恢复默认
</text>
</template>
</view>
</view>
<wd-switch :model-value="valueOf(def.path) === true" @change="handleSwitchChange(def, $event)" />
<wd-switch :model-value="row.boolValue" @change="handleSwitchChange(row, $event)" />
</view>
<!-- 枚举选择(指示器位置) -->
<view v-else class="pick-row flex items-center justify-between px-4 py-4"
:class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(def)">
:class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(row)">
<view class="row-left flex flex-col gap-1">
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ row.label }}</text>
<view class="flex items-center gap-2">
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<text v-if="row.following" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
<template v-else>
<view
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义
</view>
<text class="revert-text text-2xs text-gray-400 underline"
@click.stop="handleRevert(def.path)">恢复默认</text>
@click.stop="handleRevert(row.path)">
恢复默认
</text>
</template>
</view>
</view>
<view class="row-value flex items-center gap-2">
<text
class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
<text class="value-text text-[26rpx] text-gray-400">{{ row.displayValue }}</text>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
</view>
</view>
</template>
</view>
</view>
<!-- 底部操作栏(玻璃悬浮) -->
<view class="box-border w-full px-2">
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full"
@click="handleResetAll">恢复默认</uh-button>
<!-- 底部操作栏-->
<view class="box-border w-full">
<uh-button custom-class="uh-global-card-glass py-2 !rounded-xl" @click="handleResetAll">
恢复默认
</uh-button>
</view>
</view>
<!-- 枚举选择弹层(wd-picker 自带底部弹层与工具栏) -->
<wd-picker
v-model:visible="enumSheet.show" :title="enumSheet.def?.label || ''" :columns="enumColumns"
:model-value="enumValue" confirm-button-text="确定" cancel-button-text="取消"
@confirm="handlePickerConfirm"
/>
<!-- 枚举选择弹层(uh-glass-popup + wd-picker-view,底部取消/确认,参考 uh-album-photo-viewer 布局) -->
<uh-glass-popup v-model="enumSheet.show" :hide-when-close="false" position="bottom" custom-class="rounded-xl">
<view class="box-border px-4 py-4">
<!-- 标题 -->
<view class="mb-3 flex items-center justify-between">
<text class="text-md font-bold">{{ enumSheet.def?.label || '请选择' }}</text>
</view>
<!-- 选择器 -->
<wd-picker-view :columns="enumColumns" v-model="pickerValue"
custom-class="!p-0 !bg-transparent !rounded-xl overflow-hidden" @change="handlePickerChange" />
<!-- 底部操作:取消 / 确认 -->
<view class="mt-4 flex items-center justify-center gap-x-3">
<uh-button custom-class="flex-1 py-2 uh-global-card-glass border !rounded-xl bg-white/90"
@click="handlePickerCancel">
取消
</uh-button>
<uh-button
custom-class="flex-1 py-2 uh-global-card-glass !rounded-xl border bg-primary text-gray-900"
@click="handlePickerConfirm">
确定
</uh-button>
</view>
</view>
</uh-glass-popup>
</view>
</template>
</template>
+12 -12
View File
@@ -4,6 +4,8 @@
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url'
import { usePluginAvailable } from '@/hooks/usePluginAvailable'
import { sleep } from '@/utils/common'
import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
@@ -25,7 +27,7 @@
/** 依赖插件(PluginPhotos) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: 'PluginPhotos',
tips: '很抱歉,功能正在维护中...',
tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
@@ -114,6 +116,7 @@
? dataList.value.concat(list)
: list
}
await sleep(600)
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
}
@@ -123,11 +126,7 @@
loadMoreText.value = t('common.loadFailed')
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
lock.value = false
}, 500)
uni.stopPullDownRefresh()
}
}
@@ -191,29 +190,30 @@
</script>
<template>
<view class="min-h-screen w-screen flex flex-col bg-page pb-6">
<view class="box-border min-h-screen w-screen flex flex-col bg-page pb-6">
<uh-navbar :use-back="false" default-title="我的图库" title-color="text-gray-900"></uh-navbar>
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="checkPluginAvailable" />
<template v-else>
<wd-sticky v-if="category.list.length!==0" class="w-full">
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-2">
<view v-for="(cate, index) in category.list" :key="cate.spec.displayName"
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-block border rounded-2xl px-4 py-1 text-sm"
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-flex border rounded-2xl px-4 py-1 text-sm"
:class="{ 'bg-primary text-gray-900 font-bold': index === category.activeIndex }"
@click="handleGetDataByCategory(index, cate)">
{{ cate.spec.displayName }} <text
v-if="cate.spec.displayName!=='全部'">({{ cate.status?.photoCount ?? 0 }})</text>
{{ cate.spec.displayName }}
<text v-if="cate.spec.displayName!=='全部'">
({{ cate.status?.photoCount ?? 0 }})
</text>
</view>
</scroll-view>
</wd-sticky>
<!-- 加载/错误占位 -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
@refresh="handleGetCategory" />
<!-- 内容区域 -->
<view v-else class="box-border w-full p-3">
<view class="grid grid-cols-2 gap-2.5">
<view v-for="(item, index) in dataList" :key="index"
+117 -147
View File
@@ -1,11 +1,7 @@
<script lang="ts" setup>
/**
* 瞬间页(源自旧项目 pages/tabbar/moments/moments.vue,新建复刻)
* 功能:瞬间卡片列表(头像/内容/图片/音频/视频/标签) + 分页加载
* 设计:社交信息流(实心白纸卡 + 着色昵称 + 朋友圈式不缩进正文),区别于工具页的玻璃拟态
*/
import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import { getMomentList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
@@ -13,8 +9,8 @@
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import { buildMomentFavoriteItem } from '@/utils/favorite'
import { generateUUID } from '@/utils/uuid'
import { sleep } from '@/utils/common'
import { formatTime } from '@/utils/formatTime'
import { randomTagColor } from '@/utils/random'
import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { markdownConfig } from '@/config/markdown'
@@ -42,22 +38,26 @@
}
})
/** 站点名称(原 startConfig.title 已随启动页下线,改读 appConfig.appInfo.name) */
const siteName = computed(() => {
const appInfo = haloConfigs.value.appConfig?.appInfo as { name ?: string } | undefined
return appInfo?.name || bloggerInfo.value.nickname || 'uni-halo'
})
/** 依赖插件(plugin-moments,参考 gallery 对象传参模式) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginMoments,
tips: '检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员',
tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetData()
}
})
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() {
if (await checkPluginAvailable())
handleGetData()
if (await checkPluginAvailable()) { handleGetData() }
}
/* ---------------- 状态 ---------------- */
@@ -70,6 +70,10 @@
videos ?: { id ?: string, url : string }[]
audios ?: { type ?: string, url : string }[]
spec : IMoment['spec'] & { newHtml ?: string }
year : string
month : string
day : string
weekend : string
}
const dataList = ref<MomentCard[]>([])
const isLoadMore = ref(false)
@@ -77,20 +81,33 @@
const videoContexts = ref<Record<string, UniApp.VideoContext | undefined>>({})
const currentVideoId = ref<string | null>(null)
/** 移除内容中的 tag 链接 */
function removeTagLinksCompletely(htmlString : string) : string {
const regex = /<a\b[^>]+class=(['"])[^'"]*\btag\b[^'"]*\1[^>]*>[\s\S]*?<\/a>/gi
return htmlString.replace(regex, '')
}
/** 瞬间项映射(spec.content.medium 拆分为 images/videos/audios + 内容 tag 清理 + 作者兜底) */
const WEEKDAY_TEXT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
function splitMomentDate(timeStr ?: string) {
const d = timeStr ? dayjs(timeStr) : null
if (!d || !d.isValid()) {
return { year: '', month: '', day: '', weekend: '' }
}
return {
year: `${d.year()}`,
month: `${d.month() + 1}`,
day: `${d.date()}`,
weekend: WEEKDAY_TEXT[d.day()],
}
}
function mapMomentItem(item : IMoment) : MomentCard {
const medium = (item.spec.content?.medium || [])
.map(x => ({ ...x, url: x.url || '' }))
const owner = item.owner
return {
...item,
// 无顶层 owner(如个别历史接口)时兜底为博主信息
...splitMomentDate(item.spec?.releaseTime),
owner: owner?.displayName
? owner
: { displayName: bloggerInfo.value.nickname || '', name: bloggerInfo.value.nickname || '', avatar: bloggerInfo.value.avatar },
@@ -107,7 +124,6 @@
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
if (calcAuditModeEnabled.value) {
// 审核模式:真实瞬间按 audit-data moments 过滤(数组顺序即展示顺序)
const auditMomentNames = appConfigStore.auditData.spec?.moments || []
try {
const res = await getMomentList({ page: 1, size: 0 })
@@ -117,12 +133,9 @@
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
const tempItems = filtered.map(mapMomentItem)
dataList.value = tempItems
nextTick(() => {
createVideoContexts(tempItems)
})
await sleep(600)
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh()
}
catch (err) {
@@ -133,7 +146,6 @@
return
}
uni.showLoading({ mask: true, title: t('common.loading') })
if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
}
@@ -151,11 +163,9 @@
dataList.value = isLoadMore.value
? dataList.value.concat(tempItems)
: tempItems
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
nextTick(() => {
createVideoContexts(tempItems)
})
await sleep(600)
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
}
catch (err) {
console.error(err)
@@ -163,46 +173,10 @@
loadMoreText.value = t('common.loadFailed')
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 500)
uni.stopPullDownRefresh()
}
}
/* ---------------- 视频互斥 ---------------- */
function createVideoContexts(list : { videos ?: { id ?: string }[] }[]) {
stopAllVideos()
list.map(item => item.videos || []).flat().forEach((item) => {
if (item.id) {
videoContexts.value[item.id] = uni.createVideoContext(`video_${item.id}`)
}
})
}
function stopAllVideos(excludesVideoId : string | null = null) {
Object.keys(videoContexts.value).forEach((videoId) => {
if (!excludesVideoId || excludesVideoId !== videoId) {
videoContexts.value[videoId]?.pause()
}
})
}
function onVideoPlay(videoId : string) {
currentVideoId.value = videoId
stopAllVideos(videoId)
}
function onVideoPause(videoId : string) {
if (currentVideoId.value === videoId) {
currentVideoId.value = null
}
}
function onVideoEnded() {
currentVideoId.value = null
}
/* ---------------- 交互 ---------------- */
function handlePreview(index : number, list : { url : string }[]) {
uni.previewImage({
@@ -220,12 +194,10 @@
})
}
/** 是否已收藏该瞬间(卡片收藏格高亮) */
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))
@@ -271,7 +243,8 @@
})
onReachBottom(() => {
if (!uniHaloPluginAvailable.value) { return }
if (!uniHaloPluginAvailable.value)
return
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
@@ -288,108 +261,105 @@
</script>
<template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page ">
<uh-navbar :use-back="false" default-title="我的日常" title-color="text-gray-900"></uh-navbar>
<view class="box-border min-h-screen w-screen flex flex-col bg-page">
<uh-navbar :use-back="false" default-title="我的日常" title-color="text-gray-900" />
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="handlePluginRefresh" />
<template v-else>
<!-- 加载失败(可重试) -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="60vh" @refresh="handleGetData" />
min-height="75vh" @refresh="handleGetData" />
<view v-else class="box-border flex flex-col gap-3 p-3 pt-0">
<!-- 瞬间卡片-->
<view v-for="moment in dataList" :key="moment.metadata.name"
class="uh-global-card-glass uh-shadow-xs overflow-hidden rounded-xl">
<!-- 作者 -->
<view class="box-border flex items-center px-4 pt-4">
<view class="flex-1 flex items-center">
<image class="avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full"
:src="checkAvatarUrl(moment.owner?.avatar || bloggerInfo.avatar)" mode="aspectFill" />
<view class="ml-3 flex flex-col">
<view class="text-sm text-gray-900 font-bold">
{{ moment.owner?.displayName || bloggerInfo.nickname }}
</view>
<view class="mt-0.5 text-xs text-gray-400">
{{ formatMomentTime(moment.spec.releaseTime) }}
<!-- 瞬间卡片 -->
<view v-for="moment in dataList" :key="moment.metadata.name" class="flex gap-x-2">
<view class="shrink-0 flex flex-col gap-y-2 w-13">
<view class="shrink-0 flex flex-col items-center font-bold">
<text
class="date-day text-xl text-primary leading-none">{{ moment.day }}/{{ moment.month }}</text>
<text class="date-year-month mt-2 text-sm text-gray-600">{{ moment.year }}</text>
<text class="date-weekend mt-1 text-xs text-gray-600">{{ moment.weekend }}</text>
</view>
<view class="flex-1 w-full flex flex-col items-center">
<view class="shrink-0 w-4 h-4 bg-primary rounded-full uh-global-card-glass"></view>
<view class="w-1 h-full flex-1 bg-primary uh-global-card-glass rounded-full border"></view>
</view>
</view>
<view class="uh-global-card-glass uh-shadow-xs flex-1 overflow-hidden rounded-xl">
<view class="box-border flex items-center px-4 pt-4">
<view class="flex flex-1 items-center">
<image class="avatar h-9 w-9 shrink-0 rounded-full"
:src="checkAvatarUrl(moment.owner?.avatar || bloggerInfo.avatar)"
mode="aspectFill" />
<view class="ml-2 flex flex-col">
<view class="text-sm text-gray-900 font-bold">
{{ moment.owner?.displayName || bloggerInfo.nickname }}
</view>
<view class="text-xs text-gray-400">
{{ formatMomentTime(moment.spec.releaseTime) }}
</view>
</view>
</view>
<view class="shrink-0">
<uh-button custom-class="!py-1 bg-secondary text-xs font-semibold"
@click="handleToMomentDetail(moment)">
详情
</uh-button>
</view>
</view>
<view class="shrink-0">
<uh-button custom-class="!py-1 bg-secondary font-semibold">详情</uh-button>
<!-- 正文 -->
<view class="box-border px-4 pt-3">
<view class="relative box-border rounded-lg bg-page p-3">
<mp-html lazy-load :domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
:content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true"
:show-language-name="true" copy-by-long-press />
</view>
</view>
</view>
<!-- 正文-->
<view class="box-border px-4 pt-3">
<view class="relative box-border bg-page p-3 rounded-lg">
<mp-html lazy-load :domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
:content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true"
:show-language-name="true" copy-by-long-press
@click.stop="handleToMomentDetail(moment)" />
<!-- 图片 -->
<view v-if="moment.images && moment.images.length !== 0"
class="box-border flex flex-wrap items-start px-3 pt-2">
<view v-for="(image, mediumIndex) in moment.images" :key="mediumIndex"
class="image-item box-border p-1"
:class="moment.images && moment.images.length === 1 ? 'h-32 w-full' : (moment.images && moment.images.length === 2 ? 'h-[250rpx] w-1/2' : 'h-20 w-1/3')">
<image mode="aspectFill" class="h-full w-full rounded-lg" :src="image.url"
@click="handlePreview(mediumIndex, moment.images || [])" />
</view>
</view>
</view>
<!-- 图片 -->
<view v-if="moment.images && moment.images.length !== 0"
class="box-border flex flex-wrap items-start px-3 pt-3">
<view v-for="(image, mediumIndex) in moment.images" :key="mediumIndex"
class="image-item box-border p-1"
:class="moment.images && moment.images.length === 1 ? 'h-[350rpx] w-full' : (moment.images && moment.images.length === 2 ? 'h-[250rpx] w-1/2' : 'h-[200rpx] w-1/3')">
<image mode="aspectFill" class="image-src h-full w-full rounded-lg" :src="image.url"
@click="handlePreview(mediumIndex, moment.images || [])" />
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
class="mt-3 box-border flex flex-wrap gap-2 px-4">
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
class="rounded-xl bg-secondary px-2 py-1 text-xs">
# {{ tag }}
</text>
</view>
</view>
<!-- 音频 -->
<view v-if="moment.audios && moment.audios.length !== 0"
class="box-border flex flex-col gap-3 px-4 pt-3">
<uh-audio-player v-for="audio in moment.audios" :key="audio.url" :src="audio.url"
:poster="bloggerInfo.avatar" :name="`来自${siteName}的声音`" :author="bloggerInfo.nickname" />
</view>
<!-- 视频 -->
<view v-if="moment.videos && moment.videos.length !== 0"
class="box-border flex flex-col gap-3 px-4 pt-3">
<video v-for="(video, index) in moment.videos" :id="`video_${video.id}`" :key="index"
class="video-src h-[400rpx] w-full rounded-xl" :src="video.url" :show-mute-btn="true"
:controls="true" :show-center-play-btn="true" :enable-progress-gesture="true"
@play="onVideoPlay(video.id || '')" @pause="onVideoPause(video.id || '')"
@ended="onVideoEnded" />
</view>
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
class="box-border px-4 mt-3 flex flex-wrap gap-2">
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
class="py-1 px-2 text-xs rounded-xl bg-secondary">
# {{ tag }}
</text>
</view>
<!-- (点赞/评论) -->
<view
class="mt-2 mb-1 box-border w-full flex items-center justify-between gap-x-12 border-t border-black/5 py-3 px-4 text-xs text-gray-400">
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 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">评论 {{ moment.stats.totalComment || 0 }}</text>
</view>
<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
class="mb-1 mt-2 box-border w-full flex items-center justify-between border-t border-black/5 px-4 py-3 text-xs text-gray-400">
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 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">评论 {{ moment.stats.totalComment || 0 }}</text>
</view>
<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>
</view>
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">
{{ loadMoreText }}
</view>