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

refactor: 重构维护模式相关逻辑,拆分维护说明与详情

- 拆分维护接口类型,新增notice和description字段区分纯文本说明与富文本详情
- 重构useMaintenanceIntercept钩子,统一维护拦截逻辑
- 新增维护详情弹窗组件,优化维护页UI与交互
- 修复按钮布局、滚动置顶黑名单等细节问题
- 优化公告弹窗组件样式与代码结构
This commit is contained in:
小莫唐尼
2026-09-04 16:05:09 +08:00
parent 570c394501
commit 629b1f9219
14 changed files with 676 additions and 1035 deletions
+1 -1
View File
@@ -534,7 +534,7 @@ function handleSubmit() {
投票状态、自己提交投票,页面只传 `vote-name`、监听 `on-vote-success` 提示
- **页面尽量薄**:页面只负责数据请求编排(hooks)、跳转、传参给组件
- **能抽 hooks 的就抽 hooks**:跨页面复用的业务逻辑放 `src/hooks/`(如
`use-maintenance-intercept`、`useScroll`、`useUpload`),auto-import 免 import 直接调用
`useMaintenanceIntercept`、`useScroll`、`useUpload`),auto-import 免 import 直接调用
- 组件不直接依赖 store 的散装字段,通过 props 传入、events 传出,降低耦合
---
+3 -1
View File
@@ -141,7 +141,9 @@ export interface IPublicMaintenance {
status: 'scheduled' | 'active'
/** 维护页标题 */
title?: string
/** 维护说明(文本 HTML,mp-html 渲染) */
/** 维护说明(文本,维护页标题下方直接展示,留空展示默认文案) */
notice?: string
/** 维护详情(富文本 HTML,「维护详情」弹窗内 mp-html 渲染) */
description?: string
/** 维护开始时间(RFC3339 UTC 字符串) */
startTime?: string
+1 -1
View File
@@ -10,7 +10,7 @@
</script>
<template>
<view v-bind="attrs" class="bg-primary uh-shadow-xs text-black text-sm px-4 py-1.5 rounded-lg" :class="props.customClass">
<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">
<slot></slot>
</view>
</template>
@@ -0,0 +1,42 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { markdownConfig } from '@/config/markdown'
const props = withDefaults(defineProps<{
modelValue ?: boolean
content ?: string
}>(), {
modelValue: false,
content: '',
})
const emit = defineEmits<{
(e : 'update:modelValue', value : boolean) : void
}>()
const show = computed({
get: () => props.modelValue,
set: (value : boolean) => {
emit('update:modelValue', value)
},
})
</script>
<template>
<wd-popup v-model="show" position="bottom" :z-index="200" :safe-area-inset-bottom="true" closable
custom-class="rounded-lt-4 rounded-rt-4">
<view class="flex flex-col bg-white">
<view class="box-border px-4 py-4 text-center">
<text class="text-[30rpx] text-[#17181a] font-bold">
维护详情
</text>
</view>
<scroll-view scroll-y
class="box-border max-h-[60vh] px-[32rpx] pb-4 text-[26rpx] text-[#3a3e44] leading-[1.8]">
<mp-html :content="content" lazy-load :domain="markdownConfig.domain ?? ''" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
copy-by-long-press />
</scroll-view>
</view>
</wd-popup>
</template>
@@ -1,12 +1,4 @@
<script lang="ts" setup>
/**
* 通知/最新公告弹窗(源自旧项目 components/notify-dialog,2026-09-03 改造为
* plugin-uni-halo 最新公告自治宿主):挂载后自动拉取 GET /notices/latest,
* 「今日已看」命中(键 notice_latest_{name}_{yyyyMMdd})则不打扰;
* 「不再提醒/查看全文」写今日键;查看全文跳公告详情页。
* 放置:页面模板中直接 <uh-notify-dialog /> 即可(easycom)。
* UIUX 见 .docs/notice-module-client-design.md
*/
import { onMounted, ref } from 'vue'
import { getNoticeLatest } from '@/api/uni-halo'
import { getCache, setCache } from '@/utils/storage'
@@ -89,23 +81,23 @@ onMounted(() => {
<wd-popup
v-model="isShow"
position="center"
width="80vw"
custom-style="border-radius:16rpx;"
custom-class="rounded-xl"
z-index="9999"
@close="handleClose"
>
<view v-if="notice" class="uh-notify-dialog box-border w-[80vw] p-6">
<view v-if="notice" class="box-border w-[80vw] p-6">
<!-- 头部:标题 + 关闭 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<text class="text-[28rpx]">
<text class="text-sm">
📢
</text>
<text class="text-[30rpx] font-bold text-[#333]">
<text class="text-md font-bold text-gray-900">
最新公告
</text>
<view
v-if="notice.typeDisplayName"
class="rounded px-1.5 py-0.5 text-[20rpx]"
class="rounded px-1.5 py-0.5 text-xs"
:style="{
color: notice.typeColor || '#f83856',
backgroundColor: notice.typeColor ? `${notice.typeColor}1a` : '#fdeef1',
@@ -114,7 +106,7 @@ onMounted(() => {
{{ notice.typeDisplayName }}
</view>
</view>
<view class="flex h-8 w-8 items-center justify-center text-[#bbb]" @click="handleClose">
<view class="flex h-8 w-8 items-center justify-center text-gray-500" @click="handleClose">
<wd-icon name="close" size="16px" />
</view>
</view>
@@ -124,29 +116,23 @@ onMounted(() => {
<view class="text-[32rpx] font-bold leading-snug text-[#222]">
{{ notice.title }}
</view>
<view v-if="notice.summary" class="mt-3 text-[26rpx] leading-relaxed text-[#666]">
<view v-if="notice.summary" class="mt-3 text-[26rpx] leading-relaxed text-gray-500">
{{ notice.summary }}
</view>
<view v-if="notice.publishTime" class="mt-3 text-[22rpx] text-[#bbb]">
{{ formatDate(notice.publishTime) }}
<view v-if="notice.publishTime" class="mt-3 text-[22rpx] text-gray-400">
日期{{ formatDate(notice.publishTime) }}
</view>
</view>
<!-- 操作 -->
<view class="mt-6 flex items-center justify-between border-t border-[#f5f5f5] pt-4">
<view class="text-[24rpx] text-[#999]" @click="handleDismissForever">
<view class="box-border mt-4 flex items-center justify-between pt-4">
<view class="text-xs text-gray-600 " @click="handleDismissForever">
今日不再提醒
</view>
<wd-button size="small" type="primary" round @click="handleViewAll">
<uh-button custom-class="font-semibold text-xs" @click="handleViewAll">
查看全文
</wd-button>
</uh-button>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
.uh-notify-dialog {
/* 布局全部由 UnoCSS 原子类实现 */
}
</style>
@@ -11,10 +11,18 @@
function handleScrollTop() {
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
}
// 获取当前页面,并且设置黑名单模式,因为有的页面可能不需要滚动到顶部
const balckList = ['pages/maintenance/maintenance']
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const visible = computed(() => {
return !balckList.includes(currentPage.route)
})
</script>
<template>
<view
<view v-if="visible"
class="uh-global-card-glass border fixed bottom-24 right-4 z-50 h-10 w-10 flex items-center justify-center rounded-full text-primary"
:class="props.customClass" @click="handleScrollTop">
<wd-icon name="arrow-up" size="20px" />
+1 -5
View File
@@ -1,15 +1,11 @@
import type { DataLoadingStatus } from './useDataLoading'
/**
* @deprecated 请改用 useDataLoading:状态由 hook 内部流转,
* 无需再在 try/catch 里手工调用 updateLoadingStatus 搬运状态。
* 本文件保留仅为兼容存量页面(category/article-detail 等),后续逐步迁移。
*/
export const DataLoadingStatusEnum = {
Loading: 'loading',
Error: 'error',
Empty: 'empty',
Success: 'success',
NoNetwork: 'noNetwork',
} as const
export function useDataLoadingStatus(status?: DataLoadingStatus) {
+32 -42
View File
@@ -1,44 +1,37 @@
<script lang="ts" setup>
/**
* 入口页(源自旧项目 pages/index/index.vue,新建复刻)
* 职责:检查插件可用性 + 维护模式 → 重定向维护页 → 获取配置 → 二维码 scene 跳文章 →
* 审计模式 mock → 启动页/首页分流
* 拦截规则(2026-09-04):主插件未激活或维护模式开启(任一命中)均跳转维护页,
* 不再展示 uh-plugin-unavailable 组件;维护页按 from 参数区分原因展示默认/配置文案。
*/
import { onLoad } from '@dcloudio/uni-app'
import { getQRCodeInfo } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { collectSiteDefaults } from '@/utils/preference'
import { onLoad } from '@dcloudio/uni-app'
import { getQRCodeInfo } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { collectSiteDefaults } from '@/utils/preference'
import { useMaintenanceIntercept } from '@/hooks/useMaintenanceIntercept'
definePage({
definePage({
// 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
type: 'home',
style: {
// 'custom' 表示开启自定义导航栏,默认 'default'
navigationStyle: 'custom',
navigationBarTitleText: '初始页面',
},
})
})
/* ---------------- 常量 ---------------- */
const homePagePath = '/pages/tabbar/home/home'
const articleDetailPath = '/pages-blog/article-detail/article-detail'
/* ---------------- 常量 ---------------- */
const homePagePath = '/pages/tabbar/home/home'
const articleDetailPath = '/pages-blog/article-detail/article-detail'
// 本地开发快速跳转页面,发布请置为 false
const DEV_MODE = false
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
const DEV_TO_PATH = `/pages-blog/test/test`
// 本地开发快速跳转页面,发布请置为 false
const DEV_MODE = false
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
const DEV_TO_PATH = `/pages-blog/test/test`
/* ---------------- 状态 ---------------- */
const appConfigStore = useAppConfigStore()
const settingStore = useSettingStore()
/** 维护拦截(插件可用性 + 维护模式检查与跳转封装,见 hooks/use-maintenance-intercept) */
const { interceptOrContinue } = useMaintenanceIntercept()
/* ---------------- 状态 ---------------- */
const appConfigStore = useAppConfigStore()
const settingStore = useSettingStore()
// 维护拦截
const { interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept()
/** 通过二维码 scene 获取文章 id */
async function getPostIdByQRCode(key: string): Promise<string | null> {
/** 通过二维码 scene 获取文章 id */
async function getPostIdByQRCode(key : string) : Promise<string | null> {
try {
const response = await getQRCodeInfo(key)
if (response.data?.postId)
@@ -48,9 +41,9 @@ async function getPostIdByQRCode(key: string): Promise<string | null> {
console.error('二维码解析失败', err)
}
return null
}
}
onLoad(async (options) => {
onLoad(async (options) => {
// 本地开发,快速跳转页面,发布请设置 DEV_MODE = false
if (DEV_MODE && DEV_TO_PATH) {
if (DEV_TO_TYPE === 'tabbar') {
@@ -62,12 +55,7 @@ onLoad(async (options) => {
return
}
// 拦截:主插件未激活 或 维护模式开启(任一命中)→ 跳转维护页
if (await interceptOrContinue())
return
// 获取配置(统一 bootstrap: getConfigs + audit-data + love-config 并行一次;
// TTL 内直接返回缓存。设计见 .docs/static-config-unified-fetch-design.md)
// 获取配置
try {
const { ok } = await appConfigStore.bootstrap()
if (!ok) {
@@ -87,20 +75,22 @@ onLoad(async (options) => {
}
}
// 两层偏好合并:应用站点默认(L0)到 setting store(内部合并本地差异,含旧数据迁移);
// 审核模式数据已随 bootstrap 拉取(auditData/auditModeEnabled 即可用)
settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs))
// 启动页已下线(v2.2 ⑤):直接进首
// 拦截:主插件未激活 或 维护模式开启(任一命中)→ 跳转维护
if (await interceptOrContinue()) { return }
uni.switchTab({ url: homePagePath })
}
catch (err) {
console.error('入口页初始化失败', err)
uni.switchTab({ url: homePagePath })
redirectToMaintenance()
}
})
})
</script>
<template>
<view class="app-page h-screen w-screen flex items-center justify-center" style="background-color: #fff;" />
<view>
<!-- 初始化页面 -->
</view>
</template>
+117 -484
View File
@@ -3,7 +3,6 @@
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { useAppConfigStore } from '@/store/appConfig'
import { checkUrl } from '@/utils/url'
import { markdownConfig } from '@/config/markdown'
import { usePluginAvailable } from '@/utils/plugin'
import type { IPublicMaintenance } from '@/api/types/uni-halo'
@@ -34,6 +33,8 @@
const spinning = ref(false)
/** Toast 文案(空 = 隐藏) */
const toast = ref('')
/** 「维护详情」弹窗显隐 */
const showDetail = ref(false)
let timer : ReturnType<typeof setInterval> | null = null
let recoveryTimer : ReturnType<typeof setInterval> | null = null
let toastTimer : ReturnType<typeof setTimeout> | null = null
@@ -42,8 +43,10 @@
const isIntercepted = computed(() => !!fromReason.value)
const isScheduled = computed(() => maintenance.value?.status === 'scheduled')
const title = computed(() => maintenance.value?.title || DEFAULT_MAINTENANCE_TITLE)
/** 配置的富文本说明(未配置时展示设计稿默认文案) */
const description = computed(() => maintenance.value?.description || '')
/** 维护详情(富文本 HTML;小程序端「维护详情」弹窗内 mp-html 渲染) */
const detailHtml = computed(() => maintenance.value?.description || '')
/** 维护说明(纯文本;标题下方按行直接展示,留空展示设计稿默认文案) */
const noticeLines = computed(() => (maintenance.value?.notice || '').split('\n'))
/** 应用信息 logo(相对插件内置资源路径 → BASE_API 补全) */
const appLogo = computed(() => {
@@ -137,7 +140,8 @@
* 拉取失败(服务器停机)保持维护页不打扰。
*/
async function silentCheck() {
if (refreshing) { return }
if (refreshing)
return
refreshing = true
try {
await store.bootstrap({ force: true })
@@ -189,14 +193,12 @@
startRecoveryCheck()
}
else if (isIntercepted.value) {
// 拦截场景无维护信息(插件未激活/未配置)→ 默认文案
maintenance.value = null
viewState.value = 'maintenance'
startRecoveryCheck()
}
else if (ok) {
// 拉取成功但无 maintenance 键:未维护(或已到点自动结束)→ 服务正常
viewState.value = 'normal'
goIndex()
}
else {
viewState.value = 'error'
@@ -251,7 +253,7 @@
onLoad((options) => {
const from = options?.from
fromReason.value = from === 'plugin' || from === 'maintenance' ? from : null
void load()
load()
})
onUnload(() => {
@@ -264,142 +266,153 @@
</script>
<template>
<view class="maintenance-page">
<!-- 加载中 -->
<view v-if="viewState === 'loading'" class="flex flex-col items-center justify-center py-48">
<text class="text-[26rpx] text-[#999]">
加载中...
</text>
<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 v-else-if="viewState === 'normal'"
class="flex flex-col items-center justify-center px-10 py-48 text-center">
<text class="text-[64rpx]">
</text>
<text class="mt-6 text-[28rpx] text-[#333]">
当前服务正常无需维护
</text>
<text class="mt-2 text-[24rpx] text-[#999]">
如果仍然无法访问请稍后重试或联系站长
</text>
<view class="mt-10">
<wd-button type="primary" round @click="goIndex">
返回首页
</wd-button>
</view>
</view>
<!-- 刷新 -->
<uh-data-loading v-if="viewState === 'loading'" />
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
<view v-else-if="viewState === 'error'"
class="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="120rpx" />
</text>
<text class="mt-6 text-[28rpx] text-[#333]">
<text class="mt-6 text-md text-gray-900 font-bold">
服务暂时无法访问
</text>
<text class="mt-2 text-[24rpx] text-[#999]">
<text class="mt-3 text-xs text-gray-500">
站点可能正在维护中请稍后重试
</text>
<view class="mt-10">
<wd-button type="primary" plain round @click="load(true)">
重新加载
</wd-button>
<uh-button custom-class="uh-global-card-glass border flex-1 py-2 !px-8 !rounded-full font-semibold"
@click="load(true)">刷新试试</uh-button>
</view>
</view>
<!-- 维护页(参考 .design/维护页面.html) -->
<view v-else class="app-container">
<!-- ===== Hero(无边界淡出) ===== -->
<view class="hero">
<view class="blob breathe hero-blob-1 b-white" />
<view class="blob b-acc hero-blob-2" />
<view class="blob hero-blob-3 b-white" />
<!-- 维护页-->
<view v-else class="relative z-10 w-full min-h-screen flex items-center justify-center flex-col ">
<view
class="w-full relative overflow-hidden px-[40rpx] pb-[64rpx] pt-[calc(var(--status-bar-height)+76rpx)] text-center">
<view
class="breathe pointer-events-none absolute left-[-60rpx] top-[104rpx] z-0 h-[260rpx] w-[260rpx] rounded-full bg-white/40 blur-[52rpx]" />
<view
class="pointer-events-none absolute right-[-48rpx] top-[40rpx] z-0 h-[200rpx] w-[200rpx] rounded-full bg-[rgba(184,236,63,0.28)] blur-[52rpx]" />
<view
class="pointer-events-none absolute bottom-[-40rpx] right-[72rpx] z-0 h-[160rpx] w-[160rpx] rounded-full bg-white/40 blur-[52rpx]" />
<view class="medal">
<view class="medal-bg" />
<image v-if="appLogo" class="medal-img" :src="appLogo" mode="aspectFill" />
<wd-icon v-else class="medal-emoji" name="tool" size="52px" />
<view class="gear gear-1">
<view class="relative z-2 mx-auto mt-[44rpx] h-[236rpx] w-[236rpx]">
<view
class="bob flex items-center justify-center absolute inset-0 rounded-full from-[#ebfabf] to-[#b8ec3f] bg-gradient-to-br shadow-[0_0_0_12rpx_#fff,0_28rpx_60rpx_rgba(98,124,44,0.22)]">
<image v-if="appLogo" class="h-full w-full rounded-full" :src="appLogo" mode="aspectFill" />
<wd-icon v-else class="flex items-center justify-center text-gray-900" name="tool"
size="120rpx" />
</view>
<view
class="gear-spin absolute right-[-28rpx] top-[-16rpx] h-[68rpx] w-[68rpx] flex items-center justify-center">
<wd-icon name="settings" size="24px" />
</view>
<view class="gear gear-2">
<view
class="gear-spin-reverse absolute bottom-[16rpx] left-[-32rpx] h-[48rpx] w-[48rpx] flex items-center justify-center">
<wd-icon name="settings" size="16px" />
</view>
<view class="sticker st-acc medal-st">
<view
class="absolute bottom-[-28rpx] left-1/2 inline-flex items-center whitespace-nowrap border-2 border-solid border-[#ebfabf] rounded-full bg-[#ebfabf] px-[24rpx] py-[12rpx] text-[22rpx] font-extrabold leading-none shadow-[0_10rpx_28rpx_rgba(90,110,45,0.18)] -translate-x-1/2 -rotate-5">
MAINTENANCE
</view>
</view>
<view class="hero-h1">
<view class="relative z-2 mt-[64rpx] text-[54rpx] font-black leading-[64rpx]">
{{ title }}
</view>
<view class="doodle" />
<view
class="relative z-2 mx-auto mt-[8rpx] h-[8rpx] w-[224rpx] rounded-full from-transparent via-[#a7e93b] to-transparent bg-gradient-to-r" />
<view class="hero-sub">
<mp-html v-if="description" :content="description" lazy-load :domain="markdownConfig.domain ?? ''"
scroll-table selectable :tag-style="markdownConfig.tagStyle"
:container-style="markdownConfig.containStyle" copy-by-long-press />
<view
class="relative z-2 mt-[20rpx] px-[16rpx] text-[25rpx] text-[rgba(23,24,26,0.55)] font-medium leading-[1.7]">
<template v-if="noticeLines.length > 0">
<text v-for="(line, index) in noticeLines" :key="index" class="block">{{ line }}</text>
</template>
<template v-else>
<text>为了给你带来更好的体验站点正在维护升级中</text>
<text class="block">
别担心你的数据都安然无恙 💚
别担心我们很快就回来
</text>
</template>
</view>
</view>
<!-- ===== 内容区 ===== -->
<view class="layer">
<view class="bridge bridge-1" />
<view class="bridge bridge-2" />
<view class="relative z-10 w-full">
<view
class="pointer-events-none absolute left-[64rpx] top-[-52rpx] z-0 h-[192rpx] w-[192rpx] rounded-full bg-[#ebfabf] opacity-90 blur-[44rpx]" />
<view
class="pointer-events-none absolute right-[-52rpx] top-[300rpx] z-0 h-[168rpx] w-[168rpx] rounded-full bg-[#ffd53d] opacity-20 blur-[44rpx]" />
<view class="content">
<view
class="relative box-border w-full z-1 flex flex-col items-center gap-[28rpx] px-6 pb-[68rpx] pt-[32rpx]">
<!-- 恢复倒计时 -->
<view v-if="countdownParts" class="eta-card">
<view class="eta-wm">
<view v-if="countdownParts"
class="relative w-full uh-global-card-glass overflow-hidden border rounded-2xl p-[32rpx] text-center shadow-[0_4rpx_24rpx_rgba(98,124,44,0.08)]">
<view
class="absolute bottom-[-48rpx] right-[-12rpx] text-[176rpx] text-[#a7e93b] font-black leading-none opacity-10">
GO!
</view>
<view class="eta-k">
<wd-icon name="clock-circle" size="14px" />
<view
class="inline-flex items-center justify-center text-xs text-gray-500 font-extrabold tracking-[4rpx]">
{{ countdownPrefix }}
</view>
<view class="eta-clock">
<view class="eta-cell hot">
<text class="cell-num">{{ countdownParts.days }}</text>
<text class="cell-unit"> DAY</text>
<view class="mt-4 w-full flex justify-center gap-4">
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
<text
class="block text-[42rpx] text-[#17181a] font-black leading-none tabular-nums">{{ countdownParts.days }}</text>
<text
class="mt-[10rpx] block text-[18rpx] text-[#8a9099] font-extrabold tracking-[1px]">
DAY</text>
</view>
<view class="eta-cell">
<text class="cell-num">{{ countdownParts.hours }}</text>
<text class="cell-unit"> HR</text>
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
<text
class="block text-[42rpx] text-primary font-black leading-none tabular-nums">{{ countdownParts.hours }}</text>
<text
class="mt-[10rpx] block text-[18rpx] text-[#8a9099] font-extrabold tracking-[1px]">
HR</text>
</view>
<view class="eta-cell">
<text class="cell-num">{{ countdownParts.minutes }}</text>
<text class="cell-unit"> MIN</text>
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
<text
class="block text-[42rpx] text-[#17181a] font-black leading-none tabular-nums">{{ countdownParts.minutes }}</text>
<text
class="mt-[10rpx] block text-[18rpx] text-[#8a9099] font-extrabold tracking-[1px]">
MIN</text>
</view>
<view class="eta-cell">
<text class="cell-num">{{ countdownParts.seconds }}</text>
<text class="cell-unit"> SEC</text>
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
<text
class="block text-[42rpx] text-[#17181a] font-black leading-none tabular-nums">{{ countdownParts.seconds }}</text>
<text
class="mt-[10rpx] block text-[18rpx] text-[#8a9099] font-extrabold tracking-[1px]">
SEC</text>
</view>
</view>
<view v-if="etaNote" class="eta-note">
<view v-if="etaNote" class="mt-[24rpx] text-[21rpx] text-primary font-medium">
{{ etaNote }}
</view>
</view>
<!-- 操作 -->
<view class="cta-row">
<view class="btn-refresh ink-btn press" @click="handleRefresh">
<wd-icon class="refresh-ic" :class="{ spinning }" name="refresh" size="17px" />
<text>刷新看看</text>
<view class="mt-2 w-full flex flex-col gap-4">
<uh-button custom-class="uh-global-card-glass border flex-1 py-3 !rounded-full font-semibold"
@click="handleRefresh">刷新看看</uh-button>
<view v-if="detailHtml"
class="uh-global-card-glass py-2 flex flex-1 items-center justify-center rounded-full bg-white text-sm text-primary font-extrabold uh-shadow-xs"
@click="showDetail = true">
维护详情
</view>
</view>
<!-- 页脚 -->
<view class="foot">
<view class="foot-text">
<view class="mt-[12rpx] flex flex-col items-center">
<view class="mt-[18rpx] text-center text-[21rpx] text-[#c9cdd4] font-medium leading-[1.7]">
升级期间给你带来不便非常抱歉
<text class="block">
去喝杯奶茶等等吧
@@ -410,70 +423,19 @@
</view>
<!-- Toast -->
<view v-if="toast" class="toast">
<view v-if="toast"
class="uh-global-card-glass border fixed bottom-[calc(72rpx+env(safe-area-inset-bottom))] left-1/2 z-60 whitespace-nowrap rounded-full px-6 py-2 text-xs font-bold -translate-x-1/2">
{{ toast }}
</view>
<!-- 维护详情弹窗(复用封装组件, components/uh-maintenance-detail) -->
<uh-maintenance-detail v-model="showDetail" :content="detailHtml" />
</view>
</view>
</template>
<style scoped lang="scss">
$ink: #17181a;
$ink2: #8a9099;
$ink3: #c9cdd4;
$sun: #ffd53d;
$acc: #b8ec3f;
$acc-deep: #a7e93b;
$acc-soft: #ebfabf;
$acc-pale: #f4fbe0;
$acc-ink: #63a002;
$hero: linear-gradient(175deg, #d9f77f 0%, #e8fbaf 52%, #f5fae8 100%);
$page: #f7f9f1;
$glow: rgba(168, 232, 52, 0.5);
.maintenance-page {
min-height: 100vh;
background: #e9edf1;
}
/* ========== 应用容器 ========== */
.app-container {
max-width: 960rpx;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: $page;
box-shadow: 0 0 60rpx rgba(50, 60, 25, 0.12);
}
/* ========== Hero ========== */
.hero {
position: relative;
overflow: hidden;
flex-shrink: 0;
padding: calc(var(--status-bar-height) + 76rpx) 40rpx 64rpx;
background: $hero;
text-align: center;
}
.blob {
position: absolute;
border-radius: 50%;
filter: blur(52rpx);
pointer-events: none;
z-index: 0;
}
.b-white {
background: rgba(255, 255, 255, 0.4);
}
.b-acc {
background: $acc;
opacity: 0.28;
}
/* 光斑呼吸 */
.breathe {
animation: breathe 5s ease-in-out infinite;
}
@@ -492,65 +454,11 @@
}
}
.hero-blob-1 {
left: -60rpx;
top: 104rpx;
width: 260rpx;
height: 260rpx;
}
.hero-blob-2 {
right: -48rpx;
top: 40rpx;
width: 200rpx;
height: 200rpx;
}
.hero-blob-3 {
right: 72rpx;
bottom: -40rpx;
width: 160rpx;
height: 160rpx;
}
/* 勋章 */
.medal {
position: relative;
width: 236rpx;
height: 236rpx;
margin: 44rpx auto 0;
z-index: 2;
}
.medal-bg {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(135deg, $acc-soft, $acc);
box-shadow:
0 0 0 12rpx #fff,
0 28rpx 60rpx rgba(98, 124, 44, 0.22);
/* 勋章浮动 */
.bob {
animation: bob 3.2s ease-in-out infinite;
}
.medal-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: 50%;
z-index: 1;
}
.medal-emoji {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: $ink;
}
@keyframes bob {
0%,
@@ -563,251 +471,22 @@
}
}
.gear {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
/* 齿轮旋转(正/反两档;keyframes 会被 scoped 重命名,须自定义承载类) */
.gear-spin {
animation: spin 6s linear infinite;
}
.gear-spin-reverse {
animation: spin 4.5s linear infinite reverse;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.gear-1 {
width: 68rpx;
height: 68rpx;
right: -28rpx;
top: -16rpx;
}
.gear-2 {
width: 48rpx;
height: 48rpx;
left: -32rpx;
bottom: 16rpx;
animation-duration: 4.5s;
animation-direction: reverse;
}
.sticker {
display: inline-flex;
align-items: center;
background: #fff;
border: 4rpx solid #fff;
border-radius: 999rpx;
box-shadow: 0 10rpx 28rpx rgba(90, 110, 45, 0.18);
font-weight: 800;
line-height: 1;
white-space: nowrap;
position: relative;
z-index: 2;
}
.st-acc {
background: $acc-soft;
border-color: $acc-soft;
}
.medal-st {
position: absolute;
left: 50%;
transform: translateX(-50%) rotate(-5deg);
bottom: -28rpx;
font-size: 22rpx;
padding: 12rpx 24rpx;
}
.rot-l {
transform: rotate(-7deg);
}
/* 标题与副标题 */
.hero-h1 {
margin-top: 64rpx;
font-size: 54rpx;
line-height: 64rpx;
font-weight: 900;
position: relative;
z-index: 2;
}
.doodle {
width: 224rpx;
height: 8rpx;
margin: 8rpx auto 0;
border-radius: 999rpx;
background: linear-gradient(90deg, transparent, $acc-deep, transparent);
position: relative;
z-index: 2;
}
.hero-sub {
margin-top: 20rpx;
font-size: 25rpx;
font-weight: 500;
color: rgba(23, 24, 26, 0.55);
position: relative;
z-index: 2;
line-height: 1.7;
}
/* ========== 内容区 ========== */
.layer {
position: relative;
flex: 1;
}
.layer> :not(.bridge) {
position: relative;
z-index: 1;
}
.bridge {
position: absolute;
border-radius: 999rpx;
filter: blur(44rpx);
pointer-events: none;
z-index: 0;
}
.bridge-1 {
top: -52rpx;
left: 64rpx;
width: 192rpx;
height: 192rpx;
background: $acc-soft;
opacity: 0.9;
}
.bridge-2 {
top: 300rpx;
right: -52rpx;
width: 168rpx;
height: 168rpx;
background: $sun;
opacity: 0.2;
}
.content {
padding: 32rpx 32rpx 68rpx;
display: flex;
flex-direction: column;
gap: 28rpx;
align-items: center;
}
/* 倒计时卡 */
.eta-card {
border-radius: 36rpx;
padding: 36rpx 32rpx;
text-align: center;
position: relative;
overflow: hidden;
width: 100%;
background: #fff;
border: 1rpx solid rgba(112, 138, 42, 0.08);
box-shadow: 0 4rpx 24rpx rgba(98, 124, 44, 0.08);
}
.eta-wm {
position: absolute;
right: -12rpx;
bottom: -48rpx;
font-size: 176rpx;
font-weight: 900;
color: $acc-deep;
opacity: 0.1;
line-height: 1;
pointer-events: none;
}
.eta-k {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 20rpx;
font-weight: 800;
letter-spacing: 2px;
color: $ink2;
}
.eta-clock {
margin-top: 22rpx;
display: flex;
justify-content: center;
gap: 10rpx;
}
.eta-cell {
min-width: 118rpx;
padding: 18rpx 8rpx 16rpx;
border-radius: 24rpx;
background: $acc-pale;
}
.cell-num {
display: block;
font-size: 42rpx;
font-weight: 900;
line-height: 1;
color: $ink;
font-variant-numeric: tabular-nums;
}
.eta-cell.hot .cell-num {
color: $acc-ink;
}
.cell-unit {
display: block;
margin-top: 10rpx;
font-size: 18rpx;
font-weight: 800;
color: $ink2;
letter-spacing: 1px;
}
.eta-note {
margin-top: 24rpx;
font-size: 21rpx;
color: $ink3;
font-weight: 500;
}
/* 操作区 */
.cta-row {
display: flex;
gap: 20rpx;
width: 100%;
margin-top: 4rpx;
}
.btn-refresh {
flex: 1;
height: 100rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.ink-btn {
color: #fff;
background: linear-gradient(150deg, #3a3e44 0%, #1d1f22 55%, #151619 100%);
box-shadow:
0 10rpx 28rpx -10rpx rgba(21, 23, 25, 0.4),
0 12rpx 36rpx -10rpx $glow,
inset 0 2rpx 0 rgba(255, 255, 255, 0.16);
}
/* 刷新图标点击转圈 */
.refresh-ic {
display: inline-block;
}
@@ -816,6 +495,7 @@
animation: spin 0.7s linear;
}
/* 按压反馈 */
.press {
transition: transform 0.12s ease;
}
@@ -823,51 +503,4 @@
.press:active {
transform: scale(0.94);
}
/* 页脚 */
.foot {
margin-top: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.foot .sticker {
font-size: 44rpx;
padding: 14rpx 22rpx;
}
.foot-text {
margin-top: 18rpx;
font-size: 21rpx;
color: $ink3;
font-weight: 500;
text-align: center;
line-height: 1.7;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: calc(72rpx + env(safe-area-inset-bottom));
transform: translateX(-50%);
padding: 18rpx 36rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 700;
color: #fff;
background: linear-gradient(150deg, #3a3e44 0%, #1d1f22 55%, #151619 100%);
box-shadow:
0 10rpx 28rpx -10rpx rgba(21, 23, 25, 0.4),
0 12rpx 36rpx -10rpx $glow,
inset 0 2rpx 0 rgba(255, 255, 255, 0.16);
white-space: nowrap;
z-index: 60;
}
:deep(img) {
max-width: 100%;
border-radius: 8rpx;
}
</style>
+46 -51
View File
@@ -1,55 +1,55 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkThumbnailUrl } from '@/utils/url'
import { sleep } from '@/utils/common'
import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ICategory } from '@/api/types/halo'
import { computed, ref } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkThumbnailUrl } from '@/utils/url'
import { sleep } from '@/utils/common'
import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ICategory } from '@/api/types/halo'
definePage({
definePage({
style: {
navigationBarTitleText: '分类',
enablePullDownRefresh: true,
backgroundColor: '#f6f3ee',
},
})
})
const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
/* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({
/* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const queryParams = ref({
size: 20,
page: 1,
fieldSelector: ['spec.hideFromList=false'],
})
const hasNext = ref(false)
const dataList = ref<ICategory[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading'))
})
const hasNext = ref(false)
const dataList = ref<ICategory[]>([])
const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading'))
function handleResetInit() {
function handleResetInit() {
dataList.value = []
queryParams.value.page = 1
hasNext.value = false
isLoadMore.value = false
loadMoreText.value = t('common.loading')
}
}
function handleInitPage() {
function handleInitPage() {
handleResetInit()
handleGetData()
}
}
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
/* ---------------- 数据加载 ---------------- */
async function handleGetData() {
updateLoadingStatus(DataLoadingStatusEnum.Loading)
// 增加延迟,提升用户体验
await sleep(800)
@@ -68,7 +68,7 @@ async function handleGetData() {
const orderMap = new Map(auditCategoryNames.map((name, index) => [name, index]))
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
dataList.value = filtered
updateLoadingStatus(DataLoadingStatusEnum.Success)
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
loadMoreText.value = t('common.noMore')
uni.hideLoading()
uni.stopPullDownRefresh()
@@ -89,7 +89,6 @@ async function handleGetData() {
try {
const res = await getCategoryList({ ...queryParams.value })
updateLoadingStatus(DataLoadingStatusEnum.Success)
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
hasNext.value = res.data.hasNext
@@ -102,6 +101,7 @@ async function handleGetData() {
dataList.value = isLoadMore.value
? dataList.value.concat(tempItems)
: tempItems
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
}
catch (err) {
console.error(err)
@@ -114,29 +114,28 @@ async function handleGetData() {
uni.stopPullDownRefresh()
}, 500)
}
}
}
function handleToCategory(category: ICategory) {
function handleToCategory(category : ICategory) {
if (calcAuditModeEnabled.value) {
return
}
uni.navigateTo({
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
})
}
}
/* ---------------- 生命周期 ---------------- */
onMounted(() => {
onMounted(() => {
handleInitPage()
})
})
onPullDownRefresh(() => {
onPullDownRefresh(() => {
handleResetInit()
handleGetData()
})
})
onReachBottom(() => {
onReachBottom(() => {
if (calcAuditModeEnabled.value) {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
return
@@ -149,25 +148,21 @@ onReachBottom(() => {
else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
}
})
})
</script>
<template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page p-3">
<!-- 骨架屏 -->
<view v-if="loadingStatus !== DataLoadingStatusEnum.Success">
<uh-data-loading :loading-status="loadingStatus" />
</view>
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" />
<block v-else>
<view class="grid grid-cols-2 gap-3">
<view
v-for="(item, index) in dataList" :key="index"
<view class="grid grid-cols-2 gap-2.5">
<view v-for="(item, index) in dataList" :key="index"
class="uh-global-card-glass relative box-border w-full overflow-hidden rounded-xl"
@click="handleToCategory(item)"
>
@click="handleToCategory(item)">
<image v-if="item.spec.cover" class="block h-32 w-full" :src="item.spec.cover" mode="aspectFill" />
<view class="absolute bottom-0 left-0 h-[140rpx] w-full from-black/0 to-black/30 bg-gradient-to-b" />
<view
class="absolute bottom-0 left-0 h-[140rpx] w-full from-black/0 to-black/30 bg-gradient-to-b" />
<view class="absolute bottom-0 left-0 box-border w-full flex flex-col gap-1 p-2.5">
<text class="truncate text-sm text-white font-bold">
{{ item.spec.displayName }}
+62 -79
View File
@@ -1,49 +1,49 @@
<script lang="ts" setup>
/**
/**
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
*/
import { computed, ref, watch } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url'
import { t } from '@/locale'
import { usePluginAvailable } from '@/utils/plugin'
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
import { computed, ref, watch } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url'
import { t } from '@/locale'
import { usePluginAvailable } from '@/utils/plugin'
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
definePage({
definePage({
style: {
navigationBarTitleText: '图库',
enablePullDownRefresh: true,
},
})
})
const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const appConfigStore = useAppConfigStore()
const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
/** 依赖插件(plugin-photos) */
const uniHaloPluginId = 'plugin-photos'
const uniHaloPluginAvailable = ref(true)
/** 依赖插件(plugin-photos) */
const uniHaloPluginId = 'plugin-photos'
const uniHaloPluginAvailable = ref(true)
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading')
const category = ref<{ activeIndex: number, list: IPhotoGroup[] }>({
/* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error' | 'empty'>('loading')
const category = ref<{ activeIndex : number, list : IPhotoGroup[] }>({
activeIndex: 0,
list: [],
})
const queryParams = ref({ size: 10, page: 1, group: '' })
const isLoadMore = ref(false)
const loadMoreText = ref('')
const hasNext = ref(false)
const dataList = ref<IPhoto[]>([])
const lock = ref(false)
})
const queryParams = ref({ size: 10, page: 1, group: '' })
const isLoadMore = ref(false)
const loadMoreText = ref('')
const hasNext = ref(false)
const dataList = ref<IPhoto[]>([])
const lock = ref(false)
/* ---------------- 数据加载 ---------------- */
async function handleGetCategory() {
/* ---------------- 数据加载 ---------------- */
async function handleGetCategory() {
if (calcAuditModeEnabled.value) {
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
@@ -85,9 +85,9 @@ async function handleGetCategory() {
loading.value = 'error'
category.value = { activeIndex: 0, list: [] }
}
}
}
async function handleGetData(isClearList = false) {
async function handleGetData(isClearList = false) {
if (isClearList) {
dataList.value = []
queryParams.value.page = 1
@@ -101,7 +101,6 @@ async function handleGetData(isClearList = false) {
try {
const res = await getPhotoListByGroupName({ ...queryParams.value })
hasNext.value = res.data.hasNext
loading.value = 'success'
if (res.data.items.length !== 0) {
const list = res.data.items.map(item => ({
...item,
@@ -111,6 +110,7 @@ async function handleGetData(isClearList = false) {
? dataList.value.concat(list)
: list
}
loading.value = dataList.value.length !== 0 ? 'success' : 'empty'
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
}
catch (err) {
@@ -125,19 +125,19 @@ async function handleGetData(isClearList = false) {
lock.value = false
}, 500)
}
}
}
function handleGetDataByCategory(index: number, cate: IPhotoGroup) {
function handleGetDataByCategory(index : number, cate : IPhotoGroup) {
queryParams.value.group = cate.metadata.name || ''
queryParams.value.page = 1
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
dataList.value = []
category.value.activeIndex = index
handleGetData(true)
}
}
/* ---------------- 图片预览 ---------------- */
function handlePreview(data: IPhoto) {
/* ---------------- 图片预览 ---------------- */
function handlePreview(data : IPhoto) {
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
uni.previewImage({
current,
@@ -145,26 +145,26 @@ function handlePreview(data: IPhoto) {
indicator: 'number',
loop: true,
})
}
}
/* ---------------- 生命周期 ---------------- */
onLoad(async () => {
/* ---------------- 生命周期 ---------------- */
onLoad(async () => {
// 检查插件可用性
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh()
return
}
})
})
watch(galleryConfig, (newVal) => {
watch(galleryConfig, (newVal) => {
if (!newVal)
return
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
handleGetCategory()
}, { deep: true, immediate: true })
}, { deep: true, immediate: true })
onPullDownRefresh(() => {
onPullDownRefresh(() => {
if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh()
return
@@ -173,9 +173,9 @@ onPullDownRefresh(() => {
isLoadMore.value = false
queryParams.value.page = 1
handleGetData(true)
})
})
onReachBottom(() => {
onReachBottom(() => {
if (!uniHaloPluginAvailable.value)
return
if (calcAuditModeEnabled.value) {
@@ -190,60 +190,43 @@ onReachBottom(() => {
else {
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
}
})
})
</script>
<template>
<view class="min-h-screen w-screen flex flex-col bg-page pb-6">
<uh-plugin-unavailable
v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
error-text="检测到当前插件没有安装或者启用无法使用图库功能哦请联系管理员" @on-refresh="handleGetCategory"
/>
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
error-text="检测到当前插件没有安装或者启用无法使用图库功能哦请联系管理员" @on-refresh="handleGetCategory" />
<template v-else>
<wd-sticky>
<wd-sticky v-if="category.list.length!==0">
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
<view
v-for="(cate, index) in category.list" :key="cate.spec.displayName"
<view v-for="(cate, index) in category.list" :key="cate.spec.displayName"
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-block border rounded-2xl px-4 py-1 text-sm"
:class="{
'bg-primary text-gray-900 font-bold': index === category.activeIndex,
}" @click="handleGetDataByCategory(index, cate)"
>
{{ cate.spec.displayName }}({{ cate.status?.photoCount ?? 0 }})
:class="{ 'bg-primary text-gray-900 font-bold': index === category.activeIndex }"
@click="handleGetDataByCategory(index, cate)">
{{ cate.spec.displayName }} <text
v-if="cate.spec.displayName!=='全部'">({{ cate.status?.photoCount ?? 0 }})</text>
</view>
</scroll-view>
</wd-sticky>
<!-- 加载/错误占位(统一 uh-data-loading,错误可重试) -->
<!-- 加载/错误占位 -->
<view v-if="loading !== 'success'" class="box-border p-3">
<uh-data-loading :loading-status="loading" @refresh="handleGetCategory" />
</view>
<!-- 内容区域 -->
<view v-else class="box-border w-full p-3">
<view
v-if="dataList.length === 0"
class="h-[70vh] w-full flex items-center justify-center content-empty"
>
<wd-empty description="博主还没有分享图片~" />
</view>
<block v-else>
<!-- 瀑布流(双列) -->
<view class="grid grid-cols-2 gap-3">
<view
v-for="(item, index) in dataList" :key="index"
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl"
>
<image
class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load
@click="handlePreview(item)"
/>
<view class="grid grid-cols-2 gap-2.5">
<view v-for="(item, index) in dataList" :key="index"
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl">
<image class="h-full w-full" :src="item.spec.url" mode="aspectFill" lazy-load
@click="handlePreview(item)" />
</view>
</view>
<view class="load-text w-full py-4 text-center text-xs text-gray-500">
{{ loadMoreText }}
</view>
</block>
</view>
</template>
</view>
+9 -7
View File
@@ -6,6 +6,7 @@
import { useSettingStore } from '@/store/setting'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { t } from '@/locale'
import { useMaintenanceIntercept } from '@/hooks/useMaintenanceIntercept'
import type { IPost } from '@/api/types/halo'
definePage({
@@ -147,11 +148,19 @@
})
}
function init(){
if (!intercepted.value) {
handleQuery()
}
}
init()
/* ---------------- 生命周期 ---------------- */
// 维护检查
onShow(async () => {
intercepted.value = await interceptOrContinue()
console.log('拦截状态', intercepted.value)
})
onPullDownRefresh(() => {
@@ -175,13 +184,6 @@
}
})
// 首次加载
onMounted(() => {
if (intercepted.value) {
return
}
handleQuery()
})
</script>
<template>
+21 -17
View File
@@ -274,7 +274,7 @@
</script>
<template>
<view class="box-border min-h-screen w-screen flex flex-col bg-page py-4">
<view class="box-border min-h-screen w-screen flex flex-col bg-page py-3">
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
error-text="检测到当前插件没有安装或者启用无法使用瞬间功能哦请联系管理员" @on-refresh="handleGetData" />
<template v-else>
@@ -282,7 +282,7 @@
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
@refresh="handleGetData" />
<view v-else class="flex flex-col gap-3 px-4">
<view v-else class="flex flex-col gap-3 px-3">
<view v-if="dataList.length === 0"
class="min-h-[70vh] w-full flex items-center justify-center content-empty">
<wd-empty :description="t('common.empty')" />
@@ -290,7 +290,7 @@
<block v-else>
<!-- 瞬间卡片(社交信息流:着色昵称 + 朋友圈式不缩进正文 + 媒体九宫格 + 内嵌互动脚注) -->
<view v-for="(moment, mIndex) in dataList" :key="moment.metadata.name"
<view v-for="moment in dataList" :key="moment.metadata.name"
class="moment-card uh-shadow-xs overflow-hidden rounded-[24rpx] bg-white">
<!-- 作者 -->
<view class="box-border flex items-center px-4 pt-4">
@@ -313,16 +313,8 @@
</view>
<!-- 正文-->
<view class="moment-content px-4 pt-3 ">
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
class="mb-3 flex flex-wrap gap-x-2">
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
class="py-1 px-2 text-xs rounded-xl bg-secondary">
# {{ tag }}
</text>
</view>
<view class="box-border px-4 pt-3">
<view class="relative box-border bg-gray-100 p-2 rounded-lg">
<mp-html lazy-load :domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
@@ -330,6 +322,7 @@
:show-language-name="true" copy-by-long-press
@click.stop="handleToMomentDetail(moment)" />
</view>
</view>
<!-- 图片 -->
<view v-if="moment.images && moment.images.length !== 0"
@@ -360,19 +353,30 @@
@ended="onVideoEnded" />
</view>
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
class="box-border px-4 mt-4 flex flex-wrap gap-2">
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
class="py-1 px-2 text-xs rounded-xl bg-secondary">
# {{ tag }}
</text>
</view>
<!-- (点赞/评论) -->
<view
class="mt-3 mb-1 box-border w-full flex items-center justify-center gap-x-12 border-t border-black/5 py-3 text-xs text-gray-400">
<view class="flex items-center gap-x-2">
class="mt-2 mb-1 box-border w-full flex items-center justify-between gap-x-12 border-t border-black/5 py-3 px-4 text-xs text-gray-400">
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text>
</view>
<view class="flex items-center gap-x-2">
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
<text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text>
</view>
<view class="flex items-center gap-x-1">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
<text class="text-sm text-gray-600">收藏</text>
</view>
</view>
</view>
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">