mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
feat(love): 重构恋爱模块适配新接口格式与分页
1. 重构恋爱相册、清单、故事页面的数据处理逻辑,新增数据加载状态管理 2. 更新API类型定义,新增元数据、spec字段支持与分页响应格式 3. 替换原生导航栏为自定义导航组件,统一页面样式 4. 优化列表渲染逻辑,使用预处理后的展示数据类型,简化模板代码 5. 调整恋爱故事页面的key命名,从story改为stories
This commit is contained in:
+111
-107
@@ -1,50 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getLoveStories } from '@/api/uni-halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import type { ILoveStory } from '@/api/types/uni-halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '恋爱故事',
|
||||
navigationStyle: 'custom',
|
||||
navigationStyle: 'custom',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const scrollTop = ref(0)
|
||||
const stories = ref<ILoveStory[]>([])
|
||||
const showDetail = ref(false)
|
||||
const currentStory = ref<ILoveStory>({})
|
||||
const currentStoryHtml = ref('')
|
||||
const storyImageIndex = ref(0)
|
||||
/* ---------------- 展示层类型 ---------------- */
|
||||
/** 时间轴故事卡片(script 预处理后的干净展示数据) */
|
||||
interface IStoryCard {
|
||||
/** 唯一 key(metadata.name,无则用索引) */
|
||||
key: string
|
||||
title: string
|
||||
date: string
|
||||
location: string
|
||||
/** 故事正文(HTML) */
|
||||
content: string
|
||||
/** 全部图片(已预处理 URL) */
|
||||
images: string[]
|
||||
/** 时间轴封面图(最多 3 张,已预处理 URL) */
|
||||
coverImages: string[]
|
||||
}
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
/** 弹窗故事图片(预处理路径) */
|
||||
const currentStoryImages = computed(() => {
|
||||
const images = (currentStory.value as unknown as { spec?: { images?: string[] } }).spec?.images || []
|
||||
return images.map(img => checkImageUrl(img || ''))
|
||||
})
|
||||
/** 空故事占位(弹窗未打开时) */
|
||||
const EMPTY_STORY: IStoryCard = {
|
||||
key: '',
|
||||
title: '',
|
||||
date: '',
|
||||
location: '',
|
||||
content: '',
|
||||
images: [],
|
||||
coverImages: [],
|
||||
}
|
||||
|
||||
/** 故事卡片映射:字段取值 + 图片路径预处理(模板不感知原始接口结构) */
|
||||
function mapStoryCard(story: ILoveStory, index: number): IStoryCard {
|
||||
const spec = story.spec || {}
|
||||
const images = (spec.images || []).map(img => checkImageUrl(img || ''))
|
||||
return {
|
||||
key: story.metadata?.name || `story-${index}`,
|
||||
title: spec.title || '',
|
||||
date: spec.date || '',
|
||||
location: spec.location || '',
|
||||
content: spec.content || '',
|
||||
images,
|
||||
coverImages: images.slice(0, 3),
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||
const stories = ref<IStoryCard[]>([])
|
||||
const showDetail = ref(false)
|
||||
const currentStory = ref<IStoryCard>(EMPTY_STORY)
|
||||
const storyImageIndex = ref(0)
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetStories() {
|
||||
loading.value = 'loading'
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||
try {
|
||||
const res = await getLoveStories({})
|
||||
if (res.data && (res.data as unknown as { items?: unknown[] }).items && ((res.data as unknown as { items: unknown[] }).items.length > 0)) {
|
||||
// 按 priority 排序
|
||||
stories.value = ((res.data as unknown as { items: ILoveStory[] }).items).sort((a, b) => {
|
||||
const priorityA = (a as unknown as { spec?: { priority?: number } }).spec?.priority || 0
|
||||
const priorityB = (b as unknown as { spec?: { priority?: number } }).spec?.priority || 0
|
||||
return priorityB - priorityA
|
||||
})
|
||||
loading.value = 'success'
|
||||
const items = res.data?.items || []
|
||||
if (items.length > 0) {
|
||||
// 按 priority 排序(越大越靠前)
|
||||
const sorted = [...items].sort((a, b) => (b.spec?.priority || 0) - (a.spec?.priority || 0))
|
||||
stories.value = sorted.map(mapStoryCard)
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
||||
}
|
||||
else {
|
||||
// 降级:从旧配置读取单条故事
|
||||
@@ -63,29 +95,27 @@ async function handleGetStories() {
|
||||
}
|
||||
|
||||
function handleLoadFromLegacy() {
|
||||
const appConfigs = appConfigStore.configs
|
||||
const loveModuleConfig = appConfigs.loveConfig as { ourStory?: { content?: string } } | undefined
|
||||
const loveModuleConfig = appConfigStore.configs.loveConfig as { ourStory?: { content?: string } } | undefined
|
||||
if (loveModuleConfig?.ourStory?.content) {
|
||||
stories.value = [{
|
||||
name: 'legacy-story',
|
||||
spec: {
|
||||
title: '我们的故事',
|
||||
content: loveModuleConfig.ourStory.content,
|
||||
date: '',
|
||||
images: [],
|
||||
},
|
||||
key: 'legacy-story',
|
||||
title: '我们的故事',
|
||||
date: '',
|
||||
location: '',
|
||||
content: loveModuleConfig.ourStory.content,
|
||||
images: [],
|
||||
coverImages: [],
|
||||
}]
|
||||
loading.value = 'success'
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
||||
return
|
||||
}
|
||||
stories.value = []
|
||||
loading.value = 'success'
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Empty)
|
||||
}
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handleOnStoryClick(story: ILoveStory) {
|
||||
function handleOnStoryClick(story: IStoryCard) {
|
||||
currentStory.value = story
|
||||
currentStoryHtml.value = (story as unknown as { spec?: { content?: string } }).spec?.content || ''
|
||||
storyImageIndex.value = 0
|
||||
showDetail.value = true
|
||||
}
|
||||
@@ -94,24 +124,16 @@ function handleOnStoryImageChange(e: { detail: { current: number } }) {
|
||||
storyImageIndex.value = e.detail.current
|
||||
}
|
||||
|
||||
/** 时间轴封面图:最多 3 张,预处理路径 */
|
||||
function storyCoverImages(story: ILoveStory): string[] {
|
||||
const images = (story as unknown as { spec?: { images?: string[] } }).spec?.images || []
|
||||
return images.slice(0, 3).map(img => checkImageUrl(img || ''))
|
||||
}
|
||||
|
||||
/** 预览时间轴封面图 */
|
||||
function handlePreviewStoryImages(story: ILoveStory, index: number) {
|
||||
const images = (story as unknown as { spec?: { images?: string[] } }).spec?.images || []
|
||||
const urls = images.map(img => checkImageUrl(img || ''))
|
||||
if (urls.length === 0)
|
||||
/** 预览时间轴封面图(基于已预处理 URL) */
|
||||
function handlePreviewStoryImages(story: IStoryCard, index: number) {
|
||||
if (story.images.length === 0)
|
||||
return
|
||||
uni.previewImage({ current: urls[index], urls })
|
||||
uni.previewImage({ current: story.images[index], urls: story.images })
|
||||
}
|
||||
|
||||
/** 预览弹窗内大图 */
|
||||
function handlePreviewImage(index: number) {
|
||||
const images = (currentStory.value as unknown as { spec?: { images?: string[] } }).spec?.images || []
|
||||
const urls = images.map(img => checkImageUrl(img || ''))
|
||||
const urls = currentStory.value.images
|
||||
if (urls.length > 0) {
|
||||
uni.previewImage({ current: urls[index], urls })
|
||||
}
|
||||
@@ -129,7 +151,6 @@ function handleToTopPage(duration = 500) {
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({ title: '恋爱故事' })
|
||||
handleGetStories()
|
||||
})
|
||||
|
||||
@@ -139,63 +160,50 @@ onPullDownRefresh(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page box-border min-h-screen w-screen p-6 pb-[144rpx]" style="background: linear-gradient(-45deg, rgb(247 149 51 / 10%), rgb(243 112 85 / 10%) 15%, rgb(239 78 123 / 10%) 30%, rgb(161 102 171 / 10%) 44%, rgb(80 115 184 / 10%) 58%, rgb(16 152 173 / 10%) 72%, rgb(7 179 155 / 10%) 86%, rgb(109 186 130 / 10%)); color: rgb(26 26 26);">
|
||||
<view v-if="loading === 'loading'" class="loading-wrap box-border h-[60vh] w-full flex items-center justify-center p-9">
|
||||
<view class="loadig-text mt-7 text-[28rpx] text-[#56bbf9]">
|
||||
故事正在努力加载中啦~
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="loading === 'error'" class="loading-wrap box-border h-[60vh] w-full flex items-center justify-center p-9">
|
||||
<wd-empty description="啊偶,加载失败了呢~">
|
||||
<wd-button size="small" plain type="danger" @click="handleGetStories()">
|
||||
刷新试试
|
||||
</wd-button>
|
||||
</wd-empty>
|
||||
</view>
|
||||
<view v-else class="content-wrap">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="stories.length === 0" class="empty-state h-[60vh] w-full flex items-center justify-center">
|
||||
<wd-empty description="还没有故事,等待你们来书写...">
|
||||
<wd-button size="small" plain type="primary" @click="handleGetStories()">
|
||||
刷新试试
|
||||
</wd-button>
|
||||
</wd-empty>
|
||||
</view>
|
||||
<!-- 时间轴 -->
|
||||
<view v-else class="timeline relative pl-10">
|
||||
<view v-for="(story, index) in stories" :key="String((story as unknown as { name?: string })?.name ?? index)" class="timeline-item relative pb-10" :class="index === stories.length - 1 ? 'timeline-item-last' : ''" @click="handleOnStoryClick(story)">
|
||||
<view class="app-page box-border min-h-screen w-screen flex flex-col" style="background: linear-gradient(-45deg, rgb(247 149 51 / 10%), rgb(243 112 85 / 10%) 15%, rgb(239 78 123 / 10%) 30%, rgb(161 102 171 / 10%) 44%, rgb(80 115 184 / 10%) 58%, rgb(16 152 173 / 10%) 72%, rgb(7 179 155 / 10%) 86%, rgb(109 186 130 / 10%)); color: rgb(26 26 26);">
|
||||
<!-- 自定义导航 -->
|
||||
<uh-navbar default-title="恋爱故事" title-color="text-gray-900" />
|
||||
|
||||
<!-- 加载/错误/空占位(状态机) -->
|
||||
<uh-data-loading
|
||||
v-if="loadingStatus !== DataLoadingStatusEnum.Success"
|
||||
:loading-status="loadingStatus"
|
||||
min-height="60vh"
|
||||
empty-text="还没有故事,等待你们来书写..."
|
||||
@refresh="handleGetStories"
|
||||
/>
|
||||
|
||||
<!-- 时间轴 -->
|
||||
<view v-else class="content-wrap box-border flex-1 p-6 pb-[144rpx]">
|
||||
<view class="timeline relative pl-10">
|
||||
<view v-for="(story, index) in stories" :key="story.key" class="timeline-item relative pb-10" :class="index === stories.length - 1 ? 'timeline-item-last' : ''" @click="handleOnStoryClick(story)">
|
||||
<view class="timeline-dot absolute left-[-32rpx] top-4 z-2 h-5 w-5 rounded-full" style="background-color: #f88ca2; box-shadow: 0 0 0 6rpx rgb(248 140 162 / 20%);" />
|
||||
<view class="timeline-card rounded-xl bg-white p-6 shadow-sm">
|
||||
<view v-if="(story as unknown as { spec?: { date?: string } }).spec?.date" class="timeline-date mb-2 text-[24rpx] text-[#f88ca2]">
|
||||
{{ (story as unknown as { spec?: { date?: string } }).spec?.date }}
|
||||
<view v-if="story.date" class="timeline-date mb-2 text-[24rpx] text-[#f88ca2]">
|
||||
{{ story.date }}
|
||||
</view>
|
||||
<view class="timeline-title text-[32rpx] text-[#333] font-bold">
|
||||
{{ (story as unknown as { spec?: { title?: string } }).spec?.title || '' }}
|
||||
{{ story.title }}
|
||||
</view>
|
||||
<view v-if="(story as unknown as { spec?: { location?: string } }).spec?.location" class="timeline-location mt-2 flex items-center text-[24rpx] text-[#999]">
|
||||
<text class="location-text ml-1">{{ (story as unknown as { spec?: { location?: string } }).spec?.location }}</text>
|
||||
<view v-if="story.location" class="timeline-location mt-2 flex items-center text-[24rpx] text-[#999]">
|
||||
<text class="location-text ml-1">{{ story.location }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="(story as unknown as { spec?: { images?: string[] } }).spec?.images?.length"
|
||||
class="timeline-covers mt-4 flex flex-wrap gap-2"
|
||||
>
|
||||
<view v-if="story.coverImages.length" class="timeline-covers mt-4 flex flex-wrap gap-2">
|
||||
<view
|
||||
v-for="(img, imgIndex) in storyCoverImages(story)"
|
||||
v-for="(img, imgIndex) in story.coverImages"
|
||||
:key="imgIndex"
|
||||
class="timeline-cover h-[180rpx] w-[calc((100%-16rpx)/3)] overflow-hidden rounded-lg"
|
||||
|
||||
@click.stop="handlePreviewStoryImages(story, imgIndex)"
|
||||
>
|
||||
<image class="timeline-cover-img h-full w-full" :src="img" mode="aspectFill" lazy-load />
|
||||
</view>
|
||||
<view
|
||||
v-if="((story as unknown as { spec?: { images?: string[] } }).spec?.images?.length || 0) > 3"
|
||||
v-if="story.images.length > 3"
|
||||
class="timeline-cover timeline-cover-more h-[180rpx] w-[calc((100%-16rpx)/3)] flex items-center justify-center bg-black/50"
|
||||
|
||||
@click.stop="handleOnStoryClick(story)"
|
||||
>
|
||||
<text class="more-text text-[32rpx] text-white font-bold">
|
||||
+{{ ((story as unknown as { spec?: { images?: string[] } }).spec?.images?.length || 0) - 3 }}
|
||||
+{{ story.images.length - 3 }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -213,25 +221,21 @@ onPullDownRefresh(() => {
|
||||
<view class="story-detail h-full w-full flex flex-col overflow-hidden rounded-xl bg-white">
|
||||
<view class="story-detail-header box-border shrink-0 border-b border-black/5 px-7 py-6">
|
||||
<view class="story-detail-title text-[32rpx] text-[#333] font-bold">
|
||||
{{ (currentStory as unknown as { spec?: { title?: string } }).spec?.title || '' }}
|
||||
{{ currentStory.title }}
|
||||
</view>
|
||||
<view
|
||||
v-if="(currentStory as unknown as { spec?: { date?: string; location?: string } }).spec?.date
|
||||
|| (currentStory as unknown as { spec?: { date?: string; location?: string } }).spec?.location"
|
||||
class="story-detail-meta mt-2 flex items-center text-[24rpx] text-[#999]"
|
||||
>
|
||||
<text v-if="(currentStory as unknown as { spec?: { date?: string } }).spec?.date" class="story-detail-date">
|
||||
{{ (currentStory as unknown as { spec?: { date?: string } }).spec?.date }}
|
||||
<view v-if="currentStory.date || currentStory.location" class="story-detail-meta mt-2 flex items-center text-[24rpx] text-[#999]">
|
||||
<text v-if="currentStory.date" class="story-detail-date">
|
||||
{{ currentStory.date }}
|
||||
</text>
|
||||
<text v-if="(currentStory as unknown as { spec?: { location?: string } }).spec?.location" class="story-detail-location ml-6">
|
||||
{{ (currentStory as unknown as { spec?: { location?: string } }).spec?.location }}
|
||||
<text v-if="currentStory.location" class="story-detail-location ml-6">
|
||||
{{ currentStory.location }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 故事图片:多图 swiper 轮播 -->
|
||||
<view v-if="currentStoryImages.length > 0" class="story-images shrink-0">
|
||||
<view v-if="currentStory.images.length > 0" class="story-images shrink-0">
|
||||
<swiper
|
||||
v-if="currentStoryImages.length > 1"
|
||||
v-if="currentStory.images.length > 1"
|
||||
class="story-images-swiper h-[360rpx] w-full"
|
||||
circular
|
||||
indicator-dots
|
||||
@@ -240,14 +244,14 @@ onPullDownRefresh(() => {
|
||||
:current="storyImageIndex"
|
||||
@change="handleOnStoryImageChange"
|
||||
>
|
||||
<swiper-item v-for="(img, imgIndex) in currentStoryImages" :key="imgIndex" class="story-images-item h-full w-full">
|
||||
<swiper-item v-for="(img, imgIndex) in currentStory.images" :key="imgIndex" class="story-images-item h-full w-full">
|
||||
<image :src="img" mode="aspectFill" class="story-image h-full w-full" @click="handlePreviewImage(imgIndex)" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<image v-else :src="currentStoryImages[0]" mode="aspectFill" class="story-image story-image-single h-[360rpx] w-full" @click="handlePreviewImage(0)" />
|
||||
<image v-else :src="currentStory.images[0]" mode="aspectFill" class="story-image story-image-single h-[360rpx] w-full" @click="handlePreviewImage(0)" />
|
||||
</view>
|
||||
<scroll-view scroll-y class="story-detail-content box-border min-h-0 flex-1 px-7 py-6">
|
||||
<view class="story-html text-[28rpx] text-[#333] leading-[1.8]" v-html="currentStoryHtml" />
|
||||
<view class="story-html text-[28rpx] text-[#333] leading-[1.8]" v-html="currentStory.content" />
|
||||
</scroll-view>
|
||||
<view class="story-detail-close box-border shrink-0 border-t border-black/5 px-7 py-5">
|
||||
<text class="close-text block h-20 rounded-[40rpx] text-center text-[30rpx] text-white font-bold" style="background: linear-gradient(135deg, #f88ca2, #ff6b9d); line-height: 80rpx;" @click="showDetail = false">关闭</text>
|
||||
|
||||
Reference in New Issue
Block a user