Bläddra i källkod

feat: 新增投票功能

小莫唐尼 7 månader sedan
förälder
incheckning
c0d8cb2c3b

+ 330 - 0
components/vote-card/vote-card.vue

@@ -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 - 0
pages.json

@@ -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
pages/index/index.vue

@@ -1,93 +1,132 @@
 <template>
-    <view class="app-page"></view>
+	<view class="app-page flex flex-center">
+		<PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId"
+			:error-text="uniHaloPluginAvailableError" />
+	</view>
 </template>
 
 <script>
-const homePagePath = '/pages/tabbar/home/home'
-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'
-                        });
-                    }
-                }
-            }
+	import pluginAvailable from "@/common/mixins/pluginAvailable.js"
 
-            // #ifdef MP-WEIXIN
-            // uni.$tm.vx.commit('setWxShare', res.shareConfig);
-            // #endif
+	const homePagePath = '/pages/tabbar/home/home'
+	const startPagePath = '/pagesA/start/start'
+	const articleDetailPath = '/pagesA/article-detail/article-detail';
 
-            // 获取mockjson
-            if (res.auditConfig.auditModeEnabled) {
-                if (res.auditConfig.auditModeData.jsonUrl) {
-                    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)
-                    }
-                }
-            }
+	const _DEV_ = false
+	const _DEV_TO_TYPE_ = "page"
+	const _DEV_TO_PATH_ = "/pagesA/votes/votes"
 
-            // 进入检查
-            this.fnCheckShowStarted();
-        }).catch((err) => {
-            uni.switchTab({
-                url: homePagePath
-            });
-        })
-    },
-    methods: {
-        fnCheckShowStarted() {
-            if (!this.configs.appConfig.startConfig.enabled) {
-                uni.switchTab({
-                    url: homePagePath
-                });
-                return;
-            }
+	export default {
+		mixins: [pluginAvailable],
+		computed: {
+			configs() {
+				return this.$tm.vx.getters().getConfigs;
+			}
+		},
+		async onLoad(options) {
+			// 检查插件
+			this.setPluginId(this.NeedPluginIds.PluginUniHalo)
+			this.setPluginError("阿偶,检测到当前插件没有安装或者启用,无法启动 uni-halo 哦,请联系管理员")
+			if (!await this.checkPluginAvailable()) return
 
-            // 是否每次都显示启动页
-            if (this.configs.appConfig.startConfig.alwaysShow) {
-                uni.removeStorageSync('APP_HAS_STARTED')
-                uni.redirectTo({
-                    url: startPagePath
-                });
-                return;
-            }
+			// 获取配置
+			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'
+							});
+						}
+					}
+				}
 
-            // 只显示一次启动页
-            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;
-        }
-    }
-};
+				// #ifdef MP-WEIXIN
+				// uni.$tm.vx.commit('setWxShare', res.shareConfig);
+				// #endif
+
+				// 获取mockjson
+				if (res.auditConfig.auditModeEnabled) {
+					if (res.auditConfig.auditModeData.jsonUrl) {
+						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)
+						}
+					}
+				}
+
+				// 进入检查
+				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>
+
+<style lang="scss" scoped>
+	.app-page {
+		width: 100vw;
+		height: 100vh;
+	}
+</style>

+ 603 - 586
pages/tabbar/about/about.vue

@@ -1,593 +1,610 @@
 <template>
