mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
refactor: 重构项目页面与组件,优化整体结构与体验
1. 重构分类页面路由与实现,替换旧分类详情页为文章列表页 2. 统一使用自定义导航栏,替换原有原生导航配置 3. 重构加载状态管理,统一使用useDataLoadingStatus hook 4. 优化插件可用性检查逻辑与组件展示 5. 调整数据加载占位与空状态样式 6. 优化文章卡片样式与交互,新增分类跳转功能 7. 重构设置页枚举选择弹窗,使用wd-picker替换自定义实现 8. 删除废弃的IPluginAvailable类型与相关代码
This commit is contained in:
+1
-2
@@ -24,7 +24,6 @@ import type {
|
|||||||
IPhotoGroupListRes,
|
IPhotoGroupListRes,
|
||||||
IPhotoListReq,
|
IPhotoListReq,
|
||||||
IPhotoListRes,
|
IPhotoListRes,
|
||||||
IPluginAvailable,
|
|
||||||
IPost,
|
IPost,
|
||||||
IPostListReq,
|
IPostListReq,
|
||||||
IPostListRes,
|
IPostListRes,
|
||||||
@@ -293,7 +292,7 @@ export function postTrackersCounter(data: ITrackerCounterReq) {
|
|||||||
* 检查插件是否可用
|
* 检查插件是否可用
|
||||||
*/
|
*/
|
||||||
export function checkPluginAvailable(name: string) {
|
export function checkPluginAvailable(name: string) {
|
||||||
return http.Get<IResponse<IPluginAvailable>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
|
return http.Get<IResponse<boolean>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
|
||||||
cacheFor: 0,
|
cacheFor: 0,
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -456,10 +456,3 @@ export interface IUpvoteReq {
|
|||||||
name: string
|
name: string
|
||||||
plural?: string
|
plural?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 插件可用性 ---------- */
|
|
||||||
|
|
||||||
export interface IPluginAvailable {
|
|
||||||
available: boolean
|
|
||||||
reason?: string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,79 +1,105 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { checkThumbnailUrl } from '@/utils/url'
|
import { checkThumbnailUrl } from '@/utils/url'
|
||||||
import { useSettingStore } from '@/store/setting'
|
import { useSettingStore } from '@/store/setting'
|
||||||
import { formatTime} from '@/utils/formatTime'
|
import { formatTime } from '@/utils/formatTime'
|
||||||
import type { IPost } from '@/api/types/halo'
|
import type { IPost,ICategory } from '@/api/types/halo'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
/** 来源页面(home 时跟随首页布局) */
|
from ?: string
|
||||||
from?: string
|
auditMode?: boolean
|
||||||
article: IPost
|
article : IPost
|
||||||
}>(), {
|
}>(), {
|
||||||
from: '',
|
auditMode: false,
|
||||||
})
|
from: '',
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const settingStore = useSettingStore()
|
||||||
(e: 'on-click', article: IPost): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
/** 卡片布局 class(由全局设置 layout 决定) */
|
||||||
|
const cardType = computed(() => {
|
||||||
|
const layout = settingStore.settings.layout
|
||||||
|
// 首页双列时强制上图下文布局,除非显式指定其他
|
||||||
|
if (props.from === 'home' && layout.home === 'h_row_col2') {
|
||||||
|
if (!['tb_image_text', 'tb_text_image', 'only_text'].includes(layout.cardType)) {
|
||||||
|
return [props.from, layout.home, 'tb_image_text']
|
||||||
|
}
|
||||||
|
return [props.from, layout.home, layout.cardType]
|
||||||
|
}
|
||||||
|
return [layout.home, layout.cardType]
|
||||||
|
})
|
||||||
|
|
||||||
/** 卡片布局 class(由全局设置 layout 决定) */
|
/** 发布时间格式化 yyyy-MM-dd */
|
||||||
const cardType = computed(() => {
|
const publishTimeText = computed(() => {
|
||||||
const layout = settingStore.settings.layout
|
const time = props.article.spec.publishTime
|
||||||
// 首页双列时强制上图下文布局,除非显式指定其他
|
return time ? formatTime({ d: time, f: 'yyyy-MM-dd' }) : ''
|
||||||
if (props.from === 'home' && layout.home === 'h_row_col2') {
|
})
|
||||||
if (!['tb_image_text', 'tb_text_image', 'only_text'].includes(layout.cardType)) {
|
|
||||||
return [props.from, layout.home, 'tb_image_text']
|
|
||||||
}
|
|
||||||
return [props.from, layout.home, layout.cardType]
|
|
||||||
}
|
|
||||||
return [layout.home, layout.cardType]
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 发布时间格式化 yyyy-MM-dd */
|
/** 阅读数(兼容 status.stats.visits 与旧版顶层 stats.visit) */
|
||||||
const publishTimeText = computed(() => {
|
const visitCount = computed(() => {
|
||||||
const time = props.article.spec.publishTime
|
return props.article.status?.stats?.visits ?? props.article.stats?.visit ?? 0
|
||||||
return time ? formatTime({d:time,f:'yyyy-MM-dd'}) : ''
|
})
|
||||||
})
|
|
||||||
|
|
||||||
/** 阅读数(兼容 status.stats.visits 与旧版顶层 stats.visit) */
|
function handleToArticleDetail() {
|
||||||
const visitCount = computed(() => {
|
uni.navigateTo({
|
||||||
return props.article.status?.stats?.visits ?? props.article.stats?.visit ?? 0
|
url: `/pages-blog/article-detail/article-detail?name=${props.article.metadata.name}`,
|
||||||
})
|
animationType: 'slide-in-right',
|
||||||
|
})
|
||||||
function handleClick() {
|
}
|
||||||
emit('on-click', props.article)
|
|
||||||
}
|
function handleToCategory(category : ICategory) {
|
||||||
|
if (props.auditMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="uh-global-card-glass overflow-hidden relative rounded-xl p-3" @click="handleClick">
|
<view class="uh-global-card-glass uh-shadow-xs overflow-hidden relative rounded-xl p-3"
|
||||||
<!-- v-if="article.spec.pinned" -->
|
@click.stop="handleToArticleDetail()">
|
||||||
<text class="absolute right-6 top-6 z-1 bg-secondary text-gray-60 text-xs px-2 py-1 rounded-lg"> 置顶 </text>
|
<text v-if="article.spec.pinned"
|
||||||
<image class="w-full h-36 rounded-lg" :src="checkThumbnailUrl(article.spec.cover)" mode="aspectFill" lazy-load />
|
class="absolute right-6 top-6 z-1 bg-secondary text-gray-60 text-xs px-2 py-1 rounded-lg"> 置顶 </text>
|
||||||
<view class="flex flex-col w-full gap-y-2 text-sm">
|
<image class="w-full h-36 rounded-lg" :src="checkThumbnailUrl(article.spec.cover)" mode="aspectFill"
|
||||||
<view class="mt-2 font-bold truncate">
|
lazy-load />
|
||||||
{{ article.spec.title }}
|
<view class="flex flex-col w-full gap-y-2 text-sm">
|
||||||
</view>
|
<view class="mt-2 font-bold truncate">
|
||||||
<view class="content line-clamp-2 text-gray-600">
|
{{ article.spec.title }}
|
||||||
{{ article.status?.excerpt }}
|
</view>
|
||||||
</view>
|
<view class="content line-clamp-2 text-gray-600">
|
||||||
<view class="flex items-center justify-between text-xs text-gray-500">
|
{{ article.status?.excerpt }}
|
||||||
<view class="flex items-center gap-x-1">
|
</view>
|
||||||
<image :src="article.owner.avatar" class="uh-global-card-glass rounded-full w-5 h-5" mode="aspectFill"></image>
|
<view class="my-1 box-border flex flex-wrap gap-2">
|
||||||
<text>{{article.owner.displayName}}</text>
|
<template v-if="article.categories && article.categories.length !== 0">
|
||||||
|
<text v-for="cate in article.categories" :key="cate.metadata.name"
|
||||||
|
class="py-1 px-2 text-xs rounded-xl bg-secondary" @click.stop="handleToCategory(cate)">
|
||||||
|
{{ cate.spec.displayName }}
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
<template v-if="article.tags && article.tags.length !== 0">
|
||||||
|
<text v-for="tag in article.tags" :key="tag.metadata.name"
|
||||||
|
class="py-1 px-2 text-xs rounded-xl bg-secondary">
|
||||||
|
# {{ tag.spec.displayName }}
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view class="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<view class="flex items-center gap-x-1">
|
||||||
|
<image :src="article.owner.avatar" class="uh-global-card-glass rounded-full w-5 h-5"
|
||||||
|
mode="aspectFill"></image>
|
||||||
|
<text>{{article.owner.displayName}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="flex items-center gap-x-2">
|
||||||
|
{{ publishTimeText }}
|
||||||
|
</view>
|
||||||
|
<view class="visits">
|
||||||
|
浏览
|
||||||
|
<text class="number">{{ visitCount }}</text>
|
||||||
|
次
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center gap-x-2">
|
</view>
|
||||||
{{ publishTimeText }}
|
</template>
|
||||||
</view>
|
|
||||||
<view class="visits">
|
|
||||||
浏览
|
|
||||||
<text class="number">{{ visitCount }}</text>
|
|
||||||
次
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
@@ -18,7 +18,7 @@ interface IProps {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<IProps>(), {
|
const props = withDefaults(defineProps<IProps>(), {
|
||||||
loadingStatus: 'loading',
|
loadingStatus: 'loading',
|
||||||
minHeight: '75vh',
|
minHeight: '80vh',
|
||||||
loadingText: '稍等,正在加载中哦',
|
loadingText: '稍等,正在加载中哦',
|
||||||
errorText: '哎呀,加载失败了呢~',
|
errorText: '哎呀,加载失败了呢~',
|
||||||
emptyText: '啊偶,暂时没有数据呢~',
|
emptyText: '啊偶,暂时没有数据呢~',
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
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-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,11 @@
|
|||||||
import { NeedPlugins } from '@/hooks/usePluginAvailable'
|
import { NeedPlugins } from '@/hooks/usePluginAvailable'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
/** 插件名称(与 NeedPlugins 中的 id 对应) */
|
|
||||||
pluginId : string
|
pluginId : string
|
||||||
errorText ?: string
|
errorText ?: string
|
||||||
useDecoration ?: boolean
|
checking : boolean
|
||||||
useBorder ?: boolean
|
|
||||||
customStyle ?: Record<string, string>
|
|
||||||
}>(), {
|
}>(), {
|
||||||
errorText: '',
|
errorText: '',
|
||||||
useDecoration: true,
|
|
||||||
useBorder: true,
|
|
||||||
customStyle: () => ({}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -25,75 +19,40 @@
|
|||||||
const info = NeedPlugins.get(props.pluginId)
|
const info = NeedPlugins.get(props.pluginId)
|
||||||
return info || {
|
return info || {
|
||||||
id: props.pluginId,
|
id: props.pluginId,
|
||||||
name: props.pluginId,
|
name: props.name,
|
||||||
desc: '',
|
desc: '',
|
||||||
logo: '',
|
|
||||||
url: '',
|
url: '',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultStyle = {
|
function handleRefresh() {
|
||||||
width: '80vw',
|
if (props.checking) { return }
|
||||||
borderRadius: '24rpx',
|
emit('on-refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcCustomStyle = computed(() => ({
|
|
||||||
...defaultStyle,
|
|
||||||
...props.customStyle,
|
|
||||||
}))
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view v-if="pluginInfo"
|
<view v-if="pluginInfo" class="mx-auto my-auto box-border flex flex-col items-center justify-center gap-6 text-sm">
|
||||||
class="uh-plugin-unavailable mx-auto my-auto box-border flex flex-col gap-6 p-10 text-[28rpx]"
|
|
||||||
:class="{ border: useBorder, decoration: useDecoration }" :style="[calcCustomStyle]">
|
<wd-icon class-prefix="uhemoji-icon" name="-cry" size="160rpx"></wd-icon>
|
||||||
<!-- 图标 -->
|
|
||||||
<image class="plugin-logo box-border h-[120rpx] w-[120rpx] rounded-3xl" :src="pluginInfo.logo"
|
<view class="box-border text-lg text-gray-900 font-bold">
|
||||||
mode="scaleToFill" />
|
|
||||||
<!-- 名称 -->
|
|
||||||
<view class="plugin-name box-border text-[32rpx] text-[#333] font-bold">
|
|
||||||
{{ pluginInfo.name }}
|
{{ pluginInfo.name }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 自定义错误提示 -->
|
<view v-if="errorText" class=" text-yellow-500 text-sm">
|
||||||
<view v-if="errorText" class="plugin-tip box-border border-2 rounded-xl border-dashed px-5 py-2.5 text-[24rpx]"
|
|
||||||
style="border-color: #f2c97d; color: #f0a020;">
|
|
||||||
{{ errorText }}
|
{{ errorText }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 反馈按钮/复制地址 -->
|
<view class="w-full flex flex-col gap-y-4">
|
||||||
<view class="plugin-btns box-border w-full">
|
<uh-button custom-class="!rounded-full py-2 !uh-shadow-xs" @click="handleRefresh()">
|
||||||
|
{{props.checking?'正在刷新':'刷新试试'}}
|
||||||
|
</uh-button>
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<wd-button type="warning" plain block size="medium" open-type="contact">
|
<uh-button custom-class="bg-white py-2 !rounded-full" open-type="contact">
|
||||||
提交反馈
|
提交反馈
|
||||||
</wd-button>
|
</uh-button>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
<!-- 刷新按钮 -->
|
|
||||||
<view class="flex justify-center">
|
|
||||||
<wd-button size="small" plain type="info" @click="emit('on-refresh')">
|
|
||||||
刷新试试
|
|
||||||
</wd-button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="plugin-copyright text-[20rpx] text-[#999]" style="transform: scale(0.9) translateY(20px);">
|
|
||||||
提示:请确保 Halo 博客已安装相关插件
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.uh-plugin-unavailable {
|
|
||||||
&.border {
|
|
||||||
border: 2rpx solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.decoration {
|
|
||||||
background-color: rgb(255 255 255 / 95%);
|
|
||||||
box-shadow: 0 0 12rpx rgb(226 232 240 / 35%);
|
|
||||||
backdrop-filter: blur(6rpx);
|
|
||||||
border-top: 12rpx solid rgb(3 169 244);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,27 +1,19 @@
|
|||||||
/**
|
import { useAppConfigStore } from '@/store/appConfig';
|
||||||
* 维护拦截 hook(2026-09-04)
|
|
||||||
* 统一「主插件未激活 / 维护模式开启」两项检查与维护页跳转,供入口页与首页等
|
|
||||||
* 页面复用(auto-import 已配置 src/hooks,页面直接调用无需 import)。
|
|
||||||
* 拦截规则:任一命中即跳转 /pages/maintenance/maintenance?from=reason,
|
|
||||||
* 维护页按 reason 展示默认(未配置维护信息)或配置文案。设计见插件
|
|
||||||
* .docs/maintenance-config-design.md §8。
|
|
||||||
*/
|
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
|
||||||
|
|
||||||
/** 维护拦截原因:plugin 主插件未激活 / maintenance 维护模式开启 */
|
/** 维护拦截原因:plugin 主插件未激活 / maintenance 维护模式开启 */
|
||||||
export type MaintenanceInterceptReason = 'plugin' | 'maintenance'
|
export type MaintenanceInterceptReason = 'plugin' | 'maintenance';
|
||||||
|
|
||||||
export interface IMaintenanceInterceptResult {
|
export interface IMaintenanceInterceptResult {
|
||||||
/** 是否命中拦截(需要跳转维护页) */
|
/** 是否命中拦截(需要跳转维护页) */
|
||||||
intercepted: boolean
|
intercepted: boolean;
|
||||||
/** 命中原因;未命中为 null */
|
/** 命中原因;未命中为 null */
|
||||||
reason: MaintenanceInterceptReason | null
|
reason: MaintenanceInterceptReason | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 维护页路径 */
|
/** 维护页路径 */
|
||||||
export const MAINTENANCE_PAGE_PATH = '/pages/maintenance/maintenance'
|
export const MAINTENANCE_PAGE_PATH = '/pages/maintenance/maintenance';
|
||||||
/** 主插件 ID(与 utils/plugin NeedPluginIds.PluginUniHalo 一致) */
|
/** 主插件 ID(与 utils/plugin NeedPluginIds.PluginUniHalo 一致) */
|
||||||
export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo'
|
export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 维护拦截能力:检查 + 跳转封装
|
* 维护拦截能力:检查 + 跳转封装
|
||||||
@@ -31,43 +23,52 @@ export const MAINTENANCE_PLUGIN_ID = 'plugin-uni-halo'
|
|||||||
* onLoad(async () => { if (await interceptOrContinue()) return })
|
* onLoad(async () => { if (await interceptOrContinue()) return })
|
||||||
*/
|
*/
|
||||||
export function useMaintenanceIntercept() {
|
export function useMaintenanceIntercept() {
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore();
|
||||||
/** 主插件可用性 hook(checkIntercept 内 await check 后读取 available) */
|
const reason = ref<MaintenanceInterceptReason | null>(null);
|
||||||
const { available: pluginAvailable, check: checkPluginAvailable } = usePluginAvailable(MAINTENANCE_PLUGIN_ID)
|
/** 主插件可用性 hook(checkIntercept 内 await check 后读取 available) */
|
||||||
|
const { available: pluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
|
||||||
|
pluginId: MAINTENANCE_PLUGIN_ID
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否命中拦截(插件可用性 + 维护模式)。
|
* 检查是否命中拦截(插件可用性 + 维护模式)。
|
||||||
* @param force 是否强制刷新配置(默认 false 走 bootstrap TTL 缓存)
|
* @param force 是否强制刷新配置(默认 false 走 bootstrap TTL 缓存)
|
||||||
*/
|
*/
|
||||||
async function checkIntercept(force = false): Promise<IMaintenanceInterceptResult> {
|
async function checkIntercept(force = false): Promise<IMaintenanceInterceptResult> {
|
||||||
await checkPluginAvailable()
|
await checkPluginAvailable();
|
||||||
if (!pluginAvailable.value)
|
if (!pluginAvailable.value) {
|
||||||
return { intercepted: true, reason: 'plugin' }
|
reason.value = 'plugin';
|
||||||
|
return { intercepted: true, reason: 'plugin' };
|
||||||
|
}
|
||||||
|
|
||||||
const { ok } = await appConfigStore.bootstrap({ force })
|
const { ok } = await appConfigStore.bootstrap({ force });
|
||||||
if (!ok)
|
if (!ok) {
|
||||||
return { intercepted: false, reason: null }
|
reason.value = null;
|
||||||
if (appConfigStore.configs.maintenance)
|
return { intercepted: false, reason: null };
|
||||||
return { intercepted: true, reason: 'maintenance' }
|
}
|
||||||
|
|
||||||
return { intercepted: false, reason: null }
|
if (appConfigStore.configs.maintenance) {
|
||||||
}
|
reason.value = 'maintenance';
|
||||||
|
return { intercepted: true, reason: 'maintenance' };
|
||||||
|
}
|
||||||
|
|
||||||
/** 跳转维护页(带原因参数,供维护页区分默认/配置文案) */
|
return { intercepted: false, reason: null };
|
||||||
function redirectToMaintenance(reason: MaintenanceInterceptReason) {
|
}
|
||||||
uni.redirectTo({ url: `${MAINTENANCE_PAGE_PATH}?from=${reason}` })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** 跳转维护页(带原因参数,供维护页区分默认/配置文案) */
|
||||||
* 一站式:检查并跳转维护页。
|
function redirectToMaintenance(reason: MaintenanceInterceptReason) {
|
||||||
* @returns true = 已命中并跳转,调用方应中断后续逻辑;false = 放行
|
uni.redirectTo({ url: `${MAINTENANCE_PAGE_PATH}?from=${reason}` });
|
||||||
*/
|
}
|
||||||
async function interceptOrContinue(force = false): Promise<boolean> {
|
|
||||||
const { intercepted, reason } = await checkIntercept(force)
|
|
||||||
if (intercepted && reason)
|
|
||||||
redirectToMaintenance(reason)
|
|
||||||
return intercepted
|
|
||||||
}
|
|
||||||
|
|
||||||
return { checkIntercept, redirectToMaintenance, interceptOrContinue }
|
/**
|
||||||
|
* 一站式:检查并跳转维护页。
|
||||||
|
* @returns true = 已命中并跳转,调用方应中断后续逻辑;false = 放行
|
||||||
|
*/
|
||||||
|
async function interceptOrContinue(force = false): Promise<boolean> {
|
||||||
|
const { intercepted, reason } = await checkIntercept(force);
|
||||||
|
if (intercepted && reason) redirectToMaintenance(reason);
|
||||||
|
return intercepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { reason, checkIntercept, redirectToMaintenance, interceptOrContinue };
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-136
@@ -15,150 +15,152 @@
|
|||||||
* handleGetData()
|
* handleGetData()
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue';
|
||||||
import { checkPluginAvailable } from '@/api/halo'
|
import { checkPluginAvailable } from '@/api/halo';
|
||||||
import { checkUrl } from '@/utils/url'
|
import { checkUrl } from '@/utils/url';
|
||||||
|
|
||||||
/** 依赖插件 ID 常量 */
|
/** 依赖插件 ID 常量 */
|
||||||
export const NeedPluginIds = Object.freeze({
|
export const NeedPluginIds = Object.freeze({
|
||||||
PluginUniHalo: 'plugin-uni-halo',
|
PluginUniHalo: 'plugin-uni-halo',
|
||||||
PluginPhotos: 'PluginPhotos',
|
PluginPhotos: 'PluginPhotos',
|
||||||
PluginLinks: 'PluginLinks',
|
PluginLinks: 'PluginLinks',
|
||||||
PluginMoments: 'PluginMoments',
|
PluginMoments: 'PluginMoments',
|
||||||
PluginSearchWidget: 'PluginSearchWidget',
|
PluginSearchWidget: 'PluginSearchWidget',
|
||||||
PluginCommentWidget: 'PluginCommentWidget',
|
PluginCommentWidget: 'PluginCommentWidget',
|
||||||
PluginVote: 'vote',
|
PluginVote: 'vote',
|
||||||
PluginDataStatistics: 'data-statistics',
|
PluginDataStatistics: 'data-statistics'
|
||||||
})
|
});
|
||||||
|
|
||||||
|
interface IPluginAvailableOption {
|
||||||
|
pluginId: string;
|
||||||
|
// 用于提示用户插件未安装或未启用时显示的提示信息
|
||||||
|
tips?: string;
|
||||||
|
// 是否直接隐藏UI,不显示 uh-plugin-unavailable 组件
|
||||||
|
hideUI?: boolean;
|
||||||
|
// 是否默认启用插件
|
||||||
|
initalAvailable?: boolean;
|
||||||
|
// 检查完成后的回调函数
|
||||||
|
callback?: (available: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPluginInfo {
|
export interface IPluginInfo {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
desc: string
|
desc: string;
|
||||||
logo: string
|
logo: string;
|
||||||
url: string
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 依赖插件清单 */
|
/** 依赖插件清单 */
|
||||||
export const NeedPlugins = new Map<string, IPluginInfo>([
|
export const NeedPlugins = new Map<string, IPluginInfo>([
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginUniHalo,
|
NeedPluginIds.PluginUniHalo,
|
||||||
{
|
{
|
||||||
id: 'plugin-uni-halo',
|
id: 'plugin-uni-halo',
|
||||||
name: 'UniHalo配置',
|
name: 'UniHalo配置',
|
||||||
desc: 'uni-halo 核心插件,未安装和启用的情况下,将无法使用 uni-halo,请检查是否已安装和启用',
|
desc: 'uni-halo 核心插件,未安装和启用的情况下,将无法使用 uni-halo,请检查是否已安装和启用',
|
||||||
logo: checkUrl('/plugins/plugin-uni-halo/assets/logo.png'),
|
logo: checkUrl('/plugins/plugin-uni-halo/assets/logo.png'),
|
||||||
url: 'https://www.halo.run/store/apps/app-ryemX',
|
url: 'https://www.halo.run/store/apps/app-ryemX'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginPhotos,
|
NeedPluginIds.PluginPhotos,
|
||||||
{
|
{
|
||||||
id: 'PluginPhotos',
|
id: 'PluginPhotos',
|
||||||
name: '图库管理',
|
name: '图库管理',
|
||||||
desc: '图库功能模块所需要的插件',
|
desc: '图库功能模块所需要的插件',
|
||||||
logo: checkUrl('/plugins/PluginPhotos/assets/logo.svg'),
|
logo: checkUrl('/plugins/PluginPhotos/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-BmQJW',
|
url: 'https://www.halo.run/store/apps/app-BmQJW'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginLinks,
|
NeedPluginIds.PluginLinks,
|
||||||
{
|
{
|
||||||
id: 'PluginLinks',
|
id: 'PluginLinks',
|
||||||
name: '链接管理',
|
name: '链接管理',
|
||||||
desc: '链接管理模块,用于网站友情链接功能模块',
|
desc: '链接管理模块,用于网站友情链接功能模块',
|
||||||
logo: checkUrl('/plugins/PluginLinks/assets/logo.svg'),
|
logo: checkUrl('/plugins/PluginLinks/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-hfbQg',
|
url: 'https://www.halo.run/store/apps/app-hfbQg'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginMoments,
|
NeedPluginIds.PluginMoments,
|
||||||
{
|
{
|
||||||
id: 'PluginMoments',
|
id: 'PluginMoments',
|
||||||
name: '瞬间',
|
name: '瞬间',
|
||||||
desc: '提供一个轻量级的内容图文、视频、音频等内容展示',
|
desc: '提供一个轻量级的内容图文、视频、音频等内容展示',
|
||||||
logo: checkUrl('/plugins/PluginMoments/assets/logo.svg'),
|
logo: checkUrl('/plugins/PluginMoments/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-SnwWD',
|
url: 'https://www.halo.run/store/apps/app-SnwWD'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginSearchWidget,
|
NeedPluginIds.PluginSearchWidget,
|
||||||
{
|
{
|
||||||
id: 'PluginSearchWidget',
|
id: 'PluginSearchWidget',
|
||||||
name: '搜索组件',
|
name: '搜索组件',
|
||||||
desc: '为应用提供统一的搜索组件',
|
desc: '为应用提供统一的搜索组件',
|
||||||
logo: checkUrl('/plugins/PluginSearchWidget/assets/logo.svg'),
|
logo: checkUrl('/plugins/PluginSearchWidget/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-DlacW',
|
url: 'https://www.halo.run/store/apps/app-DlacW'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginCommentWidget,
|
NeedPluginIds.PluginCommentWidget,
|
||||||
{
|
{
|
||||||
id: 'PluginCommentWidget',
|
id: 'PluginCommentWidget',
|
||||||
name: '评论组件',
|
name: '评论组件',
|
||||||
desc: '为用户前台提供完整的评论解决方案',
|
desc: '为用户前台提供完整的评论解决方案',
|
||||||
logo: checkUrl('/plugins/PluginCommentWidget/assets/logo.svg'),
|
logo: checkUrl('/plugins/PluginCommentWidget/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-YXyaD',
|
url: 'https://www.halo.run/store/apps/app-YXyaD'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginVote,
|
NeedPluginIds.PluginVote,
|
||||||
{
|
{
|
||||||
id: 'vote',
|
id: 'vote',
|
||||||
name: '投票管理',
|
name: '投票管理',
|
||||||
desc: '投票模块所需要的插件,用于展示投票和提交投票',
|
desc: '投票模块所需要的插件,用于展示投票和提交投票',
|
||||||
logo: checkUrl('/plugins/vote/assets/logo.png'),
|
logo: checkUrl('/plugins/vote/assets/logo.png'),
|
||||||
url: 'https://www.halo.run/store/apps/app-veyvzyhv',
|
url: 'https://www.halo.run/store/apps/app-veyvzyhv'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NeedPluginIds.PluginDataStatistics,
|
NeedPluginIds.PluginDataStatistics,
|
||||||
{
|
{
|
||||||
id: 'data-statistics',
|
id: 'data-statistics',
|
||||||
name: '数据看板',
|
name: '数据看板',
|
||||||
desc: '为 Halo2 提供强大的数据可视化统计功能,支持 Umami 流量统计、uptime、网站内部数据图表(标签、分类、文章趋势、评论排行、热门文章等)',
|
desc: '为 Halo2 提供强大的数据可视化统计功能,支持 Umami 流量统计、uptime、网站内部数据图表(标签、分类、文章趋势、评论排行、热门文章等)',
|
||||||
logo: checkUrl('/plugins/data-statistics/assets/logo.svg'),
|
logo: checkUrl('/plugins/data-statistics/assets/logo.svg'),
|
||||||
url: 'https://www.halo.run/store/apps/app-rtnbbgfk',
|
url: 'https://www.halo.run/store/apps/app-rtnbbgfk'
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
])
|
]);
|
||||||
|
|
||||||
/**
|
export function usePluginAvailable(option: IPluginAvailableOption) {
|
||||||
* 检查插件是否启用、安装
|
const { pluginId, callback, tips = '功能正在开发中...', initalAvailable = false } = option;
|
||||||
* @param pluginId 插件 id
|
const checking = ref(false);
|
||||||
* @returns true = 安装、启用;false = 未安装启用
|
const available = ref(initalAvailable ?? false);
|
||||||
*/
|
|
||||||
export async function checkNeedPluginAvailable(pluginId: string): Promise<boolean> {
|
/**
|
||||||
try {
|
* 检查插件是否启用、安装
|
||||||
const available = await checkPluginAvailable(pluginId)
|
*/
|
||||||
return available?.data?.available !== false
|
function check(): Promise<boolean> {
|
||||||
}
|
return new Promise<boolean>(async (resolve) => {
|
||||||
catch (err) {
|
try {
|
||||||
console.error(`检查插件 ${pluginId} 可用性失败`, err)
|
checking.value = true;
|
||||||
return false
|
const result = await checkPluginAvailable(pluginId);
|
||||||
}
|
console.log(`检查插件 ${pluginId} 可用性成功`, result);
|
||||||
}
|
available.value = result.data;
|
||||||
|
resolve(result.data);
|
||||||
export function usePluginAvailable(pluginId: string, initial = true) {
|
} catch (err) {
|
||||||
/** 插件是否可用(默认 true,避免首帧闪现插件不可用占位;需要先置 false 的页面传 initial=false) */
|
console.error(`检查插件 ${pluginId} 可用性失败`, err);
|
||||||
const available = ref(initial)
|
available.value = false;
|
||||||
/** 是否校验中 */
|
resolve(false);
|
||||||
const checking = ref(false)
|
} finally {
|
||||||
|
typeof callback === 'function' && callback(available.value);
|
||||||
/**
|
checking.value = false;
|
||||||
* 执行插件可用性校验(刷新 available)
|
}
|
||||||
* @returns 当前是否可用(与 available.value 一致,便于一次性调用方直接取返回值)
|
});
|
||||||
*/
|
}
|
||||||
async function check(): Promise<boolean> {
|
|
||||||
checking.value = true
|
return { pluginId, tips, checking, available, check };
|
||||||
try {
|
|
||||||
available.value = await checkNeedPluginAvailable(pluginId)
|
|
||||||
return available.value
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
checking.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { available, checking, check }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
|||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '关于项目',
|
navigationBarTitleText: '关于项目',
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -37,6 +38,9 @@ onLoad(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-6">
|
<view class="app-page box-border min-h-screen w-screen flex flex-col overflow-hidden bg-page px-4 pb-8 pt-6">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="关于项目" title-color="text-gray-900" />
|
||||||
|
|
||||||
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
|
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
|
||||||
<view class="hero-wrap relative">
|
<view class="hero-wrap relative">
|
||||||
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
|
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
|
||||||
@@ -68,7 +72,7 @@ onLoad(() => {
|
|||||||
:class="index < links.length - 1 ? 'border-b border-black/5' : ''"
|
:class="index < links.length - 1 ? 'border-b border-black/5' : ''"
|
||||||
@click="copyText(link.copy, link.tip)"
|
@click="copyText(link.copy, link.tip)"
|
||||||
>
|
>
|
||||||
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5" :style="{ backgroundColor: link.tileColor + '1A' }">
|
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl border border-black/5" :style="{ backgroundColor: `${link.tileColor}1A` }">
|
||||||
<text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text>
|
<text class="text-[30rpx] font-bold" :style="{ color: link.tileColor }">{{ link.tileLetter }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="min-w-0 flex flex-1 flex-col">
|
<view class="min-w-0 flex flex-1 flex-col">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
|||||||
import { getPostList } from '@/api/halo'
|
import { getPostList } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { useSettingStore } from '@/store/setting'
|
import { useSettingStore } from '@/store/setting'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { checkThumbnailUrl } from '@/utils/url'
|
import { checkThumbnailUrl } from '@/utils/url'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
import type { IPost } from '@/api/types/halo'
|
import type { IPost } from '@/api/types/halo'
|
||||||
@@ -16,6 +17,7 @@ definePage({
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '归档',
|
navigationBarTitleText: '归档',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
|||||||
const globalAppSettings = computed(() => settingStore.settings)
|
const globalAppSettings = computed(() => settingStore.settings)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const activeTabIndex = ref(0)
|
const activeTabIndex = ref(0)
|
||||||
const queryParams = ref({ size: 10, page: 1 })
|
const queryParams = ref({ size: 10, page: 1 })
|
||||||
const result = ref<{ hasNext: boolean }>({ hasNext: false })
|
const result = ref<{ hasNext: boolean }>({ hasNext: false })
|
||||||
@@ -125,14 +127,16 @@ async function handleGetData() {
|
|||||||
const posts = handleGetPosts(filtered)
|
const posts = handleGetPosts(filtered)
|
||||||
dataList.value = handleGetShowDataList(posts)
|
dataList.value = handleGetShowDataList(posts)
|
||||||
cacheDataList.value = filtered
|
cacheDataList.value = filtered
|
||||||
loading.value = 'success'
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
loadMoreText.value = '呜呜,没有更多数据啦~'
|
loadMoreText.value = '呜呜,没有更多数据啦~'
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -142,7 +146,7 @@ async function handleGetData() {
|
|||||||
uni.showLoading({ title: '加载中...' })
|
uni.showLoading({ title: '加载中...' })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
loadMoreText.value = '加载中...'
|
loadMoreText.value = '加载中...'
|
||||||
|
|
||||||
@@ -178,12 +182,14 @@ async function handleGetData() {
|
|||||||
cacheDataList.value = res.data.items
|
cacheDataList.value = res.data.items
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = 'success'
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -192,6 +198,12 @@ async function handleGetData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 顶部 tab 定义(同收藏页胶囊 chip) */
|
||||||
|
const archiveTabs = [
|
||||||
|
{ key: 'month', label: '按月份查看' },
|
||||||
|
{ key: 'year', label: '按年份查看' },
|
||||||
|
]
|
||||||
|
|
||||||
function handleOnTabChange(e: { index: number }) {
|
function handleOnTabChange(e: { index: number }) {
|
||||||
activeTabIndex.value = e.index
|
activeTabIndex.value = e.index
|
||||||
queryParams.value.page = 1
|
queryParams.value.page = 1
|
||||||
@@ -250,27 +262,38 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
||||||
<!-- 顶部 tab(玻璃吸顶,wd-tabs 需用 wd-tab 子组件声明页签) -->
|
<!-- 自定义导航 -->
|
||||||
<view class="archive-tabs uh-global-card-glass sticky top-0 z-10">
|
<uh-navbar default-title="归档" title-color="text-gray-900" />
|
||||||
<wd-tabs v-model="activeTabIndex" align="center" custom-style="background: transparent;" @change="handleOnTabChange">
|
|
||||||
<wd-tab title="按月份查看" />
|
|
||||||
<wd-tab title="按年份查看" />
|
|
||||||
</wd-tabs>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 顶部 tab(吸顶玻璃胶囊 chip,同收藏页) -->
|
||||||
<view v-if="loading !== 'success'">
|
<wd-sticky>
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
|
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
||||||
|
<view class="flex gap-2 px-3 pb-1 pt-3">
|
||||||
|
<view
|
||||||
|
v-for="(tab, index) in archiveTabs" :key="tab.key"
|
||||||
|
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
|
||||||
|
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
|
||||||
|
@click="handleOnTabChange({ index })"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</wd-sticky>
|
||||||
|
|
||||||
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
|
<view v-if="loadingStatus !== 'success'">
|
||||||
|
<uh-data-loading
|
||||||
|
:loading-status="loadingStatus"
|
||||||
|
:empty-text="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'"
|
||||||
|
@refresh="handleGetData"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view v-if="dataList.length === 0" class="list-empty min-h-[60vh] flex items-center justify-center">
|
|
||||||
<wd-empty :description="calcAuditModeEnabled ? '暂无归档的内容' : '暂无归档的文章'" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 时间线 -->
|
<!-- 时间线 -->
|
||||||
<view v-else class="timeline px-4 pt-3">
|
<view class="timeline px-4 pt-3">
|
||||||
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
|
<view v-for="(item, index) in dataList" :key="item.key" class="timeline-item flex">
|
||||||
<view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center">
|
<view class="timeline-left w-[96rpx] flex shrink-0 flex-col items-center">
|
||||||
<view class="timeline-dot mt-2 h-4 w-4 rounded-full bg-secondary shadow-[0_0_0_8rpx_rgba(215,249,76,0.3)]" />
|
<view class="timeline-dot mt-2 h-4 w-4 rounded-full bg-secondary shadow-[0_0_0_8rpx_rgba(215,249,76,0.3)]" />
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
/* ---------------- 跳转 ---------------- */
|
/* ---------------- 跳转 ---------------- */
|
||||||
function handleToCate(category : { metadata : { name : string }, spec : { displayName : string } }) {
|
function handleToCate(category : { metadata : { name : string }, spec : { displayName : string } }) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
url: `/pages-blog/category-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '文章列表',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
||||||
|
}, 3000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="w-full min-h-screen bg-page">
|
||||||
|
<uh-navbar default-title="文章列表" :need-placeholder="true" title-color="text-gray-900"></uh-navbar>
|
||||||
|
|
||||||
|
<!-- 内容区:由于 uh-navbar 内置有占位,所以我们的页面的主要内容应该从这里开始,比如这里就可以设置内边距或者其他样式,最外层的 <view class="w-full min-h-screen bg-page"> 仅作为容器-->
|
||||||
|
<view class="box-border px-3">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<uh-data-loading v-if="loadingStatus!==DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
|
||||||
|
min-height="80vh"></uh-data-loading>
|
||||||
|
|
||||||
|
<!-- 实际内容 -->
|
||||||
|
<view v-else>
|
||||||
|
请求成功啦
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
+32
-29
@@ -1,24 +1,23 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
|
||||||
* 分类详情页(源自旧项目 pagesA/category-detail,新建复刻)
|
|
||||||
* 展示某分类下的文章列表,分页加载
|
|
||||||
*/
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { getCategoryPostList } from '@/api/halo'
|
import { getCategoryPostList } from '@/api/halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { IPost } from '@/api/types/halo'
|
import type { IPost } from '@/api/types/halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '分类详情',
|
navigationBarTitleText: '分类详情',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const queryParams = ref({ size: 10, page: 0 })
|
const queryParams = ref({ size: 10, page: 0 })
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const pageTitle = ref('加载中...')
|
const pageTitle = ref('加载中...')
|
||||||
|
const navbarTitle = ref('分类详情')
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
const dataList = ref<IPost[]>([])
|
const dataList = ref<IPost[]>([])
|
||||||
const isLoadMore = ref(false)
|
const isLoadMore = ref(false)
|
||||||
@@ -26,25 +25,27 @@ const loadMoreText = ref('')
|
|||||||
|
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
loadMoreText.value = '加载中...'
|
loadMoreText.value = '加载中...'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getCategoryPostList(name.value, { ...queryParams.value })
|
const res = await getCategoryPostList(name.value, { ...queryParams.value })
|
||||||
uni.setNavigationBarTitle({ title: `${pageTitle.value} (共${res.data.total}篇)` })
|
navbarTitle.value = `${pageTitle.value} (共${res.data.total}篇)`
|
||||||
hasNext.value = res.data.hasNext
|
hasNext.value = res.data.hasNext
|
||||||
dataList.value = isLoadMore.value
|
dataList.value = isLoadMore.value
|
||||||
? dataList.value.concat(res.data.items)
|
? dataList.value.concat(res.data.items)
|
||||||
: res.data.items
|
: res.data.items
|
||||||
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = 'success'
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -74,6 +75,7 @@ function handleToTopPage(duration = 500) {
|
|||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
name.value = options?.name || ''
|
name.value = options?.name || ''
|
||||||
pageTitle.value = options?.title || '分类详情'
|
pageTitle.value = options?.title || '分类详情'
|
||||||
|
navbarTitle.value = pageTitle.value
|
||||||
handleGetData()
|
handleGetData()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,39 +98,40 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: pageTitle.value,
|
title: pageTitle.value,
|
||||||
path: `/pages-blog/category-detail/category-detail?name=${name.value}&title=${pageTitle.value}`,
|
path: `/pages-blog/category-articles/category-articles?name=${name.value}&title=${pageTitle.value}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
onShareTimeline(() => ({
|
onShareTimeline(() => ({
|
||||||
title: pageTitle.value,
|
title: pageTitle.value,
|
||||||
path: `/pages-blog/category-detail/category-detail?name=${name.value}&title=${pageTitle.value}`,
|
path: `/pages-blog/category-articles/category-articles?name=${name.value}&title=${pageTitle.value}`,
|
||||||
}))
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col py-6" style="background-color: #fafafd;">
|
<view class="app-page min-h-screen w-screen flex flex-col" style="background-color: #fafafd;">
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 自定义导航 -->
|
||||||
<view v-if="loading !== 'success'">
|
<uh-navbar :default-title="navbarTitle" title-color="text-gray-900" />
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
|
|
||||||
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
|
<view v-if="loadingStatus !== 'success'">
|
||||||
|
<uh-data-loading
|
||||||
|
:loading-status="loadingStatus"
|
||||||
|
empty-text="该分类下暂无文章"
|
||||||
|
@refresh="handleGetData"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view v-if="dataList.length === 0" class="empty h-[60vh] flex items-center justify-center">
|
<uh-article-card
|
||||||
<wd-empty description="该分类下暂无文章" />
|
v-for="(article, index) in dataList"
|
||||||
|
:key="index"
|
||||||
|
:article="article"
|
||||||
|
@on-click="handleToArticleDetail"
|
||||||
|
/>
|
||||||
|
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
|
||||||
|
{{ loadMoreText }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
|
||||||
<uh-article-card
|
|
||||||
v-for="(article, index) in dataList"
|
|
||||||
:key="index"
|
|
||||||
:article="article"
|
|
||||||
@on-click="handleToArticleDetail"
|
|
||||||
/>
|
|
||||||
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
|
|
||||||
{{ loadMoreText }}
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<view class="to-top-btn fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
<view class="to-top-btn fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
||||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||||
</view>
|
</view>
|
||||||
@@ -3,14 +3,15 @@
|
|||||||
* 联系博主页(源自旧项目 pagesA/contact,新建复刻)
|
* 联系博主页(源自旧项目 pagesA/contact,新建复刻)
|
||||||
* 数字名片式设计:Hero 名片卡(渐变光斑透卡) + 品牌色字母瓦片联系方式列表,点击复制
|
* 数字名片式设计:Hero 名片卡(渐变光斑透卡) + 品牌色字母瓦片联系方式列表,点击复制
|
||||||
*/
|
*/
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch, watchEffect } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { checkAvatarUrl } from '@/utils/url'
|
import { checkAvatarUrl } from '@/utils/url'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '联系博主',
|
navigationBarTitleText: '联系博主',
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,6 +59,12 @@ const platformMeta: Record<string, { color: string, letter: string }> = {
|
|||||||
|
|
||||||
const calcIsNotEmpty = computed(() => result.value.some(item => item.value !== ''))
|
const calcIsNotEmpty = computed(() => result.value.some(item => item.value !== ''))
|
||||||
|
|
||||||
|
/* ---------------- 加载状态机(本地配置,状态直接推导) ---------------- */
|
||||||
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
watchEffect(() => {
|
||||||
|
updateLoadingStatus(calcIsNotEmpty.value ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
|
||||||
|
})
|
||||||
|
|
||||||
function handleGetData() {
|
function handleGetData() {
|
||||||
for (const key in socialConfig.value) {
|
for (const key in socialConfig.value) {
|
||||||
if (key === 'enabled')
|
if (key === 'enabled')
|
||||||
@@ -84,14 +91,13 @@ function handleOnClick(item: { value: string, name: string }) {
|
|||||||
watch(socialConfig, () => {
|
watch(socialConfig, () => {
|
||||||
handleGetData()
|
handleGetData()
|
||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
onLoad(() => {
|
|
||||||
uni.setNavigationBarTitle({ title: '联系博主' })
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-6">
|
<view class="app-page box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-6">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="联系博主" title-color="text-gray-900" />
|
||||||
|
|
||||||
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
|
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
|
||||||
<view class="hero-wrap relative">
|
<view class="hero-wrap relative">
|
||||||
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
|
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
|
||||||
@@ -113,8 +119,12 @@ onLoad(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 联系方式 -->
|
<!-- 联系方式(状态机:无联系方式 → empty 态) -->
|
||||||
<block v-if="calcIsNotEmpty">
|
<uh-data-loading
|
||||||
|
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="30vh"
|
||||||
|
empty-text="暂无联系方式" empty-sub-text="" @refresh="handleGetData"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
<uh-section-title class="mb-3 mt-6 text-[30rpx]">
|
<uh-section-title class="mb-3 mt-6 text-[30rpx]">
|
||||||
联系方式
|
联系方式
|
||||||
</uh-section-title>
|
</uh-section-title>
|
||||||
@@ -137,9 +147,6 @@ onLoad(() => {
|
|||||||
<wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" />
|
<wd-icon name="copy" size="28rpx" color="#c8c2b4" class="shrink-0" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</template>
|
||||||
<view v-else class="pt-12">
|
|
||||||
<wd-empty description="暂无联系方式" />
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
import { getChartData } from '@/api/uni-halo'
|
import { getChartData } from '@/api/uni-halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { IDataStatistics } from '@/api/uni-halo'
|
import type { IDataStatistics } from '@/api/uni-halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '数据看板',
|
navigationBarTitleText: '数据看板',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const statistics = ref<IDataStatistics>({
|
const statistics = ref<IDataStatistics>({
|
||||||
tags: [],
|
tags: [],
|
||||||
categories: [],
|
categories: [],
|
||||||
@@ -109,7 +111,7 @@
|
|||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
uni.showLoading({ mask: true, title: '加载中...' })
|
uni.showLoading({ mask: true, title: '加载中...' })
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getChartData()
|
const res = await getChartData()
|
||||||
statistics.value = res.data
|
statistics.value = res.data
|
||||||
@@ -118,11 +120,14 @@
|
|||||||
handleTrendArticlesChart()
|
handleTrendArticlesChart()
|
||||||
handleUserCommentsChart()
|
handleUserCommentsChart()
|
||||||
handleTop10ArticlesChart()
|
handleTop10ArticlesChart()
|
||||||
loading.value = 'success'
|
// 五类统计数据全部为空 → 空态
|
||||||
|
const hasData = [res.data.tags, res.data.categories, res.data.articles, res.data.comments, res.data.top10Articles]
|
||||||
|
.some(list => list.length > 0)
|
||||||
|
updateLoadingStatus(hasData ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -155,10 +160,13 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="bg-page box-border min-h-screen w-screen p-3">
|
<view class="bg-page box-border min-h-screen w-screen p-3">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="数据看板" title-color="text-gray-900" />
|
||||||
|
|
||||||
<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>
|
||||||
<uh-data-loading v-if="loading !== 'success'" :loading-status="loading" @refresh="handleGetData" />
|
<uh-data-loading v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" empty-text="暂无统计数据" @refresh="handleGetData" />
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<view v-else class="content flex flex-col gap-3">
|
<view v-else class="content flex flex-col gap-3">
|
||||||
@@ -253,4 +261,4 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAppConfigStore } from '@/store/appConfig'
|
|||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '免责声明',
|
navigationBarTitleText: '免责声明',
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ function copyText(content: string, tips = '复制成功') {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen bg-white p-12 text-[30rpx] text-[#303133] leading-[1.65]">
|
<view class="app-page box-border min-h-screen bg-white p-12 text-[30rpx] text-[#303133] leading-[1.65]">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="免责声明" title-color="text-gray-900" />
|
||||||
|
|
||||||
<!-- 通过配置 -->
|
<!-- 通过配置 -->
|
||||||
<view v-if="disclaimersContent" style="min-height: 100%;" v-html="disclaimersContent" />
|
<view v-if="disclaimersContent" style="min-height: 100%;" v-html="disclaimersContent" />
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getFriendLinkGroupList, getFriendLinkList } from '@/api/halo'
|
|||||||
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
|
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { useSettingStore } from '@/store/setting'
|
import { useSettingStore } from '@/store/setting'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||||
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
import { NeedPluginIds } from '@/hooks/usePluginAvailable'
|
||||||
import type { ILink, ILinkGroup } from '@/api/types/halo'
|
import type { ILink, ILinkGroup } from '@/api/types/halo'
|
||||||
@@ -20,6 +21,7 @@ definePage({
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '友情链接',
|
navigationBarTitleText: '友情链接',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -40,6 +42,12 @@ const { available: miniPluginAvailable, check: checkMiniPluginAvailable } = useP
|
|||||||
/* ---------------- tabs ---------------- */
|
/* ---------------- tabs ---------------- */
|
||||||
const activeTabIndex = ref(0)
|
const activeTabIndex = ref(0)
|
||||||
|
|
||||||
|
/** 顶部 tab 定义(同收藏页胶囊 chip;审核模式下小程序 tab 隐藏) */
|
||||||
|
const friendLinkTabs = computed(() => [
|
||||||
|
{ key: 'site', label: '站点' },
|
||||||
|
...(appConfigStore.auditModeEnabled ? [] : [{ key: 'mini', label: '小程序' }]),
|
||||||
|
])
|
||||||
|
|
||||||
function handleOnTabChange(e: { index: number }) {
|
function handleOnTabChange(e: { index: number }) {
|
||||||
activeTabIndex.value = e.index
|
activeTabIndex.value = e.index
|
||||||
}
|
}
|
||||||
@@ -52,7 +60,7 @@ watch(() => appConfigStore.auditModeEnabled, (enabled) => {
|
|||||||
|
|
||||||
/* ==================== 站点 tab(plugin-links) ==================== */
|
/* ==================== 站点 tab(plugin-links) ==================== */
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus: siteLoadingStatus, updateLoadingStatus: updateSiteLoadingStatus } = useDataLoadingStatus()
|
||||||
const queryParams = ref({ size: 10, page: 1 })
|
const queryParams = ref({ size: 10, page: 1 })
|
||||||
const detail = ref<{ show: boolean, data: ILink | null }>({ show: false, data: null })
|
const detail = ref<{ show: boolean, data: ILink | null }>({ show: false, data: null })
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
@@ -77,13 +85,13 @@ async function handleGetLinkGroupData() {
|
|||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
loading.value = 'loading'
|
updateSiteLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
loadMoreText.value = ''
|
loadMoreText.value = ''
|
||||||
|
|
||||||
@@ -106,13 +114,15 @@ async function handleGetData() {
|
|||||||
}))
|
}))
|
||||||
dataList.value = dataList.value.concat(list)
|
dataList.value = dataList.value.concat(list)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = 'success'
|
updateSiteLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
loadMoreText.value = res.data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateSiteLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -163,24 +173,26 @@ function calcSiteThumbnail(val?: string): string {
|
|||||||
|
|
||||||
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
|
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const miniLoading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus: miniLoadingStatus, updateLoadingStatus: updateMiniLoadingStatus } = useDataLoadingStatus()
|
||||||
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
|
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
|
||||||
const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null })
|
const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null })
|
||||||
const applyShow = ref(false)
|
const applyShow = ref(false)
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetMiniProgramLinks() {
|
async function handleGetMiniProgramLinks() {
|
||||||
miniLoading.value = 'loading'
|
updateMiniLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getMiniProgramLinkGroupedList()
|
const res = await getMiniProgramLinkGroupedList()
|
||||||
miniGroups.value = res.data || []
|
miniGroups.value = res.data || []
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
miniLoading.value = 'success'
|
updateMiniLoadingStatus(
|
||||||
|
miniGroups.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
miniLoading.value = 'error'
|
updateMiniLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -321,13 +333,24 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
||||||
<!-- 顶部 tabs -->
|
<!-- 自定义导航 -->
|
||||||
<view class="tabs-wrap uh-global-card-glass sticky top-0 z-10 px-6">
|
<uh-navbar default-title="友情链接" title-color="text-gray-900" />
|
||||||
<wd-tabs v-model="activeTabIndex" align="left" custom-style="background: transparent;" @change="handleOnTabChange">
|
|
||||||
<wd-tab title="站点" />
|
<!-- 顶部 tab(吸顶玻璃胶囊 chip,同收藏页) -->
|
||||||
<wd-tab v-if="!appConfigStore.auditModeEnabled" title="小程序" />
|
<wd-sticky>
|
||||||
</wd-tabs>
|
<scroll-view scroll-x class="w-full whitespace-nowrap">
|
||||||
</view>
|
<view class="flex gap-2 px-3 pb-1 pt-3">
|
||||||
|
<view
|
||||||
|
v-for="(tab, index) in friendLinkTabs" :key="tab.key"
|
||||||
|
class="uh-global-card-glass uh-shadow-xs inline-block border rounded-2xl px-5 py-1.5 text-sm"
|
||||||
|
:class="activeTabIndex === index ? 'bg-primary font-bold' : 'text-gray-500'"
|
||||||
|
@click="handleOnTabChange({ index })"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</wd-sticky>
|
||||||
|
|
||||||
<!-- ==================== 站点 tab ==================== -->
|
<!-- ==================== 站点 tab ==================== -->
|
||||||
<template v-if="activeTabIndex === 0">
|
<template v-if="activeTabIndex === 0">
|
||||||
@@ -338,18 +361,18 @@ onReachBottom(() => {
|
|||||||
@on-refresh="handleGetLinkGroupData"
|
@on-refresh="handleGetLinkGroupData"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
<view v-if="loading !== 'success'">
|
<view v-if="siteLoadingStatus !== 'success'">
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
|
<uh-data-loading
|
||||||
|
:loading-status="siteLoadingStatus"
|
||||||
|
empty-text="啊偶,博主还没有朋友呢~"
|
||||||
|
@refresh="handleGetData"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="content pt-4">
|
<view v-else class="content pt-4">
|
||||||
<view v-if="dataList.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
|
|
||||||
<wd-empty description="啊偶,博主还没有朋友呢~" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 友链列表 -->
|
<!-- 友链列表 -->
|
||||||
<view v-else class="link-list flex flex-col gap-4 px-4 pb-4">
|
<view class="link-list flex flex-col gap-4 px-4 pb-4">
|
||||||
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName">
|
<view v-for="link in dataList" :key="link.metadata?.name || link.spec.displayName">
|
||||||
<!-- 色彩版 -->
|
<!-- 色彩版 -->
|
||||||
<view
|
<view
|
||||||
@@ -436,19 +459,18 @@ onReachBottom(() => {
|
|||||||
@on-refresh="handleGetMiniProgramLinks"
|
@on-refresh="handleGetMiniProgramLinks"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
<view v-if="miniLoading !== 'success'">
|
<view v-if="miniLoadingStatus !== 'success'">
|
||||||
<uh-data-loading :loading-status="miniLoading" @refresh="handleGetMiniProgramLinks" />
|
<uh-data-loading
|
||||||
|
:loading-status="miniLoadingStatus"
|
||||||
|
empty-text="还没有收录的小程序呢~"
|
||||||
|
@refresh="handleGetMiniProgramLinks"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="content flex flex-1 flex-col">
|
<view v-else class="content flex flex-1 flex-col">
|
||||||
<!-- 空态 -->
|
|
||||||
<view v-if="miniGroups.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
|
|
||||||
<wd-empty description="还没有收录的小程序呢~" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 分组列表 -->
|
<!-- 分组列表 -->
|
||||||
<view v-else class="mini-link-list flex-1 px-6 py-4">
|
<view class="mini-link-list flex-1 px-6 py-4">
|
||||||
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
|
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
|
||||||
<view class="group-title mb-4 flex items-center">
|
<view class="group-title mb-4 flex items-center">
|
||||||
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
|
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full bg-secondary" />
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ import { generateUUID } from '@/utils/uuid'
|
|||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
import { randomTagColor } from '@/utils/random'
|
import { randomTagColor } from '@/utils/random'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
import { markdownConfig } from '@/config/markdown'
|
||||||
import { useDataLoading } from '@/hooks/useDataLoading'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { IMoment } from '@/api/types/halo'
|
import type { IMoment } from '@/api/types/halo'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '瞬间详情',
|
navigationBarTitleText: '瞬间详情',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
// 下拉/回弹露出的窗口底色对齐页面底色
|
// 下拉/回弹露出的窗口底色对齐页面底色
|
||||||
backgroundColor: '#f6f3ee',
|
backgroundColor: '#f6f3ee',
|
||||||
},
|
},
|
||||||
@@ -88,24 +89,29 @@ function buildMomentCard(res: IMoment): MomentCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 数据加载(useDataLoading 试点:状态由 hook 接管) ---------------- */
|
/* ---------------- 数据加载(useDataLoadingStatus 状态机接管) ---------------- */
|
||||||
const { data: moment, status, run: loadMoment } = useDataLoading(
|
const moment = ref<MomentCard | null>(null)
|
||||||
async (): Promise<MomentCard> => {
|
const { loadingStatus: status, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
|
||||||
|
async function loadMoment() {
|
||||||
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
|
try {
|
||||||
const res = await getMomentByName(queryName.value)
|
const res = await getMomentByName(queryName.value)
|
||||||
uni.setNavigationBarTitle({ title: '瞬间详情' })
|
const card = buildMomentCard(res.data)
|
||||||
return buildMomentCard(res.data)
|
moment.value = card
|
||||||
},
|
// 对象无键视为空 → 空态(与原 useDataLoading 默认判空一致)
|
||||||
{
|
updateLoadingStatus(
|
||||||
onSuccess: (card) => {
|
Object.keys(card).length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
nextTick(() => {
|
)
|
||||||
createVideoContexts(card.videos || [])
|
nextTick(() => {
|
||||||
})
|
createVideoContexts(card.videos || [])
|
||||||
},
|
})
|
||||||
onError: () => {
|
}
|
||||||
uni.setNavigationBarTitle({ title: '瞬间详情' })
|
catch (err) {
|
||||||
},
|
console.error('[moment-detail] 加载失败', err)
|
||||||
},
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */
|
/** 标签颜色(随机模式下按数据稳定,避免每次渲染重新随机变色) */
|
||||||
const calcTagColors = computed(() => {
|
const calcTagColors = computed(() => {
|
||||||
@@ -255,7 +261,6 @@ function handleToTopPage(duration = 500) {
|
|||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
uni.setNavigationBarTitle({ title: '瞬间加载中...' })
|
|
||||||
queryName.value = options?.name || ''
|
queryName.value = options?.name || ''
|
||||||
loadMoment()
|
loadMoment()
|
||||||
})
|
})
|
||||||
@@ -280,6 +285,9 @@ onShareTimeline(() => ({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen bg-page px-4 pb-8 pt-4">
|
<view class="app-page box-border min-h-screen w-screen bg-page px-4 pb-8 pt-4">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="瞬间详情" title-color="text-gray-900" />
|
||||||
|
|
||||||
<!-- 状态区(加载中/失败可重试/空) -->
|
<!-- 状态区(加载中/失败可重试/空) -->
|
||||||
<uh-data-loading
|
<uh-data-loading
|
||||||
v-if="status !== 'success'"
|
v-if="status !== 'success'"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getNoticeDetail } from '@/api/uni-halo'
|
import { getNoticeDetail } from '@/api/uni-halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { checkIsUrl } from '@/utils/url'
|
import { checkIsUrl } from '@/utils/url'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
import { markdownConfig } from '@/config/markdown'
|
||||||
import type { INoticeDetail } from '@/api/types/uni-halo'
|
import type { INoticeDetail } from '@/api/types/uni-halo'
|
||||||
@@ -15,11 +16,12 @@ import type { INoticeDetail } from '@/api/types/uni-halo'
|
|||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '公告详情',
|
navigationBarTitleText: '公告详情',
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(true)
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const notFound = ref(false)
|
const name = ref('')
|
||||||
const detail = ref<INoticeDetail | null>(null)
|
const detail = ref<INoticeDetail | null>(null)
|
||||||
|
|
||||||
const spec = computed(() => detail.value?.spec)
|
const spec = computed(() => detail.value?.spec)
|
||||||
@@ -41,17 +43,6 @@ function formatDate(value?: string): string {
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBack() {
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
if (pages.length > 1) {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
uni.switchTab({ url: '/pages/tabbar/home/home' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 外链 → 复用 website 承载页 */
|
|
||||||
function handleToExternal() {
|
function handleToExternal() {
|
||||||
if (!spec.value?.link)
|
if (!spec.value?.link)
|
||||||
return
|
return
|
||||||
@@ -63,57 +54,44 @@ function handleToExternal() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(async (options) => {
|
/** 加载公告详情(状态机;404/无数据 → 空态,其余错误 → error 态,可重试) */
|
||||||
const name = options?.name || ''
|
async function loadDetail() {
|
||||||
if (!name) {
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
notFound.value = true
|
if (!name.value) {
|
||||||
loading.value = false
|
updateLoadingStatus(DataLoadingStatusEnum.Empty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getNoticeDetail(name)
|
const res = await getNoticeDetail(name.value)
|
||||||
detail.value = res.data || null
|
detail.value = res.data || null
|
||||||
if (!detail.value?.spec) {
|
updateLoadingStatus(
|
||||||
notFound.value = true
|
detail.value?.spec ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('公告详情加载失败', err)
|
console.error('公告详情加载失败', err)
|
||||||
const code = (err as { code?: number }).code
|
const code = (err as { code?: number }).code
|
||||||
notFound.value = code === 404
|
updateLoadingStatus(code === 404 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Error)
|
||||||
if (code !== 404) {
|
|
||||||
uni.showToast({ icon: 'none', title: '公告加载失败' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
name.value = options?.name || ''
|
||||||
|
loadDetail()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="notice-detail min-h-screen w-screen bg-white pb-12">
|
<view class="notice-detail min-h-screen w-screen bg-white pb-12">
|
||||||
<!-- 加载中 -->
|
<!-- 自定义导航 -->
|
||||||
<view v-if="loading" class="flex flex-col items-center justify-center py-40">
|
<uh-navbar default-title="公告详情" title-color="text-gray-900" />
|
||||||
<text class="text-[26rpx] text-[#999]">
|
|
||||||
加载中...
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 不存在/已下线 -->
|
<!-- 加载/错误/空态(状态机) -->
|
||||||
<view v-else-if="notFound" class="flex flex-col items-center justify-center py-40">
|
<uh-data-loading
|
||||||
<text class="text-[60rpx]">
|
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="55vh"
|
||||||
🕳️
|
error-text="公告加载失败" empty-text="公告不存在或已下线" empty-sub-text=""
|
||||||
</text>
|
@refresh="loadDetail"
|
||||||
<text class="mt-6 text-[26rpx] text-[#999]">
|
/>
|
||||||
公告不存在或已下线
|
|
||||||
</text>
|
|
||||||
<view class="mt-10">
|
|
||||||
<wd-button size="small" type="primary" plain @click="handleBack">
|
|
||||||
返回
|
|
||||||
</wd-button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 正文 -->
|
<!-- 正文 -->
|
||||||
<view v-else class="px-6 py-6">
|
<view v-else class="px-6 py-6">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { computed, ref } from 'vue'
|
|||||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
import { getPostListByKeyword } from '@/api/halo'
|
import { getPostListByKeyword } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
import { markdownConfig } from '@/config/markdown'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
import { debounce } from '@/utils/debounce'
|
import { debounce } from '@/utils/debounce'
|
||||||
@@ -15,6 +16,7 @@ definePage({
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '内容搜索',
|
navigationBarTitleText: '内容搜索',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ const uniHaloPluginId = 'plugin-search-widget'
|
|||||||
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
limit: 50,
|
limit: 50,
|
||||||
@@ -52,19 +54,26 @@ const calcAniDelays = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 空态文案(无关键词提示输入;有关键词提示未搜到) */
|
||||||
|
const emptyText = computed(() =>
|
||||||
|
queryParams.value.keyword ? `未搜到 ${queryParams.value.keyword} 相关内容` : '请输入关键词搜索',
|
||||||
|
)
|
||||||
|
|
||||||
/* ---------------- 搜索 ---------------- */
|
/* ---------------- 搜索 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (calcAuditModeEnabled.value)
|
if (calcAuditModeEnabled.value)
|
||||||
return
|
return
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getPostListByKeyword({ ...queryParams.value })
|
const res = await getPostListByKeyword({ ...queryParams.value })
|
||||||
loading.value = 'success'
|
|
||||||
dataList.value = (res.data as unknown as { hits?: typeof dataList.value }).hits || []
|
dataList.value = (res.data as unknown as { hits?: typeof dataList.value }).hits || []
|
||||||
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -76,7 +85,7 @@ async function handleGetData() {
|
|||||||
function handleOnSearch() {
|
function handleOnSearch() {
|
||||||
if (!queryParams.value.keyword) {
|
if (!queryParams.value.keyword) {
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
loading.value = 'success'
|
updateLoadingStatus(DataLoadingStatusEnum.Empty)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
handleGetData()
|
handleGetData()
|
||||||
@@ -128,7 +137,7 @@ onLoad(async () => {
|
|||||||
}
|
}
|
||||||
// 关键词非空(如带参进入)时自动搜索,否则展示空态
|
// 关键词非空(如带参进入)时自动搜索,否则展示空态
|
||||||
if (!queryParams.value.keyword) {
|
if (!queryParams.value.keyword) {
|
||||||
loading.value = 'success'
|
updateLoadingStatus(DataLoadingStatusEnum.Empty)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
handleGetData()
|
handleGetData()
|
||||||
@@ -145,64 +154,67 @@ onPullDownRefresh(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col bg-page pb-6">
|
<view class="box-border min-h-screen w-screen flex flex-col bg-page pb-6">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="内容搜索" title-color="text-gray-900" />
|
||||||
|
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable
|
||||||
v-if="!uniHaloPluginAvailable"
|
v-if="!uniHaloPluginAvailable"
|
||||||
:plugin-id="uniHaloPluginId"
|
:plugin-id="uniHaloPluginId"
|
||||||
error-text="检测到当前插件没有安装或者启用,无法使用搜索功能哦,请联系管理员"
|
error-text="检测到当前插件没有安装或者启用,无法使用搜索功能哦,请联系管理员"
|
||||||
@on-refresh="handleOnSearch"
|
@on-refresh="handleOnSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 顶部搜索框(玻璃吸顶,呼应首页搜索条) -->
|
<!-- 顶部搜索框-->
|
||||||
<view class="search-bar uh-global-card-glass sticky top-0 z-10 px-3 py-2">
|
<wd-sticky class="">
|
||||||
<view class="search-input h-[72rpx] flex items-center gap-3 rounded-full bg-[#f6f3ee] px-5">
|
<view class="w-screen box-border px-3 py-2">
|
||||||
<wd-icon name="search" size="16px" color="#a8a294" />
|
<view class="uh-global-card-glass h-9 flex items-center gap-3 rounded-full px-5">
|
||||||
<input
|
<wd-icon name="search" size="16px" />
|
||||||
v-model="queryParams.keyword"
|
<input
|
||||||
class="search-field flex-1 text-[26rpx] text-gray-900"
|
v-model="queryParams.keyword"
|
||||||
placeholder="哈喽,想看些什么呢~"
|
class="flex-1 text-[26rpx] text-gray-900"
|
||||||
placeholder-class="text-gray-400"
|
placeholder="哈喽,想看些什么呢~"
|
||||||
confirm-type="search"
|
placeholder-class="text-gray-400"
|
||||||
@input="handleOnInput"
|
confirm-type="search"
|
||||||
@confirm="handleOnSearch"
|
@input="handleOnInput"
|
||||||
>
|
@confirm="handleOnSearch"
|
||||||
<view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()">
|
>
|
||||||
<wd-icon name="close" size="14px" color="#a8a294" />
|
<view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()">
|
||||||
|
<wd-icon name="close" size="14px" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</wd-sticky>
|
||||||
|
|
||||||
<!-- 骨架屏 -->
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
<view v-if="loading === 'loading'" class="loading-wrap p-3">
|
<uh-data-loading
|
||||||
<wd-skeleton :row="4" :animated="true" />
|
v-if="loadingStatus !== 'success'"
|
||||||
</view>
|
:loading-status="loadingStatus"
|
||||||
<view v-else-if="loading === 'error'" class="min-h-[60vh] flex items-center justify-center content-empty">
|
min-height="65vh"
|
||||||
<wd-empty description="搜索异常" />
|
error-text="搜索异常"
|
||||||
</view>
|
:empty-text="emptyText"
|
||||||
|
@refresh="handleOnSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域(成功态) -->
|
||||||
<view v-else class="content pt-4">
|
<view v-else class="box-border pt-2 px-3 flex flex-col gap-y-3">
|
||||||
<view v-if="dataList.length === 0" class="min-h-[60vh] flex items-center justify-center content-empty">
|
<block v-if="dataList.length !== 0">
|
||||||
<wd-empty v-if="!queryParams.keyword" description="请输入关键词搜索" />
|
|
||||||
<wd-empty v-else :description="`未搜到 ${queryParams.keyword} 相关内容`" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<block v-else>
|
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in dataList"
|
v-for="(item, index) in dataList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="article-card fade-up uh-global-card-glass mx-4 mb-4 flex flex-col overflow-hidden rounded-2xl p-4"
|
class="uh-global-card-glass uh-shadow-xs border flex flex-col overflow-hidden rounded-2xl p-4"
|
||||||
:style="{ animationDelay: `${calcAniDelays[index]}ms` }"
|
:style="{ animationDelay: `${calcAniDelays[index]}ms` }"
|
||||||
@click="handleToDetail(item)"
|
@click="handleToDetail(item)"
|
||||||
>
|
>
|
||||||
<view class="card-head mb-3 flex items-center">
|
<view class="card-head mb-3 flex items-center">
|
||||||
<view
|
<view
|
||||||
class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-0.5 text-[20rpx] leading-none"
|
class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-1 text-xs leading-none"
|
||||||
:class="isArticle(item) ? 'bg-secondary text-[#4d7c0f]' : 'bg-[#e8e3d8] text-gray-600'"
|
:class="isArticle(item) ? 'bg-secondary text-gray-900' : 'bg-blue-500 text-gray-50'"
|
||||||
>
|
>
|
||||||
{{ isArticle(item) ? '文章' : '瞬间' }}
|
{{ isArticle(item) ? '文章' : '瞬间' }}
|
||||||
</view>
|
</view>
|
||||||
<text class="card-title flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[28rpx] text-gray-900 font-bold">{{ item.title }}</text>
|
<text class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-gray-900 font-bold">{{ item.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<mp-html
|
<mp-html
|
||||||
class="evan-markdown"
|
class="evan-markdown"
|
||||||
@@ -219,14 +231,7 @@ onPullDownRefresh(() => {
|
|||||||
:show-language-name="true"
|
:show-language-name="true"
|
||||||
copy-by-long-press
|
copy-by-long-press
|
||||||
/>
|
/>
|
||||||
<view class="card-foot mt-3 flex items-center">
|
</view>
|
||||||
<text class="text-[24rpx] text-gray-400">{{ item.updateTimestamp ? `最近更新:${formatTimeUtil({ d: item.updateTimestamp, f: 'yyyy年MM月dd日 HH点mm分ss秒' })}` : '' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="to-top-btn uh-global-card-glass fixed bottom-[100rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full" @click="handleToTopPage()">
|
|
||||||
<wd-icon name="arrow-up" size="20px" color="#6b7280" />
|
|
||||||
</view>
|
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { DefaultAppSettings } from '@/config/appSettings'
|
import { DefaultAppSettings } from '@/config/appSettings'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
@@ -157,6 +157,32 @@
|
|||||||
handleCloseEnum()
|
handleCloseEnum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- wd-picker 弹层数据 ---------------- */
|
||||||
|
/** 枚举弹层列(首项「跟随站点默认」,空串哨兵映射 null) */
|
||||||
|
const enumColumns = computed(() => {
|
||||||
|
const def = enumSheet.value.def
|
||||||
|
if (!def)
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{ label: '跟随站点默认', value: '' },
|
||||||
|
...(def.options || []).map(opt => ({ label: opt.label, value: opt.value })),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 当前选中列值(单列;跟随站点默认时为空串) */
|
||||||
|
const enumValue = computed(() => {
|
||||||
|
const def = enumSheet.value.def
|
||||||
|
if (!def)
|
||||||
|
return ['']
|
||||||
|
return [isFollowing(def) ? '' : String(valueOf(def.path) ?? '')]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** wd-picker 确认:空串哨兵还原为「跟随站点默认」 */
|
||||||
|
function handlePickerConfirm(payload: { value: (string | number)[] }) {
|
||||||
|
const picked = String(payload.value[0] ?? '')
|
||||||
|
handleChooseEnum(picked === '' ? null : picked)
|
||||||
|
}
|
||||||
|
|
||||||
/** 当前枚举项是否处于「跟随站点默认」 */
|
/** 当前枚举项是否处于「跟随站点默认」 */
|
||||||
function isFollowing(def : PrefDef) : boolean {
|
function isFollowing(def : PrefDef) : boolean {
|
||||||
return !isOverridden(def.path)
|
return !isOverridden(def.path)
|
||||||
@@ -247,7 +273,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<wd-switch :model-value="def.path" @change="handleSwitchChange(def, $event)" />
|
<wd-switch :model-value="valueOf(def.path) === true" @change="handleSwitchChange(def, $event)" />
|
||||||
</view>
|
</view>
|
||||||
<!-- 枚举选择(指示器位置) -->
|
<!-- 枚举选择(指示器位置) -->
|
||||||
<view v-else class="pick-row flex items-center justify-between px-4 py-4"
|
<view v-else class="pick-row flex items-center justify-between px-4 py-4"
|
||||||
@@ -276,34 +302,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 底部操作栏(玻璃悬浮-->
|
<!-- 底部操作栏(玻璃悬浮) -->
|
||||||
<view class="box-border w-full px-2">
|
<view class="box-border w-full px-2">
|
||||||
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full"
|
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full"
|
||||||
@click="handleResetAll">恢复默认</uh-button>
|
@click="handleResetAll">恢复默认</uh-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 枚举选择底部弹层 -->
|
<!-- 枚举选择弹层(wd-picker 自带底部弹层与工具栏) -->
|
||||||
<wd-popup v-model="enumSheet.show" position="bottom" closable custom-style="border-radius: 24rpx 24rpx 0 0;"
|
<wd-picker
|
||||||
@close="handleCloseEnum">
|
v-model:visible="enumSheet.show" :title="enumSheet.def?.label || ''" :columns="enumColumns"
|
||||||
<view v-if="enumSheet.def" class="enum-sheet box-border w-full pb-[env(safe-area-inset-bottom)]">
|
:model-value="enumValue" confirm-button-text="确定" cancel-button-text="取消"
|
||||||
<view class="enum-title py-6 text-center text-[30rpx] text-gray-900 font-bold">
|
@confirm="handlePickerConfirm"
|
||||||
{{ enumSheet.def.label }}
|
/>
|
||||||
</view>
|
|
||||||
<view class="enum-item flex items-center justify-between px-6 py-5"
|
|
||||||
:class="isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
|
|
||||||
@click="handleChooseEnum(null)">
|
|
||||||
<text class="text-[28rpx]">跟随站点默认</text>
|
|
||||||
<wd-icon v-if="isFollowing(enumSheet.def)" name="check" size="16px" color="#4d7c0f" />
|
|
||||||
</view>
|
|
||||||
<view v-for="opt in enumSheet.def.options" :key="opt.value"
|
|
||||||
class="enum-item flex items-center justify-between border-t border-[#f0ece2] px-6 py-5"
|
|
||||||
:class="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
|
|
||||||
@click="handleChooseEnum(opt.value)">
|
|
||||||
<text class="text-[28rpx]">{{ opt.label }}</text>
|
|
||||||
<wd-icon v-if="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def)"
|
|
||||||
name="check" size="16px" color="#4d7c0f" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</wd-popup>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { getVoteDetail, submitVote } from '@/api/uni-halo'
|
import { getVoteDetail, submitVote } from '@/api/uni-halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { calcVotePercent, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
|
import { calcVotePercent, VOTE_TYPES, voteCacheUtil } from '@/utils/vote'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
|
import type { IVote, IVoteDetail, IVoteOption } from '@/api/types/uni-halo'
|
||||||
@@ -14,11 +15,12 @@ definePage({
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '投票详情',
|
navigationBarTitleText: '投票详情',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
const pageTitle = ref('加载中...')
|
const pageTitle = ref('加载中...')
|
||||||
const safeAreaBottom = ref(24)
|
const safeAreaBottom = ref(24)
|
||||||
@@ -76,7 +78,7 @@ function handleCalcIsChecked(option: { id?: string }): boolean {
|
|||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
pageTitle.value = '加载中...'
|
pageTitle.value = '加载中...'
|
||||||
try {
|
try {
|
||||||
const res = await getVoteDetail(name.value)
|
const res = await getVoteDetail(name.value)
|
||||||
@@ -133,19 +135,20 @@ async function handleGetData() {
|
|||||||
vote.value = tempVote
|
vote.value = tempVote
|
||||||
detail.value = res
|
detail.value = res
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = 'success'
|
updateLoadingStatus(
|
||||||
|
tempVote ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty,
|
||||||
|
)
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
pageTitle.value = '加载失败,请重试...'
|
pageTitle.value = '加载失败,请重试...'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
uni.setNavigationBarTitle({ title: pageTitle.value })
|
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,18 +272,21 @@ onShareTimeline(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen flex flex-col py-6 pb-[160rpx]" style="background-color: #fafafd;">
|
<view class="app-page box-border min-h-screen w-screen flex flex-col pb-[160rpx]" style="background-color: #fafafd;">
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 自定义导航 -->
|
||||||
<view v-if="loading !== 'success'">
|
<uh-navbar :default-title="pageTitle" title-color="text-gray-900" />
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
|
|
||||||
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
|
<view v-if="loadingStatus !== 'success'">
|
||||||
|
<uh-data-loading
|
||||||
|
:loading-status="loadingStatus"
|
||||||
|
empty-text="未查询到数据"
|
||||||
|
@refresh="handleGetData"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view v-if="!vote" class="empty h-[60vh] flex items-center justify-center">
|
<template v-if="vote">
|
||||||
<wd-empty description="未查询到数据" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<block v-else>
|
|
||||||
<!-- 投票信息 -->
|
<!-- 投票信息 -->
|
||||||
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
|
<view class="vote-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm">
|
||||||
<view class="sub-title relative box-border pl-6 text-[30rpx]">
|
<view class="sub-title relative box-border pl-6 text-[30rpx]">
|
||||||
@@ -392,7 +398,7 @@ onShareTimeline(() => ({
|
|||||||
提交投票
|
提交投票
|
||||||
</wd-button>
|
</wd-button>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</template>
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
import { getVoteList } from '@/api/uni-halo'
|
import { getVoteList } from '@/api/uni-halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import type { IVoteItem } from '@/api/types/uni-halo'
|
import type { IVoteItem } from '@/api/types/uni-halo'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ definePage({
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '投票中心',
|
navigationBarTitleText: '投票中心',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
|
navigationStyle: 'custom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ const uniHaloPluginId = 'plugin-vote'
|
|||||||
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
const dataList = ref<IVoteItem[]>([])
|
const dataList = ref<IVoteItem[]>([])
|
||||||
const hasNext = ref(false)
|
const hasNext = ref(false)
|
||||||
const queryParams = ref({ page: 1, size: 10 })
|
const queryParams = ref({ page: 1, size: 10 })
|
||||||
@@ -33,7 +35,9 @@ const loadMoreText = ref('加载中...')
|
|||||||
|
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (calcAuditModeEnabled.value) {
|
if (calcAuditModeEnabled.value) {
|
||||||
loading.value = 'success'
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
loadMoreText.value = '呜呜,没有更多数据啦~'
|
loadMoreText.value = '呜呜,没有更多数据啦~'
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
@@ -41,22 +45,24 @@ async function handleGetData() {
|
|||||||
|
|
||||||
uni.showLoading({ mask: true, title: '加载中...' })
|
uni.showLoading({ mask: true, title: '加载中...' })
|
||||||
if (!isLoadMore.value) {
|
if (!isLoadMore.value) {
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
}
|
}
|
||||||
loadMoreText.value = '加载中...'
|
loadMoreText.value = '加载中...'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getVoteList({ ...queryParams.value })
|
const res = await getVoteList({ ...queryParams.value })
|
||||||
loading.value = 'success'
|
|
||||||
hasNext.value = res.data.hasNext || false
|
hasNext.value = res.data.hasNext || false
|
||||||
dataList.value = isLoadMore.value
|
dataList.value = isLoadMore.value
|
||||||
? dataList.value.concat(res.data.items)
|
? dataList.value.concat(res.data.items)
|
||||||
: res.data.items
|
: res.data.items
|
||||||
|
updateLoadingStatus(
|
||||||
|
dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success,
|
||||||
|
)
|
||||||
loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
loadMoreText.value = hasNext.value ? '上拉加载更多' : '呜呜,没有更多数据啦~'
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -117,6 +123,9 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
<view class="app-page min-h-screen w-screen flex flex-col bg-page">
|
||||||
|
<!-- 自定义导航 -->
|
||||||
|
<uh-navbar default-title="投票中心" title-color="text-gray-900" />
|
||||||
|
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable
|
||||||
v-if="!uniHaloPluginAvailable"
|
v-if="!uniHaloPluginAvailable"
|
||||||
:plugin-id="uniHaloPluginId"
|
:plugin-id="uniHaloPluginId"
|
||||||
@@ -124,16 +133,17 @@ onReachBottom(() => {
|
|||||||
@on-refresh="handleGetData"
|
@on-refresh="handleGetData"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 加载/错误占位 -->
|
<!-- 加载/错误/空占位(状态机) -->
|
||||||
<view v-if="loading !== 'success'">
|
<view v-if="loadingStatus !== 'success'">
|
||||||
<uh-data-loading :loading-status="loading" @refresh="handleGetData" />
|
<uh-data-loading
|
||||||
|
:loading-status="loadingStatus"
|
||||||
|
empty-text="博主还未发布投票~"
|
||||||
|
@refresh="handleGetData"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="content flex flex-col gap-4 p-3">
|
<view v-else class="content flex flex-col gap-4 p-3">
|
||||||
<view v-if="dataList.length === 0" class="min-h-[60vh] flex items-center justify-center content-empty">
|
<block v-if="dataList.length !== 0">
|
||||||
<wd-empty description="博主还未发布投票~" />
|
|
||||||
</view>
|
|
||||||
<block v-else>
|
|
||||||
<uh-vote-card
|
<uh-vote-card
|
||||||
v-for="vote in dataList"
|
v-for="vote in dataList"
|
||||||
:key="vote.metadata?.name"
|
:key="vote.metadata?.name"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
// 维护拦截
|
// 维护拦截
|
||||||
const { interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept()
|
const { reason, interceptOrContinue, redirectToMaintenance } = useMaintenanceIntercept()
|
||||||
|
|
||||||
/** 通过二维码 scene 获取文章 id */
|
/** 通过二维码 scene 获取文章 id */
|
||||||
async function getPostIdByQRCode(key : string) : Promise<string | null> {
|
async function getPostIdByQRCode(key : string) : Promise<string | null> {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('入口页初始化失败', err)
|
console.error('入口页初始化失败', err)
|
||||||
redirectToMaintenance()
|
redirectToMaintenance(reason.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -121,11 +121,10 @@
|
|||||||
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-articles/category-articles?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleInitPage()
|
handleInitPage()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
|
||||||
* 图库页(源自旧项目 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'
|
||||||
@@ -25,9 +21,19 @@
|
|||||||
|
|
||||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||||
|
|
||||||
/** 依赖插件(plugin-photos) */
|
/** 依赖插件(PluginPhotos) */
|
||||||
const uniHaloPluginId = 'plugin-photos'
|
const { pluginId, checking, tips, available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable({
|
||||||
const { available: uniHaloPluginAvailable, check: checkPluginAvailable } = usePluginAvailable(uniHaloPluginId, false)
|
pluginId: 'PluginPhotos',
|
||||||
|
tips: '很抱歉,功能正在维护中...',
|
||||||
|
callback: (isAvailable) => {
|
||||||
|
if (!isAvailable) { return }
|
||||||
|
uni.pageScrollTo({
|
||||||
|
scrollTop: 0,
|
||||||
|
duration: 0,
|
||||||
|
})
|
||||||
|
handleGetCategory()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
@@ -152,11 +158,9 @@
|
|||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始正常数据请求
|
|
||||||
handleGetCategory()
|
handleGetCategory()
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
if (!uniHaloPluginAvailable.value) {
|
if (!uniHaloPluginAvailable.value) {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
@@ -169,8 +173,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
@@ -188,8 +191,8 @@
|
|||||||
|
|
||||||
<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 v-if="!uniHaloPluginAvailable" :plugin-id="uniHaloPluginId"
|
<uh-plugin-unavailable v-if="!uniHaloPluginAvailable" :plugin-id="pluginId" :error-text="tips"
|
||||||
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员" @on-refresh="handleGetCategory" />
|
:checking="checking" @on-refresh="checkPluginAvailable" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<wd-sticky v-if="category.list.length!==0">
|
<wd-sticky v-if="category.list.length!==0">
|
||||||
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
<scroll-view :scroll-x="true" class="w-full whitespace-nowrap pt-3">
|
||||||
|
|||||||
@@ -124,15 +124,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 跳转 ---------------- */
|
/* ---------------- 跳转 ---------------- */
|
||||||
function handleToArticleDetail(article : IPost) {
|
|
||||||
uni.navigateTo({
|
function handleToArticles() {
|
||||||
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
|
uni.navigateTo({ url: '/pages-blog/articles/articles' })
|
||||||
animationType: 'slide-in-right',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToSearch() {
|
|
||||||
uni.navigateTo({ url: '/pages-blog/search/search' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnLogoToPage() {
|
function handleOnLogoToPage() {
|
||||||
@@ -202,10 +196,10 @@
|
|||||||
|
|
||||||
<!-- 最新文章 -->
|
<!-- 最新文章 -->
|
||||||
<uh-section-title class="mb-4 box-border px-3">
|
<uh-section-title class="mb-4 box-border px-3">
|
||||||
最新内容
|
最新推荐
|
||||||
<template #right>
|
<template #right>
|
||||||
<view class="uh-global-card-glass flex items-center justify-center rounded-md p-1 text-gray-400"
|
<view class="uh-global-card-glass flex items-center justify-center gap-x-1 rounded-md p-1 text-gray-400"
|
||||||
@click="handleToSearch()">
|
@click="handleToArticles()">
|
||||||
<wd-icon name="arrow-right" size="12px" />
|
<wd-icon name="arrow-right" size="12px" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -217,8 +211,9 @@
|
|||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view class="flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home">
|
<view class="flex flex-col gap-y-3 p-3 pt-0" :class="globalAppSettings.layout.home">
|
||||||
<uh-article-card v-for="(article, index) in articleList" :key="index" from="home" :article="article"
|
<uh-article-card v-for="(article, index) in articleList" :key="index"
|
||||||
@on-click="handleToArticleDetail" />
|
from="home" :article="article" :audit-mode="calcAuditModeEnabled"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400">
|
<view class="load-text mt-3 pb-5 text-center text-xs text-gray-400">
|
||||||
{{ loadMoreText }}
|
{{ loadMoreText }}
|
||||||
|
|||||||
Reference in New Issue
Block a user