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

feat: 新增友链相关组件与优化投票页面

1. 新增友链底部悬浮操作组件uh-links-actions
2. 新增站点友链信息弹窗组件uh-links-site-info
3. 新增站点友链申请表单组件uh-links-site-apply
4. 新增小程序友链信息弹窗组件uh-links-mini-info
5. 优化投票页面:替换提示文案、添加插件检测回调、新增防抖sleep工具、调整样式与交互细节
This commit is contained in:
小莫唐尼
2026-09-09 02:18:10 +08:00
parent bcb91bf58f
commit 9cd21be138
6 changed files with 1286 additions and 570 deletions
@@ -0,0 +1,41 @@
<script lang="ts" setup>
/**
* 友链底部悬浮操作栏(通用组件)
* 参考瞬间详情页底部悬浮按钮设计;通过 :actions 控制显示的按钮,事件回调 emit('apply') / emit('info')
* 站点 tab / 小程序 tab 分别引入(如 :actions="['apply','info']" @apply=... @info=...)
*/
withDefaults(defineProps<{
/** 需要显示的按钮:apply=提交申请,info=友链信息;包含即显示 */
actions?: string[]
}>(), {
actions: () => ['apply', 'info'],
})
const emit = defineEmits<{
(e: 'apply'): void
(e: 'info'): void
}>()
</script>
<template>
<view class="fixed bottom-8 left-1/2 z-10 flex items-center justify-center pb-safe -translate-x-1/2">
<view class="uh-global-card-glass box-border flex items-center justify-center gap-2 border rounded-full p-1 text-primary">
<view
v-if="actions.includes('apply')"
class="uh-global-card-glass box-border h-9 flex flex-1 items-center justify-center gap-x-1 border rounded-full px-8 shadow-none"
@click="emit('apply')"
>
<wd-icon name="edit" size="36rpx" />
<text class="shrink-0 text-sm text-gray-900 font-semibold">提交申请</text>
</view>
<view
v-if="actions.includes('info')"
class="uh-global-card-glass box-border h-9 flex flex-1 items-center justify-center gap-x-1 border rounded-full px-8 shadow-none"
@click="emit('info')"
>
<wd-icon name="info" size="36rpx" />
<text class="shrink-0 text-sm text-gray-900 font-semibold">友链信息</text>
</view>
</view>
</view>
</template>
@@ -0,0 +1,208 @@
<script lang="ts" setup>
/**
* 小程序友链信息弹窗
* 展示本站小程序申请提交的信息,字段结构与小程序提交申请弹窗(uh-links-mini-apply)一致:
* 小程序名称/太阳码/跳转地址/作者昵称/作者头像/作者网站/描述/申请说明/邮箱
* 数据源:linksSubmitPlugin 配置(blogName→名称、blogLogo→太阳码、blogUrl→跳转地址、blogDesc→描述)
*/
import { computed, ref, watch } from 'vue'
import { useAppConfigStore } from '@/store/appConfig'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
const props = withDefaults(defineProps<{
show?: boolean
}>(), {
show: false,
})
const emit = defineEmits<{
(e: 'on-close'): void
}>()
const isShow = ref(false)
const appConfigStore = useAppConfigStore()
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致,从 linksSubmitPlugin 配置读取) */
const miniInfo = computed(() => {
const cfg = (appConfigStore.configs.pluginConfig?.linksSubmitPlugin || {}) as Record<string, unknown>
const str = (key: string, fallback = '') => String(cfg[key] || fallback || '')
return {
displayName: str('blogName'),
miniProgramCode: str('blogLogo'),
link: str('blogUrl'),
authorName: str('authorName'),
avatar: str('avatar'),
website: str('website'),
description: str('blogDesc'),
applyRemark: str('applyRemark'),
email: str('email'),
}
})
const hasInfo = computed(() => !!(miniInfo.value.displayName || miniInfo.value.miniProgramCode))
/** 复制跳转地址 */
function handleCopyLink() {
if (!miniInfo.value.link) {
uni.showToast({ icon: 'none', title: '暂未填写跳转地址' })
return
}
uni.setClipboardData({
data: miniInfo.value.link,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: '地址复制成功!' })
},
fail: () => {
uni.showToast({ icon: 'none', title: '复制失败!' })
},
})
}
/** 复制小程序申请信息(拼接文案) */
function handleCopyInfo() {
const info = miniInfo.value
const text = [
info.displayName ? `小程序名称:${info.displayName}` : '',
info.link ? `小程序地址:${info.link}` : '',
info.authorName ? `作者昵称:${info.authorName}` : '',
info.website ? `作者网站:${info.website}` : '',
info.description ? `小程序描述:${info.description}` : '',
info.applyRemark ? `申请说明:${info.applyRemark}` : '',
info.email ? `通知邮箱:${info.email}` : '',
].filter(Boolean).join('\n')
uni.setClipboardData({
data: text,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: '复制成功!' })
},
fail: () => {
uni.showToast({ icon: 'none', title: '复制失败!' })
},
})
}
/** 预览太阳码 */
function handlePreviewCode() {
if (!miniInfo.value.miniProgramCode)
return
uni.previewImage({
urls: [checkImageUrl(miniInfo.value.miniProgramCode)],
current: checkImageUrl(miniInfo.value.miniProgramCode),
})
}
function handleClose() {
isShow.value = false
emit('on-close')
}
watch(() => props.show, (val) => {
isShow.value = val
})
</script>
<template>
<uh-glass-popup
v-model="isShow" :z-index="100" position="bottom"
custom-class="!border rounded-xl" @close="handleClose"
>
<view class="relative box-border w-full flex items-center justify-around px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">小程序友链信息</text>
<text class="text-xs text-gray-500">本站小程序申请提交的信息,欢迎互换</text>
</view>
<view
class="uh-global-card-glass absolute right-4 top-4 h-6 w-6 border rounded-lg text-center shadow-none"
@click="handleClose"
>
<wd-icon name="close" size="32rpx" class="text-gray-500" />
</view>
</view>
<scroll-view :scroll-y="true" :show-scrollbar="false" class="box-border max-h-[60vh] p-4">
<!-- 未配置信息占位 -->
<view v-if="!hasInfo" class="py-10 text-center text-xs text-gray-400">
暂未配置小程序申请信息
</view>
<template v-else>
<!-- 太阳码大图(点击预览) -->
<view v-if="miniInfo.miniProgramCode" class="code-area flex flex-col items-center">
<image
class="code-img h-32 w-32 rounded-xl"
:src="checkImageUrl(miniInfo.miniProgramCode)" mode="aspectFill"
@click="handlePreviewCode"
/>
<view class="code-tip mt-3 flex items-center text-xs text-gray-400">
<wd-icon name="picture" size="14px" color="#a8a294" />
<text class="ml-1">点击预览太阳码</text>
</view>
</view>
<!-- 名称 -->
<view v-if="miniInfo.displayName" class="mini-head mt-5 flex items-center">
<text class="mini-name text-[34rpx] text-gray-900 font-bold">{{ miniInfo.displayName }}</text>
</view>
<!-- 描述 -->
<view v-if="miniInfo.description" class="mini-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]">
{{ miniInfo.description }}
</view>
<!-- 作者信息 -->
<view
v-if="miniInfo.authorName || miniInfo.avatar || miniInfo.website"
class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f6f3ee] p-4"
>
<image
v-if="miniInfo.avatar" class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full"
:src="checkAvatarUrl(miniInfo.avatar)" mode="aspectFill"
/>
<view class="author-detail ml-4 flex flex-1 flex-col">
<text
v-if="miniInfo.authorName"
class="author-name text-[28rpx] text-gray-900 font-medium"
>
{{ miniInfo.authorName }}
</text>
<text
v-if="miniInfo.website"
class="author-website mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400"
>
网站{{ miniInfo.website }}
</text>
</view>
</view>
<!-- 跳转地址 -->
<view v-if="miniInfo.link" class="mini-link mt-5 flex items-center justify-between rounded-xl bg-secondary p-4">
<view class="link-text flex-1 overflow-hidden truncate whitespace-nowrap text-[26rpx] text-[#4d7c0f]">
{{ miniInfo.link }}
</view>
<text class="ml-3 shrink-0 text-[26rpx] text-[#4d7c0f] font-bold" @click="handleCopyLink">复制</text>
</view>
<!-- 申请说明 / 邮箱 -->
<view
v-if="miniInfo.applyRemark || miniInfo.email"
class="mini-extra mt-5 flex flex-col gap-2 rounded-xl bg-[#f6f3ee] p-4 text-xs text-gray-500"
>
<view v-if="miniInfo.applyRemark">
<text class="text-gray-400">申请说明</text>{{ miniInfo.applyRemark }}
</view>
<view v-if="miniInfo.email">
<text class="text-gray-400">通知邮箱</text>{{ miniInfo.email }}
</view>
</view>
<view class="my-6">
<uh-button custom-class="py-2 !rounded-xl" @click="handleCopyInfo">
复制小程序申请信息
</uh-button>
</view>
</template>
</scroll-view>
</uh-glass-popup>
</template>
@@ -0,0 +1,231 @@
<script lang="ts" setup>
/**
* 友链申请表单弹窗(站点版,源自旧页面 pages-blog/submit-link)
* 参考小程序申请弹窗 uh-links-mini-apply 的封装方式(uh-glass-popup bottom + 表单 + 提交)
* 提交后等待站长审核,通过后展示在「站点」列表中
*/
import { ref, watch } from 'vue'
import { submitLink } from '@/api/uni-halo'
import type { ISubmitLinkForm } from '@/api/types/uni-halo'
const props = withDefaults(defineProps<{
show?: boolean
}>(), {
show: false,
})
const emit = defineEmits<{
(e: 'on-close', data: { isSubmit: boolean, refresh: boolean }): void
}>()
const isShow = ref(false)
interface IApplyForm {
name: string
url: string
logo: string
linkPageUrl: string
email: string
rssUrl: string
description: string
}
const form = ref<IApplyForm>({
name: '',
url: '',
logo: '',
linkPageUrl: '',
email: '',
rssUrl: '',
description: '',
})
const submitting = ref(false)
function handleResetForm() {
form.value = {
name: '',
url: '',
logo: '',
linkPageUrl: '',
email: '',
rssUrl: '',
description: '',
}
}
function checkIsUrl(url: string): boolean {
return /^https?:\/\//i.test(url)
}
function checkIsEmail(email: string): boolean {
return /^[\w.-]+@[\w-]+(?:\.[\w-]+)+$/.test(email)
}
/** 提交校验 */
function validateForm(): boolean {
if (!form.value.name.trim()) {
uni.showToast({ icon: 'none', title: '请输入网站名称!' })
return false
}
if (!checkIsUrl(form.value.url)) {
uni.showToast({ icon: 'none', title: '请输入正确的网站地址!' })
return false
}
if (form.value.logo.trim() && !checkIsUrl(form.value.logo.trim())) {
uni.showToast({ icon: 'none', title: '请输入正确的Logo地址!' })
return false
}
if (form.value.email.trim() && !checkIsEmail(form.value.email.trim())) {
uni.showToast({ icon: 'none', title: '请输入正确的邮箱地址!' })
return false
}
return true
}
/** 提交申请 */
async function handleSubmit() {
if (!validateForm())
return
submitting.value = true
uni.showLoading({ title: '正在提交...' })
try {
const payload: ISubmitLinkForm = {
name: form.value.name.trim(),
url: form.value.url.trim(),
logo: form.value.logo.trim() || undefined,
linkPageUrl: form.value.linkPageUrl.trim() || undefined,
email: form.value.email.trim() || undefined,
rssUrl: form.value.rssUrl.trim() || undefined,
description: form.value.description.trim() || undefined,
}
const res = await submitLink(payload)
const msg = res.data?.msg || res.data?.message || res.message || '提交成功'
uni.showToast({ icon: 'none', title: msg })
if (res.code === 200 || res.code === undefined) {
handleClose(true)
handleResetForm()
}
}
catch (err) {
console.error('友链申请提交失败', err)
uni.showToast({ icon: 'none', title: '提交失败,请稍后重试!' })
}
finally {
submitting.value = false
uni.hideLoading()
}
}
function handleOnChange(isOpen: boolean) {
isShow.value = isOpen
if (!isOpen)
emit('on-close', { isSubmit: false, refresh: false })
}
function handleClose(refresh = false) {
isShow.value = false
emit('on-close', { isSubmit: true, refresh })
}
watch(() => props.show, (val) => {
if (!val)
return
isShow.value = true
handleResetForm()
})
</script>
<template>
<uh-glass-popup
v-model="isShow" :z-index="100" position="bottom"
custom-class="!border rounded-xl" @close="handleClose(false)"
>
<view class="relative mb-4 box-border w-full flex items-center justify-around px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">申请友链</text>
<text class="text-xs text-gray-500">提交后等待站长审核,通过后展示在站点列表中</text>
</view>
<view
class="uh-global-card-glass absolute right-4 top-4 h-6 w-6 border rounded-lg text-center shadow-none"
@click="handleClose(false)"
>
<wd-icon name="close" size="32rpx" class="text-gray-500" />
</view>
</view>
<scroll-view :scroll-y="true" :show-scrollbar="false" class="box-border max-h-[60vh] p-4 pt-0">
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">名称 *</text>
<input
v-model="form.name"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="请输入网站名称"
>
</view>
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">网址 *</text>
<input
v-model="form.url"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="请输入网站地址"
>
</view>
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">Logo</text>
<input
v-model="form.logo"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="请输入网站Logo(选填)"
>
</view>
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">邮箱</text>
<input
v-model="form.email"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="请输入邮箱(选填)"
>
</view>
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">友链页面</text>
<input
v-model="form.linkPageUrl"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="贵站友情链接页面地址(选填)"
>
</view>
<view class="mb-2 pl-[140rpx] text-xs text-gray-400 -mt-3">
即包含本站链接的页面
</view>
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">RSS地址</text>
<input
v-model="form.rssUrl"
class="uh-global-card-glass h-9 flex-1 border rounded-xl px-4 text-sm shadow-none"
placeholder="用于抓取文章(选填)"
>
</view>
<view class="mb-5">
<text class="label mb-2 block text-sm text-[#666]">网站描述</text>
<textarea
v-model="form.description"
class="uh-global-card-glass box-border h-24 w-full flex-1 border rounded-xl p-3 text-sm shadow-none"
placeholder="请输入网站描述,不超过30字符(选填)" :maxlength="30"
/>
</view>
<view class="my-6">
<uh-button custom-class="py-2 !rounded-xl" :loading="submitting" @click="handleSubmit">
提交申请
</uh-button>
</view>
</scroll-view>
</uh-glass-popup>
</template>
@@ -0,0 +1,121 @@
<script lang="ts" setup>
/**
* 站点友链信息弹窗(源自旧页面 pages-blog/submit-link 的博客详情弹窗,重设计为底部玻璃弹窗)
* 展示本站友链交换信息(博客名片 + 复制交换信息 + 站点缩略图)
* 数据源为 linksSubmitPlugin 配置(即本站申请提交的信息)
*/
import { computed, ref, watch } from 'vue'
import { useAppConfigStore } from '@/store/appConfig'
import { checkAvatarUrl } from '@/utils/url'
const props = withDefaults(defineProps<{
show?: boolean
}>(), {
show: false,
})
const emit = defineEmits<{
(e: 'on-close'): void
}>()
const isShow = ref(false)
const appConfigStore = useAppConfigStore()
const blogDetail = computed(() => (appConfigStore.configs.pluginConfig?.linksSubmitPlugin as {
blogName?: string
blogUrl?: string
blogLogo?: string
blogDesc?: string
} | undefined) || {})
/** 友链交换信息文案(复制用) */
const calcBlogContent = computed(() => `
博客名称:${blogDetail.value.blogName || ''}
博客地址:${blogDetail.value.blogUrl || ''}
博客logo${checkAvatarUrl(blogDetail.value.blogLogo)}
博客简介:${blogDetail.value.blogDesc || ''}
`)
function calcSiteThumbnail(val?: string): string {
if (!val)
return ''
const _val = val.endsWith('/') ? val : `${val}/`
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
}
function handleCopyLink() {
uni.setClipboardData({
data: calcBlogContent.value,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: '复制成功!' })
},
fail: () => {
uni.showToast({ icon: 'none', title: '复制失败!' })
},
})
}
function handleClose() {
isShow.value = false
emit('on-close')
}
watch(() => props.show, (val) => {
isShow.value = val
})
</script>
<template>
<uh-glass-popup
v-model="isShow" :z-index="100" position="bottom"
custom-class="!border rounded-xl" @close="handleClose"
>
<view class="relative box-border w-full flex items-center justify-around px-4 pt-4">
<view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">友链信息</text>
<text class="text-xs text-gray-500">本站友链交换信息,欢迎申请互换友链</text>
</view>
<view
class="uh-global-card-glass absolute right-4 top-4 h-6 w-6 border rounded-lg text-center shadow-none"
@click="handleClose"
>
<wd-icon name="close" size="32rpx" class="text-gray-500" />
</view>
</view>
<scroll-view :scroll-y="true" :show-scrollbar="false" class="box-border max-h-[60vh] p-4">
<!-- 博客名片 -->
<view class="flex items-center">
<image
class="uh-global-card-glass h-20 w-20 shrink-0 rounded-2xl"
:src="checkAvatarUrl(blogDetail.blogLogo)" mode="aspectFill"
/>
<view class="ml-4 flex flex-1 flex-col justify-center gap-y-1">
<text class="text-md text-gray-900 font-bold">
{{ blogDetail.blogName || '未命名博客' }}
</text>
<text class="text-xs text-gray-500">
{{ blogDetail.blogDesc || '这个博主很懒,没写简介~' }}
</text>
</view>
</view>
<!-- 交换信息文案 -->
<view class="mt-4 whitespace-pre-wrap text-xs text-gray-600 leading-5">
<text>{{ calcBlogContent }}</text>
</view>
<!-- 站点缩略图 -->
<image
v-if="blogDetail.blogUrl" class="mt-4 h-[320rpx] w-full rounded-xl"
:src="calcSiteThumbnail(blogDetail.blogUrl)" mode="aspectFill"
/>
<view class="my-6">
<uh-button custom-class="py-2 !rounded-xl" @click="handleCopyLink">
复制友链交换信息
</uh-button>
</view>
</scroll-view>
</uh-glass-popup>
</template>
File diff suppressed because it is too large Load Diff
+23 -24
View File
@@ -6,6 +6,7 @@
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
import { useAppConfigStore } from '@/store/appConfig'
import { debounce } from '@/utils/debounce'
import { sleep } from '@/utils/common'
import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import type { IVoteItem } from '@/api/types/uni-halo'
@@ -20,16 +21,22 @@
const appConfigStore = useAppConfigStore()
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
/** 依赖插件(plugin-vote,参考 gallery 对象传参模式) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginVote,
tips: '检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员',
tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetData()
}
})
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() {
if (await checkPluginAvailable())
handleGetData()
if (await checkPluginAvailable()) { handleGetData() }
}
/* ---------------- 状态 ---------------- */
@@ -38,7 +45,6 @@
const hasNext = ref(false)
const isLoadMore = ref(false)
const loadMoreText = ref('加载中...')
/** 是否已投过滤(前端过滤,接口无此参数) */
const filterIsVoted = ref<boolean | undefined>(undefined)
const queryParams = ref<Record<string, unknown>>({
keyword: '',
@@ -124,8 +130,7 @@
function handleSelectFilter(option : IFilterOption) {
const item = filterPopup.value.item
if (!item)
return
if (!item) { return }
filterValues.value[item.key] = option.value
filterPopup.value.show = false
@@ -169,7 +174,6 @@
return
}
uni.showLoading({ mask: true, title: '加载中...' })
if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
}
@@ -179,7 +183,6 @@
const res = await getVoteList({ ...queryParams.value })
hasNext.value = res.data.hasNext || false
// 加工列表数据(与旧项目一致):isVoted/_uh_state/_uh_type
const tempItems = res.data.items.map((item) => {
item.spec = item.spec || {}
item.spec.disabled = true
@@ -202,7 +205,7 @@
if (filterIsVoted.value !== undefined) {
dataList.value = dataList.value.filter(x => x.spec?.isVoted === filterIsVoted.value)
}
await sleep(600)
updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
@@ -214,10 +217,7 @@
loadMoreText.value = '加载失败,请下拉刷新!'
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 500)
uni.stopPullDownRefresh()
}
}
@@ -281,18 +281,16 @@
<template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 自定义导航 -->
<uh-navbar default-title="投票中心" title-color="text-gray-900" />
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="handlePluginRefresh" />
<template v-else>
<!-- 顶部搜索框 -->
<view class="box-border w-screen px-3 pt-2">
<view class="uh-global-card-glass flex h-9 items-center gap-3 rounded-full px-5">
<wd-icon name="search" size="16px" />
<input v-model="queryParams.keyword as string" class="flex-1 text-[26rpx] text-gray-900"
<input v-model="queryParams.keyword" class="flex-1 text-[26rpx] text-gray-900"
placeholder="搜索投票..." placeholder-class="text-gray-400" confirm-type="search"
@input="handleOnInput" @confirm="handleOnSearch">
<view v-if="queryParams.keyword" class="flex items-center"
@@ -304,17 +302,18 @@
<view class="box-border flex items-center justify-between mt-1 py-2 gap-x-2">
<view v-for="f in filterConfig" :key="f.key"
class="uh-global-card-glass border rounded-full box-border flex flex-1 items-center justify-center gap-1 px-2 py-1 text-gray-500"
:class="[filterValues[f.key]?'bg-secondary text-gray-900 font-bold':'bg-white/80 text-gray-600']" @click="handleOpenFilter(f)">
<text class="text-xs truncate" >
:class="[filterValues[f.key]?'bg-secondary text-gray-900 font-bold':'bg-white/80 text-gray-600']"
@click="handleOpenFilter(f)">
<text class="text-xs truncate">
{{ filterLabels[f.key] }}
</text>
<wd-icon name="arrow-down" size="10px" />
<wd-icon name="arrow-down" size="24rpx" />
</view>
</view>
</view>
<!-- 加载/错误/空占位(状态机) -->
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" empty-text="博主还未发布投票~" min-height="65vh" @refresh="handleGetData" />
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
empty-text="还没有任何投票哦~" min-height="70vh" @refresh="handleGetData" />
<view v-else class="box-border flex flex-col gap-4 p-3">
<block v-if="dataList.length !== 0">
@@ -335,7 +334,7 @@
</view>
<view class="flex flex-col gap-2">
<view v-for="opt in filterPopup.item.options" :key="opt.label"
class="uh-global-card-glass border box-border rounded-xl px-5 py-2 text-center text-sm"
class="uh-global-card-glass shadow-none border box-border rounded-xl px-5 py-2 text-center text-sm"
:class="filterValues[filterPopup.item.key] === opt.value ? 'bg-primary text-gray-900 font-bold' : 'text-gray-700'"
@click="handleSelectFilter(opt)">
{{ opt.label }}