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:
@@ -31,8 +31,29 @@ export interface IBannerConfig {
|
||||
showIndicator?: boolean
|
||||
height?: 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 {
|
||||
|
||||
@@ -15,6 +15,8 @@ import { getPersonalToken } from '@/store/token'
|
||||
import type {
|
||||
IAppConfig,
|
||||
IAuditDataResult,
|
||||
IBannerPublicDetail,
|
||||
IBannerPublicItem,
|
||||
ICommentWidgetConfig,
|
||||
IDoubanDetail,
|
||||
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 全局配置信息
|
||||
*/
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 轮播组件(源自旧项目 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 {
|
||||
id: string | number
|
||||
/** 条目标识(Banner 为 metadata.name;兼容旧数据) */
|
||||
id?: string | number
|
||||
/** Banner 条目 metadata.name(custom 详情页跳转用) */
|
||||
name?: string
|
||||
title?: string
|
||||
image?: string
|
||||
src?: string
|
||||
mp4?: string
|
||||
/** 来源:post=文章快照 / custom=自定义 */
|
||||
type?: string
|
||||
/** 文章 id(source=post 时跳转文章详情) */
|
||||
postId?: string
|
||||
content?: string
|
||||
url?: string
|
||||
/** 展示日期(ISO 快照) */
|
||||
date?: string
|
||||
authorName?: string
|
||||
authorAvatar?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -21,12 +35,16 @@ const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
height?: string
|
||||
dotPosition?: string
|
||||
/** 日期角标(显示当前条目 date) */
|
||||
useTop?: boolean
|
||||
/** 底部小图指示器 */
|
||||
useDot?: boolean
|
||||
/** 标题浮层 */
|
||||
useTitle?: boolean
|
||||
/** 作者/日期信息浮层 */
|
||||
useUser?: boolean
|
||||
/** 轮播图数据列表 */
|
||||
list: IBannerItem[]
|
||||
/** 轮播数据列表(可选;不传时组件内部调公开 banners 接口拉取) */
|
||||
list?: IBannerItem[]
|
||||
/** 当前选中的项(指示器坐标位置) */
|
||||
current?: number
|
||||
/** 是否自动轮播 */
|
||||
@@ -54,26 +72,91 @@ const currentIndex = ref(props.current)
|
||||
/** 是否禁止用户 touch 操作 */
|
||||
const disableTouch = ref(false)
|
||||
|
||||
/** 日期(今日首推指示器用) */
|
||||
const date = ref({ year: '-', monthEn: '-', month: '-' })
|
||||
/* ---------------- 数据(高内聚:内部请求公开接口) ---------------- */
|
||||
const internalList = ref<IBannerItem[]>([])
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
/** 仅渲染前 5 条 */
|
||||
const displayList = computed(() => props.list.slice(0, 5))
|
||||
/** 展示列表:外部传入(list)优先,否则使用内部拉取数据 */
|
||||
const displayItems = computed<IBannerItem[]>(() =>
|
||||
props.list && props.list.length > 0 ? props.list : internalList.value,
|
||||
)
|
||||
|
||||
const currentTitle = computed(() => props.list[currentIndex.value]?.title || '')
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
function initDate() {
|
||||
const now = new Date()
|
||||
const monthArray = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
date.value.year = String(now.getFullYear())
|
||||
const month = now.getMonth() + 1
|
||||
date.value.month = month < 10 ? `0${month}` : String(month)
|
||||
date.value.monthEn = monthArray[now.getMonth()].toUpperCase()
|
||||
/** 公开 Banner 条目 → 轮播展示项 */
|
||||
function mapBanners(items: IBannerPublicItem[]): IBannerItem[] {
|
||||
return items.map(item => ({
|
||||
id: item.name,
|
||||
name: item.name,
|
||||
title: item.title || '',
|
||||
image: checkThumbnailUrl(item.cover),
|
||||
src: checkThumbnailUrl(item.cover),
|
||||
type: item.source,
|
||||
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} */
|
||||
@@ -100,8 +183,7 @@ function handleOnClick(item: IBannerItem) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 轮播图 -->
|
||||
<view class="uh-e-swiper">
|
||||
<view v-if="displayItems.length > 0" class="uh-e-swiper">
|
||||
<view class="swiper-box" :class="[dotPosition]">
|
||||
<swiper
|
||||
class="swiper"
|
||||
@@ -115,48 +197,27 @@ function handleOnClick(item: IBannerItem) {
|
||||
:disable-touch="disableTouch"
|
||||
@change="change"
|
||||
>
|
||||
<!-- 只需要前5条数据 -->
|
||||
<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
|
||||
v-else
|
||||
:src="item.image || item.src"
|
||||
class="image h-full w-full"
|
||||
mode="aspectFill"
|
||||
@click.stop="handleOnClick(item)"
|
||||
/>
|
||||
</swiper-item>
|
||||
</block>
|
||||
<swiper-item v-for="(item, index) in displayItems" :key="index" class="swiper-mfw-item">
|
||||
<image
|
||||
:src="item.image || item.src"
|
||||
class="image h-full w-full"
|
||||
mode="aspectFill"
|
||||
@click.stop="handleOnClick(item)"
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 指示器 [Top 今日首推] -->
|
||||
<view v-if="useTop" class="indicator-box indicator-top-box">
|
||||
<!-- 指示器 [Top 日期角标]:显示当前条目 date(年/月/日) -->
|
||||
<view v-if="useTop && dateParts" class="indicator-box indicator-top-box">
|
||||
<view class="top-date-hot">
|
||||
<view class="left-date-ri">
|
||||
<text class="date-ri-text">{{ date.month }}</text>
|
||||
<text class="date-ri-text">{{ dateParts.day }}</text>
|
||||
</view>
|
||||
<view class="center-date-nianyue">
|
||||
<view class="left-width-bgcolor" />
|
||||
<view class="right-date-nianyue">
|
||||
<text class="top-yue-usa">{{ date.monthEn }}</text>
|
||||
<text class="bottom-nian">{{ date.year }}</text>
|
||||
<text class="top-yue-usa">{{ dateParts.monthEn }}</text>
|
||||
<text class="bottom-nian">{{ dateParts.year }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="right-hot-ttf">
|
||||
@@ -165,39 +226,32 @@ function handleOnClick(item: IBannerItem) {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 指示器 标题区域 -->
|
||||
<!-- 指示器 标题区域 + 作者/日期信息(useUser) -->
|
||||
<view v-if="useTitle" class="indicator-top" :class="{ 'no-dot': !useDot }">
|
||||
<block v-for="(item, index) in list" :key="index">
|
||||
<view v-if="currentIndex === index" class="top-item" :class="currentIndex === index ? 'current' : 'no'">
|
||||
<!-- 如果存在视频,则显示"视频预览"提示 -->
|
||||
<view v-if="item.mp4" class="top-image-video">
|
||||
<view class="icons">
|
||||
<text class="video-icon">▶</text>
|
||||
</view>
|
||||
<text class="image-video-text">视频预览</text>
|
||||
</view>
|
||||
<!-- 标题 -->
|
||||
<view class="top-title">
|
||||
<text class="title-text text-overflow-2">{{ item.title }}</text>
|
||||
</view>
|
||||
<view v-if="useUser && (currentItem.authorName || authorDateText)" class="author-line">
|
||||
<view v-if="currentItem.authorAvatar" class="author-avatar">
|
||||
<image :src="currentItem.authorAvatar" class="h-full w-full" mode="aspectFill" />
|
||||
</view>
|
||||
</block>
|
||||
<text class="author-name">{{ currentItem.authorName }}</text>
|
||||
<text v-if="authorDateText" class="author-date">{{ authorDateText }}</text>
|
||||
</view>
|
||||
<view v-if="currentTitle" class="top-title">
|
||||
<text class="title-text text-overflow-2">{{ currentTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 指示器 [左边图片列表+右边按钮] -->
|
||||
<!-- 指示器 [左边图片列表] -->
|
||||
<view v-if="useDot" class="indicator-bottom">
|
||||
<!-- 左边 -->
|
||||
<view class="bottom-left-imagelist">
|
||||
<block v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
v-if="Number(index) <= 4"
|
||||
class="bottom-item"
|
||||
:class="currentIndex === index ? 'current' : 'no'"
|
||||
@click="swiperIndTap(index)"
|
||||
>
|
||||
<image :src="item.image || item.src" class="image h-full w-full" mode="aspectFill" />
|
||||
</view>
|
||||
</block>
|
||||
<view
|
||||
v-for="(item, index) in displayItems"
|
||||
:key="index"
|
||||
class="bottom-item"
|
||||
:class="currentIndex === index ? 'current' : 'no'"
|
||||
@click="swiperIndTap(index)"
|
||||
>
|
||||
<image :src="item.image || item.src" class="image h-full w-full" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -220,7 +274,7 @@ function handleOnClick(item: IBannerItem) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 今日首推指示器 */
|
||||
/* 日期角标(顶部) */
|
||||
.indicator-top-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -285,44 +339,50 @@ function handleOnClick(item: IBannerItem) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
/* 底部标题/作者浮层 */
|
||||
.indicator-top {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 60rpx;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
padding: 0 24rpx;
|
||||
padding: 48rpx 24rpx 20rpx;
|
||||
background: linear-gradient(to top, rgb(0 0 0 / 45%), transparent);
|
||||
|
||||
.top-item {
|
||||
.top-image-video {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
.author-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.icons {
|
||||
.video-icon {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.image-video-text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.author-avatar {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid rgb(255 255 255 / 60%);
|
||||
}
|
||||
|
||||
.top-title {
|
||||
margin-top: 8rpx;
|
||||
.author-name {
|
||||
font-size: 22rpx;
|
||||
color: rgb(255 255 255 / 92%);
|
||||
text-shadow: 0 1rpx 4rpx rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
.title-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 8rpx rgb(0 0 0 / 40%);
|
||||
}
|
||||
.author-date {
|
||||
font-size: 20rpx;
|
||||
color: rgb(255 255 255 / 70%);
|
||||
text-shadow: 0 1rpx 4rpx rgb(0 0 0 / 40%);
|
||||
}
|
||||
}
|
||||
|
||||
.top-title {
|
||||
.title-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 8rpx rgb(0 0 0 / 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,6 +395,8 @@ function handleOnClick(item: IBannerItem) {
|
||||
bottom: 16rpx;
|
||||
z-index: 5;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.bottom-left-imagelist {
|
||||
display: flex;
|
||||
|
||||
@@ -34,8 +34,6 @@ export const DefaultAppConfigs: IAppConfig = {
|
||||
showIndicator: true,
|
||||
height: '400rpx',
|
||||
dotPosition: 'right',
|
||||
type: 'post',
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
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>
|
||||
@@ -8,7 +8,7 @@ import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getCategoryList, getPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
import { checkAvatarUrl, checkImageUrl, checkThumbnailUrl } from '@/utils/url'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import type { ICategory, IPost } from '@/api/types/halo'
|
||||
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 articleList = ref<IPost[]>([])
|
||||
const categoryList = ref<ICategory[]>([])
|
||||
const bannerList = ref<IBannerItem[]>([])
|
||||
const result = ref<{ hasNext: boolean }>({ hasNext: false })
|
||||
|
||||
const notify = ref({
|
||||
show: false,
|
||||
data: {} as IBannerItem,
|
||||
})
|
||||
|
||||
const queryParams = ref({
|
||||
size: 5,
|
||||
page: 1,
|
||||
@@ -131,55 +125,10 @@ const navList = computed(() => {
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleQuery() {
|
||||
handleGetBanner()
|
||||
// 轮播图数据由 uh-swiper 组件内部请求公开 banners 接口,页面不再组装
|
||||
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() {
|
||||
if (calcAuditModeEnabled.value || !calcIsShowCategory.value) {
|
||||
@@ -218,10 +167,6 @@ async function handleGetArticleList() {
|
||||
articleList.value = filtered
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
// post 型轮播依赖文章列表,若启用则刷新
|
||||
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
|
||||
handleGetBanner()
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取审核文章失败', err)
|
||||
@@ -248,10 +193,6 @@ async function handleGetArticleList() {
|
||||
: res.data.items
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
// post 型轮播依赖文章列表,若启用则刷新
|
||||
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
|
||||
handleGetBanner()
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
loading.value = 'error'
|
||||
@@ -309,30 +250,22 @@ function handleToTopPage(duration = 500) {
|
||||
}
|
||||
|
||||
function handleOnBannerClick(item: IBannerItem) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
// 审核模式下照常展示 Banner,点击分发不拦截(详情页自行处理审核限制)
|
||||
if (item.type === 'custom') {
|
||||
if (item.content) {
|
||||
notify.value = { show: true, data: item }
|
||||
return
|
||||
}
|
||||
if (item.url) {
|
||||
// 自定义条目:跳转 Banner 详情页,页面内调公开详情接口展示 content/外链
|
||||
if (item.name) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/website/website?data=${JSON.stringify({
|
||||
title: item.title || t('common.loading'),
|
||||
url: encodeURIComponent(item.url),
|
||||
})}`,
|
||||
url: `/pages-blog/banner-detail/banner-detail?name=${item.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!item.id)
|
||||
// 文章来源条目:用 postId 跳文章详情
|
||||
const postId = item.postId || String(item.id || '')
|
||||
if (!postId)
|
||||
return
|
||||
handleToArticleDetail({ metadata: { name: String(item.id) } } as IPost)
|
||||
}
|
||||
|
||||
function handleOnNotifyChange(show: boolean) {
|
||||
notify.value.show = show
|
||||
handleToArticleDetail({ metadata: { name: postId } } as IPost)
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
@@ -393,17 +326,15 @@ handleQuery()
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<!-- 轮播 Banner -->
|
||||
<!-- 轮播 Banner(数据由 uh-swiper 组件内部请求公开 banners 接口) -->
|
||||
<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
|
||||
:height="bannerConfig.height"
|
||||
:dot-position="bannerConfig.dotPosition"
|
||||
:autoplay="true"
|
||||
:use-dot="bannerConfig.showIndicator"
|
||||
:show-title="bannerConfig.showTitle"
|
||||
:type="bannerConfig.type"
|
||||
:list="bannerList"
|
||||
:use-title="bannerConfig.showTitle"
|
||||
@on-click="handleOnBannerClick"
|
||||
/>
|
||||
</view>
|
||||
@@ -484,15 +415,5 @@ handleQuery()
|
||||
</view>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user