418 lines
10 KiB
Markdown
418 lines
10 KiB
Markdown
# 图集功能使用指南
|
||
|
||
## 概述
|
||
|
||
新增的 `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. ✅ **扩展性强** - 可轻松添加新的图片属性和元数据
|
||
|
||
这个设计为构建丰富的图片社交功能提供了强大的数据基础!
|