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.  新增自定义导航栏适配,修复导航栏层级与返回按钮逻辑
3.  调整列表页面布局,统一添加box-border与间距规范
4.  优化友链弹窗与联系页面样式,修复文案与交互细节
5.  重构关于与免责声明页面,适配动态站点信息
6.  统一瞬间页面与投票页面的UI风格与代码结构
This commit is contained in:
小莫唐尼
2026-09-08 16:04:49 +08:00
parent 7b79a2c5a3
commit d8d08f9689
14 changed files with 1031 additions and 1060 deletions
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { checkUrl } from '@/utils/url'
import { useAppConfigStore } from '@/store/appConfig'
import type { DataLoadingStatus } from '@/hooks/useDataLoading' import type { DataLoadingStatus } from '@/hooks/useDataLoading'
interface IProps { interface IProps {
@@ -31,6 +33,16 @@
const emit = defineEmits<{ (e : 'refresh') : void }>() const emit = defineEmits<{ (e : 'refresh') : void }>()
const appConfigStore = useAppConfigStore()
const appInfo = computed(() => {
const _appInfo = (appConfigStore.configs?.appConfig?.appInfo as any)
return {
name: _appInfo.name ?? 'UniHalo',
logo: checkUrl(_appInfo?.logo ?? 'https://uni-halo.925i.cn/logo.png')
}
})
const isLoading = computed(() => props.loadingStatus === 'loading') const isLoading = computed(() => props.loadingStatus === 'loading')
const statusScene = computed(() => { const statusScene = computed(() => {
@@ -64,9 +76,13 @@
</script> </script>
<template> <template>
<view class="w-full flex flex-col items-center justify-center gap-y-4 text-sm" <view class="relative w-full flex flex-col items-center justify-center gap-y-4 text-sm"
:style="{ minHeight: props.minHeight }"> :style="{ minHeight: props.minHeight }">
<view class="scene relative h-[250rpx] w-[250rpx] flex items-center justify-center"
<!-- logo背景 -->
<image v-if="false" :src="appInfo.logo" class="absolute left-1/2 top-1/2 -translate-1/2 z-0 opacity-10 w-46 h-46"></image>
<view class="scene relative z-1 h-[250rpx] w-[250rpx] flex items-center justify-center"
:class="statusScene.stageClass"> :class="statusScene.stageClass">
<view class="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" /> <view class="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" />
<view class="deco-dot dot-a absolute rounded-full" /> <view class="deco-dot dot-a absolute rounded-full" />
@@ -79,7 +95,7 @@
</view> </view>
<!-- 文案区 --> <!-- 文案区 -->
<view class="flex flex-col items-center"> <view class="relative z-2 flex flex-col items-center">
<view class="flex items-center justify-center text-sm font-bold" :class="statusScene.mainTextClass"> <view class="flex items-center justify-center text-sm font-bold" :class="statusScene.mainTextClass">
<text>{{ statusScene.mainText }}</text> <text>{{ statusScene.mainText }}</text>
<view v-if="isLoading" class="ml-1 flex items-end gap-1"> <view v-if="isLoading" class="ml-1 flex items-end gap-1">
+3 -3
View File
@@ -87,11 +87,11 @@
<template> <template>
<view class="w-full box-border"> <view class="w-full box-border">
<view class="box-border pt-safe w-full fixed left-0 top-0 z-50" :class="customCalss" :style="[customStyle]"> <view class="box-border pt-safe w-full fixed left-0 top-0 z-100" :class="customCalss" :style="[customStyle]">
<view class="w-full h-[46px] flex items-center gap-x-4 box-border px-3 backdrop-blur-[2rpx]"> <view class="w-full h-[46px] flex items-center gap-x-4 box-border px-3 backdrop-blur-[2rpx]">
<!-- 左边 --> <!-- 左边 -->
<view class="shrink-0" @click="handleBack()"> <view class="shrink-0 min-w-18" @click="handleBack()">
<view <view v-if="props.useBack"
class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-gray-900 text-sm"> class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-gray-900 text-sm">
<wd-icon name="arrow-left" size="32rpx"></wd-icon> <wd-icon name="arrow-left" size="32rpx"></wd-icon>
<view class="w-[1px] h-4 bg-white/60" /> <view class="w-[1px] h-4 bg-white/60" />
+1 -5
View File
@@ -1,8 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 投票卡片(源自旧项目 components/vote-card,新建复刻)
* 纯展示卡片:列表接口一次返回,页面加工后传入,点击跳转详情
*/
import { formatTime as formatTimeUtil } from '@/utils/formatTime' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
interface IVoteCardOption { interface IVoteCardOption {
@@ -50,7 +46,7 @@ function formatTime(date?: string, fmt = 'yyyy-MM-dd HH:mm'): string {
</script> </script>
<template> <template>
<view class="uh-vote-card uh-global-card-glass box-border w-full rounded-2xl p-4" @click="handleToDetail"> <view class="uh-global-card-glass box-border w-full rounded-2xl p-4" @click="handleToDetail">
<!-- 头部:类型/状态/已投票 --> <!-- 头部:类型/状态/已投票 -->
<view class="flex items-center justify-between"> <view class="flex items-center justify-between">
<view class="flex flex-wrap items-center gap-1"> <view class="flex flex-wrap items-center gap-1">
+88 -82
View File
@@ -1,93 +1,99 @@
<script lang="ts" setup> <script lang="ts" setup>
/** import { onLoad } from '@dcloudio/uni-app'
* 关于项目页(源自旧项目 pagesA/about,新建复刻) import { useAppConfigStore } from '@/store/appConfig'
* 数字名片式设计:Hero 名片卡(渐变光斑透卡 + 徽章) + 彩色瓦片链接列表,点击复制 import { checkUrl } from '@/utils/url'
*/
import { onLoad } from '@dcloudio/uni-app'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '关于项目', navigationBarTitleText: '关于项目',
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const links = [ const appConfigStore = useAppConfigStore()
{ title: '开源组织', value: '巷子工坊', copy: 'https://www.ialley.cn', tip: '巷子工坊官网已复制成功!', tileColor: '#5c6bc0', tileLetter: '组' },
{ title: '开源作者', value: '小莫唐尼', copy: 'https://www.xiaoxiaomo.cn', tip: '作者主页地址已复制', tileColor: '#26a69a', tileLetter: '作' }, const appInfo = computed(() => {
{ title: '作者博客', value: 'https://blog.xiaoxiaomo.cn', copy: 'https://blog.xiaoxiaomo.cn', tip: '作者博客地址已复制', tileColor: '#7e57c2', tileLetter: '博' }, const _appInfo = (appConfigStore.configs?.appConfig?.appInfo as any)
{ title: '文档地址', value: 'https://uni-halo.925i.cn', copy: 'https://uni-halo.925i.cn', tip: '项目码云仓库已复制', tileColor: '#039be5', tileLetter: '文' }, return {
{ title: '码云仓库', value: 'https://gitee.com/ialley-workshop-open/uni-halo', copy: 'https://gitee.com/ialley-workshop-open/uni-halo', tip: '码云仓库地址已复制', tileColor: '#c71d23', tileLetter: '码' }, name: _appInfo.name ?? 'UniHalo',
{ title: 'Github', value: 'https://github.com/ialley-workshop-open/uni-halo', copy: 'https://github.com/ialley-workshop-open/uni-halo', tip: 'Github地址已复制', tileColor: '#24292f', tileLetter: 'G' }, logo: checkUrl(_appInfo?.logo ?? 'https://uni-halo.925i.cn/logo.png')
] }
})
function copyText(content: string, tips: string) { const links = [
uni.setClipboardData({ { title: '开源组织', value: '巷子工坊', copy: 'https://www.ialley.cn', tip: '巷子工坊官网已复制成功!', tileColor: '#5c6bc0', tileLetter: '组' },
data: content, { title: '开源作者', value: '小莫唐尼', copy: 'https://www.xiaoxiaomo.cn', tip: '作者主页地址已复制', tileColor: '#26a69a', tileLetter: '作' },
showToast: false, { title: '作者博客', value: 'https://blog.xiaoxiaomo.cn', copy: 'https://blog.xiaoxiaomo.cn', tip: '作者博客地址已复制', tileColor: '#7e57c2', tileLetter: '博' },
success: () => { { title: '文档地址', value: 'https://uni-halo.925i.cn', copy: 'https://uni-halo.925i.cn', tip: '项目码云仓库已复制', tileColor: '#039be5', tileLetter: '文' },
uni.showToast({ icon: 'none', title: tips }) { title: '码云仓库', value: 'https://gitee.com/ialley-workshop-open/uni-halo', copy: 'https://gitee.com/ialley-workshop-open/uni-halo', tip: '码云仓库地址已复制', tileColor: '#c71d23', tileLetter: '码' },
}, { title: 'Github', value: 'https://github.com/ialley-workshop-open/uni-halo', copy: 'https://github.com/ialley-workshop-open/uni-halo', tip: 'Github地址已复制', tileColor: '#24292f', tileLetter: 'G' },
}) ]
}
onLoad(() => { function copyText(content : string, tips : string) {
uni.setNavigationBarTitle({ title: '关于项目' }) uni.setClipboardData({
}) data: content,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: tips })
},
})
}
onLoad(() => {
uni.setNavigationBarTitle({ title: '关于项目' })
})
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-6"> <view class="box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-2">
<!-- 自定义导航 --> <!-- 自定义导航 -->
<uh-navbar default-title="关于项目" title-color="text-gray-900" /> <uh-navbar default-title="关于项目" title-color="text-gray-900" />
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) --> <view class="relative">
<view class="hero-wrap relative"> <view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" /> <view
<view class="absolute top-[150rpx] h-[180rpx] w-[180rpx] rounded-full bg-[rgba(215,249,76,0.45)] -left-10" /> class="absolute top-[150rpx] h-[180rpx] w-[180rpx] rounded-full bg-[rgba(215,249,76,0.45)] -left-10" />
<view class="hero-card uh-global-card-glass relative flex flex-col items-center rounded-3xl px-6 pb-7 pt-10"> <view class="uh-global-card-glass relative flex flex-col items-center rounded-2xl px-6 pb-7 pt-10">
<image class="logo-img h-[140rpx] w-[140rpx] rounded-3xl shadow-lg" src="https://uni-halo.925i.cn/logo.png" mode="aspectFill" /> <image class="h-[140rpx] w-[140rpx] rounded-3xl shadow-lg" :src="appInfo.logo" mode="aspectFill" />
<view class="mt-4 text-[40rpx] text-gray-900 font-bold"> <view class="mt-4 text-xl text-gray-900 font-bold">
uni-halo {{appInfo.name}}
</view> </view>
<view class="mt-3 flex items-center gap-2"> <view class="mt-3 flex items-center gap-2">
<text class="rounded-full bg-secondary px-3 py-1 text-[20rpx] text-[#4d7c0f]">AGPL-3.0 开源协议</text> <text class="rounded-full bg-secondary px-3 py-1 text-[20rpx] text-[#4d7c0f]">AGPL-3.0 开源协议</text>
<text class="rounded-full bg-[#f6f3ee] px-3 py-1 text-[20rpx] text-gray-500">uni-app × Halo</text> <text class="rounded-full bg-[#f6f3ee] px-3 py-1 text-[20rpx] text-gray-500">UniApp × Halo</text>
</view> </view>
<view class="mt-4 text-center text-[24rpx] text-gray-500 leading-relaxed"> <view class="mt-4 text-center text-xs text-gray-500 leading-relaxed">
基于 uni-app 打造的 Halo 博客跨端客户端 基于 uni-app 打造的 Halo 博客跨端客户端
</view> </view>
</view> </view>
</view> </view>
<!-- 相关链接 --> <!-- 相关链接 -->
<uh-section-title class="mb-3 mt-6 text-[30rpx]"> <uh-section-title class="mb-3 mt-6">
相关链接 相关链接
</uh-section-title> </uh-section-title>
<view class="link-list uh-global-card-glass overflow-hidden rounded-2xl"> <view class="link-list uh-global-card-glass overflow-hidden rounded-2xl">
<view <view v-for="(link, index) in links" :key="link.title" class="link-item flex items-center gap-3 px-4 py-4"
v-for="(link, index) in links" :class="index < links.length - 1 ? 'border-b border-black/5' : ''"
:key="link.title" @click="copyText(link.copy, link.tip)">
class="link-item flex items-center gap-3 px-4 py-4" <view
:class="index < links.length - 1 ? 'border-b border-black/5' : ''" class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5"
@click="copyText(link.copy, link.tip)" :style="{ backgroundColor: `${link.tileColor}1A` }">
> <text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text>
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5" :style="{ backgroundColor: `${link.tileColor}1A` }"> </view>
<text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text> <view class="min-w-0 flex flex-1 flex-col">
</view> <text class="text-[28rpx] text-gray-900 font-bold">{{ link.title }}</text>
<view class="min-w-0 flex flex-1 flex-col"> <view class="mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
<text class="text-[28rpx] text-gray-900 font-bold">{{ link.title }}</text> {{ link.value }}
<view class="mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400"> </view>
{{ link.value }} </view>
</view> <wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" />
</view> </view>
<wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" /> </view>
</view>
</view>
<!-- 版权 --> <!-- 版权 -->
<view class="copyright mt-auto pt-8 text-center text-[22rpx] text-gray-400"> <view class="copyright mt-auto pt-8 text-center text-[22rpx] text-gray-400">
<view> 2022 uni-halo 开源项目丨巷子工坊@小莫唐尼 </view> <view> 2022 uni-halo 开源项目丨巷子工坊@小莫唐尼 </view>
</view> </view>
</view> </view>
</template> </template>
+6 -10
View File
@@ -1,8 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 联系博主页(源自旧项目 pagesA/contact,新建复刻)
* 数字名片式设计:Hero 名片卡(渐变光斑透卡) + 品牌色字母瓦片联系方式列表,点击复制
*/
import { computed, ref, watch, watchEffect } from 'vue' import { computed, ref, watch, watchEffect } from 'vue'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
@@ -94,7 +90,7 @@ watch(socialConfig, () => {
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-6"> <view class="box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-2">
<!-- 自定义导航 --> <!-- 自定义导航 -->
<uh-navbar default-title="联系博主" title-color="text-gray-900" /> <uh-navbar default-title="联系博主" title-color="text-gray-900" />
@@ -102,7 +98,7 @@ watch(socialConfig, () => {
<view class="hero-wrap relative"> <view class="hero-wrap relative">
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" /> <view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
<view class="absolute top-[150rpx] h-[180rpx] w-[180rpx] rounded-full bg-[rgba(215,249,76,0.45)] -left-10" /> <view class="absolute top-[150rpx] h-[180rpx] w-[180rpx] rounded-full bg-[rgba(215,249,76,0.45)] -left-10" />
<view class="hero-card uh-global-card-glass relative flex flex-col items-center rounded-3xl px-6 pb-6 pt-10"> <view class="hero-card uh-global-card-glass uh-shadow-xs relative flex flex-col items-center rounded-2xl px-6 pb-6 pt-10">
<view class="avatar relative box-border h-[150rpx] w-[150rpx] overflow-hidden border-5 border-white/90 rounded-full shadow-lg"> <view class="avatar relative box-border h-[150rpx] w-[150rpx] overflow-hidden border-5 border-white/90 rounded-full shadow-lg">
<image class="avatar-img h-full w-full" :src="bloggerInfo.avatar" mode="aspectFill" /> <image class="avatar-img h-full w-full" :src="bloggerInfo.avatar" mode="aspectFill" />
</view> </view>
@@ -125,26 +121,26 @@ watch(socialConfig, () => {
empty-text="暂无联系方式" empty-sub-text="" @refresh="handleGetData" empty-text="暂无联系方式" empty-sub-text="" @refresh="handleGetData"
/> />
<template v-else> <template v-else>
<uh-section-title class="mb-3 mt-6 text-[30rpx]"> <uh-section-title class="mb-3 mt-6">
联系方式 联系方式
</uh-section-title> </uh-section-title>
<view class="flex flex-col gap-3"> <view class="flex flex-col gap-3">
<view <view
v-for="item in result.filter(i => i.value)" v-for="item in result.filter(i => i.value)"
:key="item.key" :key="item.key"
class="item uh-global-card-glass flex items-center gap-3 rounded-2xl px-4 py-3" class="box-border uh-global-card-glass uh-shadow-xs flex items-center gap-3 rounded-2xl px-4 py-3"
@click="handleOnClick(item)" @click="handleOnClick(item)"
> >
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl" :style="{ backgroundColor: platformMeta[item.key]?.color || '#8a8a7a' }"> <view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl" :style="{ backgroundColor: platformMeta[item.key]?.color || '#8a8a7a' }">
<text class="text-[30rpx] text-white font-bold">{{ platformMeta[item.key]?.letter || item.name.slice(0, 1) }}</text> <text class="text-[30rpx] text-white font-bold">{{ platformMeta[item.key]?.letter || item.name.slice(0, 1) }}</text>
</view> </view>
<view class="min-w-0 flex flex-1 flex-col"> <view class="min-w-0 flex flex-1 flex-col">
<text class="text-[24rpx] text-gray-400">{{ item.name }}</text> <text class="text-xs text-gray-500">{{ item.name }}</text>
<view class="mt-1 break-all text-[26rpx] text-gray-900 leading-snug"> <view class="mt-1 break-all text-[26rpx] text-gray-900 leading-snug">
{{ item.value }} {{ item.value }}
</view> </view>
</view> </view>
<wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" /> <wd-icon name="copy" size="28rpx" class="shrink-0 text-gray-400" />
</view> </view>
</view> </view>
</template> </template>
+74 -71
View File
@@ -1,83 +1,86 @@
<script lang="ts" setup> <script lang="ts" setup>
/** /**
* 免责声明页(源自旧项目 pagesA/disclaimers,新建复刻) * 免责声明页(源自旧项目 pagesA/disclaimers,新建复刻)
*/ */
import { computed } from 'vue' import { computed } from 'vue'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '免责声明', navigationBarTitleText: '免责声明',
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs) const haloConfigs = computed(() => appConfigStore.configs)
const disclaimersContent = computed(() => { const disclaimersContent = computed(() => {
const basicConfig = haloConfigs.value.basicConfig as { disclaimers?: { content?: string } } | undefined const basicConfig = haloConfigs.value.basicConfig as { disclaimers ?: { content ?: string } } | undefined
return basicConfig?.disclaimers?.content || '' return basicConfig?.disclaimers?.content || ''
}) })
const bloggerInfo = computed(() => { const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, email?: string } | undefined const blogger = haloConfigs.value.authorConfig?.blogger as { nickname ?: string, email ?: string } | undefined
return { return {
nickname: blogger?.nickname || '', nickname: blogger?.nickname || '',
email: blogger?.email || '', email: blogger?.email || '',
} }
}) })
function copyText(content: string, tips = '复制成功') { function copyText(content : string, tips = '复制成功') {
uni.setClipboardData({ uni.setClipboardData({
data: content, data: content,
showToast: false, showToast: false,
success: () => { success: () => {
uni.showToast({ icon: 'none', title: tips }) uni.showToast({ icon: 'none', title: tips })
}, },
}) })
} }
</script> </script>
<template> <template>
<view class="box-border min-h-screen bg-page "> <view class="box-border min-h-screen bg-page p-3 pt-2">
<!-- 自定义导航 --> <!-- 自定义导航 -->
<uh-navbar default-title="免责声明" title-color="text-gray-900" /> <uh-navbar default-title="免责声明" title-color="text-gray-900" />
<view class="w-full h-full uh-global-card-glass uh-shadow-xs rounded-xl">
<!-- 通过配置 -->
<view v-if="disclaimersContent" v-html="disclaimersContent" />
<!-- 通过配置 --> <!-- 静态写法 -->
<view v-if="disclaimersContent" style="min-height: 100%;" v-html="disclaimersContent" /> <view v-else class="box-border p-4 pt-0 text-sm text-gray-900 leading-6">
<view class="item mt-4">
<!-- 静态写法 --> 1本应用属于个人非盈利性质的网站所有转载的文章都以遵循原作者的版权声明注明了文章来源
<view v-else class="box-border p-4 pt-0 text-sm text-gray-900 leading-7"> </view>
<view class="item mt-6"> <view class="item mt-4">
1本应用属于个人非盈利性质的网站所有转载的文章都以遵循原作者的版权声明注明了文章来源 2如果原文没有版权声明按照目前互联网开放的原则本博客将在不通知作者的情况下转载文章
</view> </view>
<view class="item mt-6"> <view class="item mt-4">
2如果原文没有版权声明按照目前互联网开放的原则本博客将在不通知作者的情况下转载文章 3如果原文明确注明"禁止转载"本博客将不会转载
</view> </view>
<view class="item mt-6"> <view class="item mt-4">
3如果原文明确注明"禁止转载"本博客将不会转载 4如果本博客转载的文章不符合作者的版权声明或者作者不想让本博客转载您的文章请邮件告知
</view> <text class="email mx-3 text-primary"
<view class="item mt-6"> @click="copyText(bloggerInfo.email, '电子邮箱已复制到剪贴板!')">{{ bloggerInfo.email }}</text>
4如果本博客转载的文章不符合作者的版权声明或者作者不想让本博客转载您的文章请邮件告知 博主将会在第一时间删除相关信息
<text class="email mx-3 text-primary" @click="copyText(bloggerInfo.email, '电子邮箱已复制到剪贴板!')">{{ bloggerInfo.email }}</text> </view>
博主将会在第一时间删除相关信息 <view class="item mt-4">
</view> 5本博客转载文章仅为留做备份和知识点分享的目的
<view class="item mt-6"> </view>
5本博客转载文章仅为留做备份和知识点分享的目的 <view class="item mt-4">
</view> 6本博客将尽力确保所提供信息的准确性及可靠性但不保证信息的正确性和完整性且不对因信息的不正确或遗漏导致的任何损失或损害承担相关责任
<view class="item mt-6"> </view>
6本博客将尽力确保所提供信息的准确性及可靠性但不保证信息的正确性和完整性且不对因信息的不正确或遗漏导致的任何损失或损害承担相关责任 <view class="item mt-4">
</view> 7本博客所发布转载的文章其版权均归原作者所有如其他自媒体网站或个人从本博客下载使用请在转载有关文章时务必尊重该文章的著作权保留本博客注明的"原文来源"或者自行去原文处复制版权声明并自负版权等法律责任
<view class="item mt-6"> </view>
7本博客所发布转载的文章其版权均归原作者所有如其他自媒体网站或个人从本博客下载使用请在转载有关文章时务必尊重该文章的著作权保留本博客注明的"原文来源"或者自行去原文处复制版权声明并自负版权等法律责任 <view class="item mt-4">
</view> 8本博客的所有原创文章皆可以任意转载但转载时务必请注明出处
<view class="item mt-6"> </view>
8本博客的所有原创文章皆可以任意转载但转载时务必请注明出处 <view class="item mt-4">
</view> 9尊重原创知识共享
<view class="item mt-6"> </view>
9尊重原创知识共享 </view>
</view> </view>
</view> </view>
</view> </template>
</template>
+11 -10
View File
@@ -402,10 +402,10 @@
</view> </view>
<!-- 详情弹窗 --> <!-- 详情弹窗 -->
<uh-glass-popup v-model="detail.show" position="center" custom-class="w-[90vw] 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>
</view> </view>
<view <view
class="absolute right-4 top-4 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg text-center" class="absolute right-4 top-4 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg text-center"
@@ -416,26 +416,27 @@
<scroll-view v-if="detail.data" :scroll-y="true" :show-scrollbar="false" <scroll-view v-if="detail.data" :scroll-y="true" :show-scrollbar="false"
class="box-border p-4 max-h-[60vh]"> class="box-border p-4 max-h-[60vh]">
<view class="flex"> <view class="flex">
<image class="poup-logo h-[140rpx] w-[140rpx] shrink-0 rounded-full" <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 justify-center">
<view class="poup-name text-[34rpx] text-gray-900 font-bold"> <view class="text-lg text-gray-900 font-bold">
{{ detail.data.spec.displayName }} {{ detail.data.spec.displayName }}
</view> </view>
<view class="text-xs text-gray-500"> <view class="flex items-center gap-x-2">
{{ detail.data.spec.groupName }} <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>
<view @click="handleCopyLink(detail.data)"> <view @click="handleCopyLink(detail.data)">
<text <text
class="rounded-lg bg-secondary px-2 py-1 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-[28rpx] text-gray-600 leading-[1.6]">
{{ detail.data.spec.description || '这个博主很懒,没写简介~' }} {{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
</view> </view>
<image class="poup-img mt-4 h-[320rpx] w-full rounded-xl"
:src="calcSiteThumbnail(detail.data.spec.url)" mode="aspectFill" />
</scroll-view> </scroll-view>
</uh-glass-popup> </uh-glass-popup>
@@ -500,7 +501,7 @@
</view> </view>
<!-- 小程序详情弹窗 --> <!-- 小程序详情弹窗 -->
<uh-glass-popup v-model="miniDetail.show" position="center" custom-class="w-[90vw] 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>
+5 -5
View File
@@ -184,18 +184,18 @@
<wd-sticky> <wd-sticky>
<scroll-view scroll-x class="w-full whitespace-nowrap" :show-scrollbar="false"> <scroll-view scroll-x class="w-full whitespace-nowrap" :show-scrollbar="false">
<view class="flex gap-2 px-3 pb-1 pt-2"> <view class="box-border flex gap-2 px-3 pb-1 pt-2">
<view class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-4 py-1.5 text-sm" <view class="flex-1 uh-global-card-glass uh-shadow-xs inline-flex border rounded-2xl px-4 py-1.5 text-sm"
:class="activeType === '' ? 'bg-primary font-bold' : 'text-gray-500'" @click="activeType = ''"> :class="activeType === '' ? 'bg-primary font-bold' : 'text-gray-500'" @click="activeType = ''">
全部 全部
</view> </view>
<view v-for="(type) in typeOptions" :key="type.typeName || '__none__'" <view v-for="(type) in typeOptions" :key="type.typeDisplayName"
class="uh-global-card-glass uh-shadow-xs inline-flex items-center gap-1 border rounded-2xl px-4 py-1.5 text-sm" class="flex-1 box-border uh-global-card-glass uh-shadow-xs inline-flex items-center gap-1 border rounded-2xl px-4 py-1.5 text-sm"
:class="activeType === type.typeName ? 'bg-primary font-bold' : 'text-gray-500'" :class="activeType === type.typeName ? 'bg-primary font-bold' : 'text-gray-500'"
@click="activeType = activeType === type.typeName ? '' : type.typeName"> @click="activeType = activeType === type.typeName ? '' : type.typeName">
<view v-if="type.typeColor" class="shrink-0 h-2 w-2 rounded-full" <view v-if="type.typeColor" class="shrink-0 h-2 w-2 rounded-full"
:style="{ backgroundColor: type.typeColor }" /> :style="{ backgroundColor: type.typeColor }" />
<text class="shrink-0">{{ type.typeDisplayName }}</text> <view class="shrink-0">{{ type.typeDisplayName }}</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
+412 -423
View File
@@ -1,460 +1,449 @@
<script lang="ts" setup> <script lang="ts" setup>
/** import { computed, ref } from 'vue'
* 投票详情页(源自旧项目 pagesA/vote-detail,新建复刻) import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
* 支持 single/multiple/pk 三种投票类型,已投票展示结果 import { getVoteDetail, submitVote } from '@/api/uni-halo'
*/ import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { computed, ref } from 'vue' import { calcVotePercent, calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import { getVoteDetail, submitVote } from '@/api/uni-halo' import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { calcVotePercent, calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '投票详情', navigationBarTitleText: '投票详情',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const submitLoading = ref(false) const submitLoading = ref(false)
const pageTitle = ref('加载中...') const pageTitle = ref('加载中...')
const safeAreaBottom = ref(24) const safeAreaBottom = ref(24)
const name = ref('') const name = ref('')
const detail = ref<unknown>(null) const detail = ref<unknown>(null)
const vote = ref<(IVote & { const vote = ref<(IVote & {
spec?: { spec ?: {
title?: string title ?: string
remark?: string remark ?: string
type?: string type ?: string
maxVotes?: number maxVotes ?: number
startDate?: string startDate ?: string
endDate?: string endDate ?: string
timeLimit?: string timeLimit ?: string
canAnonymously?: boolean canAnonymously ?: boolean
options?: (IVoteOption & { options ?: (IVoteOption & {
id?: string id ?: string
title?: string title ?: string
count?: number count ?: number
checked?: boolean checked ?: boolean
isVoted?: boolean isVoted ?: boolean
disabled?: boolean disabled ?: boolean
_uh_percent?: number _uh_percent ?: number
})[] })[]
isVoted?: boolean isVoted ?: boolean
hasEnded?: boolean hasEnded ?: boolean
disabled?: boolean disabled ?: boolean
_uh_type?: string _uh_type ?: string
_uh_state?: { state: string, color: string } _uh_state ?: { state : string, color : string }
} }
stats?: { voteCount?: number } stats ?: { voteCount ?: number }
}) | null>(null) }) | null>(null)
const submitForm = ref<{ voteData: string[] }>({ voteData: [] }) const submitForm = ref<{ voteData : string[] }>({ voteData: [] })
/* ---------------- 计算属性 ---------------- */ /* ---------------- 计算属性 ---------------- */
const isVoted = computed(() => voteCacheUtil.has(name.value)) const isVoted = computed(() => voteCacheUtil.has(name.value))
const isEnded = computed(() => vote.value?.spec?.hasEnded || false) const isEnded = computed(() => vote.value?.spec?.hasEnded || false)
/* ---------------- 工具 ---------------- */ /* ---------------- 工具 ---------------- */
function formatTime(date?: string, fmt = 'yyyy-MM-dd HH:mm'): string { function formatTime(date ?: string, fmt = 'yyyy-MM-dd HH:mm') : string {
// 与旧项目一致:yyyy-MM-dd HH:mm // 与旧项目一致:yyyy-MM-dd HH:mm
return date ? formatTimeUtil({ d: date, f: fmt }) : '' return date ? formatTimeUtil({ d: date, f: fmt }) : ''
} }
function showToast(content: string) { function showToast(content : string) {
uni.showToast({ icon: 'none', title: content, mask: true }) uni.showToast({ icon: 'none', title: content, mask: true })
} }
function handleCalcIsChecked(option: { id?: string }): boolean { function handleCalcIsChecked(option : { id ?: string }) : boolean {
const data = voteCacheUtil.get(name.value) const data = voteCacheUtil.get(name.value)
if (!data) if (!data)
return false return false
return data.selected.includes(option.id || '') return data.selected.includes(option.id || '')
} }
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Loading) updateLoadingStatus(DataLoadingStatusEnum.Loading)
pageTitle.value = '加载中...' pageTitle.value = '加载中...'
try { try {
const res = await getVoteDetail(name.value) const res = await getVoteDetail(name.value)
const detailRes = res.data as IVoteDetail const detailRes = res.data as IVoteDetail
const tempVote = detailRes.vote as typeof vote.value const tempVote = detailRes.vote as typeof vote.value
if (tempVote) { if (tempVote) {
const typeKey = ((tempVote.spec?.type || 'single') as string).toLowerCase() const typeKey = ((tempVote.spec?.type || 'single') as string).toLowerCase()
pageTitle.value = `投票详情(${VOTE_TYPES[typeKey] || tempVote.spec?.type}` pageTitle.value = `投票详情(${VOTE_TYPES[typeKey] || tempVote.spec?.type}`
tempVote.spec = tempVote.spec || {} tempVote.spec = tempVote.spec || {}
tempVote.spec.isVoted = isVoted.value tempVote.spec.isVoted = isVoted.value
tempVote.spec.disabled = isVoted.value tempVote.spec.disabled = isVoted.value
tempVote.spec._uh_type = VOTE_TYPES[typeKey] || tempVote.spec.type tempVote.spec._uh_type = VOTE_TYPES[typeKey] || tempVote.spec.type
// 计算状态(与旧项目 calcVoteState 一致,含 timeLimit 非 custom 时 hasEnded 兜底) // 计算状态(与旧项目 calcVoteState 一致,含 timeLimit 非 custom 时 hasEnded 兜底)
tempVote.spec._uh_state = calcVoteState(tempVote) tempVote.spec._uh_state = calcVoteState(tempVote)
if (tempVote.spec._uh_state.state === '已结束') if (tempVote.spec._uh_state.state === '已结束')
tempVote.spec.hasEnded = true tempVote.spec.hasEnded = true
// 选项计算 // 选项计算
// 插件选项为 {id,title},票数在 VoteDetail.voteDataList / Vote.stats.voteDataList // 插件选项为 {id,title},票数在 VoteDetail.voteDataList / Vote.stats.voteDataList
const countList = (detailRes.voteDataList || tempVote.stats?.voteDataList || []) as { id?: string, voteCount?: number }[] const countList = (detailRes.voteDataList || tempVote.stats?.voteDataList || []) as { id ?: string, voteCount ?: number }[]
const countMap: Record<string, number> = {} const countMap : Record<string, number> = {}
countList.forEach((item) => { countList.forEach((item) => {
if (item.id) if (item.id)
countMap[item.id] = item.voteCount || 0 countMap[item.id] = item.voteCount || 0
}) })
tempVote.spec.options = (tempVote.spec.options || []).map((option) => { tempVote.spec.options = (tempVote.spec.options || []).map((option) => {
const checked = handleCalcIsChecked(option) const checked = handleCalcIsChecked(option)
const optionWithCount = { const optionWithCount = {
...option, ...option,
value: option.id, value: option.id,
label: option.title, label: option.title,
count: countMap[option.id || ''] || 0, count: countMap[option.id || ''] || 0,
isVoted: isVoted.value, isVoted: isVoted.value,
checked, checked,
disabled: isVoted.value, disabled: isVoted.value,
} }
return { return {
...optionWithCount, ...optionWithCount,
_uh_percent: calcVotePercent(tempVote, optionWithCount), _uh_percent: calcVotePercent(tempVote, optionWithCount),
} }
}) })
} }
vote.value = tempVote vote.value = tempVote
detail.value = res detail.value = res
setTimeout(() => { setTimeout(() => {
updateLoadingStatus( updateLoadingStatus(
tempVote ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty, tempVote ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
) )
}, 200) }, 200)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
updateLoadingStatus(DataLoadingStatusEnum.Error) updateLoadingStatus(DataLoadingStatusEnum.Error)
pageTitle.value = '加载失败,请重试...' pageTitle.value = '加载失败,请重试...'
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
uni.hideLoading() uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 200) }, 200)
} }
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleSelectSingleOption(option: { id?: string }) { function handleSelectSingleOption(option : { id ?: string }) {
if (vote.value?.spec?._uh_state?.state === '未开始') { if (vote.value?.spec?._uh_state?.state === '未开始') {
showToast('投票未开始') showToast('投票未开始')
return return
} }
if (vote.value?.spec?.hasEnded) if (vote.value?.spec?.hasEnded)
return return
if (vote.value?.spec?.disabled) if (vote.value?.spec?.disabled)
return return
vote.value!.spec!.options!.forEach((item) => { vote.value!.spec!.options!.forEach((item) => {
item.checked = option.id === item.id item.checked = option.id === item.id
}) })
submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '') submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '')
} }
function handleSelectCheckboxOption(option: { id?: string }) { function handleSelectCheckboxOption(option : { id ?: string }) {
if (vote.value?.spec?._uh_state?.state === '未开始') { if (vote.value?.spec?._uh_state?.state === '未开始') {
showToast('投票未开始') showToast('投票未开始')
return return
} }
if (vote.value?.spec?.hasEnded) if (vote.value?.spec?.hasEnded)
return return
if (vote.value?.spec?.disabled) if (vote.value?.spec?.disabled)
return return
const checkedList = vote.value!.spec!.options!.filter(x => x.checked && x.id !== option.id) const checkedList = vote.value!.spec!.options!.filter(x => x.checked && x.id !== option.id)
if (vote.value?.spec?.type === 'multiple' && checkedList.length >= (vote.value.spec.maxVotes || 0)) { if (vote.value?.spec?.type === 'multiple' && checkedList.length >= (vote.value.spec.maxVotes || 0)) {
showToast(`最多选择 ${vote.value.spec.maxVotes}`) showToast(`最多选择 ${vote.value.spec.maxVotes}`)
return return
} }
vote.value!.spec!.options!.forEach((item) => { vote.value!.spec!.options!.forEach((item) => {
if (option.id === item.id) { if (option.id === item.id) {
item.checked = !item.checked item.checked = !item.checked
} }
}) })
submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '') submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '')
} }
function handleSubmitTip(text: string) { function handleSubmitTip(text : string) {
showToast(text) showToast(text)
} }
async function handleSubmit() { async function handleSubmit() {
if (!vote.value?.spec?.canAnonymously) { if (!vote.value?.spec?.canAnonymously) {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '该投票不支持匿名,请到博主的 网站端 进行投票!', content: '该投票不支持匿名,请到博主的 网站端 进行投票!',
cancelColor: '#666666', cancelColor: '#666666',
cancelText: '关闭', cancelText: '关闭',
confirmText: '复制地址', confirmText: '复制地址',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.setClipboardData({ uni.setClipboardData({
data: import.meta.env.VITE_SERVER_BASEURL || '', data: import.meta.env.VITE_SERVER_BASEURL || '',
showToast: false, showToast: false,
success: () => { success: () => {
showToast('复制成功') showToast('复制成功')
}, },
}) })
} }
}, },
}) })
return return
} }
submitLoading.value = true submitLoading.value = true
uni.showLoading({ title: '正在保存...' }) uni.showLoading({ title: '正在保存...' })
try { try {
await submitVote(name.value, submitForm.value, vote.value.spec.canAnonymously) await submitVote(name.value, submitForm.value, vote.value.spec.canAnonymously)
showToast('提交成功') showToast('提交成功')
voteCacheUtil.set(name.value, { voteCacheUtil.set(name.value, {
selected: [...submitForm.value.voteData], selected: [...submitForm.value.voteData],
data: vote.value, data: vote.value,
}) })
setTimeout(() => { setTimeout(() => {
uni.startPullDownRefresh() uni.startPullDownRefresh()
submitLoading.value = false submitLoading.value = false
}, 1500) }, 1500)
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
showToast('提交失败,请重试') showToast('提交失败,请重试')
submitLoading.value = false submitLoading.value = false
} }
finally { finally {
uni.hideLoading() uni.hideLoading()
} }
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad((options) => { onLoad((options) => {
name.value = options?.name || '' name.value = options?.name || ''
// #ifndef H5 // #ifndef H5
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom ? systemInfo.safeAreaInsets.bottom + 12 : 24 safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom ? systemInfo.safeAreaInsets.bottom + 12 : 24
// #endif // #endif
handleGetData() handleGetData()
}) })
onPullDownRefresh(() => { onPullDownRefresh(() => {
handleGetData() handleGetData()
}) })
onShareAppMessage(() => ({ onShareAppMessage(() => ({
path: `/pages-blog/vote-detail/vote-detail?name=${name.value}`, path: `/pages-blog/vote-detail/vote-detail?name=${name.value}`,
title: vote.value?.spec?.title || '来投个票吧', title: vote.value?.spec?.title || '来投个票吧',
imageUrl: '', imageUrl: '',
})) }))
onShareTimeline(() => ({ onShareTimeline(() => ({
title: vote.value?.spec?.title || '来投个票吧', title: vote.value?.spec?.title || '来投个票吧',
query: name.value ? `name=${name.value}` : '', query: name.value ? `name=${name.value}` : '',
imageUrl: '', imageUrl: '',
})) }))
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen flex flex-col bg-page pb-[160rpx]"> <view class="box-border min-h-screen w-screen flex flex-col bg-page pb-safe">
<!-- 自定义导航 --> <!-- 自定义导航 -->
<uh-navbar :default-title="pageTitle" title-color="text-gray-900" /> <uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
<!-- 加载/错误/空占位(状态机) --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'"> <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
<uh-data-loading empty-text="未查询到数据" @refresh="handleGetData" />
:loading-status="loadingStatus"
empty-text="未查询到数据"
@refresh="handleGetData"
/>
</view>
<block v-else> <view v-else class="box-border px-3 pt-2 pb-16 flex flex-col gap-y-3">
<template v-if="vote"> <!-- 投票信息 -->
<!-- 投票信息 --> <view class="uh-global-card-glass box-border flex flex-col gap-y-3 rounded-2xl p-3">
<view class="uh-global-card-glass box-border mx-6 mb-6 flex flex-col rounded-2xl p-6"> <uh-section-title> 投票信息 </uh-section-title>
<view class="sub-title relative box-border pl-6 text-[30rpx]"> <view class="flex flex-col gap-3 rounded-xl bg-gray-100 p-4 text-sm text-gray-600">
投票信息 <view class="info-row">
</view> <text>投票类型</text>
<view class="vote-card-body mt-3 flex flex-col gap-3 rounded-xl bg-page p-6 text-[28rpx] text-[#3f3f3f]"> <text class="tag">{{ vote.spec?._uh_type }}</text>
<view class="info-row"> </view>
<text>投票类型</text> <view class="info-row">
<text class="tag">{{ vote.spec?._uh_type }}</text> <text>投票状态</text>
</view> <text class="tag"
<view class="info-row"> :style="{ color: vote.spec?._uh_state?.color }">{{ vote.spec?._uh_state?.state }}</text>
<text>投票状态</text> </view>
<text class="tag" :style="{ color: vote.spec?._uh_state?.color }">{{ vote.spec?._uh_state?.state }}</text> <view class="info-row">
</view> <text>投票方式</text>
<view class="info-row"> <text class="tag" :class="vote.spec?.canAnonymously ? 'text-primary' : 'text-[#f44336]'">
<text>投票方式</text> {{ vote.spec?.canAnonymously ? '匿名' : '不匿名' }}
<text class="tag" :class="vote.spec?.canAnonymously ? 'text-primary' : 'text-[#f44336]'"> </text>
{{ vote.spec?.canAnonymously ? '匿名' : '不匿名' }} </view>
</text> <view class="info-row">
</view> <text>开始时间{{ formatTime(vote.spec?.startDate) }}</text>
<view class="info-row"> </view>
<text>开始时间{{ formatTime(vote.spec?.startDate) }}</text> <view class="info-row">
</view> <text v-if="vote.spec?.timeLimit === 'permanent'">结束时间永久有效</text>
<view class="info-row"> <text v-else>结束时间{{ formatTime(vote.spec?.endDate) }}</text>
<text v-if="vote.spec?.timeLimit === 'permanent'">结束时间永久有效</text> </view>
<text v-else>结束时间{{ formatTime(vote.spec?.endDate) }}</text> </view>
</view> </view>
</view>
</view>
<!-- 投票内容 --> <!-- 投票内容 -->
<view class="uh-global-card-glass box-border mx-6 mb-6 flex flex-col rounded-2xl p-6"> <view class="uh-global-card-glass box-border flex flex-col rounded-2xl p-3 gap-3">
<view class="sub-title relative box-border pl-6 text-[30rpx]"> <uh-section-title> 投票内容 </uh-section-title>
投票内容 <view class="box-border flex flex-col gap-y-2 p-4 rounded-xl bg-gray-100">
</view> <view class="text-sm text-gray-900 font-bold">
<view class="sub-content mb-3 pt-3 text-[30rpx] text-[#2b2f33] font-bold"> {{ vote.spec?.title }}
{{ vote.spec?.title }} </view>
</view> <view v-if="vote.spec?.remark" class="text-xs text-gray-500">
<view v-if="vote.spec?.remark" class="sub-remark mb-9 pt-3 text-[28rpx] text-[#3f3f3f]"> {{ vote.spec.remark }}
{{ vote.spec.remark }} </view>
</view> </view>
<view class="sub-title relative box-border pl-6 text-[30rpx]"> <view class="w-full flex flex-col gap-y-2">
投票选项 <view class="relative box-border text-sm flex items-center gap-x-2">
<text v-if="vote.spec?.type === 'multiple'" class="sub-title-count text-[24rpx] font-normal">最多选择 {{ vote.spec?.maxVotes }} </text> 投票选项
</view> <text v-if="vote.spec?.type === 'multiple'"
<view class="options mt-6 flex flex-col gap-4"> class="text-xs font-normal">最多选择
<!-- PK 对抗条(与旧项目 pk-container 一致) --> {{ vote.spec?.maxVotes }}
<view v-if="vote.spec?.type === 'pk'" class="pk-container box-border flex w-full"> </text>
<view </view>
v-for="(option, optionIndex) in vote.spec?.options" <view class="options flex flex-col gap-3">
:key="optionIndex" <!-- PK 对抗条(与旧项目 pk-container 一致) -->
class="radio-item flex-grow" <view v-if="vote.spec?.type === 'pk'" class="pk-container box-border flex w-full">
:class="optionIndex === 0 ? 'radio-left' : 'radio-right'" <view v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
:style="{ width: `${option._uh_percent}%` }" class="radio-item flex-grow" :class="optionIndex === 0 ? 'radio-left' : 'radio-right'"
> :style="{ width: `${option._uh_percent}%` }">
<view class="option-item box-border w-full rounded-xl p-6" :class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'"> <view class="option-item box-border w-full rounded-xl py-3 px-3"
{{ option._uh_percent }}% :class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'">
</view> {{ option._uh_percent }}%
</view> </view>
</view> </view>
</view>
<template v-if="isVoted || isEnded"> <template v-if="isVoted || isEnded">
<view <view v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
v-for="(option, optionIndex) in vote.spec?.options" class="is-voted-item relative box-border overflow-hidden rounded-xl text-xs"
:key="optionIndex" :class="option.checked ? 'bg-primary text-gray-900 font-bold' : 'bg-gray-100'"
class="is-voted-item relative box-border min-h-[72rpx] overflow-hidden rounded-xl text-[24rpx]" :style="{ '--percent': `${option._uh_percent}%` }">
:class="option.checked ? 'bg-primary/40 text-[#4d7c0f] font-bold' : 'bg-[#e5e5e5]/75'" <view class="is-voted-item-content relative z-2 box-border px-4 py-3">
:style="{ '--percent': `${option._uh_percent}%` }" <view class="flex items-center justify-between">
> <view class="flex-1 text-left">
<view class="is-voted-item-content relative z-2 box-border min-h-[72rpx] px-6 py-3"> {{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}` : '' }}{{ option.title }}
<view class="flex items-center justify-between"> </view>
<view class="flex-1 text-left"> <view class="shrink-0">
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}` : '' }}{{ option.title }} {{ option._uh_percent }}%
</view> </view>
<view class="shrink-0"> </view>
{{ option._uh_percent }}% </view>
</view> </view>
</view> </template>
</view> <template v-else>
</view> <view v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
</template> class="vote-select-option box-border rounded-xl bg-gray-100 px-6 py-5 text-xs"
<template v-else> :class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
<view @click="vote.spec?.type === 'multiple' ? handleSelectCheckboxOption(option) : handleSelectSingleOption(option)">
v-for="(option, optionIndex) in vote.spec?.options" {{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}` : '' }}{{ option.title }}
:key="optionIndex" </view>
class="vote-select-option box-border rounded-xl bg-[#f3f4f6] px-6 py-5 text-[24rpx]" </template>
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''" </view>
@click="vote.spec?.type === 'multiple' ? handleSelectCheckboxOption(option) : handleSelectSingleOption(option)" </view>
> </view>
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}` : '' }}{{ option.title }}
</view>
</template>
</view>
</view>
<!-- 投票统计 --> <!-- 投票统计 -->
<view class="uh-global-card-glass box-border mx-6 mb-6 flex flex-col rounded-2xl p-6"> <view class="uh-global-card-glass box-border flex flex-col rounded-2xl p-3">
<view class="sub-title relative box-border pl-6 text-[30rpx]"> <uh-section-title> 投票统计 </uh-section-title>
投票统计 <view class="stat-text mt-3 text-xs text-gray-600">
</view> {{ vote.stats?.voteCount || 0 }} 人已参与
<view class="stat-text mt-3 text-[26rpx] text-[#606266]"> </view>
{{ vote.stats?.voteCount || 0 }} 人已参与 </view>
</view>
</view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
<view class="vote-submit fixed bottom-0 left-0 z-99 box-border w-screen border-t border-black/5 bg-white/90 px-9 py-6 shadow-sm backdrop-blur" :style="{ paddingBottom: `${safeAreaBottom}rpx` }"> <view class="fixed bottom-0 left-0 z-99 box-border w-screen pb-safe px-3">
<wd-button v-if="isVoted" disabled block> <view
您已参与投票 class="uh-global-card-glass border rounded-xl w-full flex items-center justify-center gap-x-2 mb-2">
</wd-button> <uh-button v-if="isVoted" custom-class="flex-1 py-2 !rounded-xl">
<wd-button v-else-if="vote.spec?._uh_state?.state === '未开始'" plain block type="warning" @click="handleSubmitTip('投票未开始')"> 您已参与投票
投票未开始 </uh-button>
</wd-button> <uh-button v-else-if="vote.spec?._uh_state?.state === '未开始'" custom-class="flex-1 py-2 !rounded-xl"
<wd-button v-else-if="vote.spec?._uh_state?.state === '已结束'" plain block type="danger" @click="handleSubmitTip('投票已结束')"> @click="handleSubmitTip('投票未开始')">
投票已结束 投票未开始
</wd-button> </uh-button>
<wd-button v-else-if="!vote.spec?.canAnonymously" plain block type="danger" @click="handleSubmit()"> <uh-button v-else-if="vote.spec?._uh_state?.state === '已结束'" custom-class="flex-1 py-2 !rounded-xl"
不支持匿名投票 @click="handleSubmitTip('投票已结束')">
</wd-button> 投票已结束
<wd-button v-else-if="submitForm.voteData.length === 0" plain block @click="handleSubmitTip('请选择选项')"> </uh-button>
提交投票请选择选项 <uh-button v-else-if="!vote.spec?.canAnonymously" custom-class="flex-1 py-2 !rounded-xl"
</wd-button> @click="handleSubmit()">
<wd-button v-else block type="primary" :loading="submitLoading" :disabled="submitLoading" @click="handleSubmit()"> 不支持匿名投票
提交投票 </uh-button>
</wd-button> <uh-button v-else-if="submitForm.voteData.length === 0" custom-class="flex-1 py-2 !rounded-xl"
</view> @click="handleSubmitTip('请选择选项')">
</template> 提交投票请选择选项
</block> </uh-button>
</view> <uh-button v-else custom-class="flex-1 py-2 !rounded-xl" @click="handleSubmit()">
提交投票
</uh-button>
</view>
</view>
</view>
</view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.vote-card { .vote-card {
.sub-title { .sub-title {
&::before { &::before {
content: ''; content: '';
width: 8rpx; width: 8rpx;
height: 28rpx; height: 28rpx;
position: absolute; position: absolute;
left: 0; left: 0;
top: 6rpx; top: 6rpx;
background: var(--wot-color-theme, #b9e424); background: var(--wot-color-theme, #b9e424);
border-radius: 6rpx; border-radius: 6rpx;
} }
} }
.is-voted-item { .is-voted-item {
&::before { &::before {
content: ''; content: '';
width: var(--percent); width: var(--percent);
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
background-color: #d0d0d0; background-color: #d0d0d0;
z-index: 0; z-index: 0;
border-radius: 6rpx; border-radius: 6rpx;
} }
} }
.pk-container { .pk-container {
.radio-item { .radio-item {
min-width: 30%; min-width: 30%;
max-width: 70%; max-width: 70%;
} }
.option-item-left { .option-item-left {
background: linear-gradient(90deg, #3b82f6, #60a5fa); background: linear-gradient(90deg, #3b82f6, #60a5fa);
color: white; color: white;
clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 100%, 0 100%); clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 100%, 0 100%);
} }
.option-item-right { .option-item-right {
background: linear-gradient(90deg, #f87171, #ef4444); background: linear-gradient(90deg, #f87171, #ef4444);
color: white; color: white;
clip-path: polygon(0 0, 100% 0, 100% 100%, 40rpx 100%); clip-path: polygon(0 0, 100% 0, 100% 100%, 40rpx 100%);
text-align: right; text-align: right;
} }
} }
} }
</style> </style>
+309 -344
View File
@@ -1,382 +1,347 @@
<script lang="ts" setup> <script lang="ts" setup>
/** import { computed, ref } from 'vue'
* 投票列表页(源自旧项目 pagesA/votes,新建复刻) import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
* 展示投票列表,支持搜索 + 类型/状态/排序/是否已投筛选,每个投票项用 uh-vote-card 渲染 import { getVoteList } from '@/api/uni-halo'
*/ import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { computed, ref } from 'vue' import { NeedPluginIds } from '@/hooks/usePluginAvailable'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { useAppConfigStore } from '@/store/appConfig'
import { getVoteList } from '@/api/uni-halo' import { debounce } from '@/utils/debounce'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import { NeedPluginIds } from '@/hooks/usePluginAvailable' import type { IVoteItem } from '@/api/types/uni-halo'
import { useAppConfigStore } from '@/store/appConfig'
import { debounce } from '@/utils/debounce'
import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import type { IVoteItem } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '投票中心', navigationBarTitleText: '投票中心',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
/** 依赖插件(plugin-vote,参考 gallery 对象传参模式) */ /** 依赖插件(plugin-vote,参考 gallery 对象传参模式) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({ const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginVote, pluginId: NeedPluginIds.PluginVote,
tips: '检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员', tips: '检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员',
}) })
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() { async function handlePluginRefresh() {
if (await checkPluginAvailable()) if (await checkPluginAvailable())
handleGetData() handleGetData()
} }
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus() const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const dataList = ref<IVoteItem[]>([]) const dataList = ref<IVoteItem[]>([])
const hasNext = ref(false) const hasNext = ref(false)
const isLoadMore = ref(false) const isLoadMore = ref(false)
const loadMoreText = ref('加载中...') const loadMoreText = ref('加载中...')
/** 是否已投过滤(前端过滤,接口无此参数) */ /** 是否已投过滤(前端过滤,接口无此参数) */
const filterIsVoted = ref<boolean | undefined>(undefined) const filterIsVoted = ref<boolean | undefined>(undefined)
const queryParams = ref<Record<string, unknown>>({ const queryParams = ref<Record<string, unknown>>({
keyword: '', keyword: '',
page: 1, page: 1,
size: 10, size: 10,
sort: undefined, sort: undefined,
type: undefined, type: undefined,
hasEnded: undefined, hasEnded: undefined,
}) })
/* ---------------- 筛选 ---------------- */ /* ---------------- 筛选 ---------------- */
interface IFilterOption { interface IFilterOption {
label: string label : string
value: string value : string
} }
interface IFilterItem { interface IFilterItem {
key: 'type' | 'hasEnded' | 'sort' | 'isVoted' key : 'type' | 'hasEnded' | 'sort' | 'isVoted'
label: string label : string
options: IFilterOption[] options : IFilterOption[]
} }
/** 筛选维度(与旧项目 tm-dropDownMenu 一致) */ /** 筛选维度(与旧项目 tm-dropDownMenu 一致) */
const filterConfig: IFilterItem[] = [ const filterConfig : IFilterItem[] = [
{ {
key: 'type', key: 'type',
label: '类型', label: '类型',
options: [ options: [
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '单选', value: 'single' }, { label: '单选', value: 'single' },
{ label: '多选', value: 'multiple' }, { label: '多选', value: 'multiple' },
{ label: '双选PK', value: 'pk' }, { label: '双选PK', value: 'pk' },
], ],
}, },
{ {
key: 'hasEnded', key: 'hasEnded',
label: '状态', label: '状态',
options: [ options: [
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '进行中', value: 'false' }, { label: '进行中', value: 'false' },
{ label: '已结束', value: 'true' }, { label: '已结束', value: 'true' },
], ],
}, },
{ {
key: 'sort', key: 'sort',
label: '排序', label: '排序',
options: [ options: [
{ label: '默认排序', value: '' }, { label: '默认排序', value: '' },
{ label: '较近创建', value: 'metadata.creationTimestamp,desc' }, { label: '较近创建', value: 'metadata.creationTimestamp,desc' },
{ label: '较早创建', value: 'metadata.creationTimestamp,asc' }, { label: '较早创建', value: 'metadata.creationTimestamp,asc' },
], ],
}, },
{ {
key: 'isVoted', key: 'isVoted',
label: '是否已投', label: '是否已投',
options: [ options: [
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '未投票', value: 'false' }, { label: '未投票', value: 'false' },
{ label: '已投票', value: 'true' }, { label: '已投票', value: 'true' },
], ],
}, },
] ]
/** 各维度当前选中值(空串 = 全部) */ /** 各维度当前选中值(空串 = 全部) */
const filterValues = ref<Record<string, string>>({ type: '', hasEnded: '', sort: '', isVoted: '' }) const filterValues = ref<Record<string, string>>({ type: '', hasEnded: '', sort: '', isVoted: '' })
/** 当前选中中文标签(用于筛选栏展示) */ /** 当前选中中文标签(用于筛选栏展示) */
const filterLabels = computed(() => { const filterLabels = computed(() => {
const map: Record<string, string> = {} const map : Record<string, string> = {}
for (const f of filterConfig) { for (const f of filterConfig) {
const cur = filterValues.value[f.key] const cur = filterValues.value[f.key]
map[f.key] = f.options.find(o => o.value === cur)?.label || '全部' map[f.key] = f.options.find(o => o.value === cur)?.label || '全部'
} }
return map return map
}) })
/** 筛选弹层 */ /** 筛选弹层 */
const filterPopup = ref<{ show: boolean, item: IFilterItem | null }>({ show: false, item: null }) const filterPopup = ref<{ show : boolean, item : IFilterItem | null }>({ show: false, item: null })
function handleOpenFilter(item: IFilterItem) { function handleOpenFilter(item : IFilterItem) {
filterPopup.value = { show: true, item } filterPopup.value = { show: true, item }
} }
function handleSelectFilter(option: IFilterOption) { function handleSelectFilter(option : IFilterOption) {
const item = filterPopup.value.item const item = filterPopup.value.item
if (!item) if (!item)
return return
filterValues.value[item.key] = option.value filterValues.value[item.key] = option.value
filterPopup.value.show = false filterPopup.value.show = false
if (item.key === 'isVoted') { if (item.key === 'isVoted') {
filterIsVoted.value = option.value === '' ? undefined : option.value === 'true' filterIsVoted.value = option.value === '' ? undefined : option.value === 'true'
} }
else if (item.key === 'hasEnded') { else if (item.key === 'hasEnded') {
queryParams.value.hasEnded = option.value === '' ? undefined : option.value === 'true' queryParams.value.hasEnded = option.value === '' ? undefined : option.value === 'true'
} }
else { else {
queryParams.value[item.key] = option.value === '' ? undefined : option.value queryParams.value[item.key] = option.value === '' ? undefined : option.value
} }
queryParams.value.page = 1 queryParams.value.page = 1
isLoadMore.value = false isLoadMore.value = false
handleGetData() handleGetData()
} }
/* ---------------- 搜索 ---------------- */ /* ---------------- 搜索 ---------------- */
/** 实时搜索:输入防抖 400ms 后触发(对应旧版 tm-search 的 @input) */ /** 实时搜索:输入防抖 400ms 后触发(对应旧版 tm-search 的 @input) */
const handleOnInput = debounce(() => { const handleOnInput = debounce(() => {
queryParams.value.page = 1 queryParams.value.page = 1
isLoadMore.value = false isLoadMore.value = false
handleGetData() handleGetData()
}, 400) }, 400)
function handleOnSearch() { function handleOnSearch() {
queryParams.value.page = 1 queryParams.value.page = 1
isLoadMore.value = false isLoadMore.value = false
handleGetData() handleGetData()
} }
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
updateLoadingStatus( updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
) )
loadMoreText.value = '呜呜,没有更多数据啦~' loadMoreText.value = '呜呜,没有更多数据啦~'
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
uni.showLoading({ mask: true, title: '加载中...' }) uni.showLoading({ mask: true, title: '加载中...' })
if (!isLoadMore.value) { if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading) updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
loadMoreText.value = '加载中...' loadMoreText.value = '加载中...'
try { try {
const res = await getVoteList({ ...queryParams.value }) const res = await getVoteList({ ...queryParams.value })
hasNext.value = res.data.hasNext || false hasNext.value = res.data.hasNext || false
// 加工列表数据(与旧项目一致):isVoted/_uh_state/_uh_type // 加工列表数据(与旧项目一致):isVoted/_uh_state/_uh_type
const tempItems = res.data.items.map((item) => { const tempItems = res.data.items.map((item) => {
item.spec = item.spec || {} item.spec = item.spec || {}
item.spec.disabled = true item.spec.disabled = true
item.spec.isVoted = voteCacheUtil.has(item.metadata?.name || '') item.spec.isVoted = voteCacheUtil.has(item.metadata?.name || '')
item.spec._uh_state = calcVoteState(item) item.spec._uh_state = calcVoteState(item)
item.spec._uh_type = VOTE_TYPES[item.spec.type || ''] || item.spec.type item.spec._uh_type = VOTE_TYPES[item.spec.type || ''] || item.spec.type
return item return item
}) })
dataList.value = isLoadMore.value dataList.value = isLoadMore.value
? dataList.value.concat(tempItems) ? dataList.value.concat(tempItems)
: tempItems : tempItems
// 未投优先排序(与旧项目一致) // 未投优先排序(与旧项目一致)
dataList.value = dataList.value.sort((a, b) => { dataList.value = dataList.value.sort((a, b) => {
return Number(a.spec?.isVoted) - Number(b.spec?.isVoted) return Number(a.spec?.isVoted) - Number(b.spec?.isVoted)
}) })
// 是否已投过滤(与旧项目一致) // 是否已投过滤(与旧项目一致)
if (filterIsVoted.value !== undefined) { if (filterIsVoted.value !== undefined) {
dataList.value = dataList.value.filter(x => x.spec?.isVoted === filterIsVoted.value) dataList.value = dataList.value.filter(x => x.spec?.isVoted === filterIsVoted.value)
} }
updateLoadingStatus( updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
) )
loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~' loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~'
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
updateLoadingStatus(DataLoadingStatusEnum.Error) updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
uni.hideLoading() uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 500) }, 500)
} }
} }
/** 卡片点击跳详情(与旧项目 VoteCard @on-click 一致) */ /** 卡片点击跳详情(与旧项目 VoteCard @on-click 一致) */
function handleOnVoteClick(vote: IVoteItem) { function handleOnVoteClick(vote : IVoteItem) {
const name = vote.metadata?.name const name = vote.metadata?.name
if (!name) if (!name)
return return
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/vote-detail/vote-detail?name=${name}`, url: `/pages-blog/vote-detail/vote-detail?name=${name}`,
}) })
} }
function handleToTopPage(duration = 500) { function handleToTopPage(duration = 500) {
uni.pageScrollTo({ uni.pageScrollTo({
scrollTop: 0, scrollTop: 0,
duration, duration,
fail: (err) => { fail: (err) => {
console.error('回顶失败', err) console.error('回顶失败', err)
}, },
}) })
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad(async () => { onLoad(async () => {
await checkPluginAvailable() await checkPluginAvailable()
if (!uniHaloPluginAvailable.value) { if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
handleGetData() handleGetData()
}) })
onPullDownRefresh(() => { onPullDownRefresh(() => {
if (!uniHaloPluginAvailable.value) { if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
isLoadMore.value = false isLoadMore.value = false
queryParams.value.page = 1 queryParams.value.page = 1
handleGetData() handleGetData()
}) })
onReachBottom(() => { onReachBottom(() => {
if (!uniHaloPluginAvailable.value) if (!uniHaloPluginAvailable.value)
return return
if (calcAuditModeEnabled.value) { if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: '没有更多数据了' }) uni.showToast({ icon: 'none', title: '没有更多数据了' })
return return
} }
if (hasNext.value) { if (hasNext.value) {
queryParams.value.page = Number(queryParams.value.page) + 1 queryParams.value.page = Number(queryParams.value.page) + 1
isLoadMore.value = true isLoadMore.value = true
handleGetData() handleGetData()
} }
else { else {
uni.showToast({ icon: 'none', title: '没有更多数据了' }) uni.showToast({ icon: 'none', title: '没有更多数据了' })
} }
}) })
</script> </script>
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page"> <view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 自定义导航 --> <!-- 自定义导航 -->
<uh-navbar default-title="投票中心" title-color="text-gray-900" /> <uh-navbar default-title="投票中心" title-color="text-gray-900" />
<uh-plugin-unavailable <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
v-if="!uniHaloPluginAvailable" :checking="checking" @on-refresh="handlePluginRefresh" />
:plugin-id="pluginId"
:error-text="tips" <template v-else>
:checking="checking" <!-- 顶部搜索框 -->
@on-refresh="handlePluginRefresh" <view class="box-border w-screen px-3 pt-2">
/> <view class="uh-global-card-glass flex h-9 items-center gap-3 rounded-full px-5">
<template v-else> <wd-icon name="search" size="16px" />
<!-- 顶部搜索框 --> <input v-model="queryParams.keyword as string" class="flex-1 text-[26rpx] text-gray-900"
<view class="box-border w-screen px-3 pt-2"> placeholder="搜索投票..." placeholder-class="text-gray-400" confirm-type="search"
<view class="uh-global-card-glass flex h-9 items-center gap-3 rounded-full px-5"> @input="handleOnInput" @confirm="handleOnSearch">
<wd-icon name="search" size="16px" /> <view v-if="queryParams.keyword" class="flex items-center"
<input @click="queryParams.keyword = ''; handleOnSearch()">
v-model="queryParams.keyword as string" <wd-icon name="close" size="14px" />
class="flex-1 text-[26rpx] text-gray-900" </view>
placeholder="搜索投票..." </view>
placeholder-class="text-gray-400" <!-- 筛选栏 -->
confirm-type="search" <view class="box-border flex items-center justify-between mt-1 py-2 gap-x-2">
@input="handleOnInput" <view v-for="f in filterConfig" :key="f.key"
@confirm="handleOnSearch" class="uh-global-card-glass border rounded-full box-border flex flex-1 items-center justify-center gap-1 px-2 py-1 text-gray-500"
> :class="[filterValues[f.key]?'bg-secondary text-gray-900 font-bold':'bg-white/80 text-gray-600']" @click="handleOpenFilter(f)">
<view v-if="queryParams.keyword" class="flex items-center" @click="queryParams.keyword = ''; handleOnSearch()"> <text class="text-xs truncate" >
<wd-icon name="close" size="14px" /> {{ filterLabels[f.key] }}
</view> </text>
</view> <wd-icon name="arrow-down" size="10px" />
<!-- 筛选栏 --> </view>
<view class="flex items-center justify-between py-2"> </view>
<view </view>
v-for="f in filterConfig"
:key="f.key"
class="flex flex-1 items-center justify-center gap-1"
@click="handleOpenFilter(f)"
>
<text class="text-[24rpx]" :class="filterValues[f.key] ? 'text-primary font-bold' : 'text-gray-600'">
{{ filterLabels[f.key] }}
</text>
<wd-icon name="arrow-down" size="10px" color="#9ca3af" />
</view>
</view>
</view>
<!-- 加载/错误/空占位(状态机) --> <!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'"> <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" empty-text="博主还未发布投票~" min-height="65vh" @refresh="handleGetData" />
<uh-data-loading
:loading-status="loadingStatus"
empty-text="博主还未发布投票~"
@refresh="handleGetData"
/>
</view>
<view v-else class="content flex flex-col gap-4 p-3"> <view v-else class="box-border flex flex-col gap-4 p-3">
<block v-if="dataList.length !== 0"> <block v-if="dataList.length !== 0">
<uh-vote-card <uh-vote-card v-for="vote in dataList" :key="vote.metadata?.name" :vote="vote"
v-for="vote in dataList" @click="handleOnVoteClick(vote)" />
:key="vote.metadata?.name" <view class="box-border py-5 text-center text-xs text-gray-400">
:vote="vote" {{ loadMoreText }}
@click="handleOnVoteClick(vote)" </view>
/> </block>
<view class="load-text py-5 text-center text-[24rpx] text-gray-400"> </view>
{{ loadMoreText }} </template>
</view>
<view
class="to-top-btn uh-global-card-glass fixed bottom-[100rpx] right-6 z-6 flex h-[72rpx] w-[72rpx] items-center justify-center rounded-full"
@click="handleToTopPage()"
>
<wd-icon name="arrow-up" size="20px" color="#6b7280" />
</view>
</block>
</view>
</template>
<!-- 筛选弹层 --> <!-- 筛选弹层 -->
<wd-popup v-model="filterPopup.show" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0;"> <uh-glass-popup v-model="filterPopup.show" :z-index="99" position="bottom" custom-class="rounded-2xl">
<view v-if="filterPopup.item" class="box-border p-6 pb-10"> <view v-if="filterPopup.item" class="box-border p-4">
<view class="mb-4 text-center text-[30rpx] font-bold text-gray-900"> <view class="mb-4 text-center text-md font-bold text-gray-900">
{{ filterPopup.item.label }} {{ filterPopup.item.label }}
</view> </view>
<view class="flex flex-col gap-2"> <view class="flex flex-col gap-2">
<view <view v-for="opt in filterPopup.item.options" :key="opt.label"
v-for="opt in filterPopup.item.options" class="uh-global-card-glass border box-border rounded-xl px-5 py-2 text-center text-sm"
:key="opt.label" :class="filterValues[filterPopup.item.key] === opt.value ? 'bg-primary text-gray-900 font-bold' : 'text-gray-700'"
class="box-border rounded-xl px-5 py-3 text-center text-[28rpx]" @click="handleSelectFilter(opt)">
:class="filterValues[filterPopup.item.key] === opt.value ? 'bg-primary/15 text-primary font-bold' : 'bg-[#f6f3ee] text-gray-700'" {{ opt.label }}
@click="handleSelectFilter(opt)" </view>
> </view>
{{ opt.label }} </view>
</view> </uh-glass-popup>
</view> </view>
</view> </template>
</wd-popup>
</view>
</template>
+5 -2
View File
@@ -12,6 +12,7 @@
definePage({ definePage({
style: { style: {
navigationBarTitleText: '分类', navigationBarTitleText: '分类',
navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
backgroundColor: '#f6f3ee', backgroundColor: '#f6f3ee',
}, },
@@ -151,11 +152,13 @@
</script> </script>
<template> <template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page p-3"> <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-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" /> <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" />
<block v-else> <block v-else>
<view class="grid grid-cols-2 gap-2.5"> <view class="box-border grid grid-cols-2 gap-2.5 p-3 pt-0">
<view v-for="(item, index) in dataList" :key="index" <view v-for="(item, index) in dataList" :key="index"
class="uh-global-card-glass relative box-border w-full overflow-hidden rounded-xl" class="uh-global-card-glass relative box-border w-full overflow-hidden rounded-xl"
@click="handleToCategory(item)"> @click="handleToCategory(item)">
+5 -2
View File
@@ -11,6 +11,7 @@
definePage({ definePage({
style: { style: {
navigationBarTitleText: '图库', navigationBarTitleText: '图库',
navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
@@ -191,11 +192,13 @@
<template> <template>
<view class="min-h-screen w-screen flex flex-col bg-page pb-6"> <view class="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" <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"> <wd-sticky v-if="category.list.length!==0" class="w-full">
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3"> <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-block 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 }"
+2 -5
View File
@@ -210,17 +210,14 @@
min-height="36vh" @refresh="handleQuery" /> min-height="36vh" @refresh="handleQuery" />
<block v-else> <block v-else>
<view class="flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home"> <view class="box-border flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home">
<uh-article-card v-for="(article, index) in articleList" :key="index" <uh-article-card v-for="(article, index) in articleList" :key="index"
from="home" :article="article" :audit-mode="calcAuditModeEnabled" from="home" :article="article" :audit-mode="calcAuditModeEnabled"
/> />
</view> </view>
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400"> <view class="box-border mt-3 pb-5 text-center text-xs text-gray-400">
{{ loadMoreText }} {{ loadMoreText }}
</view> </view>
<view v-if="articleList.length > 10" class="to-top-btn" @click="handleToTopPage()">
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
</view>
</block> </block>
</view> </view>
<uh-notify-dialog /> <uh-notify-dialog />
+91 -95
View File
@@ -23,6 +23,7 @@
definePage({ definePage({
style: { style: {
navigationBarTitleText: '瞬间', navigationBarTitleText: '瞬间',
navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
@@ -287,116 +288,111 @@
</script> </script>
<template> <template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page py-3"> <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-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="60vh" @refresh="handleGetData" />
<view v-else class="flex flex-col gap-3 px-3"> <view v-else class="box-border flex flex-col gap-3 p-3 pt-0">
<view v-if="dataList.length === 0"
class="min-h-[70vh] w-full flex items-center justify-center content-empty">
<wd-empty :description="t('common.empty')" />
</view>
<block v-else> <!-- 瞬间卡片-->
<!-- 瞬间卡片--> <view v-for="moment in dataList" :key="moment.metadata.name"
<view v-for="moment in dataList" :key="moment.metadata.name" class="uh-global-card-glass uh-shadow-xs overflow-hidden rounded-xl">
class="moment-card uh-shadow-xs overflow-hidden rounded-[24rpx] bg-white"> <!-- 作者 -->
<!-- 作者 --> <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-1 flex items-center"> <image class="avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full"
<image class="avatar h-[72rpx] w-[72rpx] 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="text-sm text-gray-900 font-bold">
<view class="ml-3 flex flex-col"> {{ moment.owner?.displayName || bloggerInfo.nickname }}
<view class="text-sm text-gray-900 font-bold"> </view>
{{ moment.owner?.displayName || bloggerInfo.nickname }} <view class="mt-0.5 text-xs text-gray-400">
</view> {{ formatMomentTime(moment.spec.releaseTime) }}
<view class="mt-0.5 text-xs text-gray-400">
{{ formatMomentTime(moment.spec.releaseTime) }}
</view>
</view> </view>
</view> </view>
<view class="shrink-0">
<uh-button custom-class="!py-1 bg-secondary font-semibold">详情</uh-button>
</view>
</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 bg-gray-100 p-2 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>
</view>
<!-- 图片 -->
<view v-if="moment.images && moment.images.length !== 0"
class="images flex flex-wrap items-start px-4 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>
</view>
<!-- 音频 -->
<view v-if="moment.audios && moment.audios.length !== 0"
class="audio-list 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="video-list 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-4 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>
</view> </view>
</view> </view>
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500"> <!-- 正文-->
{{ loadMoreText }} <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>
</view> </view>
</block>
<!-- 图片 -->
<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>
</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>
</view>
</view>
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">
{{ loadMoreText }}
</view>
</view> </view>
</template> </template>
</view> </view>