欢迎光临
我们一直在努力

Git-RSCLIP智能相册开发:Vue前端+Node.js后端全栈实现

Git-RSCLIP智能相册开发:Vue前端+Node.js后端全栈实现

你是不是也有过这样的经历?手机里存了几千张照片,想找一张“去年夏天在海边拍的、有红色遮阳伞和狗狗”的照片,结果翻了半小时也没找到。传统的相册应用只能按时间、地点或手动添加的标签来搜索,一旦标签没打好,照片就像石沉大海。

现在,情况不一样了。想象一下,你只需要在搜索框里输入“红色汽车的照片”,或者“有彩虹的风景照”,系统就能瞬间从成千上万张照片中精准地找到它们。这听起来像是科幻电影里的场景,但今天,我们就要用Git-RSCLIP模型,结合Vue3和Node.js,亲手把它变成现实。

这篇文章,我就带你一步步搭建一个基于自然语言搜索的智能相册系统。我们不用去理解复杂的深度学习算法,而是聚焦于如何将前沿的AI能力,通过一套清晰、可落地的全栈技术方案,变成一个真正能用的产品。无论你是前端开发者想了解如何接入AI能力,还是后端工程师想学习向量数据库的应用,都能在这里找到答案。

1. 为什么我们需要智能相册?

在开始敲代码之前,我们先聊聊为什么传统的相册管理方式已经不够用了。

我自己的手机里大概有8000多张照片。以前,我会很认真地给重要的照片打上标签,比如“家庭聚会”、“旅行-日本”、“工作截图”。但时间一长,这个习惯就坚持不下去了。一是太耗时,每张照片都要手动处理;二是标签体系会混乱,今天用“宠物”,明天用“狗狗”,搜索时就得试好几次。

更麻烦的是,很多搜索需求是模糊的、场景化的。比如,我想找“上次聚餐时那道看起来很好吃的菜的照片”,或者“我穿蓝色衬衫的那张证件照”。这些描述里包含的颜色、物体、场景、甚至是情感,都是传统标签系统难以覆盖的。

Git-RSCLIP这类视觉语言模型的出现,正好解决了这个痛点。它经过海量图文数据的训练,能够理解图片的语义内容。简单来说,它“看”一张图片,不仅能识别出里面有“汽车”、“树”、“人”,还能理解这是一个“在郊外公路上的红色汽车”。当你想搜索时,它也能理解“红色汽车”这个文本描述,并在它“记忆”的图片特征库里,找到语义最匹配的那一张。

我们即将构建的系统,核心就是利用Git-RSCLIP的这个能力。前端提供一个干净漂亮的界面让你输入描述、查看结果;后端负责把图片变成AI能理解的“特征向量”存起来,并在你搜索时,快速找到最相关的那些。整个技术栈我们选择Vue3 + Node.js + Express + Milvus,都是目前非常流行且易于上手的技术。

2. 系统架构与核心组件

在动手开发前,我们先从高处俯瞰一下整个系统是怎么工作的。这样你在写每一部分代码时,都能清楚地知道它在整个链条中扮演什么角色。

整个系统可以分为三大块:前端交互层、后端服务层和AI与数据层。它们之间的协作关系,我用下面这张图来帮你理解:

用户操作:上传图片 / 输入文字搜索

[ Vue3前端界面 ]
| (HTTP API)

[ Node.js + Express 后端服务器 ]
| |
| | (调用模型,查询向量库)
↓ ↓
(存储图片文件) [ Git-RSCLIP模型 ] ←→ [ Milvus向量数据库 ]
| |
↓ ↓
本地/云存储 特征向量存储与检索

前端(Vue3):这是用户看到和操作的部分。主要就两个页面:一个用来批量上传和管理你的图片库,另一个就是核心的搜索页面,有一个大大的搜索框和展示结果的图片墙。它的任务很简单,就是把用户的操作(上传、输入文字)打包成网络请求发给后端,然后把后端返回的结果漂亮地展示出来。

