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

refactor: 优化投票相关组件与工具代码格式与样式

1. 统一uh-article-vote与uh-article-vote-item组件代码缩进与格式
2. 更新投票提示文案与文章卡片样式适配
3. 规范vote.ts工具类代码格式与分号使用
4. 调整文章卡片布局与置顶样式
This commit is contained in:
小莫唐尼
2026-09-09 13:22:50 +08:00
parent 5b091d3c82
commit 004bac175e
4 changed files with 509 additions and 513 deletions
@@ -13,9 +13,13 @@
contentWrapper: string
footer: string
authorGroup: string
avatar: string
nickname: string
infoCol: string
time: string
tagCategory: string
visits: string
pinned: string
}
/** 旧版全局 cardType → 新版 layout;未知值(如 only_text)由 effectiveLayout 兜底 image_top */
@@ -34,19 +38,27 @@
contentWrapper: 'w-full',
footer: 'flex items-center',
authorGroup: 'flex-1 items-center justify-start gap-x-1',
avatar: '',
nickname: '',
infoCol: 'items-center gap-x-1',
time: 'flex-1 text-center',
tagCategory: '',
visits: 'flex-1 justify-end',
pinned: 'right-4 top-4',
},
image_bottom: {
container: 'flex flex-col gap-y-2',
cover: 'order-2',
contentWrapper: 'w-full',
footer: 'flex items-center',
authorGroup: 'flex-1 items-center justify-center gap-x-1',
time: 'flex-1 text-center',
footer: 'order-first flex items-center mb-1',
authorGroup: 'items-center gap-x-2',
avatar: '!h-9 !w-9 !rounded-xl',
nickname: '!text-sm',
infoCol: 'flex-col items-start leading-tight',
time: '',
tagCategory: '',
visits: 'flex-1 justify-center',
visits: '',
pinned: 'right-3 top-3',
},
image_left: {
container: 'flex gap-x-3 !p-2',
@@ -54,9 +66,13 @@
contentWrapper: 'w-0 flex-1 justify-between',
footer: 'flex items-center justify-between',
authorGroup: 'items-center gap-x-1',
avatar: '',
nickname: '',
infoCol: 'items-center gap-x-1',
time: '!hidden',
tagCategory: '!hidden',
visits: '',
pinned: 'left-3 top-3',
},
image_right: {
container: 'flex gap-x-3 !p-2',
@@ -64,9 +80,13 @@
contentWrapper: 'order-1 w-0 flex-1 justify-between',
footer: 'flex items-center justify-between',
authorGroup: 'items-center gap-x-1',
avatar: '',
nickname: '',
infoCol: 'items-center gap-x-1',
time: '!hidden',
tagCategory: '!hidden',
visits: '',
pinned: 'right-3 top-3',
},
}
@@ -104,6 +124,9 @@
const cardLayout = computed(() => CARD_LAYOUTS[effectiveLayout.value])
/** 社交卡片形态(封面在下):左上用户信息(头像 + 昵称/日期垂直)、右上浏览数 */
const isSocialCard = computed(() => effectiveLayout.value === 'image_bottom')
const publishTimeText = computed(() => {
const time = props.article.spec.publishTime
return time ? formatTime({ d: time, f: 'yyyy/MM/dd' }) : ''
@@ -137,7 +160,8 @@
<view class="uh-global-card-glass uh-shadow-xs relative overflow-hidden rounded-xl p-3"
:class="cardLayout.container" @click.stop="handleToArticleDetail()">
<text v-if="article.spec.pinned"
class="text-gray-60 absolute left-2 top-2 z-1 rounded-lg bg-secondary px-2 py-1 text-xs">
class="box-border uh-global-card-glass border text-gray-900 absolute z-1 rounded-md bg-secondary px-1.5 py-0.5 text-xs"
:class="cardLayout.pinned">
置顶
</text>
<image :class="[isGrid ? 'w-full h-24 rounded-lg' : 'w-full h-36 rounded-lg', cardLayout.cover]"
@@ -152,13 +176,13 @@
<view v-if="!isGrid" class="my-1 box-border flex flex-wrap gap-2" :class="cardLayout.tagCategory">
<template v-if="article.categories && article.categories.length !== 0">
<text v-for="cate in article.categories" :key="cate.metadata.name"
class="rounded-xl bg-secondary px-2 py-1 text-xs" @click.stop="handleToCategory(cate)">
class="box-border uh-global-card-glass border uh-shadow-xs rounded-xl bg-secondary px-2 py-0.5 text-xs" @click.stop="handleToCategory(cate)">
{{ cate.spec.displayName }}
</text>
</template>
<template v-if="article.tags && article.tags.length !== 0">
<text v-for="tag in article.tags" :key="tag.metadata.name"
class="rounded-xl bg-secondary px-2 py-1 text-xs">
class="box-border uh-global-card-glass border uh-shadow-xs rounded-xl bg-secondary px-2 py-0.5 text-xs">
# {{ tag.spec.displayName }}
</text>
</template>
@@ -166,11 +190,25 @@
<view class="flex items-center text-xs text-gray-500" :class="cardLayout.footer">
<view v-if="!isGrid" class="flex items-center" :class="cardLayout.authorGroup">
<image :src="article.owner.avatar" class="uh-global-card-glass h-5 w-5 rounded-full"
mode="aspectFill" />
<text class="truncate">{{ article.owner.displayName }}</text>
:class="cardLayout.avatar" mode="aspectFill" />
<template v-if="isSocialCard">
<view class="flex" :class="cardLayout.infoCol">
<text class="truncate" :class="cardLayout.nickname">{{ article.owner.displayName }}</text>
<view class="flex items-center gap-x-2">
<text class="text-gray-400" :class="cardLayout.time">{{ publishTimeText }}</text>
<view class="visits flex items-center gap-x-1 text-gray-400">
浏览
<text class="number">{{ visitCount }}</text>
</view>
<text v-if="!isGrid" class="text-gray-400" :class="cardLayout.time">{{ publishTimeText }}</text>
<view class="visits flex items-center gap-x-1" :class="cardLayout.visits">
</view>
</view>
</template>
<text v-else class="truncate">{{ article.owner.displayName }}</text>
</view>
<text v-if="!isGrid && !isSocialCard" class="text-gray-400"
:class="cardLayout.time">{{ publishTimeText }}</text>
<view v-if="!isSocialCard" class="visits flex items-center gap-x-1" :class="cardLayout.visits">
浏览
<text class="number">{{ visitCount }}</text>
@@ -1,8 +1,4 @@
<script lang="ts" setup>
/**
* 文章详情投票单卡(源自旧项目 components/article-vote,新建复刻)
* 支持 single/multiple/pk 三种类型,已投票/已结束展示百分比,可直接提交
*/
import { computed, ref, watch } from 'vue'
import { getVoteDetail, submitVote } from '@/api/uni-halo'
import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
@@ -19,31 +15,24 @@ const loadingText = ref('加载中,请稍等...')
const isSubmit = ref(false)
const voteData = ref<IVote | null>(null)
const submitForm = ref<{ voteData : string[] }>({ voteData: [] })
/** 选项 id → 票数(来自 VoteDetail.voteDataList 或 Vote.stats.voteDataList) */
const voteCountMap = ref<Record<string, number>>({})
/** 是否已投票(本地缓存) */
const isVoted = computed(() => voteCacheUtil.has(props.voteId))
/** 投票展示状态(与旧项目一致:未开始/进行中/已结束) */
const voteState = computed(() => {
if (!voteData.value?.spec)
return null
if (!voteData.value?.spec) { return null }
return calcVoteState(voteData.value)
})
/** 类型中文名 */
const voteTypeLabel = computed(() => {
const type = voteData.value?.spec?.type
return type ? VOTE_TYPES[type] || type : ''
})
/** 选项票数占比(取自 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
if (total === 0) { return 0 }
return Number(((count / total) * 100).toFixed(2))
}
@@ -78,14 +67,12 @@ async function handleGetData() {
function handleSelectSingleOption(option : IVoteOption) {
const spec = voteData.value?.spec
if (!spec)
return
if (!spec) { return }
if (voteState.value?.state === '未开始') {
showToast('投票未开始')
return
}
if (spec.hasEnded || isVoted.value)
return
if (spec.hasEnded || isVoted.value) { return }
spec.options?.forEach((item) => {
item.checked = option.id === item.id
})
@@ -94,14 +81,12 @@ function handleSelectSingleOption(option: IVoteOption) {
function handleSelectCheckboxOption(option : IVoteOption) {
const spec = voteData.value?.spec
if (!spec)
return
if (!spec) { return }
if (voteState.value?.state === '未开始') {
showToast('投票未开始')
return
}
if (spec.hasEnded || isVoted.value)
return
if (spec.hasEnded || isVoted.value) { return }
const checkedList = (spec.options || []).filter(x => x.checked && x.id !== option.id)
// maxVotes 缺失(0/undefined)时不限制多选数量,避免 0 >= 0 恒真导致无法选择
@@ -125,8 +110,7 @@ function handleSubmitTip(text: string) {
async function handleSubmit() {
const spec = voteData.value?.spec
if (!spec)
return
if (!spec) { return }
if (submitForm.value.voteData.length === 0) {
showToast('请先选择选项')
return
@@ -196,57 +180,51 @@ defineExpose({ refresh: handleGetData })
</script>
<template>
<view class="uh-article-vote-item uh-global-card-glass box-border w-full rounded-2xl p-4">
<!-- 加载失败:点击重试 -->
<view v-if="loading === 'error'" class="vote-error py-6 text-center text-[24rpx] text-gray-400" @click="handleGetData">
<view class="uh-global-card-glass shadow-none border border-primary box-border w-full rounded-lg p-3">
<view v-if="loading === 'error'" class="vote-error py-6 text-center text-xs text-gray-400"
@click="handleGetData">
{{ loadingText }}
</view>
<!-- 加载中 -->
<view v-else-if="loading === 'loading'" class="loading py-6">
<wd-skeleton :row="3" :animated="true" />
</view>
<template v-else-if="voteData">
<!-- 头部:序号/类型/状态 + 查看详情 -->
<view class="vote-card-head">
<view class="flex items-center justify-between">
<view class="flex flex-wrap items-center gap-1">
<text v-if="props.index !== undefined" class="rounded bg-orange-500 px-2 py-0.5 text-[22rpx] text-white">
<text v-if="props.index !== undefined"
class="rounded-md bg-orange-500 px-2 py-0.5 text-xs text-white">
{{ props.index + 1 }}
</text>
<text v-if="voteTypeLabel" class="rounded bg-primary/15 px-2 py-0.5 text-[22rpx] text-primary">
<text v-if="voteTypeLabel" class="rounded-md bg-primary px-2 py-0.5 text-xs text-gray-900">
{{ voteTypeLabel }}
</text>
<text
v-if="voteState"
class="rounded px-2 py-0.5 text-[22rpx]"
:style="{ color: voteState.color, backgroundColor: `${voteState.color}1a` }"
>
<text v-if="voteState" class="rounded-md px-2 py-0.5 text-xs"
:style="{ color: voteState.color, backgroundColor: `${voteState.color}1a` }">
{{ voteState.state }}
</text>
</view>
<text class="shrink-0 text-[22rpx] text-gray-400" @click="handleToVoteDetail">查看投票详情 ></text>
<view class="shrink-0 flex items-center justify-center text-xs text-gray-400"
@click="handleToVoteDetail"> 投票详情 <wd-icon name="right" class="mt-0.5" /> </view>
</view>
<view class="title mt-2 text-[30rpx] text-gray-900 font-bold">
<view class="title mt-2 text-sm text-gray-900 font-bold">
{{ voteData.spec?.title }}
</view>
</view>
<view class="vote-card-body mt-3">
<view v-if="voteData.spec?.remark" class="remark mb-3 text-[24rpx] text-gray-400">
<view class="vote-card-body mt-2">
<view v-if="voteData.spec?.remark" class="remark mb-3 text-xs text-gray-400">
{{ voteData.spec.remark }}
</view>
<!-- 单选 -->
<view v-if="voteData.spec?.type === 'single'" class="flex flex-col gap-2">
<template v-if="showResult">
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="is-voted-item relative box-border min-h-[72rpx] overflow-hidden rounded-xl text-[24rpx]"
:class="option.checked ? 'bg-primary/40 text-[#4d7c0f] font-bold' : 'bg-[#e5e5e5]/75'"
:style="{ '--percent': `${handleCalcPercent(option)}%` }"
>
:style="{ '--percent': `${handleCalcPercent(option)}%` }">
<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">
@@ -260,13 +238,10 @@ defineExpose({ refresh: handleGetData })
</view>
</template>
<template v-else>
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="vote-select-option box-border rounded-xl bg-[#f6f3ee] px-5 py-4 text-[24rpx]"
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
@click="handleSelectSingleOption(option)"
>
@click="handleSelectSingleOption(option)">
{{ option.title }}
</view>
</template>
@@ -275,13 +250,10 @@ defineExpose({ refresh: handleGetData })
<!-- 多选 -->
<view v-else-if="voteData.spec?.type === 'multiple'" class="flex flex-col gap-2">
<template v-if="showResult">
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="is-voted-item relative box-border min-h-[72rpx] overflow-hidden rounded-xl text-[24rpx]"
:class="option.checked ? 'bg-primary/40 text-[#4d7c0f] font-bold' : 'bg-[#e5e5e5]/75'"
:style="{ '--percent': `${handleCalcPercent(option)}%` }"
>
:style="{ '--percent': `${handleCalcPercent(option)}%` }">
<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">
@@ -295,13 +267,10 @@ defineExpose({ refresh: handleGetData })
</view>
</template>
<template v-else>
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="vote-select-option box-border rounded-xl bg-[#f6f3ee] px-5 py-4 text-[24rpx]"
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
@click="handleSelectCheckboxOption(option)"
>
@click="handleSelectCheckboxOption(option)">
{{ option.title }}
</view>
</template>
@@ -311,28 +280,22 @@ defineExpose({ refresh: handleGetData })
<view v-else-if="voteData.spec?.type === 'pk'" class="flex flex-col gap-2">
<!-- PK 对抗条 -->
<view class="pk-container box-border w-full flex">
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
class="radio-item"
:class="optionIndex === 0 ? 'radio-left' : 'radio-right'"
:style="{ width: `${handleCalcPercent(option)}%` }"
>
<view class="option-item box-border w-full rounded-xl p-4" :class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'">
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="radio-item" :class="optionIndex === 0 ? 'radio-left' : 'radio-right'"
:style="{ width: `${handleCalcPercent(option)}%` }">
<view class="option-item box-border w-full rounded-lg px-3 py-2"
:class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'">
{{ handleCalcPercent(option) }}%
</view>
</view>
</view>
<!-- PK 选项列表 -->
<template v-if="showResult">
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
class="is-voted-item relative box-border min-h-[72rpx] overflow-hidden rounded-xl text-[24rpx]"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="is-voted-item relative box-border overflow-hidden rounded-lg text-xs"
:class="option.checked ? 'bg-primary/40 text-[#4d7c0f] font-bold' : 'bg-[#e5e5e5]/75'"
:style="{ '--percent': `${handleCalcPercent(option)}%` }"
>
<view class="is-voted-item-content relative z-2 box-border min-h-[72rpx] px-6 py-3">
:style="{ '--percent': `${handleCalcPercent(option)}%` }">
<view class="is-voted-item-content relative z-2 box-border px-3 py-2">
<view class="flex items-center justify-between">
<view class="flex-1 text-left">
选项{{ optionIndex + 1 }}{{ option.title }}
@@ -345,13 +308,10 @@ defineExpose({ refresh: handleGetData })
</view>
</template>
<template v-else>
<view
v-for="(option, optionIndex) in voteData.spec?.options || []"
:key="optionIndex"
<view v-for="(option, optionIndex) in voteData.spec?.options || []" :key="optionIndex"
class="vote-select-option box-border rounded-xl bg-[#f6f3ee] px-5 py-4 text-[24rpx]"
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
@click="handleSelectSingleOption(option)"
>
@click="handleSelectSingleOption(option)">
选项{{ optionIndex + 1 }}{{ option.title }}
</view>
</template>
@@ -359,19 +319,19 @@ defineExpose({ refresh: handleGetData })
</view>
<!-- 底部:时间 + 参与人数/已投票 -->
<view class="vote-card-foot mt-3 flex items-center justify-between border-t border-[#f7f7f7] pt-2">
<text v-if="voteData.spec?.timeLimit === 'permanent'" class="text-[22rpx] text-gray-400">
<view class="mt-3 flex items-center justify-between pt-2">
<text v-if="voteData.spec?.timeLimit === 'permanent'" class="text-xs text-gray-400">
结束永久有效
</text>
<text v-else-if="voteState?.state === '未开始'" class="text-[22rpx] text-gray-400">
<text v-else-if="voteState?.state === '未开始'" class="text-xs text-gray-400">
开始{{ formatTime(voteData.spec?.startDate) }}
</text>
<text v-else class="text-[22rpx] text-gray-400">
<text v-else class="text-xs text-gray-400">
结束{{ formatTime(voteData.spec?.endDate) }}
</text>
<view class="flex items-center gap-2">
<text class="text-[22rpx] text-gray-400">{{ voteData.stats?.voteCount || 0 }} 人已参与</text>
<text v-if="isVoted" class="rounded bg-primary/15 px-2 py-0.5 text-[22rpx] text-primary">已投票</text>
<text class="text-xs text-gray-400">{{ voteData.stats?.voteCount || 0 }} 人已参与</text>
<text v-if="isVoted" class="rounded bg-primary/15 px-2 py-0.5 text-xs text-primary">已投票</text>
</view>
</view>
@@ -380,10 +340,12 @@ defineExpose({ refresh: handleGetData })
<wd-button v-if="isVoted" disabled block>
您已参与投票
</wd-button>
<wd-button v-else-if="voteState?.state === '未开始'" plain block type="warning" @click="handleSubmitTip('投票未开始')">
<wd-button v-else-if="voteState?.state === '未开始'" plain block type="warning"
@click="handleSubmitTip('投票未开始')">
投票未开始
</wd-button>
<wd-button v-else-if="voteState?.state === '已结束'" plain block type="danger" @click="handleSubmitTip('投票已结束')">
<wd-button v-else-if="voteState?.state === '已结束'" plain block type="danger"
@click="handleSubmitTip('投票已结束')">
投票已结束
</wd-button>
<wd-button v-else-if="!voteData.spec?.canAnonymously" plain block type="danger" @click="handleSubmit()">
@@ -1,9 +1,4 @@
<script lang="ts" setup>
/**
* 文章详情"相关投票"区块容器(源自旧项目 article-detail 的 vote-wrap 区块)
* 只接收 voteIds;插件可用性检查/展开收起/区块标题/卡片列表全部内置
* 插件未激活或没有投票数据时,整个组件不渲染
*/
import { onMounted, ref } from 'vue'
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
@@ -11,13 +6,11 @@ const props = defineProps<{
voteIds : string[]
}>()
/** 展开/收起(与旧项目 voteIsOpen 一致) */
const voteIsOpen = ref(true)
/** 投票插件可用性(检查失败/未安装时整个区块不显示) */
const { available: votePluginAvailable, check: checkVotePluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginVote,
tips: '检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员',
tips: '啊偶,功能正在维护中...',
})
onMounted(() => {
@@ -26,27 +19,26 @@ onMounted(() => {
</script>
<template>
<view v-if="votePluginAvailable && voteIds.length > 0" class="box-border px-1">
<view class="uh-global-card-glass uh-shadow-xs mb-3 rounded-xl p-3">
<view class="flex items-center justify-between">
<uh-section-title>相关投票</uh-section-title>
<text class="text-[24rpx] text-gray-400" @click="voteIsOpen = !voteIsOpen">
{{ voteIsOpen ? '收起' : '展开' }}
<view v-if="votePluginAvailable && voteIds.length > 0" class="box-border px-3">
<view class="box-border uh-global-card-glass mb-3 rounded-xl p-3">
<uh-section-title>
相关投票
<template #right>
<text class="text-xs text-gray-400" @click="voteIsOpen = !voteIsOpen">
<wd-icon :name="voteIsOpen?'up':'down'"></wd-icon> {{ voteIsOpen ? '收起' : '展开' }}
</text>
</view>
</template>
</uh-section-title>
<template v-if="voteIsOpen">
<view class="mt-3 flex flex-col gap-3">
<uh-article-vote-item
v-for="(voteId, voteIdIndex) in voteIds"
:key="voteId"
:vote-id="voteId"
:index="voteIdIndex"
/>
<uh-article-vote-item v-for="(voteId, voteIdIndex) in voteIds" :key="voteId" :vote-id="voteId"
:index="voteIdIndex" />
</view>
</template>
<view v-if="!voteIsOpen" class="mt-2 text-center text-[24rpx] text-gray-400" @click="voteIsOpen = !voteIsOpen">
<view v-if="!voteIsOpen" class="mt-3 text-center text-xs text-gray-400 py-3 bg-gray-100 rounded-xl"
@click="voteIsOpen = !voteIsOpen">
投票已收起点击展开 {{ voteIds.length }} 个投票项
</view>
</view>
+44 -40
View File
@@ -2,46 +2,46 @@
* 投票工具(源自旧项目 utils/vote.js,按需命名导出)
* 投票状态计算、UID 缓存、投票周期判断
*/
import { getCache, setCache } from './storage'
import { getCache, setCache } from './storage';
/** 投票 UID 缓存 key */
const UnihaloVoteUid = 'unihalo_vote_uid'
const UnihaloVoteUid = 'unihalo_vote_uid';
export type VoteType = 'single' | 'multiple' | 'pk'
export type VoteState = 'not-voted' | 'voting' | 'voted' | 'vote-ended'
export type VoteType = 'single' | 'multiple' | 'pk';
export type VoteState = 'not-voted' | 'voting' | 'voted' | 'vote-ended';
/** 投票类型中文映射(与旧项目一致:key 为插件小写 type) */
export const VOTE_TYPES: Record<string, string> = {
pk: '双选PK',
multiple: '多选',
single: '单选',
}
single: '单选'
};
/** 投票状态常量(内部状态机) */
export const VOTE_STATES: { NOT_VOTED: VoteState, VOTING: VoteState, VOTED: VoteState, VOTE_ENDED: VoteState } = {
export const VOTE_STATES: { NOT_VOTED: VoteState; VOTING: VoteState; VOTED: VoteState; VOTE_ENDED: VoteState } = {
NOT_VOTED: 'not-voted',
VOTING: 'voting',
VOTED: 'voted',
VOTE_ENDED: 'vote-ended',
}
VOTE_ENDED: 'vote-ended'
};
/** 投票展示状态(与旧项目 VOTE_STATES 一致:中文 + 颜色) */
export const VOTE_STATE_LABELS: Record<string, { state: string, color: string }> = {
export const VOTE_STATE_LABELS: Record<string, { state: string; color: string }> = {
: { state: '未开始', color: 'orange' },
: { state: '进行中', color: 'green' },
: { state: '已结束', color: 'red' },
}
: { state: '已结束', color: 'red' }
};
/**
* 获取投票 UID(不存在则生成)
*/
export function getOrCreateVoteUid(): string {
let uid = getCache<string>(UnihaloVoteUid)
let uid = getCache<string>(UnihaloVoteUid);
if (!uid) {
uid = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
setCache(UnihaloVoteUid, uid)
uid = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
setCache(UnihaloVoteUid, uid);
}
return uid
return uid;
}
/**
@@ -50,22 +50,25 @@ export function getOrCreateVoteUid(): string {
* @param vote 投票对象(含 spec.timeLimit/hasEnded/startDate/endDate)
* @returns { state: '未开始' | '进行中' | '已结束', color: 'orange' | 'green' | 'red' }
*/
export function calcVoteState(
vote: { spec?: { timeLimit?: string, hasEnded?: boolean, startDate?: string, endDate?: string, [key: string]: unknown } },
): { state: string, color: string } {
export function calcVoteState(vote: { spec?: { timeLimit?: string; hasEnded?: boolean; startDate?: string; endDate?: string; [key: string]: unknown } }): {
state: string;
color: string;
} {
if (vote.spec?.timeLimit !== 'custom') {
return vote.spec?.hasEnded ? VOTE_STATE_LABELS['已结束'] : VOTE_STATE_LABELS['进行中']
return vote.spec?.hasEnded ? VOTE_STATE_LABELS['已结束'] : VOTE_STATE_LABELS['进行中'];
}
const nowTime = new Date().getTime()
const startTime = vote.spec?.startDate ? new Date(vote.spec.startDate).getTime() : nowTime
const endTime = vote.spec?.endDate ? new Date(vote.spec.endDate).getTime() : nowTime
const nowTime = new Date().getTime();
const startTime = vote.spec?.startDate ? new Date(vote.spec.startDate).getTime() : nowTime;
const endTime = vote.spec?.endDate ? new Date(vote.spec.endDate).getTime() : nowTime;
if (nowTime < startTime)
return VOTE_STATE_LABELS['未开始']
if (nowTime < endTime)
return VOTE_STATE_LABELS['进行中']
return vote.spec?.hasEnded ? VOTE_STATE_LABELS['已结束'] : VOTE_STATE_LABELS['进行中']
if (nowTime < startTime) {
return VOTE_STATE_LABELS['未开始'];
}
if (nowTime < endTime) {
return VOTE_STATE_LABELS['进行中'];
}
return vote.spec?.hasEnded ? VOTE_STATE_LABELS['已结束'] : VOTE_STATE_LABELS['进行中'];
}
/**
@@ -74,19 +77,20 @@ export function calcVoteState(
* @param option 选项(含 count)
*/
export function calcVotePercent(vote: { stats?: { voteCount?: number } }, option: { count?: number }): number {
const total = vote.stats?.voteCount || 0
const count = option.count || 0
if (total === 0)
return 0
return Math.round((count / total) * 100)
const total = vote.stats?.voteCount || 0;
const count = option.count || 0;
if (total === 0) {
return 0;
}
return Math.round((count / total) * 100);
}
/** 投票缓存 key 前缀 */
const VOTE_CACHE_KEY = 'unihalo_vote_'
const VOTE_CACHE_KEY = 'unihalo_vote_';
interface IVoteCacheData {
selected: string[]
data: unknown
selected: string[];
data: unknown;
}
/**
@@ -95,14 +99,14 @@ interface IVoteCacheData {
export const voteCacheUtil = {
/** 是否已缓存(已投票) */
has(name: string): boolean {
return !!getCache<IVoteCacheData>(VOTE_CACHE_KEY + name)
return !!getCache<IVoteCacheData>(VOTE_CACHE_KEY + name);
},
/** 获取缓存数据 */
get(name: string): IVoteCacheData | null {
return getCache<IVoteCacheData>(VOTE_CACHE_KEY + name) || null
return getCache<IVoteCacheData>(VOTE_CACHE_KEY + name) || null;
},
/** 写入缓存 */
set(name: string, data: IVoteCacheData): void {
setCache(VOTE_CACHE_KEY + name, data)
},
setCache(VOTE_CACHE_KEY + name, data);
}
};