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 radius ?: string
/** 是否开启 wot 位置自适应圆角(bottom→上圆角 / center→四圆角等),与 radius 二选一 */ /** 是否开启 wot 位置自适应圆角(bottom→上圆角 / center→四圆角等),与 radius 二选一 */
round ?: boolean round ?: boolean
lazyRender ?: boolean
hideWhenClose?: boolean
/** 追加到面板的内联样式,如宽度:width:640rpx; */ /** 追加到面板的内联样式,如宽度:width:640rpx; */
customStyle ?: string customStyle ?: string
customClass? :string customClass? :string
@@ -43,6 +45,8 @@
safeAreaInsetBottom: false, safeAreaInsetBottom: false,
radius: '', radius: '',
round: false, round: false,
lazyRender: true,
hideWhenClose: true,
customStyle: '', customStyle: '',
customClass:'' customClass:''
}) })
@@ -66,10 +70,12 @@
:closable="closable" :closable="closable"
:modal="modal" :modal="modal"
:close-on-click-modal="closeOnClickModal" :close-on-click-modal="closeOnClickModal"
:hide-when-close="hideWhenClose"
:safe-area-inset-bottom="safeAreaInsetBottom" :safe-area-inset-bottom="safeAreaInsetBottom"
:round="round" :round="round"
:custom-style="panelStyle" :custom-style="panelStyle"
:custom-class="panelClass" :custom-class="panelClass"
:lazy-render="lazyRender"
@update:model-value="(value: boolean) => emit('update:modelValue', value)" @update:model-value="(value: boolean) => emit('update:modelValue', value)"
@close="emit('close')" @close="emit('close')"
@click-modal="emit('click-modal')" @click-modal="emit('click-modal')"
@@ -201,7 +201,7 @@
<template> <template>
<uh-glass-popup v-model="isShow" :z-index="100" position="bottom" <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="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"> <view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">申请收录小程序</text> <text class="text-md font-bold">申请收录小程序</text>
@@ -6,6 +6,7 @@
pluginId : string pluginId : string
errorText ?: string errorText ?: string
checking : boolean checking : boolean
customClass ?: string
}>(), { }>(), {
errorText: '', errorText: '',
}) })
@@ -16,7 +17,7 @@
/** 插件信息(未在清单中时兜底) */ /** 插件信息(未在清单中时兜底) */
const pluginInfo = computed(() => { const pluginInfo = computed(() => {
return NeedPlugins.get(props.pluginId) ?? {pluginId:props.pluginId, name: '未找到插件' } return NeedPlugins.get(props.pluginId) ?? { pluginId: props.pluginId, name: '未找到插件' }
}) })
function handleRefresh() { function handleRefresh() {
@@ -26,7 +27,9 @@
</script> </script>
<template> <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> <wd-icon class-prefix="uhemoji-icon" name="-cry" size="160rpx"></wd-icon>
@@ -46,8 +49,7 @@
<!-- 微信端客服会话只能由原生 button open-type="contact" 唤起,故此处不用 uh-button(view 实现) --> <!-- 微信端客服会话只能由原生 button open-type="contact" 唤起,故此处不用 uh-button(view 实现) -->
<button <button
class="uh-contact-btn bg-white py-2 px-4 !rounded-full text-black text-sm leading-none flex items-center justify-center" 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" open-type="contact" hover-class="none">提交反馈</button>
hover-class="none">提交反馈</button>
<!-- #endif --> <!-- #endif -->
</view> </view>
</view> </view>
@@ -59,5 +61,6 @@
.uh-contact-btn::after { .uh-contact-btn::after {
border: none; border: none;
} }
/* #endif */ /* #endif */
</style> </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>
+43 -40
View File
@@ -5,6 +5,7 @@
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo' import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting' import { useSettingStore } from '@/store/setting'
import { sleep } from '@/utils/common'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { NeedPluginIds } from '@/hooks/usePluginAvailable' import { NeedPluginIds } from '@/hooks/usePluginAvailable'
@@ -29,24 +30,37 @@
/** 站点 tab:PluginLinks */ /** 站点 tab:PluginLinks */
const { pluginId: sitePluginId, checking: siteChecking, tips: siteTips, available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable({ const { pluginId: sitePluginId, checking: siteChecking, tips: siteTips, available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginLinks, pluginId: NeedPluginIds.PluginLinks,
tips: '检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetLinkGroupData()
}
}) })
/** 小程序 tab:plugin-uni-halo */ /** 小程序 tab:plugin-uni-halo */
const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({ const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginUniHalo, pluginId: NeedPluginIds.PluginUniHalo,
tips: '检测到当前插件没有安装或者启用,无法使用小程序链接功能哦,请联系管理员', tips: '啊偶,功能正在维护中...', callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetMiniProgramLinks()
}
}) })
/** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleSitePluginRefresh() { async function handleSitePluginRefresh() {
if (await checkSitePluginAvailable()) if (await checkSitePluginAvailable()) { handleGetLinkGroupData() }
handleGetLinkGroupData()
} }
/** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleMiniPluginRefresh() { async function handleMiniPluginRefresh() {
if (await checkMiniPluginAvailable()) if (await checkMiniPluginAvailable()) { handleGetMiniProgramLinks() }
handleGetMiniProgramLinks()
} }
/* ---------------- tabs ---------------- */ /* ---------------- tabs ---------------- */
@@ -64,8 +78,7 @@
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab // 审核模式下小程序 tab 隐藏,强制停留在站点 tab
watch(() => appConfigStore.auditModeEnabled, (enabled) => { watch(() => appConfigStore.auditModeEnabled, (enabled) => {
if (enabled) if (enabled) { activeTabIndex.value = 0 }
activeTabIndex.value = 0
}) })
/* ==================== 站点 tab(plugin-links) ==================== */ /* ==================== 站点 tab(plugin-links) ==================== */
@@ -122,12 +135,11 @@
}, },
})) }))
dataList.value = dataList.value.concat(list) dataList.value = dataList.value.concat(list)
setTimeout(() => { await sleep(600)
updateSiteLoadingStatus( updateSiteLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
) )
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
}, 500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
@@ -135,9 +147,7 @@
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
setTimeout(() => {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 500)
} }
} }
@@ -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) ==================== */ /* ==================== 小程序 tab(plugin-uni-halo) ==================== */
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus() const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
@@ -193,20 +196,17 @@
try { try {
const res = await getMiniProgramLinkGroupedList() const res = await getMiniProgramLinkGroupedList()
miniGroups.value = res.data || [] miniGroups.value = res.data || []
setTimeout(() => { await sleep(600)
updateMiniLoadingStatus( updateMiniLoadingStatus(
miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
) )
}, 500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
updateMiniLoadingStatus(DataLoadingStatusEnum.Error) updateMiniLoadingStatus(DataLoadingStatusEnum.Error)
} }
finally { finally {
setTimeout(() => {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 500)
} }
} }
@@ -393,16 +393,17 @@
</view> </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" <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"> @click="toSubmitLinkPage">
<wd-icon name="edit" size="20px" color="#6b7280" /> <wd-icon name="edit" size="20px" color="#6b7280" />
</view> </view>
</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="relative w-full flex items-center justify-around box-border px-4 pt-4">
<view class="w-full flex flex-col gap-y-1"> <view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">站点详情</text> <text class="text-md font-bold">站点详情</text>
@@ -418,23 +419,24 @@
<view class="flex"> <view class="flex">
<image class="h-20 w-20 shrink-0 rounded-2xl uh-global-card-glass" <image class="h-20 w-20 shrink-0 rounded-2xl uh-global-card-glass"
:src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" /> :src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
<view class="ml-4 flex flex-1 flex-col gap-y-1 justify-center"> <view class="ml-4 flex flex-1 flex-col gap-y-1.5 justify-center">
<view class="text-lg text-gray-900 font-bold"> <view class="text-md text-gray-900 font-bold">
{{ detail.data.spec.displayName }} {{ detail.data.spec.displayName }}
</view> </view>
<view class="flex items-center gap-x-2"> <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
<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"> 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> </text>
</view> </view>
<view @click="handleCopyLink(detail.data)"> <view @click="handleCopyLink(detail.data)">
<text <text class="text-xs text-gray-900">{{ detail.data.spec.url }}</text>
class="text-xs text-gray-900">{{ detail.data.spec.url }}</text>
</view> </view>
</view> </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 || '这个博主很懒,没写简介~' }} {{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
</view> </view>
</scroll-view> </scroll-view>
@@ -457,11 +459,11 @@
<view v-else class="content flex flex-1 flex-col"> <view v-else class="content flex flex-1 flex-col">
<!-- 分组列表 --> <!-- 分组列表 -->
<view class="box-border flex-1 p-3"> <view class="box-border flex-1 p-3">
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8"> <view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-4">
<view class="mb-4 flex items-center"> <view class="mb-3 flex items-center">
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" /> <text class="mr-2 inline-block h-4 w-1 rounded-full bg-secondary" />
<text <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> <text class="ml-3 text-xs text-gray-400">{{ group.links.length }}</text>
</view> </view>
<view class="group-cards flex flex-col gap-4"> <view class="group-cards flex flex-col gap-4">
@@ -491,9 +493,9 @@
</view> </view>
<!-- 申请收录悬浮按钮 --> <!-- 申请收录悬浮按钮 -->
<view class="fixed bottom-10 right-4 z-50"> <view class="fixed bottom-8 right-3 z-50">
<view <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"> @click="handleOpenApply">
<text class="text-xs text-white">申请</text> <text class="text-xs text-white">申请</text>
</view> </view>
@@ -501,7 +503,8 @@
</view> </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="relative w-full flex items-center justify-around box-border px-4 pt-4">
<view class="w-full flex flex-col gap-y-1"> <view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">小程序详情</text> <text class="text-md font-bold">小程序详情</text>
+2 -10
View File
@@ -241,10 +241,8 @@
<template> <template>
<view class="uh-global-love-page box-border min-h-screen w-screen flex flex-col"> <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"/> <uh-navbar default-title="恋爱清单" title-color="text-love" back-class="text-love"/>
<!-- 粘性筛选区:参考投票页顶部胶囊设计,每个维度独立状态 -->
<wd-sticky> <wd-sticky>
<view class="box-border px-3 pb-1 pt-2"> <view class="box-border px-3 pb-1 pt-2">
<view class="box-border flex items-center justify-between gap-x-2"> <view class="box-border flex items-center justify-between gap-x-2">
@@ -263,16 +261,10 @@
</view> </view>
</wd-sticky> </wd-sticky>
<!-- 加载/错误/空占位(状态机) -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="60vh" empty-text="暂时还没有恋爱清单快去制定你们的恋爱清单吧~" @refresh="handleGetList" /> 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 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"> <block v-for="(item, index) in showList" :key="item.name">
<view <view
class="uh-global-card-glass uh-shadow-xs box-border w-full flex flex-col items-center rounded-xl p-3"> 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" <view v-if="item.open"
class="uh-global-card-glass mt-4 box-border w-full rounded-xl p-3 text-xs shadow-none"> 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 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>
<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 || '-' }} {{ item.content || '-' }}
</view> </view>
</view> </view>
+9 -1
View File
@@ -22,7 +22,15 @@
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({ const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginSearchWidget, pluginId: NeedPluginIds.PluginSearchWidget,
tips: '啊偶,当前功能未开放!', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleOnSearch()
}
}) })
async function handlePluginRefresh() { async function handlePluginRefresh() {
+99 -49
View File
@@ -10,7 +10,7 @@
definePage({ definePage({
style: { style: {
navigationBarTitleText: '偏好设置', navigationBarTitleText: '偏好设置',
navigationStyle: 'custom' navigationStyle: 'custom',
}, },
}) })
@@ -87,7 +87,7 @@
] ]
/* ---------------- 状态读取 ---------------- */ /* ---------------- 状态读取 ---------------- */
function valueOf(path : Path) : unknown { function prefValueOf(path : Path) : unknown {
return getByPath(settingStore.settings, path) return getByPath(settingStore.settings, path)
} }
@@ -133,11 +133,17 @@
settingStore.savePreference(buildPatch(path, null)) settingStore.savePreference(buildPatch(path, null))
} }
/* ---------------- 枚举底部弹层 ---------------- */ /* ---------------- 枚举底部弹层(uh-glass-popup + wd-picker-view) ---------------- */
const enumSheet = ref<{ show : boolean, def : PrefDef | null }>({ show: false, def: null }) const enumSheet = ref<{ show : boolean, def : PrefDef | null }>({ show: false, def: null })
/** 弹层内滚动中的临时选中值(单列;确认时才落库,取消不生效) */
const pickerValue = ref<(string | number)[]>([''])
function handleOpenEnum(def : PrefDef) { function handleOpenEnum(def : PrefDef) {
console.log('handleOpenEnum', def)
enumSheet.value = { show: true, def } enumSheet.value = { show: true, def }
// 打开时同步当前值(跟随站点默认 → 空串哨兵)
pickerValue.value = [isFollowing(def) ? '' : String(prefValueOf(def.path) ?? '')]
console.log('pickerValue', pickerValue.value)
} }
function handleCloseEnum() { function handleCloseEnum() {
@@ -157,37 +163,59 @@
handleCloseEnum() handleCloseEnum()
} }
/* ---------------- wd-picker 弹层数据 ---------------- */ /* ---------------- wd-picker-view 弹层数据 ---------------- */
/** 枚举弹层列(首项「跟随站点默认」,空串哨兵映射 null) */ /** 枚举弹层列(首项「跟随站点默认」,空串哨兵映射 null) */
const enumColumns = computed(() => { const enumColumns = computed(() => {
const def = enumSheet.value.def const def = enumSheet.value.def
if (!def) if (!def) { return [] }
return []
return [ return [
{ label: '跟随站点默认', value: '' }, { label: '跟随站点默认', value: '' },
...(def.options || []).map(opt => ({ label: opt.label, value: opt.value })), ...(def.options || []).map(opt => ({ label: opt.label, value: opt.value })),
] ]
}) })
/** 当前选中值(单列;跟随站点默认时为空串) */ /** wd-picker-view 滚动变化:更新临时选中值(未确认不落库) */
const enumValue = computed(() => { function handlePickerChange(payload : { selectedValues : (string | number)[] }) {
const def = enumSheet.value.def pickerValue.value = payload.selectedValues
if (!def) }
return ['']
return [isFollowing(def) ? '' : String(valueOf(def.path) ?? '')]
})
/** wd-picker 确认:空串哨兵还原为「跟随站点默认」 */ /** 确认:空串哨兵还原为「跟随站点默认」 */
function handlePickerConfirm(payload: { value: (string | number)[] }) { function handlePickerConfirm() {
const picked = String(payload.value[0] ?? '') const picked = String(pickerValue.value[0] ?? '')
handleChooseEnum(picked === '' ? null : picked) handleChooseEnum(picked === '' ? null : picked)
} }
/** 取消:不落库,直接关闭 */
function handlePickerCancel() {
handleCloseEnum()
}
/** 当前枚举项是否处于「跟随站点默认」 */ /** 当前枚举项是否处于「跟随站点默认」 */
function isFollowing(def : PrefDef) : boolean { function isFollowing(def : PrefDef) : boolean {
return !isOverridden(def.path) 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() { function handleResetAll() {
uni.showModal({ uni.showModal({
@@ -211,10 +239,10 @@
<template> <template>
<view class="box-border min-h-screen bg-page"> <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"> <view class="flex flex-col gap-y-3">
<uh-section-title> <uh-section-title>
@@ -224,14 +252,14 @@
</template> </template>
</uh-section-title> </uh-section-title>
<view class="uh-global-card-glass overflow-hidden rounded-2xl"> <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="pick-row flex items-center justify-between px-4 py-4"
:class="index < layoutPrefs.length - 1 ? 'border-b border-black/5' : ''" :class="index < layoutRows.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(def)"> @click="handleOpenEnum(row)">
<view class="row-left flex flex-col gap-1"> <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"> <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 <view v-else
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义 已自定义
@@ -239,8 +267,7 @@
</view> </view>
</view> </view>
<view class="row-value flex items-center gap-2"> <view class="row-value flex items-center gap-2">
<text <text class="value-text text-[26rpx] text-gray-400">{{ row.displayValue }}</text>
class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" /> <wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
</view> </view>
</view> </view>
@@ -255,64 +282,87 @@
</template> </template>
</uh-section-title> </uh-section-title>
<view class="setting-sheet uh-global-card-glass overflow-hidden rounded-2xl"> <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" <view v-if="row.kind === 'bool'" class="switch-row flex items-center justify-between px-4 py-4"
:class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''"> :class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''">
<view class="row-left flex flex-col gap-1"> <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"> <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> <template v-else>
<view <view
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义 已自定义
</view> </view>
<text class="revert-text text-2xs text-gray-400 underline" <text class="revert-text text-2xs text-gray-400 underline"
@click.stop="handleRevert(def.path)">恢复默认</text> @click.stop="handleRevert(row.path)">
恢复默认
</text>
</template> </template>
</view> </view>
</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>
<!-- 枚举选择(指示器位置) --> <!-- 枚举选择(指示器位置) -->
<view v-else class="pick-row flex items-center justify-between px-4 py-4" <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' : ''" :class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''"
@click="handleOpenEnum(def)"> @click="handleOpenEnum(row)">
<view class="row-left flex flex-col gap-1"> <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"> <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> <template v-else>
<view <view
class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
已自定义 已自定义
</view> </view>
<text class="revert-text text-2xs text-gray-400 underline" <text class="revert-text text-2xs text-gray-400 underline"
@click.stop="handleRevert(def.path)">恢复默认</text> @click.stop="handleRevert(row.path)">
恢复默认
</text>
</template> </template>
</view> </view>
</view> </view>
<view class="row-value flex items-center gap-2"> <view class="row-value flex items-center gap-2">
<text <text class="value-text text-[26rpx] text-gray-400">{{ row.displayValue }}</text>
class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" /> <wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
</view> </view>
</view> </view>
</template> </template>
</view> </view>
</view> </view>
<!-- 底部操作栏(玻璃悬浮) --> <!-- 底部操作栏-->
<view class="box-border w-full px-2"> <view class="box-border w-full">
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full" <uh-button custom-class="uh-global-card-glass py-2 !rounded-xl" @click="handleResetAll">
@click="handleResetAll">恢复默认</uh-button> 恢复默认
</uh-button>
</view> </view>
</view> </view>
<!-- 枚举选择弹层(wd-picker 自带底部弹层与工具栏) -->
<wd-picker <!-- 枚举选择弹层(uh-glass-popup + wd-picker-view,底部取消/确认,参考 uh-album-photo-viewer 布局) -->
v-model:visible="enumSheet.show" :title="enumSheet.def?.label || ''" :columns="enumColumns" <uh-glass-popup v-model="enumSheet.show" :hide-when-close="false" position="bottom" custom-class="rounded-xl">
:model-value="enumValue" confirm-button-text="确定" cancel-button-text="取消" <view class="box-border px-4 py-4">
@confirm="handlePickerConfirm" <!-- 标题 -->
/> <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> </view>
</template> </template>
+11 -11
View File
@@ -4,6 +4,8 @@
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo' import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url' import { checkImageUrl } from '@/utils/url'
import { usePluginAvailable } from '@/hooks/usePluginAvailable'
import { sleep } from '@/utils/common'
import { t } from '@/locale' import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { IPhoto, IPhotoGroup } from '@/api/types/halo' import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
@@ -25,7 +27,7 @@
/** 依赖插件(PluginPhotos) */ /** 依赖插件(PluginPhotos) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({ const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: 'PluginPhotos', pluginId: 'PluginPhotos',
tips: '很抱歉,功能正在维护中...', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => { callback: (isAvailable) => {
if (!isAvailable) { return } if (!isAvailable) { return }
uni.pageScrollTo({ uni.pageScrollTo({
@@ -114,6 +116,7 @@
? dataList.value.concat(list) ? dataList.value.concat(list)
: list : list
} }
await sleep(600)
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success) updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore') loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
} }
@@ -123,11 +126,7 @@
loadMoreText.value = t('common.loadFailed') loadMoreText.value = t('common.loadFailed')
} }
finally { finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
lock.value = false
}, 500)
} }
} }
@@ -191,29 +190,30 @@
</script> </script>
<template> <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-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" <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="checkPluginAvailable" /> :checking="checking" @on-refresh="checkPluginAvailable" />
<template v-else> <template v-else>
<wd-sticky v-if="category.list.length!==0" class="w-full"> <wd-sticky v-if="category.list.length!==0" class="w-full">
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-2"> <scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-2">
<view v-for="(cate, index) in category.list" :key="cate.spec.displayName" <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 }" :class="{ 'bg-primary text-gray-900 font-bold': index === category.activeIndex }"
@click="handleGetDataByCategory(index, cate)"> @click="handleGetDataByCategory(index, cate)">
{{ cate.spec.displayName }} <text {{ cate.spec.displayName }}
v-if="cate.spec.displayName!=='全部'">({{ cate.status?.photoCount ?? 0 }})</text> <text v-if="cate.spec.displayName!=='全部'">
({{ cate.status?.photoCount ?? 0 }})
</text>
</view> </view>
</scroll-view> </scroll-view>
</wd-sticky> </wd-sticky>
<!-- 加载/错误占位 -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
@refresh="handleGetCategory" /> @refresh="handleGetCategory" />
<!-- 内容区域 -->
<view v-else class="box-border w-full p-3"> <view v-else class="box-border w-full p-3">
<view class="grid grid-cols-2 gap-2.5"> <view class="grid grid-cols-2 gap-2.5">
<view v-for="(item, index) in dataList" :key="index" <view v-for="(item, index) in dataList" :key="index"
+78 -108
View File
@@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 瞬间页(源自旧项目 pages/tabbar/moments/moments.vue,新建复刻)
* 功能:瞬间卡片列表(头像/内容/图片/音频/视频/标签) + 分页加载
* 设计:社交信息流(实心白纸卡 + 着色昵称 + 朋友圈式不缩进正文),区别于工具页的玻璃拟态
*/
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import { getMomentList } from '@/api/halo' import { getMomentList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { NeedPluginIds } from '@/hooks/usePluginAvailable' import { NeedPluginIds } from '@/hooks/usePluginAvailable'
@@ -13,8 +9,8 @@
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url' import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import { buildMomentFavoriteItem } from '@/utils/favorite' import { buildMomentFavoriteItem } from '@/utils/favorite'
import { generateUUID } from '@/utils/uuid' import { generateUUID } from '@/utils/uuid'
import { sleep } from '@/utils/common'
import { formatTime } from '@/utils/formatTime' import { formatTime } from '@/utils/formatTime'
import { randomTagColor } from '@/utils/random'
import { t } from '@/locale' import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { markdownConfig } from '@/config/markdown' import { markdownConfig } from '@/config/markdown'
@@ -42,22 +38,26 @@
} }
}) })
/** 站点名称(原 startConfig.title 已随启动页下线,改读 appConfig.appInfo.name) */
const siteName = computed(() => { const siteName = computed(() => {
const appInfo = haloConfigs.value.appConfig?.appInfo as { name ?: string } | undefined const appInfo = haloConfigs.value.appConfig?.appInfo as { name ?: string } | undefined
return appInfo?.name || bloggerInfo.value.nickname || 'uni-halo' return appInfo?.name || bloggerInfo.value.nickname || 'uni-halo'
}) })
/** 依赖插件(plugin-moments,参考 gallery 对象传参模式) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({ const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginMoments, pluginId: NeedPluginIds.PluginMoments,
tips: '检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetData()
}
}) })
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() { async function handlePluginRefresh() {
if (await checkPluginAvailable()) if (await checkPluginAvailable()) { handleGetData() }
handleGetData()
} }
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
@@ -70,6 +70,10 @@
videos ?: { id ?: string, url : string }[] videos ?: { id ?: string, url : string }[]
audios ?: { type ?: string, url : string }[] audios ?: { type ?: string, url : string }[]
spec : IMoment['spec'] & { newHtml ?: string } spec : IMoment['spec'] & { newHtml ?: string }
year : string
month : string
day : string
weekend : string
} }
const dataList = ref<MomentCard[]>([]) const dataList = ref<MomentCard[]>([])
const isLoadMore = ref(false) const isLoadMore = ref(false)
@@ -77,20 +81,33 @@
const videoContexts = ref<Record<string, UniApp.VideoContext | undefined>>({}) const videoContexts = ref<Record<string, UniApp.VideoContext | undefined>>({})
const currentVideoId = ref<string | null>(null) const currentVideoId = ref<string | null>(null)
/** 移除内容中的 tag 链接 */
function removeTagLinksCompletely(htmlString : string) : string { function removeTagLinksCompletely(htmlString : string) : string {
const regex = /<a\b[^>]+class=(['"])[^'"]*\btag\b[^'"]*\1[^>]*>[\s\S]*?<\/a>/gi const regex = /<a\b[^>]+class=(['"])[^'"]*\btag\b[^'"]*\1[^>]*>[\s\S]*?<\/a>/gi
return htmlString.replace(regex, '') 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 { function mapMomentItem(item : IMoment) : MomentCard {
const medium = (item.spec.content?.medium || []) const medium = (item.spec.content?.medium || [])
.map(x => ({ ...x, url: x.url || '' })) .map(x => ({ ...x, url: x.url || '' }))
const owner = item.owner const owner = item.owner
return { return {
...item, ...item,
// 无顶层 owner(如个别历史接口)时兜底为博主信息 ...splitMomentDate(item.spec?.releaseTime),
owner: owner?.displayName owner: owner?.displayName
? owner ? owner
: { displayName: bloggerInfo.value.nickname || '', name: bloggerInfo.value.nickname || '', avatar: bloggerInfo.value.avatar }, : { displayName: bloggerInfo.value.nickname || '', name: bloggerInfo.value.nickname || '', avatar: bloggerInfo.value.avatar },
@@ -107,7 +124,6 @@
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
// 审核模式:真实瞬间按 audit-data moments 过滤(数组顺序即展示顺序)
const auditMomentNames = appConfigStore.auditData.spec?.moments || [] const auditMomentNames = appConfigStore.auditData.spec?.moments || []
try { try {
const res = await getMomentList({ page: 1, size: 0 }) 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)) filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
const tempItems = filtered.map(mapMomentItem) const tempItems = filtered.map(mapMomentItem)
dataList.value = tempItems dataList.value = tempItems
nextTick(() => { await sleep(600)
createVideoContexts(tempItems)
})
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success) updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = t('common.noMore') loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
} }
catch (err) { catch (err) {
@@ -133,7 +146,6 @@
return return
} }
uni.showLoading({ mask: true, title: t('common.loading') })
if (!isLoadMore.value) { if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading) updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
@@ -151,11 +163,9 @@
dataList.value = isLoadMore.value dataList.value = isLoadMore.value
? dataList.value.concat(tempItems) ? dataList.value.concat(tempItems)
: tempItems : tempItems
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
nextTick(() => { await sleep(600)
createVideoContexts(tempItems) updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
})
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
@@ -163,46 +173,10 @@
loadMoreText.value = t('common.loadFailed') loadMoreText.value = t('common.loadFailed')
} }
finally { finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 500)
} }
} }
/* ---------------- 视频互斥 ---------------- */
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 }[]) { function handlePreview(index : number, list : { url : string }[]) {
uni.previewImage({ uni.previewImage({
@@ -220,12 +194,10 @@
}) })
} }
/** 是否已收藏该瞬间(卡片收藏格高亮) */
function isMomentFavorite(moment : IMoment) : boolean { function isMomentFavorite(moment : IMoment) : boolean {
return favoritesStore.isFavorite('moment', moment.metadata.name) return favoritesStore.isFavorite('moment', moment.metadata.name)
} }
/** 切换收藏(收藏/取消),收藏时按当前卡片内容生成快照入库 */
function handleToggleMomentFavorite(moment : MomentCard) { function handleToggleMomentFavorite(moment : MomentCard) {
if (!moment) { return } if (!moment) { return }
const favorited = favoritesStore.toggle(buildMomentFavoriteItem(moment)) const favorited = favoritesStore.toggle(buildMomentFavoriteItem(moment))
@@ -271,7 +243,8 @@
}) })
onReachBottom(() => { onReachBottom(() => {
if (!uniHaloPluginAvailable.value) { return } if (!uniHaloPluginAvailable.value)
return
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') }) uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return return
@@ -288,92 +261,87 @@
</script> </script>
<template> <template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page "> <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> <uh-navbar :use-back="false" default-title="我的日常" title-color="text-gray-900" />
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips" <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="handlePluginRefresh" /> :checking="checking" @on-refresh="handlePluginRefresh" />
<template v-else> <template v-else>
<!-- 加载失败(可重试) -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" <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-else class="box-border flex flex-col gap-3 p-3 pt-0">
<!-- 瞬间卡片 -->
<!-- 瞬间卡片--> <view v-for="moment in dataList" :key="moment.metadata.name" class="flex gap-x-2">
<view v-for="moment in dataList" :key="moment.metadata.name" <view class="shrink-0 flex flex-col gap-y-2 w-13">
class="uh-global-card-glass uh-shadow-xs overflow-hidden rounded-xl"> <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="box-border flex items-center px-4 pt-4">
<view class="flex-1 flex items-center"> <view class="flex flex-1 items-center">
<image class="avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full" <image class="avatar h-9 w-9 shrink-0 rounded-full"
:src="checkAvatarUrl(moment.owner?.avatar || bloggerInfo.avatar)" mode="aspectFill" /> :src="checkAvatarUrl(moment.owner?.avatar || bloggerInfo.avatar)"
<view class="ml-3 flex flex-col"> mode="aspectFill" />
<view class="ml-2 flex flex-col">
<view class="text-sm text-gray-900 font-bold"> <view class="text-sm text-gray-900 font-bold">
{{ moment.owner?.displayName || bloggerInfo.nickname }} {{ moment.owner?.displayName || bloggerInfo.nickname }}
</view> </view>
<view class="mt-0.5 text-xs text-gray-400"> <view class="text-xs text-gray-400">
{{ formatMomentTime(moment.spec.releaseTime) }} {{ formatMomentTime(moment.spec.releaseTime) }}
</view> </view>
</view> </view>
</view> </view>
<view class="shrink-0"> <view class="shrink-0">
<uh-button custom-class="!py-1 bg-secondary font-semibold">详情</uh-button> <uh-button custom-class="!py-1 bg-secondary text-xs font-semibold"
@click="handleToMomentDetail(moment)">
详情
</uh-button>
</view> </view>
</view> </view>
<!-- 正文--> <!-- 正文 -->
<view class="box-border px-4 pt-3"> <view class="box-border px-4 pt-3">
<view class="relative box-border bg-page p-3 rounded-lg"> <view class="relative box-border rounded-lg bg-page p-3">
<mp-html lazy-load :domain="markdownConfig.domain ?? ''" <mp-html lazy-load :domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif" scroll-table selectable :loading-img="markdownConfig.loadingGif" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle" :tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
:content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true" :content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true"
:show-language-name="true" copy-by-long-press :show-language-name="true" copy-by-long-press />
@click.stop="handleToMomentDetail(moment)" />
</view> </view>
</view> </view>
<!-- 图片 --> <!-- 图片 -->
<view v-if="moment.images && moment.images.length !== 0" <view v-if="moment.images && moment.images.length !== 0"
class="box-border flex flex-wrap items-start px-3 pt-3"> class="box-border flex flex-wrap items-start px-3 pt-2">
<view v-for="(image, mediumIndex) in moment.images" :key="mediumIndex" <view v-for="(image, mediumIndex) in moment.images" :key="mediumIndex"
class="image-item box-border p-1" 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')"> :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="image-src h-full w-full rounded-lg" :src="image.url" <image mode="aspectFill" class="h-full w-full rounded-lg" :src="image.url"
@click="handlePreview(mediumIndex, moment.images || [])" /> @click="handlePreview(mediumIndex, moment.images || [])" />
</view> </view>
</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" <view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
class="box-border px-4 mt-3 flex flex-wrap gap-2"> class="mt-3 box-border flex flex-wrap gap-2 px-4">
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex" <text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
class="py-1 px-2 text-xs rounded-xl bg-secondary"> class="rounded-xl bg-secondary px-2 py-1 text-xs">
# {{ tag }} # {{ tag }}
</text> </text>
</view> </view>
<!-- (点赞/评论) --> <!-- (点赞/评论) -->
<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"> 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"> <view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text> <text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text>
@@ -385,11 +353,13 @@
<view class="flex items-center gap-x-1" @click.stop="handleToggleMomentFavorite(moment)"> <view class="flex items-center gap-x-1" @click.stop="handleToggleMomentFavorite(moment)">
<wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
<text class="text-sm text-gray-600" <text class="text-sm text-gray-600"
:style="isMomentFavorite(moment) ? { color: '#ffb300' } : ''">{{ isMomentFavorite(moment) ? '已收藏' : '收藏' }}</text> :style="isMomentFavorite(moment) ? { color: '#ffb300' } : ''">
{{ isMomentFavorite(moment) ? '已收藏' : '收藏' }}
</text>
</view>
</view> </view>
</view> </view>
</view> </view>
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500"> <view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">
{{ loadMoreText }} {{ loadMoreText }}
</view> </view>