1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00
Files
uni-halo/src/pages-blog/vote-detail/vote-detail.vue
T
小莫唐尼 02fa9e4294 refactor: 重构项目页面与组件,优化整体结构与体验
1.  重构分类页面路由与实现,替换旧分类详情页为文章列表页
2.  统一使用自定义导航栏,替换原有原生导航配置
3.  重构加载状态管理,统一使用useDataLoadingStatus hook
4.  优化插件可用性检查逻辑与组件展示
5.  调整数据加载占位与空状态样式
6.  优化文章卡片样式与交互,新增分类跳转功能
7.  重构设置页枚举选择弹窗,使用wd-picker替换自定义实现
8.  删除废弃的IPluginAvailable类型与相关代码
2026-09-04 20:31:46 +08:00

436 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
/**
* 投票详情页(源自旧项目 pagesA/vote-detail,新建复刻)
* 支持 single/multiple/pk 三种投票类型,已投票展示结果
*/
import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getVoteDetail, submitVote } from '@/api/uni-halo'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { calcVotePercent, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
definePage({
style: {
navigationBarTitleText: '投票详情',
enablePullDownRefresh: true,
navigationStyle: 'custom',
},
})
/* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const submitLoading = ref(false)
const pageTitle = ref('加载中...')
const safeAreaBottom = ref(24)
const name = ref('')
const detail = ref<unknown>(null)
const vote = ref<(IVote & {
spec?: {
title?: string
remark?: string
type?: string
maxVotes?: number
startDate?: string
endDate?: string
timeLimit?: string
canAnonymously?: boolean
options?: (IVoteOption & {
id?: string
title?: string
count?: number
checked?: boolean
isVoted?: boolean
disabled?: boolean
_uh_percent?: number
})[]
isVoted?: boolean
hasEnded?: boolean
disabled?: boolean
_uh_type?: string
_uh_state?: { state: string, color: string }
}
stats?: { voteCount?: number }
}) | null>(null)
const submitForm = ref<{ voteData: string[] }>({ voteData: [] })
/* ---------------- 计算属性 ---------------- */
const isVoted = computed(() => voteCacheUtil.has(name.value))
const isEnded = computed(() => vote.value?.spec?.hasEnded || false)
/* ---------------- 工具 ---------------- */
function formatTime(date?: string, fmt = 'yyyy-MM-dd HH:mm'): string {
// 与旧项目一致:yyyy-MM-dd HH:mm
return date ? formatTimeUtil({ d: date, f: fmt }) : ''
}
function showToast(content: string) {
uni.showToast({ icon: 'none', title: content, mask: true })
}
function handleCalcIsChecked(option: { id?: string }): boolean {
const data = voteCacheUtil.get(name.value)
if (!data)
return false
return data.selected.includes(option.id || '')
}
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
pageTitle.value = '加载中...'
try {
const res = await getVoteDetail(name.value)
const detailRes = res.data as IVoteDetail
const tempVote = detailRes.vote as typeof vote.value
if (tempVote) {
const typeKey = ((tempVote.spec?.type || 'SINGLE') as string).toUpperCase()
pageTitle.value = `投票详情(${VOTE_TYPES[typeKey as keyof typeof VOTE_TYPES] || tempVote.spec?.type}`
tempVote.spec = tempVote.spec || {}
tempVote.spec.isVoted = isVoted.value
tempVote.spec.disabled = isVoted.value
tempVote.spec._uh_type = VOTE_TYPES[typeKey as keyof typeof VOTE_TYPES] || tempVote.spec.type
// 计算状态
const startTime = tempVote.spec.startDate ? new Date(tempVote.spec.startDate).getTime() : Date.now()
const endTime = tempVote.spec.endDate ? new Date(tempVote.spec.endDate).getTime() : Date.now()
const now = Date.now()
if (endTime < now) {
tempVote.spec._uh_state = { state: '已结束', color: 'red' }
tempVote.spec.hasEnded = true
}
else if (startTime > now) {
tempVote.spec._uh_state = { state: '未开始', color: 'orange' }
}
else {
tempVote.spec._uh_state = { state: '进行中', color: 'green' }
}
// 选项计算
// 插件选项为 {id,title},票数在 VoteDetail.voteDataList / Vote.stats.voteDataList
const countList = (detailRes.voteDataList || tempVote.stats?.voteDataList || []) as { id?: string, voteCount?: number }[]
const countMap: Record<string, number> = {}
countList.forEach((item) => {
if (item.id)
countMap[item.id] = item.voteCount || 0
})
tempVote.spec.options = (tempVote.spec.options || []).map((option) => {
const checked = handleCalcIsChecked(option)
const optionWithCount = {
...option,
value: option.id,
label: option.title,
count: countMap[option.id || ''] || 0,
isVoted: isVoted.value,
checked,
disabled: isVoted.value,
}
return {
...optionWithCount,
_uh_percent: calcVotePercent(tempVote, optionWithCount),
}
})
}
vote.value = tempVote
detail.value = res
setTimeout(() => {
updateLoadingStatus(
tempVote ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
)
}, 200)
}
catch (err) {
console.error(err)
updateLoadingStatus(DataLoadingStatusEnum.Error)
pageTitle.value = '加载失败,请重试...'
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 200)
}
}
/* ---------------- 交互 ---------------- */
function handleSelectSingleOption(option: { id?: string }) {
if (vote.value?.spec?._uh_state?.state === '未开始') {
showToast('投票未开始')
return
}
if (vote.value?.spec?.hasEnded)
return
if (vote.value?.spec?.disabled)
return
vote.value!.spec!.options!.forEach((item) => {
item.checked = option.id === item.id
})
submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '')
}
function handleSelectCheckboxOption(option: { id?: string }) {
if (vote.value?.spec?._uh_state?.state === '未开始') {
showToast('投票未开始')
return
}
if (vote.value?.spec?.hasEnded)
return
if (vote.value?.spec?.disabled)
return
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)) {
showToast(`最多选择 ${vote.value.spec.maxVotes}`)
return
}
vote.value!.spec!.options!.forEach((item) => {
if (option.id === item.id) {
item.checked = !item.checked
}
})
submitForm.value.voteData = vote.value!.spec!.options!.filter(x => x.checked).map(item => item.id || '')
}
function handleSubmitTip(text: string) {
showToast(text)
}
async function handleSubmit() {
if (!vote.value?.spec?.canAnonymously) {
uni.showModal({
title: '提示',
content: '该投票不支持匿名,请到博主的 网站端 进行投票!',
cancelColor: '#666666',
cancelText: '关闭',
confirmText: '复制地址',
success: (res) => {
if (res.confirm) {
uni.setClipboardData({
data: import.meta.env.VITE_SERVER_BASEURL || '',
showToast: false,
success: () => {
showToast('复制成功')
},
})
}
},
})
return
}
submitLoading.value = true
uni.showLoading({ title: '正在保存...' })
try {
await submitVote(name.value, submitForm.value, vote.value.spec.canAnonymously)
showToast('提交成功')
voteCacheUtil.set(name.value, {
selected: [...submitForm.value.voteData],
data: vote.value,
})
setTimeout(() => {
uni.startPullDownRefresh()
submitLoading.value = false
}, 1500)
}
catch (err) {
console.error(err)
showToast('提交失败,请重试')
submitLoading.value = false
}
finally {
uni.hideLoading()
}
}
/* ---------------- 生命周期 ---------------- */
onLoad((options) => {
name.value = options?.name || ''
// #ifndef H5
const systemInfo = uni.getSystemInfoSync()
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom ? systemInfo.safeAreaInsets.bottom + 12 : 24
// #endif
handleGetData()
})
onPullDownRefresh(() => {
handleGetData()
})
onShareAppMessage(() => ({
path: `/pages-blog/vote-detail/vote-detail?name=${name.value}`,
title: vote.value?.spec?.title || '来投个票吧',
imageUrl: '',
}))
onShareTimeline(() => ({
title: vote.value?.spec?.title || '来投个票吧',
query: name.value ? `name=${name.value}` : '',
imageUrl: '',
}))
</script>
<template>
<view class="app-page box-border min-h-screen w-screen flex flex-col pb-[160rpx]" style="background-color: #fafafd;">
<!-- 自定义导航 -->
<uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
:loading-status="loadingStatus"
empty-text="未查询到数据"
@refresh="handleGetData"
/>
</view>
<block v-else>
<template v-if="vote">
<!-- 投票信息 -->
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
<view class="sub-title relative box-border pl-6 text-[30rpx]">
投票信息
</view>
<view class="vote-card-body flex flex-col gap-3 rounded-xl bg-[#f3f4f6] p-6 text-[28rpx] text-[#3f3f3f]">
<view class="info-row">
<text>投票类型</text>
<text class="tag">{{ vote.spec?._uh_type }}</text>
</view>
<view class="info-row">
<text>投票状态</text>
<text class="tag" :style="{ color: vote.spec?._uh_state?.color }">{{ vote.spec?._uh_state?.state }}</text>
</view>
<view class="info-row">
<text>投票方式</text>
<text class="tag" :class="vote.spec?.canAnonymously ? 'text-[#03a9f4]' : 'text-[#f44336]'">
{{ vote.spec?.canAnonymously ? '匿名' : '不匿名' }}
</text>
</view>
<view class="info-row">
<text>开始时间{{ formatTime(vote.spec?.startDate) }}</text>
</view>
<view class="info-row">
<text v-if="vote.spec?.timeLimit === 'permanent'">结束时间永久有效</text>
<text v-else>结束时间{{ formatTime(vote.spec?.endDate) }}</text>
</view>
</view>
</view>
<!-- 投票内容 -->
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
<view class="sub-title relative box-border pl-6 text-[30rpx]">
投票内容
</view>
<view class="sub-content mb-3 pt-3 text-[30rpx] text-[#2b2f33] font-bold">
{{ vote.spec?.title }}
</view>
<view v-if="vote.spec?.remark" class="sub-remark mb-9 pt-3 text-[28rpx] text-[#3f3f3f]">
{{ vote.spec.remark }}
</view>
<view class="sub-title relative box-border pl-6 text-[30rpx]">
投票选项
<text v-if="vote.spec?.type === 'multiple'" class="sub-title-count text-[24rpx] font-normal">最多选择 {{ vote.spec?.maxVotes }} </text>
</view>
<view class="options mt-6 flex flex-col gap-4">
<template v-if="isVoted || isEnded">
<view
v-for="(option, optionIndex) in vote.spec?.options"
:key="optionIndex"
class="is-voted-item relative box-border min-h-[72rpx] overflow-hidden rounded-xl text-[24rpx]"
:class="option.checked ? 'bg-[#03a9f4]/35 text-white' : 'bg-[#e5e5e5]/75'"
:style="{ '--percent': `${option._uh_percent}%` }"
>
<view class="is-voted-item-content relative z-2 box-border min-h-[72rpx] px-6 py-3">
<view class="flex items-center justify-between">
<view class="flex-1 text-left">
{{ option.title }}
</view>
<view class="shrink-0">
{{ option._uh_percent }}%
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view
v-for="(option, optionIndex) in vote.spec?.options"
:key="optionIndex"
class="vote-select-option box-border rounded-xl bg-[#f3f4f6] px-6 py-5 text-[24rpx]"
:class="option.checked ? 'border-2 border-[#03a9f4] bg-[#03a9f4]/15 text-[#03a9f4]' : ''"
@click="vote.spec?.type === 'multiple' ? handleSelectCheckboxOption(option) : handleSelectSingleOption(option)"
>
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}` : '' }}{{ option.title }}
</view>
</template>
</view>
</view>
<!-- 投票统计 -->
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
<view class="sub-title relative box-border pl-6 text-[30rpx]">
投票统计
</view>
<view class="stat-text mt-3 text-[26rpx] text-[#606266]">
{{ vote.stats?.voteCount || 0 }} 人已参与
</view>
</view>
<!-- 提交按钮 -->
<view class="vote-submit fixed bottom-0 left-0 z-99 box-border w-screen border-t-2 border-[#eee] bg-white/98 px-9 py-6 shadow-sm" :style="{ paddingBottom: `${safeAreaBottom}rpx` }">
<wd-button v-if="isVoted" disabled block>
您已参与投票
</wd-button>
<wd-button v-else-if="vote.spec?._uh_state?.state === '未开始'" plain block type="warning" @click="handleSubmitTip('投票未开始')">
投票未开始
</wd-button>
<wd-button v-else-if="vote.spec?._uh_state?.state === '已结束'" plain block type="danger" @click="handleSubmitTip('投票已结束')">
投票已结束
</wd-button>
<wd-button v-else-if="!vote.spec?.canAnonymously" plain block type="danger" @click="handleSubmit()">
不支持匿名投票
</wd-button>
<wd-button v-else-if="submitForm.voteData.length === 0" plain block @click="handleSubmitTip('请选择选项')">
提交投票请选择选项
</wd-button>
<wd-button v-else block type="primary" :loading="submitLoading" :disabled="submitLoading" @click="handleSubmit()">
提交投票
</wd-button>
</view>
</template>
</block>
</view>
</template>
<style scoped lang="scss">
.vote-card {
.sub-title {
&::before {
content: '';
width: 8rpx;
height: 28rpx;
position: absolute;
left: 0;
top: 6rpx;
background: #03a9f4;
border-radius: 6rpx;
}
}
.is-voted-item {
&::before {
content: '';
width: var(--percent);
position: absolute;
left: 0;
top: 0;
bottom: 0;
background-color: #d0d0d0;
z-index: 0;
border-radius: 6rpx;
}
}
}
</style>