-    <view class="app-page pb-24">
-        <!-- 博主信息 -->
-        <view class="blogger-info" :style="[calcProfileStyle]">
-            <image class="avatar" :src="$utils.checkAvatarUrl(bloggerInfo.avatar)" mode="aspectFill"></image>
-            <view class="profile">
-                <view class="author mt-24 text-size-g text-weight-b">{{ bloggerInfo.nickname }}</view>
-                <view class="desc mt-24 text-size-m">
-                    {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
-                </view>
-            </view>
-            <image v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill" class="gif-wave"></image>
-        </view>
-
-        <!-- 统计信息 -->
-        <view class="statistics-wrap bg-white">
-            <tm-more iconColor="light-blue" :open.sync="statisticsShowMore" :maxHeight="62" label=" " open-label=" ">
-                <template>
-                    <view class="statistics flex pt-24 pb-24" :class="{ 'has-solid': statisticsShowMore }">
-                        <view class="item flex-1 text-align-center">
-                            <view class="number text-size-xl text-bg-gradient-orange-accent">
-                                <tm-flop :startVal="0" :decimals="0" :endVal="statistics.post"
-                                         :duration="3000"></tm-flop>
-                            </view>
-                            <view class="mt-6 text-align-center text-size-s text-grey-darken-1">内容数量</view>
-                        </view>
-                        <view class="item flex-1 text-align-center">
-                            <view class="number text-size-xl text-bg-gradient-green-accent">
-                                <tm-flop :startVal="0" :decimals="0" :endVal="statistics.visit"
-                                         :duration="3000"></tm-flop>
-                            </view>
-                            <view class="mt-6 text-size-s text-grey-darken-1">访客数量</view>
-                        </view>
-                        <view class="item flex-1 text-align-center">
-                            <view class="number text-size-xl text-bg-gradient-blue-accent">
-                                <tm-flop :startVal="0" :decimals="0" :endVal="statistics.category"
-                                         :duration="3000"></tm-flop>
-                            </view>
-                            <view class="mt-6 text-align-center text-size-s text-grey-darken-1">分类总数</view>
-                        </view>
-                    </view>
-                    <view class="statistics solid-top has-solid flex pt-24 pb-24">
-                        <view class="item flex-1 text-align-center">
-                            <view class="number text-size-xl text-bg-gradient-orange-accent">
-                                <tm-flop :startVal="0" :decimals="0" :endVal="statistics.comment"
-                                         :duration="3000"></tm-flop>
-                            </view>
-                            <view class="mt-6 text-align-center text-size-s text-grey-darken-1">评论数量</view>
-                        </view>
-
-                        <view class="item flex-1 text-align-center">
-                            <view class="number text-size-xl text-bg-gradient-blue-accent">
-                                <tm-flop :startVal="0" :decimals="0" :endVal="statistics.upvote"
-                                         :duration="3000"></tm-flop>
-                            </view>
-                            <view class="mt-6 text-size-s text-grey-darken-1">点赞数量</view>
-                        </view>
-                    </view>
-                </template>
-            </tm-more>
-        </view>
-
-        <!-- 功能导航 -->
-        <view class="nav-wrap ma-24 round-3">
-            <tm-grouplist :shadow="0" :round="3" :margin="[0, 0]">
-                <block v-for="(nav, index) in navList" :key="index">
-                    <tm-listitem v-if="nav.show" :title="nav.title" :left-icon="nav.leftIcon" show-left-icon
-                                 :left-icon-color="nav.leftIconColor" :value="nav.rightText" @click="fnOnNav(nav)">
-                        <template slot="rightValue">
-                            <button class="right-value-btn" v-if="nav.openType" :open-type="nav.openType">
-                                {{ nav.rightText }}
-                            </button>
-                            <text v-else>{{ nav.rightText }}</text>
-                        </template>
-                    </tm-listitem>
-                </block>
-            </tm-grouplist>
-        </view>
-        <!-- 版权 -->
-        <view v-if="copyrightConfig.enabled" class="copyright mt-40 text-size-xs text-align-center">
-            <view class="">{{ copyrightConfig.content }}</view>
-        </view>
-    </view>
+	<view class="app-page pb-24">
+		<!-- 博主信息 -->
+		<view class="blogger-info" :style="[calcProfileStyle]">
+			<image class="avatar" :src="$utils.checkAvatarUrl(bloggerInfo.avatar)" mode="aspectFill"></image>
+			<view class="profile">
+				<view class="author mt-24 text-size-g text-weight-b">{{ bloggerInfo.nickname }}</view>
+				<view class="desc mt-24 text-size-m">
+					{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+				</view>
+			</view>
+			<image v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill" class="gif-wave"></image>
+		</view>
+
+		<!-- 统计信息 -->
+		<view class="statistics-wrap bg-white">
+			<tm-more iconColor="light-blue" :open.sync="statisticsShowMore" :maxHeight="62" label=" " open-label=" ">
+				<template>
+					<view class="statistics flex pt-24 pb-24" :class="{ 'has-solid': statisticsShowMore }">
+						<view class="item flex-1 text-align-center">
+							<view class="number text-size-xl text-bg-gradient-orange-accent">
+								<tm-flop :startVal="0" :decimals="0" :endVal="statistics.post"
+									:duration="3000"></tm-flop>
+							</view>
+							<view class="mt-6 text-align-center text-size-s text-grey-darken-1">内容数量</view>
+						</view>
+						<view class="item flex-1 text-align-center">
+							<view class="number text-size-xl text-bg-gradient-green-accent">
+								<tm-flop :startVal="0" :decimals="0" :endVal="statistics.visit"
+									:duration="3000"></tm-flop>
+							</view>
+							<view class="mt-6 text-size-s text-grey-darken-1">访客数量</view>
+						</view>
+						<view class="item flex-1 text-align-center">
+							<view class="number text-size-xl text-bg-gradient-blue-accent">
+								<tm-flop :startVal="0" :decimals="0" :endVal="statistics.category"
+									:duration="3000"></tm-flop>
+							</view>
+							<view class="mt-6 text-align-center text-size-s text-grey-darken-1">分类总数</view>
+						</view>
+					</view>
+					<view class="statistics solid-top has-solid flex pt-24 pb-24">
+						<view class="item flex-1 text-align-center">
+							<view class="number text-size-xl text-bg-gradient-orange-accent">
+								<tm-flop :startVal="0" :decimals="0" :endVal="statistics.comment"
+									:duration="3000"></tm-flop>
+							</view>
+							<view class="mt-6 text-align-center text-size-s text-grey-darken-1">评论数量</view>
+						</view>
+
+						<view class="item flex-1 text-align-center">
+							<view class="number text-size-xl text-bg-gradient-blue-accent">
+								<tm-flop :startVal="0" :decimals="0" :endVal="statistics.upvote"
+									:duration="3000"></tm-flop>
+							</view>
+							<view class="mt-6 text-size-s text-grey-darken-1">点赞数量</view>
+						</view>
+					</view>
+				</template>
+			</tm-more>
+		</view>
+
+		<!-- 功能导航 -->
+		<view class="nav-wrap ma-24 round-3">
+			<tm-grouplist :shadow="0" :round="3" :margin="[0, 0]">
+				<block v-for="(nav, index) in navList" :key="index">
+					<tm-listitem v-if="nav.show" :title="nav.title" :left-icon="nav.leftIcon" show-left-icon
+						:left-icon-color="nav.leftIconColor" :value="nav.rightText" @click="fnOnNav(nav)">
+						<template slot="rightValue">
+							<button class="right-value-btn" v-if="nav.openType" :open-type="nav.openType">
+								{{ nav.rightText }}
+							</button>
+							<text v-else>{{ nav.rightText }}</text>
+						</template>
+					</tm-listitem>
+				</block>
+			</tm-grouplist>
+		</view>
+		<!-- 版权 -->
+		<view v-if="copyrightConfig.enabled" class="copyright mt-40 text-size-xs text-align-center">
+			<view class="">{{ copyrightConfig.content }}</view>
+		</view>
+	</view>
 </template>
 
 <script>
-import {checkHasAdminLogin} from '@/utils/auth.js';
-import CheckAppUpdate from '@/uni_modules/uni-upgrade-center-app/utils/check-update';
-import {CheckWxUpdate} from '@/utils/update.js';
-
-import tmGrouplist from '@/tm-vuetify/components/tm-grouplist/tm-grouplist.vue';
-import tmListitem from '@/tm-vuetify/components/tm-listitem/tm-listitem.vue';
-import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
-import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
-import tmMore from '@/tm-vuetify/components/tm-more/tm-more.vue';
-import tmFlop from '@/tm-vuetify/components/tm-flop/tm-flop.vue';
-import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
-import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
-import wave from '@/components/wave/wave.vue';
-
-export default {
-    components: {
-        tmGrouplist,
-        tmListitem,
-        tmTranslate,
-        tmPoup,
-        tmMore,
-        tmFlop,
-        tmButton,
-        tmIcons,
-        wave
-    },
-    data() {
-        return {
-            statisticsShowMore: false,
-            // 统计信息
-            statistics: {
-                post: 0, // 文章数量
-                comment: 0, // 评论数量
-                category: 0, // 分类数量
-                visit: 0, // 访客数量
-                upvote: 0 // 点赞数量
-            },
-            // 导航信息
-            navList: [],
-            miniProfileCard: {
-                show: false
-            }
-        };
-    },
-    computed: {
-        haloConfigs() {
-            return this.$tm.vx.getters().getConfigs
-        },
-        pageConfig() {
-            return this.haloConfigs.pageConfig.aboutConfig;
-        },
-        postDetailConfig() {
-            return this.haloConfigs.basicConfig.postDetailConfig;
-        },
-        bloggerInfo() {
-            return this.haloConfigs.authorConfig.blogger;
-        },
-        calcProfileStyle() {
-            const _imgUrlOr = this.pageConfig.bgImageUrl;
-            return {
-                backgroundImage: `url(${this.$utils.checkImageUrl(_imgUrlOr)})`
-            }
-        },
-        calcWaveUrl() {
-            return this.$utils.checkImageUrl(this.pageConfig.waveImageUrl);
-        },
-        copyrightConfig() {
-            return this.haloConfigs.basicConfig.copyrightConfig;
-        }, calcAuditModeEnabled() {
-            return this.haloConfigs.auditConfig.auditModeEnabled
-        },
-    },
-    watch: {
-        haloConfigs: {
-            handler(val) {
-                if (!val) return;
-                this.fnGetNavList();
-            },
-            deep: true,
-            immediate: true,
-        }
-    },
-    created() {
-        this.fnGetData();
-    },
-    onPullDownRefresh() {
-        this.fnGetData();
-    },
-    methods: {
-        fnGetNavList() {
-            const systemInfo = uni.getSystemInfoSync();
-            let _isWx = false;
-            // #ifdef MP-WEIXIN
-            _isWx = true;
-            // #endif
-            this.navList = [
-                {
-                    key: 'archives',
-                    title: this.calcAuditModeEnabled ? '内容归档' : '文章归档',
-                    leftIcon: 'halocoloricon-classify',
-                    leftIconColor: 'red',
-                    rightText: this.calcAuditModeEnabled ? '已归档的内容' : '已归档的文章',
-                    path: '/pagesA/archives/archives',
-                    isAdmin: false,
-                    type: 'page',
-                    show: true
-                }, {
-                    key: 'love',
-                    title: '恋爱日记',
-                    leftIcon: 'halocoloricon-attent',
-                    leftIconColor: 'red',
-                    rightText: '博主的恋爱日记',
-                    path: '/pagesA/love/love',
-                    isAdmin: false,
-                    type: 'page',
-                    show: this.haloConfigs.loveConfig.loveEnabled
-                }, {
-                    key: 'disclaimers',
-                    title: '友情链接',
-                    leftIcon: 'icon-lianjie',
-                    leftIconColor: 'blue',
-                    rightText: '看看博主朋友们吧',
-                    path: '/pagesA/friend-links/friend-links',
-                    isAdmin: false,
-                    type: 'page',
-                    show: true
-                },
-                {
-                    key: 'disclaimers',
-                    title: '免责声明',
-                    leftIcon: 'icon-map',
-                    leftIconColor: 'red',
-                    rightText: '博客内容免责声明',
-                    path: '/pagesA/disclaimers/disclaimers',
-                    isAdmin: false,
-                    type: 'page',
-                    show: this.haloConfigs.basicConfig.disclaimers.enabled
-                },
-                {
-                    key: 'contact-blogger',
-                    title: '联系博主',
-                    leftIcon: 'icon-paper-plane',
-                    leftIconColor: 'orange',
-                    rightText: '博主常用联系方式',
-                    path: '/pagesA/contact/contact',
-                    isAdmin: false,
-                    type: 'page',
-                    show: this.haloConfigs.authorConfig.social.enabled
-                },
-                {
-                    key: 'session',
-                    title: '在线客服',
-                    leftIcon: 'icon-headset-fill',
-                    leftIconColor: 'cyan',
-                    rightText: '在线客服为您答疑',
-                    path: null,
-                    isAdmin: false,
-                    type: 'page',
-                    openType: 'contact',
-                    show: _isWx
-                },
-                {
-                    key: 'feedback',
-                    title: '意见反馈',
-                    leftIcon: 'icon-comment-dots',
-                    leftIconColor: 'light-blue',
-                    rightText: '提交系统使用反馈',
-                    path: null,
-                    isAdmin: false,
-                    type: 'page',
-                    openType: 'feedback',
-                    show: _isWx
-                },
-                {
-                    key: 'about',
-                    title: '关于项目',
-                    leftIcon: 'icon-exclamation-circle',
-                    leftIconColor: 'blue',
-                    rightText: '小莫唐尼开源项目',
-                    path: '/pagesA/about/about',
-                    isAdmin: false,
-                    type: 'page',
-                    show: this.haloConfigs.basicConfig.showAboutSystem
-                },
-                // {
-                // 	key: 'cache',
-                // 	title: '清除缓存',
-                // 	leftIcon: 'icon-delete',
-                // 	leftIconColor: 'gray',
-                // 	rightText: uni.getStorageInfoSync().currentSize + 'KB',
-                // 	path: 'clear',
-                // 	isAdmin: false,
-                // 	type: 'poup',
-                // 	show: true
-                // },
-                // {
-                // 	key: 'update',
-                // 	title: '版本更新',
-                // 	leftIcon: 'icon-clouddownload',
-                // 	leftIconColor: 'pink',
-                // 	rightText: `当前版本 v${systemInfo.appVersion}`,
-                // 	path: 'update',
-                // 	isAdmin: false,
-                // 	type: 'poup',
-                // 	show: true
-                // },
-
-            ];
-        },
-        fnGetData() {
-            this.$httpApi.v2
-                .getBlogStatistics()
-                .then(res => {
-                    this.statistics = res;
-                })
-                .catch(err => {
-                    this.$tm.toast('数据加载失败,请重试!');
-                })
-                .finally(() => {
-                    uni.stopPullDownRefresh();
-                });
-        },
-
-        fnOnNav(data) {
-            const {
-                type,
-                path,
-                isAdmin,
-                openType
-            } = data;
-            if (openType) {
-                // #ifndef MP-WEIXIN
-                return uni.$tm.toast('仅支持微信小程序打开!');
-                // #endif
-                // #ifdef MP-WEIXIN
-                return;
-                // #endif
-            }
-            if (!path) return;
-
-            // 拦截后台管理页面(插件拦截不友好,无法阻断)
-            if (isAdmin && !checkHasAdminLogin()) {
-                uni.$eShowModal({
-                    title: '提示',
-                    content: '未登录超管账号或登录状态已过期,是否立即登录?',
-                    showCancel: true,
-                    cancelText: '否',
-                    cancelColor: '#999999',
-                    confirmText: '是',
-                    confirmColor: '#03a9f4'
-                })
-                    .then(res => {
-                        uni.navigateTo({
-                            url: '/pagesB/login/login'
-                        });
-                    })
-                    .catch(err => {
-                    });
-                return;
-            }
-
-            if (type == 'poup') {
-                switch (path) {
-                    case 'clear':
-                        uni.$eShowModal({
-                            title: '提示',
-                            content: '清除后可能退出您当前的登录或已授权状态,是否确定清除缓存吗?',
-                            showCancel: true,
-                            cancelText: '否',
-                            cancelColor: '#999999',
-                            confirmText: '是',
-                            confirmColor: '#03a9f4'
-                        })
-                            .then(res => {
-                                uni.clearStorageSync();
-                                this.navList.find(x => x.key == 'cache').rightText =
-                                    uni.getStorageInfoSync().currentSize + 'KB';
-                            })
-                            .catch(err => {
-                            });
-                        break;
-                    case 'update':
-                        // #ifdef APP-PLUS
-                        CheckAppUpdate();
-                        // #endif
-
-                        // #ifdef MP-WEIXIN
-                        CheckWxUpdate(true);
-                        // #endif
-
-                        // #ifndef APP-PLUS|| MP-WEIXIN
-                        uni.showToast({
-                            icon: 'none',
-                            title: '版本无需更新!'
-                        });
-                        // #endif
-
-                        break;
-                }
-            } else if (type == 'page') {
-                uni.navigateTo({
-                    url: path
-                })
-            }
-        },
-
-        // 快捷导航页面跳转
-        fnToNavPage(item) {
-            // 判断是内置页面还是网页
-            if (this.$utils.checkIsUrl(item.path)) {
-                uni.navigateTo({
-                    url: '/pagesC/website/website?data=' +
-                        JSON.stringify({
-                            title: item.text || this.$haloConfig.title,
-                            url: encodeURIComponent(item.path)
-                        })
-                });
-                return;
-            }
-            switch (item.type) {
-                case 'tabbar':
-                    uni.switchTab({
-                        url: item.path
-                    });
-                    break;
-                case 'page':
-                    uni.navigateTo({
-                        url: item.path
-                    });
-                    break;
-            }
-        },
-        fnOnToAdTest(path) {
-            uni.navigateTo({
-                url: path
-            });
-        }
-    }
-};
+	import {
+		checkHasAdminLogin
+	} from '@/utils/auth.js';
+	import CheckAppUpdate from '@/uni_modules/uni-upgrade-center-app/utils/check-update';
+	import {
+		CheckWxUpdate
+	} from '@/utils/update.js';
+	import {
+		NeedPluginIds,
+		NeedPlugins,
+		checkNeedPluginAvailable
+	} from "@/utils/plugin.js"
+
+	import tmGrouplist from '@/tm-vuetify/components/tm-grouplist/tm-grouplist.vue';
+	import tmListitem from '@/tm-vuetify/components/tm-listitem/tm-listitem.vue';
+	import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
+	import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
+	import tmMore from '@/tm-vuetify/components/tm-more/tm-more.vue';
+	import tmFlop from '@/tm-vuetify/components/tm-flop/tm-flop.vue';
+	import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
+	import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
+	import wave from '@/components/wave/wave.vue';
+
+	export default {
+		components: {
+			tmGrouplist,
+			tmListitem,
+			tmTranslate,
+			tmPoup,
+			tmMore,
+			tmFlop,
+			tmButton,
+			tmIcons,
+			wave
+		},
+		data() {
+			return {
+				statisticsShowMore: false,
+				// 统计信息
+				statistics: {
+					post: 0, // 文章数量
+					comment: 0, // 评论数量
+					category: 0, // 分类数量
+					visit: 0, // 访客数量
+					upvote: 0 // 点赞数量
+				},
+				// 导航信息
+				navList: [],
+				miniProfileCard: {
+					show: false
+				}
+			};
+		},
+		computed: {
+			haloConfigs() {
+				return this.$tm.vx.getters().getConfigs
+			},
+			pageConfig() {
+				return this.haloConfigs.pageConfig.aboutConfig;
+			},
+			postDetailConfig() {
+				return this.haloConfigs.basicConfig.postDetailConfig;
+			},
+			bloggerInfo() {
+				return this.haloConfigs.authorConfig.blogger;
+			},
+			calcProfileStyle() {
+				const _imgUrlOr = this.pageConfig.bgImageUrl;
+				return {
+					backgroundImage: `url(${this.$utils.checkImageUrl(_imgUrlOr)})`
+				}
+			},
+			calcWaveUrl() {
+				return this.$utils.checkImageUrl(this.pageConfig.waveImageUrl);
+			},
+			copyrightConfig() {
+				return this.haloConfigs.basicConfig.copyrightConfig;
+			},
+			calcAuditModeEnabled() {
+				return this.haloConfigs.auditConfig.auditModeEnabled
+			},
+		},
+		watch: {
+			haloConfigs: {
+				handler(val) {
+					if (!val) return;
+					this.fnGetNavList();
+				},
+				deep: true,
+				immediate: true,
+			}
+		},
+		created() {
+			this.fnGetData();
+		},
+		onPullDownRefresh() {
+			this.fnGetData();
+		},
+		methods: {
+			async fnGetNavList() {
+				const systemInfo = uni.getSystemInfoSync();
+				let _isWx = false;
+				// #ifdef MP-WEIXIN
+				_isWx = true;
+				// #endif
+				this.navList = [{
+						key: 'archives',
+						title: this.calcAuditModeEnabled ? '内容归档' : '文章归档',
+						leftIcon: 'halocoloricon-classify',
+						leftIconColor: 'red',
+						rightText: this.calcAuditModeEnabled ? '已归档的内容' : '已归档的文章',
+						path: '/pagesA/archives/archives',
+						isAdmin: false,
+						type: 'page',
+						show: true
+					}, {
+						key: 'love',
+						title: '恋爱日记',
+						leftIcon: 'halocoloricon-attent',
+						leftIconColor: 'red',
+						rightText: '博主的恋爱日记',
+						path: '/pagesA/love/love',
+						isAdmin: false,
+						type: 'page',
+						show: this.haloConfigs.loveConfig.loveEnabled
+					},  {
+						key: 'vote',
+						title: '投票中心',
+						leftIcon: 'icon-box',
+						leftIconColor: 'red',
+						rightText: '查看当前的投票',
+						path: '/pagesA/votes/votes',
+						isAdmin: false,
+						type: 'page',
+						show: true
+					}, {
+						key: 'disclaimers',
+						title: '友情链接',
+						leftIcon: 'icon-lianjie',
+						leftIconColor: 'blue',
+						rightText: '看看博主朋友们吧',
+						path: '/pagesA/friend-links/friend-links',
+						isAdmin: false,
+						type: 'page',
+						show: true
+					},
+					{
+						key: 'disclaimers',
+						title: '免责声明',
+						leftIcon: 'icon-map',
+						leftIconColor: 'red',
+						rightText: '博客内容免责声明',
+						path: '/pagesA/disclaimers/disclaimers',
+						isAdmin: false,
+						type: 'page',
+						show: this.haloConfigs.basicConfig.disclaimers.enabled
+					},
+					{
+						key: 'contact-blogger',
+						title: '联系博主',
+						leftIcon: 'icon-paper-plane',
+						leftIconColor: 'orange',
+						rightText: '博主常用联系方式',
+						path: '/pagesA/contact/contact',
+						isAdmin: false,
+						type: 'page',
+						show: this.haloConfigs.authorConfig.social.enabled
+					},
+					{
+						key: 'session',
+						title: '在线客服',
+						leftIcon: 'icon-headset-fill',
+						leftIconColor: 'cyan',
+						rightText: '在线客服为您答疑',
+						path: null,
+						isAdmin: false,
+						type: 'page',
+						openType: 'contact',
+						show: _isWx
+					},
+					{
+						key: 'feedback',
+						title: '意见反馈',
+						leftIcon: 'icon-comment-dots',
+						leftIconColor: 'light-blue',
+						rightText: '提交系统使用反馈',
+						path: null,
+						isAdmin: false,
+						type: 'page',
+						openType: 'feedback',
+						show: _isWx
+					},
+					{
+						key: 'about',
+						title: '关于项目',
+						leftIcon: 'icon-exclamation-circle',
+						leftIconColor: 'blue',
+						rightText: '小莫唐尼开源项目',
+						path: '/pagesA/about/about',
+						isAdmin: false,
+						type: 'page',
+						show: this.haloConfigs.basicConfig.showAboutSystem
+					},
+					// {
+					// 	key: 'cache',
+					// 	title: '清除缓存',
+					// 	leftIcon: 'icon-delete',
+					// 	leftIconColor: 'gray',
+					// 	rightText: uni.getStorageInfoSync().currentSize + 'KB',
+					// 	path: 'clear',
+					// 	isAdmin: false,
+					// 	type: 'poup',
+					// 	show: true
+					// },
+					// {
+					// 	key: 'update',
+					// 	title: '版本更新',
+					// 	leftIcon: 'icon-clouddownload',
+					// 	leftIconColor: 'pink',
+					// 	rightText: `当前版本 v${systemInfo.appVersion}`,
+					// 	path: 'update',
+					// 	isAdmin: false,
+					// 	type: 'poup',
+					// 	show: true
+					// },
+
+				];
+			},
+			fnGetData() {
+				this.$httpApi.v2
+					.getBlogStatistics()
+					.then(res => {
+						this.statistics = res;
+					})
+					.catch(err => {
+						this.$tm.toast('数据加载失败,请重试!');
+					})
+					.finally(() => {
+						uni.stopPullDownRefresh();
+					});
+			},
+
+			fnOnNav(data) {
+				const {
+					type,
+					path,
+					isAdmin,
+					openType
+				} = data;
+				if (openType) {
+					// #ifndef MP-WEIXIN
+					return uni.$tm.toast('仅支持微信小程序打开!');
+					// #endif
+					// #ifdef MP-WEIXIN
+					return;
+					// #endif
+				}
+				if (!path) return;
+
+				// 拦截后台管理页面(插件拦截不友好,无法阻断)
+				if (isAdmin && !checkHasAdminLogin()) {
+					uni.$eShowModal({
+							title: '提示',
+							content: '未登录超管账号或登录状态已过期,是否立即登录?',
+							showCancel: true,
+							cancelText: '否',
+							cancelColor: '#999999',
+							confirmText: '是',
+							confirmColor: '#03a9f4'
+						})
+						.then(res => {
+							uni.navigateTo({
+								url: '/pagesB/login/login'
+							});
+						})
+						.catch(err => {});
+					return;
+				}
+
+				if (type == 'poup') {
+					switch (path) {
+						case 'clear':
+							uni.$eShowModal({
+									title: '提示',
+									content: '清除后可能退出您当前的登录或已授权状态,是否确定清除缓存吗?',
+									showCancel: true,
+									cancelText: '否',
+									cancelColor: '#999999',
+									confirmText: '是',
+									confirmColor: '#03a9f4'
+								})
+								.then(res => {
+									uni.clearStorageSync();
+									this.navList.find(x => x.key == 'cache').rightText =
+										uni.getStorageInfoSync().currentSize + 'KB';
+								})
+								.catch(err => {});
+							break;
+						case 'update':
+							// #ifdef APP-PLUS
+							CheckAppUpdate();
+							// #endif
+
+							// #ifdef MP-WEIXIN
+							CheckWxUpdate(true);
+							// #endif
+
+							// #ifndef APP-PLUS|| MP-WEIXIN
+							uni.showToast({
+								icon: 'none',
+								title: '版本无需更新!'
+							});
+							// #endif
+
+							break;
+					}
+				} else if (type == 'page') {
+					uni.navigateTo({
+						url: path
+					})
+				}
+			},
+
+			// 快捷导航页面跳转
+			fnToNavPage(item) {
+				// 判断是内置页面还是网页
+				if (this.$utils.checkIsUrl(item.path)) {
+					uni.navigateTo({
+						url: '/pagesC/website/website?data=' +
+							JSON.stringify({
+								title: item.text || this.$haloConfig.title,
+								url: encodeURIComponent(item.path)
+							})
+					});
+					return;
+				}
+				switch (item.type) {
+					case 'tabbar':
+						uni.switchTab({
+							url: item.path
+						});
+						break;
+					case 'page':
+						uni.navigateTo({
+							url: item.path
+						});
+						break;
+				}
+			},
+			fnOnToAdTest(path) {
+				uni.navigateTo({
+					url: path
+				});
+			}
+		}
+	};
 </script>
 
 <style scoped lang="scss">
-.app-page {
-    width: 100vw;
-    min-height: 100vh;
-}
-
-.blogger-info {
-    position: relative;
-    width: 100%;
-    height: 600rpx;
-    background-size: cover;
-    background-repeat: no-repeat;
-
-    &:before {
-        content: '';
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        background-color: rgba(0, 0, 0, 0.3);
-        background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAKUlEQVQImU3IMREAIAgAwJfNkQCEsH8cijjpMf6vnXlQaIiJFx+omEBfmqIEZLe2jzcAAAAASUVORK5CYII=);
-        z-index: 0;
-    }
-
-    .avatar {
-        position: absolute;
-        top: 200rpx;
-        left: 50%;
-        transform: translateX(-50%);
-        width: 130rpx;
-        height: 130rpx;
-        border-radius: 50%;
-        border: 6rpx solid #ffffff;
-    }
-
-    .profile {
-        width: 100%;
-        position: absolute;
-        top: 340rpx;
-        left: 0;
-        z-index: 6;
-        color: #fff;
-        text-align: center;
-    }
-
-    .gif-wave {
-        position: absolute;
-        width: 100%;
-        bottom: 0;
-        left: 0;
-        z-index: 99;
-        mix-blend-mode: screen;
-        height: 100rpx;
-    }
-}
-
-.profile-card {
-    position: relative;
-    background-color: #fff;
-    overflow: hidden;
-
-    &_label {
-        width: 120rpx;
-        position: absolute;
-        top: 8rpx;
-        left: -36rpx;
-        transform: rotateZ(-45deg);
-        text-align: center;
-        color: #ffffff;
-    }
-
-    .left {
-        width: 260rpx;
-
-        .avatar {
-            width: 130rpx;
-            height: 130rpx;
-            border-radius: 50%;
-        }
-    }
-
-    .right {
-        width: 0;
-        flex-grow: 1;
-
-        .photos {
-            &-img {
-                width: 130rpx;
-                height: 130rpx;
-            }
-        }
-
-        .photos-img + .photos-img {
-            margin-left: 12rpx;
-        }
-    }
-}
-
-.statistics-wrap {
-    box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
-    border-radius: 0rpx 0rpx 24rpx 24rpx;
-    overflow: hidden;
-
-    .statistics {
-        &.has-solid {
-            .item + .item {
-                border-left: 2rpx solid #fafafa;
-            }
-        }
-
-        &.solid-top {
-            position: relative;
-
-            &:before {
-                content: '';
-                position: absolute;
-                top: 0;
-                left: 36rpx;
-                right: 36rpx;
-                height: 2rpx;
-                background-color: #fafafa;
-            }
-        }
-    }
-}
-
-.quick-nav {
-    background-color: #fff;
-    box-sizing: border-box;
-    box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
-
-    .name {
-        color: var(--main-text-color);
-    }
-
-    .icon {
-        border-radius: 50%;
-        font-size: 80rpx;
-    }
-}
-
-.nav-wrap {
-    box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
-    background-color: #fff;
-}
-
-.copyright {
-    color: #c0c4c7;
-}
-
-.right-value-btn {
-    background-color: transparent;
-    border: none;
-    padding: 0;
-    margin: 0;
-    font-size: 24rpx;
-    color: #c0c4c7;
-    border-radius: 0;
-    line-height: initial;
-
-    &::after {
-        border: none;
-        border-radius: 0;
-        transform: initial;
-    }
-}
-</style>
+	.app-page {
+		width: 100vw;
+		min-height: 100vh;
+	}
+
+	.blogger-info {
+		position: relative;
+		width: 100%;
+		height: 600rpx;
+		background-size: cover;
+		background-repeat: no-repeat;
+
+		&:before {
+			content: '';
+			width: 100%;
+			height: 100%;
+			position: absolute;
+			background-color: rgba(0, 0, 0, 0.3);
+			background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAKUlEQVQImU3IMREAIAgAwJfNkQCEsH8cijjpMf6vnXlQaIiJFx+omEBfmqIEZLe2jzcAAAAASUVORK5CYII=);
+			z-index: 0;
+		}
+
+		.avatar {
+			position: absolute;
+			top: 200rpx;
+			left: 50%;
+			transform: translateX(-50%);
+			width: 130rpx;
+			height: 130rpx;
+			border-radius: 50%;
+			border: 6rpx solid #ffffff;
+		}
+
+		.profile {
+			width: 100%;
+			position: absolute;
+			top: 340rpx;
+			left: 0;
+			z-index: 6;
+			color: #fff;
+			text-align: center;
+		}
+
+		.gif-wave {
+			position: absolute;
+			width: 100%;
+			bottom: 0;
+			left: 0;
+			z-index: 99;
+			mix-blend-mode: screen;
+			height: 100rpx;
+		}
+	}
+
+	.profile-card {
+		position: relative;
+		background-color: #fff;
+		overflow: hidden;
+
+		&_label {
+			width: 120rpx;
+			position: absolute;
+			top: 8rpx;
+			left: -36rpx;
+			transform: rotateZ(-45deg);
+			text-align: center;
+			color: #ffffff;
+		}
+
+		.left {
+			width: 260rpx;
+
+			.avatar {
+				width: 130rpx;
+				height: 130rpx;
+				border-radius: 50%;
+			}
+		}
+
+		.right {
+			width: 0;
+			flex-grow: 1;
+
+			.photos {
+				&-img {
+					width: 130rpx;
+					height: 130rpx;
+				}
+			}
+
+			.photos-img+.photos-img {
+				margin-left: 12rpx;
+			}
+		}
+	}
+
+	.statistics-wrap {
+		box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
+		border-radius: 0rpx 0rpx 24rpx 24rpx;
+		overflow: hidden;
+
+		.statistics {
+			&.has-solid {
+				.item+.item {
+					border-left: 2rpx solid #fafafa;
+				}
+			}
+
+			&.solid-top {
+				position: relative;
+
+				&:before {
+					content: '';
+					position: absolute;
+					top: 0;
+					left: 36rpx;
+					right: 36rpx;
+					height: 2rpx;
+					background-color: #fafafa;
+				}
+			}
+		}
+	}
+
+	.quick-nav {
+		background-color: #fff;
+		box-sizing: border-box;
+		box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
+
+		.name {
+			color: var(--main-text-color);
+		}
+
+		.icon {
+			border-radius: 50%;
+			font-size: 80rpx;
+		}
+	}
+
+	.nav-wrap {
+		box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
+		background-color: #fff;
+	}
+
+	.copyright {
+		color: #c0c4c7;
+	}
+
+	.right-value-btn {
+		background-color: transparent;
+		border: none;
+		padding: 0;
+		margin: 0;
+		font-size: 24rpx;
+		color: #c0c4c7;
+		border-radius: 0;
+		line-height: initial;
+
+		&::after {
+			border: none;
+			border-radius: 0;
+			transform: initial;
+		}
+	}
+</style>

