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

feat: 小程序端轮播图重写,接入 Banner 归一化公开接口

- uh-swiper 组件高内聚:内部请求 banners 公开列表,去掉前 5 条
  上限,日期角标显示条目 date 快照,新增作者/日期信息浮层
- home.vue 移除配置 list 与审核模式劫持组装,点击分发改为
  post→文章详情(postId)、custom→banner-detail 详情页
- 新增 banner-detail 详情页:公开详情接口 + mp-html 富文本 +
  外链平台条件编译(非 App 端复制链接 / App 端访问按钮)
- 类型与默认配置同步清理(IBannerConfig 移除 type/list)

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
This commit is contained in:
小莫唐尼
2026-09-02 16:32:39 +08:00
parent 938c09ad4e
commit cf9c02cfc3
6 changed files with 422 additions and 211 deletions
+23 -2
View File
@@ -31,8 +31,29 @@ export interface IBannerConfig {
showIndicator?: boolean showIndicator?: boolean
height?: string height?: string
dotPosition?: string dotPosition?: string
type?: string }
list?: unknown[]
/** 轮播图公开条目(plugin-uni-halo Banner 归一化模型公开接口,列表脱敏不含 content) */
export interface IBannerPublicItem {
/** Banner 条目 metadata.name */
name: string
title?: string
cover?: string
/** 展示日期(ISO) */
date?: string
authorName?: string
authorAvatar?: string
/** 来源:post=文章快照 / custom=自定义 */
source?: 'post' | 'custom'
/** 文章 id(source=post 时跳转文章详情) */
postId?: string
link?: string
priority?: number
}
/** 轮播图公开详情(含 content 富文本 HTML) */
export interface IBannerPublicDetail extends IBannerPublicItem {
content?: string
} }
export interface IPageConfig { export interface IPageConfig {
+20
View File
@@ -15,6 +15,8 @@ import { getPersonalToken } from '@/store/token'
import type { import type {
IAppConfig, IAppConfig,
IAuditDataResult, IAuditDataResult,
IBannerPublicDetail,
IBannerPublicItem,
ICommentWidgetConfig, ICommentWidgetConfig,
IDoubanDetail, IDoubanDetail,
IHaloGlobalConfig, IHaloGlobalConfig,
@@ -67,6 +69,24 @@ export function getAuditData() {
}) })
} }
/**
* 获取首页轮播图列表(公开;按 priority 有序,脱敏不含 content)
*/
export function getBanners() {
return http.Get<IResponse<IBannerPublicItem[]>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/banners', {
meta: { requestFrom: RequestFrom.Halo },
})
}
/**
* 获取轮播图详情(公开;含 content 富文本 HTML)
*/
export function getBannerDetail(name: string) {
return http.Get<IResponse<IBannerPublicDetail>>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/banners/${name}`, {
meta: { requestFrom: RequestFrom.Halo },
})
}
/** /**
* 获取 Halo 全局配置信息 * 获取 Halo 全局配置信息
*/ */
+147 -85
View File
@@ -1,19 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
/** /**
* 轮播组件(源自旧项目 components/e-swiper,新建复刻) * 轮播组件(源自旧项目 components/e-swiper,新建复刻)
* 支持:图片/视频轮播、今日首推日期指示器(useTop)、标题区域(useTitle)、底部小图指示器(useDot) * 数据高内聚:默认内部请求 plugin-uni-halo 公开 banners 接口(getBanners),支持外部 list 覆盖
* 支持:图片轮播、日期角标(useTop,显示当前条目 date 快照)、标题浮层(useTitle)、
* 作者/日期信息浮层(useUser)、底部小图指示器(useDot)
*/ */
import { computed, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { getBanners } from '@/api/uni-halo'
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import type { IBannerPublicItem } from '@/api/types/uni-halo'
export interface IBannerItem { export interface IBannerItem {
id: string | number /** 条目标识(Banner 为 metadata.name;兼容旧数据) */
id?: string | number
/** Banner 条目 metadata.name(custom 详情页跳转用) */
name?: string
title?: string title?: string
image?: string image?: string
src?: string src?: string
mp4?: string /** 来源:post=文章快照 / custom=自定义 */
type?: string type?: string
/** 文章 id(source=post 时跳转文章详情) */
postId?: string
content?: string content?: string
url?: string url?: string
/** 展示日期(ISO 快照) */
date?: string
authorName?: string
authorAvatar?: string
[key: string]: unknown [key: string]: unknown
} }
@@ -21,12 +35,16 @@ const props = withDefaults(defineProps<{
title?: string title?: string
height?: string height?: string
dotPosition?: string dotPosition?: string
/** 日期角标(显示当前条目 date) */
useTop?: boolean useTop?: boolean
/** 底部小图指示器 */
useDot?: boolean useDot?: boolean
/** 标题浮层 */
useTitle?: boolean useTitle?: boolean
/** 作者/日期信息浮层 */
useUser?: boolean useUser?: boolean
/** 轮播数据列表 */ /** 轮播数据列表(可选;不传时组件内部调公开 banners 接口拉取) */
list: IBannerItem[] list?: IBannerItem[]
/** 当前选中的项(指示器坐标位置) */ /** 当前选中的项(指示器坐标位置) */
current?: number current?: number
/** 是否自动轮播 */ /** 是否自动轮播 */
@@ -54,26 +72,91 @@ const currentIndex = ref(props.current)
/** 是否禁止用户 touch 操作 */ /** 是否禁止用户 touch 操作 */
const disableTouch = ref(false) const disableTouch = ref(false)
/** 日期(今日首推指示器用) */ /* ---------------- 数据(高内聚:内部请求公开接口) ---------------- */
const date = ref({ year: '-', monthEn: '-', month: '-' }) const internalList = ref<IBannerItem[]>([])
/* ---------------- 计算属性 ---------------- */ /** 展示列表:外部传入(list)优先,否则使用内部拉取数据 */
/** 仅渲染前 5 条 */ const displayItems = computed<IBannerItem[]>(() =>
const displayList = computed(() => props.list.slice(0, 5)) props.list && props.list.length > 0 ? props.list : internalList.value,
)
const currentTitle = computed(() => props.list[currentIndex.value]?.title || '') /** 公开 Banner 条目 → 轮播展示项 */
function mapBanners(items: IBannerPublicItem[]): IBannerItem[] {
/* ---------------- 初始化 ---------------- */ return items.map(item => ({
function initDate() { id: item.name,
const now = new Date() name: item.name,
const monthArray = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] title: item.title || '',
date.value.year = String(now.getFullYear()) image: checkThumbnailUrl(item.cover),
const month = now.getMonth() + 1 src: checkThumbnailUrl(item.cover),
date.value.month = month < 10 ? `0${month}` : String(month) type: item.source,
date.value.monthEn = monthArray[now.getMonth()].toUpperCase() postId: item.postId,
url: item.link,
date: item.date,
authorName: item.authorName,
authorAvatar: item.authorAvatar ? checkAvatarUrl(item.authorAvatar) : '',
}))
} }
initDate() onMounted(async () => {
// 外部已传数据时不再重复请求
if (props.list && props.list.length > 0) {
return
}
try {
const res = await getBanners()
internalList.value = mapBanners(res.data || [])
}
catch (err) {
console.error('获取轮播图失败', err)
}
})
// 列表变化(外部覆盖/接口返回)后索引越界时归零
watch(displayItems, (val) => {
if (currentIndex.value >= val.length) {
currentIndex.value = 0
}
})
/* ---------------- 计算属性 ---------------- */
const currentItem = computed<IBannerItem>(() =>
displayItems.value[currentIndex.value] || {},
)
/** 日期角标(useTop):当前条目 date 快照转换(年/月/日) */
const dateParts = computed(() => {
const dateStr = currentItem.value.date as string | undefined
if (!dateStr) {
return null
}
const d = new Date(dateStr)
if (Number.isNaN(d.getTime())) {
return null
}
const monthArray = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
return {
day: String(d.getDate()).padStart(2, '0'),
month: String(d.getMonth() + 1).padStart(2, '0'),
monthEn: monthArray[d.getMonth()],
year: String(d.getFullYear()),
}
})
const currentTitle = computed(() => currentItem.value.title || props.title || '')
/** 作者日期展示(useUser 用) */
const authorDateText = computed(() => {
const dateStr = currentItem.value.date as string | undefined
if (!dateStr) {
return ''
}
const d = new Date(dateStr)
if (Number.isNaN(d.getTime())) {
return ''
}
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
})
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
/** current 改变时会触发 change 事件,event.detail = {current, source} */ /** current 改变时会触发 change 事件,event.detail = {current, source} */
@@ -100,8 +183,7 @@ function handleOnClick(item: IBannerItem) {
</script> </script>
<template> <template>
<!-- 轮播图 --> <view v-if="displayItems.length > 0" class="uh-e-swiper">
<view class="uh-e-swiper">
<view class="swiper-box" :class="[dotPosition]"> <view class="swiper-box" :class="[dotPosition]">
<swiper <swiper
class="swiper" class="swiper"
@@ -115,48 +197,27 @@ function handleOnClick(item: IBannerItem) {
:disable-touch="disableTouch" :disable-touch="disableTouch"
@change="change" @change="change"
> >
<!-- 只需要前5条数据 --> <swiper-item v-for="(item, index) in displayItems" :key="index" class="swiper-mfw-item">
<block v-for="(item, index) in list" :key="index">
<swiper-item v-if="index <= 4" class="swiper-mfw-item">
<!-- 如果有视频,则显示视频 -->
<template v-if="item.mp4 && currentIndex === index">
<video
:id="`ImageVideo${index}`"
:src="item.mp4"
class="image-video h-full w-full"
:loop="true"
:muted="false"
:autoplay="true"
:controls="false"
:show-fullscreen-btn="false"
:show-play-btn="false"
:enable-progress-gesture="false"
:poster="item.image || item.src"
/>
</template>
<!-- 否则显示图片 -->
<image <image
v-else
:src="item.image || item.src" :src="item.image || item.src"
class="image h-full w-full" class="image h-full w-full"
mode="aspectFill" mode="aspectFill"
@click.stop="handleOnClick(item)" @click.stop="handleOnClick(item)"
/> />
</swiper-item> </swiper-item>
</block>
</swiper> </swiper>
<!-- 指示器 [Top 今日首推] --> <!-- 指示器 [Top 日期角标]:显示当前条目 date(//) -->
<view v-if="useTop" class="indicator-box indicator-top-box"> <view v-if="useTop && dateParts" class="indicator-box indicator-top-box">
<view class="top-date-hot"> <view class="top-date-hot">
<view class="left-date-ri"> <view class="left-date-ri">
<text class="date-ri-text">{{ date.month }}</text> <text class="date-ri-text">{{ dateParts.day }}</text>
</view> </view>
<view class="center-date-nianyue"> <view class="center-date-nianyue">
<view class="left-width-bgcolor" /> <view class="left-width-bgcolor" />
<view class="right-date-nianyue"> <view class="right-date-nianyue">
<text class="top-yue-usa">{{ date.monthEn }}</text> <text class="top-yue-usa">{{ dateParts.monthEn }}</text>
<text class="bottom-nian">{{ date.year }}</text> <text class="bottom-nian">{{ dateParts.year }}</text>
</view> </view>
</view> </view>
<view class="right-hot-ttf"> <view class="right-hot-ttf">
@@ -165,39 +226,32 @@ function handleOnClick(item: IBannerItem) {
</view> </view>
</view> </view>
<!-- 指示器 标题区域 --> <!-- 指示器 标题区域 + 作者/日期信息(useUser) -->
<view v-if="useTitle" class="indicator-top" :class="{ 'no-dot': !useDot }"> <view v-if="useTitle" class="indicator-top" :class="{ 'no-dot': !useDot }">
<block v-for="(item, index) in list" :key="index"> <view v-if="useUser && (currentItem.authorName || authorDateText)" class="author-line">
<view v-if="currentIndex === index" class="top-item" :class="currentIndex === index ? 'current' : 'no'"> <view v-if="currentItem.authorAvatar" class="author-avatar">
<!-- 如果存在视频,则显示"视频预览"提示 --> <image :src="currentItem.authorAvatar" class="h-full w-full" mode="aspectFill" />
<view v-if="item.mp4" class="top-image-video">
<view class="icons">
<text class="video-icon"></text>
</view> </view>
<text class="image-video-text">视频预览</text> <text class="author-name">{{ currentItem.authorName }}</text>
<text v-if="authorDateText" class="author-date">{{ authorDateText }}</text>
</view> </view>
<!-- 标题 --> <view v-if="currentTitle" class="top-title">
<view class="top-title"> <text class="title-text text-overflow-2">{{ currentTitle }}</text>
<text class="title-text text-overflow-2">{{ item.title }}</text>
</view> </view>
</view> </view>
</block>
</view>
<!-- 指示器 [左边图片列表+右边按钮] --> <!-- 指示器 [左边图片列表] -->
<view v-if="useDot" class="indicator-bottom"> <view v-if="useDot" class="indicator-bottom">
<!-- 左边 -->
<view class="bottom-left-imagelist"> <view class="bottom-left-imagelist">
<block v-for="(item, index) in list" :key="index">
<view <view
v-if="Number(index) <= 4" v-for="(item, index) in displayItems"
:key="index"
class="bottom-item" class="bottom-item"
:class="currentIndex === index ? 'current' : 'no'" :class="currentIndex === index ? 'current' : 'no'"
@click="swiperIndTap(index)" @click="swiperIndTap(index)"
> >
<image :src="item.image || item.src" class="image h-full w-full" mode="aspectFill" /> <image :src="item.image || item.src" class="image h-full w-full" mode="aspectFill" />
</view> </view>
</block>
</view> </view>
</view> </view>
</view> </view>
@@ -220,7 +274,7 @@ function handleOnClick(item: IBannerItem) {
} }
} }
/* 今日首推指示器 */ /* 日期角标(顶部) */
.indicator-top-box { .indicator-top-box {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -285,37 +339,44 @@ function handleOnClick(item: IBannerItem) {
} }
} }
/* 标题区域 */ /* 底部标题/作者浮层 */
.indicator-top { .indicator-top {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 60rpx; bottom: 0;
z-index: 5; z-index: 5;
padding: 0 24rpx; padding: 48rpx 24rpx 20rpx;
background: linear-gradient(to top, rgb(0 0 0 / 45%), transparent);
.top-item { .author-line {
.top-image-video {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8rpx; gap: 8rpx;
margin-bottom: 8rpx;
.icons { .author-avatar {
.video-icon { width: 36rpx;
font-size: 20rpx; height: 36rpx;
color: #fff; border-radius: 50%;
} overflow: hidden;
border: 1rpx solid rgb(255 255 255 / 60%);
} }
.image-video-text { .author-name {
font-size: 22rpx;
color: rgb(255 255 255 / 92%);
text-shadow: 0 1rpx 4rpx rgb(0 0 0 / 40%);
}
.author-date {
font-size: 20rpx; font-size: 20rpx;
color: #fff; color: rgb(255 255 255 / 70%);
text-shadow: 0 1rpx 4rpx rgb(0 0 0 / 40%);
} }
} }
.top-title { .top-title {
margin-top: 8rpx;
.title-text { .title-text {
display: block; display: block;
font-size: 28rpx; font-size: 28rpx;
@@ -325,7 +386,6 @@ function handleOnClick(item: IBannerItem) {
} }
} }
} }
}
/* 底部小图指示器 */ /* 底部小图指示器 */
.indicator-bottom { .indicator-bottom {
@@ -335,6 +395,8 @@ function handleOnClick(item: IBannerItem) {
bottom: 16rpx; bottom: 16rpx;
z-index: 5; z-index: 5;
padding: 0 24rpx; padding: 0 24rpx;
display: flex;
justify-content: flex-end;
.bottom-left-imagelist { .bottom-left-imagelist {
display: flex; display: flex;
-2
View File
@@ -34,8 +34,6 @@ export const DefaultAppConfigs: IAppConfig = {
showIndicator: true, showIndicator: true,
height: '400rpx', height: '400rpx',
dotPosition: 'right', dotPosition: 'right',
type: 'post',
list: [],
}, },
}, },
categoryConfig: { categoryConfig: {
@@ -0,0 +1,189 @@
<script lang="ts" setup>
/**
* 轮播详情页(自定义 Banner 条目点击后进入)
* 通过公开详情接口(getBannerDetail)拉取 content 富文本与外链,类文章详情页
* 外链平台差异(条件编译):非 APP-PLUS(小程序/H5)提供复制+显示链接,APP-PLUS 提供访问按钮(web-view)
*/
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getBannerDetail } from '@/api/uni-halo'
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
import { formatTime } from '@/utils/formatTime'
import { copyToClipboard } from '@/utils/restrictRead'
import type { IBannerPublicDetail } from '@/api/types/uni-halo'
definePage({
style: {
navigationBarTitleText: '轮播详情',
},
})
const loading = ref<'loading' | 'success' | 'error'>('loading')
const detail = ref<IBannerPublicDetail | null>(null)
const linkCopied = ref(false)
async function handleGetData(name: string) {
loading.value = 'loading'
try {
const res = await getBannerDetail(name)
detail.value = res.data || null
if (detail.value?.title) {
uni.setNavigationBarTitle({ title: detail.value.title })
}
loading.value = 'success'
}
catch (err) {
console.error('获取轮播详情失败', err)
loading.value = 'error'
}
}
function handleRetry() {
const name = detail.value?.name || ''
if (name) {
handleGetData(name)
}
}
onLoad((options) => {
const name = options?.name
if (name) {
handleGetData(name)
}
else {
loading.value = 'error'
}
})
const coverUrl = computed(() => checkThumbnailUrl(detail.value?.cover, true))
const authorName = computed(() => detail.value?.authorName || '')
const authorAvatar = computed(() =>
detail.value?.authorAvatar ? checkAvatarUrl(detail.value.authorAvatar) : '',
)
const dateText = computed(() => {
if (!detail.value?.date) {
return ''
}
const d = new Date(detail.value.date)
if (Number.isNaN(d.getTime())) {
return ''
}
return formatTime({ d: detail.value.date, f: 'yyyy-MM-dd' })
})
/** 非 APP-PLUS:复制链接 */
function handleCopyLink() {
if (!detail.value?.link) {
return
}
copyToClipboard(detail.value.link, '链接已复制')
linkCopied.value = true
setTimeout(() => {
linkCopied.value = false
}, 1500)
}
/** APP-PLUS:打开内置 web-view 访问外链 */
function handleOpenLink() {
if (!detail.value?.link) {
return
}
uni.navigateTo({
url: `/pages-blog/website/website?data=${JSON.stringify({
title: detail.value.title || '查看链接',
url: encodeURIComponent(detail.value.link),
})}`,
})
}
</script>
<template>
<view class="app-page">
<!-- 加载骨架 -->
<view v-if="loading === 'loading'" class="p-4">
<wd-skeleton :row="3" :animated="true" />
</view>
<!-- 加载失败 -->
<view v-else-if="loading === 'error'" class="flex flex-col items-center gap-4 py-20">
<wd-empty description="详情加载失败" />
<wd-button size="small" @click="handleRetry">重新加载</wd-button>
</view>
<!-- 详情内容 -->
<view v-else-if="detail" class="pb-8">
<!-- 封面图 -->
<image v-if="coverUrl" :src="coverUrl" class="h-[420rpx] w-full" mode="aspectFill" />
<view class="px-4">
<!-- 标题 -->
<view class="mt-6 text-[34rpx] font-bold leading-snug text-[#303133]">
{{ detail.title }}
</view>
<!-- 作者/日期信息 -->
<view v-if="authorName || dateText" class="mt-3 flex items-center gap-2">
<image
v-if="authorAvatar"
:src="authorAvatar"
class="h-[44rpx] w-[44rpx] rounded-full"
mode="aspectFill"
/>
<text class="text-[24rpx] text-[#909399]">{{ authorName }}</text>
<text v-if="dateText" class="text-[22rpx] text-[#c0c4cc]">{{ dateText }}</text>
</view>
<!-- 富文本内容 -->
<view v-if="detail.content" class="mt-6 border-t border-[#f0f0f0] pt-6">
<mp-html :content="detail.content" />
</view>
<!-- 外链(平台差异,条件编译) -->
<view v-if="detail.link" class="link-card mt-8 rounded-xl bg-[#f7f7f9] p-4">
<view class="mb-3 text-[24rpx] text-[#909399]">相关链接</view>
<text class="link-text block break-all text-[26rpx] leading-relaxed text-[#606266]">
{{ detail.link }}
</text>
<!-- #ifndef APP-PLUS -->
<!-- App (小程序/H5):复制 + 提示已复制 -->
<view
class="link-btn mt-3 inline-flex items-center rounded-lg px-6 py-2 text-[24rpx] text-white"
:class="linkCopied ? 'link-btn-copied' : ''"
@click="handleCopyLink"
>
{{ linkCopied ? '已复制' : '复制链接' }}
</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<!-- App :访问按钮(内置 web-view) -->
<view class="link-btn mt-3 inline-flex items-center rounded-lg px-6 py-2 text-[24rpx] text-white" @click="handleOpenLink">
访问链接
</view>
<!-- #endif -->
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.app-page {
min-height: 100vh;
background: #fff;
}
.link-text {
word-break: break-all;
}
.link-btn {
background: #2563eb;
&.link-btn-copied {
background: #10b981;
}
}
</style>
+14 -93
View File
@@ -8,7 +8,7 @@ import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getCategoryList, getPostList } from '@/api/halo' import { getCategoryList, 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 { checkAvatarUrl, checkImageUrl, checkThumbnailUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { t } from '@/locale' import { t } from '@/locale'
import type { ICategory, IPost } from '@/api/types/halo' import type { ICategory, IPost } from '@/api/types/halo'
import type { IBannerItem } from '@/components/uh-swiper/uh-swiper.vue' import type { IBannerItem } from '@/components/uh-swiper/uh-swiper.vue'
@@ -33,14 +33,8 @@ const isLoadMore = ref(false)
const loadMoreText = ref(t('common.loading')) const loadMoreText = ref(t('common.loading'))
const articleList = ref<IPost[]>([]) const articleList = ref<IPost[]>([])
const categoryList = ref<ICategory[]>([]) const categoryList = ref<ICategory[]>([])
const bannerList = ref<IBannerItem[]>([])
const result = ref<{ hasNext: boolean }>({ hasNext: false }) const result = ref<{ hasNext: boolean }>({ hasNext: false })
const notify = ref({
show: false,
data: {} as IBannerItem,
})
const queryParams = ref({ const queryParams = ref({
size: 5, size: 5,
page: 1, page: 1,
@@ -131,55 +125,10 @@ const navList = computed(() => {
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleQuery() { async function handleQuery() {
handleGetBanner() // 轮播图数据由 uh-swiper 组件内部请求公开 banners 接口,页面不再组装
await Promise.all([handleGetArticleList(), handleGetCategoryList()]) await Promise.all([handleGetArticleList(), handleGetCategoryList()])
} }
/** 轮播图 */
function handleGetBanner() {
if (calcAuditModeEnabled.value) {
// 审核模式:轮播取选中文章前 5 条(articleList 已按 audit-data posts 过滤)
bannerList.value = articleList.value.slice(0, 5).map(item => ({
id: item.metadata.name,
title: item.spec.title,
image: checkThumbnailUrl(item.spec.cover),
src: checkThumbnailUrl(item.spec.cover),
type: 'post',
content: item.status?.excerpt || '',
url: '',
}))
return
}
if (!bannerConfig.value?.enabled)
return
if (bannerConfig.value.type === 'custom') {
bannerList.value = (bannerConfig.value.list as { title?: string, cover?: string, content?: string, url?: string }[]).map(item => ({
id: Date.now() * Math.random(),
title: item.title,
image: checkThumbnailUrl(item.cover),
src: checkThumbnailUrl(item.cover),
type: 'custom',
content: item.content || '',
url: item.url || '',
}))
return
}
// post 类型:取最新文章作为轮播
const list = articleList.value.slice(0, 5).map(item => ({
id: item.metadata.name,
title: item.spec.title,
image: checkThumbnailUrl(item.spec.cover),
src: checkThumbnailUrl(item.spec.cover),
type: 'post',
content: item.status?.excerpt || '',
url: '',
}))
bannerList.value = list
}
/** 精选分类 */ /** 精选分类 */
async function handleGetCategoryList() { async function handleGetCategoryList() {
if (calcAuditModeEnabled.value || !calcIsShowCategory.value) { if (calcAuditModeEnabled.value || !calcIsShowCategory.value) {
@@ -218,10 +167,6 @@ async function handleGetArticleList() {
articleList.value = filtered articleList.value = filtered
loading.value = 'success' loading.value = 'success'
loadMoreText.value = t('common.noMore') loadMoreText.value = t('common.noMore')
// post 型轮播依赖文章列表,若启用则刷新
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
handleGetBanner()
}
} }
catch (err) { catch (err) {
console.error('获取审核文章失败', err) console.error('获取审核文章失败', err)
@@ -248,10 +193,6 @@ async function handleGetArticleList() {
: res.data.items : res.data.items
loading.value = 'success' loading.value = 'success'
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore') loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
// post 型轮播依赖文章列表,若启用则刷新
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
handleGetBanner()
}
} }
catch (err) { catch (err) {
loading.value = 'error' loading.value = 'error'
@@ -309,30 +250,22 @@ function handleToTopPage(duration = 500) {
} }
function handleOnBannerClick(item: IBannerItem) { function handleOnBannerClick(item: IBannerItem) {
if (calcAuditModeEnabled.value) // 审核模式下照常展示 Banner,点击分发不拦截(详情页自行处理审核限制)
return
if (item.type === 'custom') { if (item.type === 'custom') {
if (item.content) { // 自定义条目:跳转 Banner 详情页,页面内调公开详情接口展示 content/外链
notify.value = { show: true, data: item } if (item.name) {
return
}
if (item.url) {
uni.navigateTo({ uni.navigateTo({
url: `/pages-blog/website/website?data=${JSON.stringify({ url: `/pages-blog/banner-detail/banner-detail?name=${item.name}`,
title: item.title || t('common.loading'), animationType: 'slide-in-right',
url: encodeURIComponent(item.url),
})}`,
}) })
} }
return return
} }
if (!item.id) // 文章来源条目:用 postId 跳文章详情
const postId = item.postId || String(item.id || '')
if (!postId)
return return
handleToArticleDetail({ metadata: { name: String(item.id) } } as IPost) handleToArticleDetail({ metadata: { name: postId } } as IPost)
}
function handleOnNotifyChange(show: boolean) {
notify.value.show = show
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
@@ -393,17 +326,15 @@ handleQuery()
</view> </view>
<block v-else> <block v-else>
<!-- 轮播 Banner --> <!-- 轮播 Banner(数据由 uh-swiper 组件内部请求公开 banners 接口) -->
<view v-if="bannerConfig?.enabled" class="mb-4 bg-white"> <view v-if="bannerConfig?.enabled" class="mb-4 bg-white">
<view v-if="bannerList.length !== 0" class="banner mx-3 mt-3 overflow-hidden rounded-xl"> <view class="banner mx-3 mt-3 overflow-hidden rounded-xl">
<uh-swiper <uh-swiper
:height="bannerConfig.height" :height="bannerConfig.height"
:dot-position="bannerConfig.dotPosition" :dot-position="bannerConfig.dotPosition"
:autoplay="true" :autoplay="true"
:use-dot="bannerConfig.showIndicator" :use-dot="bannerConfig.showIndicator"
:show-title="bannerConfig.showTitle" :use-title="bannerConfig.showTitle"
:type="bannerConfig.type"
:list="bannerList"
@on-click="handleOnBannerClick" @on-click="handleOnBannerClick"
/> />
</view> </view>
@@ -484,15 +415,5 @@ handleQuery()
</view> </view>
</block> </block>
</block> </block>
<!-- 通知弹窗 -->
<uh-notify-dialog
v-if="notify.show"
:show="notify.show"
:title="notify.data.title || ''"
:content="notify.data.content || ''"
:url="notify.data.url || ''"
@on-change="handleOnNotifyChange"
/>
</view> </view>
</template> </template>