后端(Node.js + Express):这是系统的“大脑”和“调度中心”。它对外提供API接口,对内要处理两件核心任务:

  • 图片入库处理:当用户上传一批新照片时,后端需要调用Git-RSCLIP模型,为每一张图片生成一个对应的“特征向量”(你可以理解为一串能代表图片内容的数字指纹),然后把这个向量和图片的路径信息,一起存进Milvus数据库。
  • 文本搜索:当用户输入一段描述文字(如“日落时的海滩”)时,后端同样用Git-RSCLIP模型,把这段文字也变成一个“特征向量”。接着,它去Milvus数据库里,寻找和这个“文字向量”最相似的“图片向量”,找到后,就把对应的图片信息返回给前端。
  • AI与数据层:

    • Git-RSCLIP模型:这是我们系统的“智能核心”。它是一个预训练好的模型,我们不需要自己训练,直接调用就行。它的作用就是当“翻译官”,把图片和文字都“翻译”成同一种语言——高维特征向量,这样计算机才能计算它们之间的相似度。
    • Milvus向量数据库:传统的数据库(如MySQL)擅长存文本、数字,但不擅长快速查找一堆向量中谁和谁最像。Milvus就是专门干这个的。它把我们所有图片的特征向量存起来,并提供超快的相似度搜索能力,即使图片库有几十万张,也能在毫秒级返回结果。

    理清了架构,我们就可以开始准备“施工”了。

    3. 环境搭建与项目初始化

    磨刀不误砍柴工,我们先花一点时间把开发环境准备好。这里假设你已经安装了Node.js(建议16+版本)和npm/yarn。

    3.1 后端项目初始化

    打开终端,我们先创建并进入后端项目目录:

    mkdir smart-album-backend
    cd smart-album-backend
    npm init -y

    接下来,安装我们需要的核心依赖。Express是Web框架,Multer用来处理文件上传,CORS解决跨域问题(因为前端和后端在不同端口运行),Dotenv管理环境变量。

    npm install express multer cors dotenv
    npm install –save-dev nodemon

    然后,安装与Git-RSCLIP模型和Milvus交互相关的库。这里我们需要用到@xenova/transformers,这是一个在浏览器和Node.js中运行Transformer模型的库,非常轻量。同时安装Milvus的Node.js客户端。

    npm install @xenova/transformers milvus2-sdk-node

    现在,创建后端项目的入口文件 server.js 和一个简单的目录结构:

    smart-album-backend/
    ├── server.js # 主入口文件
    ├── uploads/ # 存放上传的图片(需手动创建)
    ├── routes/ # API路由(后续创建)
    ├── controllers/ # 业务逻辑控制器(后续创建)
    ├── services/ # 核心服务,如模型调用、数据库操作(后续创建)
    └── .env # 环境变量配置文件(后续创建)

    3.2 前端项目初始化

    我们使用Vite来快速搭建一个Vue3项目,它比传统的Vue CLI更轻更快。

    打开一个新的终端窗口,执行:

    npm create vue@latest smart-album-frontend

    在创建过程中,你可以根据提示选择需要的特性。为了简化,我们这里只选择:

    • Vue Router: No (本示例单页应用可不用)
    • Pinia: No (本示例状态管理简单,可不用)
    • ESLint: Yes (保持代码规范)

    创建完成后,进入项目并安装两个我们需要的UI组件库:Element Plus(提供丰富的UI组件)和Axios(用于发送HTTP请求)。

    cd smart-album-frontend
    npm install
    npm install element-plus axios

    安装完成后,你可以先运行 npm run dev 看看项目是否正常启动。一个基础的Vue3项目就准备好了。

    3.3 启动Milvus向量数据库

    Milvus的安装方式很多,为了最快上手,我们使用Docker来运行一个单机版的Milvus。请确保你的电脑上已经安装了Docker和Docker Compose。

    在一个方便的位置(比如项目根目录的同级),创建一个 docker-compose.yml 文件,内容如下:

    version: '3.5'

    services:
    etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    environment:
    – ETCD_AUTO_COMPACTION_MODE=revision
    – ETCD_AUTO_COMPACTION_RETENTION=1000
    – ETCD_QUOTA_BACKEND_BYTES=4294967296
    – ETCD_SNAPSHOT_COUNT=50000
    volumes:
    – ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 –data-dir /etcd

    minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
    MINIO_ACCESS_KEY: minioadmin
    MINIO_SECRET_KEY: minioadmin
    volumes:
    – ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
    command: minio server /minio_data
    healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
    interval: 30s
    timeout: 20s
    retries: 3

    standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.3.3
    command: ["milvus", "run", "standalone"]
    environment:
    ETCD_ENDPOINTS: etcd:2379
    MINIO_ADDRESS: minio:9000
    volumes:
    – ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
    ports:
    – "19530:19530"
    – "9091:9091"
    depends_on:
    – "etcd"
    – "minio"

    networks:
    default:
    name: milvus

    保存文件后,在终端中进入该文件所在目录,运行以下命令启动所有服务:

    docker-compose up -d

    等待片刻,使用 docker ps 命令查看容器状态,当三个容器(etcd, minio, milvus-standalone)都显示为“Up”状态时,说明Milvus已经成功启动在 localhost:19530 端口。

    好了,我们的“施工场地”已经平整完毕,接下来开始砌第一块砖——后端服务。

    4. 后端开发:构建AI服务引擎

    后端是整个系统的动力舱,我们来一步步实现它。

    4.1 基础服务器与配置

    首先,在后端项目的根目录下创建 .env 文件,存放配置信息:

    PORT=3000
    MILVUS_HOST=localhost
    MILVUS_PORT=19530
    UPLOAD_DIR=./uploads
    MODEL_NAME=Xenova/clip-vit-base-patch32

    然后,编写 server.js 文件,搭建一个基础的Express服务器,并连接Milvus。

    // server.js
    require('dotenv').config();
    const express = require('express');
    const cors = require('cors');
    const path = require('path');

    const app = express();
    const PORT = process.env.PORT || 3000;

    // 中间件
    app.use(cors()); // 允许跨域
    app.use(express.json()); // 解析JSON请求体
    app.use(express.urlencoded({ extended: true }));

    // 静态文件服务,用于访问上传的图片
    app.use('/uploads', express.static(path.join(__dirname, process.env.UPLOAD_DIR)));

    // 简单的健康检查端点
    app.get('/health', (req, res) => {
    res.json({ status: 'ok', message: 'Smart Album API is running' });
    });

    // 在这里引入后续创建的路由
    // const uploadRoutes = require('./routes/upload');
    // const searchRoutes = require('./routes/search');
    // app.use('/api/upload', uploadRoutes);
    // app.use('/api/search', searchRoutes);

    // 启动服务器
    app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
    console.log(`Upload directory: ${path.join(__dirname, process.env.UPLOAD_DIR)}`);
    });

    4.2 核心服务:模型与数据库

    接下来,我们创建两个最核心的服务文件。

    1. 模型服务 (services/modelService.js) 这个文件负责加载Git-RSCLIP模型,并提供图片和文本的特征提取功能。

    // services/modelService.js
    const { pipeline } = require('@xenova/transformers');

    class ModelService {
    constructor() {
    this.model = null;
    this.processor = null;
    this.initialized = false;
    }

    async initialize() {
    if (this.initialized) return;

    console.log('Loading CLIP model…');
    try {
    // 使用Xenova提供的CLIP模型
    // 注意:这里我们使用基础版做演示。Git-RSCLIP可能需要根据其具体实现调整。
    // 核心是调用 `feature-extraction` pipeline 来获取特征。
    const extractor = await pipeline('feature-extraction', 'Xenova/clip-vit-base-patch32');
    this.model = extractor;
    this.initialized = true;
    console.log('CLIP model loaded successfully.');
    } catch (error) {
    console.error('Failed to load CLIP model:', error);
    throw error;
    }
    }

    // 从图片文件路径提取特征向量
    async extractImageFeatures(imagePath) {
    await this.initialize();
    // 注意:@xenova/transformers 当前版本对本地图片文件支持可能有限。
    // 在实际生产中,可能需要先将图片读入为Buffer或Base64,或使用其他库预处理。
    // 此处为简化流程,示意核心逻辑:将图片转换为模型可接受的张量。
    console.log(`Extracting features from image: ${imagePath}`);
    // 伪代码:实际需要将图片加载并预处理
    // const imageInput = await preprocessImage(imagePath);
    // const features = await this.model(imageInput);
    // return features.tolist()[0]; // 返回一维向量数组

    // 模拟返回一个768维的向量
    return Array.from({length: 768}, () => Math.random());
    }

    // 从文本提取特征向量
    async extractTextFeatures(text) {
    await this.initialize();
    console.log(`Extracting features from text: "${text}"`);
    // 伪代码:实际处理
    // const textInput = text;
    // const features = await this.model(textInput, { pooling: 'mean' });
    // return features.tolist()[0];

    // 模拟返回
    return Array.from({length: 768}, () => Math.random());
    }
    }

    // 导出单例
    module.exports = new ModelService();

    2. 数据库服务 (services/milvusService.js) 这个文件负责连接Milvus,并管理向量数据的插入和搜索。

    // services/milvusService.js
    const { MilvusClient, DataType } = require('@zilliz/milvus2-sdk-node');
    require('dotenv').config();

    class MilvusService {
    constructor() {
    this.client = null;
    this.collectionName = 'smart_album';
    this.dimension = 768; // 与模型提取的特征维度一致
    this.connected = false;
    }

    async connect() {
    if (this.connected) return;

    try {
    this.client = new MilvusClient({
    address: `${process.env.MILVUS_HOST}:${process.env.MILVUS_PORT}`,
    });
    await this.client.connect();
    this.connected = true;
    console.log('Connected to Milvus successfully.');
    } catch (error) {
    console.error('Failed to connect to Milvus:', error);
    throw error;
    }
    }

    async createCollectionIfNotExists() {
    await this.connect();

    const hasCollection = await this.client.hasCollection({
    collection_name: this.collectionName,
    });

    if (!hasCollection.value) {
    console.log(`Creating collection: ${this.collectionName}`);
    await this.client.createCollection({
    collection_name: this.collectionName,
    fields: [
    {
    name: 'id',
    data_type: DataType.Int64,
    is_primary_key: true,
    autoID: true,
    },
    {
    name: 'image_path',
    data_type: DataType.VarChar,
    max_length: 500,
    },
    {
    name: 'vector',
    data_type: DataType.FloatVector,
    dim: this.dimension,
    },
    ],
    });
    // 创建索引以加速搜索
    await this.client.createIndex({
    collection_name: this.collectionName,
    field_name: 'vector',
    index_name: 'vector_index',
    index_type: 'IVF_FLAT',
    metric_type: 'L2',
    params: { nlist: 1024 },
    });
    console.log(`Collection ${this.collectionName} created and indexed.`);
    } else {
    console.log(`Collection ${this.collectionName} already exists.`);
    }
    }

    async insertImageVector(imagePath, vector) {
    await this.createCollectionIfNotExists();

    const entities = [
    {
    image_path: imagePath,
    vector: vector,
    },
    ];

    const result = await this.client.insert({
    collection_name: this.collectionName,
    fields_data: entities,
    });

    console.log(`Inserted vector for image: ${imagePath}, ID: ${result.IDs}`);
    return result.IDs;
    }

    async searchSimilarImages(queryVector, limit = 10) {
    await this.connect();

    const searchParams = {
    anns_field: 'vector',
    topk: limit,
    params: JSON.stringify({ nprobe: 16 }),
    metric_type: 'L2',
    };

    const result = await this.client.search({
    collection_name: this.collectionName,
    vector: queryVector,
    output_fields: ['image_path'],
    search_params: searchParams,
    });

    // 格式化结果
    return result.results.map(item => ({
    image_path: item.entity.image_path,
    score: item.score, // 距离分数,越小越相似
    }));
    }
    }

    module.exports = new MilvusService();

    4.3 业务逻辑与API路由

    有了核心服务,我们来编写处理具体业务逻辑的控制器和路由。

    1. 上传控制器 (controllers/uploadController.js) 处理图片上传、特征提取和向量入库。

    // controllers/uploadController.js
    const path = require('path');
    const fs = require('fs').promises;
    const modelService = require('../services/modelService');
    const milvusService = require('../services/milvusService');

    async function handleUpload(req, res) {
    try {
    if (!req.file) {
    return res.status(400).json({ error: 'No image file uploaded' });
    }

    const imagePath = `/uploads/${req.file.filename}`;
    const absolutePath = path.join(__dirname, '..', 'uploads', req.file.filename);

    console.log(`Processing upload: ${imagePath}`);

    // 1. 提取图片特征向量
    const vector = await modelService.extractImageFeatures(absolutePath);

    // 2. 将向量存入Milvus
    await milvusService.insertImageVector(imagePath, vector);

    res.json({
    success: true,
    message: 'Image uploaded and processed successfully',
    data: {
    filename: req.file.filename,
    path: imagePath,
    vector_dim: vector.length,
    },
    });
    } catch (error) {
    console.error('Upload processing error:', error);
    res.status(500).json({ error: 'Failed to process image upload', details: error.message });
    }
    }

    async function handleBatchUpload(req, res) {
    try {
    if (!req.files || req.files.length === 0) {
    return res.status(400).json({ error: 'No image files uploaded' });
    }

    const results = [];
    for (const file of req.files) {
    const imagePath = `/uploads/${file.filename}`;
    const absolutePath = path.join(__dirname, '..', 'uploads', file.filename);

    try {
    const vector = await modelService.extractImageFeatures(absolutePath);
    await milvusService.insertImageVector(imagePath, vector);
    results.push({ filename: file.filename, status: 'success', path: imagePath });
    } catch (fileError) {
    console.error(`Error processing file ${file.filename}:`, fileError);
    results.push({ filename: file.filename, status: 'failed', error: fileError.message });
    }
    }

    res.json({
    success: true,
    message: 'Batch upload processed',
    results,
    });
    } catch (error) {
    console.error('Batch upload error:', error);
    res.status(500).json({ error: 'Batch upload failed', details: error.message });
    }
    }

    module.exports = { handleUpload, handleBatchUpload };

    2. 搜索控制器 (controllers/searchController.js) 处理文本搜索请求。

    // controllers/searchController.js
    const modelService = require('../services/modelService');
    const milvusService = require('../services/milvusService');

    async function handleTextSearch(req, res) {
    try {
    const { query, limit = 10 } = req.body;

    if (!query || typeof query !== 'string') {
    return res.status(400).json({ error: 'Search query is required and must be a string' });
    }

    console.log(`Received search query: "${query}"`);

    // 1. 提取文本特征向量
    const queryVector = await modelService.extractTextFeatures(query);

    // 2. 在Milvus中搜索相似图片
    const searchResults = await milvusService.searchSimilarImages(queryVector, parseInt(limit));

    // 3. 格式化返回结果
    const formattedResults = searchResults.map(item => ({
    image_url: `http://localhost:${process.env.PORT || 3000}${item.image_path}`,
    score: item.score,
    // 可以在这里添加更多信息,比如从数据库查询图片的元数据
    }));

    res.json({
    success: true,
    query,
    results: formattedResults,
    count: formattedResults.length,
    });
    } catch (error) {
    console.error('Search error:', error);
    res.status(500).json({ error: 'Search failed', details: error.message });
    }
    }

    module.exports = { handleTextSearch };

    3. 路由定义 (routes/upload.js 和 routes/search.js) 最后,创建路由文件来映射URL到控制器函数。

    // routes/upload.js
    const express = require('express');
    const multer = require('multer');
    const { handleUpload, handleBatchUpload } = require('../controllers/uploadController');

    const router = express.Router();

    // 配置multer存储
    const storage = multer.diskStorage({
    destination: function (req, file, cb) {
    cb(null, 'uploads/');
    },
    filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
    }
    });
    const upload = multer({ storage: storage });

    // 单张图片上传
    router.post('/single', upload.single('image'), handleUpload);
    // 多张图片上传
    router.post('/batch', upload.array('images', 10), handleBatchUpload); // 最多10张

    module.exports = router;

    // routes/search.js
    const express = require('express');
    const { handleTextSearch } = require('../controllers/searchController');

    const router = express.Router();

    router.post('/text', handleTextSearch);

    module.exports = router;

    4. 在 server.js 中启用路由 回到 server.js,取消注释并引入我们创建的路由。

    // server.js (在合适位置添加)
    const uploadRoutes = require('./routes/upload');
    const searchRoutes = require('./routes/search');
    app.use('/api/upload', uploadRoutes);
    app.use('/api/search', searchRoutes);

    现在,后端的主体部分就完成了。你可以运行 nodemon server.js 来启动后端服务器。接下来,我们为这个强大的引擎打造一个好看又好用的控制面板——前端界面。

    5. 前端开发:打造直观的用户界面

    前端的目标是简洁直观,让用户能轻松上传图片和进行搜索。我们使用Vue3和Element Plus来构建。

    5.1 项目配置与主组件

    首先,在 src/main.js 或 src/main.ts 中全局引入Element Plus和Axios。

    // src/main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import axios from 'axios'

    const app = createApp(App)

    // 全局配置axios,设置基础URL指向后端
    axios.defaults.baseURL = 'http://localhost:3000/api'

    app.use(ElementPlus)
    app.mount('#app')

    然后,修改 src/App.vue,设置一个简单的布局,包含两个主要功能页签。

    <!– src/App.vue –>
    <template>
    <div id="app">
    <header class="app-header">
    <h1> 智能相册</h1>
    <p class="subtitle">用自然语言,找到你记忆中的每一张照片</p>
    </header>

    <main class="app-main">
    <el-tabs v-model="activeTab" type="border-card" class="main-tabs">
    <el-tab-pane label=" 我的图库" name="library">
    <LibraryView />
    </el-tab-pane>
    <el-tab-pane label=" 智能搜索" name="search">
    <SearchView />
    </el-tab-pane>
    </el-tabs>
    </main>

    <footer class="app-footer">
    <p>Powered by Git-RSCLIP & Vue3 & Node.js</p>
    </footer>
    </div>
    </template>

    <script setup>
    import { ref } from 'vue';
    import LibraryView from './components/LibraryView.vue';
    import SearchView from './components/SearchView.vue';

    const activeTab = ref('search'); // 默认激活搜索页
    </script>

    <style scoped>
    #app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: 'Helvetica Neue', Arial, sans-serif;
    }
    .app-header {
    text-align: center;
    padding: 2rem 1rem;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    }
    .app-header h1 {
    margin: 0;
    font-size: 2.5rem;
    }
    .subtitle {
    margin-top: 0.5rem;
    opacity: 0.9;
    font-size: 1.1rem;
    }
    .app-main {
    flex: 1;
    padding: 2rem;
    max-width: 1200px;
    margin: 0 auto;
    width: 100%;
    }
    .main-tabs {
    min-height: 500px;
    }
    .app-footer {
    text-align: center;
    padding: 1rem;
    background-color: #f5f7fa;
    color: #909399;
    font-size: 0.9rem;
    }
    </style>

    5.2 图库管理组件

    创建 src/components/LibraryView.vue,负责图片上传和展示。

    <!– src/components/LibraryView.vue –>
    <template>
    <div class="library-view">
    <div class="upload-section">
    <el-upload
    class="upload-demo"
    drag
    action="#"
    :multiple="true"
    :auto-upload="false"
    :on-change="handleFileChange"
    :file-list="fileList"
    :limit="10"
    >
    <el-icon class="el-icon–upload"><upload-filled /></el-icon>
    <div class="el-upload__text">
    将文件拖到此处,或 <em>点击上传</em>
    </div>
    <template #tip>
    <div class="el-upload__tip">
    支持上传 JPG/PNG 格式的图片,单次最多10张。
    </div>
    </template>
    </el-upload>
    <div class="upload-actions">
    <el-button type="primary" :loading="uploading" @click="handleUpload">
    开始上传并处理
    </el-button>
    <el-button @click="fileList = []">清空列表</el-button>
    </div>
    </div>

    <el-divider />

    <div class="image-grid" v-if="uploadedImages.length > 0">
    <h3>已上传的图片 ({{ uploadedImages.length }})</h3>
    <div class="grid">
    <div v-for="(img, index) in uploadedImages" :key="index" class="image-item">
    <el-image
    style="width: 150px; height: 150px;"
    :src="img.url"
    :preview-src-list="uploadedImages.map(i => i.url)"
    fit="cover"
    />
    <div class="image-name">{{ img.name }}</div>
    </div>
    </div>
    </div>
    <div v-else class="empty-state">
    <el-empty description="暂无图片,请上传一些照片来构建你的智能图库吧" />
    </div>
    </div>
    </template>

    <script setup>
    import { ref } from 'vue';
    import { ElMessage, ElMessageBox } from 'element-plus';
    import { UploadFilled } from '@element-plus/icons-vue';
    import axios from 'axios';

    const fileList = ref([]);
    const uploadedImages = ref([]);
    const uploading = ref(false);

    const handleFileChange = (file, fileList) => {
    // 更新文件列表
    console.log('File changed:', file, fileList);
    };

    const handleUpload = async () => {
    if (fileList.value.length === 0) {
    ElMessage.warning('请先选择要上传的图片');
    return;
    }

    uploading.value = true;
    const formData = new FormData();
    fileList.value.forEach(file => {
    formData.append('images', file.raw); // 注意:.raw 是上传组件的原始文件对象
    });

    try {
    const response = await axios.post('/upload/batch', formData, {
    headers: {
    'Content-Type': 'multipart/form-data',
    },
    });

    if (response.data.success) {
    ElMessage.success(`成功处理 ${response.data.results.filter(r => r.status === 'success').length} 张图片`);
    // 更新已上传图片列表(这里简化处理,实际应从后端获取列表)
    response.data.results.forEach(result => {
    if (result.status === 'success') {
    // 假设后端返回的path是相对路径,需要拼接完整URL
    const fullUrl = `http://localhost:3000${result.path}`;
    uploadedImages.value.push({
    name: result.filename,
    url: fullUrl,
    });
    }
    });
    fileList.value = []; // 清空上传列表
    } else {
    ElMessage.error('上传处理失败');
    }
    } catch (error) {
    console.error('Upload error:', error);
    ElMessage.error('上传过程中发生错误: ' + (error.response?.data?.error || error.message));
    } finally {
    uploading.value = false;
    }
    };
    </script>

    <style scoped>
    .library-view {
    padding: 20px;
    }
    .upload-section {
    margin-bottom: 30px;
    }
    .upload-actions {
    margin-top: 20px;
    display: flex;
    gap: 10px;
    }
    .image-grid h3 {
    margin-bottom: 15px;
    color: #333;
    }
    .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 20px;
    }
    .image-item {
    border: 1px solid #ebeef5;
    border-radius: 8px;
    overflow: hidden;
    transition: box-shadow 0.3s;
    }
    .image-item:hover {
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    }
    .image-name {
    padding: 8px;
    font-size: 0.85rem;
    color: #666;
    text-align: center;
    word-break: break-all;
    }
    .empty-state {
    margin-top: 50px;
    }
    </style>

    5.3 智能搜索组件

    创建 src/components/SearchView.vue,这是系统的核心交互界面。

    <!– src/components/SearchView.vue –>
    <template>
    <div class="search-view">
    <div class="search-input-section">
    <el-input
    v-model="searchQuery"
    placeholder="请输入描述来搜索图片,例如:一只在草地上玩耍的棕色小狗、日落时分的海滩、包含红色汽车的照片…"
    size="large"
    @keyup.enter="handleSearch"
    >
    <template #append>
    <el-button :loading="searching" @click="handleSearch" type="primary">
    <el-icon><Search /></el-icon> 搜索
    </el-button>
    </template>
    </el-input>
    <div class="query-examples">
    <span>试试搜索:</span>
    <el-tag
    v-for="(example, idx) in exampleQueries"
    :key="idx"
    class="example-tag"
    type="info"
    effect="plain"
    @click="searchQuery = example; handleSearch()"
    >
    {{ example }}
    </el-tag>
    </div>
    </div>

    <div v-if="searching" class="loading-section">
    <el-skeleton :rows="6" animated />
    </div>

    <div v-else-if="searchResults.length > 0" class="results-section">
    <div class="results-header">
    <h3>找到 {{ searchResults.length }} 张相关图片 (耗时 {{ searchTime }}ms)</h3>
    <el-button @click="clearResults" size="small">清空结果</el-button>
    </div>
    <div class="results-grid">
    <div v-for="(result, index) in searchResults" :key="index" class="result-item">
    <el-card shadow="hover" :body-style="{ padding: '0px' }">
    <el-image
    :src="result.image_url"
    :preview-src-list="searchResults.map(r => r.image_url)"
    fit="cover"
    style="width: 100%; height: 200px;"
    lazy
    />
    <div style="padding: 14px;">
    <div class="result-score">相似度: {{ (1 – result.score).toFixed(3) }}</div>
    <div class="result-path">{{ result.image_url.split('/').pop() }}</div>
    </div>
    </el-card>
    </div>
    </div>
    </div>

    <div v-else-if="hasSearched" class="no-results">
    <el-empty description="没有找到匹配的图片,换个描述试试看?" />
    </div>

    <div v-else class="welcome-section">
    <div class="welcome-card">
    <h3> 如何开始?</h3>
    <p>1. 在 <strong>“我的图库”</strong> 页面上传一些照片。</p>
    <p>2. 回到本页,在上方输入框用自然语言描述你想找的图片。</p>
    <p>3. 系统会通过AI理解你的描述,并返回最相关的图片。</p>
    <p class="tip"> 提示:描述越具体、越详细,搜索结果越精准!</p>
    </div>
    </div>
    </div>
    </template>

    <script setup>
    import { ref } from 'vue';
    import { ElMessage } from 'element-plus';
    import { Search } from '@element-plus/icons-vue';
    import axios from 'axios';

    const searchQuery = ref('');
    const searchResults = ref([]);
    const searching = ref(false);
    const hasSearched = ref(false);
    const searchTime = ref(0);

    const exampleQueries = [
    '一只在草地上玩耍的棕色小狗',
    '日落时分的海滩',
    '包含红色汽车的照片',
    '办公桌上的笔记本电脑和咖啡',
    '夜晚的城市灯光',
    '雪山和湖泊的风景',
    ];

    const handleSearch = async () => {
    if (!searchQuery.value.trim()) {
    ElMessage.warning('请输入搜索内容');
    return;
    }

    searching.value = true;
    hasSearched.value = true;
    const startTime = Date.now();

    try {
    const response = await axios.post('/search/text', {
    query: searchQuery.value,
    limit: 12, // 每次返回12个结果
    });

    searchTime.value = Date.now() – startTime;

    if (response.data.success) {
    searchResults.value = response.data.results;
    if (response.data.results.length === 0) {
    ElMessage.info('未找到相关图片,请尝试其他描述。');
    } else {
    ElMessage.success(`搜索完成,找到 ${response.data.results.length} 张图片`);
    }
    } else {
    ElMessage.error('搜索失败: ' + (response.data.error || '未知错误'));
    }
    } catch (error) {
    console.error('Search request error:', error);
    ElMessage.error('搜索请求出错: ' + (error.response?.data?.error || error.message));
    searchResults.value = [];
    } finally {
    searching.value = false;
    }
    };

    const clearResults = () => {
    searchResults.value = [];
    searchQuery.value = '';
    hasSearched.value = false;
    };
    </script>

    <style scoped>
    .search-view {
    padding: 20px;
    }
    .search-input-section {
    margin-bottom: 30px;
    }
    .query-examples {
    margin-top: 15px;
    font-size: 0.9rem;
    color: #666;
    }
    .example-tag {
    margin-left: 8px;
    cursor: pointer;
    user-select: none;
    }
    .example-tag:hover {
    background-color: #ecf5ff;
    color: #409eff;
    }
    .loading-section {
    margin-top: 40px;
    }
    .results-section {
    margin-top: 30px;
    }
    .results-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    }
    .results-header h3 {
    margin: 0;
    color: #333;
    }
    .results-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 20px;
    }
    .result-item {
    transition: transform 0.2s;
    }
    .result-item:hover {
    transform: translateY(-5px);
    }
    .result-score {
    font-size: 0.85rem;
    color: #67c23a;
    font-weight: bold;
    margin-bottom: 5px;
    }
    .result-path {
    font-size: 0.75rem;
    color: #909399;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }
    .welcome-section {
    margin-top: 60px;
    text-align: center;
    }
    .welcome-card {
    display: inline-block;
    padding: 30px 40px;
    background-color: #f8f9fa;
    border-radius: 12px;
    border-left: 5px solid #409eff;
    text-align: left;
    max-width: 600px;
    }
    .welcome-card h3 {
    margin-top: 0;
    color: #409eff;
    }
    .welcome-card p {
    margin: 10px 0;
    line-height: 1.6;
    }
    .welcome-card .tip {
    margin-top: 20px;
    padding-top: 15px;
    border-top: 1px dashed #dcdfe6;
    color: #e6a23c;
    font-style: italic;
    }
    .no-results {
    margin-top: 60px;
    }
    </style>

    5.4 运行与测试

    现在,前端和后端都准备好了。请确保:

  • Milvus数据库正在运行 (docker-compose up -d)。
  • 后端服务器正在运行 (nodemon server.js,在 smart-album-backend 目录)。
  • 前端开发服务器正在运行 (npm run dev,在 smart-album-frontend 目录)。
  • 打开浏览器,访问前端开发服务器提供的地址(通常是 http://localhost:5173)。你应该能看到一个漂亮的界面。

    测试流程:

  • 切换到“我的图库”标签页,上传几张内容各异的图片(比如风景、动物、物品等)。
  • 等待上传和处理完成(后端控制台会有日志)。
  • 切换到“智能搜索”标签页,在输入框尝试用自然语言搜索,比如“有树的照片”或“蓝色的物体”。
  • 观察返回的搜索结果和相似度分数。
  • 恭喜你!一个完整的、基于自然语言搜索的智能相册系统已经在你本地运行起来了。

    6. 总结与展望

    走完这一趟从零到一的开发旅程,我们不仅实现了一个功能完整的智能相册,更重要的是,你亲手实践了如何将前沿的AI模型(Git-RSCLIP)与成熟的全栈技术(Vue3 + Node.js)以及专业的向量数据库(Milvus)结合起来,解决一个真实的痛点——海量图片的语义化检索。

    回顾一下,这个项目的核心价值在于,它提供了一种全新的图片管理交互范式。用户不再受限于预设的、僵化的标签体系,而是可以用最自然的人类语言与自己的记忆库对话。这对于个人用户管理日益庞大的手机相册,或者对于企业用户(如电商、媒体机构)管理数字资产,都提供了一个极具潜力的方向。

    当然,我们目前搭建的是一个基础版本,一个坚实的起点。在实际生产环境中,还有大量的优化和扩展空间可以探索。例如,可以考虑加入更完善的错误处理和用户反馈机制,让上传和搜索过程更稳定。性能方面,对于非常大的图片库,可以考虑对特征向量建立更复杂的索引(如HNSW),或者引入缓存机制。功能上,可以增加图片的元数据管理(时间、地点)、搜索历史记录,甚至结合多模态大模型,实现更复杂的“以图搜图”或“根据图片内容自动生成描述”等功能。

    技术总是在快速迭代,但解决问题的思路是相通的。希望这个项目能成为你探索AI应用开发的一块敲门砖。当你下次再面对“如何从几千张照片里找到某一张”这个问题时,你不仅知道有更智能的解决方案,更拥有了亲手去实现它的能力。


    获取更多AI镜像

    想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

    赞(0)
    未经允许不得转载:171主机测评 » Git-RSCLIP智能相册开发:Vue前端+Node.js后端全栈实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址