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

10 KiB
Raw Permalink Blame History

图集功能使用指南

概述

新增的 images JSONB 字段支持在单个内容条目中存储多张图片,适用于:

  • 📸 图片相册/图集
  • 🎠 轮播图展示
  • 🖼️ 产品多图展示
  • 📱 社交媒体多图帖子
  • 🏞️ 风景/旅游图集

images 字段结构

推荐的 JSONB 数据结构:

{
  "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. 插入图集内容

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. 查询图集数据

-- 查询所有图集内容
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. 更新图集

-- 添加新图片到图集
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)

<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)

<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 文件中添加:

// 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. 常用查询索引

-- 为常用的 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. 查询示例

-- 查询热门图集(按图片数量排序)
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. 扩展性强 - 可轻松添加新的图片属性和元数据

这个设计为构建丰富的图片社交功能提供了强大的数据基础!