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

refactor: 统一加载状态组件,重构投票相关类型与逻辑,新增维护拦截功能

1. 替换所有页面的wd-skeleton与空状态为uh-data-loading组件
2. 重构投票相关API类型与列表/详情逻辑,适配Halo扩展对象结构
3. 新增维护拦截hook与维护页,统一插件可用性与维护模式检查跳转
4. 优化多个页面的代码结构与样式细节
This commit is contained in:
小莫唐尼
2026-09-04 04:19:50 +08:00
parent d0dd7d7a46
commit 2fbd3fb19a
15 changed files with 987 additions and 178 deletions
+56 -15
View File
@@ -1,11 +1,12 @@
<script lang="ts" setup>
/**
* 投票卡片(源自旧项目 components/vote-card,新建复刻)
* 适配 plugin-vote 真实结构:详情接口返回 VoteDetail(嵌套 vote),选项为 {id,title}
*/
import { computed, ref, watch } from 'vue'
import { getVoteDetail } from '@/api/uni-halo'
import { calcVoteState, VOTE_STATES, VOTE_TYPES } from '@/utils/vote'
import type { IVote, IVoteOption } from '@/api/types/uni-halo'
import { VOTE_STATES } from '@/utils/vote'
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
const props = defineProps<{
voteName: string
@@ -20,8 +21,27 @@ const isSubmit = ref(false)
const voteData = ref<IVote | null>(null)
const voteTypes = ref<string[]>([])
const canAnonymously = ref(true)
/** 选项 id → 票数(来自 VoteDetail.voteDataList 或 Vote.stats.voteDataList) */
const voteCountMap = ref<Record<string, number>>({})
const voteState = computed(() => calcVoteState(voteData.value || {}, voteTypes.value, canAnonymously.value))
/** 投票状态(基于插件字段 spec.startDate/endDate/hasEnded/canAnonymously) */
const voteState = computed(() => {
const spec = voteData.value?.spec
if (!spec)
return VOTE_STATES.NOT_VOTED
const now = Date.now()
const startTime = spec.startDate ? new Date(spec.startDate).getTime() : now
const endTime = spec.endDate ? new Date(spec.endDate).getTime() : now
if (spec.hasEnded || endTime < now)
return VOTE_STATES.VOTE_ENDED
if (startTime > now)
return VOTE_STATES.NOT_VOTED
if (voteTypes.value.length !== 0)
return VOTE_STATES.VOTED
if (!spec.canAnonymously)
return VOTE_STATES.NOT_VOTED
return VOTE_STATES.VOTING
})
const voteLabel = computed(() => {
if (voteState.value === VOTE_STATES.VOTE_ENDED)
@@ -35,13 +55,34 @@ const voteLabel = computed(() => {
const voteResultLabel = computed(() => (voteState.value === VOTE_STATES.VOTED ? '查看结果' : ''))
const isSingle = computed(() => voteData.value?.spec?.type === 'single')
/** 选项票数占比(取自 voteCountMap) */
function handleCalcPercent(option: IVoteOption): number {
const total = voteData.value?.stats?.voteCount || 0
const count = voteCountMap.value[option.id || ''] || 0
if (total === 0)
return 0
return Number(((count / total) * 100).toFixed(2))
}
async function handleGetData() {
loading.value = true
try {
const res = await getVoteDetail(props.voteName)
voteData.value = res.data
// 已投票项从缓存恢复(简化:根据 options 计数判断)
const detail = res.data as IVoteDetail
const vote = detail.vote || (detail as unknown as IVote)
voteData.value = vote
canAnonymously.value = !!vote.spec?.canAnonymously
voteTypes.value = []
// 票数映射:详情 voteDataList 优先,其次 stats.voteDataList
const countList = detail.voteDataList?.length ? detail.voteDataList : vote.stats?.voteDataList
const map: Record<string, number> = {}
;(countList || []).forEach((item) => {
if (item.id)
map[item.id] = item.voteCount || 0
})
voteCountMap.value = map
}
catch (err) {
console.error('获取投票失败', err)
@@ -54,8 +95,8 @@ async function handleGetData() {
function handleSelectOption(option: IVoteOption) {
if (voteState.value !== VOTE_STATES.VOTING)
return
const optionName = option.name || ''
if (voteData.value && (voteData.value as { type?: string }).type === VOTE_TYPES.SINGLE) {
const optionName = option.id || ''
if (isSingle.value) {
voteTypes.value = [optionName]
}
else {
@@ -98,25 +139,25 @@ defineExpose({ refresh: handleGetData })
<view v-else-if="voteData" class="vote-body">
<view class="vote-title text-[30rpx] text-gray-900 font-bold">
{{ voteData.title }}
{{ voteData.spec?.title }}
</view>
<view class="vote-desc mt-1 text-[24rpx] text-gray-400">
{{ voteData.description }}
<view v-if="voteData.spec?.remark" class="vote-desc mt-1 text-[24rpx] text-gray-400">
{{ voteData.spec.remark }}
</view>
<view class="options mt-5">
<view
v-for="option in voteData.options"
:key="option.name"
v-for="option in voteData.spec?.options || []"
:key="option.id"
class="option mb-4 flex flex-col border-2 rounded-xl p-5"
:class="voteTypes.includes(option.name || '') ? 'border-[#b9e424] bg-[#f0f7d9]' : 'border-transparent bg-[#f6f3ee]'"
:class="voteTypes.includes(option.id || '') ? 'border-[#b9e424] bg-[#f0f7d9]' : 'border-transparent bg-[#f6f3ee]'"
@click="handleSelectOption(option)"
>
<view class="option-label text-[28rpx] text-gray-700">
<text>{{ option.label }}</text>
<text>{{ option.title }}</text>
</view>
<view v-if="voteState === VOTE_STATES.VOTED" class="option-bar mt-3 h-4 overflow-hidden rounded-lg bg-black/5">
<view class="option-bar-inner h-full rounded-lg" :style="{ width: `${option.count || 0}%`, background: 'linear-gradient(90deg, #B9E424, #D7F94C)' }" />
<view class="option-bar-inner h-full rounded-lg" :style="{ width: `${handleCalcPercent(option)}%`, background: 'linear-gradient(90deg, #B9E424, #D7F94C)' }" />
</view>
</view>
</view>