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:
@@ -534,7 +534,7 @@ function handleSubmit() {
|
|||||||
投票状态、自己提交投票,页面只传 `vote-name`、监听 `on-vote-success` 提示
|
投票状态、自己提交投票,页面只传 `vote-name`、监听 `on-vote-success` 提示
|
||||||
- **页面尽量薄**:页面只负责数据请求编排(hooks)、跳转、传参给组件
|
- **页面尽量薄**:页面只负责数据请求编排(hooks)、跳转、传参给组件
|
||||||
- **能抽 hooks 的就抽 hooks**:跨页面复用的业务逻辑放 `src/hooks/`(如
|
- **能抽 hooks 的就抽 hooks**:跨页面复用的业务逻辑放 `src/hooks/`(如
|
||||||
`use-maintenance-intercept`、`useScroll`、`useUpload`),auto-import 免 import 直接调用
|
`useMaintenanceIntercept`、`useScroll`、`useUpload`),auto-import 免 import 直接调用
|
||||||
- 组件不直接依赖 store 的散装字段,通过 props 传入、events 传出,降低耦合
|
- 组件不直接依赖 store 的散装字段,通过 props 传入、events 传出,降低耦合
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ export interface IPublicMaintenance {
|
|||||||
status: 'scheduled' | 'active'
|
status: 'scheduled' | 'active'
|
||||||
/** 维护页标题 */
|
/** 维护页标题 */
|
||||||
title?: string
|
title?: string
|
||||||
/** 维护说明(富文本 HTML,mp-html 渲染) */
|
/** 维护说明(纯文本,维护页标题下方直接展示,留空展示默认文案) */
|
||||||
|
notice?: string
|
||||||
|
/** 维护详情(富文本 HTML,「维护详情」弹窗内 mp-html 渲染) */
|
||||||
description?: string
|
description?: string
|
||||||
/** 维护开始时间(RFC3339 UTC 字符串) */
|
/** 维护开始时间(RFC3339 UTC 字符串) */
|
||||||
startTime?: string
|
startTime?: string
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<slot></slot>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</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>
|
<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 { onMounted, ref } from 'vue'
|
||||||
import { getNoticeLatest } from '@/api/uni-halo'
|
import { getNoticeLatest } from '@/api/uni-halo'
|
||||||
import { getCache, setCache } from '@/utils/storage'
|
import { getCache, setCache } from '@/utils/storage'
|
||||||
@@ -89,23 +81,23 @@ onMounted(() => {
|
|||||||
<wd-popup
|
<wd-popup
|
||||||
v-model="isShow"
|
v-model="isShow"
|
||||||
position="center"
|
position="center"
|
||||||
width="80vw"
|
custom-class="rounded-xl"
|
||||||
custom-style="border-radius:16rpx;"
|
z-index="9999"
|
||||||
@close="handleClose"
|
@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 justify-between">
|
||||||
<view class="flex items-center gap-2">
|
<view class="flex items-center gap-2">
|
||||||
<text class="text-[28rpx]">
|
<text class="text-sm">
|
||||||
📢
|
📢
|
||||||
</text>
|
</text>
|
||||||
<text class="text-[30rpx] font-bold text-[#333]">
|
<text class="text-md font-bold text-gray-900">
|
||||||
最新公告
|
最新公告
|
||||||
</text>
|
</text>
|
||||||
<view
|
<view
|
||||||
v-if="notice.typeDisplayName"
|
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="{
|
:style="{
|
||||||
color: notice.typeColor || '#f83856',
|
color: notice.typeColor || '#f83856',
|
||||||
backgroundColor: notice.typeColor ? `${notice.typeColor}1a` : '#fdeef1',
|
backgroundColor: notice.typeColor ? `${notice.typeColor}1a` : '#fdeef1',
|
||||||
@@ -114,7 +106,7 @@ onMounted(() => {
|
|||||||
{{ notice.typeDisplayName }}
|
{{ notice.typeDisplayName }}
|
||||||
</view>
|
</view>
|
||||||
</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" />
|
<wd-icon name="close" size="16px" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -124,29 +116,23 @@ onMounted(() => {
|
|||||||
<view class="text-[32rpx] font-bold leading-snug text-[#222]">
|
<view class="text-[32rpx] font-bold leading-snug text-[#222]">
|
||||||
{{ notice.title }}
|
{{ notice.title }}
|
||||||
</view>
|
</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 }}
|
{{ notice.summary }}
|
||||||
</view>
|
</view>
|
||||||
<view v-if="notice.publishTime" class="mt-3 text-[22rpx] text-[#bbb]">
|
<view v-if="notice.publishTime" class="mt-3 text-[22rpx] text-gray-400">
|
||||||
{{ formatDate(notice.publishTime) }}
|
日期:{{ formatDate(notice.publishTime) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作 -->
|
||||||
<view class="mt-6 flex items-center justify-between border-t border-[#f5f5f5] pt-4">
|
<view class="box-border mt-4 flex items-center justify-between pt-4">
|
||||||
<view class="text-[24rpx] text-[#999]" @click="handleDismissForever">
|
<view class="text-xs text-gray-600 " @click="handleDismissForever">
|
||||||
今日不再提醒
|
今日不再提醒
|
||||||
</view>
|
</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>
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.uh-notify-dialog {
|
|
||||||
/* 布局全部由 UnoCSS 原子类实现 */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,14 +7,22 @@
|
|||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleScrollTop() {
|
function handleScrollTop() {
|
||||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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">
|
:class="props.customClass" @click="handleScrollTop">
|
||||||
<wd-icon name="arrow-up" size="20px" />
|
<wd-icon name="arrow-up" size="20px" />
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import type { DataLoadingStatus } from './useDataLoading'
|
import type { DataLoadingStatus } from './useDataLoading'
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 请改用 useDataLoading:状态由 hook 内部流转,
|
|
||||||
* 无需再在 try/catch 里手工调用 updateLoadingStatus 搬运状态。
|
|
||||||
* 本文件保留仅为兼容存量页面(category/article-detail 等),后续逐步迁移。
|
|
||||||
*/
|
|
||||||
export const DataLoadingStatusEnum = {
|
export const DataLoadingStatusEnum = {
|
||||||
Loading: 'loading',
|
Loading: 'loading',
|
||||||
Error: 'error',
|
Error: 'error',
|
||||||
Empty: 'empty',
|
Empty: 'empty',
|
||||||
Success: 'success',
|
Success: 'success',
|
||||||
|
NoNetwork: 'noNetwork',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function useDataLoadingStatus(status?: DataLoadingStatus) {
|
export function useDataLoadingStatus(status?: DataLoadingStatus) {
|
||||||
|
|||||||
+82
-92
@@ -1,106 +1,96 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
* 入口页(源自旧项目 pages/index/index.vue,新建复刻)
|
import { getQRCodeInfo } from '@/api/uni-halo'
|
||||||
* 职责:检查插件可用性 + 维护模式 → 重定向维护页 → 获取配置 → 二维码 scene 跳文章 →
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
* 审计模式 mock → 启动页/首页分流
|
import { useSettingStore } from '@/store/setting'
|
||||||
* 拦截规则(2026-09-04):主插件未激活或维护模式开启(任一命中)均跳转维护页,
|
import { collectSiteDefaults } from '@/utils/preference'
|
||||||
* 不再展示 uh-plugin-unavailable 组件;维护页按 from 参数区分原因展示默认/配置文案。
|
import { useMaintenanceIntercept } from '@/hooks/useMaintenanceIntercept'
|
||||||
*/
|
|
||||||
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'
|
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
// 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
|
// 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
|
||||||
type: 'home',
|
type: 'home',
|
||||||
style: {
|
style: {
|
||||||
// 'custom' 表示开启自定义导航栏,默认 'default'
|
navigationStyle: 'custom',
|
||||||
navigationStyle: 'custom',
|
navigationBarTitleText: '初始页面',
|
||||||
navigationBarTitleText: '初始页面',
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
/* ---------------- 常量 ---------------- */
|
/* ---------------- 常量 ---------------- */
|
||||||
const homePagePath = '/pages/tabbar/home/home'
|
const homePagePath = '/pages/tabbar/home/home'
|
||||||
const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
||||||
|
|
||||||
// 本地开发快速跳转页面,发布请置为 false
|
// 本地开发快速跳转页面,发布请置为 false
|
||||||
const DEV_MODE = false
|
const DEV_MODE = false
|
||||||
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
||||||
const DEV_TO_PATH = `/pages-blog/test/test`
|
const DEV_TO_PATH = `/pages-blog/test/test`
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
/** 维护拦截(插件可用性 + 维护模式检查与跳转封装,见 hooks/use-maintenance-intercept) */
|
// 维护拦截
|
||||||
const { interceptOrContinue } = useMaintenanceIntercept()
|
const { interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept()
|
||||||
|
|
||||||
/** 通过二维码 scene 获取文章 id */
|
/** 通过二维码 scene 获取文章 id */
|
||||||
async function getPostIdByQRCode(key: string): Promise<string | null> {
|
async function getPostIdByQRCode(key : string) : Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await getQRCodeInfo(key)
|
const response = await getQRCodeInfo(key)
|
||||||
if (response.data?.postId)
|
if (response.data?.postId)
|
||||||
return response.data.postId as string
|
return response.data.postId as string
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('二维码解析失败', err)
|
console.error('二维码解析失败', err)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
// 本地开发,快速跳转页面,发布请设置 DEV_MODE = false
|
// 本地开发,快速跳转页面,发布请设置 DEV_MODE = false
|
||||||
if (DEV_MODE && DEV_TO_PATH) {
|
if (DEV_MODE && DEV_TO_PATH) {
|
||||||
if (DEV_TO_TYPE === 'tabbar') {
|
if (DEV_TO_TYPE === 'tabbar') {
|
||||||
uni.switchTab({ url: DEV_TO_PATH })
|
uni.switchTab({ url: DEV_TO_PATH })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
uni.navigateTo({ url: DEV_TO_PATH })
|
uni.navigateTo({ url: DEV_TO_PATH })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拦截:主插件未激活 或 维护模式开启(任一命中)→ 跳转维护页
|
// 获取配置
|
||||||
if (await interceptOrContinue())
|
try {
|
||||||
return
|
const { ok } = await appConfigStore.bootstrap()
|
||||||
|
if (!ok) {
|
||||||
|
uni.switchTab({ url: homePagePath })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 获取配置(统一 bootstrap: getConfigs + audit-data + love-config 并行一次;
|
// 二维码 scene 进入:解析 postId 跳文章详情
|
||||||
// TTL 内直接返回缓存。设计见 .docs/static-config-unified-fetch-design.md)
|
if (options.scene && options.scene !== '') {
|
||||||
try {
|
const postId = await getPostIdByQRCode(decodeURIComponent(options.scene))
|
||||||
const { ok } = await appConfigStore.bootstrap()
|
if (postId) {
|
||||||
if (!ok) {
|
uni.redirectTo({
|
||||||
uni.switchTab({ url: homePagePath })
|
url: `${articleDetailPath}?name=${postId}`,
|
||||||
return
|
animationType: 'slide-in-right',
|
||||||
}
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 二维码 scene 进入:解析 postId 跳文章详情
|
// 审核模式数据已随 bootstrap 拉取(auditData/auditModeEnabled 即可用)
|
||||||
if (options.scene && options.scene !== '') {
|
settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs))
|
||||||
const postId = await getPostIdByQRCode(decodeURIComponent(options.scene))
|
|
||||||
if (postId) {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: `${articleDetailPath}?name=${postId}`,
|
|
||||||
animationType: 'slide-in-right',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 两层偏好合并:应用站点默认(L0)到 setting store(内部合并本地差异,含旧数据迁移);
|
// 拦截:主插件未激活 或 维护模式开启(任一命中)→ 跳转维护页
|
||||||
// 审核模式数据已随 bootstrap 拉取(auditData/auditModeEnabled 即可用)
|
if (await interceptOrContinue()) { return }
|
||||||
settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs))
|
uni.switchTab({ url: homePagePath })
|
||||||
|
}
|
||||||
// 启动页已下线(v2.2 ⑤):直接进首页
|
catch (err) {
|
||||||
uni.switchTab({ url: homePagePath })
|
console.error('入口页初始化失败', err)
|
||||||
}
|
redirectToMaintenance()
|
||||||
catch (err) {
|
}
|
||||||
console.error('入口页初始化失败', err)
|
})
|
||||||
uni.switchTab({ url: homePagePath })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page h-screen w-screen flex items-center justify-center" style="background-color: #fff;" />
|
<view>
|
||||||
</template>
|
<!-- 初始化页面 -->
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkUrl } from '@/utils/url'
|
import { checkUrl } from '@/utils/url'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
|
||||||
import { usePluginAvailable } from '@/utils/plugin'
|
import { usePluginAvailable } from '@/utils/plugin'
|
||||||
import type { IPublicMaintenance } from '@/api/types/uni-halo'
|
import type { IPublicMaintenance } from '@/api/types/uni-halo'
|
||||||
|
|
||||||
@@ -34,6 +33,8 @@
|
|||||||
const spinning = ref(false)
|
const spinning = ref(false)
|
||||||
/** Toast 文案(空 = 隐藏) */
|
/** Toast 文案(空 = 隐藏) */
|
||||||
const toast = ref('')
|
const toast = ref('')
|
||||||
|
/** 「维护详情」弹窗显隐 */
|
||||||
|
const showDetail = ref(false)
|
||||||
let timer : ReturnType<typeof setInterval> | null = null
|
let timer : ReturnType<typeof setInterval> | null = null
|
||||||
let recoveryTimer : ReturnType<typeof setInterval> | null = null
|
let recoveryTimer : ReturnType<typeof setInterval> | null = null
|
||||||
let toastTimer : ReturnType<typeof setTimeout> | null = null
|
let toastTimer : ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -42,8 +43,10 @@
|
|||||||
const isIntercepted = computed(() => !!fromReason.value)
|
const isIntercepted = computed(() => !!fromReason.value)
|
||||||
const isScheduled = computed(() => maintenance.value?.status === 'scheduled')
|
const isScheduled = computed(() => maintenance.value?.status === 'scheduled')
|
||||||
const title = computed(() => maintenance.value?.title || DEFAULT_MAINTENANCE_TITLE)
|
const title = computed(() => maintenance.value?.title || DEFAULT_MAINTENANCE_TITLE)
|
||||||
/** 配置的富文本说明(未配置时展示设计稿默认文案) */
|
/** 维护详情(富文本 HTML;小程序端「维护详情」弹窗内 mp-html 渲染) */
|
||||||
const description = computed(() => maintenance.value?.description || '')
|
const detailHtml = computed(() => maintenance.value?.description || '')
|
||||||
|
/** 维护说明(纯文本;标题下方按行直接展示,留空展示设计稿默认文案) */
|
||||||
|
const noticeLines = computed(() => (maintenance.value?.notice || '').split('\n'))
|
||||||
|
|
||||||
/** 应用信息 logo(相对插件内置资源路径 → BASE_API 补全) */
|
/** 应用信息 logo(相对插件内置资源路径 → BASE_API 补全) */
|
||||||
const appLogo = computed(() => {
|
const appLogo = computed(() => {
|
||||||
@@ -137,7 +140,8 @@
|
|||||||
* 拉取失败(服务器停机)保持维护页不打扰。
|
* 拉取失败(服务器停机)保持维护页不打扰。
|
||||||
*/
|
*/
|
||||||
async function silentCheck() {
|
async function silentCheck() {
|
||||||
if (refreshing) { return }
|
if (refreshing)
|
||||||
|
return
|
||||||
refreshing = true
|
refreshing = true
|
||||||
try {
|
try {
|
||||||
await store.bootstrap({ force: true })
|
await store.bootstrap({ force: true })
|
||||||
@@ -189,14 +193,12 @@
|
|||||||
startRecoveryCheck()
|
startRecoveryCheck()
|
||||||
}
|
}
|
||||||
else if (isIntercepted.value) {
|
else if (isIntercepted.value) {
|
||||||
// 拦截场景无维护信息(插件未激活/未配置)→ 默认文案
|
|
||||||
maintenance.value = null
|
maintenance.value = null
|
||||||
viewState.value = 'maintenance'
|
viewState.value = 'maintenance'
|
||||||
startRecoveryCheck()
|
startRecoveryCheck()
|
||||||
}
|
}
|
||||||
else if (ok) {
|
else if (ok) {
|
||||||
// 拉取成功但无 maintenance 键:未维护(或已到点自动结束)→ 服务正常
|
goIndex()
|
||||||
viewState.value = 'normal'
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
viewState.value = 'error'
|
viewState.value = 'error'
|
||||||
@@ -251,7 +253,7 @@
|
|||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
const from = options?.from
|
const from = options?.from
|
||||||
fromReason.value = from === 'plugin' || from === 'maintenance' ? from : null
|
fromReason.value = from === 'plugin' || from === 'maintenance' ? from : null
|
||||||
void load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
@@ -264,142 +266,153 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="maintenance-page">
|
<view class="relative min-h-screen flex flex-col justify-center bg-[#f5fae8]">
|
||||||
<!-- 加载中 -->
|
<!-- 背景 -->
|
||||||
<view v-if="viewState === 'loading'" class="flex flex-col items-center justify-center py-48">
|
<view class="absolute lef-0 top-0 w-full h-[46vh] from-[#d9f77f] via-[#e8fbaf] to-[#f5fae8] bg-gradient-to-b">
|
||||||
<text class="text-[26rpx] text-[#999]">
|
|
||||||
加载中...
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 服务正常(未维护,手动进入时) -->
|
<!-- 刷新 -->
|
||||||
<view v-else-if="viewState === 'normal'"
|
<uh-data-loading v-if="viewState === 'loading'" />
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
|
<!-- 拉取失败(手动进入,通常为服务器停机中) -->
|
||||||
<view v-else-if="viewState === 'error'"
|
<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]">
|
<text class="text-[64rpx]">
|
||||||
⚠️
|
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="120rpx" />
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-6 text-[28rpx] text-[#333]">
|
<text class="mt-6 text-md text-gray-900 font-bold">
|
||||||
服务暂时无法访问
|
服务暂时无法访问
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-2 text-[24rpx] text-[#999]">
|
<text class="mt-3 text-xs text-gray-500">
|
||||||
站点可能正在维护中,请稍后重试
|
站点可能正在维护中,请稍后重试
|
||||||
</text>
|
</text>
|
||||||
<view class="mt-10">
|
<view class="mt-10">
|
||||||
<wd-button type="primary" plain round @click="load(true)">
|
<uh-button custom-class="uh-global-card-glass border flex-1 py-2 !px-8 !rounded-full font-semibold"
|
||||||
重新加载
|
@click="load(true)">刷新试试</uh-button>
|
||||||
</wd-button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 维护页(参考 .design/维护页面.html) -->
|
<!-- 维护页-->
|
||||||
<view v-else class="app-container">
|
<view v-else class="relative z-10 w-full min-h-screen flex items-center justify-center flex-col ">
|
||||||
<!-- ===== Hero(无边界淡出) ===== -->
|
<view
|
||||||
<view class="hero">
|
class="w-full relative overflow-hidden px-[40rpx] pb-[64rpx] pt-[calc(var(--status-bar-height)+76rpx)] text-center">
|
||||||
<view class="blob breathe hero-blob-1 b-white" />
|
<view
|
||||||
<view class="blob b-acc hero-blob-2" />
|
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="blob hero-blob-3 b-white" />
|
<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="relative z-2 mx-auto mt-[44rpx] h-[236rpx] w-[236rpx]">
|
||||||
<view class="medal-bg" />
|
<view
|
||||||
<image v-if="appLogo" class="medal-img" :src="appLogo" mode="aspectFill" />
|
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)]">
|
||||||
<wd-icon v-else class="medal-emoji" name="tool" size="52px" />
|
<image v-if="appLogo" class="h-full w-full rounded-full" :src="appLogo" mode="aspectFill" />
|
||||||
<view class="gear gear-1">
|
<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" />
|
<wd-icon name="settings" size="24px" />
|
||||||
</view>
|
</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" />
|
<wd-icon name="settings" size="16px" />
|
||||||
</view>
|
</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
|
MAINTENANCE
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="hero-h1">
|
<view class="relative z-2 mt-[64rpx] text-[54rpx] font-black leading-[64rpx]">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</view>
|
</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">
|
<view
|
||||||
<mp-html v-if="description" :content="description" lazy-load :domain="markdownConfig.domain ?? ''"
|
class="relative z-2 mt-[20rpx] px-[16rpx] text-[25rpx] text-[rgba(23,24,26,0.55)] font-medium leading-[1.7]">
|
||||||
scroll-table selectable :tag-style="markdownConfig.tagStyle"
|
<template v-if="noticeLines.length > 0">
|
||||||
:container-style="markdownConfig.containStyle" copy-by-long-press />
|
<text v-for="(line, index) in noticeLines" :key="index" class="block">{{ line }}</text>
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<text>为了给你带来更好的体验,站点正在维护升级中</text>
|
<text>为了给你带来更好的体验,站点正在维护升级中</text>
|
||||||
<text class="block">
|
<text class="block">
|
||||||
别担心,你的数据都安然无恙 💚
|
别担心,我们很快就回来!
|
||||||
</text>
|
</text>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- ===== 内容区 ===== -->
|
<!-- ===== 内容区 ===== -->
|
||||||
<view class="layer">
|
<view class="relative z-10 w-full">
|
||||||
<view class="bridge bridge-1" />
|
<view
|
||||||
<view class="bridge bridge-2" />
|
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 v-if="countdownParts"
|
||||||
<view class="eta-wm">
|
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!
|
GO!
|
||||||
</view>
|
</view>
|
||||||
<view class="eta-k">
|
<view
|
||||||
<wd-icon name="clock-circle" size="14px" />
|
class="inline-flex items-center justify-center text-xs text-gray-500 font-extrabold tracking-[4rpx]">
|
||||||
{{ countdownPrefix }}
|
{{ countdownPrefix }}
|
||||||
</view>
|
</view>
|
||||||
<view class="eta-clock">
|
<view class="mt-4 w-full flex justify-center gap-4">
|
||||||
<view class="eta-cell hot">
|
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
|
||||||
<text class="cell-num">{{ countdownParts.days }}</text>
|
<text
|
||||||
<text class="cell-unit">天 DAY</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>
|
||||||
<view class="eta-cell">
|
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
|
||||||
<text class="cell-num">{{ countdownParts.hours }}</text>
|
<text
|
||||||
<text class="cell-unit">时 HR</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>
|
||||||
<view class="eta-cell">
|
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
|
||||||
<text class="cell-num">{{ countdownParts.minutes }}</text>
|
<text
|
||||||
<text class="cell-unit">分 MIN</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>
|
||||||
<view class="eta-cell">
|
<view class="flex-1 rounded-[24rpx] bg-[#f4fbe0] px-[8rpx] pb-[16rpx] pt-[18rpx]">
|
||||||
<text class="cell-num">{{ countdownParts.seconds }}</text>
|
<text
|
||||||
<text class="cell-unit">秒 SEC</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>
|
</view>
|
||||||
<view v-if="etaNote" class="eta-note">
|
<view v-if="etaNote" class="mt-[24rpx] text-[21rpx] text-primary font-medium">
|
||||||
{{ etaNote }}
|
{{ etaNote }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作 -->
|
||||||
<view class="cta-row">
|
<view class="mt-2 w-full flex flex-col gap-4">
|
||||||
<view class="btn-refresh ink-btn press" @click="handleRefresh">
|
<uh-button custom-class="uh-global-card-glass border flex-1 py-3 !rounded-full font-semibold"
|
||||||
<wd-icon class="refresh-ic" :class="{ spinning }" name="refresh" size="17px" />
|
@click="handleRefresh">刷新看看</uh-button>
|
||||||
<text>刷新看看</text>
|
<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>
|
</view>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<view class="foot">
|
<view class="mt-[12rpx] flex flex-col items-center">
|
||||||
<view class="foot-text">
|
<view class="mt-[18rpx] text-center text-[21rpx] text-[#c9cdd4] font-medium leading-[1.7]">
|
||||||
升级期间给你带来不便,非常抱歉
|
升级期间给你带来不便,非常抱歉
|
||||||
<text class="block">
|
<text class="block">
|
||||||
去喝杯奶茶等等吧~
|
去喝杯奶茶等等吧~
|
||||||
@@ -410,70 +423,19 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Toast -->
|
<!-- 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 }}
|
{{ toast }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 维护详情弹窗(复用封装组件,见 components/uh-maintenance-detail) -->
|
||||||
|
<uh-maintenance-detail v-model="showDetail" :content="detailHtml" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<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 {
|
.breathe {
|
||||||
animation: breathe 5s ease-in-out infinite;
|
animation: breathe 5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -492,65 +454,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-blob-1 {
|
/* 勋章浮动 */
|
||||||
left: -60rpx;
|
.bob {
|
||||||
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);
|
|
||||||
animation: bob 3.2s ease-in-out infinite;
|
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 {
|
@keyframes bob {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
@@ -563,251 +471,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gear {
|
/* 齿轮旋转(正/反两档;keyframes 会被 scoped 重命名,须自定义承载类) */
|
||||||
position: absolute;
|
.gear-spin {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
animation: spin 6s linear infinite;
|
animation: spin 6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gear-spin-reverse {
|
||||||
|
animation: spin 4.5s linear infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
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 {
|
.refresh-ic {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -816,6 +495,7 @@
|
|||||||
animation: spin 0.7s linear;
|
animation: spin 0.7s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 按压反馈 */
|
||||||
.press {
|
.press {
|
||||||
transition: transform 0.12s ease;
|
transition: transform 0.12s ease;
|
||||||
}
|
}
|
||||||
@@ -823,51 +503,4 @@
|
|||||||
.press:active {
|
.press:active {
|
||||||
transform: scale(0.94);
|
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>
|
</style>
|
||||||
@@ -1,186 +1,181 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
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 { getCategoryList } from '@/api/halo'
|
import { getCategoryList } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkThumbnailUrl } from '@/utils/url'
|
import { checkThumbnailUrl } from '@/utils/url'
|
||||||
import { sleep } from '@/utils/common'
|
import { sleep } from '@/utils/common'
|
||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { ICategory } from '@/api/types/halo'
|
import type { ICategory } from '@/api/types/halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '分类',
|
navigationBarTitleText: '分类',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
backgroundColor: '#f6f3ee',
|
backgroundColor: '#f6f3ee',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const haloConfigs = computed(() => appConfigStore.configs)
|
const haloConfigs = computed(() => appConfigStore.configs)
|
||||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||||
|
|
||||||
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
|
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
size: 20,
|
size: 20,
|
||||||
page: 1,
|
page: 1,
|
||||||
fieldSelector: ['spec.hideFromList=false'],
|
fieldSelector: ['spec.hideFromList=false'],
|
||||||
})
|
})
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
const dataList = ref<ICategory[]>([])
|
const dataList = ref<ICategory[]>([])
|
||||||
const isLoadMore = ref(false)
|
const isLoadMore = ref(false)
|
||||||
const loadMoreText = ref(t('common.loading'))
|
const loadMoreText = ref(t('common.loading'))
|
||||||
|
|
||||||
function handleResetInit() {
|
function handleResetInit() {
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
queryParams.value.page = 1
|
queryParams.value.page = 1
|
||||||
hasNext.value = false
|
hasNext.value = false
|
||||||
isLoadMore.value = false
|
isLoadMore.value = false
|
||||||
loadMoreText.value = t('common.loading')
|
loadMoreText.value = t('common.loading')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInitPage() {
|
function handleInitPage() {
|
||||||
handleResetInit()
|
handleResetInit()
|
||||||
handleGetData()
|
handleGetData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
// 增加延迟,提升用户体验
|
// 增加延迟,提升用户体验
|
||||||
await sleep(800)
|
await sleep(800)
|
||||||
// 审核模式
|
// 审核模式
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
|
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
|
||||||
try {
|
try {
|
||||||
const res = await getCategoryList({ page: 1, size: 99999 })
|
const res = await getCategoryList({ page: 1, size: 99999 })
|
||||||
const filtered = res.data.items
|
const filtered = res.data.items
|
||||||
.filter(item => auditCategoryNames.includes(item.metadata.name))
|
.filter(item => auditCategoryNames.includes(item.metadata.name))
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
postCount: item.postCount ?? 0,
|
postCount: item.postCount ?? 0,
|
||||||
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
||||||
}))
|
}))
|
||||||
const orderMap = new Map(auditCategoryNames.map((name, index) => [name, index]))
|
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))
|
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
|
||||||
dataList.value = filtered
|
dataList.value = filtered
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
|
||||||
loadMoreText.value = t('common.noMore')
|
loadMoreText.value = t('common.noMore')
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = t('common.loadFailed')
|
loadMoreText.value = t('common.loadFailed')
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
loadMoreText.value = t('common.loading')
|
loadMoreText.value = t('common.loading')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getCategoryList({ ...queryParams.value })
|
const res = await getCategoryList({ ...queryParams.value })
|
||||||
|
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
hasNext.value = res.data.hasNext
|
||||||
hasNext.value = res.data.hasNext
|
|
||||||
|
|
||||||
const tempItems = res.data.items.map(item => ({
|
const tempItems = res.data.items.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
postCount: item.postCount ?? 0,
|
postCount: item.postCount ?? 0,
|
||||||
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
dataList.value = isLoadMore.value
|
dataList.value = isLoadMore.value
|
||||||
? dataList.value.concat(tempItems)
|
? dataList.value.concat(tempItems)
|
||||||
: tempItems
|
: tempItems
|
||||||
}
|
updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
|
||||||
catch (err) {
|
}
|
||||||
console.error(err)
|
catch (err) {
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
console.error(err)
|
||||||
loadMoreText.value = t('common.loadFailed')
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
loadMoreText.value = t('common.loadFailed')
|
||||||
finally {
|
}
|
||||||
setTimeout(() => {
|
finally {
|
||||||
uni.hideLoading()
|
setTimeout(() => {
|
||||||
uni.stopPullDownRefresh()
|
uni.hideLoading()
|
||||||
}, 500)
|
uni.stopPullDownRefresh()
|
||||||
}
|
}, 500)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleToCategory(category: ICategory) {
|
function handleToCategory(category : ICategory) {
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleInitPage()
|
handleInitPage()
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
handleResetInit()
|
handleResetInit()
|
||||||
handleGetData()
|
handleGetData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (hasNext.value) {
|
if (hasNext.value) {
|
||||||
queryParams.value.page += 1
|
queryParams.value.page += 1
|
||||||
isLoadMore.value = true
|
isLoadMore.value = true
|
||||||
handleGetData()
|
handleGetData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="box-border min-h-screen w-screen flex flex-col bg-page p-3">
|
<view class="box-border min-h-screen w-screen flex flex-col bg-page p-3">
|
||||||
<!-- 骨架屏 -->
|
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" />
|
||||||
<view v-if="loadingStatus !== DataLoadingStatusEnum.Success">
|
|
||||||
<uh-data-loading :loading-status="loadingStatus" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view class="grid grid-cols-2 gap-3">
|
<view class="grid grid-cols-2 gap-2.5">
|
||||||
<view
|
<view v-for="(item, index) in dataList" :key="index"
|
||||||
v-for="(item, index) in dataList" :key="index"
|
class="uh-global-card-glass relative box-border w-full overflow-hidden rounded-xl"
|
||||||
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
|
||||||
<image v-if="item.spec.cover" class="block h-32 w-full" :src="item.spec.cover" mode="aspectFill" />
|
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">
|
||||||
<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">
|
||||||
<text class="truncate text-sm text-white font-bold">
|
{{ item.spec.displayName }}
|
||||||
{{ item.spec.displayName }}
|
</text>
|
||||||
</text>
|
<text class="text-xs text-white opacity-80">
|
||||||
<text class="text-xs text-white opacity-80">
|
共 {{ item.postCount }} 篇文章
|
||||||
共 {{ item.postCount }} 篇文章
|
</text>
|
||||||
</text>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="w-full py-5 text-center text-xs text-gray-400">
|
||||||
<view class="w-full py-5 text-center text-xs text-gray-400">
|
{{ loadMoreText }}
|
||||||
{{ loadMoreText }}
|
</view>
|
||||||
</view>
|
</block>
|
||||||
</block>
|
</view>
|
||||||
</view>
|
</template>
|
||||||
</template>
|
|
||||||
@@ -1,250 +1,233 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
/**
|
||||||
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
|
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
|
||||||
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
|
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
|
||||||
*/
|
*/
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
|
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkImageUrl } from '@/utils/url'
|
import { checkImageUrl } from '@/utils/url'
|
||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
import { usePluginAvailable } from '@/utils/plugin'
|
import { usePluginAvailable } from '@/utils/plugin'
|
||||||
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
|
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '图库',
|
navigationBarTitleText: '图库',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const haloConfigs = computed(() => appConfigStore.configs)
|
const haloConfigs = computed(() => appConfigStore.configs)
|
||||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||||
|
|
||||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||||
|
|
||||||
/** 依赖插件(plugin-photos) */
|
/** 依赖插件(plugin-photos) */
|
||||||
const uniHaloPluginId = 'plugin-photos'
|
const uniHaloPluginId = 'plugin-photos'
|
||||||
const uniHaloPluginAvailable = ref(true)
|
const uniHaloPluginAvailable = ref(true)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const loading = ref<'loading' | 'success' | 'error' | 'empty'>('loading')
|
||||||
const category = ref<{ activeIndex: number, list: IPhotoGroup[] }>({
|
const category = ref<{ activeIndex : number, list : IPhotoGroup[] }>({
|
||||||
activeIndex: 0,
|
activeIndex: 0,
|
||||||
list: [],
|
list: [],
|
||||||
})
|
})
|
||||||
const queryParams = ref({ size: 10, page: 1, group: '' })
|
const queryParams = ref({ size: 10, page: 1, group: '' })
|
||||||
const isLoadMore = ref(false)
|
const isLoadMore = ref(false)
|
||||||
const loadMoreText = ref('')
|
const loadMoreText = ref('')
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
const dataList = ref<IPhoto[]>([])
|
const dataList = ref<IPhoto[]>([])
|
||||||
const lock = ref(false)
|
const lock = ref(false)
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetCategory() {
|
async function handleGetCategory() {
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
|
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
|
||||||
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
|
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
|
||||||
try {
|
try {
|
||||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||||
const filtered = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
const filtered = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||||
.filter(item => auditGroupNames.includes(item.metadata.name))
|
.filter(item => auditGroupNames.includes(item.metadata.name))
|
||||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||||
category.value.list = filtered
|
category.value.list = filtered
|
||||||
if (category.value.list.length !== 0) {
|
if (category.value.list.length !== 0) {
|
||||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||||
handleGetData(true)
|
handleGetData(true)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loading.value = 'success'
|
loading.value = 'success'
|
||||||
loadMoreText.value = t('common.noMore')
|
loadMoreText.value = t('common.noMore')
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
loading.value = 'error'
|
loading.value = 'error'
|
||||||
category.value = { activeIndex: 0, list: [] }
|
category.value = { activeIndex: 0, list: [] }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||||
category.value.list = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
category.value.list = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||||
.sort((a, b) => a.spec.priority - b.spec.priority)
|
.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||||
category.value.list.unshift({ metadata: { name: undefined }, spec: { displayName: '全部', priority: 0 } })
|
category.value.list.unshift({ metadata: { name: undefined }, spec: { displayName: '全部', priority: 0 } })
|
||||||
if (category.value.list.length !== 0) {
|
if (category.value.list.length !== 0) {
|
||||||
queryParams.value.group = category.value.list[0].metadata.name || ''
|
queryParams.value.group = category.value.list[0].metadata.name || ''
|
||||||
handleGetData(true)
|
handleGetData(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
loading.value = 'error'
|
loading.value = 'error'
|
||||||
category.value = { activeIndex: 0, list: [] }
|
category.value = { activeIndex: 0, list: [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGetData(isClearList = false) {
|
async function handleGetData(isClearList = false) {
|
||||||
if (isClearList) {
|
if (isClearList) {
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
queryParams.value.page = 1
|
queryParams.value.page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
loading.value = 'loading'
|
loading.value = 'loading'
|
||||||
}
|
}
|
||||||
loadMoreText.value = ''
|
loadMoreText.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getPhotoListByGroupName({ ...queryParams.value })
|
const res = await getPhotoListByGroupName({ ...queryParams.value })
|
||||||
hasNext.value = res.data.hasNext
|
hasNext.value = res.data.hasNext
|
||||||
loading.value = 'success'
|
if (res.data.items.length !== 0) {
|
||||||
if (res.data.items.length !== 0) {
|
const list = res.data.items.map(item => ({
|
||||||
const list = res.data.items.map(item => ({
|
...item,
|
||||||
...item,
|
spec: { ...item.spec, url: checkImageUrl(item.spec.url || item.spec.cover) },
|
||||||
spec: { ...item.spec, url: checkImageUrl(item.spec.url || item.spec.cover) },
|
}))
|
||||||
}))
|
dataList.value = isLoadMore.value
|
||||||
dataList.value = isLoadMore.value
|
? dataList.value.concat(list)
|
||||||
? dataList.value.concat(list)
|
: list
|
||||||
: list
|
}
|
||||||
}
|
loading.value = dataList.value.length !== 0 ? 'success' : 'empty'
|
||||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
loading.value = 'error'
|
||||||
loadMoreText.value = t('common.loadFailed')
|
loadMoreText.value = t('common.loadFailed')
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
lock.value = false
|
lock.value = false
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGetDataByCategory(index: number, cate: IPhotoGroup) {
|
function handleGetDataByCategory(index : number, cate : IPhotoGroup) {
|
||||||
queryParams.value.group = cate.metadata.name || ''
|
queryParams.value.group = cate.metadata.name || ''
|
||||||
queryParams.value.page = 1
|
queryParams.value.page = 1
|
||||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
category.value.activeIndex = index
|
category.value.activeIndex = index
|
||||||
handleGetData(true)
|
handleGetData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 图片预览 ---------------- */
|
/* ---------------- 图片预览 ---------------- */
|
||||||
function handlePreview(data: IPhoto) {
|
function handlePreview(data : IPhoto) {
|
||||||
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
|
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
|
||||||
uni.previewImage({
|
uni.previewImage({
|
||||||
current,
|
current,
|
||||||
urls: dataList.value.map(x => x.spec.url),
|
urls: dataList.value.map(x => x.spec.url),
|
||||||
indicator: 'number',
|
indicator: 'number',
|
||||||
loop: true,
|
loop: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
onLoad(async () => {
|
onLoad(async () => {
|
||||||
// 检查插件可用性
|
// 检查插件可用性
|
||||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||||
if (!uniHaloPluginAvailable.value) {
|
if (!uniHaloPluginAvailable.value) {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(galleryConfig, (newVal) => {
|
watch(galleryConfig, (newVal) => {
|
||||||
if (!newVal)
|
if (!newVal)
|
||||||
return
|
return
|
||||||
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
|
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
|
||||||
handleGetCategory()
|
handleGetCategory()
|
||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
if (!uniHaloPluginAvailable.value) {
|
if (!uniHaloPluginAvailable.value) {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
isLoadMore.value = false
|
isLoadMore.value = false
|
||||||
queryParams.value.page = 1
|
queryParams.value.page = 1
|
||||||
handleGetData(true)
|
handleGetData(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (!uniHaloPluginAvailable.value)
|
if (!uniHaloPluginAvailable.value)
|
||||||
return
|
return
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (hasNext.value) {
|
if (hasNext.value) {
|
||||||
queryParams.value.page += 1
|
queryParams.value.page += 1
|
||||||
isLoadMore.value = true
|
isLoadMore.value = true
|
||||||
handleGetData(false)
|
handleGetData(false)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="min-h-screen w-screen flex flex-col bg-page pb-6">
|
<view class="min-h-screen w-screen flex flex-col bg-page pb-6">
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
||||||
v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员" @on-refresh="handleGetCategory" />
|
||||||
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员" @on-refresh="handleGetCategory"
|
<template v-else>
|
||||||
/>
|
<wd-sticky v-if="category.list.length!==0">
|
||||||
<template v-else>
|
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
||||||
<wd-sticky>
|
<view v-for="(cate, index) in category.list" :key="cate.spec.displayName"
|
||||||
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-block border rounded-2xl px-4 py-1 text-sm"
|
||||||
<view
|
:class="{ 'bg-primary text-gray-900 font-bold': index === category.activeIndex }"
|
||||||
v-for="(cate, index) in category.list" :key="cate.spec.displayName"
|
@click="handleGetDataByCategory(index, cate)">
|
||||||
class="uh-global-card-glass uh-shadow-xs mb-1 ml-3 inline-block border rounded-2xl px-4 py-1 text-sm"
|
{{ cate.spec.displayName }} <text
|
||||||
:class="{
|
v-if="cate.spec.displayName!=='全部'">({{ cate.status?.photoCount ?? 0 }})</text>
|
||||||
'bg-primary text-gray-900 font-bold': index === category.activeIndex,
|
</view>
|
||||||
}" @click="handleGetDataByCategory(index, cate)"
|
</scroll-view>
|
||||||
>
|
</wd-sticky>
|
||||||
{{ cate.spec.displayName }}({{ cate.status?.photoCount ?? 0 }})
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</wd-sticky>
|
|
||||||
|
|
||||||
<!-- 加载/错误占位(统一 uh-data-loading,错误可重试) -->
|
<!-- 加载/错误占位 -->
|
||||||
<view v-if="loading !== 'success'" class="box-border p-3">
|
<view v-if="loading !== 'success'" class="box-border p-3">
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetCategory" />
|
<uh-data-loading :loading-status="loading" @refresh="handleGetCategory" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<view v-else class="box-border w-full p-3">
|
<view v-else class="box-border w-full p-3">
|
||||||
<view
|
<view class="grid grid-cols-2 gap-2.5">
|
||||||
v-if="dataList.length === 0"
|
<view v-for="(item, index) in dataList" :key="index"
|
||||||
class="h-[70vh] w-full flex items-center justify-center content-empty"
|
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
|
||||||
<wd-empty description="博主还没有分享图片~" />
|
@click="handlePreview(item)" />
|
||||||
</view>
|
</view>
|
||||||
<block v-else>
|
</view>
|
||||||
<!-- 瀑布流(双列) -->
|
<view class="load-text w-full py-4 text-center text-xs text-gray-500">
|
||||||
<view class="grid grid-cols-2 gap-3">
|
{{ loadMoreText }}
|
||||||
<view
|
</view>
|
||||||
v-for="(item, index) in dataList" :key="index"
|
</view>
|
||||||
class="uh-global-card-glass h-38 w-full overflow-hidden rounded-xl"
|
</template>
|
||||||
>
|
</view>
|
||||||
<image
|
</template>
|
||||||
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>
|
|
||||||
</template>
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
import { useSettingStore } from '@/store/setting'
|
import { useSettingStore } from '@/store/setting'
|
||||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
|
import { useMaintenanceIntercept } from '@/hooks/useMaintenanceIntercept'
|
||||||
import type { IPost } from '@/api/types/halo'
|
import type { IPost } from '@/api/types/halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
@@ -146,12 +147,20 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
if (!intercepted.value) {
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
|
|
||||||
// 维护检查
|
// 维护检查
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
intercepted.value = await interceptOrContinue()
|
intercepted.value = await interceptOrContinue()
|
||||||
|
console.log('拦截状态', intercepted.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
@@ -175,13 +184,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 首次加载
|
|
||||||
onMounted(() => {
|
|
||||||
if (intercepted.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleQuery()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -274,7 +274,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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"
|
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
||||||
error-text="检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员" @on-refresh="handleGetData" />
|
error-text="检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员" @on-refresh="handleGetData" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
|
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" min-height="60vh"
|
||||||
@refresh="handleGetData" />
|
@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"
|
<view v-if="dataList.length === 0"
|
||||||
class="min-h-[70vh] w-full flex items-center justify-center content-empty">
|
class="min-h-[70vh] w-full flex items-center justify-center content-empty">
|
||||||
<wd-empty :description="t('common.empty')" />
|
<wd-empty :description="t('common.empty')" />
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
|
|
||||||
<block v-else>
|
<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">
|
class="moment-card uh-shadow-xs overflow-hidden rounded-[24rpx] bg-white">
|
||||||
<!-- 作者 -->
|
<!-- 作者 -->
|
||||||
<view class="box-border flex items-center px-4 pt-4">
|
<view class="box-border flex items-center px-4 pt-4">
|
||||||
@@ -313,22 +313,15 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 正文-->
|
<!-- 正文-->
|
||||||
<view class="moment-content px-4 pt-3 ">
|
<view class="box-border px-4 pt-3">
|
||||||
|
<view class="relative box-border bg-gray-100 p-2 rounded-lg">
|
||||||
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0"
|
<mp-html lazy-load :domain="markdownConfig.domain ?? ''"
|
||||||
class="mb-3 flex flex-wrap gap-x-2">
|
:loading-img="markdownConfig.loadingGif" scroll-table selectable
|
||||||
<text v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex"
|
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
|
||||||
class="py-1 px-2 text-xs rounded-xl bg-secondary">
|
:content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true"
|
||||||
# {{ tag }}
|
:show-language-name="true" copy-by-long-press
|
||||||
</text>
|
@click.stop="handleToMomentDetail(moment)" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<mp-html lazy-load :domain="markdownConfig.domain ?? ''"
|
|
||||||
:loading-img="markdownConfig.loadingGif" scroll-table selectable
|
|
||||||
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
|
|
||||||
:content="moment.spec.newHtml || ''" :markdown="true" :show-line-number="true"
|
|
||||||
:show-language-name="true" copy-by-long-press
|
|
||||||
@click.stop="handleToMomentDetail(moment)" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 图片 -->
|
<!-- 图片 -->
|
||||||
@@ -360,19 +353,30 @@
|
|||||||
@ended="onVideoEnded" />
|
@ended="onVideoEnded" />
|
||||||
</view>
|
</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
|
<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">
|
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-2">
|
<view class="flex items-center gap-x-1">
|
||||||
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
|
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
|
||||||
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text>
|
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text>
|
||||||
</view>
|
</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" />
|
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
|
||||||
<text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text>
|
<text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
|
|
||||||
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">
|
<view class="load-text pb-5 pt-1 text-center text-xs text-gray-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user