1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-06-12 21:29:31 +08:00

feat: 新增投票功能

This commit is contained in:
小莫唐尼
2025-08-20 21:56:34 +08:00
parent 9c6a054b12
commit c0d8cb2c3b
12 changed files with 2939 additions and 1266 deletions
+330
View File
@@ -0,0 +1,330 @@
<template>
<view class="vote-card" @click="$emit('on-click',vote)">
<view class="vote-card-head flex">
<view class="left flex flex-center">
<view class="flex-shrink">
<tm-tags v-if="vote.spec.type==='single'" color="light-blue" :shadow="0" rounded size="s"
model="fill">单选</tm-tags>
<tm-tags v-else-if="vote.spec.type==='multiple'" color="light-blue" :shadow="0" rounded size="s"
model="fill">多选</tm-tags>
<tm-tags v-else-if="vote.spec.type==='pk'" color="light-blue" :shadow="0" rounded size="s"
model="fill">双选PK</tm-tags>
</view>
<view class="title text-overflow">
{{ vote.spec.title }}
</view>
</view>
<view class="flex-shrink right flex flex-end">
<tm-tags v-if="vote.spec.hasEnded" color="red" rounded :shadow="0" size="s" model="text">已结束</tm-tags>
<tm-tags v-else color="green" rounded size="s" :shadow="0" model="text">进行中</tm-tags>
</view>
</view>
<view class="vote-card-body">
<view v-if="vote.spec.remark" class="remark text-overflow-2 text-size-s">
{{vote.spec.remark}}
</view>
<template v-if="showOptions">
<!-- 单选 -->
<view v-if="vote.spec.type==='single'" class="single">
<tm-groupradio @change="onOptionRadioChange">
<tm-radio v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked" :extendData="option">
<template v-slot:default="{checkData}">
<tm-button :shadow="0" :theme="checkData.checked?'light-blue':'grey-lighten-3'"
:plan="false" :block="true" class="w-full" size="m" :height="72">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow"> {{ checkData.extendData.title }}
</text>
<text class="flex-shrink"> {{checkData.extendData.percent }}% </text>
</view>
</tm-button>
</template>
</tm-radio>
</tm-groupradio>
</view>
<!-- 多选 -->
<view v-else-if="vote.spec.type==='multiple'" class="multiple">
<tm-groupcheckbox @change="onOptionCheckboxChange">
<tm-checkbox v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked" :extendData="option">
<template v-slot:default="{checkData}">
<tm-button :shadow="0" :theme="checkData.checked?'light-blue':'grey-lighten-3'"
:plan="false" :block="true" class="w-full" size="m" :height="72">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow"> {{ checkData.extendData.title }}
</text>
<text class="flex-shrink"> {{checkData.extendData.percent }}% </text>
</view>
</tm-button>
</template>
</tm-checkbox>
</tm-groupcheckbox>
</view>
<!-- PK -->
<view v-else-if="vote.spec.type==='pk'" class="pk">
<tm-groupradio @change="onOptionPkChange">
<tm-radio v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked" :extendData="option"
class="radio-item" :class="[optionIndex==0?'radio-left':'radio-right']"
:style="{width:option.percent + '%'}">
<template v-slot:default="{checkData}">
<view class="option-item"
:class="[optionIndex==0?'option-item-left':'option-item-right']">
{{checkData.extendData.percent }}%
</view>
</template>
</tm-radio>
</tm-groupradio>
<view class="option-foot w-full flex flex-between">
<view v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex">
<view v-if="optionIndex==0" class="left flex-1 flex-shrink text-overflow">
{{option.title}}
</view>
<view v-else class="right flex-1 flex-shrink text-overflow text-align-right">
{{option.title}}
</view>
</view>
</view>
</view>
</template>
</view>
<view class="vote-card-foot flex flex-between">
<view class="left flex">
<tm-tags color="grey-darken-2" rounded size="s"
model="text">{{ {d: vote.spec.endDate, f: 'yyyy-MM-dd HH:mm'} | formatTime }} 结束</tm-tags>
</view>
<view class="right flex flex-end">
<tm-tags color="grey-darken-2" rounded size="s" model="text">{{ vote.stats.voteCount }} 人已参与</tm-tags>
<tm-tags v-if="vote.spec.isVoted" color="blue" rounded size="s" model="text">已投票</tm-tags>
</view>
</view>
</view>
</template>
<script>
import tmGroupradio from '@/tm-vuetify/components/tm-groupradio/tm-groupradio.vue';
import tmRadio from '@/tm-vuetify/components/tm-radio/tm-radio.vue';
import tmGroupcheckbox from '@/tm-vuetify/components/tm-groupcheckbox/tm-groupcheckbox.vue';
import tmCheckbox from '@/tm-vuetify/components/tm-checkbox/tm-checkbox.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
import tmTags from '@/tm-vuetify/components/tm-tags/tm-tags.vue';
export default {
name: "VoteCard",
options: {
virtualHost: true,
styleIsolation: 'shared'
},
components: {
tmGroupradio,
tmRadio,
tmGroupcheckbox,
tmCheckbox,
tmButton,
tmTags
},
props: {
vote: {
type: Object,
default: () => ({})
},
index: {
type: Number,
default: 0
},
showOptions: {
type: Boolean,
default: false
}
},
methods: {
onOptionRadioChange(e) {
console.log("onOptionRadioChange", e)
},
onOptionCheckboxChange(e) {
console.log("onOptionCheckboxChange", e)
},
onOptionPkChange(e) {
console.log("onOptionPkChange", e)
},
}
}
</script>
<style lang="scss" scoped>
.w-full {
width: 100%;
}
.wp-50 {
width: 50%;
}
.vote-card {
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 24rpx;
padding: 24rpx;
border-radius: 12rpx;
background-color: #ffff;
box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.075);
overflow: hidden;
margin-bottom: 24rpx;
// border: 1px solid #eee;
}
.vote-card-head {
margin-bottom: 12rpx;
display: flex;
align-items: center;
justify-content: space-between;
.left {
.title {
font-size: 28rpx;
font-weight: bold;
}
}
}
.vote-card-body {
.remark {
box-sizing: border-box;
padding: 12rpx 6rpx;
padding-top: 0;
color: rgba(0, 0, 0, 0.75);
}
}
.vote-card-foot {
box-sizing: border-box;
padding-top: 6px;
margin-top: 6px;
border-top: 2rpx solid #eee;
.left{
}
}
.single {
::v-deep {
.tm-groupradio {
width: 100%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
gap: 12rpx 0;
}
.tm-checkbox {
box-sizing: border-box;
display: block;
padding: 0 12rpx;
width: 50%;
}
.tm-button-label {
width: 100%;
}
}
}
.multiple {
::v-deep {
.tm-groupcheckbox {
width: 100%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
gap: 12rpx 0;
}
.tm-checkbox {
box-sizing: border-box;
display: block;
padding: 0 12rpx;
width: 50%;
}
.tm-button-label {
width: 100%;
}
}
}
.pk {
box-sizing: border-box;
width: 100%;
padding: 0 12rpx;
::v-deep {
.tm-groupradio {
display: flex;
width: 100%;
}
.tm-checkbox {
flex-grow: 1;
min-width: 30% !important;
max-width: 70% !important;
}
.radio-item {
position: relative;
.selected {
z-index: 10;
}
}
.radio-left {}
.radio-right {}
.option-item {
width: 100%;
padding: 12rpx 24rpx;
border-radius: 12rpx;
}
.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;
}
.option-foot {
width: 100%;
margin-top: 6rpx;
font-size: 24rpx;
color: #666;
.left {
box-sizing: border-box;
padding-right: 24rpx;
}
.right {
box-sizing: border-box;
padding-left: 24rpx;
}
}
}
}
</style>
+26
View File
@@ -279,6 +279,32 @@
} }
} }
} }
},
{
"path": "votes/votes",
"style": {
"navigationBarTitleText": "投票列表",
"enablePullDownRefresh": true,
"app-plus": {
"pullToRefresh": {
"color": "#03a9f4",
"style": "circle"
}
}
}
},
{
"path": "vote-detail/vote-detail",
"style": {
"navigationBarTitleText": "投票详情",
"enablePullDownRefresh": true,
"app-plus": {
"pullToRefresh": {
"color": "#03a9f4",
"style": "circle"
}
}
}
} }
] ]
}, },
+122 -83
View File
@@ -1,93 +1,132 @@
<template> <template>
<view class="app-page"></view> <view class="app-page flex flex-center">
<PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId"
:error-text="uniHaloPluginAvailableError" />
</view>
</template> </template>
<script> <script>
const homePagePath = '/pages/tabbar/home/home' import pluginAvailable from "@/common/mixins/pluginAvailable.js"
const startPagePath = '/pagesA/start/start'
const articleDetailPath = '/pagesA/article-detail/article-detail';
export default {
computed: {
configs() {
return this.$tm.vx.getters().getConfigs;
}
},
onLoad: function (options) {
uni.$tm.vx.actions('config/fetchConfigs').then(async (res) => {
if (options.scene) {
if ('' !== options.scene) {
const postId = await this.getPostIdByQRCode(options.scene);
if (postId) {
uni.redirectTo({
url: articleDetailPath + `?name=${postId}`,
animationType: 'slide-in-right'
});
}
}
}
// #ifdef MP-WEIXIN const homePagePath = '/pages/tabbar/home/home'
// uni.$tm.vx.commit('setWxShare', res.shareConfig); const startPagePath = '/pagesA/start/start'
// #endif const articleDetailPath = '/pagesA/article-detail/article-detail';
// 获取mockjson const _DEV_ = false
if (res.auditConfig.auditModeEnabled) { const _DEV_TO_TYPE_ = "page"
if (res.auditConfig.auditModeData.jsonUrl) { const _DEV_TO_PATH_ = "/pagesA/votes/votes"
await uni.$tm.vx.actions('config/fetchMockJson')
} else {
const mockJson = uni.$utils.checkJsonAndParse(res.auditConfig.auditModeData.jsonData)
if (mockJson.ok) {
uni.$tm.vx.commit('config/setMockJson', mockJson.jsonData)
}
}
}
// 进入检查 export default {
this.fnCheckShowStarted(); mixins: [pluginAvailable],
}).catch((err) => { computed: {
uni.switchTab({ configs() {
url: homePagePath return this.$tm.vx.getters().getConfigs;
}); }
}) },
}, async onLoad(options) {
methods: { // 检查插件
fnCheckShowStarted() { this.setPluginId(this.NeedPluginIds.PluginUniHalo)
if (!this.configs.appConfig.startConfig.enabled) { this.setPluginError("阿偶,检测到当前插件没有安装或者启用,无法启动 uni-halo 哦,请联系管理员")
uni.switchTab({ if (!await this.checkPluginAvailable()) return
url: homePagePath
});
return;
}
// 是否每次都显示启动页 // 获取配置
if (this.configs.appConfig.startConfig.alwaysShow) { uni.$tm.vx.actions('config/fetchConfigs').then(async (res) => {
uni.removeStorageSync('APP_HAS_STARTED') if (options.scene) {
uni.redirectTo({ if ('' !== options.scene) {
url: startPagePath const postId = await this.getPostIdByQRCode(options.scene);
}); if (postId) {
return; uni.redirectTo({
} url: articleDetailPath + `?name=${postId}`,
animationType: 'slide-in-right'
});
}
}
}
// 只显示一次启动页 // #ifdef MP-WEIXIN
if (uni.getStorageSync('APP_HAS_STARTED')) { // uni.$tm.vx.commit('setWxShare', res.shareConfig);
uni.switchTab({ // #endif
url: homePagePath
}); // 获取mockjson
} else { if (res.auditConfig.auditModeEnabled) {
uni.redirectTo({ if (res.auditConfig.auditModeData.jsonUrl) {
url: startPagePath await uni.$tm.vx.actions('config/fetchMockJson')
}); } else {
} const mockJson = uni.$utils.checkJsonAndParse(res.auditConfig.auditModeData
}, .jsonData)
async getPostIdByQRCode(key) { if (mockJson.ok) {
const response = await this.$httpApi.v2.getQRCodeInfo(key); uni.$tm.vx.commit('config/setMockJson', mockJson.jsonData)
if (response) { }
if (response && response.postId) { }
return response.postId; }
}
} // 进入检查
return null; this.fnCheckShowStarted();
} }).catch((err) => {
} uni.switchTab({
}; url: homePagePath
});
})
},
methods: {
fnCheckShowStarted() {
// 本地开发,快速跳转页面,发布请设置 _DEV_ = false
if (_DEV_) {
if (_DEV_TO_TYPE_ == 'tabbar') {
uni.switchTab({
url: _DEV_TO_PATH_
});
} else if (_DEV_TO_TYPE_ == 'page') {
uni.navigateTo({
url: _DEV_TO_PATH_
});
}
return
}
if (!this.configs.appConfig.startConfig.enabled) {
uni.switchTab({
url: homePagePath
});
return;
}
// 是否每次都显示启动页
if (this.configs.appConfig.startConfig.alwaysShow) {
uni.removeStorageSync('APP_HAS_STARTED')
uni.redirectTo({
url: startPagePath
});
return;
}
// 只显示一次启动页
if (uni.getStorageSync('APP_HAS_STARTED')) {
uni.switchTab({
url: homePagePath
});
} else {
uni.redirectTo({
url: startPagePath
});
}
},
async getPostIdByQRCode(key) {
const response = await this.$httpApi.v2.getQRCodeInfo(key);
if (response) {
if (response && response.postId) {
return response.postId;
}
}
return null;
}
}
};
</script> </script>
<style lang="scss" scoped>
.app-page {
width: 100vw;
height: 100vh;
}
</style>
File diff suppressed because it is too large Load Diff
+695 -573
View File
File diff suppressed because it is too large Load Diff
+70 -55
View File
@@ -1,62 +1,70 @@
<template> <template>
<view class="app-page"> <view class="app-page">
<!-- 顶部切换 --> <PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId"
<view class="e-fixed shadow-1"> :error-text="uniHaloPluginAvailableError" />
<tm-search v-model="queryParams.keyword" :round="24" :shadow="0" color="light-blue" <template v-else>
insert-color="light-blue" :clear="true" @input="fnOnSearch" @confirm="fnOnSearch"></tm-search>
<tm-tabs v-if="false" color="light-blue" :shadow="0" v-model="tab.activeIndex" :list="tab.list" <!-- 顶部切换 -->
align="center" @change="fnOnTabChange"></tm-tabs> <view class="e-fixed shadow-1">
</view> <tm-search v-model="queryParams.keyword" :round="24" :shadow="0" color="light-blue"
<!-- 占位区域 --> insert-color="light-blue" :clear="true" @input="fnOnSearch" @confirm="fnOnSearch"></tm-search>
<view style="width: 100vw;height: 100rpx;"></view> <tm-tabs v-if="false" color="light-blue" :shadow="0" v-model="tab.activeIndex" :list="tab.list"
<!-- 加载区域 --> align="center" @change="fnOnTabChange"></tm-tabs>
<view v-if="loading == 'loading'" class="loading-wrap pa-24">
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
</view>
<view v-else-if="loading == 'error'" class="content-empty flex flex-center">
<tm-empty icon="icon-wind-cry" label="搜索异常"></tm-empty>
</view>
<!-- 内容区域 -->
<view v-else class="content">
<view v-if="dataList.length == 0" class="content-empty flex flex-center">
<!-- 空布局 -->
<tm-empty v-if="!queryParams.keyword" icon="icon-shiliangzhinengduixiang-" label="请输入关键词搜索"></tm-empty>
<tm-empty v-else icon="icon-shiliangzhinengduixiang-"
:label="`未搜到 ${queryParams.keyword} 相关内容`"></tm-empty>
</view> </view>
<block v-else> <!-- 占位区域 -->
<tm-translate v-for="(item, index) in dataList" :key="item.metadataName" animation-name="fadeUp" <view style="width: 100vw;height: 100rpx;"></view>
:wait="calcAniWait(index)"> <!-- 加载区域 -->
<view class="article-card" @click="fnToDetail(item)"> <view v-if="loading == 'loading'" class="loading-wrap pa-24">
<view class="mb-12 flex flex-start"> <tm-skeleton model="listAvatr"></tm-skeleton>
<view class="flex-shrink ml--12"> <tm-skeleton model="listAvatr"></tm-skeleton>
<tm-tags v-if="isArticle(item)" color="blue" size="n" model="text">文章</tm-tags> <tm-skeleton model="listAvatr"></tm-skeleton>
<tm-tags v-else color="green" size="n" model="text">瞬间</tm-tags> <tm-skeleton model="listAvatr"></tm-skeleton>
</view>
<view v-else-if="loading == 'error'" class="content-empty flex flex-center">
<tm-empty icon="icon-wind-cry" label="搜索异常"></tm-empty>
</view>
<!-- 内容区域 -->
<view v-else class="content">
<view v-if="dataList.length == 0" class="content-empty flex flex-center">
<!-- 空布局 -->
<tm-empty v-if="!queryParams.keyword" icon="icon-shiliangzhinengduixiang-"
label="请输入关键词搜索"></tm-empty>
<tm-empty v-else icon="icon-shiliangzhinengduixiang-"
:label="`未搜到 ${queryParams.keyword} 相关内容`"></tm-empty>
</view>
<block v-else>
<tm-translate v-for="(item, index) in dataList" :key="item.metadataName" animation-name="fadeUp"
:wait="calcAniWait(index)">
<view class="article-card" @click="fnToDetail(item)">
<view class="mb-12 flex flex-start">
<view class="flex-shrink ml--12">
<tm-tags v-if="isArticle(item)" color="blue" size="n" model="text">文章</tm-tags>
<tm-tags v-else color="green" size="n" model="text">瞬间</tm-tags>
</view>
<text class="ml-2 text-overflow text-size-n text-weight-b"
style="color: #333;">{{ item.title }}</text>
</view> </view>
<text class="ml-2 text-overflow text-size-n text-weight-b" <mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
style="color: #333;">{{ item.title }}</text> :loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
</view> :tag-style="markdownConfig.tagStyle" :content="item.description || item.content"
<mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain" :markdown="true" :showLineNumber="true" :showLanguageName="true"
:loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true" :copyByLongPress="true" />
:tag-style="markdownConfig.tagStyle" :content="item.description || item.content" <view class="mt-12 flex flex-center flex-between">
:markdown="true" :showLineNumber="true" :showLanguageName="true" :copyByLongPress="true" /> <text style="font-size: 24rpx;color:#888">
<view class="mt-12 flex flex-center flex-between"> 最近更新时间{{ {d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒'} | formatTime }}
<text style="font-size: 24rpx;color:#888"> </text>
最近更新时间{{ {d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒'} | formatTime }} <!-- <tm-tags v-if="isArticle(item)" color="blue" size="n" model="text">文章</tm-tags>
</text>
<!-- <tm-tags v-if="isArticle(item)" color="blue" size="n" model="text">文章</tm-tags>
<tm-tags v-else color="green" size="n" model="text">瞬间</tm-tags> --> <tm-tags v-else color="green" size="n" model="text">瞬间</tm-tags> -->
</view>
</view> </view>
</view> </tm-translate>
</tm-translate>
<tm-flotbutton @click="fnToTopPage" size="m" color="light-blue" icon="icon-angle-up"></tm-flotbutton> <tm-flotbutton @click="fnToTopPage" size="m" color="light-blue"
icon="icon-angle-up"></tm-flotbutton>
</block> </block>
</view> </view>
</template>
</view> </view>
</template> </template>
@@ -71,7 +79,10 @@
import MarkdownConfig from '@/common/markdown/markdown.config.js'; import MarkdownConfig from '@/common/markdown/markdown.config.js';
import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue'; import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue';
import pluginAvailable from "@/common/mixins/pluginAvailable.js"
export default { export default {
mixins: [pluginAvailable],
components: { components: {
tmSkeleton, tmSkeleton,
tmSearch, tmSearch,
@@ -92,7 +103,7 @@
}, },
queryParams: { queryParams: {
keyword: "", keyword: "",
limit: 5, limit: 50,
highlightPreTag: "", highlightPreTag: "",
highlightPostTag: "" highlightPostTag: ""
}, },
@@ -111,17 +122,21 @@
return this.haloConfigs.auditConfig.auditModeEnabled return this.haloConfigs.auditConfig.auditModeEnabled
}, },
}, },
onLoad() { async onLoad() {
this.fnSetPageTitle('内容搜索'); this.fnSetPageTitle('内容搜索');
}, // 检查插件
created() { this.setPluginId(this.NeedPluginIds.PluginSearchWidget)
this.setPluginError("阿偶,检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员")
if (!await this.checkPluginAvailable()) return
if (!this.queryParams.keyword) { if (!this.queryParams.keyword) {
this.loading = 'success' this.loading = 'success'
} else { } else {
this.fnGetData(); this.fnGetData();
} }
}, },
onPullDownRefresh() { onPullDownRefresh() {
if (!this.uniHaloPluginAvailable) return;
this.fnResetSetAniWaitIndex(); this.fnResetSetAniWaitIndex();
this.fnGetData(); this.fnGetData();
}, },
+680
View File
@@ -0,0 +1,680 @@
<template>
<view class="app-page">
<view v-if="loading != 'success'" class="loading-wrap">
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
</view>
<block v-else>
<view v-if="!detail" class="empty">
<tm-empty icon="icon-shiliangzhinengduixiang-" label="未查询到数据"></tm-empty>
</view>
<block v-else>
<view class="vote-card">
<view class="sub-title"> 投票信息 </view>
<view class="vote-card-body flex flex-col"
style="margin-top:12rpx;font-size:28rpx;gap:12rpx 0;background:#F3F4F6;color:#2B2F33;padding:24rpx;border-radius:12rpx;">
<view class="">
投票类型<tm-tags v-if="vote.spec.type==='single'" color="light-blue" :shadow="0" size="xs"
model="fill">单选</tm-tags>
<tm-tags v-else-if="vote.spec.type==='multiple'" color="light-blue" :shadow="0" size="xs"
model="fill">多选</tm-tags>
<tm-tags v-else-if="vote.spec.type==='pk'" color="light-blue" :shadow="0" size="xs"
model="fill">双选PK</tm-tags>
</view>
<view class="">
投票状态<tm-tags v-if="vote.spec.hasEnded" color="red" size="xs" :shadow="0"
model="fill">已结束</tm-tags>
<tm-tags v-else color="green" size="xs" :shadow="0" model="fill">进行中</tm-tags>
</view>
<view class="">
投票方式<tm-tags v-if="vote.spec.canAnonymously" color="light-blue" size="xs" :shadow="0"
model="fill">匿名</tm-tags>
<tm-tags v-else color="red" size="xs" :shadow="0" model="fill">不匿名</tm-tags>
</view>
<view v-if="vote.spec.remark" class="">
投票说明{{ vote.spec.remark||"暂无说明" }}
</view>
<view class="">
截止时间{{ {d: vote.spec.endDate, f: 'yyyy-MM-dd HH:mm'} | formatTime }}
</view>
</view>
</view>
<view class="vote-card">
<view class="vote-card-head flex flex-col items-start mb-12">
<view class="sub-title"> 投票内容 </view>
<view class="sub-content">
{{ vote.spec.title }}
</view>
</view>
<view class="vote-card-body">
<view class="sub-title"> 投票选项 <text v-if="vote.spec.type==='multiple'"
class="sub-title-count">最多选择 {{ vote.spec.maxVotes }} </text> </view>
<view v-if="vote.spec.type==='single'" class="single">
<!-- <tm-groupradio @change="onOptionRadioChange">
<tm-radio v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked" :extendData="option">
<template v-slot:default="{checkData}">
<tm-button :shadow="0" :theme="checkData.checked?'light-blue':'grey-lighten-3'"
:plan="false" size="m" :height="72">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
{{ checkData.extendData.title }}
</text>
<text v-if="checkData.extendData.isVoted" class="flex-shrink ml-12">
{{checkData.extendData.percent }}%
</text>
</view>
</tm-button>
</template>
</tm-radio>
</tm-groupradio> -->
<view class="w-full flex flex-col gap-8">
<tm-button v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex"
:shadow="0" :theme="option.checked?'light-blue':'grey-lighten-3'" :plan="false"
size="m" :height="72" :block="true" class="flex-1 w-full"
@click="handleSelectSingleOption(option)">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
{{option.title }}
</text>
<text v-if="vote.spec.isVoted" class="flex-shrink ml-12">
{{option.percent }}%
</text>
</view>
</tm-button>
</view>
</view>
<view v-else-if="vote.spec.type==='multiple'" class="multiple">
<!-- <tm-groupcheckbox @change="onOptionCheckboxChange">
<tm-checkbox v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked" :extendData="option">
<template v-slot:default="{checkData}">
<tm-button :shadow="0" :theme="checkData.checked?'light-blue':'grey-lighten-3'"
:plan="false" size="m" :height="72">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
{{ checkData.extendData.title }}
</text>
<text v-if="checkData.extendData.isVoted" class="flex-shrink ml-12">
{{checkData.extendData.percent }}%
</text>
</view>
</tm-button>
</template>
</tm-checkbox>
</tm-groupcheckbox> -->
<view class="w-full flex flex-col gap-8">
<tm-button v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex"
:shadow="0" :theme="option.checked?'light-blue':'grey-lighten-3'" :plan="false"
size="m" :height="72" :block="true" class="flex-1 full"
@click="handleSelectCheckboxOption(option)">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
{{option.title }}
</text>
<text v-if="vote.spec.isVoted" class="flex-shrink ml-12">
{{option.percent }}%
</text>
</view>
</tm-button>
</view>
</view>
<view v-else-if="vote.spec.type==='pk'" class="pk">
<view class="pk-container">
<view class="radio-item" v-for="(option,optionIndex) in vote.spec.options"
:key="optionIndex" :class="[optionIndex==0?'radio-left':'radio-right']"
:style="{width:option.percent + '%'}">
<view class="option-item"
:class="[optionIndex==0?'option-item-left':'option-item-right']">
{{option.percent }}%
</view>
</view>
</view>
<view class="option-foot w-full flex flex-between">
<!-- <tm-groupradio @change="onOptionPkChange" >
<tm-radio v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex" dense
:disabled="vote.spec.disabled" v-model="option.checked"
:extendData="{optionIndex:optionIndex,...option}" >
<template v-slot:default="{checkData}">
<tm-button :shadow="0"
:theme="checkData.checked?'light-blue':'grey-lighten-3'" :plan="false"
size="m" :height="72">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
选项{{checkData.extendData.optionIndex+1}}{{ checkData.extendData.title }}
</text>
</view>
</tm-button>
</template>
</tm-radio>
</tm-groupradio> -->
<view class="w-full flex flex-between gap-8">
<tm-button v-for="(option,optionIndex) in vote.spec.options" :key="optionIndex"
:shadow="0" :theme="option.checked?'light-blue':'grey-lighten-3'" :plan="false"
size="m" :height="72" :block="true" class="flex-1"
@click="handleSelectSingleOption(option)">
<view class="flex flex-between w-full">
<text class="text-align-left text-overflow">
选项{{ optionIndex+1}}{{option.title }}
</text>
</view>
</tm-button>
</view>
</view>
</view>
</view>
</view>
<view class="vote-card">
<view class="vote-card-body">
<view class="sub-title"> 投票统计 </view>
<view class="">
<tm-tags color="grey-darken-4" size="s" model="text">{{ vote.stats.voteCount }}
人已参与</tm-tags>
</view>
</view>
</view>
<view class="vote-submit flex w-full flex-center" :style="{
paddingBottom:safeAreaBottom + 'rpx'
}">
<tm-button v-if="!vote.spec.canAnonymously" theme="red" :shadow="0" class="w-full" text
:block="true" @click="handleSubmit()">不允许匿名投票</tm-button>
<tm-button v-else-if="fnCalcIsVoted()" theme="white" text :block="true"
class="w-full">您已参与投票</tm-button>
<tm-button v-else theme="light-blue" class="w-full" :block="true"
@click="handleSubmit()">提交投票</tm-button>
</view>
</block>
</block>
</view>
</template>
<script>
import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
import tmGroupradio from '@/tm-vuetify/components/tm-groupradio/tm-groupradio.vue';
import tmRadio from '@/tm-vuetify/components/tm-radio/tm-radio.vue';
import tmGroupcheckbox from '@/tm-vuetify/components/tm-groupcheckbox/tm-groupcheckbox.vue';
import tmCheckbox from '@/tm-vuetify/components/tm-checkbox/tm-checkbox.vue';
import tmTags from '@/tm-vuetify/components/tm-tags/tm-tags.vue';
import {
voteCacheUtil
} from '@/utils/vote.js'
const types = {
"pk": "双选PK",
"multiple": "多选",
"single": "单选"
}
export default {
components: {
tmSkeleton,
tmEmpty,
tmButton,
tmGroupradio,
tmRadio,
tmGroupcheckbox,
tmCheckbox,
tmTags,
},
data() {
return {
safeAreaBottom: 24,
loading: 'loading',
pageTitle: '加载中...',
name: '',
detail: null,
vote: null,
submitForm: {
voteData: []
},
votedSelected: {
checkbox: [],
radio: [],
pk: []
}
};
},
onLoad(e) {
this.name = e.name;
this.fnGetData();
// #ifndef H5
const systemInfo = uni.getSystemInfoSync();
this.safeAreaBottom = systemInfo.safeAreaInsets.bottom + 12;
// #endif
},
onPullDownRefresh() {
this.fnGetData();
},
methods: {
fnGetData() {
// 设置状态为加载中
this.loading = 'loading';
this.pageTitle = "加载中..."
this.$httpApi.v2
.getVoteDetail(this.name)
.then(res => {
this.pageTitle = "投票详情" + `${types[res.vote.spec.type]}`
const tempVoteRes = res;
tempVoteRes.vote.spec.isVoted = this.fnCalcIsVoted()
tempVoteRes.vote.spec.disabled = this.fnCalcIsVoted()
tempVoteRes.vote.spec.options.map((option, index) => {
option.value = option.id
option.label = option.title
option.isVoted = this.fnCalcIsVoted()
option.checked = this.fnCalcIsChecked(option)
if (tempVoteRes.vote.spec.type === 'single') {
option.percent = this.fnCalcPercent(option, tempVoteRes.vote.stats);
} else if (tempVoteRes.vote.spec.type === 'multiple') {
option.percent = this.fnCalcPercent(option, tempVoteRes.vote.stats);
} else if (tempVoteRes.vote.spec.type === 'pk') {
option.percent = this.fnCalcPercent(option, tempVoteRes.vote.stats);
}
option.dataStr = JSON.stringify(option)
return option
})
this.vote = tempVoteRes.vote
console.log("this.vote", this.vote)
this.detail = tempVoteRes;
setTimeout(() => {
this.loading = 'success';
}, 200);
})
.catch(err => {
console.error(err);
this.loading = 'error';
this.pageTitle = "加载失败,请重试..."
})
.finally(() => {
setTimeout(() => {
uni.hideLoading();
uni.stopPullDownRefresh();
this.fnSetPageTitle(this.pageTitle);
}, 200);
});
},
fnCalcPercent(voteOption, stats) {
if (!this.fnCalcIsVoted()) return 0;
if (!stats?.voteDataList) return 0;
const option = stats.voteDataList.find(x => x.id == voteOption.id)
if (!option) return 0;
const percent = (option.voteCount / stats.voteCount) * 100
return Math.round(percent)
},
fnCalcIsVoted() {
return voteCacheUtil.has(this.name)
},
fnCalcIsChecked(option) {
const data = voteCacheUtil.get(this.name)
if (!data) return false;
const checked = data.selected.includes(option.id)
return checked
},
onOptionRadioChange(e) {
this.submitForm.voteData = e.map(item => this.vote.spec.options[item.index]?.id);
},
onOptionCheckboxChange(e) {
this.submitForm.voteData = e.map(item => this.vote.spec.options[item.index]?.id);
},
onOptionPkChange(e) {
this.submitForm.voteData = e.map(item => this.vote.spec.options[item.index]?.id);
},
formatJsonStr(jsonStr) {
return jsonStr ? JSON.parse(jsonStr) : {}
},
handleSubmit() {
if (!this.vote.spec.canAnonymously) {
uni.showModal({
icon: "none",
title: "提示",
content: "该投票不允许匿名,请到博主的 网站端 进行投票!",
cancelColor: "#666666",
cancelText: "关闭",
confirmText: "复制地址",
success: (res) => {
if (res.confirm) {
this.$utils.copyText(this.$baseApiUrl, "复制成功")
}
}
})
return
}
uni.showLoading({
title: "正在保存..."
})
// 使用简单版
this.submitForm.voteData = this.vote.spec.options.filter(x => x.checked).map(item => item.id)
this.$httpApi.v2
.submitVote(this.name, this.submitForm, this.vote.spec.canAnonymously)
.then(res => {
uni.showToast({
icon: "none",
title: "提交成功"
})
voteCacheUtil.set(this.name, {
selected: [...this.submitForm.voteData],
data: this.vote
})
setTimeout(() => {
uni.startPullDownRefresh()
}, 1500);
})
.catch(err => {
console.error(err);
uni.showToast({
icon: "none",
title: "提交失败,请重试"
})
})
},
handleSelectSingleOption(option) {
if (this.vote.spec.disabled) return
this.vote.spec.options.map(item => {
if (option.id == item.id) {
item.checked = true
} else {
item.checked = false
}
})
},
handleSelectCheckboxOption(option) {
if (this.vote.spec.disabled) return
this.vote.spec.options.map(item => {
if (option.id == item.id) {
item.checked = !item.checked
}
})
}
}
};
</script>
<style lang="scss" scoped>
.app-page {
box-sizing: border-box;
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 24rpx 0;
padding-bottom: 160rpx;
background-color: #fafafd;
}
.loading-wrap {
padding: 0 24rpx;
min-height: 100vh;
}
.empty {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
.w-full {
width: 100%;
}
.wp-50 {
width: 50%;
}
.vote-card {
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 24rpx;
padding: 24rpx;
border-radius: 12rpx;
background-color: #ffff;
box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
overflow: hidden;
margin-bottom: 24rpx;
}
.vote-card-head {
margin-bottom: 12rpx;
display: flex;
align-items: flex-satrt;
justify-content: space-between;
.left {
.title {
font-size: 28rpx;
font-weight: bold;
}
}
}
.vote-card-body {
.remark {
box-sizing: border-box;
padding: 12rpx 6rpx;
padding-top: 0;
color: rgba(0, 0, 0, 0.75);
}
}
.vote-card-foot {
box-sizing: border-box;
margin-top: 12px;
padding-top: 6px;
border-top: 2rpx solid #eee;
}
.single {
::v-deep {
.tm-groupradio {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
gap: 16rpx 0;
}
.tm-checkbox {
box-sizing: border-box;
// display: block;
// width: 100%;
}
.tm-button-label {
width: 100%;
}
}
}
.multiple {
::v-deep {
.tm-groupcheckbox {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
gap: 16rpx 0;
}
.tm-checkbox {
box-sizing: border-box;
// display: block;
// width: 100%;
}
.tm-button-label {
width: 100%;
}
}
}
.pk {
box-sizing: border-box;
width: 100%;
::v-deep {
.pk-container {
box-sizing: border-box;
width: 100%;
display: flex;
}
.radio-item {
flex-grow: 1;
min-width: 30% !important;
max-width: 70% !important;
}
.radio-left {}
.radio-right {}
.option-item {
box-sizing: border-box;
width: 100%;
padding: 24rpx;
border-radius: 12rpx;
}
.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;
}
.option-foot {
margin-top: 24rpx;
width: 100%;
font-size: 24rpx;
color: #666;
.tm-groupradio {
display: flex;
gap: 12rpx;
}
.tm-checkbox {
// width: 100%;
max-width: initial !important;
}
.tm-button {
width: 100%;
}
.left {
box-sizing: border-box;
}
.right {
box-sizing: border-box;
.flex-start.fulled {
justify-content: flex-end;
}
}
}
}
}
.sub-content {
font-weight: bold;
color: #2B2F33;
padding: 12rpx 0;
font-size: 32rpx;
margin-bottom: 12rpx;
}
.sub-title {
box-sizing: border-box;
position: relative;
margin-bottom: 12rpx;
padding-left: 24rpx;
font-weight: bold;
font-size: 30rpx;
&:before {
content: "";
width: 8rpx;
height: 28rpx;
position: absolute;
left: 0;
top: 6rpx;
background: #03A9F4;
border-radius: 6rpx;
}
.sub-title-count {
font-size: 24rpx;
font-weight: normal;
}
}
.vote-submit {
box-sizing: border-box;
padding: 24rpx 36rpx;
position: fixed;
left: 0;
width: 100vw;
bottom: 0;
background-color: rgba(255, 255, 255, 0.98);
box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
border-top: 2rpx solid #eee;
z-index: 99;
::v-deep {
.tm-button {
text-align: center;
}
.tm-button-btn {
margin: 0;
width: 100%;
}
}
}
</style>
+395
View File
@@ -0,0 +1,395 @@
<template>
<view class="app-page">
<PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId"
:error-text="uniHaloPluginAvailableError" />
<template v-else>
<!-- 顶部切换 -->
<view class="e-fixed">
<tm-search v-model="queryParams.keyword" :round="24" :shadow="0" color="light-blue"
insert-color="light-blue" :clear="true" @input="fnOnSearch" @confirm="fnOnSearch"></tm-search>
<tm-dropDownMenu :shadow="1" color="light-blue" active-color="light-blue"
:default-selected="filterOption.selected" :list="filterOption.list"
@confirm="fnOnFilterConfirm"></tm-dropDownMenu>
</view>
<!-- 占位区域 -->
<view style="width: 100vw;height: 210rpx;"></view>
<!-- 加载区域 -->
<view v-if="loading == 'loading'" class="loading-wrap pa-24">
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
<tm-skeleton model="listAvatr"></tm-skeleton>
</view>
<view v-else-if="loading == 'error'" class="content-empty flex flex-center">
<tm-empty icon="icon-wind-cry" label="加载异常"></tm-empty>
</view>
<!-- 内容区域 -->
<view v-else class="content">
<view v-if="dataList.length == 0" class="content-empty flex flex-center">
<tm-empty icon="icon-shiliangzhinengduixiang-" label="暂无数据"></tm-empty>
</view>
<block v-else>
<tm-translate v-for="(item, index) in dataList" :key="item.metadata.name" animation-name="fadeUp"
:wait="calcAniWait(index)">
<VoteCard :vote="item" :index="index" @on-click="fnToDetail"></VoteCard>
</tm-translate>
<view class="load-text">{{ loadMoreText }}</view>
<tm-flotbutton @click="fnToTopPage" size="m" color="light-blue"
icon="icon-angle-up"></tm-flotbutton>
</block>
</view>
</template>
</view>
</template>
<script>
import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
import tmSearch from '@/tm-vuetify/components/tm-search/tm-search.vue';
import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
import tmTabs from '@/tm-vuetify/components/tm-tabs/tm-tabs.vue';
import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
import tmTags from '@/tm-vuetify/components/tm-tags/tm-tags.vue';
import tmDropDownMenu from '@/tm-vuetify/components/tm-dropDownMenu/tm-dropDownMenu.vue';
import VoteCard from '@/components/vote-card/vote-card.vue'
import {
voteCacheUtil
} from '@/utils/vote.js'
import pluginAvailable from "@/common/mixins/pluginAvailable.js"
export default {
options: {
styleIsolation: 'shared'
},
mixins: [pluginAvailable],
components: {
tmSkeleton,
tmSearch,
tmTranslate,
tmTabs,
tmFlotbutton,
tmEmpty,
tmTags,
tmDropDownMenu,
VoteCard
},
data() {
return {
loading: 'loading',
hasNext: false,
isLoadMore: false,
loadMoreText: '加载中...',
filterOption: {
selected: [],
list: [{
title: '类型',
children: [{
title: "",
model: "list",
name: "type",
children: [{
title: "全部",
id: undefined
},
{
title: "单选",
id: 'single'
},
{
title: "多选",
id: 'multiple'
},
{
title: "双选PK",
id: 'pk'
}
]
}]
}, {
title: '状态',
children: [{
title: "",
model: "list",
name: "hasEnded",
children: [{
title: "全部",
id: undefined
},
{
title: "进行中",
id: false
},
{
title: "已结束",
id: true
}
]
}]
}, {
title: '排序',
children: [{
title: "",
model: "list",
name: "sort",
children: [{
title: "默认排序",
id: undefined
},
{
title: "较近创建",
id: 'metadata.creationTimestamp,desc'
},
{
title: "较早创建",
id: 'metadata.creationTimestamp,asc'
}
]
}]
}, {
title: '是否已投',
children: [{
title: "",
model: "list",
name: "isVoted",
children: [{
title: "全部",
id: undefined
},
{
title: "未投票",
id: false
},
{
title: "已投票",
id: true
}
]
}]
}]
},
filterIsVoted: undefined,
queryParams: {
keyword: "",
page: 1,
size: 10,
sort: undefined,
type: undefined,
hasEnded: undefined
},
dataList: []
};
},
computed: {
haloConfigs() {
return this.$tm.vx.getters().getConfigs;
},
calcAuditModeEnabled() {
return this.haloConfigs.auditConfig.auditModeEnabled
},
},
async onLoad() {
this.fnSetPageTitle('投票列表');
// 检查插件
this.setPluginId(this.NeedPluginIds.PluginVote)
this.setPluginError("阿偶,检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员")
if (!await this.checkPluginAvailable()) return
this.fnGetData();
},
onPullDownRefresh() {
if (!this.uniHaloPluginAvailable) return;
this.fnResetSetAniWaitIndex();
this.isLoadMore = false;
this.queryParams.page = 0;
this.fnGetData();
},
onReachBottom(e) {
if (!this.uniHaloPluginAvailable) return;
if (this.calcAuditModeEnabled) {
uni.showToast({
icon: 'none',
title: '没有更多数据了'
});
return
}
if (this.hasNext) {
this.queryParams.page += 1;
this.isLoadMore = true;
this.fnGetData();
} else {
uni.showToast({
icon: 'none',
title: '没有更多数据了'
});
}
},
methods: {
fnOnSearch() {
this.fnResetSetAniWaitIndex();
this.fnToTopPage();
this.fnGetData();
},
fnGetData() {
if (this.calcAuditModeEnabled) {
return;
}
uni.showLoading({
mask: true,
title: '加载中...'
});
// 设置状态为加载中
if (!this.isLoadMore) {
this.loading = 'loading';
}
this.loadMoreText = '加载中...';
this.$httpApi.v2
.getVoteList(this.queryParams)
.then(res => {
this.loading = 'success';
this.loadMoreText = res.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~';
this.hasNext = res.hasNext;
const tempItems = res.items.map(item => {
item.spec.disabled = true
item.spec.isVoted = this.fnCalcIsVoted(item.metadata.name)
item.spec.options.map((option, index) => {
option.checked = this.fnCalcIsChecked(item.metadata.name, option)
option.value = option.id
option.label = option.title
// todo:计算当前的选择占比
if (item.spec.type === 'single') {
option.percent = this.fnCalcPercent(option, item.stats);
} else if (item.spec.type === 'multiple') {
option.percent = this.fnCalcPercent(option, item.stats);
} else if (item.spec.type === 'pk') {
option.percent = this.fnCalcPercent(option, item.stats);
}
return option
})
return item;
})
if (this.isLoadMore) {
this.dataList = this.dataList.concat(tempItems);
} else {
this.dataList = tempItems;
}
this.dataList = this.dataList.sort((a, b) => {
return a.spec.isVoted - b.spec.isVoted
})
if (this.filterIsVoted != undefined) {
this.dataList = this.dataList.filter(x => x.spec.isVoted == this.filterIsVoted)
}
})
.catch(err => {
console.error(err);
this.loading = 'error';
this.loadMoreText = '加载失败,请下拉刷新!';
})
.finally(() => {
setTimeout(() => {
uni.hideLoading();
uni.stopPullDownRefresh();
}, 100);
});
},
fnCalcPercent(voteOption, stats) {
if (!stats?.voteDataList) return 0;
const option = stats.voteDataList.find(x => x.id == voteOption.id)
if (!option) return 0;
const percent = (option.voteCount / stats.voteCount) * 100
return Math.round(percent)
},
fnCalcIsVoted(name) {
return voteCacheUtil.has(name)
},
fnCalcIsChecked(name, option) {
const data = voteCacheUtil.get(name)
if (!data) return false;
const checked = data.selected.includes(option.id)
return checked
},
//跳转详情
fnToDetail(item) {
if (this.calcAuditModeEnabled) return;
uni.navigateTo({
url: '/pagesA/vote-detail/vote-detail?name=' + item.metadata.name,
animationType: 'slide-in-right'
});
},
fnOnFilterConfirm(e) {
// 类型
const type = e.find(x => x.name == 'type')
if (type.children.length == 0) {
this.queryParams.type = undefined
} else {
this.queryParams.type = type.children[0]?.id
}
// 状态
const hasEnded = e.find(x => x.name == 'hasEnded')
if (hasEnded.children.length == 0) {
this.queryParams.hasEnded = undefined
} else {
this.queryParams.hasEnded = hasEnded.children[0]?.id
}
// 排序
const sort = e.find(x => x.name == 'sort')
if (sort.children.length == 0) {
this.queryParams.sort = undefined
} else {
this.queryParams.sort = sort.children[0]?.id
}
// 是否已经投
const isVoted = e.find(x => x.name == 'isVoted')
if (isVoted.children.length == 0) {
this.filterIsVoted = undefined
} else {
this.filterIsVoted = isVoted.children[0]?.id
}
this.queryParams.page = 0;
this.isLoadMore = false;
this.fnResetSetAniWaitIndex();
this.fnToTopPage();
this.fnGetData();
}
}
};
</script>
<style lang="scss" scoped>
.app-page {
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: 24rpx;
background-color: #fafafd;
&.is-balck {
background-color: #212121;
}
}
.content {
padding-top: 24rpx;
}
.content-empty {
height: 60vh;
}
</style>
@@ -2,7 +2,7 @@
<view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block':'']"> <view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block':'']">
<view class=" flex-start"> <view class=" flex-start">
<slot name="default" :checkData="{label:label,checked:changValue}" :on="onclick"> <slot name="default" :checkData="{label:label,checked:changValue,extendData}" :on="onclick">
<view :style="{width: sizes.wk,height: sizes.wk}" class="tm-checkbox-boey relative d-inline-block" <view :style="{width: sizes.wk,height: sizes.wk}" class="tm-checkbox-boey relative d-inline-block"
:class="[black?'bk':'','flex-shrink mr-10', :class="[black?'bk':'','flex-shrink mr-10',
changValue?'ani':'', changValue?'ani':'',
@@ -51,6 +51,10 @@
*/ */
import tmIcons from "@/tm-vuetify/components/tm-icons/tm-icons.vue" import tmIcons from "@/tm-vuetify/components/tm-icons/tm-icons.vue"
export default { export default {
options: {
virtualHost: true,
styleIsolation: 'shared'
},
components:{tmIcons}, components:{tmIcons},
name: 'tm-checkbox', name: 'tm-checkbox',
model: { model: {
@@ -117,6 +121,10 @@
fllowTheme:{ fllowTheme:{
type:Boolean|String, type:Boolean|String,
default:true default:true
},
extendData:{
type:Object,
default:()=>({})
} }
}, },
data() { data() {
@@ -17,6 +17,10 @@
* *
*/ */
export default { export default {
options: {
virtualHost: true,
styleIsolation: 'shared'
},
name:'tm-groupcheckbox', name:'tm-groupcheckbox',
props:{ props:{
// 最大选择数量 // 最大选择数量
+6 -3
View File
@@ -2,7 +2,7 @@
<view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block ':'fulled']"> <view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block ':'fulled']">
<view class="flex-start fulled"> <view class="flex-start fulled">
<slot name="default" :checkData="{label:label,checked:changValue}" :on="onclick"> <slot name="default" :checkData="{label:label,checked:changValue,extendData}" :on="onclick">
<view :style="{width: sizes.wk,height: sizes.wk}" class="tm-checkbox-boey relative d-inline-block" <view :style="{width: sizes.wk,height: sizes.wk}" class="tm-checkbox-boey relative d-inline-block"
:class="[black?'bk':'','flex-shrink mr-10 ', :class="[black?'bk':'','flex-shrink mr-10 ',
changValue?'ani':'', changValue?'ani':'',
@@ -119,8 +119,11 @@
fllowTheme:{ fllowTheme:{
type:Boolean|String, type:Boolean|String,
default:true default:true
} },
extendData:{
type:Object,
default:()=>({})
},
}, },
data() { data() {
return { return {
+34
View File
@@ -0,0 +1,34 @@
const UnihaloVoteUid = "unihalo_vote_uid"
export const voteCacheUtil = {
getAll() {
const data = uni.getStorageSync(UnihaloVoteUid)
if (!data) {
return null
}
return JSON.parse(data)
},
get(name) {
const data = this.getAll()
if (!data) {
return null
}
return data[name]
},
has(name) {
const data = this.getAll()
if (!data) return false
return data[name] != undefined
},
set(name, value) {
let data = this.getAll()
if (!data) {
data = {
[name]: value
}
} else {
data[name] = value
}
uni.setStorageSync(UnihaloVoteUid, JSON.stringify(data))
}
}