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>
|
||||||
<view v-else class="photo-list box-border flex items-start p-5">
|
<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">
|
<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
|
<image
|
||||||
class="photo-image w-full"
|
class="photo-image w-full"
|
||||||
@@ -154,13 +154,3 @@ function handlePreview(url?: string) {
|
|||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.album-photo-viewer {
|
|
||||||
/* 布局全部由 UnoCSS 原子类实现 */
|
|
||||||
|
|
||||||
.photo-column:first-child {
|
|
||||||
margin-right: 20rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAttrs } from 'vue';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
customClass ?: string | Array<string>;
|
customClass ?: string | Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IEmits {
|
||||||
|
(e : 'click') : void;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<IProps>();
|
const props = defineProps<IProps>();
|
||||||
const attrs = useAttrs();
|
const emit = defineEmits<IEmits>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<slot></slot>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -225,9 +225,9 @@
|
|||||||
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
|
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
|
||||||
captchaData.value.status = 'success'
|
captchaData.value.status = 'success'
|
||||||
form.value.captchaCode = undefined
|
form.value.captchaCode = undefined
|
||||||
if(error?.data?.captcha){
|
if (error?.data?.captcha) {
|
||||||
captchaData.value.image = error?.data?.captcha
|
captchaData.value.image = error?.data?.captcha
|
||||||
}else{
|
} else {
|
||||||
handleGetCaptchaImage()
|
handleGetCaptchaImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,75 +265,72 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<wd-popup v-model="isShow" position="bottom" :z-index="100" closable custom-style="border-radius:12rpx 12rpx 0 0;"
|
<uh-glass-popup v-model="isShow" position="bottom" custom-class="!border rounded-lt-2xl rounded-rt-2xl"
|
||||||
@close="handleOnChange(false)">
|
:z-index="100" @close="handleOnChange(false)">
|
||||||
<view class="uh-comment-modal box-border p-3">
|
<view class="box-border p-4">
|
||||||
<view class="title text-center text-md font-bold">
|
<view
|
||||||
|
class="relative w-full flex items-center justify-around box-border px-12 text-md font-bold text-center">
|
||||||
{{ calcTitle }}
|
{{ 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>
|
||||||
|
|
||||||
<view class="form mt-6 max-h-[70vh] overflow-y-auto">
|
<scroll-view :scroll-y="true" class="form mt-6">
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-4 flex items-center">
|
||||||
<textarea v-model="form.content"
|
<textarea v-model="form.content"
|
||||||
class="content-input box-border w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]"
|
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"
|
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'" :maxlength="200" />
|
||||||
style="height: 200rpx;" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-4 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的昵称</text>
|
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">我的昵称</text>
|
||||||
<input v-model="form.author"
|
<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="请输入您的昵称...">
|
placeholder="请输入您的昵称...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-4 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的邮箱</text>
|
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">我的邮箱</text>
|
||||||
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
|
<input v-model="form.email"
|
||||||
|
class="uh-global-card-glass shadow-none border h-9 flex-1 rounded-xl px-5 text-sm"
|
||||||
placeholder="请输入您的邮箱...">
|
placeholder="请输入您的邮箱...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-4 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的网站</text>
|
<text class="label w-[140rpx] shrink-0 text-sm text-[#666]">我的网站</text>
|
||||||
<input v-model="form.authorUrl"
|
<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="请输入您的网址...">
|
placeholder="请输入您的网址...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 匿名评论验证码 -->
|
<!-- 匿名评论验证码 -->
|
||||||
<view v-if="config?.security?.captcha?.anonymousCommentCaptcha"
|
<view v-if="config?.security?.captcha?.anonymousCommentCaptcha"
|
||||||
class="form-item mb-5 flex items-center">
|
class="form-item mb-4 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">验证码</text>
|
<text class="w-[140rpx] shrink-0 text-sm text-[#666]">验证码</text>
|
||||||
<view class="captcha-row flex flex-1 items-center gap-4">
|
<view class="flex flex-1 items-center gap-4">
|
||||||
<input v-model="form.captchaCode"
|
<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="请输入验证码结果">
|
placeholder="请输入验证码结果">
|
||||||
<view class="captcha-wrapper h-[68rpx] w-[200rpx] flex shrink-0 items-center justify-center">
|
<view class="h-10 w-29 flex shrink-0 items-center justify-center">
|
||||||
<text v-if="captchaData.status === 'loading'"
|
<uh-button v-if="captchaData.status === 'loading'"
|
||||||
class="captcha-tip text-[24rpx] text-[#999]">获取中...</text>
|
class="w-full uh-global-card-glass shadow-none border py-2.5 !rounded-xl text-xs text-gray-900">获取中...</uh-button>
|
||||||
<text v-else-if="captchaData.status === 'fail'"
|
<uh-button v-else-if="captchaData.status === 'fail'"
|
||||||
class="captcha-tip text-[24rpx] text-[#f56c6c]"
|
class="w-full uh-global-card-glass shadow-none border py-2.5 !rounded-xl text-xs text-red-400"
|
||||||
@click="handleGetCaptchaImage()">请重试</text>
|
@click="handleGetCaptchaImage()">请重试</uh-button>
|
||||||
<image v-else :src="captchaData.image" class="captcha-img h-full w-full" mode="aspectFit"
|
<image v-else :src="captchaData.image" class="block rounded-xl h-full w-full"
|
||||||
@click="handleGetCaptchaImage()" />
|
mode="aspectFit" @click="handleGetCaptchaImage()" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="submit-btn my-6">
|
<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>
|
</view>
|
||||||
</view>
|
</uh-glass-popup>
|
||||||
</wd-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.uh-comment-modal {
|
|
||||||
.content-input {
|
|
||||||
min-height: 200rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -14,6 +14,7 @@ interface IProps {
|
|||||||
loadingSubText?: string
|
loadingSubText?: string
|
||||||
errorSubText?: string
|
errorSubText?: string
|
||||||
emptySubText?: string
|
emptySubText?: string
|
||||||
|
useLoadingButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<IProps>(), {
|
const props = withDefaults(defineProps<IProps>(), {
|
||||||
@@ -25,6 +26,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
|||||||
loadingSubText: '',
|
loadingSubText: '',
|
||||||
errorSubText: '请检查网络连接,或稍后再试',
|
errorSubText: '请检查网络连接,或稍后再试',
|
||||||
emptySubText: '稍后再来看看吧~',
|
emptySubText: '稍后再来看看吧~',
|
||||||
|
useLoadingButton: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'refresh'): void }>()
|
const emit = defineEmits<{ (e: 'refresh'): void }>()
|
||||||
@@ -45,7 +47,7 @@ const statusScene = computed(() => {
|
|||||||
return {
|
return {
|
||||||
icon: '-confused',
|
icon: '-confused',
|
||||||
stageClass: 'stage-empty',
|
stageClass: 'stage-empty',
|
||||||
mainTextClass: 'text-gray-600',
|
mainTextClass: 'text-gray-900',
|
||||||
mainText: props.emptyText,
|
mainText: props.emptyText,
|
||||||
subText: props.emptySubText,
|
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"
|
class="w-full flex flex-col items-center justify-center gap-y-4 text-sm"
|
||||||
:style="{ minHeight: props.minHeight }"
|
:style="{ minHeight: props.minHeight }"
|
||||||
>
|
>
|
||||||
<!-- 状态舞台:光晕 + 漂浮装饰点 + 毛玻璃表情珠 -->
|
|
||||||
<view class="scene relative h-[250rpx] w-[250rpx] flex items-center justify-center" :class="statusScene.stageClass">
|
<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="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" />
|
||||||
<view class="deco-dot dot-a absolute 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 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>
|
<text>{{ statusScene.mainText }}</text>
|
||||||
<!-- 加载中三点跳动 -->
|
|
||||||
<view v-if="isLoading" class="ml-1 flex items-end gap-1">
|
<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>
|
||||||
</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 }}
|
{{ statusScene.subText }}
|
||||||
</text>
|
</text>
|
||||||
<uh-button class="mt-5" @click="emit('refresh')">
|
<uh-button v-if="props.useLoadingButton" class="mt-5" @click="emit('refresh')">
|
||||||
刷新试试
|
刷新试试
|
||||||
</uh-button>
|
</uh-button>
|
||||||
</view>
|
</view>
|
||||||
@@ -158,20 +158,12 @@ const statusScene = computed(() => {
|
|||||||
transform-origin: 50% 85%;
|
transform-origin: 50% 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 加载中三点跳动 —— */
|
/* —— 加载中三点跳动(错峰延迟经模板 :style 注入,避开 WXSS 不支持的 :nth-child) —— */
|
||||||
.typing-dot {
|
.typing-dot {
|
||||||
width: 10rpx;
|
width: 10rpx;
|
||||||
height: 10rpx;
|
height: 10rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: dot-jump 1s ease-in-out infinite;
|
animation: dot-jump 1s ease-in-out infinite;
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
animation-delay: 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— keyframes —— */
|
/* —— 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<wd-popup v-model="isShow" position="center" custom-class="rounded-xl" :z-index="9999" @close="handleClose">
|
<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-6">
|
<view v-if="notice" class="box-border w-[80vw] p-4">
|
||||||
<!-- 头部:标题 + 关闭 -->
|
<!-- 头部:标题 + 关闭 -->
|
||||||
<view class="flex items-center justify-between">
|
<view class="flex items-center justify-between">
|
||||||
<view class="flex items-center gap-2">
|
<view class="flex items-center gap-2">
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
{{ notice.typeDisplayName }}
|
{{ notice.typeDisplayName }}
|
||||||
</view>
|
</view>
|
||||||
</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" />
|
<wd-icon name="close" size="16px" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -122,5 +122,5 @@
|
|||||||
</uh-button>
|
</uh-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</uh-glass-popup>
|
||||||
</template>
|
</template>
|
||||||
@@ -43,10 +43,21 @@
|
|||||||
{{props.checking?'正在刷新':'刷新试试'}}
|
{{props.checking?'正在刷新':'刷新试试'}}
|
||||||
</uh-button>
|
</uh-button>
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<uh-button custom-class="bg-white py-2 !rounded-full" open-type="contact">
|
<!-- 微信端客服会话只能由原生 button 的 open-type="contact" 唤起,故此处不用 uh-button(view 实现) -->
|
||||||
提交反馈
|
<button
|
||||||
</uh-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 -->
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
</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>
|
<script lang="ts" setup>
|
||||||
/**
|
|
||||||
* 归档页(源自旧项目 pagesA/archives,新建复刻)
|
|
||||||
* 按月份/年份分组展示文章时间线
|
|
||||||
*/
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
import { getPostList } from '@/api/halo'
|
import { getPostList } from '@/api/halo'
|
||||||
@@ -10,6 +6,7 @@ import { useAppConfigStore } from '@/store/appConfig'
|
|||||||
import { useSettingStore } from '@/store/setting'
|
import { useSettingStore } from '@/store/setting'
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { checkThumbnailUrl } from '@/utils/url'
|
import { checkThumbnailUrl } from '@/utils/url'
|
||||||
|
import { sleep } from '@/utils/common'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
import type { IPost } from '@/api/types/halo'
|
import type { IPost } from '@/api/types/halo'
|
||||||
|
|
||||||
@@ -127,11 +124,11 @@ async function handleGetData() {
|
|||||||
const posts = handleGetPosts(filtered)
|
const posts = handleGetPosts(filtered)
|
||||||
dataList.value = handleGetShowDataList(posts)
|
dataList.value = handleGetShowDataList(posts)
|
||||||
cacheDataList.value = filtered
|
cacheDataList.value = filtered
|
||||||
|
c
|
||||||
updateLoadingStatus(
|
updateLoadingStatus(
|
||||||
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
)
|
)
|
||||||
loadMoreText.value = '呜呜,没有更多数据啦~'
|
loadMoreText.value = '呜呜,没有更多数据啦~'
|
||||||
uni.hideLoading()
|
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -181,7 +178,7 @@ async function handleGetData() {
|
|||||||
dataList.value = showDataList
|
dataList.value = showDataList
|
||||||
cacheDataList.value = res.data.items
|
cacheDataList.value = res.data.items
|
||||||
}
|
}
|
||||||
|
await sleep(500)
|
||||||
updateLoadingStatus(
|
updateLoadingStatus(
|
||||||
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
)
|
)
|
||||||
@@ -193,7 +190,6 @@ async function handleGetData() {
|
|||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
uni.hideLoading()
|
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,14 +257,14 @@ onReachBottom(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<wd-sticky>
|
||||||
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
||||||
<view class="flex gap-2 px-3 pb-1 pt-3">
|
<view class="box-border flex gap-2 px-3 pb-1 pt-3">
|
||||||
<view
|
<view
|
||||||
v-for="(tab, index) in archiveTabs" :key="tab.key"
|
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"
|
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
|
||||||
@@ -281,36 +277,35 @@ onReachBottom(() => {
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
</wd-sticky>
|
</wd-sticky>
|
||||||
|
|
||||||
<!-- 加载/错误/空占位(状态机) -->
|
<uh-data-loading v-if="loadingStatus !== 'success'"
|
||||||
<view v-if="loadingStatus !== 'success'">
|
|
||||||
<uh-data-loading
|
|
||||||
:loading-status="loadingStatus"
|
:loading-status="loadingStatus"
|
||||||
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
|
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
|
||||||
@refresh="handleGetData"
|
@refresh="handleGetData"
|
||||||
/>
|
/>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<!-- 时间线 -->
|
<!-- 时间线 -->
|
||||||
<view class="timeline px-4 pt-3">
|
<view class="timeline px-4 pt-3">
|
||||||
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
|
<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 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 v-if="index !== dataList.length - 1" class="timeline-line mt-2 w-[2rpx] flex-1 bg-black/5" />
|
||||||
</view>
|
</view> -->
|
||||||
<view class="timeline-content min-w-0 flex-1 pb-10 pl-5">
|
<!-- <view class="timeline-content min-w-0 flex-1 pb-10 pl-5"> -->
|
||||||
<view class="time mb-5 flex items-center gap-2">
|
<view class="flex-1 pb-10">
|
||||||
<text class="text-[32rpx] text-gray-900 font-bold">{{ item.year }}年</text>
|
<view class="mb-3 flex items-center gap-2">
|
||||||
<text v-if="activeTabIndex === 0" class="text-[32rpx] text-gray-900 font-bold">{{ item.month }}月</text>
|
<text class="text-md text-gray-900 font-bold">{{ item.year }}年</text>
|
||||||
<text class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">共 {{ item.posts.length }} 篇{{ calcAuditModeEnabled ? '内容' : '文章' }}</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>
|
||||||
|
|
||||||
<view v-if="item.posts.length !== 0">
|
<view v-if="item.posts.length !== 0">
|
||||||
<view
|
<view
|
||||||
v-for="post in item.posts"
|
v-for="post in item.posts"
|
||||||
:key="post.metadata.name"
|
: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"
|
:class="calcCardLayout.card"
|
||||||
@click="handleToArticleDetail(post)"
|
@click="handleToArticleDetail(post)"
|
||||||
>
|
>
|
||||||
@@ -323,7 +318,7 @@ onReachBottom(() => {
|
|||||||
{{ post.status?.excerpt }}
|
{{ post.status?.excerpt }}
|
||||||
</view>
|
</view>
|
||||||
<view class="post-info-time mt-2 text-[24rpx] text-gray-400">
|
<view class="post-info-time mt-2 text-[24rpx] text-gray-400">
|
||||||
发布时间:{{ formatTime(post.spec.publishTime) }}
|
日期:{{ formatTime(post.spec.publishTime) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -338,9 +333,6 @@ onReachBottom(() => {
|
|||||||
<view class="load-text pb-6 text-center text-[24rpx] text-gray-400">
|
<view class="load-text pb-6 text-center text-[24rpx] text-gray-400">
|
||||||
{{ loadMoreText }}
|
{{ loadMoreText }}
|
||||||
</view>
|
</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>
|
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
||||||
import type { IDataStatistics } from '@/api/uni-halo'
|
import type { IDataStatistics } from '@/api/uni-halo'
|
||||||
|
import { sleep } from '@/utils/common'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
@@ -22,9 +23,10 @@
|
|||||||
|
|
||||||
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
|
/** 重新检测插件:可用则拉取数据(供 uh-plugin-unavailable 刷新按钮) */
|
||||||
async function handlePluginRefresh() {
|
async function handlePluginRefresh() {
|
||||||
if (await checkPluginAvailable())
|
if (await checkPluginAvailable()) {
|
||||||
handleGetData()
|
handleGetData()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
@@ -119,10 +121,10 @@
|
|||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
uni.showLoading({ mask: true, title: '加载中...' })
|
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getChartData()
|
const res = await getChartData()
|
||||||
|
await sleep(500)
|
||||||
statistics.value = res.data
|
statistics.value = res.data
|
||||||
handleTagChart()
|
handleTagChart()
|
||||||
handleCategoriesChart()
|
handleCategoriesChart()
|
||||||
@@ -140,7 +142,6 @@
|
|||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.hideLoading()
|
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
@@ -174,8 +175,10 @@
|
|||||||
|
|
||||||
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
|
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
|
||||||
:checking="checking" @on-refresh="handlePluginRefresh" />
|
:checking="checking" @on-refresh="handlePluginRefresh" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<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">
|
<view v-else class="content flex flex-col gap-3">
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ watchEffect(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** uh-data-loading「刷新试试」:空收藏无可刷新来源,引导回首页发现可收藏内容 */
|
|
||||||
function handleExplore() {
|
|
||||||
uni.switchTab({ url: '/pages/tabbar/home/home' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------- 收藏时间 ---------------- */
|
/* ---------------- 收藏时间 ---------------- */
|
||||||
function formatCollectTime(time: string): string {
|
function formatCollectTime(time: string): string {
|
||||||
return formatTime({ d: time, f: 'yyyy-MM-dd' })
|
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">
|
<view class="flex flex-col gap-3 px-3 pt-2">
|
||||||
<!-- 空态(当前 Tab 无收藏):uh-data-loading 统一渲染,视觉与 tabbar 页一致 -->
|
<!-- 空态(当前 Tab 无收藏):uh-data-loading 统一渲染,视觉与 tabbar 页一致 -->
|
||||||
<uh-data-loading
|
<uh-data-loading
|
||||||
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="55vh"
|
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="65vh"
|
||||||
:empty-text="emptyText" empty-sub-text="在文章或瞬间的详情页点亮星标,内容会出现在这里"
|
:use-loading-button="false" :empty-text="emptyText" empty-sub-text="快去阅读文章/瞬间点击收藏吧"
|
||||||
@refresh="handleExplore"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 成功态:当前 Tab 列表 -->
|
<!-- 成功态:当前 Tab 列表 -->
|
||||||
|
|||||||
@@ -1,99 +1,93 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
import { computed, ref, watch } from 'vue'
|
||||||
* 友情链接页(源自旧项目 pagesA/friend-links,新建复刻 + tabs 改造)
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
* 顶部 tabs 切换「站点 / 小程序」:
|
import { getFriendLinkGroupList, getFriendLinkList } from '@/api/halo'
|
||||||
* - 站点:plugin-links 博客友链(色彩版/简洁版/详情弹窗/申请入口,保持现状)
|
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
|
||||||
* - 小程序:plugin-uni-halo 小程序链接(按分组聚合展示 + 详情弹窗 + 申请收录弹窗)
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
*/
|
import { useSettingStore } from '@/store/setting'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||||
import { getFriendLinkGroupList, getFriendLinkList } from '@/api/halo'
|
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
||||||
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
|
import type { ILink, ILinkGroup } from '@/api/types/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import type { IMiniProgramLink, IMiniProgramLinkGroupVo } from '@/api/types/uni-halo'
|
||||||
import { useSettingStore } from '@/store/setting'
|
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
|
||||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
|
||||||
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
|
||||||
import type { ILink, ILinkGroup } from '@/api/types/halo'
|
|
||||||
import type { IMiniProgramLink, IMiniProgramLinkGroupVo } from '@/api/types/uni-halo'
|
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '友情链接',
|
navigationBarTitleText: '友情链接',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const haloPluginConfigs = computed(() => appConfigStore.configs.pluginConfig)
|
const haloPluginConfigs = computed(() => appConfigStore.configs.pluginConfig)
|
||||||
const globalAppSettings = computed(() => settingStore.settings)
|
const globalAppSettings = computed(() => settingStore.settings)
|
||||||
|
|
||||||
/* ---------------- 依赖插件(参考 gallery 对象传参模式) ---------------- */
|
/* ---------------- 依赖插件(参考 gallery 对象传参模式) ---------------- */
|
||||||
/** 站点 tab:PluginLinks */
|
/** 站点 tab:PluginLinks */
|
||||||
const { pluginId: sitePluginId, checking: siteChecking, tips: siteTips, available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable({
|
const { pluginId: sitePluginId, checking: siteChecking, tips: siteTips, available: sitePluginAvailable, check: checkSitePluginAvailable } = usePluginAvailable({
|
||||||
pluginId: NeedPluginIds.PluginLinks,
|
pluginId: NeedPluginIds.PluginLinks,
|
||||||
tips: '检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员',
|
tips: '检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员',
|
||||||
})
|
})
|
||||||
/** 小程序 tab:plugin-uni-halo */
|
/** 小程序 tab:plugin-uni-halo */
|
||||||
const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({
|
const { pluginId: miniPluginId, checking: miniChecking, tips: miniTips, available: miniPluginAvailable, check: checkMiniPluginAvailable } = usePluginAvailable({
|
||||||
pluginId: NeedPluginIds.PluginUniHalo,
|
pluginId: NeedPluginIds.PluginUniHalo,
|
||||||
tips: '检测到当前插件没有安装或者启用,无法使用小程序链接功能哦,请联系管理员',
|
tips: '检测到当前插件没有安装或者启用,无法使用小程序链接功能哦,请联系管理员',
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */
|
/** 重新检测站点插件:可用则拉取友链数据(供 uh-plugin-unavailable 刷新按钮) */
|
||||||
async function handleSitePluginRefresh() {
|
async function handleSitePluginRefresh() {
|
||||||
if (await checkSitePluginAvailable())
|
if (await checkSitePluginAvailable())
|
||||||
handleGetLinkGroupData()
|
handleGetLinkGroupData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */
|
/** 重新检测小程序插件:可用则拉取小程序链接数据(供 uh-plugin-unavailable 刷新按钮) */
|
||||||
async function handleMiniPluginRefresh() {
|
async function handleMiniPluginRefresh() {
|
||||||
if (await checkMiniPluginAvailable())
|
if (await checkMiniPluginAvailable())
|
||||||
handleGetMiniProgramLinks()
|
handleGetMiniProgramLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- tabs ---------------- */
|
/* ---------------- tabs ---------------- */
|
||||||
const activeTabIndex = ref(0)
|
const activeTabIndex = ref(0)
|
||||||
|
|
||||||
/** 顶部 tab 定义(同收藏页胶囊 chip;审核模式下小程序 tab 隐藏) */
|
/** 顶部 tab 定义(同收藏页胶囊 chip;审核模式下小程序 tab 隐藏) */
|
||||||
const friendLinkTabs = computed(() => [
|
const friendLinkTabs = computed(() => [
|
||||||
{ key: 'site', label: '站点' },
|
{ key: 'site', label: '站点' },
|
||||||
...(appConfigStore.auditModeEnabled ? [] : [{ key: 'mini', label: '小程序' }]),
|
...(appConfigStore.auditModeEnabled ? [] : [{ key: 'mini', label: '小程序' }]),
|
||||||
])
|
])
|
||||||
|
|
||||||
function handleOnTabChange(e: { index: number }) {
|
function handleOnTabChange(e : { index : number }) {
|
||||||
activeTabIndex.value = e.index
|
activeTabIndex.value = e.index
|
||||||
}
|
}
|
||||||
|
|
||||||
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab
|
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab
|
||||||
watch(() => appConfigStore.auditModeEnabled, (enabled) => {
|
watch(() => appConfigStore.auditModeEnabled, (enabled) => {
|
||||||
if (enabled)
|
if (enabled)
|
||||||
activeTabIndex.value = 0
|
activeTabIndex.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ==================== 站点 tab(plugin-links) ==================== */
|
/* ==================== 站点 tab(plugin-links) ==================== */
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus: siteLoadingStatus, updateLoadingStatus: updateSiteLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus: siteLoadingStatus, updateLoadingStatus: updateSiteLoadingStatus } = useDataLoadingStatus()
|
||||||
const queryParams = ref({ size: 10, page: 1 })
|
const queryParams = ref({ size: 10, page: 1 })
|
||||||
const detail = ref<{ show: boolean, data: ILink | null }>({ show: false, data: null })
|
const detail = ref<{ show : boolean, data : ILink | null }>({ show: false, data: null })
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
const isLoadMore = ref(false)
|
const isLoadMore = ref(false)
|
||||||
const loadMoreText = ref('')
|
const loadMoreText = ref('')
|
||||||
const linkGroupList = ref<ILinkGroup[]>([])
|
const linkGroupList = ref<ILinkGroup[]>([])
|
||||||
const dataList = ref<ILink[]>([])
|
const dataList = ref<ILink[]>([])
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
function findLinkGroupDisplayNameByGroupMetadataName(groupName?: string): string {
|
function findLinkGroupDisplayNameByGroupMetadataName(groupName ?: string) : string {
|
||||||
if (linkGroupList.value.length === 0)
|
if (linkGroupList.value.length === 0)
|
||||||
return groupName || '未分组'
|
return groupName || '未分组'
|
||||||
const found = linkGroupList.value.find(item => item.metadata.name === groupName)
|
const found = linkGroupList.value.find(item => item.metadata.name === groupName)
|
||||||
return found?.spec.displayName || groupName || '未分组'
|
return found?.spec.displayName || groupName || '未分组'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGetLinkGroupData() {
|
async function handleGetLinkGroupData() {
|
||||||
try {
|
try {
|
||||||
const res = await getFriendLinkGroupList({ page: 1, size: 0 })
|
const res = await getFriendLinkGroupList({ page: 1, size: 0 })
|
||||||
linkGroupList.value = res.data.items || []
|
linkGroupList.value = res.data.items || []
|
||||||
@@ -103,9 +97,9 @@ async function handleGetLinkGroupData() {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
|
updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
updateSiteLoadingStatus(DataLoadingStatusEnum.Loading)
|
updateSiteLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
@@ -146,14 +140,14 @@ async function handleGetData() {
|
|||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 站点交互 ---------------- */
|
/* ---------------- 站点交互 ---------------- */
|
||||||
function handleOnLinkEvent(link: ILink) {
|
function handleOnLinkEvent(link : ILink) {
|
||||||
detail.value = { show: true, data: link }
|
detail.value = { show: true, data: link }
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopyLink(link: ILink) {
|
function handleCopyLink(link : ILink) {
|
||||||
uni.setClipboardData({
|
uni.setClipboardData({
|
||||||
data: `${link.spec.displayName}:${link.spec.url}`,
|
data: `${link.spec.displayName}:${link.spec.url}`,
|
||||||
showToast: false,
|
showToast: false,
|
||||||
@@ -164,13 +158,13 @@ function handleCopyLink(link: ILink) {
|
|||||||
uni.showToast({ icon: 'none', title: '复制失败!' })
|
uni.showToast({ icon: 'none', title: '复制失败!' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSubmitLinkPage() {
|
function toSubmitLinkPage() {
|
||||||
uni.navigateTo({ url: '/pages-blog/submit-link/submit-link' })
|
uni.navigateTo({ url: '/pages-blog/submit-link/submit-link' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToTopPage(duration = 500) {
|
function handleToTopPage(duration = 500) {
|
||||||
uni.pageScrollTo({
|
uni.pageScrollTo({
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
duration,
|
duration,
|
||||||
@@ -178,24 +172,24 @@ function handleToTopPage(duration = 500) {
|
|||||||
console.error('回顶失败', err)
|
console.error('回顶失败', err)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcSiteThumbnail(val?: string): string {
|
function calcSiteThumbnail(val ?: string) : string {
|
||||||
if (!val)
|
if (!val)
|
||||||
return ''
|
return ''
|
||||||
const _val = val.endsWith('/') ? val : `${val}/`
|
const _val = val.endsWith('/') ? val : `${val}/`
|
||||||
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
|
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
|
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
|
||||||
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
|
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
|
||||||
const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null })
|
const miniDetail = ref<{ show : boolean, data : IMiniProgramLink | null }>({ show: false, data: null })
|
||||||
const applyShow = ref(false)
|
const applyShow = ref(false)
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetMiniProgramLinks() {
|
async function handleGetMiniProgramLinks() {
|
||||||
updateMiniLoadingStatus(DataLoadingStatusEnum.Loading)
|
updateMiniLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getMiniProgramLinkGroupedList()
|
const res = await getMiniProgramLinkGroupedList()
|
||||||
@@ -215,24 +209,24 @@ async function handleGetMiniProgramLinks() {
|
|||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnMiniLinkEvent(link: IMiniProgramLink) {
|
function handleOnMiniLinkEvent(link : IMiniProgramLink) {
|
||||||
miniDetail.value = { show: true, data: link }
|
miniDetail.value = { show: true, data: link }
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenApply() {
|
function handleOpenApply() {
|
||||||
applyShow.value = true
|
applyShow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleApplyClose(data: { isSubmit: boolean, refresh: boolean }) {
|
function handleApplyClose(data : { isSubmit : boolean, refresh : boolean }) {
|
||||||
applyShow.value = false
|
applyShow.value = false
|
||||||
if (data.refresh)
|
if (data.refresh)
|
||||||
handleGetMiniProgramLinks()
|
handleGetMiniProgramLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 复制小程序地址 */
|
/** 复制小程序地址 */
|
||||||
function handleCopyMiniProgramCode(link: IMiniProgramLink) {
|
function handleCopyMiniProgramCode(link : IMiniProgramLink) {
|
||||||
const url = link.spec?.link
|
const url = link.spec?.link
|
||||||
if (!url) {
|
if (!url) {
|
||||||
uni.showToast({ icon: 'none', title: '该小程序未填写跳转地址' })
|
uni.showToast({ icon: 'none', title: '该小程序未填写跳转地址' })
|
||||||
@@ -248,10 +242,10 @@ function handleCopyMiniProgramCode(link: IMiniProgramLink) {
|
|||||||
uni.showToast({ icon: 'none', title: '复制失败!' })
|
uni.showToast({ icon: 'none', title: '复制失败!' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 预览太阳码(支持长按识别/保存) */
|
/** 预览太阳码(支持长按识别/保存) */
|
||||||
function handlePreviewMiniProgramCode(link: IMiniProgramLink) {
|
function handlePreviewMiniProgramCode(link : IMiniProgramLink) {
|
||||||
const code = link.spec?.miniProgramCode
|
const code = link.spec?.miniProgramCode
|
||||||
if (!code)
|
if (!code)
|
||||||
return
|
return
|
||||||
@@ -259,10 +253,10 @@ function handlePreviewMiniProgramCode(link: IMiniProgramLink) {
|
|||||||
urls: [checkImageUrl(code)],
|
urls: [checkImageUrl(code)],
|
||||||
current: checkImageUrl(code),
|
current: checkImageUrl(code),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存太阳码到相册 */
|
/** 保存太阳码到相册 */
|
||||||
function handleSaveMiniProgramCode(link: IMiniProgramLink) {
|
function handleSaveMiniProgramCode(link : IMiniProgramLink) {
|
||||||
const code = link.spec?.miniProgramCode
|
const code = link.spec?.miniProgramCode
|
||||||
if (!code)
|
if (!code)
|
||||||
return
|
return
|
||||||
@@ -292,10 +286,10 @@ function handleSaveMiniProgramCode(link: IMiniProgramLink) {
|
|||||||
uni.showToast({ icon: 'none', title: '图片下载失败' })
|
uni.showToast({ icon: 'none', title: '图片下载失败' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
onLoad(async () => {
|
onLoad(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
checkSitePluginAvailable(),
|
checkSitePluginAvailable(),
|
||||||
checkMiniPluginAvailable(),
|
checkMiniPluginAvailable(),
|
||||||
@@ -306,9 +300,9 @@ onLoad(async () => {
|
|||||||
handleGetMiniProgramLinks()
|
handleGetMiniProgramLinks()
|
||||||
if (!sitePluginAvailable.value && !miniPluginAvailable.value)
|
if (!sitePluginAvailable.value && !miniPluginAvailable.value)
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
if (activeTabIndex.value === 0) {
|
if (activeTabIndex.value === 0) {
|
||||||
if (!sitePluginAvailable.value) {
|
if (!sitePluginAvailable.value) {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
@@ -326,12 +320,11 @@ onPullDownRefresh(() => {
|
|||||||
}
|
}
|
||||||
handleGetMiniProgramLinks()
|
handleGetMiniProgramLinks()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (activeTabIndex.value === 0) {
|
if (activeTabIndex.value === 0) {
|
||||||
if (!sitePluginAvailable.value)
|
if (!sitePluginAvailable.value) { return }
|
||||||
return
|
|
||||||
if (hasNext.value) {
|
if (hasNext.value) {
|
||||||
queryParams.value.page += 1
|
queryParams.value.page += 1
|
||||||
isLoadMore.value = true
|
isLoadMore.value = true
|
||||||
@@ -344,7 +337,7 @@ onReachBottom(() => {
|
|||||||
else {
|
else {
|
||||||
uni.showToast({ icon: 'none', title: '没有更多数据了' })
|
uni.showToast({ icon: 'none', title: '没有更多数据了' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -356,12 +349,10 @@ onReachBottom(() => {
|
|||||||
<wd-sticky>
|
<wd-sticky>
|
||||||
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
||||||
<view class="flex gap-2 px-3 pb-1 pt-3">
|
<view class="flex gap-2 px-3 pb-1 pt-3">
|
||||||
<view
|
<view v-for="(tab, index) in friendLinkTabs" :key="tab.key"
|
||||||
v-for="(tab, index) in friendLinkTabs" :key="tab.key"
|
|
||||||
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
|
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
|
||||||
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
|
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
|
||||||
@click="handleOnTabChange({ index })"
|
@click="handleOnTabChange({ index })">
|
||||||
>
|
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -370,21 +361,13 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<!-- ==================== 站点 tab ==================== -->
|
<!-- ==================== 站点 tab ==================== -->
|
||||||
<template v-if="activeTabIndex === 0">
|
<template v-if="activeTabIndex === 0">
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable v-if="!sitePluginAvailable" :plugin-id="sitePluginId" :error-text="siteTips"
|
||||||
v-if="!sitePluginAvailable"
|
:checking="siteChecking" @on-refresh="handleSitePluginRefresh" />
|
||||||
:plugin-id="sitePluginId"
|
|
||||||
:error-text="siteTips"
|
|
||||||
:checking="siteChecking"
|
|
||||||
@on-refresh="handleSitePluginRefresh"
|
|
||||||
/>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 加载/错误/空占位(状态机) -->
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
<view v-if="siteLoadingStatus !== 'success'">
|
<view v-if="siteLoadingStatus !== 'success'">
|
||||||
<uh-data-loading
|
<uh-data-loading :loading-status="siteLoadingStatus" empty-text="啊偶,博主还没有朋友呢~"
|
||||||
:loading-status="siteLoadingStatus"
|
@refresh="handleGetData" />
|
||||||
empty-text="啊偶,博主还没有朋友呢~"
|
|
||||||
@refresh="handleGetData"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="content pt-4">
|
<view v-else class="content pt-4">
|
||||||
@@ -392,33 +375,37 @@ onReachBottom(() => {
|
|||||||
<view class="link-list flex flex-col gap-4 px-4 pb-4">
|
<view class="link-list flex flex-col gap-4 px-4 pb-4">
|
||||||
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName">
|
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName">
|
||||||
<!-- 色彩版 -->
|
<!-- 色彩版 -->
|
||||||
<view
|
<view v-if="!globalAppSettings.links.useSimple"
|
||||||
v-if="!globalAppSettings.links.useSimple"
|
class="info uh-global-card-glass flex rounded-2xl p-3" @click="handleOnLinkEvent(link)">
|
||||||
class="info uh-global-card-glass flex rounded-2xl p-3"
|
<image class="link-logo h-[140rpx] w-[140rpx] shrink-0 rounded-xl" :src="link.spec.logo"
|
||||||
@click="handleOnLinkEvent(link)"
|
mode="aspectFill" />
|
||||||
>
|
|
||||||
<image class="link-logo h-[140rpx] w-[140rpx] shrink-0 rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
|
||||||
<view class="info-detail flex flex-1 flex-col justify-center pl-5">
|
<view class="info-detail flex flex-1 flex-col justify-center pl-5">
|
||||||
<view class="link-card-name text-[30rpx] text-gray-900 font-bold">
|
<view class="link-card-name text-[30rpx] text-gray-900 font-bold">
|
||||||
<text class="group-tag mr-3 rounded-md bg-secondary px-1.5 py-0.5 text-[20rpx] text-[#4d7c0f] font-normal">{{ link.spec.groupName || '暂未分组' }}</text>
|
<text
|
||||||
|
class="group-tag mr-3 rounded-md bg-secondary px-1.5 py-0.5 text-[20rpx] text-[#4d7c0f] font-normal">{{ link.spec.groupName || '暂未分组' }}</text>
|
||||||
{{ link.spec.displayName }}
|
{{ link.spec.displayName }}
|
||||||
</view>
|
</view>
|
||||||
<view class="link-card-url mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
<view
|
||||||
|
class="link-card-url mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
||||||
站点地址:{{ link.spec.url }}
|
站点地址:{{ link.spec.url }}
|
||||||
</view>
|
</view>
|
||||||
<view class="link-card-desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-600">
|
<view
|
||||||
|
class="link-card-desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-600">
|
||||||
博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
|
博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 简洁版 -->
|
<!-- 简洁版 -->
|
||||||
<view v-else class="link-card uh-global-card-glass flex items-center rounded-2xl p-4" @click="handleOnLinkEvent(link)">
|
<view v-else class="link-card uh-global-card-glass flex items-center rounded-2xl p-4"
|
||||||
<image class="logo h-[80rpx] w-[80rpx] shrink-0 border-4 border-white/90 rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
@click="handleOnLinkEvent(link)">
|
||||||
|
<image class="logo h-[80rpx] w-[80rpx] shrink-0 border-4 border-white/90 rounded-xl"
|
||||||
|
:src="link.spec.logo" mode="aspectFill" />
|
||||||
<view class="link-info flex-1 pl-5">
|
<view class="link-info flex-1 pl-5">
|
||||||
<view class="name text-[30rpx] text-gray-900 font-bold">
|
<view class="name text-[30rpx] text-gray-900 font-bold">
|
||||||
{{ link.spec.displayName }}
|
{{ link.spec.displayName }}
|
||||||
</view>
|
</view>
|
||||||
<view class="desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
<view
|
||||||
|
class="desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
||||||
{{ link.spec.description }}
|
{{ link.spec.description }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -427,11 +414,11 @@ onReachBottom(() => {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 悬浮按钮 -->
|
<!-- 悬浮按钮 -->
|
||||||
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
|
<view class="flot-buttons fixed bottom-10 right-4 z-999 flex flex-col gap-1.5">
|
||||||
<view class="fab-btn uh-global-card-glass h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full" @click="handleToTopPage()">
|
<view
|
||||||
<wd-icon name="arrow-up" size="20px" color="#6b7280" />
|
v-if="!haloPluginConfigs?.linksSubmitPlugin?.enabled"
|
||||||
</view>
|
class="fab-btn uh-global-card-glass h-10 w-10 flex items-center justify-center rounded-full"
|
||||||
<view v-if="(haloPluginConfigs?.linksSubmitPlugin as { enabled?: boolean } | undefined)?.enabled" class="fab-btn uh-global-card-glass h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full" @click="toSubmitLinkPage">
|
@click="toSubmitLinkPage">
|
||||||
<wd-icon name="edit" size="20px" color="#6b7280" />
|
<wd-icon name="edit" size="20px" color="#6b7280" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -440,7 +427,8 @@ onReachBottom(() => {
|
|||||||
<wd-popup v-model="detail.show" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
<wd-popup v-model="detail.show" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||||
<view v-if="detail.data" class="poup p-8">
|
<view v-if="detail.data" class="poup p-8">
|
||||||
<view class="info flex">
|
<view class="info flex">
|
||||||
<image class="poup-logo h-[140rpx] w-[140rpx] shrink-0 rounded-full" :src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
|
<image class="poup-logo h-[140rpx] w-[140rpx] shrink-0 rounded-full"
|
||||||
|
:src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
|
||||||
<view class="poup-info ml-6 flex flex-1 flex-col justify-center">
|
<view class="poup-info ml-6 flex flex-1 flex-col justify-center">
|
||||||
<view class="poup-name text-[34rpx] text-gray-900 font-bold">
|
<view class="poup-name text-[34rpx] text-gray-900 font-bold">
|
||||||
{{ detail.data.spec.displayName }}
|
{{ detail.data.spec.displayName }}
|
||||||
@@ -449,14 +437,16 @@ onReachBottom(() => {
|
|||||||
{{ detail.data.spec.groupName }}
|
{{ detail.data.spec.groupName }}
|
||||||
</view>
|
</view>
|
||||||
<view class="poup-link mt-3" @click="handleCopyLink(detail.data)">
|
<view class="poup-link mt-3" @click="handleCopyLink(detail.data)">
|
||||||
<text class="poup-url rounded-lg bg-secondary px-2 py-1 text-[24rpx] text-[#4d7c0f]">{{ detail.data.spec.url }}</text>
|
<text
|
||||||
|
class="poup-url rounded-lg bg-secondary px-2 py-1 text-[24rpx] text-[#4d7c0f]">{{ detail.data.spec.url }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="poup-desc mt-5 text-[28rpx] text-gray-600 leading-[1.6]">
|
<view class="poup-desc mt-5 text-[28rpx] text-gray-600 leading-[1.6]">
|
||||||
博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
|
博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
|
||||||
</view>
|
</view>
|
||||||
<image class="poup-img mt-6 h-[320rpx] w-full rounded-xl" :src="calcSiteThumbnail(detail.data.spec.url)" mode="aspectFill" />
|
<image class="poup-img mt-6 h-[320rpx] w-full rounded-xl"
|
||||||
|
:src="calcSiteThumbnail(detail.data.spec.url)" mode="aspectFill" />
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
@@ -469,52 +459,38 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<!-- ==================== 小程序 tab ==================== -->
|
<!-- ==================== 小程序 tab ==================== -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable v-if="!miniPluginAvailable" :plugin-id="miniPluginId" :error-text="miniTips"
|
||||||
v-if="!miniPluginAvailable"
|
:checking="miniChecking" @on-refresh="handleMiniPluginRefresh" />
|
||||||
:plugin-id="miniPluginId"
|
|
||||||
:error-text="miniTips"
|
|
||||||
:checking="miniChecking"
|
|
||||||
@on-refresh="handleMiniPluginRefresh"
|
|
||||||
/>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 加载/错误/空占位(状态机) -->
|
<uh-data-loading v-if="miniLoadingStatus !== 'success'" :loading-status="miniLoadingStatus" empty-text="还没有收录的小程序呢~"
|
||||||
<view v-if="miniLoadingStatus !== 'success'">
|
@refresh="handleGetMiniProgramLinks" />
|
||||||
<uh-data-loading
|
|
||||||
:loading-status="miniLoadingStatus"
|
|
||||||
empty-text="还没有收录的小程序呢~"
|
|
||||||
@refresh="handleGetMiniProgramLinks"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-else class="content flex flex-1 flex-col">
|
<view v-else class="content flex flex-1 flex-col">
|
||||||
<!-- 分组列表 -->
|
<!-- 分组列表 -->
|
||||||
<view class="mini-link-list flex-1 px-6 py-4">
|
<view class="mini-link-list flex-1 px-6 py-4">
|
||||||
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
|
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
|
||||||
<view class="group-title mb-4 flex items-center">
|
<view class="group-title mb-4 flex items-center">
|
||||||
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
|
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
|
||||||
<text class="text-[30rpx] text-gray-900 font-bold">{{ group.displayName || '未分组' }}</text>
|
<text
|
||||||
|
class="text-[30rpx] text-gray-900 font-bold">{{ group.displayName || '未分组' }}</text>
|
||||||
<text class="ml-3 text-[24rpx] text-gray-400">({{ group.links.length }})</text>
|
<text class="ml-3 text-[24rpx] text-gray-400">({{ group.links.length }})</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="group-cards flex flex-col gap-4">
|
<view class="group-cards flex flex-col gap-4">
|
||||||
<view
|
<view v-for="link in group.links" :key="link.metadata?.name"
|
||||||
v-for="link in group.links"
|
|
||||||
:key="link.metadata?.name"
|
|
||||||
class="mini-card uh-global-card-glass flex items-center rounded-2xl p-4"
|
class="mini-card uh-global-card-glass flex items-center rounded-2xl p-4"
|
||||||
@click="handleOnMiniLinkEvent(link)"
|
@click="handleOnMiniLinkEvent(link)">
|
||||||
>
|
<image class="mini-code h-[120rpx] w-[120rpx] shrink-0 rounded-lg"
|
||||||
<image
|
:src="checkImageUrl(link.spec?.miniProgramCode)" mode="aspectFill" />
|
||||||
class="mini-code h-[120rpx] w-[120rpx] shrink-0 rounded-lg"
|
|
||||||
:src="checkImageUrl(link.spec?.miniProgramCode)"
|
|
||||||
mode="aspectFill"
|
|
||||||
/>
|
|
||||||
<view class="mini-info flex flex-1 flex-col pl-5">
|
<view class="mini-info flex flex-1 flex-col pl-5">
|
||||||
<view class="mini-name overflow-hidden text-ellipsis whitespace-nowrap text-[30rpx] text-gray-900 font-bold">
|
<view
|
||||||
|
class="mini-name overflow-hidden text-ellipsis whitespace-nowrap text-[30rpx] text-gray-900 font-bold">
|
||||||
{{ link.spec?.displayName }}
|
{{ link.spec?.displayName }}
|
||||||
</view>
|
</view>
|
||||||
<view v-if="link.spec?.authorName" class="mini-author mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
<view v-if="link.spec?.authorName"
|
||||||
|
class="mini-author mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400">
|
||||||
{{ link.spec.authorName }}
|
{{ link.spec.authorName }}
|
||||||
</view>
|
</view>
|
||||||
<view class="mini-desc mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-500">
|
<view
|
||||||
|
class="mini-desc mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-500">
|
||||||
{{ link.spec?.description || '暂无简介~' }}
|
{{ link.spec?.description || '暂无简介~' }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -525,10 +501,10 @@ onReachBottom(() => {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 申请收录悬浮按钮 -->
|
<!-- 申请收录悬浮按钮 -->
|
||||||
<view class="apply-btn-wrap fixed bottom-[100rpx] right-8 z-999">
|
<view class="fixed bottom-10 right-4 z-50">
|
||||||
<view class="apply-btn h-[88rpx] flex items-center rounded-full bg-gray-900 px-6" @click="handleOpenApply">
|
<view class="box-border flex flex-col w-10 h-10 items-center justify-center rounded-full bg-gray-900"
|
||||||
<wd-icon name="add" size="20px" color="#fff" />
|
@click="handleOpenApply">
|
||||||
<text class="ml-2 text-[26rpx] text-white">申请收录</text>
|
<text class="text-xs text-white">申请</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -538,13 +514,10 @@ onReachBottom(() => {
|
|||||||
<view v-if="miniDetail.data" class="mini-poup p-8">
|
<view v-if="miniDetail.data" class="mini-poup p-8">
|
||||||
<!-- 太阳码大图(点击预览/长按保存) -->
|
<!-- 太阳码大图(点击预览/长按保存) -->
|
||||||
<view class="code-area flex flex-col items-center">
|
<view class="code-area flex flex-col items-center">
|
||||||
<image
|
<image class="code-img h-[320rpx] w-[320rpx] rounded-xl"
|
||||||
class="code-img h-[320rpx] w-[320rpx] rounded-xl"
|
:src="checkImageUrl(miniDetail.data.spec?.miniProgramCode)" mode="aspectFill"
|
||||||
:src="checkImageUrl(miniDetail.data.spec?.miniProgramCode)"
|
|
||||||
mode="aspectFill"
|
|
||||||
@click="handlePreviewMiniProgramCode(miniDetail.data)"
|
@click="handlePreviewMiniProgramCode(miniDetail.data)"
|
||||||
@longpress="handleSaveMiniProgramCode(miniDetail.data)"
|
@longpress="handleSaveMiniProgramCode(miniDetail.data)" />
|
||||||
/>
|
|
||||||
<view class="code-tip mt-3 flex items-center text-[24rpx] text-gray-400">
|
<view class="code-tip mt-3 flex items-center text-[24rpx] text-gray-400">
|
||||||
<wd-icon name="picture" size="14px" color="#a8a294" />
|
<wd-icon name="picture" size="14px" color="#a8a294" />
|
||||||
<text class="ml-1">点击预览,长按保存太阳码</text>
|
<text class="ml-1">点击预览,长按保存太阳码</text>
|
||||||
@@ -553,41 +526,56 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<!-- 名称与分组 -->
|
<!-- 名称与分组 -->
|
||||||
<view class="mini-head mt-5 flex items-center">
|
<view class="mini-head mt-5 flex items-center">
|
||||||
<text class="mini-name text-[34rpx] text-gray-900 font-bold">{{ miniDetail.data.spec?.displayName }}</text>
|
<text
|
||||||
<text v-if="miniDetail.data.spec?.groupName" class="group-tag ml-3 rounded-md bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f]">
|
class="mini-name text-[34rpx] text-gray-900 font-bold">{{ miniDetail.data.spec?.displayName }}</text>
|
||||||
|
<text v-if="miniDetail.data.spec?.groupName"
|
||||||
|
class="group-tag ml-3 rounded-md bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f]">
|
||||||
{{ miniDetail.data.spec.groupName }}
|
{{ miniDetail.data.spec.groupName }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 描述 -->
|
<!-- 描述 -->
|
||||||
<view v-if="miniDetail.data.spec?.description" class="mini-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]">
|
<view v-if="miniDetail.data.spec?.description"
|
||||||
|
class="mini-desc mt-4 text-[28rpx] text-gray-600 leading-[1.6]">
|
||||||
{{ miniDetail.data.spec.description }}
|
{{ miniDetail.data.spec.description }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 作者信息 -->
|
<!-- 作者信息 -->
|
||||||
<view v-if="miniDetail.data.spec?.authorName || miniDetail.data.spec?.avatar || miniDetail.data.spec?.website" class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f6f3ee] p-4">
|
<view
|
||||||
<image v-if="miniDetail.data.spec?.avatar" class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full" :src="checkAvatarUrl(miniDetail.data.spec.avatar)" mode="aspectFill" />
|
v-if="miniDetail.data.spec?.authorName || miniDetail.data.spec?.avatar || miniDetail.data.spec?.website"
|
||||||
|
class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f6f3ee] p-4">
|
||||||
|
<image v-if="miniDetail.data.spec?.avatar"
|
||||||
|
class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full"
|
||||||
|
:src="checkAvatarUrl(miniDetail.data.spec.avatar)" mode="aspectFill" />
|
||||||
<view class="author-detail ml-4 flex flex-1 flex-col">
|
<view class="author-detail ml-4 flex flex-1 flex-col">
|
||||||
<text v-if="miniDetail.data.spec?.authorName" class="author-name text-[28rpx] text-gray-900 font-medium">{{ miniDetail.data.spec.authorName }}</text>
|
<text v-if="miniDetail.data.spec?.authorName"
|
||||||
<text v-if="miniDetail.data.spec?.website" class="author-website mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400" @click="handleCopyMiniProgramCode(miniDetail.data)">
|
class="author-name text-[28rpx] text-gray-900 font-medium">{{ miniDetail.data.spec.authorName }}</text>
|
||||||
|
<text v-if="miniDetail.data.spec?.website"
|
||||||
|
class="author-website mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-gray-400"
|
||||||
|
@click="handleCopyMiniProgramCode(miniDetail.data)">
|
||||||
网站:{{ miniDetail.data.spec.website }}
|
网站:{{ miniDetail.data.spec.website }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 小程序地址 -->
|
<!-- 小程序地址 -->
|
||||||
<view v-if="miniDetail.data.spec?.link" class="mini-link mt-5 flex items-center justify-between rounded-xl bg-secondary p-4">
|
<view v-if="miniDetail.data.spec?.link"
|
||||||
<view class="link-text flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#4d7c0f]">
|
class="mini-link mt-5 flex items-center justify-between rounded-xl bg-secondary p-4">
|
||||||
|
<view
|
||||||
|
class="link-text flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#4d7c0f]">
|
||||||
{{ miniDetail.data.spec.link }}
|
{{ miniDetail.data.spec.link }}
|
||||||
</view>
|
</view>
|
||||||
<text class="ml-3 shrink-0 text-[26rpx] text-[#4d7c0f] font-bold" @click="handleCopyMiniProgramCode(miniDetail.data)">复制</text>
|
<text class="ml-3 shrink-0 text-[26rpx] text-[#4d7c0f] font-bold"
|
||||||
|
@click="handleCopyMiniProgramCode(miniDetail.data)">复制</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 预览图轮播 -->
|
<!-- 预览图轮播 -->
|
||||||
<view v-if="miniDetail.data.spec?.screenshots?.length" class="mini-screenshots mt-6">
|
<view v-if="miniDetail.data.spec?.screenshots?.length" class="mini-screenshots mt-6">
|
||||||
<swiper class="screenshots-swiper h-[360rpx] w-full" indicator-dots circular>
|
<swiper class="screenshots-swiper h-[360rpx] w-full" indicator-dots circular>
|
||||||
<swiper-item v-for="(img, idx) in miniDetail.data.spec.screenshots" :key="idx">
|
<swiper-item v-for="(img, idx) in miniDetail.data.spec.screenshots" :key="idx">
|
||||||
<image class="screenshot-img h-full w-full rounded-xl" :src="checkImageUrl(img)" mode="aspectFill" @click="handlePreviewMiniProgramCode({ spec: { miniProgramCode: img } } as IMiniProgramLink)" />
|
<image class="screenshot-img h-full w-full rounded-xl" :src="checkImageUrl(img)"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="handlePreviewMiniProgramCode({ spec: { miniProgramCode: img } } as IMiniProgramLink)" />
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
</view>
|
</view>
|
||||||
@@ -595,7 +583,7 @@ onReachBottom(() => {
|
|||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<!-- 小程序链接申请弹窗 -->
|
<!-- 小程序链接申请弹窗 -->
|
||||||
<uh-mini-link-apply :show="applyShow" @on-close="handleApplyClose" />
|
<uh-links-mini-apply :show="applyShow" @on-close="handleApplyClose" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ onPullDownRefresh(() => {
|
|||||||
</view>
|
</view>
|
||||||
<!-- 时间轴 -->
|
<!-- 时间轴 -->
|
||||||
<view v-else class="timeline relative pl-10">
|
<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-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 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]">
|
<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%);
|
background-color: rgb(248 140 162 / 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child::before {
|
&.timeline-item-last::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,21 +310,19 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 列表项漂浮动画(无法用 UnoCSS 表达) */
|
/* 列表项漂浮动画(无法用 UnoCSS 表达;模板已生成 list-item-N 类,避开 WXSS 不支持的 :nth-child) */
|
||||||
.list-item {
|
.list-item-1 {
|
||||||
&:nth-child(1) {
|
|
||||||
animation: listItemAni1 3s ease-in-out infinite;
|
animation: listItemAni1 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
.list-item-2 {
|
||||||
animation: listItemAni1 3s ease-in-out infinite;
|
animation: listItemAni1 3s ease-in-out infinite;
|
||||||
animation-delay: 1.5s;
|
animation-delay: 1.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(3) {
|
.list-item-3 {
|
||||||
animation: listItemAni1 3s ease-in-out infinite;
|
animation: listItemAni1 3s ease-in-out infinite;
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes likeani {
|
@keyframes likeani {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
type FromReason = 'plugin' | 'maintenance'
|
type FromReason = 'plugin' | 'maintenance'
|
||||||
|
|
||||||
/** 默认维护标题(拦截场景未配置维护信息时展示) */
|
/** 默认维护标题(拦截场景未配置维护信息时展示) */
|
||||||
const DEFAULT_MAINTENANCE_TITLE = '我们正在加油升级!'
|
const DEFAULT_MAINTENANCE_TITLE = '维护中'
|
||||||
/** 恢复检测轮询间隔(ms):插件激活/维护结束探测 */
|
/** 恢复检测轮询间隔(ms):插件激活/维护结束探测 */
|
||||||
const RECOVERY_POLL_INTERVAL = 30 * 1000
|
const RECOVERY_POLL_INTERVAL = 30 * 1000
|
||||||
|
|
||||||
@@ -133,8 +133,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const remaining = new Date(target).getTime() - nowMs.value
|
const remaining = new Date(target).getTime() - nowMs.value
|
||||||
if (Number.isFinite(remaining) && remaining <= 0)
|
if (Number.isFinite(remaining) && remaining <= 0) { silentCheck() }
|
||||||
void silentCheck()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,8 +141,7 @@
|
|||||||
* 拉取失败(服务器停机)保持维护页不打扰。
|
* 拉取失败(服务器停机)保持维护页不打扰。
|
||||||
*/
|
*/
|
||||||
async function silentCheck() {
|
async function silentCheck() {
|
||||||
if (refreshing)
|
if (refreshing) { return }
|
||||||
return
|
|
||||||
refreshing = true
|
refreshing = true
|
||||||
try {
|
try {
|
||||||
await store.bootstrap({ force: true })
|
await store.bootstrap({ force: true })
|
||||||
@@ -270,8 +268,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="relative min-h-screen flex flex-col justify-center bg-[#f5fae8]">
|
<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'" />
|
<uh-data-loading v-if="viewState === 'loading'" />
|
||||||
@@ -279,9 +277,7 @@
|
|||||||
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
|
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
|
||||||
<view v-else-if="viewState === 'error'"
|
<view v-else-if="viewState === 'error'"
|
||||||
class="relative z-10 flex flex-col items-center justify-center px-10 py-48 text-center">
|
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="140rpx" />
|
||||||
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="120rpx" />
|
|
||||||
</text>
|
|
||||||
<text class="mt-6 text-md text-gray-900 font-bold">
|
<text class="mt-6 text-md text-gray-900 font-bold">
|
||||||
服务暂时无法访问
|
服务暂时无法访问
|
||||||
</text>
|
</text>
|
||||||
@@ -290,10 +286,9 @@
|
|||||||
</text>
|
</text>
|
||||||
<view class="mt-10">
|
<view class="mt-10">
|
||||||
<uh-button custom-class="uh-global-card-glass border flex-1 py-2 !px-8 !rounded-full font-semibold"
|
<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>
|
</view>
|
||||||
|
|
||||||
<!-- 维护页-->
|
<!-- 维护页-->
|
||||||
<view v-else class="relative z-10 w-full min-h-screen flex items-center justify-center flex-col ">
|
<view v-else class="relative z-10 w-full min-h-screen flex items-center justify-center flex-col ">
|
||||||
<view
|
<view
|
||||||
@@ -309,8 +304,8 @@
|
|||||||
<view
|
<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)]">
|
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" />
|
<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"
|
<wd-icon v-else class-prefix="uhemoji-icon" name="-thinking" size="140rpx"
|
||||||
size="120rpx" />
|
class="text-gray-900" />
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="gear-spin absolute right-[-28rpx] top-[-16rpx] h-[68rpx] w-[68rpx] flex items-center justify-center">
|
class="gear-spin absolute right-[-28rpx] top-[-16rpx] h-[68rpx] w-[68rpx] flex items-center justify-center">
|
||||||
@@ -331,10 +326,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<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" />
|
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-6 px-[16rpx] text-[25rpx] text-balck/50 font-medium leading-[1.7]">
|
||||||
<view
|
<template v-if="fromReason!=='plugin' && noticeLines.length > 0">
|
||||||
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">
|
|
||||||
<text v-for="(line, index) in noticeLines" :key="index" class="block">{{ line }}</text>
|
<text v-for="(line, index) in noticeLines" :key="index" class="block">{{ line }}</text>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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]" />
|
class="pointer-events-none absolute left-[64rpx] top-[-52rpx] z-0 h-[192rpx] w-[192rpx] rounded-full bg-[#ebfabf] opacity-90 blur-[44rpx]" />
|
||||||
<view
|
<view
|
||||||
class="pointer-events-none absolute right-[-52rpx] top-[300rpx] z-0 h-[168rpx] w-[168rpx] rounded-full bg-[#ffd53d] opacity-20 blur-[44rpx]" />
|
class="pointer-events-none absolute right-[-52rpx] top-[300rpx] z-0 h-[168rpx] w-[168rpx] rounded-full bg-[#ffd53d] opacity-20 blur-[44rpx]" />
|
||||||
|
|
||||||
<view
|
<view
|
||||||
class="relative box-border w-full z-1 flex flex-col items-center gap-[28rpx] px-6 pb-[68rpx] pt-[32rpx]">
|
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">
|
<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"
|
<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"
|
<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"
|
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">
|
@click="showDetail = true">
|
||||||
维护详情
|
维护详情
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<view class="mt-[12rpx] flex flex-col items-center">
|
<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">
|
<text class="block">
|
||||||
去喝杯奶茶等等吧~
|
去喝杯奶茶等等吧~
|
||||||
@@ -424,13 +415,11 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<view v-if="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">
|
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 }}
|
{{ toast }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 维护详情弹窗(复用封装组件,见 components/uh-maintenance-detail) -->
|
|
||||||
<uh-maintenance-detail v-model="showDetail" :content="detailHtml" />
|
<uh-maintenance-detail v-model="showDetail" :content="detailHtml" />
|
||||||
</view>
|
</view>
|
||||||
</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;" />
|
class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 站点统计(上浮玻璃卡,与头部衔接) -->
|
<!-- 站点统计 -->
|
||||||
<view class="uh-global-card-glass relative z-100 mx-4 flex border rounded-2xl -mt-12">
|
<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-6 text-center">
|
<view v-for="item in allStats" :key="item.key" class="flex-1 py-4 text-center">
|
||||||
<wd-count-to
|
<wd-count-to
|
||||||
:key="`${item.key}-${item.value}`" :start-val="0" :end-val="item.value"
|
:key="`${item.key}-${item.value}`" :start-val="0" :end-val="item.value"
|
||||||
:duration="900" separator="" color="#111827" custom-class="text-lg font-bold"
|
:duration="900" separator="" color="#111827" custom-class="text-lg font-bold"
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 功能导航(分组玻璃卡) -->
|
<!-- 功能导航-->
|
||||||
<template v-for="group in calcNavGroups" :key="group.key">
|
<template v-for="group in calcNavGroups" :key="group.key">
|
||||||
<uh-section-title class="mx-4 mb-3 mt-8">
|
<uh-section-title class="mx-4 mb-3 mt-8">
|
||||||
{{ group.title }}
|
{{ group.title }}
|
||||||
|
|||||||
Reference in New Issue
Block a user