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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user