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>
+215 -99
View File
@@ -32,35 +32,40 @@
pluginId: NeedPluginIds.PluginLinks, pluginId: NeedPluginIds.PluginLinks,
tips: '啊偶,功能正在维护中...', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => { callback: (isAvailable) => {
if (!isAvailable) { return } if (!isAvailable)
return
uni.pageScrollTo({ uni.pageScrollTo({
scrollTop: 0, scrollTop: 0,
duration: 0, duration: 0,
}) })
handleGetLinkGroupData() handleGetLinkGroupData()
} },
}) })
/** 小程序 tab:plugin-uni-halo */ /** 小程序 tab:plugin-uni-halo */
const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({ const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginUniHalo, pluginId: NeedPluginIds.PluginUniHalo,
tips: '啊偶,功能正在维护中...', callback: (isAvailable) => { tips: '啊偶,功能正在维护中...',
if (!isAvailable) { return } callback: (isAvailable) => {
if (!isAvailable)
return
uni.pageScrollTo({ uni.pageScrollTo({
scrollTop: 0, scrollTop: 0,
duration: 0, duration: 0,
}) })
handleGetMiniProgramLinks() handleGetMiniProgramLinks()
} },
}) })
/** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleSitePluginRefresh() { async function handleSitePluginRefresh() {
if (await checkSitePluginAvailable()) { handleGetLinkGroupData() } if (await checkSitePluginAvailable())
handleGetLinkGroupData()
} }
/** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */
async function handleMiniPluginRefresh() { async function handleMiniPluginRefresh() {
if (await checkMiniPluginAvailable()) { handleGetMiniProgramLinks() } if (await checkMiniPluginAvailable())
handleGetMiniProgramLinks()
} }
/* ---------------- tabs ---------------- */ /* ---------------- tabs ---------------- */
@@ -78,7 +83,8 @@
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab // 审核模式下小程序 tab 隐藏,强制停留在站点 tab
watch(() => appConfigStore.auditModeEnabled, (enabled) => { watch(() => appConfigStore.auditModeEnabled, (enabled) => {
if (enabled) { activeTabIndex.value = 0 } if (enabled)
activeTabIndex.value = 0
}) })
/* ==================== 站点 tab(plugin-links) ==================== */ /* ==================== 站点 tab(plugin-links) ==================== */
@@ -94,7 +100,8 @@
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
function findLinkGroupDisplayNameByGroupMetadataName(groupName?: string): string { function findLinkGroupDisplayNameByGroupMetadataName(groupName?: string): string {
if (linkGroupList.value.length === 0) { return groupName || '未分组' } if (linkGroupList.value.length === 0)
return groupName || '未分组'
const found = linkGroupList.value.find(item => item.metadata.name === groupName) const found = linkGroupList.value.find(item => item.metadata.name === groupName)
return found?.spec.displayName || groupName || '未分组' return found?.spec.displayName || groupName || '未分组'
} }
@@ -134,7 +141,8 @@
groupName: findLinkGroupDisplayNameByGroupMetadataName(item.spec.groupName), groupName: findLinkGroupDisplayNameByGroupMetadataName(item.spec.groupName),
}, },
})) }))
dataList.value = dataList.value.concat(list) // 分页加载时累加,首次/刷新时覆盖(修复重复数据)
dataList.value = isLoadMore.value ? dataList.value.concat(list) : list
await sleep(600) await sleep(600)
updateSiteLoadingStatus( updateSiteLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
@@ -151,6 +159,18 @@
} }
} }
/** 站点友链按分组聚合(无分组链接归入「未分组」,参考小程序 tab 分组展示) */
const siteGroups = computed(() => {
const map = new Map<string, ILink[]>()
for (const link of dataList.value) {
const key = link.spec.groupName || '未分组'
if (!map.has(key))
map.set(key, [])
map.get(key)!.push(link)
}
return Array.from(map.entries()).map(([groupName, links]) => ({ groupName, links }))
})
/* ---------------- 站点交互 ---------------- */ /* ---------------- 站点交互 ---------------- */
function handleOnLinkEvent(link: ILink) { function handleOnLinkEvent(link: ILink) {
detail.value = { show: true, data: link } detail.value = { show: true, data: link }
@@ -169,8 +189,36 @@
}) })
} }
function toSubmitLinkPage() { /** 站点友链申请弹窗 */
uni.navigateTo({ url: '/pages-blog/submit-link/submit-link' }) const siteApplyShow = ref(false)
/** 站点友链信息弹窗 */
const infoShow = ref(false)
function handleOpenSiteApply() {
siteApplyShow.value = true
}
function handleSiteApplyClose(data: { isSubmit: boolean, refresh: boolean }) {
siteApplyShow.value = false
}
function handleOpenInfo() {
infoShow.value = true
}
function handleInfoClose() {
infoShow.value = false
}
/** 小程序友链信息弹窗(展示小程序申请提交的信息) */
const miniInfoShow = ref(false)
function handleOpenMiniInfo() {
miniInfoShow.value = true
}
function handleMiniInfoClose() {
miniInfoShow.value = false
} }
function handleToTopPage(duration = 500) { function handleToTopPage(duration = 500) {
@@ -323,7 +371,8 @@
onReachBottom(() => { onReachBottom(() => {
if (activeTabIndex.value === 0) { if (activeTabIndex.value === 0) {
if (!sitePluginAvailable.value) { return } if (!sitePluginAvailable.value)
return
if (hasNext.value) { if (hasNext.value) {
queryParams.value.page += 1 queryParams.value.page += 1
isLoadMore.value = true isLoadMore.value = true
@@ -348,10 +397,12 @@
<wd-sticky> <wd-sticky>
<scroll-view scroll-x class="w-full whitespace-nowrap"> <scroll-view scroll-x class="w-full whitespace-nowrap">
<view class="flex gap-2 px-3 pb-1 pt-3"> <view class="flex gap-2 px-3 pb-1 pt-3">
<view v-for="(tab, index) in friendLinkTabs" :key="tab.key" <view
v-for="(tab, index) in friendLinkTabs" :key="tab.key"
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm" class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'" :class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
@click="handleOnTabChange({ index })"> @click="handleOnTabChange({ index })"
>
{{ tab.label }} {{ tab.label }}
</view> </view>
</view> </view>
@@ -360,25 +411,36 @@
<!-- ==================== 站点 tab ==================== --> <!-- ==================== 站点 tab ==================== -->
<template v-if="activeTabIndex === 0"> <template v-if="activeTabIndex === 0">
<uh-plugin-unavailable v-if="!sitePluginAvailable" :plugin-id="sitePluginId" :error-text="siteTips" <uh-plugin-unavailable
:checking="siteChecking" @on-refresh="handleSitePluginRefresh" /> v-if="!sitePluginAvailable" :plugin-id="sitePluginId" :error-text="siteTips"
:checking="siteChecking" @on-refresh="handleSitePluginRefresh"
/>
<template v-else> <template v-else>
<view v-if="siteLoadingStatus !== 'success'"> <view v-if="siteLoadingStatus !== 'success'">
<uh-data-loading :loading-status="siteLoadingStatus" empty-text="啊偶,博主还没有朋友呢~" <uh-data-loading
@refresh="handleGetData" /> :loading-status="siteLoadingStatus" empty-text="啊偶,博主还没有朋友呢~"
@refresh="handleGetData"
/>
</view> </view>
<view v-else class="content pt-4"> <view v-else class="content pt-4">
<!-- 友链列表 --> <!-- 友链分组列表(参考小程序 tab 分组展示) -->
<view class="box-border flex flex-col gap-4 px-4 pb-4"> <view class="box-border px-4 pb-4">
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName"> <view v-for="group in siteGroups" :key="group.groupName" class="group-item mb-4">
<view class="uh-global-card-glass overflow-hidden box-border flex rounded-xl p-3" <view class="mb-3 flex items-center">
@click="handleOnLinkEvent(link)"> <text class="mr-2 inline-block h-4 w-1 rounded-full bg-secondary" />
<text class="text-sm text-gray-900 font-bold">{{ group.groupName }}</text>
<text class="ml-3 text-xs text-gray-400">{{ group.links.length }}</text>
</view>
<view class="group-cards flex flex-col gap-3">
<view
v-for="link in group.links" :key="link.metadata?.name || link.spec.displayName"
class="uh-global-card-glass box-border flex overflow-hidden rounded-xl p-3"
@click="handleOnLinkEvent(link)"
>
<image class="h-16 w-16 shrink-0 rounded-lg" :src="link.spec.logo" mode="aspectFill" /> <image class="h-16 w-16 shrink-0 rounded-lg" :src="link.spec.logo" mode="aspectFill" />
<view class="overflow-hidden box-border flex flex-1 flex-col justify-center pl-4"> <view class="box-border flex flex-1 flex-col justify-center overflow-hidden pl-4">
<view class="flex items-center text-sm text-gray-900 font-bold"> <view class="flex items-center text-sm text-gray-900 font-bold">
<text
class="shrink-0 mr-3 rounded-md bg-secondary px-1.5 py-0.5 text-xs text-[#4d7c0f] font-normal">{{ link.spec.groupName || '暂未分组' }}</text>
<text class="flex-1 truncate">{{ link.spec.displayName }}</text> <text class="flex-1 truncate">{{ link.spec.displayName }}</text>
</view> </view>
<view class="mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400"> <view class="mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400">
@@ -391,43 +453,49 @@
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 悬浮按钮 --> <!-- 底部悬浮操作栏(通用组件) -->
<view class="flot-buttons fixed bottom-8 right-3 z-999 flex flex-col gap-1.5"> <uh-links-actions :actions="['apply', 'info']" @apply="handleOpenSiteApply" @info="handleOpenInfo" />
<view v-if="!haloPluginConfigs?.linksSubmitPlugin?.enabled"
class="fab-btn uh-global-card-glass h-11 w-11 flex items-center justify-center rounded-full"
@click="toSubmitLinkPage">
<wd-icon name="edit" size="20px" color="#6b7280" />
</view>
</view>
<!-- 详情弹窗 --> <!-- 详情弹窗 -->
<uh-glass-popup v-model="detail.show" position="bottom" :z-index="999" <uh-glass-popup
custom-class="rounded-xl !border"> v-model="detail.show" position="bottom" :z-index="999"
<view class="relative w-full flex items-center justify-around box-border px-4 pt-4"> custom-class="rounded-xl !border"
>
<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"> <view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">站点详情</text> <text class="text-md font-bold">站点详情</text>
</view> </view>
<view <view
class="absolute right-4 top-4 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg text-center" class="uh-global-card-glass absolute right-4 top-4 h-6 w-6 border rounded-lg text-center shadow-none"
@click="miniDetail.show = false"> @click="miniDetail.show = false"
<wd-icon name="close" size="32rpx" class="text-gray-500"></wd-icon> >
<wd-icon name="close" size="32rpx" class="text-gray-500" />
</view> </view>
</view> </view>
<scroll-view v-if="detail.data" :scroll-y="true" :show-scrollbar="false" <scroll-view
class="box-border p-4 max-h-[60vh]"> v-if="detail.data" :scroll-y="true" :show-scrollbar="false"
class="box-border max-h-[60vh] p-4"
>
<view class="flex"> <view class="flex">
<image class="h-20 w-20 shrink-0 rounded-2xl uh-global-card-glass" <image
:src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" /> class="uh-global-card-glass h-20 w-20 shrink-0 rounded-2xl"
<view class="ml-4 flex flex-1 flex-col gap-y-1.5 justify-center"> :src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill"
/>
<view class="ml-4 flex flex-1 flex-col justify-center gap-y-1.5">
<view class="text-md text-gray-900 font-bold"> <view class="text-md text-gray-900 font-bold">
{{ detail.data.spec.displayName }} {{ detail.data.spec.displayName }}
</view> </view>
<view class="flex items-center gap-x-2"> <view class="flex items-center gap-x-2">
<text <text
class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900">{{ detail.data.spec.groupName }}</text> class="uh-global-card-glass uh-shadow-xs border rounded-lg bg-secondary px-2 py-0.5 text-xs text-gray-500 text-gray-900"
>
{{ detail.data.spec.groupName }}
</text>
<text <text
class="uh-global-card-glass border uh-shadow-xs text-xs text-gray-500 rounded-lg bg-secondary px-2 py-0.5 text-gray-900"> class="uh-global-card-glass uh-shadow-xs border rounded-lg bg-secondary px-2 py-0.5 text-xs text-gray-500 text-gray-900"
>
复制地址 复制地址
</text> </text>
</view> </view>
@@ -451,11 +519,15 @@
<!-- ==================== 小程序 tab ==================== --> <!-- ==================== 小程序 tab ==================== -->
<template v-else> <template v-else>
<uh-plugin-unavailable v-if="!miniPluginAvailable" :plugin-id="miniPluginId" :error-text="miniTips" <uh-plugin-unavailable
:checking="miniChecking" @on-refresh="handleMiniPluginRefresh" /> v-if="!miniPluginAvailable" :plugin-id="miniPluginId" :error-text="miniTips"
:checking="miniChecking" @on-refresh="handleMiniPluginRefresh"
/>
<template v-else> <template v-else>
<uh-data-loading v-if="miniLoadingStatus !== 'success'" :loading-status="miniLoadingStatus" <uh-data-loading
empty-text="还没有收录的小程序呢~" @refresh="handleGetMiniProgramLinks" /> v-if="miniLoadingStatus !== 'success'" :loading-status="miniLoadingStatus"
empty-text="还没有收录的小程序呢~" @refresh="handleGetMiniProgramLinks"
/>
<view v-else class="content flex flex-1 flex-col"> <view v-else class="content flex flex-1 flex-col">
<!-- 分组列表 --> <!-- 分组列表 -->
<view class="box-border flex-1 p-3"> <view class="box-border flex-1 p-3">
@@ -463,26 +535,37 @@
<view class="mb-3 flex items-center"> <view class="mb-3 flex items-center">
<text class="mr-2 inline-block h-4 w-1 rounded-full bg-secondary" /> <text class="mr-2 inline-block h-4 w-1 rounded-full bg-secondary" />
<text <text
class="text-sm text-gray-900 font-bold">{{ group.displayName || '未分组' }}</text> class="text-sm text-gray-900 font-bold"
>
{{ group.displayName || '未分组' }}
</text>
<text class="ml-3 text-xs text-gray-400">{{ group.links.length }}</text> <text class="ml-3 text-xs text-gray-400">{{ group.links.length }}</text>
</view> </view>
<view class="group-cards flex flex-col gap-4"> <view class="group-cards flex flex-col gap-4">
<view v-for="link in group.links" :key="link.metadata?.name" <view
class="uh-global-card-glass box-border uh-shadow-xs flex items-center rounded-2xl p-3" v-for="link in group.links" :key="link.metadata?.name"
@click="handleOnMiniLinkEvent(link)"> class="uh-global-card-glass uh-shadow-xs box-border flex items-center rounded-2xl p-3"
<image class="h-16 w-16 shrink-0 rounded-lg" @click="handleOnMiniLinkEvent(link)"
:src="checkImageUrl(link.spec?.miniProgramCode)" mode="aspectFill" /> >
<image
class="h-16 w-16 shrink-0 rounded-lg"
:src="checkImageUrl(link.spec?.miniProgramCode)" mode="aspectFill"
/>
<view class="box-border flex flex-1 flex-col pl-4"> <view class="box-border flex flex-1 flex-col pl-4">
<view <view
class="overflow-hidden truncate whitespace-nowrap text-[30rpx] text-gray-900 font-bold"> class="overflow-hidden truncate whitespace-nowrap text-[30rpx] text-gray-900 font-bold"
>
{{ link.spec?.displayName }} {{ link.spec?.displayName }}
</view> </view>
<view v-if="link.spec?.authorName" <view
class="mini-author mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400"> v-if="link.spec?.authorName"
class="mini-author mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400"
>
{{ link.spec.authorName }} {{ link.spec.authorName }}
</view> </view>
<view <view
class="mini-desc mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-500"> class="mini-desc mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-500"
>
{{ link.spec?.description || '暂无简介~' }} {{ link.spec?.description || '暂无简介~' }}
</view> </view>
</view> </view>
@@ -492,37 +575,38 @@
</view> </view>
</view> </view>
<!-- 申请收录悬浮按钮 --> <!-- 底部悬浮操作栏(通用组件) -->
<view class="fixed bottom-8 right-3 z-50"> <uh-links-actions :actions="['apply', 'info']" @apply="handleOpenApply" @info="handleOpenMiniInfo" />
<view
class="box-border flex flex-col w-11 h-11 items-center justify-center rounded-full bg-gray-900"
@click="handleOpenApply">
<text class="text-xs text-white">申请</text>
</view>
</view>
</view> </view>
<!-- 小程序详情弹窗 --> <!-- 小程序详情弹窗 -->
<uh-glass-popup v-model="miniDetail.show" :z-index="999" position="bottom" <uh-glass-popup
custom-class="!rounded-xl !border"> v-model="miniDetail.show" :z-index="999" position="bottom"
<view class="relative w-full flex items-center justify-around box-border px-4 pt-4"> custom-class="!rounded-xl !border"
>
<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"> <view class="w-full flex flex-col gap-y-1">
<text class="text-md font-bold">小程序详情</text> <text class="text-md font-bold">小程序详情</text>
</view> </view>
<view <view
class="absolute right-4 top-4 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg text-center" class="uh-global-card-glass absolute right-4 top-4 h-6 w-6 border rounded-lg text-center shadow-none"
@click="miniDetail.show = false"> @click="miniDetail.show = false"
<wd-icon name="close" size="32rpx" class="text-gray-500"></wd-icon> >
<wd-icon name="close" size="32rpx" class="text-gray-500" />
</view> </view>
</view> </view>
<scroll-view v-if="miniDetail.data" :scroll-y="true" :show-scrollbar="false" <scroll-view
class="box-border p-4 max-h-[60vh]"> v-if="miniDetail.data" :scroll-y="true" :show-scrollbar="false"
class="box-border max-h-[60vh] p-4"
>
<!-- 太阳码大图(点击预览/长按保存) --> <!-- 太阳码大图(点击预览/长按保存) -->
<view class="code-area flex flex-col items-center"> <view class="code-area flex flex-col items-center">
<image class="code-img h-32 w-32 rounded-xl" <image
class="code-img h-32 w-32 rounded-xl"
:src="checkImageUrl(miniDetail.data.spec?.miniProgramCode)" mode="aspectFill" :src="checkImageUrl(miniDetail.data.spec?.miniProgramCode)" mode="aspectFill"
@click="handlePreviewMiniProgramCode(miniDetail.data)" @click="handlePreviewMiniProgramCode(miniDetail.data)"
@longpress="handleSaveMiniProgramCode(miniDetail.data)" /> @longpress="handleSaveMiniProgramCode(miniDetail.data)"
/>
<view class="code-tip mt-3 flex items-center text-xs text-gray-400"> <view class="code-tip mt-3 flex items-center text-xs text-gray-400">
<wd-icon name="picture" size="14px" color="#a8a294" /> <wd-icon name="picture" size="14px" color="#a8a294" />
<text class="ml-1">点击预览长按保存太阳码</text> <text class="ml-1">点击预览长按保存太阳码</text>
@@ -532,55 +616,80 @@
<!-- 名称与分组 --> <!-- 名称与分组 -->
<view class="mini-head mt-5 flex items-center"> <view class="mini-head mt-5 flex items-center">
<text <text
class="mini-name text-[34rpx] text-gray-900 font-bold">{{ miniDetail.data.spec?.displayName }}</text> class="mini-name text-[34rpx] text-gray-900 font-bold"
<text v-if="miniDetail.data.spec?.groupName" >
class="group-tag ml-3 rounded-md bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f]"> {{ miniDetail.data.spec?.displayName }}
</text>
<text
v-if="miniDetail.data.spec?.groupName"
class="group-tag ml-3 rounded-md bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f]"
>
{{ miniDetail.data.spec.groupName }} {{ miniDetail.data.spec.groupName }}
</text> </text>
</view> </view>
<!-- 描述 --> <!-- 描述 -->
<view v-if="miniDetail.data.spec?.description" <view
class="mini-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]"> v-if="miniDetail.data.spec?.description"
class="mini-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]"
>
{{ miniDetail.data.spec.description }} {{ miniDetail.data.spec.description }}
</view> </view>
<!-- 作者信息 --> <!-- 作者信息 -->
<view <view
v-if="miniDetail.data.spec?.authorName || miniDetail.data.spec?.avatar || miniDetail.data.spec?.website" v-if="miniDetail.data.spec?.authorName || miniDetail.data.spec?.avatar || miniDetail.data.spec?.website"
class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f6f3ee] p-4"> class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f6f3ee] p-4"
<image v-if="miniDetail.data.spec?.avatar" >
<image
v-if="miniDetail.data.spec?.avatar"
class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full" class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full"
:src="checkAvatarUrl(miniDetail.data.spec.avatar)" mode="aspectFill" /> :src="checkAvatarUrl(miniDetail.data.spec.avatar)" mode="aspectFill"
/>
<view class="author-detail ml-4 flex flex-1 flex-col"> <view class="author-detail ml-4 flex flex-1 flex-col">
<text v-if="miniDetail.data.spec?.authorName" <text
class="author-name text-[28rpx] text-gray-900 font-medium">{{ miniDetail.data.spec.authorName }}</text> v-if="miniDetail.data.spec?.authorName"
<text v-if="miniDetail.data.spec?.website" class="author-name text-[28rpx] text-gray-900 font-medium"
>
{{ miniDetail.data.spec.authorName }}
</text>
<text
v-if="miniDetail.data.spec?.website"
class="author-website mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400" class="author-website mt-1 overflow-hidden truncate whitespace-nowrap text-xs text-gray-400"
@click="handleCopyMiniProgramCode(miniDetail.data)"> @click="handleCopyMiniProgramCode(miniDetail.data)"
>
网站{{ miniDetail.data.spec.website }} 网站{{ miniDetail.data.spec.website }}
</text> </text>
</view> </view>
</view> </view>
<!-- 小程序地址 --> <!-- 小程序地址 -->
<view v-if="miniDetail.data.spec?.link"
class="mini-link mt-5 flex items-center justify-between rounded-xl bg-secondary p-4">
<view <view
class="link-text flex-1 overflow-hidden truncate whitespace-nowrap text-[26rpx] text-[#4d7c0f]"> v-if="miniDetail.data.spec?.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]"
>
{{ miniDetail.data.spec.link }} {{ miniDetail.data.spec.link }}
</view> </view>
<text class="ml-3 shrink-0 text-[26rpx] text-[#4d7c0f] font-bold" <text
@click="handleCopyMiniProgramCode(miniDetail.data)">复制</text> class="ml-3 shrink-0 text-[26rpx] text-[#4d7c0f] font-bold"
@click="handleCopyMiniProgramCode(miniDetail.data)"
>
复制
</text>
</view> </view>
<!-- 预览图轮播 --> <!-- 预览图轮播 -->
<view v-if="miniDetail.data.spec?.screenshots?.length" class="mini-screenshots mt-6"> <view v-if="miniDetail.data.spec?.screenshots?.length" class="mini-screenshots mt-6">
<swiper class="screenshots-swiper h-[360rpx] w-full" indicator-dots circular> <swiper class="screenshots-swiper h-[360rpx] w-full" indicator-dots circular>
<swiper-item v-for="(img, idx) in miniDetail.data.spec.screenshots" :key="idx"> <swiper-item v-for="(img, idx) in miniDetail.data.spec.screenshots" :key="idx">
<image class="screenshot-img h-full w-full rounded-xl" :src="checkImageUrl(img)" <image
class="screenshot-img h-full w-full rounded-xl" :src="checkImageUrl(img)"
mode="aspectFill" mode="aspectFill"
@click="handlePreviewMiniProgramCode({ spec: { miniProgramCode: img } } as IMiniProgramLink)" /> @click="handlePreviewMiniProgramCode({ spec: { miniProgramCode: img } } as IMiniProgramLink)"
/>
</swiper-item> </swiper-item>
</swiper> </swiper>
</view> </view>
@@ -591,5 +700,12 @@
<uh-links-mini-apply :show="applyShow" @on-close="handleApplyClose" /> <uh-links-mini-apply :show="applyShow" @on-close="handleApplyClose" />
</template> </template>
</template> </template>
<!-- 站点友链申请弹窗 -->
<uh-links-site-apply :show="siteApplyShow" @on-close="handleSiteApplyClose" />
<!-- 站点友链信息弹窗 -->
<uh-links-site-info :show="infoShow" @on-close="handleInfoClose" />
<!-- 小程序友链信息弹窗(展示小程序申请提交的信息) -->
<uh-links-mini-info :show="miniInfoShow" @on-close="handleMiniInfoClose" />
</view> </view>
</template> </template>
+20 -21
View File
@@ -6,6 +6,7 @@
import { NeedPluginIds } from '@/hooks/usePluginAvailable' import { NeedPluginIds } from '@/hooks/usePluginAvailable'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { debounce } from '@/utils/debounce' import { debounce } from '@/utils/debounce'
import { sleep } from '@/utils/common'
import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote' import { calcVoteState, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
import type { IVoteItem } from '@/api/types/uni-halo' import type { IVoteItem } from '@/api/types/uni-halo'
@@ -20,16 +21,22 @@
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
/** 依赖插件(plugin-vote,参考 gallery 对象传参模式) */
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({ const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
pluginId: NeedPluginIds.PluginVote, pluginId: NeedPluginIds.PluginVote,
tips: '检测到当前插件没有安装或者启用,无法使用投票功能哦,请联系管理员', tips: '啊偶,功能正在维护中...',
callback: (isAvailable) => {
if (!isAvailable) { return }
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
handleGetData()
}
}) })
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */ /** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() { async function handlePluginRefresh() {
if (await checkPluginAvailable()) if (await checkPluginAvailable()) { handleGetData() }
handleGetData()
} }
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
@@ -38,7 +45,6 @@
const hasNext = ref(false) const hasNext = ref(false)
const isLoadMore = ref(false) const isLoadMore = ref(false)
const loadMoreText = ref('加载中...') const loadMoreText = ref('加载中...')
/** 是否已投过滤(前端过滤,接口无此参数) */
const filterIsVoted = ref<boolean | undefined>(undefined) const filterIsVoted = ref<boolean | undefined>(undefined)
const queryParams = ref<Record<string, unknown>>({ const queryParams = ref<Record<string, unknown>>({
keyword: '', keyword: '',
@@ -124,8 +130,7 @@
function handleSelectFilter(option : IFilterOption) { function handleSelectFilter(option : IFilterOption) {
const item = filterPopup.value.item const item = filterPopup.value.item
if (!item) if (!item) { return }
return
filterValues.value[item.key] = option.value filterValues.value[item.key] = option.value
filterPopup.value.show = false filterPopup.value.show = false
@@ -169,7 +174,6 @@
return return
} }
uni.showLoading({ mask: true, title: '加载中...' })
if (!isLoadMore.value) { if (!isLoadMore.value) {
updateLoadingStatus(DataLoadingStatusEnum.Loading) updateLoadingStatus(DataLoadingStatusEnum.Loading)
} }
@@ -179,7 +183,6 @@
const res = await getVoteList({ ...queryParams.value }) const res = await getVoteList({ ...queryParams.value })
hasNext.value = res.data.hasNext || false hasNext.value = res.data.hasNext || false
// 加工列表数据(与旧项目一致):isVoted/_uh_state/_uh_type
const tempItems = res.data.items.map((item) => { const tempItems = res.data.items.map((item) => {
item.spec = item.spec || {} item.spec = item.spec || {}
item.spec.disabled = true item.spec.disabled = true
@@ -202,7 +205,7 @@
if (filterIsVoted.value !== undefined) { if (filterIsVoted.value !== undefined) {
dataList.value = dataList.value.filter(x => x.spec?.isVoted === filterIsVoted.value) dataList.value = dataList.value.filter(x => x.spec?.isVoted === filterIsVoted.value)
} }
await sleep(600)
updateLoadingStatus( updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success, dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
) )
@@ -214,10 +217,7 @@
loadMoreText.value = '加载失败,请下拉刷新!' loadMoreText.value = '加载失败,请下拉刷新!'
} }
finally { finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}, 500)
} }
} }
@@ -281,18 +281,16 @@
<template> <template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page"> <view class="app-page min-h-screen w-screen flex flex-col bg-page">
<!-- 自定义导航 -->
<uh-navbar default-title="投票中心" title-color="text-gray-900" /> <uh-navbar default-title="投票中心" title-color="text-gray-900" />
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips" <uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="handlePluginRefresh" /> :checking="checking" @on-refresh="handlePluginRefresh" />
<template v-else> <template v-else>
<!-- 顶部搜索框 -->
<view class="box-border w-screen px-3 pt-2"> <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"> <view class="uh-global-card-glass flex h-9 items-center gap-3 rounded-full px-5">
<wd-icon name="search" size="16px" /> <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" placeholder="搜索投票..." placeholder-class="text-gray-400" confirm-type="search"
@input="handleOnInput" @confirm="handleOnSearch"> @input="handleOnInput" @confirm="handleOnSearch">
<view v-if="queryParams.keyword" class="flex items-center" <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 class="box-border flex items-center justify-between mt-1 py-2 gap-x-2">
<view v-for="f in filterConfig" :key="f.key" <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="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)"> :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"> <text class="text-xs truncate">
{{ filterLabels[f.key] }} {{ filterLabels[f.key] }}
</text> </text>
<wd-icon name="arrow-down" size="10px" /> <wd-icon name="arrow-down" size="24rpx" />
</view> </view>
</view> </view>
</view> </view>
<!-- 加载/错误/空占位(状态机) --> <uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" empty-text="博主还未发布投票~" min-height="65vh" @refresh="handleGetData" /> empty-text="还没有任何投票哦~" min-height="70vh" @refresh="handleGetData" />
<view v-else class="box-border flex flex-col gap-4 p-3"> <view v-else class="box-border flex flex-col gap-4 p-3">
<block v-if="dataList.length !== 0"> <block v-if="dataList.length !== 0">
@@ -335,7 +334,7 @@
</view> </view>
<view class="flex flex-col gap-2"> <view class="flex flex-col gap-2">
<view v-for="opt in filterPopup.item.options" :key="opt.label" <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'" :class="filterValues[filterPopup.item.key] === opt.value ? 'bg-primary text-gray-900 font-bold' : 'text-gray-700'"
@click="handleSelectFilter(opt)"> @click="handleSelectFilter(opt)">
{{ opt.label }} {{ opt.label }}