+ 732 - 610
pages/tabbar/home/home.vue

@@ -1,617 +1,739 @@
 <template>
-    <view class="app-page">
-        <tm-menubars iconColor="white" color="white" :flat="true" :showback="false">
-            <view slot="left">
-                <image @click="fnOnLogoToPage" class="logo ml-24 round-24" :src="appInfo.logo" mode="scaleToFill"/>
-            </view>
-            <view class="search-input round-12 pt-12 pb-12 flex pl-24" @click="fnToSearch">
-                <text class="search-input_icon iconfont text-size-m icon-search text-grey"></text>
-                <view class="search-input_text pl-12 text-size-m text-grey">搜索内容...</view>
-            </view>
-            <!-- #ifdef APP-PLUS || H5 -->
-            <view slot="right" class="mr-24 text-size-m text-grey text-overflow">{{ appInfo.name }}</view>
-            <!-- #endif -->
-        </tm-menubars>
-        <view v-if="loading !== 'success' && articleList.length===0" class="loading-wrap">
-            <tm-skeleton model="card"></tm-skeleton>
-            <tm-skeleton model="cardActions"></tm-skeleton>
-            <tm-skeleton model="list"></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="bannerConfig.enabled" class="bg-white pb-24">
-                <view class="banner bg-white ml-24 mr-24 mt-12 round-3" v-if="bannerList.length !== 0">
-                    <e-swiper
-                        :height="bannerConfig.height"
-                        :dotPosition="bannerConfig.dotPosition"
-                        :autoplay="true"
-                        :useDot="bannerConfig.showIndicator"
-                        :showTitle="bannerConfig.showTitle"
-                        :type="bannerConfig.type"
-                        :list="bannerList"
-                        @on-click="fnOnBannerClick"
-                    />
-                </view>
-            </view>
-            <!-- 精品分类 -->
-            <block v-if="calcIsShowCategory">
-                <view class="flex flex-between mt-16 mb-24 pl-24 pr-24">
-                    <view class="page-item_title text-weight-b ">精选分类</view>
-                    <view class="show-more flex flex-center bg-white round-3" @click="fnToCategoryPage">
-                        <text class="iconfont icon-angle-right text-size-s text-grey-darken-1"></text>
-                    </view>
-                    <view v-if="false" class="flex flex-center text-size-s text-grey-darken-1"
-                          @click="fnToCategoryPage">
-                        <text class=" text-size-m">查看更多</text>
-                        <text class="iconfont icon-angle-right  text-size-s "></text>
-                    </view>
-                </view>
-                <scroll-view class="category" scroll-x="true">
-                    <view v-if="categoryList.length === 0" class="cate-empty round-3 mr-5 flex flex-center text-grey">
-                        还没有任何分类~
-                    </view>
-                    <block v-else>
-                        <view class="content" v-for="(category, index) in categoryList" :key="category.metadata.name"
-                              @click="fnToCategoryBy(category)">
-                            <category-mini-card :category="category"></category-mini-card>
-                        </view>
-                    </block>
-                </scroll-view>
-            </block>
-
-            <!-- 最新文章 -->
-            <view class="flex flex-between mt-24 mb-24 pl-24 pr-24">
-                <view class="page-item_title text-weight-b">最新列表</view>
-                <view class="show-more flex flex-center bg-white round-3" @click="fnToArticlesPage">
-                    <text class="iconfont icon-angle-right text-size-s text-grey-darken-1"></text>
-                </view>
-                <view v-if="false" class="flex flex-center text-size-s text-grey-darken-1" @click="fnToArticlesPage">
-                    <text class=" text-size-m ">查看更多</text>
-                    <text class="iconfont icon-angle-right text-size-s "></text>
-                </view>
-            </view>
-            <view v-if="articleList.length === 0" class="article-empty">
-                <tm-empty icon="icon-shiliangzhinengduixiang-" label="博主还没有发表任何内容~"></tm-empty>
-            </view>
-            <block v-else>
-                <view :class="globalAppSettings.layout.home">
-                    <tm-translate v-for="(article, index) in articleList" :key="index" class="ani-item"
-                                  animation-name="fadeUp" :wait="calcAniWait(index)">
-                        <article-card from="home" :article="article" :post="article"
-                                      @on-click="fnToArticleDetail"></article-card>
-                    </tm-translate>
-                </view>
-                <view class="load-text mt-12">{{ loadMoreText }}</view>
-                <tm-flotbutton v-if="articleList.length > 10" :width="90" color="light-blue" @click="fnToTopPage" size="s"
-                               icon="icon-angle-up"></tm-flotbutton>
-            </block>
-        </block>
-
-
-        <!-- 弹窗 -->
-        <NotifyDialog v-if="notify.show" :show="notify.show" :title="notify.data.title" :content="notify.data.content"
-                      :url="notify.data.url" @on-change="fnOnNotifyChange"></NotifyDialog>
-    </view>
+	<view class="app-page">
+		<tm-menubars iconColor="white" color="white" :flat="true" :showback="false">
+			<view slot="left">
+				<image @click="fnOnLogoToPage" class="logo ml-24 round-24" :src="appInfo.logo" mode="scaleToFill" />
+			</view>
+			<view class="search-input round-12 pt-12 pb-12 flex pl-24" @click="fnToSearch">
+				<text class="search-input_icon iconfont text-size-m icon-search text-grey"></text>
+				<view class="search-input_text pl-12 text-size-m text-grey">搜索内容...</view>
+			</view>
+			<!-- #ifdef APP-PLUS || H5 -->
+			<view slot="right" class="mr-24 text-size-m text-grey text-overflow">{{ appInfo.name }}</view>
+			<!-- #endif -->
+		</tm-menubars>
+		<view v-if="loading !== 'success' && articleList.length===0" class="loading-wrap">
+			<tm-skeleton model="card"></tm-skeleton>
+			<tm-skeleton model="cardActions"></tm-skeleton>
+			<tm-skeleton model="list"></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="bannerConfig.enabled" class="bg-white pb-24">
+				<view class="banner bg-white ml-24 mr-24 mt-12 round-3" v-if="bannerList.length !== 0">
+					<e-swiper :height="bannerConfig.height" :dotPosition="bannerConfig.dotPosition" :autoplay="true"
+						:useDot="bannerConfig.showIndicator" :showTitle="bannerConfig.showTitle"
+						:type="bannerConfig.type" :list="bannerList" @on-click="fnOnBannerClick" />
+				</view>
+			</view>
+
+			<!-- 金刚区 :v-if="navList.filter(x=>x.show).length>=4" -->
+			<view :v-if="navList.filter(x=>x.show).length>=4" class="nav-box">
+				<view class="nav-list flex">
+					<template v-for="(item,index) in navList" >
+						<view v-if="item.show" class="nav-item" :key="index" @click="fnClickNav(item)">
+							<!-- :class="[item.bgClass]" -->
+							<view class="nav-item-icon" :class="[item.bgClass]" :style="{
+								'--bgColor':item.bgColor,
+								// boxShadow: '0rpx 0rpx 6rpx ' + item.shadow,
+								// backgroundColor: item.bgColor
+							}">
+								<tm-icons :size="48" color="white" prefx="halocoloricon" :name="item.icon"></tm-icons>
+							</view>
+							<view class="nav-item-text">
+								{{item.title}}
+							</view>
+						</view>
+					</template>
+				</view>
+			</view>
+
+			<!-- 精品分类 -->
+			<block v-if="calcIsShowCategory">
+				<view class="flex flex-between mt-16 mb-24 pl-24 pr-24">
+					<view class="page-item_title text-weight-b ">精选分类</view>
+					<view class="show-more flex flex-center bg-white round-3" @click="fnToCategoryPage">
+						<text class="iconfont icon-angle-right text-size-s text-grey-darken-1"></text>
+					</view>
+					<view v-if="false" class="flex flex-center text-size-s text-grey-darken-1"
+						@click="fnToCategoryPage">
+						<text class=" text-size-m">查看更多</text>
+						<text class="iconfont icon-angle-right  text-size-s "></text>
+					</view>
+				</view>
+				<scroll-view class="category" scroll-x="true">
+					<view v-if="categoryList.length === 0" class="cate-empty round-3 mr-5 flex flex-center text-grey">
+						还没有任何分类~
+					</view>
+					<block v-else>
+						<view class="content" v-for="(category, index) in categoryList" :key="category.metadata.name"
+							@click="fnToCategoryBy(category)">
+							<category-mini-card :category="category"></category-mini-card>
+						</view>
+					</block>
+				</scroll-view>
+			</block>
+
+			<!-- 最新文章 -->
+			<view class="flex flex-between mt-24 mb-24 pl-24 pr-24">
+				<view class="page-item_title text-weight-b">最新列表</view>
+				<view class="show-more flex flex-center bg-white round-3" @click="fnToArticlesPage">
+					<text class="iconfont icon-angle-right text-size-s text-grey-darken-1"></text>
+				</view>
+				<view v-if="false" class="flex flex-center text-size-s text-grey-darken-1" @click="fnToArticlesPage">
+					<text class=" text-size-m ">查看更多</text>
+					<text class="iconfont icon-angle-right text-size-s "></text>
+				</view>
+			</view>
+			<view v-if="articleList.length === 0" class="article-empty">
+				<tm-empty icon="icon-shiliangzhinengduixiang-" label="博主还没有发表任何内容~"></tm-empty>
+			</view>
+			<block v-else>
+				<view :class="globalAppSettings.layout.home">
+					<tm-translate v-for="(article, index) in articleList" :key="index" class="ani-item"
+						animation-name="fadeUp" :wait="calcAniWait(index)">
+						<article-card from="home" :article="article" :post="article"
+							@on-click="fnToArticleDetail"></article-card>
+					</tm-translate>
+				</view>
+				<view class="load-text mt-12">{{ loadMoreText }}</view>
+				<tm-flotbutton v-if="articleList.length > 10" :width="90" color="light-blue" @click="fnToTopPage"
+					size="s" icon="icon-angle-up"></tm-flotbutton>
+			</block>
+		</block>
+
+
+		<!-- 弹窗 -->
+		<NotifyDialog v-if="notify.show" :show="notify.show" :title="notify.data.title" :content="notify.data.content"
+			:url="notify.data.url" @on-change="fnOnNotifyChange"></NotifyDialog>
+	</view>
 </template>
 
 <script>
