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

refactor: 大量组件与页面代码优化,新增玻璃弹窗组件

1. 新增uh-glass-popup玻璃质感弹窗组件,替换原有wd-popup使用
2. 重构列表动画类名,适配微信小程序不支持:nth-child的问题
3. 优化uh-button组件,完善类型定义与点击事件绑定
4. 修复相册查看器列间距样式,移除冗余css
5. 重构评论弹窗,使用新玻璃弹窗与统一样式
6. 优化数据加载组件,新增开关控制刷新按钮,调整文案样式
7. 替换微信客服按钮为原生button并修复样式
8. 重构收藏页空状态逻辑与文案,调整最小高度
9. 优化时间轴样式,修复最后一项线条隐藏逻辑
10. 新增sleep工具函数并优化数据加载延迟
11. 调整归档页标题与布局样式,优化空状态展示
12. 新增小程序链接申请组件,替换旧版申请弹窗
This commit is contained in:
小莫唐尼
2026-09-05 02:16:39 +08:00
parent e371f1b1d6
commit 6a0fda28d4
17 changed files with 1077 additions and 1028 deletions
@@ -95,7 +95,7 @@ function handlePreview(url?: string) {
</view>
<view v-else class="photo-list box-border flex items-start p-5">
<!-- 左列 -->
<view class="photo-column box-border min-w-0 flex-1">
<view class="photo-column box-border min-w-0 flex-1 mr-[20rpx]">
<view v-for="photo in leftPhotos" :key="photo.name" class="photo-card mb-6 box-border overflow-hidden rounded-xl bg-white shadow-sm">
<image
class="photo-image w-full"
@@ -154,13 +154,3 @@ function handlePreview(url?: string) {
</view>
</wd-popup>
</template>
<style scoped>
.album-photo-viewer {
/* 布局全部由 UnoCSS 原子类实现 */
.photo-column:first-child {
margin-right: 20rpx;
}
}
</style>
+7 -5
View File
@@ -1,16 +1,18 @@
<script setup lang="ts">
import { useAttrs } from 'vue';
interface IProps {
customClass ?: string | Array<string>;
}
interface IEmits {
(e : 'click') : void;
}
const props = defineProps<IProps>();
const attrs = useAttrs();
const emit = defineEmits<IEmits>();
</script>
<template>
<view v-bind="attrs" class="bg-primary uh-shadow-xs text-black flex items-center justify-center text-sm px-4 py-1.5 rounded-lg" :class="props.customClass">
<view class="box-border bg-primary uh-shadow-xs text-black flex items-center justify-center text-sm px-4 py-1.5 rounded-lg" :class="props.customClass" @click="emit('click')">
<slot></slot>
</view>
</template>
</template>
@@ -225,9 +225,9 @@
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
captchaData.value.status = 'success'
form.value.captchaCode = undefined
if(error?.data?.captcha){
if (error?.data?.captcha) {
captchaData.value.image = error?.data?.captcha
}else{
} else {
handleGetCaptchaImage()
}
}
@@ -265,75 +265,72 @@
</script>
<template>
<wd-popup v-model="isShow" position="bottom" :z-index="100" closable custom-style="border-radius:12rpx 12rpx 0 0;"
@close="handleOnChange(false)">
<view class="uh-comment-modal box-border p-3">
<view class="title text-center text-md font-bold">
<uh-glass-popup v-model="isShow" position="bottom" custom-class="!border rounded-lt-2xl rounded-rt-2xl"
:z-index="100" @close="handleOnChange(false)">
<view class="box-border p-4">
<view
class="relative w-full flex items-center justify-around box-border px-12 text-md font-bold text-center">
{{ calcTitle }}
<view class="absolute right-0 top-1 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg"
@click="handleClose">
<wd-icon name="close" size="32rpx" class="text-gray-500"></wd-icon>
</view>
</view>
<view class="form mt-6 max-h-[70vh] overflow-y-auto">
<view class="form-item mb-5 flex items-center">
<scroll-view :scroll-y="true" class="form mt-6">
<view class="form-item mb-4 flex items-center">
<textarea v-model="form.content"
class="content-input box-border w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]"
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'" :maxlength="200"
style="height: 200rpx;" />
class="h-22 uh-global-card-glass shadow-none border box-border w-full rounded-xl p-5 text-sm"
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'" :maxlength="200" />
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的昵称</text>
<view class="form-item mb-4 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">我的昵称</text>
<input v-model="form.author"
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
placeholder="请输入您的昵称...">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的邮箱</text>
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
<view class="form-item mb-4 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 shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
placeholder="请输入您的邮箱...">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的网站</text>
<view class="form-item mb-4 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">我的网站</text>
<input v-model="form.authorUrl"
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
placeholder="请输入您的网址...">
</view>
<!-- 匿名评论验证码 -->
<view v-if="config?.security?.captcha?.anonymousCommentCaptcha"
class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">验证码</text>
<view class="captcha-row flex flex-1 items-center gap-4">
class="form-item mb-4 flex items-center">
<text class="w-[140rpx] shrink-0 text-sm text-[#666]">验证码</text>
<view class="flex flex-1 items-center gap-4">
<input v-model="form.captchaCode"
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
placeholder="请输入验证码结果">
<view class="captcha-wrapper h-[68rpx] w-[200rpx] flex shrink-0 items-center justify-center">
<text v-if="captchaData.status === 'loading'"
class="captcha-tip text-[24rpx] text-[#999]">获取中...</text>
<text v-else-if="captchaData.status === 'fail'"
class="captcha-tip text-[24rpx] text-[#f56c6c]"
@click="handleGetCaptchaImage()">请重试</text>
<image v-else :src="captchaData.image" class="captcha-img h-full w-full" mode="aspectFit"
@click="handleGetCaptchaImage()" />
<view class="h-10 w-29 flex shrink-0 items-center justify-center">
<uh-button v-if="captchaData.status === 'loading'"
class="w-full uh-global-card-glass shadow-none border py-2.5 !rounded-xl text-xs text-gray-900">获取中...</uh-button>
<uh-button v-else-if="captchaData.status === 'fail'"
class="w-full uh-global-card-glass shadow-none border py-2.5 !rounded-xl text-xs text-red-400"
@click="handleGetCaptchaImage()">请重试</uh-button>
<image v-else :src="captchaData.image" class="block rounded-xl h-full w-full"
mode="aspectFit" @click="handleGetCaptchaImage()" />
</view>
</view>
</view>
<view class="submit-btn my-6">
<wd-button type="primary" block size="medium" @click="handleHandle">
<uh-button custom-class="py-2 !rounded-xl uh-global-card-glass border" @click="handleHandle">
提交
</wd-button>
</uh-button>
</view>
</view>
</scroll-view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
.uh-comment-modal {
.content-input {
min-height: 200rpx;
}
}
</style>
</uh-glass-popup>
</template>
@@ -14,6 +14,7 @@ interface IProps {
loadingSubText?: string
errorSubText?: string
emptySubText?: string
useLoadingButton?: boolean
}
const props = withDefaults(defineProps<IProps>(), {
@@ -25,6 +26,7 @@ const props = withDefaults(defineProps<IProps>(), {
loadingSubText: '',
errorSubText: '请检查网络连接,或稍后再试',
emptySubText: '稍后再来看看吧~',
useLoadingButton: true,
})
const emit = defineEmits<{ (e: 'refresh'): void }>()
@@ -45,7 +47,7 @@ const statusScene = computed(() => {
return {
icon: '-confused',
stageClass: 'stage-empty',
mainTextClass: 'text-gray-600',
mainTextClass: 'text-gray-900',
mainText: props.emptyText,
subText: props.emptySubText,
}
@@ -66,7 +68,6 @@ const statusScene = computed(() => {
class="w-full flex flex-col items-center justify-center gap-y-4 text-sm"
:style="{ minHeight: props.minHeight }"
>
<!-- 状态舞台:光晕 + 漂浮装饰点 + 毛玻璃表情珠 -->
<view class="scene relative h-[250rpx] w-[250rpx] flex items-center justify-center" :class="statusScene.stageClass">
<view class="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" />
<view class="deco-dot dot-a absolute rounded-full" />
@@ -80,17 +81,16 @@ const statusScene = computed(() => {
<!-- 文案区 -->
<view class="flex flex-col items-center">
<view class="flex items-center justify-center text-[28rpx] font-bold" :class="statusScene.mainTextClass">
<view class="flex items-center justify-center text-sm font-bold" :class="statusScene.mainTextClass">
<text>{{ statusScene.mainText }}</text>
<!-- 加载中三点跳动 -->
<view v-if="isLoading" class="ml-1 flex items-end gap-1">
<view v-for="n in 3" :key="n" class="typing-dot bg-primary" />
<view v-for="n in 3" :key="n" class="typing-dot bg-primary" :style="{ animationDelay: `${(n - 1) * 0.15}s` }" />
</view>
</view>
<text v-if="statusScene.subText" class="mt-3 text-[24rpx] text-gray-400">
<text v-if="statusScene.subText" class="mt-3 text-xs text-gray-500">
{{ statusScene.subText }}
</text>
<uh-button class="mt-5" @click="emit('refresh')">
<uh-button v-if="props.useLoadingButton" class="mt-5" @click="emit('refresh')">
刷新试试
</uh-button>
</view>
@@ -158,20 +158,12 @@ const statusScene = computed(() => {
transform-origin: 50% 85%;
}
/* —— 加载中三点跳动 —— */
/* —— 加载中三点跳动(错峰延迟经模板 :style 注入,避开 WXSS 不支持的 :nth-child) —— */
.typing-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
animation: dot-jump 1s ease-in-out infinite;
&:nth-child(2) {
animation-delay: 0.15s;
}
&:nth-child(3) {
animation-delay: 0.3s;
}
}
/* —— keyframes —— */
@@ -0,0 +1,99 @@
<script lang="ts" setup>
/**
* 玻璃质感弹窗:对 wd-popup 的二层封装。
* wd-popup 无背景/边框配置能力(index.scss 仅暴露 --wot-popup-bg 纯色变量),
* 故通过 custom-class 落到面板根元素后,用 :deep 覆盖为 uh-global-card-glass 同款效果。
*/
interface IProps {
/** v-model:是否显示 */
modelValue : boolean
/** 弹出位置 */
position ?: 'center' | 'top' | 'right' | 'bottom' | 'left'
/** 层级 */
zIndex ?: number
/** 是否显示右上角关闭图标 */
closable ?: boolean
/** 是否显示遮罩 */
modal ?: boolean
/** 点击遮罩是否关闭 */
closeOnClickModal ?: boolean
/** 底部安全距离适配(iphone X 类机型) */
safeAreaInsetBottom ?: boolean
/** 面板圆角(内联生效,优先级最高);传 wot 自带 round 时圆角由位置自动适配 */
radius ?: string
/** 是否开启 wot 位置自适应圆角(bottom→上圆角 / center→四圆角等),与 radius 二选一 */
round ?: boolean
/** 追加到面板的内联样式,如宽度:width:640rpx; */
customStyle ?: string
customClass? :string
}
interface IEmits {
(e : 'update:modelValue', value : boolean) : void
(e : 'close') : void
(e : 'click-modal') : void
}
const props = withDefaults(defineProps<IProps>(), {
position: 'center',
zIndex: 10,
closable: false,
modal: true,
closeOnClickModal: true,
safeAreaInsetBottom: false,
radius: '',
round: false,
customStyle: '',
customClass:''
})
const emit = defineEmits<IEmits>()
const panelStyle = computed(() => {
const radiusStyle = props.radius ? `border-radius:${props.radius};` : ''
return `${radiusStyle}${props.customStyle}`
})
const panelClass = computed(() =>{
return `uh-glass-popup-panel ${props.customClass}`
})
</script>
<template>
<wd-popup
:model-value="modelValue"
:position="position"
:z-index="zIndex"
:closable="closable"
:modal="modal"
:close-on-click-modal="closeOnClickModal"
:safe-area-inset-bottom="safeAreaInsetBottom"
:round="round"
:custom-style="panelStyle"
:custom-class="panelClass"
@update:model-value="(value: boolean) => emit('update:modelValue', value)"
@close="emit('close')"
@click-modal="emit('click-modal')"
>
<slot></slot>
</wd-popup>
</template>
<style scoped lang="scss">
/* 面板根元素(.wd-popup)同挂 custom-class,双类选择器提高优先级,覆盖 wot 默认纯白底 */
:deep(.wd-popup.uh-glass-popup-panel) {
box-sizing: border-box;
background-color: rgb(255 255 255 / 85%);
border: 4rpx solid rgb(255 255 255 / 90%);
box-shadow: inset 0 1rpx 0 rgb(255 255 255 / 75%), 0 8rpx 32rpx rgb(90 105 200 / 14%);
backdrop-filter: blur(24rpx) saturate(160%);
-webkit-backdrop-filter: blur(24rpx) saturate(160%);
}
/* 低端 WebView 不支持 backdrop-filter 的兜底:提高不透明度保证可读性 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
:deep(.wd-popup.uh-glass-popup-panel) {
background-color: rgb(255 255 255 / 88%);
}
}
</style>
@@ -0,0 +1,292 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { getPluginCaptcha, submitMiniProgramLinkApplication } from '@/api/uni-halo'
import type { ICaptchaQuery, IPluginCaptcha } from '@/api/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 {
displayName : string
miniProgramCode : string
link : string
authorName : string
avatar : string
website : string
description : string
applyRemark : string
email : string
}
const form = ref<IApplyForm>({
displayName: '',
miniProgramCode: '',
link: '',
authorName: '',
avatar: '',
website: '',
description: '',
applyRemark: '',
email: '',
})
const submitting = ref(false)
// 防刷验证码(服务端 403 附新码 / 点击图片刷新)
const captchaImage = ref('')
const captchaId = ref('')
const captchaCode = ref('')
const captchaLoading = ref(false)
const captchaSrc = computed(() => {
if (!captchaImage.value)
return ''
return captchaImage.value.startsWith('data:')
? captchaImage.value
: `data:image/png;base64,${captchaImage.value}`
})
function resetCaptcha() {
captchaImage.value = ''
captchaId.value = ''
captchaCode.value = ''
}
function applyCaptcha(captcha : IPluginCaptcha) {
captchaImage.value = captcha.imageBase64
captchaId.value = captcha.id
captchaCode.value = ''
}
async function handleRefreshCaptcha() {
if (captchaLoading.value) { return }
captchaLoading.value = true
try {
const res = await getPluginCaptcha()
if (res.data)
applyCaptcha(res.data)
}
catch (e) {
console.error('获取验证码失败', e)
}
finally {
captchaLoading.value = false
}
}
function handleResetForm() {
form.value = {
displayName: '',
miniProgramCode: '',
link: '',
authorName: '',
avatar: '',
website: '',
description: '',
applyRemark: '',
email: '',
}
resetCaptcha()
}
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.displayName.trim()) {
uni.showToast({ icon: 'none', title: '请填写小程序名称' })
return false
}
if (!form.value.miniProgramCode.trim()) {
uni.showToast({ icon: 'none', title: '请填写太阳码图片地址' })
return false
}
if (form.value.miniProgramCode.trim() && !checkIsUrl(form.value.miniProgramCode.trim())) {
uni.showToast({ icon: 'none', title: '太阳码地址需为 http(s) 链接' })
return false
}
if (form.value.link.trim() && !checkIsUrl(form.value.link.trim())) {
uni.showToast({ icon: 'none', title: '小程序地址需为 http(s) 链接' })
return false
}
if (form.value.avatar.trim() && !checkIsUrl(form.value.avatar.trim())) {
uni.showToast({ icon: 'none', title: '头像地址需为 http(s) 链接' })
return false
}
if (form.value.website.trim() && !checkIsUrl(form.value.website.trim())) {
uni.showToast({ icon: 'none', title: '网站地址需为 http(s) 链接' })
return false
}
if (form.value.email.trim() && !checkIsEmail(form.value.email.trim())) {
uni.showToast({ icon: 'none', title: '请输入正确的邮箱地址' })
return false
}
return true
}
/** 提交申请 */
async function handleHandle() {
if (!validateForm()) { return }
submitting.value = true
uni.showLoading({ title: '正在提交...' })
try {
const captchaQuery : ICaptchaQuery | undefined = captchaImage.value
? { captchaId: captchaId.value, captchaCode: captchaCode.value }
: undefined
await submitMiniProgramLinkApplication({
displayName: form.value.displayName.trim(),
miniProgramCode: form.value.miniProgramCode.trim(),
link: form.value.link.trim() || undefined,
authorName: form.value.authorName.trim() || undefined,
avatar: form.value.avatar.trim() || undefined,
website: form.value.website.trim() || undefined,
description: form.value.description.trim() || undefined,
applyRemark: form.value.applyRemark.trim() || undefined,
email: form.value.email.trim() || undefined,
}, captchaQuery)
uni.showToast({ icon: 'none', title: '申请提交成功,等待审核!' })
handleClose(true)
handleResetForm()
}
catch (err) {
console.error('小程序链接申请提交失败', err)
const e = err as { code ?: number, data ?: { message ?: string, captcha ?: IPluginCaptcha } }
if (e.code === 403 && e.data?.captcha) {
// 需要/校验失败:服务端附新验证码(一次性),展示并要求重试
applyCaptcha(e.data.captcha)
uni.showToast({ icon: 'none', title: '请完成验证码后重新提交' })
}
else {
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, (newVal) => {
if (!newVal) { return }
isShow.value = true
handleResetForm()
})
</script>
<template>
<uh-glass-popup v-model="isShow" :z-index="100" position="bottom"
custom-class="!border rounded-lt-2xl rounded-rt-2xl">
<view class="mb-4 relative w-full flex items-center justify-around box-border 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="absolute right-4 top-4 w-6 h-6 uh-global-card-glass shadow-none border rounded-lg text-center"
@click="handleClose(false)">
<wd-icon name="close" size="32rpx" class="text-gray-500"></wd-icon>
</view>
</view>
<scroll-view :scroll-y="true" :show-scrollbar="false" class="box-border p-4 pt-0 max-h-[60vh]">
<view class="mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">名称 *</text>
<input v-model="form.displayName"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
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.miniProgramCode"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
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.link"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
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.authorName"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm" 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.avatar"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
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.website"
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm" placeholder="选填">
</view>
<view class="mb-5">
<text class="label mb-2 block text-sm text-[#666]">小程序描述</text>
<textarea v-model="form.description" class="w-full h-24 uh-global-card-glass box-border shadow-none border flex-1 rounded-xl p-3 text-sm"
placeholder="介绍一下这个小程序(选填)" :maxlength="200" />
</view>
<view class="mb-5">
<text class="label mb-2 block text-sm text-[#666]">申请说明</text>
<textarea v-model="form.applyRemark" class="w-full h-24 uh-global-card-glass box-border shadow-none border flex-1 rounded-xl p-3 text-sm"
placeholder="方便管理员了解申请意图(选填)" :maxlength="200" />
</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 shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
placeholder="审核结果通知(选填)">
</view>
<!-- 防刷验证码(提交 403 后展示;点击图片可刷新) -->
<view v-if="captchaSrc" class="captcha-box mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">验证码 *</text>
<image :src="captchaSrc" class="captcha-img h-[76rpx] w-[200rpx] shrink-0 rounded-lg" mode="widthFix"
@click="handleRefreshCaptcha" />
<input v-model="captchaCode"
class="input ml-3 h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-sm" placeholder="输入图中字符">
</view>
<view class="my-6">
<uh-button custom-class="py-2 !rounded-xl" @click="handleHandle">
提交申请
</uh-button>
</view>
</scroll-view>
</uh-glass-popup>
</template>
@@ -1,298 +0,0 @@
<script lang="ts" setup>
/**
* 小程序链接申请弹窗(源自 mini-program-links-design 申请入口规划)
* 公开提交到 plugin-uni-halo POST /submissions,落库为待审核(受 linkConfig.submissionEnabled
* 开关控制);2026-09-03 接入插件防刷验证码(403+附新码 → 展示验证码行携带重试,一次性)
*/
import { computed, ref, watch } from 'vue'
import { getPluginCaptcha, submitMiniProgramLinkApplication } from '@/api/uni-halo'
import type { ICaptchaQuery, IPluginCaptcha } from '@/api/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 {
displayName: string
miniProgramCode: string
link: string
authorName: string
avatar: string
website: string
description: string
applyRemark: string
email: string
}
const form = ref<IApplyForm>({
displayName: '',
miniProgramCode: '',
link: '',
authorName: '',
avatar: '',
website: '',
description: '',
applyRemark: '',
email: '',
})
const submitting = ref(false)
// 防刷验证码(服务端 403 附新码 / 点击图片刷新)
const captchaImage = ref('')
const captchaId = ref('')
const captchaCode = ref('')
const captchaLoading = ref(false)
const captchaSrc = computed(() => {
if (!captchaImage.value)
return ''
return captchaImage.value.startsWith('data:')
? captchaImage.value
: `data:image/png;base64,${captchaImage.value}`
})
function resetCaptcha() {
captchaImage.value = ''
captchaId.value = ''
captchaCode.value = ''
}
function applyCaptcha(captcha: IPluginCaptcha) {
captchaImage.value = captcha.imageBase64
captchaId.value = captcha.id
captchaCode.value = ''
}
async function handleRefreshCaptcha() {
if (captchaLoading.value)
return
captchaLoading.value = true
try {
const res = await getPluginCaptcha()
if (res.data)
applyCaptcha(res.data)
}
catch (e) {
console.error('获取验证码失败', e)
}
finally {
captchaLoading.value = false
}
}
function handleResetForm() {
form.value = {
displayName: '',
miniProgramCode: '',
link: '',
authorName: '',
avatar: '',
website: '',
description: '',
applyRemark: '',
email: '',
}
resetCaptcha()
}
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.displayName.trim()) {
uni.showToast({ icon: 'none', title: '请填写小程序名称' })
return false
}
if (!form.value.miniProgramCode.trim()) {
uni.showToast({ icon: 'none', title: '请填写太阳码图片地址' })
return false
}
if (form.value.miniProgramCode.trim() && !checkIsUrl(form.value.miniProgramCode.trim())) {
uni.showToast({ icon: 'none', title: '太阳码地址需为 http(s) 链接' })
return false
}
if (form.value.link.trim() && !checkIsUrl(form.value.link.trim())) {
uni.showToast({ icon: 'none', title: '小程序地址需为 http(s) 链接' })
return false
}
if (form.value.avatar.trim() && !checkIsUrl(form.value.avatar.trim())) {
uni.showToast({ icon: 'none', title: '头像地址需为 http(s) 链接' })
return false
}
if (form.value.website.trim() && !checkIsUrl(form.value.website.trim())) {
uni.showToast({ icon: 'none', title: '网站地址需为 http(s) 链接' })
return false
}
if (form.value.email.trim() && !checkIsEmail(form.value.email.trim())) {
uni.showToast({ icon: 'none', title: '请输入正确的邮箱地址' })
return false
}
return true
}
/** 提交申请 */
async function handleHandle() {
if (!validateForm())
return
submitting.value = true
uni.showLoading({ title: '正在提交...' })
try {
const captchaQuery: ICaptchaQuery | undefined = captchaImage.value
? { captchaId: captchaId.value, captchaCode: captchaCode.value }
: undefined
await submitMiniProgramLinkApplication({
displayName: form.value.displayName.trim(),
miniProgramCode: form.value.miniProgramCode.trim(),
link: form.value.link.trim() || undefined,
authorName: form.value.authorName.trim() || undefined,
avatar: form.value.avatar.trim() || undefined,
website: form.value.website.trim() || undefined,
description: form.value.description.trim() || undefined,
applyRemark: form.value.applyRemark.trim() || undefined,
email: form.value.email.trim() || undefined,
}, captchaQuery)
uni.showToast({ icon: 'none', title: '申请提交成功,等待审核!' })
handleClose(true)
handleResetForm()
}
catch (err) {
console.error('小程序链接申请提交失败', err)
const e = err as { code?: number, data?: { message?: string, captcha?: IPluginCaptcha } }
if (e.code === 403 && e.data?.captcha) {
// 需要/校验失败:服务端附新验证码(一次性),展示并要求重试
applyCaptcha(e.data.captcha)
uni.showToast({ icon: 'none', title: '请完成验证码后重新提交' })
}
else {
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, (newVal) => {
if (!newVal)
return
isShow.value = true
handleResetForm()
})
</script>
<template>
<wd-popup v-model="isShow" position="center" custom-style="width:640rpx;border-radius:12rpx;">
<view class="uh-mini-link-apply max-h-[80vh] overflow-y-auto p-8">
<view class="modal-title mb-1 flex items-center justify-between">
<text class="text-[32rpx] font-bold">申请收录小程序</text>
<wd-icon name="close" size="20px" color="#999" @click="handleClose(false)" />
</view>
<view class="modal-tip mb-6 text-[24rpx] text-[#999]">
提交后将在后台审核审核通过后展示在小程序列表中
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">名称 *</text>
<input v-model="form.displayName" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入小程序名称">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">太阳码 *</text>
<input v-model="form.miniProgramCode" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="小程序码图片链接(必填)">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">小程序地址</text>
<input v-model="form.link" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="跳转链接(选填)">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者昵称</text>
<input v-model="form.authorName" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="选填">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者头像</text>
<input v-model="form.avatar" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="头像图片链接(选填)">
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者网站</text>
<input v-model="form.website" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="选填">
</view>
<view class="form-item mb-5">
<text class="label mb-2 block text-[26rpx] text-[#666]">描述</text>
<textarea v-model="form.description" class="content-input w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]" placeholder="介绍一下这个小程序(选填)" :maxlength="200" />
</view>
<view class="form-item mb-5">
<text class="label mb-2 block text-[26rpx] text-[#666]">申请说明</text>
<textarea v-model="form.applyRemark" class="content-input w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]" placeholder="方便管理员了解申请意图(选填)" :maxlength="200" />
</view>
<view class="form-item mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">邮箱</text>
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="审核结果通知(选填)">
</view>
<!-- 防刷验证码(提交 403 后展示;点击图片可刷新) -->
<view v-if="captchaSrc" class="captcha-box mb-5 flex items-center">
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">验证码 *</text>
<image
:src="captchaSrc"
class="captcha-img h-[76rpx] w-[200rpx] shrink-0 rounded-lg"
mode="widthFix"
@click="handleRefreshCaptcha"
/>
<input
v-model="captchaCode"
class="input ml-3 h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
placeholder="输入图中字符"
>
</view>
<view class="submit-btn my-6">
<wd-button type="primary" block size="medium" :loading="submitting" @click="handleHandle">
提交申请
</wd-button>
</view>
</view>
</wd-popup>
</template>
<style scoped>
.uh-mini-link-apply {
.content-input {
min-height: 140rpx;
}
}
</style>
@@ -76,8 +76,8 @@
</script>
<template>
<wd-popup v-model="isShow" position="center" custom-class="rounded-xl" :z-index="9999" @close="handleClose">
<view v-if="notice" class="box-border w-[80vw] p-6">
<uh-glass-popup v-model="isShow" position="center" :z-index="9999" radius="24rpx" @close="handleClose">
<view v-if="notice" class="box-border w-[80vw] p-4">
<!-- 头部:标题 + 关闭 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
@@ -92,7 +92,7 @@
{{ notice.typeDisplayName }}
</view>
</view>
<view class="flex h-8 w-8 items-center justify-end text-gray-500" @click="handleClose">
<view class="uh-global-card-glass !bg-white/5 border flex h-6 w-6 rounded-lg items-center justify-center text-gray-500" @click="handleClose">
<wd-icon name="close" size="16px" />
</view>
</view>
@@ -122,5 +122,5 @@
</uh-button>
</view>
</view>
</wd-popup>
</uh-glass-popup>
</template>
@@ -43,10 +43,21 @@
{{props.checking?'正在刷新':'刷新试试'}}
</uh-button>
<!-- #ifdef MP-WEIXIN -->
<uh-button custom-class="bg-white py-2 !rounded-full" open-type="contact">
提交反馈
</uh-button>
<!-- 微信端客服会话只能由原生 button open-type="contact" 唤起,故此处不用 uh-button(view 实现) -->
<button
class="uh-contact-btn bg-white py-2 px-4 !rounded-full text-black text-sm leading-none flex items-center justify-center"
open-type="contact"
hover-class="none">提交反馈</button>
<!-- #endif -->
</view>
</view>
</template>
</template>
<style lang="scss">
/* #ifdef MP-WEIXIN */
/* 重置微信原生 button 默认样式(灰底、字号行高、::after 细边框),保证与 uh-button(view 实现)视觉一致 */
.uh-contact-btn::after {
border: none;
}
/* #endif */
</style>
+27 -35
View File
@@ -1,8 +1,4 @@
<script lang="ts" setup>
/**
* 归档页(源自旧项目 pagesA/archives,新建复刻)
* 按月份/年份分组展示文章时间线
*/
import { computed, ref } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPostList } from '@/api/halo'
@@ -10,6 +6,7 @@ import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { checkThumbnailUrl } from '@/utils/url'
import { sleep } from '@/utils/common'
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
import type { IPost } from '@/api/types/halo'
@@ -127,11 +124,11 @@ async function handleGetData() {
const posts = handleGetPosts(filtered)
dataList.value = handleGetShowDataList(posts)
cacheDataList.value = filtered
c
updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
loadMoreText.value = '呜呜,没有更多数据啦~'
uni.hideLoading()
loadMoreText.value = '呜呜,没有更多数据啦~'
uni.stopPullDownRefresh()
}
catch (err) {
@@ -181,7 +178,7 @@ async function handleGetData() {
dataList.value = showDataList
cacheDataList.value = res.data.items
}
await sleep(500)
updateLoadingStatus(
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
@@ -192,8 +189,7 @@ async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Error)
loadMoreText.value = '加载失败,请下拉刷新!'
}
finally {
uni.hideLoading()
finally {
uni.stopPullDownRefresh()
}
}
@@ -261,14 +257,14 @@ onReachBottom(() => {
</script>
<template>
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
<view class="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" />
<!-- 顶部 tab(吸顶玻璃胶囊 chip,同收藏页) -->
<!-- 顶部-->
<wd-sticky>
<scroll-view scroll-x class="w-full whitespace-nowrap">
<view class="flex gap-2 px-3 pb-1 pt-3">
<view class="box-border flex gap-2 px-3 pb-1 pt-3">
<view
v-for="(tab, index) in archiveTabs" :key="tab.key"
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
@@ -280,37 +276,36 @@ onReachBottom(() => {
</view>
</scroll-view>
</wd-sticky>
<!-- 加载/错误/空占位(状态机) -->
<view v-if="loadingStatus !== 'success'">
<uh-data-loading
:loading-status="loadingStatus"
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
@refresh="handleGetData"
/>
</view>
<uh-data-loading v-if="loadingStatus !== 'success'"
:loading-status="loadingStatus"
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
@refresh="handleGetData"
/>
<!-- 内容区域 -->
<block v-else>
<!-- 时间线 -->
<view class="timeline px-4 pt-3">
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
<view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center">
<!-- <view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center">
<view class="timeline-dot mt-2 h-4 w-4 rounded-full bg-secondary shadow-[0_0_0_8rpx_rgba(215,249,76,0.3)]" />
<view v-if="index !== dataList.length - 1" class="timeline-line mt-2 w-[2rpx] flex-1 bg-black/5" />
</view>
<view class="timeline-content min-w-0 flex-1 pb-10 pl-5">
<view class="time mb-5 flex items-center gap-2">
<text class="text-[32rpx] text-gray-900 font-bold">{{ item.year }}</text>
<text v-if="activeTabIndex === 0" class="text-[32rpx] text-gray-900 font-bold">{{ item.month }}</text>
<text class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> {{ item.posts.length }} {{ calcAuditModeEnabled ? '内容' : '文章' }}</text>
</view> -->
<!-- <view class="timeline-content min-w-0 flex-1 pb-10 pl-5"> -->
<view class="flex-1 pb-10">
<view class="mb-3 flex items-center gap-2">
<text class="text-md text-gray-900 font-bold">{{ item.year }}</text>
<text v-if="activeTabIndex === 0" class="text-md text-gray-900 font-bold">{{ item.month }}</text>
<text class="rounded-full bg-secondary px-2 py-1 text-xs text-gray-500 leading-none"> {{ item.posts.length }} {{ calcAuditModeEnabled ? '内容' : '文章' }}</text>
</view>
<view v-if="item.posts.length !== 0">
<view
v-for="post in item.posts"
:key="post.metadata.name"
class="post uh-global-card-glass mb-4 flex rounded-2xl p-4"
class="uh-global-card-glass mb-4 flex rounded-2xl p-4"
:class="calcCardLayout.card"
@click="handleToArticleDetail(post)"
>
@@ -323,7 +318,7 @@ onReachBottom(() => {
{{ post.status?.excerpt }}
</view>
<view class="post-info-time mt-2 text-[24rpx] text-gray-400">
发布时间{{ formatTime(post.spec.publishTime) }}
日期{{ formatTime(post.spec.publishTime) }}
</view>
</view>
</view>
@@ -337,10 +332,7 @@ onReachBottom(() => {
<view class="load-text pb-6 text-center text-[24rpx] text-gray-400">
{{ loadMoreText }}
</view>
<view class="to-top-btn uh-global-card-glass fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full" @click="handleToTopPage()">
<wd-icon name="arrow-up" size="20px" color="#6b7280" />
</view>
</view>
</block>
</view>
</template>
+8 -5
View File
@@ -5,6 +5,7 @@
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
import type { IDataStatistics } from '@/api/uni-halo'
import { sleep } from '@/utils/common'
definePage({
style: {
@@ -22,8 +23,9 @@
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
async function handlePluginRefresh() {
if (await checkPluginAvailable())
if (await checkPluginAvailable()) {
handleGetData()
}
}
/* ---------------- 状态 ---------------- */
@@ -119,10 +121,10 @@
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
uni.showLoading({ mask: true, title: '加载中...' })
updateLoadingStatus(DataLoadingStatusEnum.Loading)
try {
const res = await getChartData()
await sleep(500)
statistics.value = res.data
handleTagChart()
handleCategoriesChart()
@@ -140,7 +142,6 @@
}
finally {
setTimeout(() => {
uni.hideLoading()
uni.stopPullDownRefresh()
}, 100)
}
@@ -174,8 +175,10 @@
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
:checking="checking" @on-refresh="handlePluginRefresh" />
<template v-else>
<uh-data-loading v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" empty-text="暂无统计数据" @refresh="handleGetData" />
<uh-data-loading v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" empty-text="暂无统计数据"
min-height="75vh" @refresh="handleGetData" />
<!-- 内容区域 -->
<view v-else class="content flex flex-col gap-3">
@@ -270,4 +273,4 @@
</view>
</template>
</view>
</template>
</template>
+3 -9
View File
@@ -39,12 +39,7 @@ watchEffect(() => {
currentItems.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
)
})
/** uh-data-loading「刷新试试」:空收藏无可刷新来源,引导回首页发现可收藏内容 */
function handleExplore() {
uni.switchTab({ url: '/pages/tabbar/home/home' })
}
/* ---------------- 收藏时间 ---------------- */
function formatCollectTime(time: string): string {
return formatTime({ d: time, f: 'yyyy-MM-dd' })
@@ -98,9 +93,8 @@ const emptyText = computed(() => (activeKind.value === 'post' ? '还没有收藏
<view class="flex flex-col gap-3 px-3 pt-2">
<!-- 空态(当前 Tab 无收藏):uh-data-loading 统一渲染,视觉与 tabbar 页一致 -->
<uh-data-loading
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="55vh"
:empty-text="emptyText" empty-sub-text="在文章或瞬间的详情页点亮星标,内容会出现在这里"
@refresh="handleExplore"
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="65vh"
:use-loading-button="false" :empty-text="emptyText" empty-sub-text="快去阅读文章/瞬间点击收藏吧"
/>
<!-- 成功态:当前 Tab 列表 -->
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -166,7 +166,7 @@ onPullDownRefresh(() => {
</view>
<!-- 时间轴 -->
<view v-else class="timeline relative pl-10">
<view v-for="(story, index) in stories" :key="String((story as unknown as { name?: string })?.name ?? index)" class="timeline-item relative pb-10" @click="handleOnStoryClick(story)">
<view v-for="(story, index) in stories" :key="String((story as unknown as { name?: string })?.name ?? index)" class="timeline-item relative pb-10" :class="index === stories.length - 1 ? 'timeline-item-last' : ''" @click="handleOnStoryClick(story)">
<view class="timeline-dot absolute left-[-32rpx] top-4 z-2 h-5 w-5 rounded-full" style="background-color: #f88ca2; box-shadow: 0 0 0 6rpx rgb(248 140 162 / 20%);" />
<view class="timeline-card rounded-xl bg-white p-6 shadow-sm">
<view v-if="(story as unknown as { spec?: { date?: string } }).spec?.date" class="timeline-date mb-2 text-[24rpx] text-[#f88ca2]">
@@ -273,7 +273,7 @@ onPullDownRefresh(() => {
background-color: rgb(248 140 162 / 40%);
}
&:last-child::before {
&.timeline-item-last::before {
display: none;
}
}
+11 -13
View File
@@ -310,21 +310,19 @@ onBeforeUnmount(() => {
}
}
/* 列表项漂浮动画(无法用 UnoCSS 表达) */
.list-item {
&:nth-child(1) {
animation: listItemAni1 3s ease-in-out infinite;
}
/* 列表项漂浮动画(无法用 UnoCSS 表达;模板已生成 list-item-N 类,避开 WXSS 不支持的 :nth-child) */
.list-item-1 {
animation: listItemAni1 3s ease-in-out infinite;
}
&:nth-child(2) {
animation: listItemAni1 3s ease-in-out infinite;
animation-delay: 1.5s;
}
.list-item-2 {
animation: listItemAni1 3s ease-in-out infinite;
animation-delay: 1.5s;
}
&:nth-child(3) {
animation: listItemAni1 3s ease-in-out infinite;
animation-delay: 2s;
}
.list-item-3 {
animation: listItemAni1 3s ease-in-out infinite;
animation-delay: 2s;
}
@keyframes likeani {
+14 -25
View File
@@ -16,14 +16,14 @@
type FromReason = 'plugin' | 'maintenance'
/** 默认维护标题(拦截场景未配置维护信息时展示) */
const DEFAULT_MAINTENANCE_TITLE = '我们正在加油升级!'
const DEFAULT_MAINTENANCE_TITLE = '维护中'
/** 恢复检测轮询间隔(ms):插件激活/维护结束探测 */
const RECOVERY_POLL_INTERVAL = 30 * 1000
const store = useAppConfigStore()
/** 插件可用性(拦截恢复检测用,参考 gallery 对象传参模式) */
const { check: checkPluginAvailable } = usePluginAvailable({
pluginId: 'plugin-uni-halo',
pluginId: 'plugin-uni-halo',
})
const viewState = ref<ViewState>('loading')
@@ -133,8 +133,7 @@
return
}
const remaining = new Date(target).getTime() - nowMs.value
if (Number.isFinite(remaining) && remaining <= 0)
void silentCheck()
if (Number.isFinite(remaining) && remaining <= 0) { silentCheck() }
}
/**
@@ -142,8 +141,7 @@
* 拉取失败(服务器停机)保持维护页不打扰。
*/
async function silentCheck() {
if (refreshing)
return
if (refreshing) { return }
refreshing = true
try {
await store.bootstrap({ force: true })
@@ -270,8 +268,8 @@
<template>
<view class="relative min-h-screen flex flex-col justify-center bg-[#f5fae8]">
<!-- 背景 -->
<view class="absolute lef-0 top-0 w-full h-[46vh] from-[#d9f77f] via-[#e8fbaf] to-[#f5fae8] bg-gradient-to-b">
</view>
<view
class="absolute lef-0 top-0 w-full h-[46vh] from-[#d9f77f] via-[#e8fbaf] to-[#f5fae8] bg-gradient-to-b" />
<!-- 刷新 -->
<uh-data-loading v-if="viewState === 'loading'" />
@@ -279,9 +277,7 @@
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
<view v-else-if="viewState === 'error'"
class="relative z-10 flex flex-col items-center justify-center px-10 py-48 text-center">
<text class="text-[64rpx]">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="120rpx" />
</text>
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="140rpx" />
<text class="mt-6 text-md text-gray-900 font-bold">
服务暂时无法访问
</text>
@@ -290,10 +286,9 @@
</text>
<view class="mt-10">
<uh-button custom-class="uh-global-card-glass border flex-1 py-2 !px-8 !rounded-full font-semibold"
@click="load(true)">刷新试试</uh-button>
@click="load(true)">刷新试试 </uh-button>
</view>
</view>
<!-- 维护页-->
<view v-else class="relative z-10 w-full min-h-screen flex items-center justify-center flex-col ">
<view
@@ -309,8 +304,8 @@
<view
class="bob flex items-center justify-center absolute inset-0 rounded-full from-[#ebfabf] to-[#b8ec3f] bg-gradient-to-br shadow-[0_0_0_12rpx_#fff,0_28rpx_60rpx_rgba(98,124,44,0.22)]">
<image v-if="appLogo" class="h-full w-full rounded-full" :src="appLogo" mode="aspectFill" />
<wd-icon v-else class="flex items-center justify-center text-gray-900" name="tool"
size="120rpx" />
<wd-icon v-else class-prefix="uhemoji-icon" name="-thinking" size="140rpx"
class="text-gray-900" />
</view>
<view
class="gear-spin absolute right-[-28rpx] top-[-16rpx] h-[68rpx] w-[68rpx] flex items-center justify-center">
@@ -331,10 +326,8 @@
</view>
<view
class="relative z-2 mx-auto mt-[8rpx] h-[8rpx] w-[224rpx] rounded-full from-transparent via-[#a7e93b] to-transparent bg-gradient-to-r" />
<view
class="relative z-2 mt-[20rpx] px-[16rpx] text-[25rpx] text-[rgba(23,24,26,0.55)] font-medium leading-[1.7]">
<template v-if="noticeLines.length > 0">
<view class="relative z-2 mt-6 px-[16rpx] text-[25rpx] text-balck/50 font-medium leading-[1.7]">
<template v-if="fromReason!=='plugin' && noticeLines.length > 0">
<text v-for="(line, index) in noticeLines" :key="index" class="block">{{ line }}</text>
</template>
<template v-else>
@@ -352,7 +345,6 @@
class="pointer-events-none absolute left-[64rpx] top-[-52rpx] z-0 h-[192rpx] w-[192rpx] rounded-full bg-[#ebfabf] opacity-90 blur-[44rpx]" />
<view
class="pointer-events-none absolute right-[-52rpx] top-[300rpx] z-0 h-[168rpx] w-[168rpx] rounded-full bg-[#ffd53d] opacity-20 blur-[44rpx]" />
<view
class="relative box-border w-full z-1 flex flex-col items-center gap-[28rpx] px-6 pb-[68rpx] pt-[32rpx]">
<!-- 恢复倒计时 -->
@@ -404,17 +396,16 @@
<!-- 操作 -->
<view class="mt-2 w-full flex flex-col gap-4">
<uh-button custom-class="uh-global-card-glass border flex-1 py-3 !rounded-full font-semibold"
@click="handleRefresh">刷新看看</uh-button>
@click="handleRefresh">{{ spinning?'请稍等...':'刷新试试'}}</uh-button>
<view v-if="detailHtml"
class="uh-global-card-glass py-2 flex flex-1 items-center justify-center rounded-full bg-white text-sm text-primary font-extrabold uh-shadow-xs"
@click="showDetail = true">
维护详情
</view>
</view>
<!-- 页脚 -->
<view class="mt-[12rpx] flex flex-col items-center">
<view class="mt-[18rpx] text-center text-[21rpx] text-[#c9cdd4] font-medium leading-[1.7]">
<view class="mt-[18rpx] text-center text-[21rpx] text-black/50 font-medium leading-[1.7]">
升级期间给你带来不便非常抱歉
<text class="block">
去喝杯奶茶等等吧
@@ -424,13 +415,11 @@
</view>
</view>
<!-- Toast -->
<view v-if="toast"
class="uh-global-card-glass border fixed bottom-[calc(72rpx+env(safe-area-inset-bottom))] left-1/2 z-60 whitespace-nowrap rounded-full px-6 py-2 text-xs font-bold -translate-x-1/2">
{{ toast }}
</view>
<!-- 维护详情弹窗(复用封装组件, components/uh-maintenance-detail) -->
<uh-maintenance-detail v-model="showDetail" :content="detailHtml" />
</view>
</view>
+4 -4
View File
@@ -336,9 +336,9 @@
class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;" />
</view>
<!-- 站点统计(上浮玻璃卡,与头部衔接) -->
<view class="uh-global-card-glass relative z-100 mx-4 flex border rounded-2xl -mt-12">
<view v-for="item in allStats" :key="item.key" class="flex-1 py-6 text-center">
<!-- 站点统计 -->
<view class="uh-global-card-glass uh-shadow-xs relative z-100 mx-4 flex border rounded-2xl -mt-12">
<view v-for="item in allStats" :key="item.key" class="flex-1 py-4 text-center">
<wd-count-to
:key="`${item.key}-${item.value}`" :start-val="0" :end-val="item.value"
:duration="900" separator="" color="#111827" custom-class="text-lg font-bold"
@@ -349,7 +349,7 @@
</view>
</view>
<!-- 功能导航(分组玻璃卡) -->
<!-- 功能导航-->
<template v-for="group in calcNavGroups" :key="group.key">
<uh-section-title class="mx-4 mb-3 mt-8">
{{ group.title }}