mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-11 16:00:44 +08:00
feat: 添加恋爱故事模块功能
1. 新增uhlove图标字体库与相关样式 2. 新增恋爱故事页面与恋爱主页 3. 修复投票组件多选限制逻辑bug 4. 优化导航栏组件自定义样式能力 5. 更新滚动置顶组件黑名单与样式 6. 开启本地开发调试模式
This commit is contained in:
@@ -104,8 +104,10 @@ function handleSelectCheckboxOption(option: IVoteOption) {
|
||||
return
|
||||
|
||||
const checkedList = (spec.options || []).filter(x => x.checked && x.id !== option.id)
|
||||
if (spec.type === 'multiple' && checkedList.length >= (spec.maxVotes || 0)) {
|
||||
showToast(`最多选择 ${spec.maxVotes} 项`)
|
||||
// maxVotes 缺失(0/undefined)时不限制多选数量,避免 0 >= 0 恒真导致无法选择
|
||||
const maxVotes = spec.maxVotes
|
||||
if (spec.type === 'multiple' && maxVotes && maxVotes > 0 && checkedList.length >= maxVotes) {
|
||||
showToast(`最多选择 ${maxVotes} 项`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -225,7 +227,7 @@ defineExpose({ refresh: handleGetData })
|
||||
</view>
|
||||
<text class="shrink-0 text-[22rpx] text-gray-400" @click="handleToVoteDetail">查看投票详情 ></text>
|
||||
</view>
|
||||
<view class="title mt-2 text-[30rpx] font-bold text-gray-900">
|
||||
<view class="title mt-2 text-[30rpx] text-gray-900 font-bold">
|
||||
{{ voteData.spec?.title }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -247,8 +249,12 @@ defineExpose({ refresh: handleGetData })
|
||||
>
|
||||
<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">{{ handleCalcPercent(option) }}%</view>
|
||||
<view class="flex-1 text-left">
|
||||
{{ option.title }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
{{ handleCalcPercent(option) }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -278,8 +284,12 @@ defineExpose({ refresh: handleGetData })
|
||||
>
|
||||
<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">{{ handleCalcPercent(option) }}%</view>
|
||||
<view class="flex-1 text-left">
|
||||
{{ option.title }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
{{ handleCalcPercent(option) }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -300,7 +310,7 @@ defineExpose({ refresh: handleGetData })
|
||||
<!-- PK -->
|
||||
<view v-else-if="voteData.spec?.type === 'pk'" class="flex flex-col gap-2">
|
||||
<!-- PK 对抗条 -->
|
||||
<view class="pk-container box-border flex w-full">
|
||||
<view class="pk-container box-border w-full flex">
|
||||
<view
|
||||
v-for="(option, optionIndex) in voteData.spec?.options || []"
|
||||
:key="optionIndex"
|
||||
@@ -324,8 +334,12 @@ defineExpose({ refresh: handleGetData })
|
||||
>
|
||||
<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">选项{{ optionIndex + 1 }}:{{ option.title }}</view>
|
||||
<view class="shrink-0">{{ handleCalcPercent(option) }}%</view>
|
||||
<view class="flex-1 text-left">
|
||||
选项{{ optionIndex + 1 }}:{{ option.title }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
{{ handleCalcPercent(option) }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
titleColor ?: string;
|
||||
scrollTitle ?: string;
|
||||
needPlaceholder ?: boolean;
|
||||
backClass ?: string;
|
||||
backStyle ?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
useBack: true,
|
||||
useTitle: true,
|
||||
needPlaceholder: true,
|
||||
backClass: 'text-gray-900'
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
@@ -35,10 +38,6 @@
|
||||
|
||||
const customCalss = computed(() => {
|
||||
const _class = []
|
||||
if (props.titleColor) {
|
||||
_class.push(props.titleColor)
|
||||
return
|
||||
}
|
||||
if (scrollThreshold.value) {
|
||||
_class.push('text-white')
|
||||
}
|
||||
@@ -48,6 +47,10 @@
|
||||
return _class;
|
||||
})
|
||||
|
||||
const titleColorClass = computed(() => {
|
||||
return [props.titleColor]
|
||||
})
|
||||
|
||||
const visibleTitle = computed(() => {
|
||||
if (!props.scrollTitle) {
|
||||
return props.defaultTitle;
|
||||
@@ -65,7 +68,7 @@
|
||||
return [
|
||||
homePage,
|
||||
'pages/maintenance/maintenance',
|
||||
...tabbarList.map(item => item.pagePath),
|
||||
...tabbarList.map((item : any) => item.pagePath),
|
||||
] as string[];
|
||||
})
|
||||
|
||||
@@ -92,14 +95,16 @@
|
||||
<!-- 左边 -->
|
||||
<view class="shrink-0 min-w-18" @click="handleBack()">
|
||||
<view v-if="props.useBack"
|
||||
class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-gray-900 text-sm">
|
||||
class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-sm"
|
||||
:class="props.backClass" :style="[props.backStyle]">
|
||||
<wd-icon name="arrow-left" size="32rpx"></wd-icon>
|
||||
<view class="w-[1px] h-4 bg-white/60" />
|
||||
<text class="text-xs font-bold">返回</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 中间 -->
|
||||
<view class="flex-1 truncate text-center font-bold transition-colors duration-300">
|
||||
<view class="flex-1 truncate text-center font-bold transition-colors duration-300"
|
||||
:class="titleColorClass">
|
||||
<slot> {{visibleTitle}} </slot>
|
||||
</view>
|
||||
<!-- 右边 -->
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
// 获取当前页面,并且设置黑名单模式,因为有的页面可能不需要滚动到顶部
|
||||
const balckList = ['pages/maintenance/maintenance','pages-blog/setting/setting']
|
||||
const balckList = ['pages/maintenance/maintenance', 'pages-blog/setting/setting', 'pages-blog/love/love']
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const visible = computed(() => {
|
||||
@@ -23,8 +23,7 @@
|
||||
|
||||
<template>
|
||||
<view v-if="visible" class="fixed bottom-22 right-3 z-50 pb-safe">
|
||||
<view
|
||||
class="uh-global-card-glass border h-11 w-11 flex items-center justify-center rounded-full text-primary"
|
||||
<view class="uh-global-card-glass border h-11 w-11 flex items-center justify-center rounded-full text-primary"
|
||||
:class="props.customClass" @click="handleScrollTop">
|
||||
<wd-icon name="arrow-up" size="20px" />
|
||||
</view>
|
||||
|
||||
+273
-327
@@ -1,363 +1,309 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 恋爱主页(源自旧项目 pagesA/love/love.vue,新建复刻)
|
||||
* 情侣信息 + 恋爱计时 + 功能导航(恋爱故事/相册/清单)
|
||||
*/
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '恋爱日记',
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '恋爱日记',
|
||||
navigationStyle: 'custom'
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const appConfigStore = useAppConfigStore()
|
||||
|
||||
/* ---------------- 恋爱配置 ---------------- */
|
||||
interface ILoveConfigPage {
|
||||
enabled: boolean
|
||||
loveDateTitle: string
|
||||
loveDate: string
|
||||
loveInfo: {
|
||||
boyNickname: string
|
||||
boyAvatar: string
|
||||
girlNickname: string
|
||||
girlAvatar: string
|
||||
}
|
||||
pageImages: {
|
||||
bgImageUrl: string
|
||||
waveImageUrl: string
|
||||
heartImageUrl: string
|
||||
}
|
||||
ourStory: { enabled: boolean, iconUrl: string }
|
||||
lovePhoto: { enabled: boolean, iconUrl: string }
|
||||
loveDaily: { enabled: boolean, iconUrl: string }
|
||||
[key: string]: unknown
|
||||
}
|
||||
/* ---------------- 恋爱配置 ---------------- */
|
||||
interface ILoveConfigPage {
|
||||
enabled : boolean
|
||||
loveDateTitle : string
|
||||
loveDate : string
|
||||
loveInfo : {
|
||||
boyNickname : string
|
||||
boyAvatar : string
|
||||
girlNickname : string
|
||||
girlAvatar : string
|
||||
}
|
||||
pageImages : {
|
||||
bgImageUrl : string
|
||||
waveImageUrl : string
|
||||
heartImageUrl : string
|
||||
}
|
||||
ourStory : { enabled : boolean, iconUrl : string }
|
||||
lovePhoto : { enabled : boolean, iconUrl : string }
|
||||
loveDaily : { enabled : boolean, iconUrl : string }
|
||||
[key : string] : unknown
|
||||
}
|
||||
|
||||
const loveConfig = ref<ILoveConfigPage>({
|
||||
enabled: false,
|
||||
loveDateTitle: '',
|
||||
loveDate: '',
|
||||
loveInfo: {
|
||||
boyNickname: '',
|
||||
boyAvatar: '',
|
||||
girlNickname: '',
|
||||
girlAvatar: '',
|
||||
},
|
||||
pageImages: {
|
||||
bgImageUrl: '',
|
||||
waveImageUrl: '',
|
||||
heartImageUrl: '',
|
||||
},
|
||||
ourStory: { enabled: false, iconUrl: '' },
|
||||
lovePhoto: { enabled: false, iconUrl: '' },
|
||||
loveDaily: { enabled: false, iconUrl: '' },
|
||||
})
|
||||
const loveConfig = ref<ILoveConfigPage>({
|
||||
enabled: false,
|
||||
loveDateTitle: '',
|
||||
loveDate: '',
|
||||
loveInfo: {
|
||||
boyNickname: '',
|
||||
boyAvatar: '',
|
||||
girlNickname: '',
|
||||
girlAvatar: '',
|
||||
},
|
||||
pageImages: {
|
||||
bgImageUrl: '',
|
||||
waveImageUrl: '',
|
||||
heartImageUrl: '',
|
||||
},
|
||||
ourStory: { enabled: false, iconUrl: '' },
|
||||
lovePhoto: { enabled: false, iconUrl: '' },
|
||||
loveDaily: { enabled: false, iconUrl: '' },
|
||||
})
|
||||
|
||||
const loveDayCount = ref({ d: 0, h: 0, m: 0, s: 0 })
|
||||
let loveDayTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const loveDayCount = ref({ d: 0, h: 0, m: 0, s: 0 })
|
||||
let loveDayTimer : ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const navList = ref<{ key: string, use: boolean, iconImageUrl: string, title: string, desc: string }[]>([])
|
||||
const navList = ref<{ key : string, use : boolean, iconPrefix : string, icon : string, title : string, desc : string }[]>([])
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const loveWrapStyle = computed(() => ({
|
||||
backgroundImage: `url(${checkImageUrl(loveConfig.value.pageImages.bgImageUrl)})`,
|
||||
}))
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
/**
|
||||
* 恋爱配置数据统一来自 appConfig store(静态配置一次性拉取,设计见
|
||||
* .docs/static-config-unified-fetch-design.md):
|
||||
* - loveConfig(公开 /love-config 内容: enabled/纪念日/恋人信息, bootstrap 已拉取)
|
||||
* - configs.loveConfig(模块开关与页面图片, getConfigs 合成下发)
|
||||
* 页面不再自行发起 /love-config 请求,也不再需要接口失败降级分支(store 兜底默认)。
|
||||
*/
|
||||
function syncLoveConfigFromStore() {
|
||||
const storeLove = appConfigStore.loveConfig
|
||||
const appConfigs = appConfigStore.configs
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
function syncLoveConfigFromStore() {
|
||||
const storeLove = appConfigStore.loveConfig
|
||||
const appConfigs = appConfigStore.configs
|
||||
|
||||
loveConfig.value = {
|
||||
...loveConfig.value,
|
||||
...(storeLove.loveDateTitle ? { loveDateTitle: storeLove.loveDateTitle } : {}),
|
||||
...(storeLove.loveDate ? { loveDate: storeLove.loveDate } : {}),
|
||||
...(storeLove.enabled !== undefined ? { enabled: storeLove.enabled } : {}),
|
||||
loveInfo: {
|
||||
...loveConfig.value.loveInfo,
|
||||
...(storeLove.loveInfo || {}),
|
||||
},
|
||||
}
|
||||
loveConfig.value = {
|
||||
...loveConfig.value,
|
||||
...(storeLove.loveDateTitle ? { loveDateTitle: storeLove.loveDateTitle } : {}),
|
||||
...(storeLove.loveDate ? { loveDate: storeLove.loveDate } : {}),
|
||||
...(storeLove.enabled !== undefined ? { enabled: storeLove.enabled } : {}),
|
||||
loveInfo: {
|
||||
...loveConfig.value.loveInfo,
|
||||
...(storeLove.loveInfo || {}),
|
||||
},
|
||||
}
|
||||
|
||||
// 从 getConfigs.loveConfig 取模块开关与页面图片(缺省保留默认)
|
||||
const loveModuleConfig = appConfigs.loveConfig as Partial<ILoveConfigPage> | undefined
|
||||
if (loveModuleConfig) {
|
||||
loveConfig.value = {
|
||||
...loveConfig.value,
|
||||
pageImages: loveModuleConfig.pageImages || loveConfig.value.pageImages,
|
||||
ourStory: loveModuleConfig.ourStory || loveConfig.value.ourStory,
|
||||
lovePhoto: loveModuleConfig.lovePhoto || loveConfig.value.lovePhoto,
|
||||
loveDaily: loveModuleConfig.loveDaily || loveConfig.value.loveDaily,
|
||||
}
|
||||
}
|
||||
// 从 getConfigs.loveConfig 取模块开关与页面图片(缺省保留默认)
|
||||
const loveModuleConfig = appConfigs.loveConfig as Partial<ILoveConfigPage> | undefined
|
||||
if (loveModuleConfig) {
|
||||
loveConfig.value = {
|
||||
...loveConfig.value,
|
||||
pageImages: loveModuleConfig.pageImages || loveConfig.value.pageImages,
|
||||
ourStory: loveModuleConfig.ourStory || loveConfig.value.ourStory,
|
||||
lovePhoto: loveModuleConfig.lovePhoto || loveConfig.value.lovePhoto,
|
||||
loveDaily: loveModuleConfig.loveDaily || loveConfig.value.loveDaily,
|
||||
}
|
||||
}
|
||||
|
||||
initList()
|
||||
// 未配置纪念日(loveDate 为空)不启动倒计时,避免 NaN
|
||||
if (loveConfig.value.loveDate) {
|
||||
handleInitLoveDayCount()
|
||||
}
|
||||
}
|
||||
initList()
|
||||
// 未配置纪念日(loveDate 为空)不启动倒计时,避免 NaN
|
||||
if (loveConfig.value.loveDate) {
|
||||
handleInitLoveDayCount()
|
||||
}
|
||||
}
|
||||
|
||||
function initList() {
|
||||
const configs = loveConfig.value
|
||||
navList.value = [
|
||||
{
|
||||
key: 'journey',
|
||||
use: configs.ourStory.enabled,
|
||||
iconImageUrl: configs.ourStory.iconUrl,
|
||||
title: '恋爱故事',
|
||||
desc: '我们一起度过的那些经历',
|
||||
},
|
||||
{
|
||||
key: 'album',
|
||||
use: configs.lovePhoto.enabled,
|
||||
iconImageUrl: configs.lovePhoto.iconUrl,
|
||||
title: '恋爱相册',
|
||||
desc: '定格了我们的那些小美好',
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
use: configs.loveDaily.enabled,
|
||||
iconImageUrl: configs.loveDaily.iconUrl,
|
||||
title: '恋爱清单',
|
||||
desc: '你我之间的约定我们都在努力实现',
|
||||
},
|
||||
]
|
||||
}
|
||||
function initList() {
|
||||
const configs = loveConfig.value
|
||||
navList.value = [
|
||||
{
|
||||
key: 'story',
|
||||
use: configs.ourStory.enabled,
|
||||
title: '恋爱故事',
|
||||
desc: '我们一起度过的那些经历',
|
||||
iconPrefix: 'uhlove-icon',
|
||||
icon: 'gushi',
|
||||
},
|
||||
{
|
||||
key: 'album',
|
||||
use: configs.lovePhoto.enabled,
|
||||
title: '恋爱相册',
|
||||
desc: '定格了我们的那些小美好',
|
||||
iconPrefix: 'uhlove-icon',
|
||||
icon: 'xiangce'
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
use: configs.loveDaily.enabled,
|
||||
title: '恋爱清单',
|
||||
desc: '你我之间的约定我们都在努力实现',
|
||||
iconPrefix: 'uhlove-icon',
|
||||
icon: 'liebiao'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/* ---------------- 恋爱计时 ---------------- */
|
||||
function handleInitLoveDayCount() {
|
||||
if (loveDayTimer) {
|
||||
clearTimeout(loveDayTimer)
|
||||
}
|
||||
const countDownFn = () => {
|
||||
loveDayTimer = setTimeout(countDownFn, 1000)
|
||||
const formatStartDate = loveConfig.value.loveDate.replace(/-/g, '/')
|
||||
const start = new Date(formatStartDate)
|
||||
const now = new Date()
|
||||
const T = now.getTime() - start.getTime()
|
||||
const i = 24 * 60 * 60 * 1000
|
||||
const d = T / i
|
||||
const D = Math.floor(d)
|
||||
const h = (d - D) * 24
|
||||
const H = Math.floor(h)
|
||||
const m = (h - H) * 60
|
||||
const M = Math.floor(m)
|
||||
const s = (m - M) * 60
|
||||
const S = Math.floor(s)
|
||||
loveDayCount.value = { d: D, h: H, m: M, s: S }
|
||||
}
|
||||
countDownFn()
|
||||
}
|
||||
/* ---------------- 恋爱计时 ---------------- */
|
||||
function handleInitLoveDayCount() {
|
||||
if (loveDayTimer) {
|
||||
clearTimeout(loveDayTimer)
|
||||
}
|
||||
const countDownFn = () => {
|
||||
loveDayTimer = setTimeout(countDownFn, 1000)
|
||||
const formatStartDate = loveConfig.value.loveDate.replace(/-/g, '/')
|
||||
const start = new Date(formatStartDate)
|
||||
const now = new Date()
|
||||
const T = now.getTime() - start.getTime()
|
||||
const i = 24 * 60 * 60 * 1000
|
||||
const d = T / i
|
||||
const D = Math.floor(d)
|
||||
const h = (d - D) * 24
|
||||
const H = Math.floor(h)
|
||||
const m = (h - H) * 60
|
||||
const M = Math.floor(m)
|
||||
const s = (m - M) * 60
|
||||
const S = Math.floor(s)
|
||||
loveDayCount.value = { d: D, h: H, m: M, s: S }
|
||||
}
|
||||
countDownFn()
|
||||
}
|
||||
|
||||
/* ---------------- 跳转 ---------------- */
|
||||
function handleToPage(pageName: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/love/${pageName}`,
|
||||
})
|
||||
}
|
||||
/* ---------------- 跳转 ---------------- */
|
||||
function handleToPage(pageName : string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/love/${pageName}`,
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({ title: '恋爱日记' })
|
||||
// 先用 store 已有数据立即渲染(index 启动已 bootstrap 或持久化缓存恢复)
|
||||
syncLoveConfigFromStore()
|
||||
})
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(() => {
|
||||
syncLoveConfigFromStore()
|
||||
})
|
||||
|
||||
// 每次进入页面:静态配置 TTL 内不重复请求(bootstrap 内部判定),刷新后重新合成视图
|
||||
onShow(async () => {
|
||||
await appConfigStore.bootstrap()
|
||||
syncLoveConfigFromStore()
|
||||
})
|
||||
onShow(async () => {
|
||||
await appConfigStore.bootstrap()
|
||||
syncLoveConfigFromStore()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (loveDayTimer) {
|
||||
clearTimeout(loveDayTimer)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (loveDayTimer) {
|
||||
clearTimeout(loveDayTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page min-h-screen w-screen">
|
||||
<!-- 情侣信息 -->
|
||||
<view class="lover-wrap relative h-[50vh] w-screen flex items-center justify-center" :style="[loveWrapStyle]">
|
||||
<view class="lover-card absolute left-1/2 top-[58%] z-2 w-[90vw] flex items-center justify-around rounded-xl -translate-x-1/2 -translate-y-1/2">
|
||||
<view class="boy">
|
||||
<image class="avatar box-border h-[180rpx] w-[180rpx] border-8 rounded-full" :style="{ borderColor: 'rgb(58 184 228 / 70%)' }" :src="checkAvatarUrl(loveConfig.loveInfo.boyAvatar)" mode="aspectFit" />
|
||||
<view class="name mt-2 text-center text-[32rpx] text-white font-bold tracking-[2rpx]">
|
||||
{{ loveConfig.loveInfo.boyNickname }}
|
||||
</view>
|
||||
</view>
|
||||
<image class="like h-[120rpx] w-[120rpx]" :src="checkImageUrl(loveConfig.pageImages.heartImageUrl)" mode="scaleToFill" />
|
||||
<view class="girl">
|
||||
<image class="avatar box-border h-[180rpx] w-[180rpx] border-8 rounded-full" :style="{ borderColor: 'rgb(245 122 179 / 70%)' }" :src="checkAvatarUrl(loveConfig.loveInfo.girlAvatar)" mode="aspectFit" />
|
||||
<view class="name mt-2 text-center text-[32rpx] text-white font-bold tracking-[2rpx]">
|
||||
{{ loveConfig.loveInfo.girlNickname }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<image class="wave-image absolute bottom-0 left-0 h-[120rpx] w-full" :src="checkImageUrl(loveConfig.pageImages.waveImageUrl)" mode="scaleToFill" />
|
||||
</view>
|
||||
<view class="bg-pink-100 min-h-screen w-screen">
|
||||
<uh-navbar default-title="恋爱日记" :need-placeholder="false" back-class="text-love"
|
||||
title-color="!text-love"></uh-navbar>
|
||||
|
||||
<!-- 恋爱记时 -->
|
||||
<view class="love-time-wrap mt-20 w-screen flex flex-col items-center justify-center">
|
||||
<view class="title text-[42rpx] text-[#333] font-bold">
|
||||
{{ loveConfig.loveDateTitle }}
|
||||
</view>
|
||||
<view class="content mt-6 flex items-center justify-center">
|
||||
<text class="text text-[28rpx]">
|
||||
第
|
||||
<text class="number mx-2 text-[46rpx] text-[#f83856] font-bold">{{ loveDayCount.d }}</text>
|
||||
天
|
||||
</text>
|
||||
<text class="text text-[28rpx]">
|
||||
<text class="number mx-2 text-[46rpx] text-[#f83856] font-bold">{{ loveDayCount.h }}</text>
|
||||
小时
|
||||
</text>
|
||||
<text class="text text-[28rpx]">
|
||||
<text class="number mx-2 text-[46rpx] text-[#f83856] font-bold">{{ loveDayCount.m }}</text>
|
||||
分钟
|
||||
</text>
|
||||
<text class="text text-[28rpx]">
|
||||
<text class="number mx-2 text-[46rpx] text-[#f83856] font-bold">{{ loveDayCount.s }}</text>
|
||||
秒
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 情侣信息 -->
|
||||
<view class="relative z-10 h-92 w-screen flex flex-col items-center justify-center">
|
||||
<view class="relative z-10 w-full h-full flex items-center justify-center rounded-xl">
|
||||
<view class="boy flex flex-col items-center justify-center translate-x-1">
|
||||
<image class="uh-global-card-glass border-3 box-border border-blue-400 h-26 w-26 rounded-full"
|
||||
:src="checkAvatarUrl(loveConfig.loveInfo.boyAvatar)" mode="aspectFill" />
|
||||
<view class="bg-blue-500 mt-2 text-center text-xs text-white font-bold px-2 py-1 rounded-lg">
|
||||
{{ loveConfig.loveInfo.boyNickname }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="girl flex flex-col items-center justify-center -translate-x-1">
|
||||
<image class="uh-global-card-glass border-3 box-border border-love h-26 w-26 rounded-full"
|
||||
:src="checkAvatarUrl(loveConfig.loveInfo.girlAvatar)" mode="aspectFill" />
|
||||
<view class="bg-love mt-2 text-center text-xs text-white font-bold px-2 py-1 rounded-lg">
|
||||
{{ loveConfig.loveInfo.girlNickname }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<image :src="checkImageUrl(loveConfig.pageImages.bgImageUrl)" class="absolute z-0 inset-0 w-full h-full"
|
||||
mode="aspectFill" />
|
||||
<view class="absolute z-2 left-0 bottom-0 w-full h-12 bg-gradient-to-b from-white/0 to-pink-100" />
|
||||
</view>
|
||||
|
||||
<!-- 功能导航 -->
|
||||
<view class="list-wrap mt-[75rpx] box-border flex flex-col items-center justify-center px-9">
|
||||
<block v-for="(nav, index) in navList" :key="index">
|
||||
<view v-if="nav.use" class="mb-8 box-border list-item w-full flex items-center justify-around rounded-[50rpx] bg-white px-8 py-7 shadow-sm" :class="`list-item-${index + 1}`" @click="handleToPage(nav.key)">
|
||||
<view class="left h-[120rpx] w-[120rpx]">
|
||||
<image class="icon h-full w-full" :src="checkImageUrl(nav.iconImageUrl)" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="right box-border flex flex-1 flex-col justify-center pl-10">
|
||||
<view class="name text-[32rpx] text-[#333] font-bold">
|
||||
{{ nav.title }}
|
||||
</view>
|
||||
<view class="desc mt-2 text-[26rpx] text-[#777]">
|
||||
{{ nav.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 恋爱记时 -->
|
||||
<view class="love-time-wrap mt-8 w-screen flex flex-col items-center justify-center">
|
||||
<view class="title text-xl text-love font-bold">
|
||||
{{ loveConfig.loveDateTitle }}
|
||||
</view>
|
||||
<view class="content mt-6 flex items-center justify-center">
|
||||
<text class="text text-sm">
|
||||
第
|
||||
<text class="number mx-2 text-2xl text-love font-bold">{{ loveDayCount.d }}</text>
|
||||
天
|
||||
</text>
|
||||
<text class="text text-sm">
|
||||
<text class="number mx-2 text-2xl text-blue-400 font-bold">{{ loveDayCount.h }}</text>
|
||||
小时
|
||||
</text>
|
||||
<text class="text text-sm">
|
||||
<text class="number mx-2 text-2xl text-love font-bold">{{ loveDayCount.m }}</text>
|
||||
分钟
|
||||
</text>
|
||||
<text class="text text-sm">
|
||||
<text class="number mx-2 text-2xl text-blue-400 font-bold">{{ loveDayCount.s }}</text>
|
||||
秒
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能导航 -->
|
||||
<view class="mt-6 box-border flex flex-col items-center justify-center gap-y-4 px-4">
|
||||
<block v-for="(nav, index) in navList" :key="index">
|
||||
<view v-if="nav.use"
|
||||
class="box-border list-item uh-global-card-glass bg-white/60 p-3 w-full flex items-center justify-around gap-x-4 rounded-2xl"
|
||||
:class="`list-item-${index + 1}`" @click="handleToPage(nav.key)">
|
||||
<view class="flex items-center justify-center h-12 w-12 rounded-xl opacity-70" :class="[index%2===0?'bg-pink-100':'bg-blue-100']">
|
||||
<wd-icon :class-prefix="nav.iconPrefix" :name="nav.icon" size="66rpx" />
|
||||
</view>
|
||||
<view class="box-border flex flex-1 flex-col justify-center gap-y-1">
|
||||
<view class="name text-md font-bold" :class="[index%2===0?'text-love':'text-blue-400']">
|
||||
{{ nav.title }}
|
||||
</view>
|
||||
<view class="text-xs truncate" :class="[index%2===0?'text-love/60':'text-blue-300']">
|
||||
{{ nav.desc }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<wd-icon name="arrow-right" :class="[index%2===0?'text-love':'text-blue-400']"
|
||||
size="32rpx"></wd-icon>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
rgb(247 149 51 / 10%),
|
||||
rgb(243 112 85 / 10%) 15%,
|
||||
rgb(239 78 123 / 10%) 30%,
|
||||
rgb(161 102 171 / 10%) 44%,
|
||||
rgb(80 115 184 / 10%) 58%,
|
||||
rgb(16 152 173 / 10%) 72%,
|
||||
rgb(7 179 155 / 10%) 86%,
|
||||
rgb(109 186 130 / 10%)
|
||||
);
|
||||
}
|
||||
.list-item-1 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.lover-wrap {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
.list-item-2 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
background-color: rgb(255 255 255 / 10%);
|
||||
z-index: 0;
|
||||
backdrop-filter: blur(4rpx);
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item-3 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -60rpx;
|
||||
width: 100vw;
|
||||
height: 60rpx;
|
||||
background-image: linear-gradient(to bottom, rgb(255 255 255), rgb(255 255 255 / 0%));
|
||||
}
|
||||
@keyframes likeani {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.like {
|
||||
animation: likeani 1s ease-in-out infinite;
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.wave-image {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 列表项漂浮动画(无法用 UnoCSS 表达;模板已生成 list-item-N 类,避开 WXSS 不支持的 :nth-child) */
|
||||
.list-item-1 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
.list-item-2 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-3 {
|
||||
animation: listItemAni1 3s ease-in-out infinite;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
@keyframes listItemAni1 {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes likeani {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes listItemAni1 {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 恋爱故事页(源自旧项目 pagesA/love/journey.vue,新建复刻)
|
||||
* 时间轴展示恋爱故事,点击查看故事详情弹窗
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getLoveStories } from '@/api/uni-halo'
|
||||
@@ -13,6 +9,7 @@ import type { ILoveStory } from '@/api/types/uni-halo'
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '恋爱故事',
|
||||
navigationStyle: 'custom',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
@@ -1,449 +1,464 @@
|
||||
<script lang="ts" setup>
|
||||
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, calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
|
||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
|
||||
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, calcVoteState, 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',
|
||||
},
|
||||
})
|
||||
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 { 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)
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
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 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 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 || '')
|
||||
}
|
||||
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).toLowerCase()
|
||||
pageTitle.value = `投票详情(${VOTE_TYPES[typeKey] || tempVote.spec?.type})`
|
||||
tempVote.spec = tempVote.spec || {}
|
||||
tempVote.spec.isVoted = isVoted.value
|
||||
tempVote.spec.disabled = isVoted.value
|
||||
tempVote.spec._uh_type = VOTE_TYPES[typeKey] || tempVote.spec.type
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
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).toLowerCase()
|
||||
pageTitle.value = `投票详情(${VOTE_TYPES[typeKey] || tempVote.spec?.type})`
|
||||
tempVote.spec = tempVote.spec || {}
|
||||
tempVote.spec.isVoted = isVoted.value
|
||||
tempVote.spec.disabled = isVoted.value
|
||||
tempVote.spec._uh_type = VOTE_TYPES[typeKey] || tempVote.spec.type
|
||||
|
||||
// 计算状态(与旧项目 calcVoteState 一致,含 timeLimit 非 custom 时 hasEnded 兜底)
|
||||
tempVote.spec._uh_state = calcVoteState(tempVote)
|
||||
if (tempVote.spec._uh_state.state === '已结束')
|
||||
tempVote.spec.hasEnded = true
|
||||
// 计算状态(与旧项目 calcVoteState 一致,含 timeLimit 非 custom 时 hasEnded 兜底)
|
||||
tempVote.spec._uh_state = calcVoteState(tempVote)
|
||||
if (tempVote.spec._uh_state.state === '已结束')
|
||||
tempVote.spec.hasEnded = true
|
||||
|
||||
// 选项计算
|
||||
// 插件选项为 {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)
|
||||
}
|
||||
}
|
||||
// 选项计算
|
||||
// 插件选项为 {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 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
|
||||
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
|
||||
}
|
||||
const checkedList = vote.value!.spec!.options!.filter(x => x.checked && x.id !== option.id)
|
||||
// maxVotes 缺失(0/undefined)时不限制多选数量,避免 0 >= 0 恒真导致无法选择
|
||||
const maxVotes = vote.value?.spec?.maxVotes
|
||||
if (vote.value?.spec?.type === 'multiple' && maxVotes && maxVotes > 0 && checkedList.length >= maxVotes) {
|
||||
showToast(`最多选择 ${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 || '')
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
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()
|
||||
})
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
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()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
handleGetData()
|
||||
})
|
||||
|
||||
onShareAppMessage(() => ({
|
||||
path: `/pages-blog/vote-detail/vote-detail?name=${name.value}`,
|
||||
title: vote.value?.spec?.title || '来投个票吧',
|
||||
imageUrl: '',
|
||||
}))
|
||||
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: '',
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: vote.value?.spec?.title || '来投个票吧',
|
||||
query: name.value ? `name=${name.value}` : '',
|
||||
imageUrl: '',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border min-h-screen w-screen flex flex-col bg-page pb-safe">
|
||||
<!-- 自定义导航 -->
|
||||
<uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
|
||||
<view class="box-border min-h-screen w-screen flex flex-col bg-page pb-safe">
|
||||
<!-- 自定义导航 -->
|
||||
<uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
|
||||
|
||||
<!-- 加载/错误/空占位(状态机) -->
|
||||
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
|
||||
empty-text="未查询到数据" @refresh="handleGetData" />
|
||||
<!-- 加载/错误/空占位(状态机) -->
|
||||
<uh-data-loading
|
||||
v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
|
||||
empty-text="未查询到数据" @refresh="handleGetData"
|
||||
/>
|
||||
|
||||
<view v-else class="box-border px-3 pt-2 pb-16 flex flex-col gap-y-3">
|
||||
<!-- 投票信息 -->
|
||||
<view class="uh-global-card-glass box-border flex flex-col gap-y-3 rounded-2xl p-3">
|
||||
<uh-section-title> 投票信息 </uh-section-title>
|
||||
<view class="flex flex-col gap-3 rounded-xl bg-gray-100 p-4 text-sm text-gray-600">
|
||||
<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-primary' : '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 v-else class="box-border flex flex-col gap-y-3 px-3 pb-16 pt-2">
|
||||
<!-- 投票信息 -->
|
||||
<view class="uh-global-card-glass box-border flex flex-col gap-y-3 rounded-2xl p-3">
|
||||
<uh-section-title> 投票信息 </uh-section-title>
|
||||
<view class="flex flex-col gap-3 rounded-xl bg-gray-100 p-4 text-sm text-gray-600">
|
||||
<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-primary' : '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="uh-global-card-glass box-border flex flex-col rounded-2xl p-3 gap-3">
|
||||
<uh-section-title> 投票内容 </uh-section-title>
|
||||
<view class="box-border flex flex-col gap-y-2 p-4 rounded-xl bg-gray-100">
|
||||
<view class="text-sm text-gray-900 font-bold">
|
||||
{{ vote.spec?.title }}
|
||||
</view>
|
||||
<view v-if="vote.spec?.remark" class="text-xs text-gray-500">
|
||||
{{ vote.spec.remark }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="w-full flex flex-col gap-y-2">
|
||||
<view class="relative box-border text-sm flex items-center gap-x-2">
|
||||
投票选项
|
||||
<text v-if="vote.spec?.type === 'multiple'"
|
||||
class="text-xs font-normal">(最多选择
|
||||
{{ vote.spec?.maxVotes }} 项)
|
||||
</text>
|
||||
</view>
|
||||
<view class="options flex flex-col gap-3">
|
||||
<!-- PK 对抗条(与旧项目 pk-container 一致) -->
|
||||
<view v-if="vote.spec?.type === 'pk'" class="pk-container box-border flex w-full">
|
||||
<view v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
|
||||
class="radio-item flex-grow" :class="optionIndex === 0 ? 'radio-left' : 'radio-right'"
|
||||
:style="{ width: `${option._uh_percent}%` }">
|
||||
<view class="option-item box-border w-full rounded-xl py-3 px-3"
|
||||
:class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'">
|
||||
{{ option._uh_percent }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 投票内容 -->
|
||||
<view class="uh-global-card-glass box-border flex flex-col gap-3 rounded-2xl p-3">
|
||||
<uh-section-title> 投票内容 </uh-section-title>
|
||||
<view class="box-border flex flex-col gap-y-2 rounded-xl bg-gray-100 p-4">
|
||||
<view class="text-sm text-gray-900 font-bold">
|
||||
{{ vote.spec?.title }}
|
||||
</view>
|
||||
<view v-if="vote.spec?.remark" class="text-xs text-gray-500">
|
||||
{{ vote.spec.remark }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="w-full flex flex-col gap-y-2">
|
||||
<view class="relative box-border flex items-center gap-x-2 text-sm">
|
||||
投票选项
|
||||
<text
|
||||
v-if="vote.spec?.type === 'multiple'"
|
||||
class="text-xs font-normal"
|
||||
>
|
||||
(最多选择
|
||||
{{ vote.spec?.maxVotes }} 项)
|
||||
</text>
|
||||
</view>
|
||||
<view class="options flex flex-col gap-3">
|
||||
<!-- PK 对抗条(与旧项目 pk-container 一致;样式需顶层定义,勿嵌套在 .vote-card 下) -->
|
||||
<view v-if="vote.spec?.type === 'pk'" class="pk-container box-border w-full flex">
|
||||
<view
|
||||
v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
|
||||
class="radio-item" :class="optionIndex === 0 ? 'radio-left' : 'radio-right'"
|
||||
:style="{ width: `${option._uh_percent}%` }"
|
||||
>
|
||||
<view
|
||||
class="option-item box-border w-full rounded-xl px-3 py-3"
|
||||
:class="optionIndex === 0 ? 'option-item-left' : 'option-item-right'"
|
||||
>
|
||||
{{ option._uh_percent }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="isVoted || isEnded">
|
||||
<view v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
|
||||
class="is-voted-item relative box-border overflow-hidden rounded-xl text-xs"
|
||||
:class="option.checked ? 'bg-primary text-gray-900 font-bold' : 'bg-gray-100'"
|
||||
:style="{ '--percent': `${option._uh_percent}%` }">
|
||||
<view class="is-voted-item-content relative z-2 box-border px-4 py-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1 text-left">
|
||||
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}:` : '' }}{{ 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-gray-100 px-6 py-5 text-xs"
|
||||
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
|
||||
@click="vote.spec?.type === 'multiple' ? handleSelectCheckboxOption(option) : handleSelectSingleOption(option)">
|
||||
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}:` : '' }}{{ option.title }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template v-if="isVoted || isEnded">
|
||||
<view
|
||||
v-for="(option, optionIndex) in vote.spec?.options" :key="optionIndex"
|
||||
class="is-voted-item relative box-border overflow-hidden rounded-xl text-xs"
|
||||
:class="option.checked ? 'bg-primary text-gray-900 font-bold' : 'bg-gray-100'"
|
||||
:style="{ '--percent': `${option._uh_percent}%` }"
|
||||
>
|
||||
<view class="is-voted-item-content relative z-2 box-border px-4 py-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1 text-left">
|
||||
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}:` : '' }}{{ 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-gray-100 px-6 py-5 text-xs"
|
||||
:class="option.checked ? 'border-2 border-primary bg-primary/15 text-primary font-bold' : ''"
|
||||
@click="vote.spec?.type === 'multiple' ? handleSelectCheckboxOption(option) : handleSelectSingleOption(option)"
|
||||
>
|
||||
{{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}:` : '' }}{{ option.title }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 投票统计 -->
|
||||
<view class="uh-global-card-glass box-border flex flex-col rounded-2xl p-3">
|
||||
<uh-section-title> 投票统计 </uh-section-title>
|
||||
<view class="stat-text mt-3 text-xs text-gray-600">
|
||||
{{ vote.stats?.voteCount || 0 }} 人已参与
|
||||
</view>
|
||||
</view>
|
||||
<!-- 投票统计 -->
|
||||
<view class="uh-global-card-glass box-border flex flex-col rounded-2xl p-3">
|
||||
<uh-section-title> 投票统计 </uh-section-title>
|
||||
<view class="stat-text mt-3 text-xs text-gray-600">
|
||||
{{ vote.stats?.voteCount || 0 }} 人已参与
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="fixed bottom-0 left-0 z-99 box-border w-screen pb-safe px-3">
|
||||
<view
|
||||
class="uh-global-card-glass border rounded-xl w-full flex items-center justify-center gap-x-2 mb-2">
|
||||
<uh-button v-if="isVoted" custom-class="flex-1 py-2 !rounded-xl">
|
||||
您已参与投票
|
||||
</uh-button>
|
||||
<uh-button v-else-if="vote.spec?._uh_state?.state === '未开始'" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('投票未开始')">
|
||||
投票未开始
|
||||
</uh-button>
|
||||
<uh-button v-else-if="vote.spec?._uh_state?.state === '已结束'" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('投票已结束')">
|
||||
投票已结束
|
||||
</uh-button>
|
||||
<uh-button v-else-if="!vote.spec?.canAnonymously" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmit()">
|
||||
不支持匿名投票
|
||||
</uh-button>
|
||||
<uh-button v-else-if="submitForm.voteData.length === 0" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('请选择选项')">
|
||||
提交投票(请选择选项)
|
||||
</uh-button>
|
||||
<uh-button v-else custom-class="flex-1 py-2 !rounded-xl" @click="handleSubmit()">
|
||||
提交投票
|
||||
</uh-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="fixed bottom-0 left-0 z-99 box-border w-screen px-3 pb-safe">
|
||||
<view
|
||||
class="uh-global-card-glass mb-2 w-full flex items-center justify-center gap-x-2 border rounded-xl"
|
||||
>
|
||||
<uh-button v-if="isVoted" custom-class="flex-1 py-2 !rounded-xl">
|
||||
您已参与投票
|
||||
</uh-button>
|
||||
<uh-button
|
||||
v-else-if="vote.spec?._uh_state?.state === '未开始'" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('投票未开始')"
|
||||
>
|
||||
投票未开始
|
||||
</uh-button>
|
||||
<uh-button
|
||||
v-else-if="vote.spec?._uh_state?.state === '已结束'" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('投票已结束')"
|
||||
>
|
||||
投票已结束
|
||||
</uh-button>
|
||||
<uh-button
|
||||
v-else-if="!vote.spec?.canAnonymously" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
不支持匿名投票
|
||||
</uh-button>
|
||||
<uh-button
|
||||
v-else-if="submitForm.voteData.length === 0" custom-class="flex-1 py-2 !rounded-xl"
|
||||
@click="handleSubmitTip('请选择选项')"
|
||||
>
|
||||
提交投票(请选择选项)
|
||||
</uh-button>
|
||||
<uh-button v-else custom-class="flex-1 py-2 !rounded-xl" @click="handleSubmit()">
|
||||
提交投票
|
||||
</uh-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vote-card {
|
||||
.sub-title {
|
||||
&::before {
|
||||
content: '';
|
||||
width: 8rpx;
|
||||
height: 28rpx;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6rpx;
|
||||
background: var(--wot-color-theme, #b9e424);
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
/* 已投票结果项:百分比进度条(此前嵌套在 .vote-card 下导致失效,现顶层定义) */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
/* PK 对抗条:宽度按选项票数占比,两侧斜切渐变(顶层定义,勿嵌套) */
|
||||
.pk-container {
|
||||
.radio-item {
|
||||
min-width: 30%;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.pk-container {
|
||||
.radio-item {
|
||||
min-width: 30%;
|
||||
max-width: 70%;
|
||||
}
|
||||
.option-item-left {
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
color: white;
|
||||
clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.option-item-left {
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
color: white;
|
||||
clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.option-item-right {
|
||||
background: linear-gradient(90deg, #f87171, #ef4444);
|
||||
color: white;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, 40rpx 100%);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.option-item-right {
|
||||
background: linear-gradient(90deg, #f87171, #ef4444);
|
||||
color: white;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, 40rpx 100%);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
||||
|
||||
// 本地开发快速跳转页面,发布请置为 false
|
||||
const DEV_MODE = false
|
||||
const DEV_MODE = true
|
||||
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
||||
const DEV_TO_PATH = `/pages-blog/test/test`
|
||||
const DEV_TO_PATH = `/pages-blog/love/love`
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const appConfigStore = useAppConfigStore()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import './iconfont.css';
|
||||
@import './uhemoji-iconfont.css';
|
||||
@import './uhemoji2-iconfont.css';
|
||||
@import './uhlove-iconfont.css';
|
||||
|
||||
:root,
|
||||
page {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@font-face {
|
||||
font-family: "uhlove-icon"; /* Project id 5231616 */
|
||||
/* Color fonts */
|
||||
src:
|
||||
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAARwAAwAAAAACOgAAAQiAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIlgjHgZgAEwKiXSHFgE2AiQDKgsoAAQgBYIUByAbjwYRFZs62eNDeNa/mSSLTIoFyGwPTI9MD/C3mOv3W5kQKcEiIRMSrf7vy3uoZXFL0JhOSKLN/CwRj3Sl3gN2Wy23sREAB0APwBeiKQABHAm8KbSFYKOwIi9bA7ABhS0AEQSM8EsAdC03cTAAMAKwI7z2AMADoeh+MwFNu1CsbTjvagueo3TgS5S7Riigh+gwgCpvHDKMBJIAQ4EY8ZXqmuoiPAjfz905RVMATgHsAAEACvrcGugYvSpwpBHxHDxS502uAGjrZdLnzp+7axoQFWifP3L73e07t6/7qjSQA3UnGIATQGYA8ijIDn6O9UzE0ZgCYchIohcLlxz0W/Ilo9FZ8NMvX0L/Yot5P9tqgaKKW/b1tOw/7zRlOejYunyrS8fha8HT5550bVQPOlnOu7TsL9Q7tsa+IBPOvE/Ub1FBF2+9D7xyL+G8n5BesqAww1YimLMG0C6JusMPsOVjbO0wUPWMc/O5wMYzwRCtnnNevHU/6VAgjapYWF8iaulWl/MeAk8w+bzq2Hx3aqPqDF6yhVmbB1aiDoG3EfP9E4i+F1YHg86GzN3edDED+i2Lp2Gb2frK1OBlzIYXQZWXmGDYetGHCym4lKu7vLwPsgqLAyOm+CzL2xub90d6/2Hn9kPiGWvzoUDArbv0z46mg2yrS0ubpXvOf5+5tihVNiNFzon+REHGzx+nfPwocvewHxjTGPufse/xSUHd413PvBb8yWv3dM9IiDVO2s8cX3sgZ2nwJ0u5/cVRafLevSllnmdv8Cd7y5Q3pMt78/BXEATxezbBvhcFvNLtUeTu4VHo0eDe6BHHvu90UxxjDRLzYFIDi3QaG21DnHuTOxL9O8QfVL3I4CWcwt1Mf/UOU0FF2cqc8oyprPL0LcVPXNfjekApgcLdrL7raJfwfZxyB7uOkYVINObob7+mZ92+JAILxdS4MkeWp7IotpSAUCLZ2izY2JZOixctb6lbgtuCHStJO49f3VKCQT9jE0zFt2P543OlFrETP3pF3BOuht8TUR7u8eFsvtOzjqrjs07ljmQW0mvfETW2IR+PtoBR5dIGpiG+t+Hj0IkDR7563hS9M812YVFQy+uuDzrl3igfk699dmL34/e3lDWq64va7qpdescNJ8cHKqeelZa+K4/q/IEFHA/70ih7j26sZ32PIfM3G+4Ruj9/bMPzXn6edhf3JFcE/F5MC/rPWJsH8pDKPfnKKQzVjy0Pm+3IPHyKJ9FB/oDLx2mSis10BfLoG5hAdbuwA/LBzaam8cgK1eBAeFsQOAORjULP06SIQyYCRTyC0SsS4I4NX7VCME7AhFFMYgaLMIQojGGAsE2jnrw1AhMm0Yd5xCEasaY3A9WosaEoaKBVryi+AYQ8X1szhsAlY6yiYrQWjEDv00TQbkECDAA=') format('woff2');
|
||||
}
|
||||
|
||||
.uhlove-icon {
|
||||
font-family: "uhlove-icon" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.uhlove-icon-gushi:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.uhlove-icon-liebiao:before {
|
||||
content: "\e616";
|
||||
}
|
||||
|
||||
.uhlove-icon-xiangce:before {
|
||||
content: "\e61a";
|
||||
}
|
||||
@@ -97,6 +97,7 @@ export default defineConfig({
|
||||
primary: 'var(--wot-color-theme,#B9E424)',
|
||||
secondary: 'var(--wot-color-secondary,#D7F94C)',
|
||||
page: 'var(--wot-color-page,#f6f3ee)',
|
||||
love: '#f83856'
|
||||
},
|
||||
fontSize: {
|
||||
/** 提供更小号的字体,用法如:text-2xs */
|
||||
|
||||
Reference in New Issue
Block a user