-import tmMenubars from '@/tm-vuetify/components/tm-menubars/tm-menubars.vue';
-import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
-import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
-import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
-import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
-import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
-
-import eSwiper from '@/components/e-swiper/e-swiper.vue';
-import NotifyDialog from "@/components/notify-dialog/notify-dialog.vue";
-import qs from 'qs'
-
-export default {
-    components: {
-        tmMenubars,
-        tmSkeleton,
-        tmTranslate,
-        tmFlotbutton,
-        tmIcons,
-        tmEmpty,
-        eSwiper,
-        NotifyDialog
-    },
-    data() {
-        return {
-            loading: 'loading',
-            queryParams: {
-                size: 5,
-                page: 1,
-                sort: ['spec.pinned,desc', 'spec.publishTime,desc']
-            },
-            result: {},
-            isLoadMore: false,
-            loadMoreText: '加载中...',
-            bannerCurrent: 0,
-            bannerList: [],
-            noticeList: [],
-            articleList: [],
-            categoryList: [],
-            notify: {
-                show: false,
-                data: {}
-            }
-        };
-    },
-    computed: {
-        haloConfigs() {
-            return this.$tm.vx.getters().getConfigs;
-        },
-        bloggerInfo() {
-            const blogger = this.$tm.vx.getters().getConfigs.authorConfig.blogger;
-            blogger.avatar = this.$utils.checkAvatarUrl(blogger.avatar, true);
-            return blogger;
-        },
-        appInfo() {
-            const appInfo = this.haloConfigs.appConfig.appInfo;
-            appInfo.logo = this.$utils.checkImageUrl(appInfo.logo)
-            return appInfo;
-        },
-        mockJson() {
-            return this.$tm.vx.getters().getMockJson;
-        },
-        calcAuditModeEnabled() {
-            return this.haloConfigs.auditConfig.auditModeEnabled
-        },
-        calcIsShowCategory() {
-            if (this.calcAuditModeEnabled && this.categoryList.length !== 0) {
-                return false
-            }
-            if (this.calcAuditModeEnabled) {
-                return false
-            }
-            return this.haloConfigs.pageConfig.homeConfig.useCategory
-        },
-        bannerConfig() {
-            return this.haloConfigs.pageConfig.homeConfig.bannerConfig
-        }
-    },
-    onLoad() {
-        this.fnSetPageTitle();
-    },
-    created() {
-        this.fnQuery();
-    },
-    onPullDownRefresh() {
-        this.isLoadMore = false;
-        this.queryParams.page = 1;
-        this.fnQuery();
-    },
-    onReachBottom(e) {
-        if (this.calcAuditModeEnabled) {
-            uni.showToast({
-                icon: 'none',
-                title: '没有更多数据了'
-            });
-            return
-        }
-        if (this.result.hasNext) {
-            this.queryParams.page += 1;
-            this.isLoadMore = true;
-            this.fnGetArticleList();
-        } else {
-            uni.showToast({
-                icon: 'none',
-                title: '没有更多数据了'
-            });
-        }
-    },
-    methods: {
-        fnQuery() {
-            this.fnGetBanner();
-            this.fnGetArticleList();
-            this.fnGetCategoryList();
-        },
-        fnGetCategoryList() {
-            if (this.calcAuditModeEnabled) {
-                this.categoryList = this.mockJson.home.categoryList.map((item) => {
-                    return {
-                        metadata: {
-                            name: Date.now() * Math.random(),
-                        },
-                        spec: {
-                            displayName: item.title,
-                            cover: item.cover
-                        },
-                        postCount: 0
-                    }
-                });
-                return;
-            }
-
-            if (!this.calcIsShowCategory) {
-                return;
-            }
-
-            this.$httpApi.v2
-                .getCategoryList({
-                    fieldSelector: ['spec.hideFromList=false']
-                })
-                .then(res => {
-                    this.categoryList = res.items.sort((a, b) => {
-                        return b.postCount - a.postCount;
-                    });
-
-                    setTimeout(() => {
-                        this.loading = 'success';
-                    }, 500);
-                })
-                .catch(err => {
-                    console.error(err);
-                    this.loading = 'error';
-                })
-                .finally(() => {
-                    setTimeout(() => {
-                        uni.hideLoading();
-                        uni.stopPullDownRefresh();
-                    }, 500);
-                });
-        },
-        // 获取轮播图
-        fnGetBanner() {
-            if (this.calcAuditModeEnabled) {
-                this.bannerList = this.mockJson.home.bannerList.map((item) => {
-                    return {
-                        mp4: '',
-                        id: Date.now() * Math.random(),
-                        nickname: this.haloConfigs.authorConfig.blogger.nickname,
-                        avatar: this.$utils.checkAvatarUrl(this.haloConfigs.authorConfig.blogger.avatar),
-                        address: '',
-                        createTime: item.time,
-                        title: item.title,
-                        src: this.$utils.checkThumbnailUrl(item.cover),
-                        image: this.$utils.checkThumbnailUrl(item.cover),
-                        type: "custom",
-                        content: "",
-                        url: ""
-                    }
-                });
-                return;
-            }
-
-
-            if (!this.bannerConfig.enabled) return;
-
-            if (this.bannerConfig.type === 'custom') {
-                this.bannerList = this.bannerConfig.list.map((item) => {
-                    return {
-                        mp4: '',
-                        id: Date.now() * Math.random(),
-                        nickname: this.haloConfigs.authorConfig.blogger.nickname,
-                        avatar: this.$utils.checkAvatarUrl(this.haloConfigs.authorConfig.blogger.avatar),
-                        address: '',
-                        createTime: "",
-                        title: item.title,
-                        src: this.$utils.checkThumbnailUrl(item.cover),
-                        image: this.$utils.checkThumbnailUrl(item.cover),
-                        type: "custom",
-                        content: item.content,
-                        url: item.url
-                    }
-                })
-                return;
-            }
-
-            const paramsStr = qs.stringify(this.queryParams, {
-                allowDots: true,
-                encodeValuesOnly: true,
-                skipNulls: true,
-                encode: true,
-                arrayFormat: 'repeat'
-            })
-            uni.request({
-                url: this.$baseApiUrl + '/apis/api.content.halo.run/v1alpha1/posts?' + paramsStr,
-                method: 'GET',
-                success: (res) => {
-                    this.bannerList = res.data.items.map((item, index) => {
-                        return {
-                            mp4: '',
-                            id: item.metadata.name,
-                            nickname: item.owner.displayName,
-                            avatar: this.$utils.checkAvatarUrl(item.owner.avatar),
-                            address: '',
-                            createTime: uni.$tm.dayjs(item.spec.publishTime).fromNow(),
-                            title: item.spec.title,
-                            src: this.$utils.checkThumbnailUrl(item.spec.cover),
-                            image: this.$utils.checkThumbnailUrl(item.spec.cover),
-                            type: "post",
-                            content: item.status.excerpt,
-                            url: ""
-                        };
-                    });
-                },
-                fail: (err) => {
-                }
-            })
-
-        },
-        fnOnNotifyChange(e) {
-            this.notify.show = e;
-        },
-        fnOnBannerClick(item) {
-            if (this.calcAuditModeEnabled) {
-                return;
-            }
-            if (item.type === 'custom') {
-                if (item.content) {
-                    this.notify.data = item
-                    this.notify.show = true
-                    return;
-                }
-                if (uni.$utils.checkIsUrl(item.url)) {
-                    uni.navigateTo({
-                        url: '/pagesC/website/website?data=' +
-                            JSON.stringify({
-                                title: item.title || "加载中...",
-                                url: encodeURIComponent(item.url)
-                            })
-                    });
-                }
-                return;
-            }
-
-            if (item.id === '') return;
-            this.fnToArticleDetail({
-                metadata: {
-                    name: item.id
-                }
-            });
-        },
-        // 文章列表
-        fnGetArticleList() {
-            if (this.calcAuditModeEnabled) {
-                this.articleList = this.mockJson.home.postList.map((item) => {
-                    return {
-                        metadata: {
-                            name: Date.now() * Math.random(),
-                        },
-                        spec: {
-                            pinned: false,
-                            cover: item.cover,
-                            title: item.title,
-                            publishTime: item.time
-                        },
-                        status: {
-                            excerpt: item.desc
-                        },
-                        stats: {
-                            visit: 0
-                        }
-                    }
-                });
-                this.loading = 'success';
-                this.loadMoreText = '呜呜,没有更多数据啦~';
-                uni.hideLoading();
-                uni.stopPullDownRefresh();
-                return;
-            }
-            // 设置状态为加载中
-            if (!this.isLoadMore) {
-                this.loading = 'loading';
-            }
-            this.loadMoreText = '加载中...';
-
-            const paramsStr = qs.stringify(this.queryParams, {
-                allowDots: true,
-                encodeValuesOnly: true,
-                skipNulls: true,
-                encode: true,
-                arrayFormat: 'repeat'
-            })
-            uni.request({
-                url: this.$baseApiUrl + '/apis/api.content.halo.run/v1alpha1/posts?' + paramsStr,
-                method: 'GET',
-                success: (res) => {
-                    const data = res.data;
-                    this.result.hasNext = data.hasNext;
-                    if (this.isLoadMore) {
-                        this.articleList = this.articleList.concat(data.items);
-                    } else {
-                        this.articleList = data.items;
-                    }
-                    this.loading = 'success';
-                    this.loadMoreText = data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~';
-                    uni.hideLoading();
-                    uni.stopPullDownRefresh();
-                },
-                fail: (err) => {
-                    this.loading = 'error';
-                    this.loadMoreText = '加载失败,请下拉刷新!';
-                    uni.$tm.toast(err.message || '数据加载失败!');
-                    uni.stopPullDownRefresh();
-                }
-            })
-        },
-        //跳转文章详情
-        fnToArticleDetail(article) {
-            if (this.calcAuditModeEnabled) {
-                return;
-            }
-            uni.navigateTo({
-                url: '/pagesA/article-detail/article-detail?name=' + article.metadata.name,
-                animationType: 'slide-in-right'
-            });
-        },
-        // 快捷导航页面跳转
-        fnToNavPage(item) {
-            switch (item.type) {
-                case 'tabbar':
-                    uni.switchTab({
-                        url: item.path
-                    });
-                    break;
-                case 'page':
-                    uni.navigateTo({
-                        url: item.path
-                    });
-                    break;
-            }
-        },
-        // 分类页面
-        fnToCategoryPage() {
-            uni.switchTab({
-                url: '/pages/tabbar/category/category'
-            });
-        },
-        // 所有的文章列表页面
-        fnToArticlesPage() {
-            uni.navigateTo({
-                url: '/pagesA/articles/articles'
-            });
-        },
-
-        // 根据slug查询分类下的文章
-        fnToCategoryBy(category) {
-            if (this.calcAuditModeEnabled) {
-                return;
-            }
-            uni.navigateTo({
-                url: `/pagesA/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`
-            });
-        },
-
-        fnChangeMode() {
-            const isBlackTheme = this.$tm.vx.state().tmVuetify.black;
-            this.$tm.theme.setBlack(!isBlackTheme);
-            uni.setNavigationBarColor({
-                backgroundColor: !isBlackTheme ? '#0a0a0a' : '#ffffff',
-                frontColor: !isBlackTheme ? '#ffffff' : '#0a0a0a'
-            });
-        },
-
-        fnToSearch() {
-            uni.navigateTo({
-                url: '/pagesA/articles/articles'
-            });
-        },
-        fnOnLogoToPage() {
-            uni.switchTab({
-                url: '/pages/tabbar/about/about'
-            })
-        }
-    }
-};
+	import tmMenubars from '@/tm-vuetify/components/tm-menubars/tm-menubars.vue';
+	import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
+	import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
+	import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
+	import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
+	import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
+	import tmGrid from '@/tm-vuetify/components/tm-grid/tm-grid.vue';
+
+	import eSwiper from '@/components/e-swiper/e-swiper.vue';
+	import NotifyDialog from "@/components/notify-dialog/notify-dialog.vue";
+	import qs from 'qs'
+
+	export default {
+		components: {
+			tmMenubars,
+			tmSkeleton,
+			tmTranslate,
+			tmFlotbutton,
+			tmIcons,
+			tmEmpty,
+			tmGrid,
+			eSwiper,
+			NotifyDialog
+		},
+		data() {
+			return {
+				loading: 'loading',
+				queryParams: {
+					size: 5,
+					page: 1,
+					sort: ['spec.pinned,desc', 'spec.publishTime,desc']
+				},
+				result: {},
+				isLoadMore: false,
+				loadMoreText: '加载中...',
+				bannerCurrent: 0,
+				bannerList: [],
+				noticeList: [],
+				articleList: [],
+				categoryList: [],
+				notify: {
+					show: false,
+					data: {}
+				},
+				navList: []
+			};
+		},
+		computed: {
+			haloConfigs() {
+				return this.$tm.vx.getters().getConfigs;
+			},
+			bloggerInfo() {
+				const blogger = this.$tm.vx.getters().getConfigs.authorConfig.blogger;
+				blogger.avatar = this.$utils.checkAvatarUrl(blogger.avatar, true);
+				return blogger;
+			},
+			appInfo() {
+				const appInfo = this.haloConfigs.appConfig.appInfo;
+				appInfo.logo = this.$utils.checkImageUrl(appInfo.logo)
+				return appInfo;
+			},
+			mockJson() {
+				return this.$tm.vx.getters().getMockJson;
+			},
+			calcAuditModeEnabled() {
+				return this.haloConfigs.auditConfig.auditModeEnabled
+			},
+			calcIsShowCategory() {
+				if (this.calcAuditModeEnabled && this.categoryList.length !== 0) {
+					return false
+				}
+				if (this.calcAuditModeEnabled) {
+					return false
+				}
+				return this.haloConfigs.pageConfig.homeConfig.useCategory
+			},
+			bannerConfig() {
+				return this.haloConfigs.pageConfig.homeConfig.bannerConfig
+			}
+		},
+		watch: {
+			haloConfigs: {
+				handler(val) {
+					if (!val) return;
+					this.fnGetNavList();
+				},
+				deep: true,
+				immediate: true,
+			}
+		},
+		onLoad() {
+			this.fnSetPageTitle();
+		},
+		created() {
+			this.fnQuery();
+		},
+		onPullDownRefresh() {
+			this.isLoadMore = false;
+			this.queryParams.page = 1;
+			this.fnQuery();
+		},
+		onReachBottom(e) {
+			if (this.calcAuditModeEnabled) {
+				uni.showToast({
+					icon: 'none',
+					title: '没有更多数据了'
+				});
+				return
+			}
+			if (this.result.hasNext) {
+				this.queryParams.page += 1;
+				this.isLoadMore = true;
+				this.fnGetArticleList();
+			} else {
+				uni.showToast({
+					icon: 'none',
+					title: '没有更多数据了'
+				});
+			}
+		},
+		methods: {
+			fnQuery() {
+				this.fnGetBanner();
+				this.fnGetArticleList();
+				this.fnGetCategoryList();
+			},
+			fnGetCategoryList() {
+				if (this.calcAuditModeEnabled) {
+					this.categoryList = this.mockJson.home.categoryList.map((item) => {
+						return {
+							metadata: {
+								name: Date.now() * Math.random(),
+							},
+							spec: {
+								displayName: item.title,
+								cover: item.cover
+							},
+							postCount: 0
+						}
+					});
+					return;
+				}
+
+				if (!this.calcIsShowCategory) {
+					return;
+				}
+
+				this.$httpApi.v2
+					.getCategoryList({
+						fieldSelector: ['spec.hideFromList=false']
+					})
+					.then(res => {
+						this.categoryList = res.items.sort((a, b) => {
+							return b.postCount - a.postCount;
+						});
+
+						setTimeout(() => {
+							this.loading = 'success';
+						}, 500);
+					})
+					.catch(err => {
+						console.error(err);
+						this.loading = 'error';
+					})
+					.finally(() => {
+						setTimeout(() => {
+							uni.hideLoading();
+							uni.stopPullDownRefresh();
+						}, 500);
+					});
+			},
+			// 获取轮播图
+			fnGetBanner() {
+				if (this.calcAuditModeEnabled) {
+					this.bannerList = this.mockJson.home.bannerList.map((item) => {
+						return {
+							mp4: '',
+							id: Date.now() * Math.random(),
+							nickname: this.haloConfigs.authorConfig.blogger.nickname,
+							avatar: this.$utils.checkAvatarUrl(this.haloConfigs.authorConfig.blogger.avatar),
+							address: '',
+							createTime: item.time,
+							title: item.title,
+							src: this.$utils.checkThumbnailUrl(item.cover),
+							image: this.$utils.checkThumbnailUrl(item.cover),
+							type: "custom",
+							content: "",
+							url: ""
+						}
+					});
+					return;
+				}
+
+
+				if (!this.bannerConfig.enabled) return;
+
+				if (this.bannerConfig.type === 'custom') {
+					this.bannerList = this.bannerConfig.list.map((item) => {
+						return {
+							mp4: '',
+							id: Date.now() * Math.random(),
+							nickname: this.haloConfigs.authorConfig.blogger.nickname,
+							avatar: this.$utils.checkAvatarUrl(this.haloConfigs.authorConfig.blogger.avatar),
+							address: '',
+							createTime: "",
+							title: item.title,
+							src: this.$utils.checkThumbnailUrl(item.cover),
+							image: this.$utils.checkThumbnailUrl(item.cover),
+							type: "custom",
+							content: item.content,
+							url: item.url
+						}
+					})
+					return;
+				}
+
+				const paramsStr = qs.stringify(this.queryParams, {
+					allowDots: true,
+					encodeValuesOnly: true,
+					skipNulls: true,
+					encode: true,
+					arrayFormat: 'repeat'
+				})
+				uni.request({
+					url: this.$baseApiUrl + '/apis/api.content.halo.run/v1alpha1/posts?' + paramsStr,
+					method: 'GET',
+					success: (res) => {
+						this.bannerList = res.data.items.map((item, index) => {
+							return {
+								mp4: '',
+								id: item.metadata.name,
+								nickname: item.owner.displayName,
+								avatar: this.$utils.checkAvatarUrl(item.owner.avatar),
+								address: '',
+								createTime: uni.$tm.dayjs(item.spec.publishTime).fromNow(),
+								title: item.spec.title,
+								src: this.$utils.checkThumbnailUrl(item.spec.cover),
+								image: this.$utils.checkThumbnailUrl(item.spec.cover),
+								type: "post",
+								content: item.status.excerpt,
+								url: ""
+							};
+						});
+					},
+					fail: (err) => {}
+				})
+
+			},
+			fnOnNotifyChange(e) {
+				this.notify.show = e;
+			},
+			fnOnBannerClick(item) {
+				if (this.calcAuditModeEnabled) {
+					return;
+				}
+				if (item.type === 'custom') {
+					if (item.content) {
+						this.notify.data = item
+						this.notify.show = true
+						return;
+					}
+					if (uni.$utils.checkIsUrl(item.url)) {
+						uni.navigateTo({
+							url: '/pagesC/website/website?data=' +
+								JSON.stringify({
+									title: item.title || "加载中...",
+									url: encodeURIComponent(item.url)
+								})
+						});
+					}
+					return;
+				}
+
+				if (item.id === '') return;
+				this.fnToArticleDetail({
+					metadata: {
+						name: item.id
+					}
+				});
+			},
+			// 文章列表
+			fnGetArticleList() {
+				if (this.calcAuditModeEnabled) {
+					this.articleList = this.mockJson.home.postList.map((item) => {
+						return {
+							metadata: {
+								name: Date.now() * Math.random(),
+							},
+							spec: {
+								pinned: false,
+								cover: item.cover,
+								title: item.title,
+								publishTime: item.time
+							},
+							status: {
+								excerpt: item.desc
+							},
+							stats: {
+								visit: 0
+							}
+						}
+					});
+					this.loading = 'success';
+					this.loadMoreText = '呜呜,没有更多数据啦~';
+					uni.hideLoading();
+					uni.stopPullDownRefresh();
+					return;
+				}
+				// 设置状态为加载中
+				if (!this.isLoadMore) {
+					this.loading = 'loading';
+				}
+				this.loadMoreText = '加载中...';
+
+				const paramsStr = qs.stringify(this.queryParams, {
+					allowDots: true,
+					encodeValuesOnly: true,
+					skipNulls: true,
+					encode: true,
+					arrayFormat: 'repeat'
+				})
+				uni.request({
+					url: this.$baseApiUrl + '/apis/api.content.halo.run/v1alpha1/posts?' + paramsStr,
+					method: 'GET',
+					success: (res) => {
+						const data = res.data;
+						this.result.hasNext = data.hasNext;
+						if (this.isLoadMore) {
+							this.articleList = this.articleList.concat(data.items);
+						} else {
+							this.articleList = data.items;
+						}
+						this.loading = 'success';
+						this.loadMoreText = data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~';
+						uni.hideLoading();
+						uni.stopPullDownRefresh();
+					},
+					fail: (err) => {
+						this.loading = 'error';
+						this.loadMoreText = '加载失败,请下拉刷新!';
+						uni.$tm.toast(err.message || '数据加载失败!');
+						uni.stopPullDownRefresh();
+					}
+				})
+			},
+			//跳转文章详情
+			fnToArticleDetail(article) {
+				if (this.calcAuditModeEnabled) {
+					return;
+				}
+				uni.navigateTo({
+					url: '/pagesA/article-detail/article-detail?name=' + article.metadata.name,
+					animationType: 'slide-in-right'
+				});
+			},
+			// 快捷导航页面跳转
+			fnToNavPage(item) {
+				switch (item.type) {
+					case 'tabbar':
+						uni.switchTab({
+							url: item.path
+						});
+						break;
+					case 'page':
+						uni.navigateTo({
+							url: item.path
+						});
+						break;
+				}
+			},
+			// 分类页面
+			fnToCategoryPage() {
+				uni.switchTab({
+					url: '/pages/tabbar/category/category'
+				});
+			},
+			// 所有的文章列表页面
+			fnToArticlesPage() {
+				uni.navigateTo({
+					url: '/pagesA/articles/articles'
+				});
+			},
+
+			// 根据slug查询分类下的文章
+			fnToCategoryBy(category) {
+				if (this.calcAuditModeEnabled) {
+					return;
+				}
+				uni.navigateTo({
+					url: `/pagesA/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`
+				});
+			},
+
+			fnChangeMode() {
+				const isBlackTheme = this.$tm.vx.state().tmVuetify.black;
+				this.$tm.theme.setBlack(!isBlackTheme);
+				uni.setNavigationBarColor({
+					backgroundColor: !isBlackTheme ? '#0a0a0a' : '#ffffff',
+					frontColor: !isBlackTheme ? '#ffffff' : '#0a0a0a'
+				});
+			},
+
+			fnToSearch() {
+				uni.navigateTo({
+					url: '/pagesA/articles/articles'
+				});
+			},
+			fnOnLogoToPage() {
+				uni.switchTab({
+					url: '/pages/tabbar/about/about'
+				})
+			},
+			fnClickNav(data) {
+				uni.navigateTo({
+					url: data.path
+				});
+			},
+			fnGetNavList() {
+				this.navList = [{
+					key: 'archives',
+					title: this.calcAuditModeEnabled ? '内容归档' : '文章归档',
+					bgColor: "rgba(3, 169, 244, 0.95)",
+					shadow: "rgba(3, 169, 244, 0.4)",
+					bgClass: 'bg-gradient-blue-accent',
+					icon: 'icon-news',
+					iconColor: '',
+					path: '/pagesA/archives/archives',
+					type: 'page',
+					show: true
+				}, {
+					key: 'vote',
+					title: '投票中心',
+					bgColor: "rgba(0, 188, 212,0.95)",
+					shadow: "rgba(0, 188, 212, 0.4)",
+					bgClass: 'bg-gradient-blue-accent',
+					icon: 'icon-box',
+					iconColor: '',
+					path: '/pagesA/votes/votes',
+					type: 'page',
+					show: this.haloConfigs.loveConfig.loveEnabled
+				}, {
+					key: 'disclaimers',
+					title: '友情链接',
+					bgColor: "rgba(0, 150, 136, 0.95)",
+					shadow: "rgba(0, 150, 136, 0.4)",
+					bgClass: 'bg-gradient-blue-accent',
+					icon: 'icon-lianjie',
+					iconColor: '',
+					path: '/pagesA/friend-links/friend-links',
+					type: 'page',
+					show: true
+				}, {
+					key: 'love',
+					title: '恋爱日记',
+					bgColor: "rgba(255,76,103, 0.95)",
+					shadow: "rgba(255,76,103, 0.4)",
+					bgClass: 'bg-gradient-blue-accent',
+					icon: 'icon-like',
+					iconColor: '',
+					path: '/pagesA/love/love',
+					type: 'page',
+					show: this.haloConfigs.loveConfig.loveEnabled
+				}, {
+					key: 'contact-blogger',
+					title: '联系博主',
+					bgColor: "rgba(255, 152, 0, 0.95)",
+					shadow: "rgba(255, 152, 0, 0.4)",
+					bgClass: 'bg-gradient-blue-accent',
+					icon: 'icon-paper-plane',
+					iconColor: '',
+					rightText: '博主主常用常用联系方式',
+					path: '/pagesA/contact/contact',
+					type: 'page',
+					show: this.haloConfigs.authorConfig.social.enabled
+				}]
+			}
+		}
+	};
 </script>
 
 <style lang="scss" scoped>
