1
0
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:
小莫唐尼
2026-09-08 18:27:37 +08:00
parent 06f48adcb7
commit 3e787cbd0a
5 changed files with 399 additions and 313 deletions
+80 -3
View File
@@ -405,6 +405,12 @@ export interface ILoveAlbum {
locked?: boolean locked?: boolean
cover?: string cover?: string
photos?: ILovePhoto[] photos?: ILovePhoto[]
/** Halo 资源元数据(接口返回 metadata) */
metadata?: {
name?: string
creationTimestamp?: string
[key: string]: unknown
}
[key: string]: unknown [key: string]: unknown
} }
@@ -420,7 +426,14 @@ export interface ILoveAlbumListReq {
[key: string]: unknown [key: string]: unknown
} }
export type ILoveAlbumListRes = ILoveAlbum[] /** 恋爱相册列表响应(插件分页包装) */
export interface ILoveAlbumListRes {
page?: number
size?: number
total?: number
hasNext?: boolean
items: ILoveAlbum[]
}
export interface ILoveAlbumDetailReq { export interface ILoveAlbumDetailReq {
[key: string]: unknown [key: string]: unknown
@@ -439,6 +452,32 @@ export interface ILoveDailyItem {
id?: string id?: string
content?: string content?: string
date?: string date?: string
/** Halo 资源元数据(接口返回 metadata) */
metadata?: {
name?: string
[key: string]: unknown
}
/** 清单项详情(接口返回 spec) */
spec?: ILoveDailyItemSpec
[key: string]: unknown
}
/** 恋爱清单项 spec(对齐插件 LoveDailyItemSpec) */
export interface ILoveDailyItemSpec {
/** 清单标题 */
title?: string
/** 清单内容 */
content?: string
/** 状态:未开始/进行中/已完成 */
status?: 'wait' | 'doing' | 'complete'
/** 计划时间 */
planDate?: string
/** 完成时间 */
completeDate?: string
/** 完成感想 */
completeRemark?: string
/** 回忆图片 */
images?: string[]
[key: string]: unknown [key: string]: unknown
} }
@@ -448,13 +487,44 @@ export interface ILoveDailyItemListReq {
[key: string]: unknown [key: string]: unknown
} }
export type ILoveDailyItemListRes = ILoveDailyItem[] /** 恋爱清单列表响应(插件分页包装) */
export interface ILoveDailyItemListRes {
page?: number
size?: number
total?: number
hasNext?: boolean
items: ILoveDailyItem[]
}
export interface ILoveStory { export interface ILoveStory {
id?: string id?: string
title?: string title?: string
content?: string content?: string
date?: string date?: string
/** Halo 资源元数据(接口返回 metadata) */
metadata?: {
name?: string
[key: string]: unknown
}
/** 故事详情(接口返回 spec) */
spec?: ILoveStorySpec
[key: string]: unknown
}
/** 恋爱故事 spec(对齐插件 LoveStorySpec) */
export interface ILoveStorySpec {
/** 故事标题 */
title?: string
/** 故事内容(HTML) */
content?: string
/** 故事日期 */
date?: string
/** 故事地点 */
location?: string
/** 故事图片 */
images?: string[]
/** 排序优先级(越大越靠前) */
priority?: number
[key: string]: unknown [key: string]: unknown
} }
@@ -556,4 +626,11 @@ export interface IMiniProgramLinkSubmissionForm {
email?: string email?: string
} }
export type ILoveStoryListRes = ILoveStory[] /** 恋爱故事列表响应(插件分页包装) */
export interface ILoveStoryListRes {
page?: number
size?: number
total?: number
hasNext?: boolean
items: ILoveStory[]
}
+83 -78
View File
@@ -10,11 +10,13 @@ import { getLoveAlbumByName, getLoveAlbums } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url' import { checkImageUrl } from '@/utils/url'
import { getCache, setCache } from '@/utils/storage' import { getCache, setCache } from '@/utils/storage'
import type { ILoveAlbum } from '@/api/types/uni-halo' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ILoveAlbum, ILovePhoto } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '恋爱相册', navigationBarTitleText: '恋爱相册',
navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
@@ -27,18 +29,48 @@ const UNLOCKED_ALBUMS_CACHE_KEY = 'unlocked_albums'
/** 解锁 token 有效期(后端默认 30 分钟) */ /** 解锁 token 有效期(后端默认 30 分钟) */
const ALBUM_TOKEN_TTL_SECONDS = 30 * 60 const ALBUM_TOKEN_TTL_SECONDS = 30 * 60
/* ---------------- 展示层类型 ---------------- */
/** 相册展示卡片(script 预处理后的干净展示数据) */
interface ILoveAlbumCard {
/** 相册 key(metadata.name,用于解锁/详情请求) */
name: string
displayName: string
locked: boolean
photoCount: number
/** 封面图(已预处理 URL) */
image: string
/** 创建时间(格式化展示) */
takeTime: string
/** 相册照片(解锁后填充) */
photos: ILovePhoto[]
}
/** 相册卡片映射:字段取值 + 封面/时间预处理(模板不感知原始接口结构) */
function mapAlbumCard(item: ILoveAlbum): ILoveAlbumCard {
const creationTimestamp = item.metadata?.creationTimestamp
return {
name: item.name || item.metadata?.name || '',
displayName: item.displayName || item.title || '',
locked: !!item.locked,
photoCount: Number(item.photoCount) || 0,
image: checkImageUrl(item.cover || ''),
takeTime: creationTimestamp ? dayjs(creationTimestamp).format('DD/MM/YYYY') : '',
photos: item.photos || [],
}
}
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const dataList = ref<(ILoveAlbum & { image?: string, takeTime?: string })[]>([]) const dataList = ref<ILoveAlbumCard[]>([])
const unlockedAlbums = ref<Record<string, string>>({}) const unlockedAlbums = ref<Record<string, string>>({})
/** 密码解锁弹窗 */ /** 密码解锁弹窗 */
const showUnlockModal = ref(false) const showUnlockModal = ref(false)
const currentUnlockAlbum = ref<(ILoveAlbum & { image?: string }) | null>(null) const currentUnlockAlbum = ref<ILoveAlbumCard | null>(null)
/** 图片查看弹窗 */ /** 图片查看弹窗 */
const showPhotoViewer = ref(false) const showPhotoViewer = ref(false)
const currentViewerAlbum = ref<(ILoveAlbum & { image?: string }) | null>(null) const currentViewerAlbum = ref<ILoveAlbumCard | null>(null)
const viewerLoading = ref(false) const viewerLoading = ref(false)
const unlockAlbumName = computed(() => currentUnlockAlbum.value?.displayName || '') const unlockAlbumName = computed(() => currentUnlockAlbum.value?.displayName || '')
@@ -70,30 +102,18 @@ function handleSaveUnlockedAlbums() {
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getLoveAlbums({}) const res = await getLoveAlbums({})
if (res.data && (res.data as unknown as { items?: unknown[] }).items) { const items = res.data?.items || []
dataList.value = ((res.data as unknown as { items: ILoveAlbum[] }).items || []).map((item) => { dataList.value = items.map(mapAlbumCard)
const creationTimestamp = (item.metadata as unknown as { creationTimestamp?: string } | undefined)?.creationTimestamp updateLoadingStatus(dataList.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
return { if (dataList.value.length > 0)
...item,
image: checkImageUrl(item.cover),
takeTime: creationTimestamp ? dayjs(creationTimestamp).format('DD/MM/YYYY') : '',
}
})
loading.value = 'success'
handleLoadUnlockedAlbumPhotos() handleLoadUnlockedAlbumPhotos()
}
else {
dataList.value = []
loading.value = 'success'
}
} }
catch (e) { catch (e) {
console.error('获取相册失败', e) console.error('获取相册失败', e)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
uni.showToast({ icon: 'none', title: '加载失败,请下拉刷新重试!' })
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
@@ -105,12 +125,12 @@ async function handleGetData() {
/** 加载已解锁相册的照片 */ /** 加载已解锁相册的照片 */
async function handleLoadUnlockedAlbumPhotos() { async function handleLoadUnlockedAlbumPhotos() {
for (const item of dataList.value) { for (const item of dataList.value) {
if (item.locked && unlockedAlbums.value[item.name || '']) { const token = unlockedAlbums.value[item.name]
const token = unlockedAlbums.value[item.name || ''] if (item.locked && token) {
try { try {
const detail = await getLoveAlbumByName(item.name || '', { token }) const detail = await getLoveAlbumByName(item.name, { token })
if (detail.locked) { if (detail.locked) {
delete unlockedAlbums.value[item.name || ''] delete unlockedAlbums.value[item.name]
handleSaveUnlockedAlbums() handleSaveUnlockedAlbums()
} }
else if (detail.photos) { else if (detail.photos) {
@@ -126,8 +146,8 @@ async function handleLoadUnlockedAlbumPhotos() {
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleOnAlbumClick(item: ILoveAlbum & { image?: string }) { function handleOnAlbumClick(item: ILoveAlbumCard) {
if (item.locked && !unlockedAlbums.value[item.name || '']) { if (item.locked && !unlockedAlbums.value[item.name]) {
currentUnlockAlbum.value = item currentUnlockAlbum.value = item
showUnlockModal.value = true showUnlockModal.value = true
return return
@@ -135,18 +155,18 @@ function handleOnAlbumClick(item: ILoveAlbum & { image?: string }) {
handleOpenPhotoViewer(item) handleOpenPhotoViewer(item)
} }
async function handleOpenPhotoViewer(item: ILoveAlbum & { image?: string }) { async function handleOpenPhotoViewer(item: ILoveAlbumCard) {
currentViewerAlbum.value = item currentViewerAlbum.value = item
showPhotoViewer.value = true showPhotoViewer.value = true
if (item.photos && item.photos.length > 0) if (item.photos.length > 0)
return return
viewerLoading.value = true viewerLoading.value = true
try { try {
const token = unlockedAlbums.value[item.name || ''] || '' const token = unlockedAlbums.value[item.name] || ''
const detail = await getLoveAlbumByName(item.name || '', { token }) const detail = await getLoveAlbumByName(item.name, { token })
if (detail) { if (detail) {
if (detail.locked) { if (detail.locked) {
delete unlockedAlbums.value[item.name || ''] delete unlockedAlbums.value[item.name]
handleSaveUnlockedAlbums() handleSaveUnlockedAlbums()
} }
else if (detail.photos) { else if (detail.photos) {
@@ -170,7 +190,7 @@ function handleOnUnlockSuccess(data: { albumKey: string, token: string, photos:
const albumIndex = dataList.value.findIndex(a => a.name === data.albumKey) const albumIndex = dataList.value.findIndex(a => a.name === data.albumKey)
if (albumIndex !== -1) { if (albumIndex !== -1) {
dataList.value[albumIndex].photos = data.photos as typeof dataList.value[number]['photos'] dataList.value[albumIndex].photos = data.photos as ILovePhoto[]
dataList.value[albumIndex].locked = false dataList.value[albumIndex].locked = false
} }
currentUnlockAlbum.value = null currentUnlockAlbum.value = null
@@ -181,7 +201,6 @@ function handleOnUnlockSuccess(data: { albumKey: string, token: string, photos:
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad(() => { onLoad(() => {
uni.setNavigationBarTitle({ title: '恋爱相册' })
handleRestoreUnlockedAlbums() handleRestoreUnlockedAlbums()
handleGetData() handleGetData()
}) })
@@ -193,52 +212,38 @@ onPullDownRefresh(() => {
<template> <template>
<view class="app-page box-border min-h-screen w-screen flex flex-col pb-[144rpx]" style="background: linear-gradient(-135deg, 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%));"> <view class="app-page box-border min-h-screen w-screen flex flex-col pb-[144rpx]" style="background: linear-gradient(-135deg, 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%));">
<view v-if="loading !== 'success'" class="loading-wrap box-border h-[60vh] w-screen flex flex-col items-center justify-center p-9"> <!-- 自定义导航 -->
<view v-if="loading === 'loading'" class="loading"> <uh-navbar default-title="恋爱相册" title-color="text-gray-900" />
<view class="loadig-text mt-7 text-[28rpx] text-[#56bbf9]">
相册正在努力加载中啦~
</view>
</view>
<view v-else class="loading-error w-full">
<wd-empty description="啊偶,加载失败了呢~">
<wd-button size="small" plain type="danger" @click="handleGetData()">
刷新试试
</wd-button>
</wd-empty>
</view>
</view>
<!-- 内容区域 --> <!-- 加载/错误/空占位(状态机) -->
<view v-else class="app-page-content"> <uh-data-loading
<view v-if="dataList.length === 0" class="h-[60vh] w-full flex items-center justify-center content-empty"> v-if="loadingStatus !== DataLoadingStatusEnum.Success"
<wd-empty description="相册暂时还没有数据~"> :loading-status="loadingStatus"
<wd-button size="small" plain type="primary" @click="handleGetData()"> min-height="60vh"
刷新试试 empty-text="相册暂时还没有数据~"
</wd-button> @refresh="handleGetData"
</wd-empty> />
</view>
<!-- 相册列表(两列网格) --> <!-- 相册列表(两列网格) -->
<view v-else class="album-list box-border flex flex-wrap px-6"> <view v-else class="album-list box-border flex flex-wrap px-6">
<view v-for="(item, index) in dataList" :key="index" class="album-card mb-6 box-border overflow-hidden rounded-xl bg-white shadow-sm" :class="index % 2 === 0 ? 'mr-6 w-[calc((100%-24rpx)/2)]' : 'w-[calc((100%-24rpx)/2)]'" @click="handleOnAlbumClick(item)"> <view v-for="(item, index) in dataList" :key="item.name" class="album-card mb-6 box-border overflow-hidden rounded-xl bg-white shadow-sm" :class="index % 2 === 0 ? 'mr-6 w-[calc((100%-24rpx)/2)]' : 'w-[calc((100%-24rpx)/2)]'" @click="handleOnAlbumClick(item)">
<view class="album-cover-wrap relative h-[320rpx] w-full"> <view class="album-cover-wrap relative h-[320rpx] w-full">
<image class="album-cover h-full w-full" :src="item.image" mode="aspectFill" lazy-load /> <image class="album-cover h-full w-full" :src="item.image" mode="aspectFill" lazy-load />
<view v-if="item.locked && !unlockedAlbums[item.name || '']" class="album-lock-mask absolute left-0 top-0 h-full w-full flex flex-col items-center justify-center bg-black/45"> <view v-if="item.locked && !unlockedAlbums[item.name]" class="album-lock-mask absolute left-0 top-0 h-full w-full flex flex-col items-center justify-center bg-black/45">
<view class="lock-icon text-[64rpx]"> <view class="lock-icon text-[64rpx]">
🔒 🔒
</view> </view>
<view class="lock-tip mt-3 text-[26rpx] text-white"> <view class="lock-tip mt-3 text-[26rpx] text-white">
已加密 已加密
</view>
</view> </view>
</view> </view>
<view class="album-info box-border p-5"> </view>
<view class="album-name overflow-hidden text-ellipsis whitespace-nowrap text-[30rpx] text-[#333] font-bold"> <view class="album-info box-border p-5">
{{ item.displayName }} <view class="album-name overflow-hidden text-ellipsis whitespace-nowrap text-[30rpx] text-[#333] font-bold">
</view> {{ item.displayName }}
<view class="album-count mt-1 text-[24rpx] text-[#999]"> </view>
{{ item.photoCount || 0 }} 张照片 <view class="album-count mt-1 text-[24rpx] text-[#999]">
</view> {{ item.photoCount }} 张照片
</view> </view>
</view> </view>
</view> </view>
+124 -124
View File
@@ -7,56 +7,66 @@ import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getLoveDailyItems } from '@/api/uni-halo' import { getLoveDailyItems } from '@/api/uni-halo'
import { checkImageUrl } from '@/utils/url' import { checkImageUrl } from '@/utils/url'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ILoveDailyItem } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '恋爱清单', navigationBarTitleText: '恋爱清单',
navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
/* ---------------- 状态 ---------------- */ /* ---------------- 展示层类型 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') /** 清单展示卡片(script 预处理后的干净展示数据) */
const list = ref<(ILoveItem & { open: boolean })[]>([]) interface ILoveItemCard {
/** 唯一 key(metadata.name,无则用索引) */
interface ILoveItem { name: string
name?: string title: string
title?: string content: string
content?: string status: 'wait' | 'doing' | 'complete'
status?: 'wait' | 'doing' | 'complete' planDate: string
planDate?: string completeDate: string
completeDate?: string completeRemark: string
completeRemark?: string /** 回忆图片(已预处理 URL) */
images?: string[] images: string[]
[key: string]: unknown /** 是否展开详情 */
open: boolean
} }
/** 清单卡片映射:字段取值 + 图片路径预处理(模板不感知原始接口结构) */
function mapItemCard(item: ILoveDailyItem, index: number): ILoveItemCard {
const spec = item.spec || {}
return {
name: item.metadata?.name || `item-${index}`,
title: spec.title || '',
content: spec.content || '',
status: spec.status || 'wait',
planDate: spec.planDate || '',
completeDate: spec.completeDate || '',
completeRemark: spec.completeRemark || '',
images: (spec.images || []).map(img => checkImageUrl(img || '')),
open: false,
}
}
/* ---------------- 状态 ---------------- */
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
const list = ref<ILoveItemCard[]>([])
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetList() { async function handleGetList() {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getLoveDailyItems({}) const res = await getLoveDailyItems({})
if (res.data && (res.data as unknown as { items?: unknown[] }).items) { const items = res.data?.items || []
list.value = ((res.data as unknown as { items: unknown[] }).items as unknown as { list.value = items.map(mapItemCard)
spec?: ILoveItem updateLoadingStatus(list.value.length === 0 ? DataLoadingStatusEnum.Empty : DataLoadingStatusEnum.Success)
metadata?: { name?: string }
name?: string
}[]).map(item => ({
...item.spec,
name: item.metadata?.name || item.name || '',
open: false,
}))
loading.value = 'success'
}
else {
list.value = []
loading.value = 'success'
}
} }
catch (e) { catch (e) {
console.error('获取清单失败', e) console.error('获取清单失败', e)
loading.value = 'error' updateLoadingStatus(DataLoadingStatusEnum.Error)
uni.showToast({ icon: 'none', title: '加载失败,请下拉刷新重试!' })
} }
finally { finally {
setTimeout(() => { setTimeout(() => {
@@ -66,15 +76,15 @@ async function handleGetList() {
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleOnItemOpen(item: ILoveItem & { open: boolean }) { function handleOnItemOpen(item: ILoveItemCard) {
item.open = !item.open item.open = !item.open
} }
function handlePreviewImages(images: string[] | undefined, index: number) { /** 预览回忆图片(基于已预处理 URL) */
const urls = (images || []).map(img => checkImageUrl(img || '')) function handlePreviewImages(images: string[], index: number) {
if (urls.length === 0) if (images.length === 0)
return return
uni.previewImage({ current: urls[index], urls }) uni.previewImage({ current: images[index], urls: images })
} }
function handleToTopPage(duration = 500) { function handleToTopPage(duration = 500) {
@@ -89,7 +99,6 @@ function handleToTopPage(duration = 500) {
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad(() => { onLoad(() => {
uni.setNavigationBarTitle({ title: '恋爱清单' })
handleGetList() handleGetList()
}) })
@@ -99,104 +108,95 @@ onPullDownRefresh(() => {
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen w-screen p-6 pb-[144rpx]" style="background: linear-gradient(135deg, 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%));"> <view class="app-page box-border min-h-screen w-screen flex flex-col" style="background: linear-gradient(135deg, 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%));">
<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]"> <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="handleGetList"
/>
<!-- 清单列表 -->
<view v-else class="list-wrap box-border flex-1 p-6 pb-[144rpx]">
<view class="list-tip mb-7 w-full text-center text-[26rpx] text-[#999]">
看看我们的恋爱清单都完成了哪些吧
</view> </view>
</view> <block v-for="item in list" :key="item.name">
<view v-else-if="loading === 'error'" class="loading-wrap box-border h-[60vh] w-full flex items-center justify-center p-9"> <view class="card mb-6 box-border w-full flex flex-col items-center rounded-3xl bg-white p-6 shadow-sm">
<wd-empty description="啊偶,加载失败了呢~"> <view class="head box-border w-full flex items-center" @click="handleOnItemOpen(item)">
<wd-button size="small" plain type="danger" @click="handleGetList()"> <view class="status w-[100rpx] flex">
刷新试试 <view v-if="item.status === 'wait'" class="text h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #ffc6ba; color: #55423b;">
</wd-button> 未开始
</wd-empty>
</view>
<view v-else class="list-wrap w-full">
<view v-if="list.length === 0" class="list h-[60vh] flex flex-col items-center justify-center">
<wd-empty description="暂时还没有恋爱清单,快去制定你们的恋爱清单吧~">
<wd-button size="small" plain type="primary" @click="handleGetList()">
刷新试试
</wd-button>
</wd-empty>
</view>
<view v-else class="list">
<view class="list-tip mb-7 w-full text-center text-[26rpx] text-[#999]">
看看我们的恋爱清单都完成了哪些吧
</view>
<block v-for="(item, index) in list" :key="item.name || index">
<view class="card mb-6 box-border w-full flex flex-col items-center rounded-3xl bg-white p-6 shadow-sm">
<view class="head box-border w-full flex items-center" @click="handleOnItemOpen(item)">
<view class="status w-[100rpx] flex">
<view v-if="item.status === 'wait'" class="text h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #ffc6ba; color: #55423b;">
未开始
</view>
<view v-else-if="item.status === 'doing'" class="text doing h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #ffe9a8; color: #55423b;">
进行中
</view>
<view v-else class="text finish h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #bfe9ef; color: #55423b;">
已完成
</view>
</view> </view>
<view class="title box-border w-0 flex-1 px-7"> <view v-else-if="item.status === 'doing'" class="text doing h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #ffe9a8; color: #55423b;">
<view class="title-name text-[30rpx] text-[#333] font-bold"> 进行中
{{ item.title }}
</view>
<view v-if="item.content" class="title-desc mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#555]">
{{ item.content }}
</view>
</view> </view>
<view class="actions w-[50rpx]"> <view v-else class="text finish h-[100rpx] w-[100rpx] rounded-full text-center text-[24rpx] leading-[100rpx]" style="background-color: #bfe9ef; color: #55423b;">
<text class="icon inline-block h-[45rpx] w-[45rpx] rounded-full bg-black/20 text-center text-[32rpx] font-bold leading-[45rpx]">{{ item.open ? '-' : '+' }}</text> 已完成
</view> </view>
</view> </view>
<view v-if="item.open" class="body mt-6 box-border w-full rounded-3xl bg-black/5 px-6 pb-3 pt-6 text-[26rpx]"> <view class="title box-border w-0 flex-1 px-7">
<view v-if="item.planDate" class="desc mb-3 flex"> <view class="title-name text-[30rpx] text-[#333] font-bold">
<view class="desc-label w-[140rpx] shrink-0 text-[#333]"> {{ item.title }}
计划时间
</view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
{{ item.planDate || '-' }}
</view>
</view> </view>
<view v-if="item.completeDate" class="desc mb-3 flex"> <view v-if="item.content" class="title-desc mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#555]">
<view class="desc-label w-[140rpx] shrink-0 text-[#333]"> {{ item.content }}
完成时间
</view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
{{ item.completeDate || '-' }}
</view>
</view> </view>
<view v-if="item.completeRemark" class="desc mb-3 flex"> </view>
<view class="desc-label w-[140rpx] shrink-0 text-[#333]"> <view class="actions w-[50rpx]">
完成感想 <text class="icon inline-block h-[45rpx] w-[45rpx] rounded-full bg-black/20 text-center text-[32rpx] font-bold leading-[45rpx]">{{ item.open ? '-' : '+' }}</text>
</view> </view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]"> </view>
{{ item.completeRemark || '-' }} <view v-if="item.open" class="body mt-6 box-border w-full rounded-3xl bg-black/5 px-6 pb-3 pt-6 text-[26rpx]">
</view> <view v-if="item.planDate" class="desc mb-3 flex">
<view class="desc-label w-[140rpx] shrink-0 text-[#333]">
计划时间
</view> </view>
<view v-if="item.images && item.images.length > 0" class="desc mb-3 flex"> <view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
<view class="desc-label w-[140rpx] shrink-0 text-[#333]"> {{ item.planDate || '-' }}
回忆图片 </view>
</view> </view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]"> <view v-if="item.completeDate" class="desc mb-3 flex">
<view class="images flex flex-wrap"> <view class="desc-label w-[140rpx] shrink-0 text-[#333]">
<view 完成时间
v-for="(img, imgIndex) in item.images" </view>
:key="imgIndex" <view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
class="image mb-3 mr-3 h-[180rpx] w-[calc((100%-24rpx)/3)] overflow-hidden rounded-lg" {{ item.completeDate || '-' }}
</view>
@click="handlePreviewImages(item.images, imgIndex)" </view>
> <view v-if="item.completeRemark" class="desc mb-3 flex">
<image class="image-src h-full w-full" :src="checkImageUrl(img)" mode="aspectFill" lazy-load /> <view class="desc-label w-[140rpx] shrink-0 text-[#333]">
</view> 完成感想
</view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
{{ item.completeRemark || '-' }}
</view>
</view>
<view v-if="item.images.length > 0" class="desc mb-3 flex">
<view class="desc-label w-[140rpx] shrink-0 text-[#333]">
回忆图片
</view>
<view class="desc-value w-0 flex-1 text-[#333] leading-[1.5]">
<view class="images flex flex-wrap">
<view
v-for="(img, imgIndex) in item.images"
:key="imgIndex"
class="image mb-3 mr-3 h-[180rpx] w-[calc((100%-24rpx)/3)] overflow-hidden rounded-lg"
@click="handlePreviewImages(item.images, imgIndex)"
>
<image class="image-src h-full w-full" :src="img" mode="aspectFill" lazy-load />
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</block> </view>
</view> </block>
</view> </view>
<view class="to-top-btn fixed bottom-[160rpx] 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-[160rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
+1 -1
View File
@@ -100,7 +100,7 @@
const configs = loveConfig.value const configs = loveConfig.value
navList.value = [ navList.value = [
{ {
key: 'story', key: 'stories',
use: configs.ourStory.enabled, use: configs.ourStory.enabled,
title: '恋爱故事', title: '恋爱故事',
desc: '我们一起度过的那些经历', desc: '我们一起度过的那些经历',
+111 -107
View File
@@ -1,50 +1,82 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getLoveStories } from '@/api/uni-halo' import { getLoveStories } from '@/api/uni-halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { checkImageUrl } from '@/utils/url' import { checkImageUrl } from '@/utils/url'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import type { ILoveStory } from '@/api/types/uni-halo' import type { ILoveStory } from '@/api/types/uni-halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '恋爱故事', navigationBarTitleText: '恋爱故事',
navigationStyle: 'custom', navigationStyle: 'custom',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
/* ---------------- 状态 ---------------- */ /* ---------------- 展示层类型 ---------------- */
const loading = ref<'loading' | 'success' | 'error'>('loading') /** 时间轴故事卡片(script 预处理后的干净展示数据) */
const scrollTop = ref(0) interface IStoryCard {
const stories = ref<ILoveStory[]>([]) /** 唯一 key(metadata.name,无则用索引) */
const showDetail = ref(false) key: string
const currentStory = ref<ILoveStory>({}) title: string
const currentStoryHtml = ref('') date: string
const storyImageIndex = ref(0) location: string
/** 故事正文(HTML) */
content: string
/** 全部图片(已预处理 URL) */
images: string[]
/** 时间轴封面图(最多 3 张,已预处理 URL) */
coverImages: string[]
}
/* ---------------- 计算属性 ---------------- */ /** 空故事占位(弹窗未打开时) */
/** 弹窗故事图片(预处理路径) */ const EMPTY_STORY: IStoryCard = {
const currentStoryImages = computed(() => { key: '',
const images = (currentStory.value as unknown as { spec?: { images?: string[] } }).spec?.images || [] title: '',
return images.map(img => checkImageUrl(img || '')) 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() { async function handleGetStories() {
loading.value = 'loading' updateLoadingStatus(DataLoadingStatusEnum.Loading)
try { try {
const res = await getLoveStories({}) 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)) { const items = res.data?.items || []
// 按 priority 排序 if (items.length > 0) {
stories.value = ((res.data as unknown as { items: ILoveStory[] }).items).sort((a, b) => { // 按 priority 排序(越大越靠前)
const priorityA = (a as unknown as { spec?: { priority?: number } }).spec?.priority || 0 const sorted = [...items].sort((a, b) => (b.spec?.priority || 0) - (a.spec?.priority || 0))
const priorityB = (b as unknown as { spec?: { priority?: number } }).spec?.priority || 0 stories.value = sorted.map(mapStoryCard)
return priorityB - priorityA updateLoadingStatus(DataLoadingStatusEnum.Success)
})
loading.value = 'success'
} }
else { else {
// 降级:从旧配置读取单条故事 // 降级:从旧配置读取单条故事
@@ -63,29 +95,27 @@ async function handleGetStories() {
} }
function handleLoadFromLegacy() { function handleLoadFromLegacy() {
const appConfigs = appConfigStore.configs const loveModuleConfig = appConfigStore.configs.loveConfig as { ourStory?: { content?: string } } | undefined
const loveModuleConfig = appConfigs.loveConfig as { ourStory?: { content?: string } } | undefined
if (loveModuleConfig?.ourStory?.content) { if (loveModuleConfig?.ourStory?.content) {
stories.value = [{ stories.value = [{
name: 'legacy-story', key: 'legacy-story',
spec: { title: '我们的故事',
title: '我们的故事', date: '',
content: loveModuleConfig.ourStory.content, location: '',
date: '', content: loveModuleConfig.ourStory.content,
images: [], images: [],
}, coverImages: [],
}] }]
loading.value = 'success' updateLoadingStatus(DataLoadingStatusEnum.Success)
return return
} }
stories.value = [] stories.value = []
loading.value = 'success' updateLoadingStatus(DataLoadingStatusEnum.Empty)
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleOnStoryClick(story: ILoveStory) { function handleOnStoryClick(story: IStoryCard) {
currentStory.value = story currentStory.value = story
currentStoryHtml.value = (story as unknown as { spec?: { content?: string } }).spec?.content || ''
storyImageIndex.value = 0 storyImageIndex.value = 0
showDetail.value = true showDetail.value = true
} }
@@ -94,24 +124,16 @@ function handleOnStoryImageChange(e: { detail: { current: number } }) {
storyImageIndex.value = e.detail.current storyImageIndex.value = e.detail.current
} }
/** 时间轴封面图:最多 3 张,预处理路径 */ /** 预览时间轴封面图(基于已预处理 URL) */
function storyCoverImages(story: ILoveStory): string[] { function handlePreviewStoryImages(story: IStoryCard, index: number) {
const images = (story as unknown as { spec?: { images?: string[] } }).spec?.images || [] if (story.images.length === 0)
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)
return return
uni.previewImage({ current: urls[index], urls }) uni.previewImage({ current: story.images[index], urls: story.images })
} }
/** 预览弹窗内大图 */
function handlePreviewImage(index: number) { function handlePreviewImage(index: number) {
const images = (currentStory.value as unknown as { spec?: { images?: string[] } }).spec?.images || [] const urls = currentStory.value.images
const urls = images.map(img => checkImageUrl(img || ''))
if (urls.length > 0) { if (urls.length > 0) {
uni.previewImage({ current: urls[index], urls }) uni.previewImage({ current: urls[index], urls })
} }
@@ -129,7 +151,6 @@ function handleToTopPage(duration = 500) {
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
onLoad(() => { onLoad(() => {
uni.setNavigationBarTitle({ title: '恋爱故事' })
handleGetStories() handleGetStories()
}) })
@@ -139,63 +160,50 @@ onPullDownRefresh(() => {
</script> </script>
<template> <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 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);">
<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]"> <uh-navbar default-title="恋爱故事" title-color="text-gray-900" />
故事正在努力加载中啦~
</view> <!-- 加载/错误/空占位(状态机) -->
</view> <uh-data-loading
<view v-else-if="loading === 'error'" class="loading-wrap box-border h-[60vh] w-full flex items-center justify-center p-9"> v-if="loadingStatus !== DataLoadingStatusEnum.Success"
<wd-empty description="啊偶,加载失败了呢~"> :loading-status="loadingStatus"
<wd-button size="small" plain type="danger" @click="handleGetStories()"> min-height="60vh"
刷新试试 empty-text="还没有故事等待你们来书写..."
</wd-button> @refresh="handleGetStories"
</wd-empty> />
</view>
<view v-else class="content-wrap"> <!-- 时间轴 -->
<!-- 空状态 --> <view v-else class="content-wrap box-border flex-1 p-6 pb-[144rpx]">
<view v-if="stories.length === 0" class="empty-state h-[60vh] w-full flex items-center justify-center"> <view class="timeline relative pl-10">
<wd-empty description="还没有故事,等待你们来书写..."> <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)">
<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="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-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 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]"> <view v-if="story.date" class="timeline-date mb-2 text-[24rpx] text-[#f88ca2]">
{{ (story as unknown as { spec?: { date?: string } }).spec?.date }} {{ story.date }}
</view> </view>
<view class="timeline-title text-[32rpx] text-[#333] font-bold"> <view class="timeline-title text-[32rpx] text-[#333] font-bold">
{{ (story as unknown as { spec?: { title?: string } }).spec?.title || '' }} {{ story.title }}
</view> </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]"> <view v-if="story.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> <text class="location-text ml-1">{{ story.location }}</text>
</view> </view>
<view <view v-if="story.coverImages.length" class="timeline-covers mt-4 flex flex-wrap gap-2">
v-if="(story as unknown as { spec?: { images?: string[] } }).spec?.images?.length"
class="timeline-covers mt-4 flex flex-wrap gap-2"
>
<view <view
v-for="(img, imgIndex) in storyCoverImages(story)" v-for="(img, imgIndex) in story.coverImages"
:key="imgIndex" :key="imgIndex"
class="timeline-cover h-[180rpx] w-[calc((100%-16rpx)/3)] overflow-hidden rounded-lg" class="timeline-cover h-[180rpx] w-[calc((100%-16rpx)/3)] overflow-hidden rounded-lg"
@click.stop="handlePreviewStoryImages(story, imgIndex)" @click.stop="handlePreviewStoryImages(story, imgIndex)"
> >
<image class="timeline-cover-img h-full w-full" :src="img" mode="aspectFill" lazy-load /> <image class="timeline-cover-img h-full w-full" :src="img" mode="aspectFill" lazy-load />
</view> </view>
<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" 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)" @click.stop="handleOnStoryClick(story)"
> >
<text class="more-text text-[32rpx] text-white font-bold"> <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> </text>
</view> </view>
</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 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-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"> <view class="story-detail-title text-[32rpx] text-[#333] font-bold">
{{ (currentStory as unknown as { spec?: { title?: string } }).spec?.title || '' }} {{ currentStory.title }}
</view> </view>
<view <view v-if="currentStory.date || currentStory.location" class="story-detail-meta mt-2 flex items-center text-[24rpx] text-[#999]">
v-if="(currentStory as unknown as { spec?: { date?: string; location?: string } }).spec?.date <text v-if="currentStory.date" class="story-detail-date">
|| (currentStory as unknown as { spec?: { date?: string; location?: string } }).spec?.location" {{ currentStory.date }}
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 }}
</text> </text>
<text v-if="(currentStory as unknown as { spec?: { location?: string } }).spec?.location" class="story-detail-location ml-6"> <text v-if="currentStory.location" class="story-detail-location ml-6">
{{ (currentStory as unknown as { spec?: { location?: string } }).spec?.location }} {{ currentStory.location }}
</text> </text>
</view> </view>
</view> </view>
<!-- 故事图片:多图 swiper 轮播 --> <!-- 故事图片:多图 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 <swiper
v-if="currentStoryImages.length > 1" v-if="currentStory.images.length > 1"
class="story-images-swiper h-[360rpx] w-full" class="story-images-swiper h-[360rpx] w-full"
circular circular
indicator-dots indicator-dots
@@ -240,14 +244,14 @@ onPullDownRefresh(() => {
:current="storyImageIndex" :current="storyImageIndex"
@change="handleOnStoryImageChange" @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)" /> <image :src="img" mode="aspectFill" class="story-image h-full w-full" @click="handlePreviewImage(imgIndex)" />
</swiper-item> </swiper-item>
</swiper> </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> </view>
<scroll-view scroll-y class="story-detail-content box-border min-h-0 flex-1 px-7 py-6"> <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> </scroll-view>
<view class="story-detail-close box-border shrink-0 border-t border-black/5 px-7 py-5"> <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> <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>