mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
feat: 新增文章投票功能,优化多项页面细节
1. 新增文章详情页投票组件,支持投票插件检查、展开收起 2. 新增下划线文本、投票卡片、投票列表页组件 3. 优化自定义tabbar样式与返回逻辑 4. 更新投票工具类与投票详情页逻辑 5. 优化公告详情页样式与功能 6. 修复部分样式与类型定义问题
This commit is contained in:
@@ -1,175 +1,99 @@
|
||||
<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 { VOTE_STATES } from '@/utils/vote'
|
||||
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
|
||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||
|
||||
interface IVoteCardOption {
|
||||
id?: string
|
||||
title?: string
|
||||
checked?: boolean
|
||||
_uh_percent?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface IVoteCardData {
|
||||
metadata?: { name?: string, [key: string]: unknown }
|
||||
spec?: {
|
||||
title?: string
|
||||
remark?: string
|
||||
type?: string
|
||||
timeLimit?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
isVoted?: boolean
|
||||
_uh_type?: string
|
||||
_uh_state?: { state: string, color: string }
|
||||
options?: IVoteCardOption[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
stats?: { voteCount?: number, voteDataList?: { id?: string, voteCount?: number }[] }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
voteName: string
|
||||
vote: IVoteCardData
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-vote-success'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
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>>({})
|
||||
|
||||
/** 投票状态(基于插件字段 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)
|
||||
return '投票已结束'
|
||||
if (voteState.value === VOTE_STATES.VOTED)
|
||||
return '已参与'
|
||||
if (voteState.value === VOTE_STATES.VOTING)
|
||||
return '投票中'
|
||||
return '开始投票'
|
||||
})
|
||||
|
||||
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))
|
||||
/** 跳转投票详情 */
|
||||
function handleToDetail() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/vote-detail/vote-detail?name=${props.vote?.metadata?.name || ''}`,
|
||||
})
|
||||
}
|
||||
|
||||
async function handleGetData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVoteDetail(props.voteName)
|
||||
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)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
/** 格式化时间 */
|
||||
function formatTime(date?: string, fmt = 'yyyy-MM-dd HH:mm'): string {
|
||||
return date ? formatTimeUtil({ d: date, f: fmt }) : ''
|
||||
}
|
||||
|
||||
function handleSelectOption(option: IVoteOption) {
|
||||
if (voteState.value !== VOTE_STATES.VOTING)
|
||||
return
|
||||
const optionName = option.id || ''
|
||||
if (isSingle.value) {
|
||||
voteTypes.value = [optionName]
|
||||
}
|
||||
else {
|
||||
const index = voteTypes.value.indexOf(optionName)
|
||||
if (index === -1) {
|
||||
voteTypes.value.push(optionName)
|
||||
}
|
||||
else {
|
||||
voteTypes.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (voteTypes.value.length === 0) {
|
||||
uni.showToast({ icon: 'none', title: '请先选择选项' })
|
||||
return
|
||||
}
|
||||
isSubmit.value = true
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
isSubmit.value = false
|
||||
emit('on-vote-success')
|
||||
}, 500)
|
||||
}
|
||||
|
||||
watch(() => props.voteName, () => {
|
||||
handleGetData()
|
||||
}, { immediate: true })
|
||||
|
||||
defineExpose({ refresh: handleGetData })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="uh-vote-card uh-global-card-glass box-border w-full rounded-2xl p-4">
|
||||
<view v-if="loading" class="loading py-6">
|
||||
<wd-skeleton :row="2" :animated="true" />
|
||||
<view class="uh-vote-card uh-global-card-glass box-border w-full rounded-2xl p-4" @click="handleToDetail">
|
||||
<!-- 头部:类型/状态/已投票 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex flex-wrap items-center gap-1">
|
||||
<text
|
||||
v-if="vote.spec?._uh_type"
|
||||
class="rounded-md bg-secondary px-1.5 py-0.5 text-[22rpx] text-[#4d7c0f]"
|
||||
>
|
||||
{{ vote.spec._uh_type }}
|
||||
</text>
|
||||
<text
|
||||
v-if="vote.spec?._uh_state"
|
||||
class="rounded-md px-1.5 py-0.5 text-[22rpx]"
|
||||
:style="{ color: vote.spec._uh_state.color, backgroundColor: `${vote.spec._uh_state.color}1a` }"
|
||||
>
|
||||
{{ vote.spec._uh_state.state }}
|
||||
</text>
|
||||
<text v-if="vote.spec?.isVoted" class="rounded-md bg-primary/15 px-1.5 py-0.5 text-[22rpx] text-primary">
|
||||
已投票
|
||||
</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="32rpx" class="text-primary" />
|
||||
</view>
|
||||
|
||||
<view v-else-if="voteData" class="vote-body">
|
||||
<view class="vote-title text-[30rpx] text-gray-900 font-bold">
|
||||
{{ voteData.spec?.title }}
|
||||
</view>
|
||||
<view v-if="voteData.spec?.remark" class="vote-desc mt-1 text-[24rpx] text-gray-400">
|
||||
{{ voteData.spec.remark }}
|
||||
</view>
|
||||
<!-- 标题 + 备注 -->
|
||||
<view class="vote-title mt-2 text-[30rpx] font-bold text-gray-900">
|
||||
{{ vote.spec?.title }}
|
||||
</view>
|
||||
<view v-if="vote.spec?.remark" class="vote-desc mt-1 text-[24rpx] text-gray-400">
|
||||
{{ vote.spec.remark }}
|
||||
</view>
|
||||
|
||||
<view class="options mt-5">
|
||||
<view
|
||||
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.id || '') ? 'border-[#b9e424] bg-[#f0f7d9]' : 'border-transparent bg-[#f6f3ee]'"
|
||||
@click="handleSelectOption(option)"
|
||||
>
|
||||
<view class="option-label text-[28rpx] text-gray-700">
|
||||
<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: `${handleCalcPercent(option)}%`, background: 'linear-gradient(90deg, #B9E424, #D7F94C)' }" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="voteState === VOTE_STATES.VOTING" class="submit-btn mt-3">
|
||||
<wd-button type="primary" size="small" block :loading="isSubmit" @click="handleSubmit">
|
||||
{{ voteLabel }}
|
||||
</wd-button>
|
||||
</view>
|
||||
<view v-else class="vote-tip mt-4 text-center text-[24rpx] text-gray-400">
|
||||
{{ voteLabel }}{{ voteResultLabel }}
|
||||
</view>
|
||||
<!-- 底部:时间 + 参与人数 -->
|
||||
<view class="vote-card-foot mt-3 flex items-center justify-between border-t border-black/5 pt-2">
|
||||
<text v-if="vote.spec?.timeLimit === 'permanent'" class="text-[22rpx] text-gray-400">
|
||||
结束:永久有效
|
||||
</text>
|
||||
<text v-else-if="vote.spec?._uh_state?.state === '未开始'" class="text-[22rpx] text-gray-400">
|
||||
开始:{{ formatTime(vote.spec?.startDate) }}
|
||||
</text>
|
||||
<text v-else class="text-[22rpx] text-gray-400">
|
||||
结束:{{ formatTime(vote.spec?.endDate) }}
|
||||
</text>
|
||||
<text class="text-[22rpx] text-gray-400">{{ vote.stats?.voteCount || 0 }} 人已参与</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user