-.app-page {
-    width: 100vw;
-    min-height: 100vh;
-    display: flex;
-    flex-direction: column;
-    // background-color: #ffffff;
-
-    .logo {
-        width: 60rpx;
-        height: 60rpx;
-        box-sizing: border-box;
-    }
-
-    ::v-deep {
-        .tm-menubars .body .body_wk .left {
-            min-width: initial;
-        }
-    }
-}
-
-.loading-wrap {
-    padding: 24rpx;
-}
-
-.search-input {
-    background-color: #f5f5f5;
-    align-items: center;
-    /* #ifdef MP-WEIXIN */
-    margin-right: 24rpx;
-
-    /* #endif */
-    &_icon {
-    }
-
-    &_text {
-    }
-}
-
-.show-more {
-    width: 42rpx;
-    height: 42rpx;
-    box-sizing: border-box;
-    box-shadow: 0rpx 0rpx 24rpx rgba(0, 0, 0, 0.03);
-}
-
-.banner {
-    overflow: hidden;
-}
-
-.quick-nav {
-    background-color: #fff;
-    box-sizing: border-box;
-
-    // box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
-    .name {
-        color: var(--main-text-color);
-    }
-}
-
-.category {
-    width: 94vw;
-    display: flex;
-    height: 200rpx;
-    white-space: nowrap;
-    margin: 0 24rpx;
-
-    .content {
-        display: inline-block;
-        padding-left: 24rpx;
-
-        &:first-child {
-            padding-left: 0;
-        }
-    }
-
-    .cate-empty {
-        height: inherit;
-    }
-}
-
-.page-item {
-    &_title {
-        position: relative;
-        padding-left: 24rpx;
-        font-size: 32rpx;
-        z-index: 1;
-        color: var(--main-text-color);
-
-        &:before {
-            content: '';
-            position: absolute;
-            left: 0rpx;
-            top: 8rpx;
-            width: 8rpx;
-            height: 30rpx;
-            background-color: rgba(33, 150, 243, 1);
-            border-radius: 6rpx;
-            z-index: 0;
-        }
-    }
-}
-
-.h_row_col2 {
-    display: flex;
-    flex-wrap: wrap;
-    box-sizing: border-box;
-    padding: 0 12rpx;
-
-    .ani-item {
-        width: 50%;
-    }
-}
-</style>
+	.app-page {
+		width: 100vw;
+		min-height: 100vh;
+		display: flex;
+		flex-direction: column;
+		// background-color: #ffffff;
+
+		.logo {
+			width: 60rpx;
+			height: 60rpx;
+			box-sizing: border-box;
+		}
+
+		::v-deep {
+			.tm-menubars .body .body_wk .left {
+				min-width: initial;
+			}
+		}
+	}
+
+	.loading-wrap {
+		padding: 24rpx;
+	}
+
+	.search-input {
+		background-color: #f5f5f5;
+		align-items: center;
+		/* #ifdef MP-WEIXIN */
+		margin-right: 24rpx;
+
+		/* #endif */
+		&_icon {}
+
+		&_text {}
+	}
+
+	.show-more {
+		width: 42rpx;
+		height: 42rpx;
+		box-sizing: border-box;
+		box-shadow: 0rpx 0rpx 24rpx rgba(0, 0, 0, 0.03);
+	}
+
+	.banner {
+		overflow: hidden;
+	}
+
+	.quick-nav {
+		background-color: #fff;
+		box-sizing: border-box;
+
+		// box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
+		.name {
+			color: var(--main-text-color);
+		}
+	}
+
+	.category {
+		width: 94vw;
+		display: flex;
+		height: 200rpx;
+		white-space: nowrap;
+		margin: 0 24rpx;
+
+		.content {
+			display: inline-block;
+			padding-left: 24rpx;
+
+			&:first-child {
+				padding-left: 0;
+			}
+		}
+
+		.cate-empty {
+			height: inherit;
+		}
+	}
+
+	.page-item {
+		&_title {
+			position: relative;
+			padding-left: 24rpx;
+			font-size: 32rpx;
+			z-index: 1;
+			color: var(--main-text-color);
+
+			&:before {
+				content: '';
+				position: absolute;
+				left: 0rpx;
+				top: 8rpx;
+				width: 8rpx;
+				height: 30rpx;
+				background-color: rgba(33, 150, 243, 1);
+				border-radius: 6rpx;
+				z-index: 0;
+			}
+		}
+	}
+
+	.h_row_col2 {
+		display: flex;
+		flex-wrap: wrap;
+		box-sizing: border-box;
+		padding: 0 12rpx;
+
+		.ani-item {
+			width: 50%;
+		}
+	}
+
+	.nav-box {
+		padding: 24rpx 8rpx;
+		background-color: #ffff;
+		overflow: hidden;
+		margin-bottom: 24rpx;
+	}
+
+	.nav-list {
+		align-items: center;
+		// justify-content: space-between;
+		justify-content: space-around;
+	}
+
+	.nav-item {
+		font-size: 26rpx;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		gap: 12rpx;
+	}
+
+	.nav-item-icon {
+		padding: 24rpx;
+		// border-radius: 24rpx 32rpx 24rpx 32rpx;
+		border-radius: 24rpx;
+		// border: 2rpx solid #fff;
+	}
+
+	.nav-item-text {
+		font-size: 24rpx;
+	}
+</style>

