Files
akmon/IMAGE_GALLERY_GUIDE.md
2026-01-20 08:04:15 +08:00

418 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 图集功能使用指南
## 概述
新增的 `images` JSONB 字段支持在单个内容条目中存储多张图片,适用于:
- 📸 图片相册/图集
- 🎠 轮播图展示
- 🖼️ 产品多图展示
- 📱 社交媒体多图帖子
- 🏞️ 风景/旅游图集
## images 字段结构
推荐的 JSONB 数据结构:
```json
{
"count": 5,
"cover_index": 0,
"layout": "grid",
"items": [
{
"url": "https://example.com/image1.jpg",
"thumbnail": "https://example.com/image1_thumb.jpg",
"width": 1920,
"height": 1080,
"size": 245760,
"format": "jpg",
"alt": "图片描述",
"caption": "图片标题",
"order": 0,
"metadata": {
"camera": "iPhone 14 Pro",
"location": "北京",
"taken_at": "2025-01-15T10:30:00Z"
}
},
{
"url": "https://example.com/image2.jpg",
"thumbnail": "https://example.com/image2_thumb.jpg",
"width": 1920,
"height": 1080,
"size": 198432,
"format": "jpg",
"alt": "第二张图片",
"caption": "美丽的风景",
"order": 1,
"metadata": {}
}
]
}
```
## 数据库操作示例
### 1. 插入图集内容
```sql
INSERT INTO ak_contents (
title,
content_type,
summary,
images
) VALUES (
'北京旅游图集',
'image',
'记录北京之旅的美好时光',
'{
"count": 3,
"cover_index": 0,
"layout": "masonry",
"items": [
{
"url": "https://example.com/beijing1.jpg",
"thumbnail": "https://example.com/beijing1_thumb.jpg",
"width": 1920,
"height": 1280,
"size": 345600,
"format": "jpg",
"alt": "天安门广场",
"caption": "庄严的天安门广场",
"order": 0
},
{
"url": "https://example.com/beijing2.jpg",
"thumbnail": "https://example.com/beijing2_thumb.jpg",
"width": 1920,
"height": 1080,
"size": 298400,
"format": "jpg",
"alt": "故宫博物院",
"caption": "金碧辉煌的紫禁城",
"order": 1
},
{
"url": "https://example.com/beijing3.jpg",
"thumbnail": "https://example.com/beijing3_thumb.jpg",
"width": 1920,
"height": 1440,
"size": 412800,
"format": "jpg",
"alt": "长城",
"caption": "雄伟的万里长城",
"order": 2
}
]
}'::jsonb
);
```
### 2. 查询图集数据
```sql
-- 查询所有图集内容
SELECT
id,
title,
images->>'count' as image_count,
images->>'layout' as layout_type,
images->'items'->0->>'url' as cover_image
FROM ak_contents
WHERE content_type = 'image'
AND images IS NOT NULL;
-- 查询包含特定图片数量的图集
SELECT * FROM ak_contents
WHERE content_type = 'image'
AND (images->>'count')::int >= 5;
-- 查询包含特定格式图片的图集
SELECT * FROM ak_contents
WHERE content_type = 'image'
AND images @> '{"items": [{"format": "jpg"}]}';
```
### 3. 更新图集
```sql
-- 添加新图片到图集
UPDATE ak_contents
SET images = jsonb_set(
images,
'{items}',
(images->'items') || '[{
"url": "https://example.com/new_image.jpg",
"thumbnail": "https://example.com/new_image_thumb.jpg",
"width": 1920,
"height": 1080,
"size": 256000,
"format": "jpg",
"alt": "新图片",
"caption": "新添加的图片",
"order": 3
}]'::jsonb
)
WHERE id = 'your-content-id';
-- 更新图片数量
UPDATE ak_contents
SET images = jsonb_set(images, '{count}', '4')
WHERE id = 'your-content-id';
```
## 前端组件设计建议
### 1. 图集展示组件 (ImageGallery.uvue)
```vue
<template>
<view class="image-gallery">
<!-- 图集头部信息 -->
<view class="gallery-header">
<text class="image-count">{{ galleryData.count }} {{ $t('images.unit') }}</text>
<text class="layout-type">{{ $t('images.layout.' + galleryData.layout) }}</text>
</view>
<!-- 图片网格 -->
<view class="image-grid" :class="['layout-' + galleryData.layout]">
<view
v-for="(item, index) in galleryData.items"
:key="index"
class="image-item"
@click="openImageViewer(index)">
<image
:src="item.thumbnail || item.url"
:alt="item.alt"
class="gallery-image"
mode="aspectFill" />
<view v-if="item.caption" class="image-caption">
{{ item.caption }}
</view>
</view>
</view>
<!-- 图片查看器 -->
<ImageViewer
v-if="showViewer"
:images="galleryData.items"
:currentIndex="currentImageIndex"
@close="closeImageViewer" />
</view>
</template>
<script setup>
const props = defineProps({
galleryData: Object,
contentId: String
})
const showViewer = ref(false)
const currentImageIndex = ref(0)
const openImageViewer = (index) => {
currentImageIndex.value = index
showViewer.value = true
// 记录图片查看行为
recordImageView(props.contentId, index)
}
const closeImageViewer = () => {
showViewer.value = false
}
const recordImageView = async (contentId, imageIndex) => {
// 记录图片浏览行为到数据库
await supa.from('ak_image_view_records').insert({
content_id: contentId,
user_id: getCurrentUserId(),
view_duration: 0,
zoom_level: 1.0,
device_type: getDeviceType()
})
}
</script>
```
### 2. 图集上传组件 (ImageUploader.uvue)
```vue
<template>
<view class="image-uploader">
<view class="upload-area" @click="selectImages">
<text class="upload-text">{{ $t('images.upload.selectImages') }}</text>
</view>
<view class="image-preview-grid">
<view
v-for="(image, index) in uploadedImages"
:key="index"
class="preview-item">
<image :src="image.thumbnail" class="preview-image" />
<input
v-model="image.caption"
:placeholder="$t('images.upload.captionPlaceholder')"
class="caption-input" />
<view class="remove-btn" @click="removeImage(index)">×</view>
</view>
</view>
<view class="upload-controls">
<picker :value="selectedLayout" @change="onLayoutChange" :range="layoutOptions">
<text>{{ $t('images.layout.title') }}: {{ $t('images.layout.' + selectedLayout) }}</text>
</picker>
<button @click="uploadGallery" :disabled="!canUpload">
{{ $t('images.upload.submit') }}
</button>
</view>
</view>
</template>
<script setup>
const uploadedImages = ref([])
const selectedLayout = ref('grid')
const layoutOptions = ['grid', 'masonry', 'carousel', 'list']
const selectImages = () => {
uni.chooseImage({
count: 9,
success: (res) => {
processSelectedImages(res.tempFilePaths)
}
})
}
const processSelectedImages = async (filePaths) => {
for (const filePath of filePaths) {
// 上传图片并获取 URL
const uploadResult = await uploadImage(filePath)
uploadedImages.value.push({
url: uploadResult.url,
thumbnail: uploadResult.thumbnail,
width: uploadResult.width,
height: uploadResult.height,
size: uploadResult.size,
format: uploadResult.format,
alt: '',
caption: '',
order: uploadedImages.value.length
})
}
}
const uploadGallery = async () => {
const galleryData = {
count: uploadedImages.value.length,
cover_index: 0,
layout: selectedLayout.value,
items: uploadedImages.value
}
// 保存到数据库
await supa.from('ak_contents').insert({
title: '新图集',
content_type: 'image',
images: galleryData
})
}
</script>
```
## 多语言支持
需要在 i18n 文件中添加:
```typescript
// zh-CN
"images": {
"unit": "张图片",
"layout": {
"title": "布局方式",
"grid": "网格布局",
"masonry": "瀑布流",
"carousel": "轮播图",
"list": "列表布局"
},
"upload": {
"selectImages": "选择图片",
"captionPlaceholder": "添加图片说明",
"submit": "上传图集"
},
"viewer": {
"prev": "上一张",
"next": "下一张",
"zoom": "缩放",
"download": "下载"
}
}
// en-US
"images": {
"unit": "images",
"layout": {
"title": "Layout",
"grid": "Grid",
"masonry": "Masonry",
"carousel": "Carousel",
"list": "List"
},
"upload": {
"selectImages": "Select Images",
"captionPlaceholder": "Add caption",
"submit": "Upload Gallery"
},
"viewer": {
"prev": "Previous",
"next": "Next",
"zoom": "Zoom",
"download": "Download"
}
}
```
## 查询优化建议
### 1. 常用查询索引
```sql
-- 为常用的 JSONB 查询创建表达式索引
CREATE INDEX idx_contents_image_count ON ak_contents ((images->>'count')::int)
WHERE content_type = 'image';
CREATE INDEX idx_contents_image_layout ON ak_contents ((images->>'layout'))
WHERE content_type = 'image';
```
### 2. 查询示例
```sql
-- 查询热门图集(按图片数量排序)
SELECT
id, title,
(images->>'count')::int as image_count,
view_count
FROM vw_image_content_detail
WHERE images IS NOT NULL
ORDER BY image_count DESC, view_count DESC
LIMIT 20;
-- 查询特定布局的图集
SELECT * FROM ak_contents
WHERE content_type = 'image'
AND images->>'layout' = 'masonry'
ORDER BY created_at DESC;
```
## 总结
通过添加 `images` JSONB 字段,系统现在可以:
1.**灵活存储多图片数据** - 支持任意数量的图片及其元数据
2.**支持多种展示布局** - 网格、瀑布流、轮播等
3.**保持查询性能** - 通过 GIN 索引优化 JSONB 查询
4.**向后兼容** - 原有单图片字段依然可用
5.**扩展性强** - 可轻松添加新的图片属性和元数据
这个设计为构建丰富的图片社交功能提供了强大的数据基础!