1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +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
+2 -1
View File
@@ -7,4 +7,5 @@ VITE_SHOW_SOURCEMAP=false
# development mode 后台请求地址 # development mode 后台请求地址
# VITE_UNI_HALO_BASEURL='https://www.yijunzhao.cn' # VITE_UNI_HALO_BASEURL='https://www.yijunzhao.cn'
VITE_UNI_HALO_BASEURL='https://www.xhhao.com' # VITE_UNI_HALO_BASEURL='https://www.xhhao.com'
VITE_UNI_HALO_BASEURL='https://ryanc.cc'
+27 -6
View File
@@ -18,6 +18,27 @@ export interface IQueryPhotosRequest {
size?: number size?: number
} }
export interface IPhotoGroup {
metadata: {
creationTimestamp: string
generateName: string
name: string
version: string
}
spec: {
displayName: string
priority: number
}
status: {
photoCount: number
}
}
export interface IPhotoTag {
name: string
photoCount: number
}
export interface IPhoto { export interface IPhoto {
metadata: { metadata: {
creationTimestamp: string creationTimestamp: string
@@ -36,12 +57,10 @@ export interface IPhoto {
/** /**
* 获取图库分组列表 * 获取图库分组列表
* @param params 获取图库分组列表请求参数
* @returns 获取图库分组列表响应参数 * @returns 获取图库分组列表响应参数
*/ */
export function queryPhotoGroups(params: IQueryPhotoGroupsRequest) { export function queryPhotoGroups() {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photogroups`, { return http.Get<Array<IPhotoGroup>>(`/apis/api.photo.halo.run/v1alpha1/photogroups`, {
params,
meta: { meta: {
isHalo: true, isHalo: true,
}, },
@@ -51,10 +70,11 @@ export function queryPhotoGroups(params: IQueryPhotoGroupsRequest) {
/** /**
* 获取图库标签列表 列出所有不重复的标签名称及对应图片数量,支持可选的 name 参数进行大小写不敏感模糊过滤 * 获取图库标签列表 列出所有不重复的标签名称及对应图片数量,支持可选的 name 参数进行大小写不敏感模糊过滤
* @param params 获取图库标签列表请求参数 * @param params 获取图库标签列表请求参数
* @param params.name 标签名称
* @returns 获取图库标签列表响应参数 * @returns 获取图库标签列表响应参数
*/ */
export function queryPhotoTags(params: any) { export function queryPhotoTags(params: { name?: string }) {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/tags`, { return http.Get<IHaloListResponseBase<IPhotoTag>>(`/apis/api.photo.halo.run/v1alpha1/tags`, {
params, params,
meta: { meta: {
isHalo: true, isHalo: true,
@@ -70,6 +90,7 @@ export function queryPhotoTags(params: any) {
export function queryPhotos(params: IQueryPhotosRequest) { export function queryPhotos(params: IQueryPhotosRequest) {
return http.Get<IHaloListResponseBase<IPhoto>>(`/apis/api.photo.halo.run/v1alpha1/photos`, { return http.Get<IHaloListResponseBase<IPhoto>>(`/apis/api.photo.halo.run/v1alpha1/photos`, {
params, params,
cacheFor: null,
meta: { meta: {
isHalo: true, isHalo: true,
}, },
@@ -3,10 +3,12 @@ import { queryPhotos } from '@/api/halo-plugin/photo'
import { completeUrl } from '@/utils/url' import { completeUrl } from '@/utils/url'
import { useHaloScroll } from '@/hooks/useHaloScroll' import { useHaloScroll } from '@/hooks/useHaloScroll'
import { random } from '@/utils/base/random' import { random } from '@/utils/base/random'
import { preview } from '@/utils/imageHelper'
import type { IPhoto, IQueryPhotosRequest } from '@/api/halo-plugin/photo' import type { IPhoto, IQueryPhotosRequest } from '@/api/halo-plugin/photo'
interface IProps { interface IProps {
groupName: string groupMetadataName: string
groupDisplayName: string
} }
const props = defineProps<IProps>() const props = defineProps<IProps>()
@@ -16,6 +18,7 @@ const emits = defineEmits<{
}>() }>()
const { const {
response, // 响应式的原始数据响应
list, // 响应式的数据列表 list, // 响应式的数据列表
loading, // 是否加载中 loading, // 是否加载中
finished, // 是否已全部加载 finished, // 是否已全部加载
@@ -27,6 +30,7 @@ const {
params: { params: {
page: 1, page: 1,
size: 12, size: 12,
group: props.groupMetadataName.trim() ?? undefined,
}, },
}) })
@@ -36,61 +40,62 @@ function getImageItemClass() {
const randomIndex = random(0, imageItemClass.length - 1) const randomIndex = random(0, imageItemClass.length - 1)
return imageItemClass[randomIndex] return imageItemClass[randomIndex]
} }
function handlePreview(item: IPhoto) {
preview(completeUrl(item.spec.url), list.value.map(item => completeUrl(item.spec.url)))
}
</script> </script>
<template> <template>
<!-- 相册列表根据分组显示 --> <!-- 相册列表根据分组显示 -->
<view class="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-center justify-center bg-black/20"> <view
class="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-center justify-center bg-black/20"
@click.stop="emits('close')"
>
<wd-transition :show="true" :lazy-render="true" :duration="300" name="slide-left"> <wd-transition :show="true" :lazy-render="true" :duration="300" name="slide-left">
<view class="box-border w-[88vw] rounded-2xl bg-page2 p-4"> <view class="box-border w-[88vw] rounded-2xl bg-white p-4" @click.stop>
<view class="box-border w-full flex shrink-0 items-center justify-between gap-x-4"> <view class="box-border w-full flex shrink-0 items-center justify-between gap-x-4">
<view class="text-gray-900 font-bold"> <view class="text-gray-900 font-bold">
{{ props.groupName }} {{ props.groupDisplayName }}
</view> </view>
<view class="shrink-0"> <view class="shrink-0">
<wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="emits('close')" /> <wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="emits('close')" />
</view> </view>
</view> </view>
<view class="mt-4 w-full"> <view class="mt-2 w-full">
<scroll-view <scroll-view
v-if="list" class="h-[56vh] w-full" scroll-y :refresher-enabled="true" v-if="list" class="h-[60vh] w-full" scroll-y :refresher-enabled="true"
:refresher-triggered="loading" @scrolltolower="loadMore" @refresherrefresh="refresh" :refresher-triggered="loading" @scrolltolower="loadMore" @refresherrefresh="refresh"
> >
<view class="w-full flex flex-wrap gap-y-4"> <view class="grid grid-cols-2 box-border w-full gap-x-3 gap-y-2 overflow-hidden px-1">
<view <view v-for="(item) in list" :key="item.metadata.name" class="box-border w-full">
v-for="(item) in list" :key="item.metadata.name"
class="box-border w-1/2 overflow-hidden px-2"
:class="[getImageItemClass()]"
>
<image <image
class="uh-shadow-xs w-full border-2 border-white rounded-xl border-solid bg-white/80" class="uh-shadow-xs box-border h-32 w-full border-2 border-white rounded-xl border-solid bg-white/80"
:src="completeUrl(item.spec.url)" :src="completeUrl(item.spec.url)" mode="aspectFill" @click="handlePreview(item)"
mode="widthFix"
/> />
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<view v-else class="h-[56vh] flex items-center justify-center"> <view v-else class="h-[60vh] flex items-center justify-center">
<wd-empty tip="暂无数据"> <wd-empty tip="暂无数据">
<template #image> <template #image>
<wd-icon name="empty" color="#1A1A1A" :size="52" /> <wd-icon name="empty" color="#1A1A1A" :size="52" />
</template> </template>
</wd-empty> </wd-empty>
</view> </view>
<view <view class="mt-4 w-full flex items-center justify-center gap-x-2 text-xs text-gray-900">
v-if="loading || error || finished" <view class="text-xs text-gray-900">
class="mt-4 w-full flex items-center justify-center text-xs text-gray-900" {{ response?.total || 0 }} 已加载 {{ list.length }}
> </view>
<view v-if="loading || error || finished">
<wd-loading v-if="loading" text="加载中..." direction="horizontal" color="#1A1A1A" :size="16" /> <wd-loading v-if="loading" text="加载中..." direction="horizontal" color="#1A1A1A" :size="16" />
<view <view v-else-if="error" class="w-full rounded-lg bg-gray-900 py-1.5 text-center text-xs text-white">
v-else-if="error"
class="w-full rounded-lg bg-gray-900 py-1.5 text-center text-xs text-white"
>
<wd-icon name="refresh" :size="14" /> 加载失败点击刷新试试 <wd-icon name="refresh" :size="14" /> 加载失败点击刷新试试
</view> </view>
<view v-else-if="finished" class="text-black/60"> <view v-else-if="finished" class="text-xs text-gray-900">
数据已全部加载没有更多了 数据已全部加载
</view>
</view> </view>
</view> </view>
</view> </view>
+11 -1
View File
@@ -8,6 +8,7 @@ interface UseScrollOptions<T, P extends IHaloListRequestBase = { page: number, s
} }
interface UseScrollReturn<T> { interface UseScrollReturn<T> {
response: Ref<IHaloListResponseBase<T>>
list: Ref<T[]> list: Ref<T[]>
loading: Ref<boolean> loading: Ref<boolean>
finished: Ref<boolean> finished: Ref<boolean>
@@ -25,6 +26,7 @@ export function useHaloScroll<T, P extends IHaloListRequestBase = { page: number
const loading = ref(false) const loading = ref(false)
const finished = ref(false) const finished = ref(false)
const error = ref<any>(null) const error = ref<any>(null)
const isLoadMore = ref(false)
const loadData = async () => { const loadData = async () => {
if (loading.value || finished.value) { if (loading.value || finished.value) {
@@ -38,17 +40,23 @@ export function useHaloScroll<T, P extends IHaloListRequestBase = { page: number
response.value = await fetchData(params) response.value = await fetchData(params)
finished.value = !response.value?.hasNext finished.value = !response.value?.hasNext
list.value.push(...response.value?.items || []) list.value.push(...response.value?.items || [])
params.page++ if (!finished.value) {
params.page += 1
}
} }
catch (err) { catch (err) {
error.value = err error.value = err
} }
finally { finally {
loading.value = false loading.value = false
isLoadMore.value = false
} }
} }
const refresh = async () => { const refresh = async () => {
if (isLoadMore.value) {
return
}
params.page = 1 params.page = 1
finished.value = false finished.value = false
list.value = [] list.value = []
@@ -56,6 +64,7 @@ export function useHaloScroll<T, P extends IHaloListRequestBase = { page: number
} }
const loadMore = async () => { const loadMore = async () => {
isLoadMore.value = true
await loadData() await loadData()
} }
@@ -64,6 +73,7 @@ export function useHaloScroll<T, P extends IHaloListRequestBase = { page: number
}) })
return { return {
response,
list, list,
loading, loading,
finished, finished,
+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>
+127 -169
View File
@@ -1,6 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getPicSumImages } from '@/utils/randomResources' import useRequest from '@/hooks/useRequest'
import { queryLinks } from '@/api/halo-plugin/link' import { queryPhotoGroups, queryPhotos } from '@/api/halo-plugin/photo'
import type { IPhoto, IPhotoGroup } from '@/api/halo-plugin/photo'
import { completeUrl } from '@/utils/url'
defineOptions({ defineOptions({
name: 'Gallery', name: 'Gallery',
@@ -14,16 +16,13 @@ definePage({
}, },
}) })
interface GalleryItem { interface IGalleryGroup {
metadataName: string metadataName: string
displayName: string displayName: string
url: string creationTimestamp: string
} photoCount: number
priority: number
interface GalleryGroup { photos: IPhoto[]
metadataName: string
displayName: string
images: GalleryItem[]
} }
const groupBanner = reactive({ const groupBanner = reactive({
@@ -33,126 +32,13 @@ const galleryStyle = ref('list')
const showListPanel = reactive({ const showListPanel = reactive({
show: false, show: false,
metadataName: '', metadataName: '',
displayName: '',
}) })
// 相册分组预览 // 相册分组预览
const galleryGroups = ref<GalleryGroup[]>([ const galleryGroups = ref<IGalleryGroup[]>([])
{
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 { loading, error, data, run } = useRequest<Array<IPhotoGroup>>(queryPhotoGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
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) { function onGroupBannerChange(e: any) {
groupBanner.current = e.detail.current groupBanner.current = e.detail.current
@@ -208,16 +94,84 @@ function getGalleryListItemClass(index: number, total: number) {
return '' return ''
} }
function handleShowListPanel(metadataName: string) { function handleShowListPanel(group: IGalleryGroup) {
showListPanel.metadataName = metadataName showListPanel.metadataName = group.metadataName
showListPanel.displayName = group.displayName
showListPanel.show = true 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(() => { }) onPageScroll(() => { })
@@ -226,47 +180,39 @@ onPageScroll(() => { })
<template> <template>
<view class="min-h-screen w-full flex flex-col bg-page"> <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 <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" @change="onGroupBannerChange"
> >
<swiper-item v-for="img in galleryGroups[0].images" :key="img.metadataName"> <swiper-item v-for="photo in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name">
<image :src="img.url" class="h-full w-full object-cover" /> <image :src="completeUrl(photo.spec.url)" class="h-full w-full object-cover" />
</swiper-item> </swiper-item>
</swiper> </swiper>
<!-- 渐变底部遮罩 -->
<view <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" 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>
<view class="absolute bottom-4 left-4 flex flex-col gap-y-1"> <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="text-sm text-white font-bold"> <view class="uh-shadow-card box-border w-full flex items-center justify-center gap-x-4 rounded-xl bg-white p-4">
显示图片的名称
</view>
<view class="text-xs text-white/50">
显示描述/日期
</view>
<view class="w-full flex items-center justify-center gap-x-2">
<image <image
v-for="(item, index) in galleryGroups[0].images" :key="item.metadataName" :src="item.url" v-for="(photo, index) in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name"
mode="scaleToFill" :src="completeUrl(photo.spec.url)" mode="aspectFill"
class="box-border h-8 w-8 border-2 border-white rounded-lg border-solid transition-all duration-300" class="box-border h-12 w-12 border-2 border-white rounded-lg border-solid transition-all duration-300"
:class="[ :class="[
{ {
'scale-110': groupBanner.current === index, 'shadow-md rotate-4': groupBanner.current === index,
}, },
index % 2 === 0 ? '-rotate-4' : 'rotate-4',
]" @click="groupBanner.current = index" ]" @click="groupBanner.current = index"
/> />
</view> </view>
</view> </view>
</view> <view class="mt-4 box-border px-4">
<!-- 分组列表区域 --> <!-- 分组列表区域 -->
<view class="mt-6 w-full flex items-center justify-between"> <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-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text> <text class="text-[10px] text-gray-600"> 30 组记录</text>
</view> </view>
@@ -279,18 +225,18 @@ onPageScroll(() => { })
<!-- 预览图区域 --> <!-- 预览图区域 -->
<view class="relative h-full w-full"> <view class="relative h-full w-full">
<view <view
v-for="(img, index) in group.images.slice(0, 3)" :key="img.metadataName" 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="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
:class="[getGroupImageRotateClass(index)]" :class="[getGroupImageRotateClass(index)]"
> >
<image :src="img.url" mode="scaleToFill" class="h-full w-full" /> <image :src="photo.spec.url" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 --> <!-- 文字区域 -->
<view <view
v-if="index === 0" 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" 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-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-[8px] text-white/80">{{ group.images.length }} 张图片</text> <text class="text-[8px] text-white/80">{{ group.photoCount }} 张图片</text>
</view> </view>
</view> </view>
</view> </view>
@@ -299,9 +245,11 @@ onPageScroll(() => { })
<!-- 列表卡片模式 --> <!-- 列表卡片模式 -->
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4"> <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 <view
v-for="group in galleryGroups" :key="group.metadataName" 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.metadataName)" class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3"
@click="handleShowListPanel(group)"
> >
<!-- 顶部区域 --> <!-- 顶部区域 -->
<view class="flex items-center justify-between"> <view class="flex items-center justify-between">
@@ -309,36 +257,46 @@ onPageScroll(() => { })
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }} <view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view> </view>
<view class="shrink-0"> <view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.images.length }} </text> <text class="text-[10px] text-gray-400">{{ group.photoCount }} </text>
</view> </view>
</view> </view>
<!-- 图片列表 --> <!-- 图片列表 -->
<view <view
class="grid gap-1 overflow-hidden rounded-lg" class="grid gap-1 overflow-hidden rounded-lg"
:class="[getGalleryListContainerClass(group.images.slice(0, 4).length)]" :class="[getGalleryListContainerClass(group.photos.slice(0, 4).length)]"
> >
<view <view
v-for="(img, index) in group.images.slice(0, 4)" :key="img.metadataName" v-for="(photo, index) in group.photos.slice(0, 4)" :key="photo.metadata.name"
class="overflow-hidden" class="overflow-hidden" :class="getGalleryListItemClass(index, group.photos.slice(0, 4).length)"
:class="getGalleryListItemClass(index, group.images.slice(0, 4).length)"
> >
<image <image
:src="img.url" mode="scaleToFill" :src="completeUrl(photo.spec.url)" mode="aspectFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105" class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/> />
</view> </view>
</view> </view>
</view> </view>
</template>
</view>
</view> </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> </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"> <view class="w-full shrink-0">
+18
View File
@@ -12,3 +12,21 @@ export function cover(cover: string) {
} }
return getRandomUrl() return getRandomUrl()
} }
/**
* 图片预览
* @param url 图片URL
* @param urls 图片URL列表
*/
export function preview(url: string, urls: string[]) {
if (urls.length === 0) {
return
}
uni.previewImage({
current: url,
urls,
showmenu: true,
indicator: 'number',
loop: true,
})
}