1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 12:30:41 +08:00

feat(gallery): 实现相册功能对接后端API,替换静态模拟数据

1.  重构相册页面,使用真实的Halo图片插件API获取分组与图片数据
2.  新增图片预览工具函数,支持大图预览
3.  更新滚动加载hook,补充响应式原始数据与加载状态控制
4.  调整相册面板组件参数与样式,适配真实数据展示
5.  修复开发环境接口地址配置
This commit is contained in:
小莫唐尼
2026-06-14 17:36:13 +08:00
parent 759373e327
commit a60def91a0
7 changed files with 615 additions and 254 deletions
+348
View File
@@ -0,0 +1,348 @@
<script lang="ts" setup>
import { getPicSumImages } from '@/utils/randomResources'
import { queryLinks } from '@/api/halo-plugin/link'
defineOptions({
name: 'Gallery',
})
definePage({
style: {
navigationBarTitleText: '相册',
navigationBarBackgroundColor: '#ffffff',
enablePullDownRefresh: true,
},
})
interface GalleryItem {
metadataName: string
displayName: string
url: string
}
interface GalleryGroup {
metadataName: string
displayName: string
images: GalleryItem[]
}
const groupBanner = reactive({
current: 0,
})
const galleryStyle = ref('list')
const showListPanel = reactive({
show: false,
metadataName: '',
})
// 相册分组预览
const galleryGroups = ref<GalleryGroup[]>([
{
metadataName: 'photo-group-HGaGs',
displayName: '我们的故事',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '履行记录',
displayName: '履行记录',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '家庭时光',
displayName: '家庭时光',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '其他',
displayName: '其他',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}],
},
])
// 相册图片
const galleryItems = ref<GalleryItem[]>([
{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(400, 400),
},
{
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
},
{
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(400, 800),
},
{
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(1920, 1080),
},
{
metadataName: '',
displayName: '图片名称5',
url: getPicSumImages(800, 1920),
},
])
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
const { list: links, refresh } = useListData<any>({
params: {
page: 1,
size: 0,
},
loading: toRef(loading),
onPullDownRefresh: async (params, updateData) => {
await run({ ...params })
updateData({
error: error.value,
hasNext: data.value?.hasNext,
items: data.value?.items ?? [],
})
},
onReachBottom: async (params, updateData) => {
await run({ ...params })
updateData({
error: error.value,
hasNext: data.value?.hasNext,
items: data.value?.items ?? [],
})
},
})
function onGroupBannerChange(e: any) {
groupBanner.current = e.detail.current
}
function getGroupImageRotateClass(index: number) {
if (index === 0) {
return 'z-30 border-2 border-solid border-white uh-shadow-card group-hover:scale-95'
}
if (index === 1) {
return 'z-20 -rotate-6 group-hover:-rotate-22 border-2 border-solid border-white uh-shadow-card'
}
if (index === 2) {
return 'z-10 rotate-6 group-hover:rotate-22 border-2 border-solid border-white uh-shadow-card'
}
}
// -- 图片列表模式
// 图片列表容器样式
function getGalleryListContainerClass(total: number) {
if (total === 1) {
return 'h-36'
}
if (total === 2) {
return 'grid-cols-2 h-32'
}
if (total === 3) {
return 'grid-cols-2 grid-rows-2 h-36'
}
if (total === 4) {
return 'grid-cols-2 grid-rows-2 h-42'
}
return ''
}
// 图片项样式
function getGalleryListItemClass(index: number, total: number) {
if (total === 1) {
return ''
}
if (total === 2) {
return ''
}
if (total === 3) {
if (index === 0) {
return 'row-span-2'
}
return ''
}
if (total === 4) {
return ''
}
return ''
}
function handleShowListPanel(metadataName: string) {
showListPanel.metadataName = metadataName
showListPanel.show = true
}
onLoad(() => {
refresh()
})
onMounted(() => {
})
onPageScroll(() => { })
</script>
<template>
<view class="min-h-screen w-full flex flex-col bg-page">
<!-- 分组列表常态显示 -->
<view class="box-border w-full px-4 py-4">
<!-- 顶部区域固定区域 -->
<view class="relative w-full">
<swiper
autoplay circular class="h-46 w-full overflow-hidden rounded-xl" :current="groupBanner.current"
@change="onGroupBannerChange"
>
<swiper-item v-for="img in galleryGroups[0].images" :key="img.metadataName">
<image :src="img.url" class="h-full w-full object-cover" />
</swiper-item>
</swiper>
<view
class="absolute left-4 top-4 flex flex-col items-center justify-center rounded-2xl bg-orange-500 px-2 py-1.5 text-xs text-white font-bold"
>
最新精选
</view>
<view class="absolute bottom-4 left-4 flex flex-col gap-y-1">
<view class="text-sm text-white font-bold">
显示图片的名称
</view>
<view class="text-xs text-white/50">
显示描述/日期
</view>
<view class="w-full flex items-center justify-center gap-x-2">
<image
v-for="(item, index) in galleryGroups[0].images" :key="item.metadataName" :src="item.url"
mode="scaleToFill"
class="box-border h-8 w-8 border-2 border-white rounded-lg border-solid transition-all duration-300"
:class="[
{
'scale-110': groupBanner.current === index,
},
index % 2 === 0 ? '-rotate-4' : 'rotate-4',
]" @click="groupBanner.current = index"
/>
</view>
</view>
</view>
<!-- 分组列表区域 -->
<view class="mt-6 w-full flex items-center justify-between">
<text class="text-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text>
</view>
<!-- 混合模式 -->
<view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="group relative box-border h-36 w-full flex items-center justify-center p-1"
>
<!-- 预览图区域 -->
<view class="relative h-full w-full">
<view
v-for="(img, index) in group.images.slice(0, 3)" :key="img.metadataName"
class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
:class="[getGroupImageRotateClass(index)]"
>
<image :src="img.url" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 -->
<view
v-if="index === 0"
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
>
<text class="text-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-[8px] text-white/80">{{ group.images.length }} 张图片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 列表卡片模式 -->
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3" @click="handleShowListPanel(group.metadataName)"
>
<!-- 顶部区域 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-x-2 text-sm font-bold">
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view>
<view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.images.length }} </text>
</view>
</view>
<!-- 图片列表 -->
<view
class="grid gap-1 overflow-hidden rounded-lg"
:class="[getGalleryListContainerClass(group.images.slice(0, 4).length)]"
>
<view
v-for="(img, index) in group.images.slice(0, 4)" :key="img.metadataName"
class="overflow-hidden"
:class="getGalleryListItemClass(index, group.images.slice(0, 4).length)"
>
<image
:src="img.url" mode="scaleToFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</view>
</view>
</view>
</view>
</view>
<!-- 分组列表无数据显示 -->
<view class="hidden">
无数据
</view>
<!-- 相册列表根据分组显示 -->
<uh-gallery-panel v-if="showListPanel.show" :group-name="showListPanel.metadataName" @close="showListPanel.show = false" />
<!-- 底部导航占位 -->
<view class="w-full shrink-0">
<uh-tabbar-page-placeholder />
</view>
</view>
</template>
+176 -218
View File
@@ -1,6 +1,8 @@
<script lang="ts" setup>
import { getPicSumImages } from '@/utils/randomResources'
import { queryLinks } from '@/api/halo-plugin/link'
import useRequest from '@/hooks/useRequest'
import { queryPhotoGroups, queryPhotos } from '@/api/halo-plugin/photo'
import type { IPhoto, IPhotoGroup } from '@/api/halo-plugin/photo'
import { completeUrl } from '@/utils/url'
defineOptions({
name: 'Gallery',
@@ -14,16 +16,13 @@ definePage({
},
})
interface GalleryItem {
interface IGalleryGroup {
metadataName: string
displayName: string
url: string
}
interface GalleryGroup {
metadataName: string
displayName: string
images: GalleryItem[]
creationTimestamp: string
photoCount: number
priority: number
photos: IPhoto[]
}
const groupBanner = reactive({
@@ -33,126 +32,13 @@ const galleryStyle = ref('list')
const showListPanel = reactive({
show: false,
metadataName: '',
displayName: '',
})
// 相册分组预览
const galleryGroups = ref<GalleryGroup[]>([
{
metadataName: 'photo-group-HGaGs',
displayName: '我们的故事',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '履行记录',
displayName: '履行记录',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '家庭时光',
displayName: '家庭时光',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '其他',
displayName: '其他',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}],
},
])
const galleryGroups = ref<IGalleryGroup[]>([])
// 相册图片
const galleryItems = ref<GalleryItem[]>([
{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(400, 400),
},
{
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
},
{
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(400, 800),
},
{
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(1920, 1080),
},
{
metadataName: '',
displayName: '图片名称5',
url: getPicSumImages(800, 1920),
},
])
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
const { list: links, refresh } = useListData<any>({
params: {
page: 1,
size: 0,
},
loading: toRef(loading),
onPullDownRefresh: async (params, updateData) => {
await run({ ...params })
updateData({
error: error.value,
hasNext: data.value?.hasNext,
items: data.value?.items ?? [],
})
},
onReachBottom: async (params, updateData) => {
await run({ ...params })
updateData({
error: error.value,
hasNext: data.value?.hasNext,
items: data.value?.items ?? [],
})
},
})
const { loading, error, data, run } = useRequest<Array<IPhotoGroup>>(queryPhotoGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
function onGroupBannerChange(e: any) {
groupBanner.current = e.detail.current
@@ -208,16 +94,84 @@ function getGalleryListItemClass(index: number, total: number) {
return ''
}
function handleShowListPanel(metadataName: string) {
showListPanel.metadataName = metadataName
function handleShowListPanel(group: IGalleryGroup) {
showListPanel.metadataName = group.metadataName
showListPanel.displayName = group.displayName
showListPanel.show = true
}
onLoad(() => {
refresh()
// 查询分组的数据
function getGalleryPhotosByGroups() {
if (galleryGroups.value.length === 0) {
return
}
// 执行对每个分组查询图片
galleryGroups.value.forEach(async (group) => {
const queryParams = {
group: undefined,
fieldSelector: undefined,
page: 1,
size: 4,
}
if (group.metadataName.trim()) {
queryParams.group = group.metadataName
}
if (!group.metadataName.trim()) {
queryParams.fieldSelector = 'spec.groupName!=""'
}
const res = await queryPhotos(queryParams)
if (res?.items) {
group.photos = res.items
}
if (!group.metadataName.trim()) {
group.photoCount = res.total
}
})
}
watch(() => loading.value, () => {
if (loading.value || error.value) {
return
}
galleryGroups.value = [
{
metadataName: '',
displayName: '未分组',
creationTimestamp: '',
photoCount: 0,
priority: 0,
photos: [],
},
]
data.value.forEach((item) => {
galleryGroups.value.push({
metadataName: item.metadata.name,
displayName: item.spec.displayName,
creationTimestamp: item.metadata.creationTimestamp,
photoCount: item.status.photoCount,
priority: item.spec.priority,
photos: [],
})
})
getGalleryPhotosByGroups()
})
onMounted(() => {
async function init() {
await run()
uni.stopPullDownRefresh()
}
onPullDownRefresh(async () => {
await init()
})
onLoad(() => {
})
onMounted(async () => {
await init()
})
onPageScroll(() => { })
@@ -226,119 +180,123 @@ onPageScroll(() => { })
<template>
<view class="min-h-screen w-full flex flex-col bg-page">
<!-- 分组列表常态显示 -->
<view class="box-border w-full px-4 py-4">
<view v-if="galleryGroups.length !== 0" class="box-border w-full">
<!-- 顶部区域固定区域 -->
<view class="relative w-full">
<view v-if="galleryGroups.length > 1 && galleryGroups[0].photos.length >= 4" class="relative w-full">
<swiper
autoplay circular class="h-46 w-full overflow-hidden rounded-xl" :current="groupBanner.current"
autoplay circular class="h-56 w-full overflow-hidden" :current="groupBanner.current"
@change="onGroupBannerChange"
>
<swiper-item v-for="img in galleryGroups[0].images" :key="img.metadataName">
<image :src="img.url" class="h-full w-full object-cover" />
<swiper-item v-for="photo in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name">
<image :src="completeUrl(photo.spec.url)" class="h-full w-full object-cover" />
</swiper-item>
</swiper>
<!-- 渐变底部遮罩 -->
<view
class="absolute left-4 top-4 flex flex-col items-center justify-center rounded-2xl bg-orange-500 px-2 py-1.5 text-xs text-white font-bold"
>
最新精选
</view>
<view class="absolute bottom-4 left-4 flex flex-col gap-y-1">
<view class="text-sm text-white font-bold">
显示图片的名称
</view>
<view class="text-xs text-white/50">
显示描述/日期
</view>
<view class="w-full flex items-center justify-center gap-x-2">
<image
v-for="(item, index) in galleryGroups[0].images" :key="item.metadataName" :src="item.url"
mode="scaleToFill"
class="box-border h-8 w-8 border-2 border-white rounded-lg border-solid transition-all duration-300"
:class="[
{
'scale-110': groupBanner.current === index,
},
index % 2 === 0 ? '-rotate-4' : 'rotate-4',
]" @click="groupBanner.current = index"
/>
</view>
class="absolute bottom-0 left-0 right-0 z-10 h-2/5 from-transparent via-page/30 to-page bg-gradient-to-b"
/>
</view>
<view v-if="galleryGroups.length > 1 && galleryGroups[0].photos.length >= 4" class="relative z-10 box-border w-full flex items-center justify-center gap-x-2 overflow-hidden p-4 -mt-12">
<view class="uh-shadow-card box-border w-full flex items-center justify-center gap-x-4 rounded-xl bg-white p-4">
<image
v-for="(photo, index) in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name"
:src="completeUrl(photo.spec.url)" mode="aspectFill"
class="box-border h-12 w-12 border-2 border-white rounded-lg border-solid transition-all duration-300"
:class="[
{
'shadow-md rotate-4': groupBanner.current === index,
},
]" @click="groupBanner.current = index"
/>
</view>
</view>
<!-- 分组列表区域 -->
<view class="mt-6 w-full flex items-center justify-between">
<text class="text-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text>
</view>
<!-- 混合模式 -->
<view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="group relative box-border h-36 w-full flex items-center justify-center p-1"
>
<!-- 预览图区域 -->
<view class="relative h-full w-full">
<view
v-for="(img, index) in group.images.slice(0, 3)" :key="img.metadataName"
class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
:class="[getGroupImageRotateClass(index)]"
>
<image :src="img.url" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 -->
<view class="mt-4 box-border px-4">
<!-- 分组列表区域 -->
<view v-if="false" class="w-full flex items-center justify-between">
<text class="text-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text>
</view>
<!-- 混合模式 -->
<view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="group relative box-border h-36 w-full flex items-center justify-center p-1"
>
<!-- 预览图区域 -->
<view class="relative h-full w-full">
<view
v-if="index === 0"
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
v-for="(photo, index) in group.photos.slice(0, 3)" :key="photo.metadata.name"
class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
:class="[getGroupImageRotateClass(index)]"
>
<text class="text-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-[8px] text-white/80">{{ group.images.length }} 张图片</text>
<image :src="photo.spec.url" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 -->
<view
v-if="index === 0"
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
>
<text class="text-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-[8px] text-white/80">{{ group.photoCount }} 张图片</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 列表卡片模式 -->
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3" @click="handleShowListPanel(group.metadataName)"
>
<!-- 顶部区域 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-x-2 text-sm font-bold">
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view>
<view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.images.length }} </text>
</view>
</view>
<!-- 图片列表 -->
<view
class="grid gap-1 overflow-hidden rounded-lg"
:class="[getGalleryListContainerClass(group.images.slice(0, 4).length)]"
>
<!-- 列表卡片模式 -->
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4">
<template v-for="group in galleryGroups" :key="group.metadataName">
<view
v-for="(img, index) in group.images.slice(0, 4)" :key="img.metadataName"
class="overflow-hidden"
:class="getGalleryListItemClass(index, group.images.slice(0, 4).length)"
v-if="group.photoCount > 0"
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3"
@click="handleShowListPanel(group)"
>
<image
:src="img.url" mode="scaleToFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
<!-- 顶部区域 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-x-2 text-sm font-bold">
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view>
<view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.photoCount }} </text>
</view>
</view>
<!-- 图片列表 -->
<view
class="grid gap-1 overflow-hidden rounded-lg"
:class="[getGalleryListContainerClass(group.photos.slice(0, 4).length)]"
>
<view
v-for="(photo, index) in group.photos.slice(0, 4)" :key="photo.metadata.name"
class="overflow-hidden" :class="getGalleryListItemClass(index, group.photos.slice(0, 4).length)"
>
<image
:src="completeUrl(photo.spec.url)" mode="aspectFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</view>
</view>
</view>
</view>
</template>
</view>
</view>
</view>
<!-- 分组列表无数据显示 -->
<view class="hidden">
无数据
<view v-else class="w-full flex flex-1 items-center justify-center">
<wd-empty icon="no-content" tip="暂无数据">
<template #image>
<wd-icon name="empty" color="#1A1A1A" :size="52" />
</template>
</wd-empty>
</view>
<!-- 相册列表根据分组显示 -->
<uh-gallery-panel v-if="showListPanel.show" :group-name="showListPanel.metadataName" @close="showListPanel.show = false" />
<uh-gallery-panel
v-if="showListPanel.show"
:group-metadata-name="showListPanel.metadataName"
:group-display-name="showListPanel.displayName"
@close="showListPanel.show = false"
/>
<!-- 底部导航占位 -->
<view class="w-full shrink-0">