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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user