+ 70 - 55
pagesA/articles/articles.vue

@@ -1,62 +1,70 @@
 <template>
 	<view class="app-page">
-		<!-- 顶部切换 -->
-		<view class="e-fixed shadow-1">
-			<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-tabs v-if="false" color="light-blue" :shadow="0" v-model="tab.activeIndex" :list="tab.list"
-				align="center" @change="fnOnTabChange"></tm-tabs>
-		</view>
-		<!-- 占位区域 -->
-		<view style="width: 100vw;height: 100rpx;"></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 v-if="!queryParams.keyword" icon="icon-shiliangzhinengduixiang-" label="请输入关键词搜索"></tm-empty>
-				<tm-empty v-else icon="icon-shiliangzhinengduixiang-"
-					:label="`未搜到 ${queryParams.keyword} 相关内容`"></tm-empty>
+		<PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId"
+			:error-text="uniHaloPluginAvailableError" />
+		<template v-else>
+
+			<!-- 顶部切换 -->
+			<view class="e-fixed shadow-1">
+				<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-tabs v-if="false" color="light-blue" :shadow="0" v-model="tab.activeIndex" :list="tab.list"
+					align="center" @change="fnOnTabChange"></tm-tabs>
+			</view>
+			<!-- 占位区域 -->
+			<view style="width: 100vw;height: 100rpx;"></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>
-			<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 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>
-							<text class="ml-2 text-overflow text-size-n text-weight-b"
-								style="color: #333;">{{ item.title }}</text>
-						</view>
-						<mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
-							:loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
-							:tag-style="markdownConfig.tagStyle" :content="item.description || item.content"
-							:markdown="true" :showLineNumber="true" :showLanguageName="true" :copyByLongPress="true" />
-						<view class="mt-12 flex flex-center flex-between">
-							<text style="font-size: 24rpx;color:#888">
-								最近更新时间:{{ {d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒'} | formatTime }}
-							</text>
-							<!-- <tm-tags v-if="isArticle(item)" color="blue" size="n" model="text">文章</tm-tags>
+							<mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
+								:loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
+								:tag-style="markdownConfig.tagStyle" :content="item.description || item.content"
+								:markdown="true" :showLineNumber="true" :showLanguageName="true"
+								:copyByLongPress="true" />
+							<view class="mt-12 flex flex-center flex-between">
+								<text style="font-size: 24rpx;color:#888">
+									最近更新时间:{{ {d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒'} | formatTime }}
+								</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> -->
+							</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>
-		</view>
+				</block>
+			</view>
+		</template>
 	</view>
 </template>
 
@@ -71,7 +79,10 @@
 
 	import MarkdownConfig from '@/common/markdown/markdown.config.js';
 	import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue';
+
+	import pluginAvailable from "@/common/mixins/pluginAvailable.js"
 	export default {
+		mixins: [pluginAvailable],
 		components: {
 			tmSkeleton,
 			tmSearch,
@@ -92,7 +103,7 @@
 				},
 				queryParams: {
 					keyword: "",
-					limit: 5,
+					limit: 50,
 					highlightPreTag: "",
 					highlightPostTag: ""
 				},
@@ -111,17 +122,21 @@
 				return this.haloConfigs.auditConfig.auditModeEnabled
 			},
 		},
-		onLoad() {
+		async onLoad() {
 			this.fnSetPageTitle('内容搜索');
-		},
-		created() {
+			// 检查插件
+			this.setPluginId(this.NeedPluginIds.PluginSearchWidget)
+			this.setPluginError("阿偶,检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员")
+			if (!await this.checkPluginAvailable()) return
 			if (!this.queryParams.keyword) {
 				this.loading = 'success'
 			} else {
 				this.fnGetData();
 			}
 		},
+
 		onPullDownRefresh() {
+			if (!this.uniHaloPluginAvailable) return;
 			this.fnResetSetAniWaitIndex();
 			this.fnGetData();
 		},

+ 680 - 0
pagesA/vote-detail/vote-detail.vue

@@ -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 - 0
pagesA/votes/votes.vue

@@ -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>

+ 9 - 1
tm-vuetify/components/tm-checkbox/tm-checkbox.vue

@@ -2,7 +2,7 @@
 	<view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block':'']">
 		<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" 
 				:class="[black?'bk':'','flex-shrink mr-10',
 				changValue?'ani':'',
@@ -51,6 +51,10 @@
 	 */
 	import tmIcons from "@/tm-vuetify/components/tm-icons/tm-icons.vue"
 	export default {
+		options: {
+			virtualHost: true,
+			styleIsolation: 'shared'
+		},
 		components:{tmIcons},
 		name: 'tm-checkbox',
 		model: {
@@ -117,6 +121,10 @@
 			fllowTheme:{
 				type:Boolean|String,
 				default:true
+			},
+			extendData:{
+				type:Object,
+				default:()=>({})
 			}
 		},
 		data() {

+ 4 - 0
tm-vuetify/components/tm-groupcheckbox/tm-groupcheckbox.vue

@@ -17,6 +17,10 @@
 	 * 
 	 */
 	export default {
+		options: {
+			virtualHost: true,
+			styleIsolation: 'shared'
+		},
 		name:'tm-groupcheckbox',
 		props:{
 			// 最大选择数量

+ 6 - 3
tm-vuetify/components/tm-radio/tm-radio.vue

@@ -2,7 +2,7 @@
 	<view @click="onclick" class=" tm-checkbox " :class="[dense?'':'pa-20',inline?'d-inline-block ':'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"
 				:class="[black?'bk':'','flex-shrink mr-10 ',
 				changValue?'ani':'',
@@ -119,8 +119,11 @@
 			fllowTheme:{
 				type:Boolean|String,
 				default:true
-			}
-			
+			},
+			extendData:{
+				type:Object,
+				default:()=>({})
+			},
 		},
 		data() {
 			return {

+ 34 - 0
utils/vote.js

@@ -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))
+	}
+}