# imgcms **Repository Path**: bullet/imgcms ## Basic Information - **Project Name**: imgcms - **Description**: 图片管理系统,图片分类系统,支持单张和一组图片进行分类。 - **Primary Language**: Go - **License**: MIT - **Default Branch**: master - **Homepage**: https://gitee.com/bullet/imgcms - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-04-25 - **Last Updated**: 2026-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 图片管理系统 (img-cms-v2) 默认登录账号为admin,密码为123456,支持修改。需要自己构建为可执行文件。 ## 用途 - 图片标签管理,最大支持6级标签,支持同级标签排序 - 支持单张图片和一组图片打标签 - 图片最多打10个标签(图片要放在temp目录下) - 浏览已打标签的图片并支持编辑(打过标签的图片在images目录下) - 浏览图片支持10倍放大和3倍缩小(滚动鼠标即可放大缩小) - 支持在系统设置中调节各种参数 ## 功能截图 ![image](./screenshot/20260511180102.png) ![image](./screenshot/20260511180052.png) ![image](./screenshot/20260511180205.png) ![image](./screenshot/20260511183144.png) ![image](./screenshot/20260521134931.png) ## 技术栈 ### 前端 - **Vue 3** - 渐进式 JavaScript 框架 - **TypeScript** - JavaScript 的超集,提供类型安全 - **Vite** - 下一代前端构建工具 - **Wails Runtime** - 前后端通信桥接 ### 后端 - **Go 1.25** - 高性能编程语言 - **Wails v2** - 跨平台桌面应用框架 - **SQLite3** - 轻量级数据库 - **GORM** - Go ORM 库 - **Zap** - 高性能日志库 - **Systray** - 系统托盘支持 ## 项目架构 ### 整体架构 ``` ┌─────────────────────────────────────────────────────────┐ │ 前端层 (Frontend) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Views │ │ Components │ │ Router │ │ │ │ (Vue3 + TS) │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ │ Wails Runtime │ ┌─────────────────────────────────────────────────────────┐ │ 应用层 (App) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ App.go │ │ MenuBar │ │ Bindings │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ 启动引导层 (Bootstrap) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Bootstrap │ │ Providers │ │ Components │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ 业务逻辑层 (Service) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ImageService │ │ UserService │ │ TagService │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ 数据层 (Database) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Connection │ │ Models │ │ SQLite3 │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 目录结构 ``` img-cms-v2/ ├── main.go # 应用主入口 ├── wails.json # Wails 配置文件 ├── go.mod # Go 依赖管理 ├── go.sum # Go 依赖锁定 │ ├── backend/ # 后端代码 │ ├── app.go # 应用核心类 │ │ │ ├── app/ # 应用层 │ │ ├── model/ # 数据模型 │ │ │ ├── image.go # 图片模型 │ │ │ ├── tag.go # 标签模型 │ │ │ └── user.go # 用户模型 │ │ │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── image_service.go # 图片服务 │ │ │ ├── tag_service.go # 标签服务 │ │ │ └── user_service.go # 用户服务 │ │ │ │ │ └── menubar/ # 菜单栏 │ │ └── menubar.go # 菜单构建器 │ │ │ ├── bootstrap/ # 启动引导 │ │ ├── bootstrap.go # 引导器核心 │ │ ├── component.go # 组件接口定义 │ │ │ │ │ └── providers/ # 组件提供者 │ │ ├── database.go # 数据库提供者 │ │ ├── event.go # 事件提供者 │ │ ├── systray.go # 系统托盘提供者 │ │ └── hotkey.go # 快捷键提供者 │ │ │ ├── config/ # 配置 │ │ ├── config.go # 配置结构定义 │ │ └── config.yaml # 配置文件 │ │ │ └── db/ # 数据库 │ ├── connection.go # 数据库连接 │ └── migrations/ # 数据库迁移 │ └── 001_init_schema.sql │ └── frontend/ # 前端代码 ├── src/ │ ├── views/ # 视图页面 │ ├── components/ # 组件 │ ├── router/ # 路由 │ └── main.ts # 前端入口 │ ├── package.json # 前端依赖 ├── vite.config.ts # Vite 配置 └── tsconfig.json # TypeScript 配置 ``` ## 图片文件命名规则 ### 问题背景 当从 temp 目录给图片打标签移动到 images 目录时,需要处理文件重命名问题,避免文件名冲突。 ### 一、后缀移除规则 temp 下的文件名末尾如果有 `-数字` 模式(如 `-1`、`-2-3`),需要先移除: | 原始文件名 | 移除后缀后 | |-----------|-----------| | `abc.jpg` | `abc.jpg`(无后缀,保持不变) | | `abc-1.jpg` | `abc.jpg` | | `abc-2.jpg` | `abc.jpg` | | `abc-1-2-3.jpg` | `abc.jpg` | **实现逻辑**:使用正则 `/-(\d+)$/` 匹配末尾的 `-数字`,匹配成功则移除,继续检查直到不匹配为止。 ### 二、移动文件时的重命名规则 假设移除后缀后得到: - **临时变量1** = `abc`(不含扩展名的文件名) - **临时变量2** = `.jpg`(扩展名) **在目标目录(如 `images/hello/`)下搜索所有以临时变量1开头的文件**,根据搜索结果决定命名: #### 重命名示例表 | 搜索结果 | 是否存在 `abc.jpg` | 新文件命名 | |---------|------------------|-----------| | `["abc.png", "abc-2.jpg"]` | **不存在** | `abc.jpg` | | `["abc.png", "abc.jpg", "abc-2.jpg"]` | **存在** | `abc-1.jpg` | | `["abc.png", "abc.jpg", "abc-1.jpg", "abc-2.jpg"]` | **存在** | `abc-3.jpg` | #### 规则总结 1. 如果搜索结果中**不存在** `临时变量1+临时变量2`(如 `abc.jpg`),直接命名为 `abc.jpg` 2. 如果存在,依次查找可用的 `-序号` 后缀,直到找到不冲突的名称 3. 如果未搜索到同名文件则可以直接把文件移动过去 ### 三、示例流程 以 `abc-1.jpg` 为例: 1. 移除后缀 → 基础名 `abc`,扩展名 `.jpg` 2. 在 `images/hello/` 下搜索 `abc*` → 得到 `["abc.png", "abc-2.jpg"]` 3. 不存在 `abc.jpg` → 新文件命名为 `images/hello/abc.jpg` ## 图片标签与追加图片功能逻辑 ### 一、两种模式区分 系统使用 `mode` 属性区分不同场景: | 场景 | mode | 说明 | |------|------|------| | Images.vue(编辑已有图片) | `"edit"` | 图片已入库,支持后端 API 调用 | | Scan.vue(扫描未打标图片) | `"add"` | 图片未入库,纯本地操作 | ### 二、图片打标签逻辑 #### Scan.vue(mode="add") 未打标签的图片存储在 `temp` 目录,用户选择图片后通过 `AddTagToImage` 一次性批量处理: ```typescript // Scan.vue - 接收 ImageViewer 传来的所有图片路径和标签ID const handleAddTags = async (filePaths: string[], tagIds: number[]) => { await AddTagToImage({ image_paths: filePaths, // [主图路径, 追加图片1, 追加图片2, ...] tag_ids: tagIds }) ElMessage.success('图片打标签成功,图片已移动') await fetchImages() } ``` **处理流程**: 1. 用户点击 temp 目录中的图片 2. 打开 ImageViewer(mode="add") 3. 可选择标签、可追加本地图片 4. 点击「保存标签」时,将所有图片路径 + 标签ID 一次性提交 5. 后端处理:移动文件到 `images/{一级标签}/` 目录,更新数据库 #### Images.vue(mode="edit") 已打标签的图片存储在 `images` 目录,编辑标签时调用后端 API: ```typescript // Images.vue - 直接更新已有图片的标签 emit('update-tags', imageId, selectedTags.value.map(t => t.id)) ``` ### 三、编辑标签逻辑 ImageViewer 组件支持多组标签选择(最多4组),每组支持4级标签: ```typescript // 标签数据结构 const tagGroups = ref([{ selectedIds: [undefined, undefined, undefined, undefined] }]) // 获取已选择的标签(取最深层的标签ID) const selectedTags = computed(() => { const selected: Tag[] = [] const seen = new Set() tagGroups.value.forEach(group => { let deepestTagId: number | undefined for (let i = 3; i >= 0; i--) { if (group.selectedIds[i] !== undefined) { deepestTagId = group.selectedIds[i] break } } if (deepestTagId && !seen.has(deepestTagId)) { const tag = props.allTags.find(t => t.id === deepestTagId) if (tag) selected.push(tag) } }) return selected }) ``` **保存时区分处理**: ```typescript const saveTags = async () => { if (props.mode === 'add') { // 传递所有图片路径 + 所有标签 const allImagePaths = [props.image.file_path, ...localAppendedImages.value] emit('add-tags', allImagePaths, selectedTags.value.map(t => t.id)) } else { emit('update-tags', props.image.id, selectedTags.value.map(t => t.id)) } } ``` ### 四、追加图片逻辑 #### 追加来源 - 扫描 `temp` 目录中的未打标图片 - 支持分页加载(每页 10 条) - 滚动到底部自动加载更多 #### 追加操作(区分 mode) ```typescript const confirmAddImages = async () => { if (props.mode === 'add') { // 本地存储,不请求后端 localAppendedImages.value = [...localAppendedImages.value, ...selectedNewImages.value] ElMessage.success(`已添加 ${selectedNewImages.value.length} 张图片到待处理列表`) } else { // 调用后端 API await AddImagesToGroup({ id: props.image.id, image_paths: selectedNewImages.value }) emit('refresh') } } ``` #### 显示条件 - 追加图片按钮:图片数量 < 20 张时显示 - 删除按钮:图片数量 > 1 张时显示 ### 五、删除图片逻辑 ```typescript const handleRemoveCurrentImage = async () => { if (props.mode === 'add') { // 从本地列表移除(跳过主图 index=0) const idxToRemove = currentIndex.value localAppendedImages.value.splice(idxToRemove - 1, 1) ElMessage.success('删除成功') } else { // 调用后端 API await RemoveImageFromGroup({ id: props.image.id, index: currentIndex.value }) emit('refresh') } } ``` ### 六、数据流示意图 ``` Scan.vue(mode="add") │ ├── 点击图片 → ImageViewer (mode="add") │ │ │ ├── 选择标签(本地操作) │ ├── 追加图片(本地 localAppendedImages) │ ├── 删除图片(本地移除) │ └── 保存 → AddTagToImage(所有路径, 所有标签) │ → 后端移动文件 → 更新数据库 │ └── fetchImages() 刷新列表 Images.vue(mode="edit") │ ├── 点击图片组 → ImageViewer (mode="edit") │ │ │ ├── 编辑标签 → update-tags API │ ├── 追加图片 → AddImagesToGroup API │ └── 删除图片 → RemoveImageFromGroup API │ └── emit('refresh') 刷新列表 ``` ### 七、图片轮播与操作 ImageViewer 支持多张图片轮播查看: | 操作 | 方式 | |------|------| | 切换图片 | 鼠标左右滑动、键盘左右箭头、点击小圆点 | | 缩放图片 | 鼠标滚轮(0.3x ~ 10x) | | 拖拽移动 | 缩放状态下拖拽图片 | | 重置缩放 | Ctrl+0 | ## 核心组件 ### 1. Bootstrap(启动引导) 负责应用的初始化和组件生命周期管理。 **核心功能:** - 组件注册与依赖管理 - 按优先级顺序初始化组件 - 组件启动和停止管理 - 错误处理和日志记录 **组件生命周期:** ``` Register → Initialize → Start → (Running) → Stop ``` ### 2. Providers(组件提供者) 实现了 `Component` 接口的各种功能提供者。 **DatabaseProvider** - 优先级:100(最高) - 功能:管理数据库连接和迁移 - 依赖:无 **EventProvider** - 优先级:50 - 功能:处理前后端事件通信 - 依赖:无 **SystrayProvider** - 优先级:30 - 功能:管理系统托盘图标和菜单 - 依赖:无 **HotkeyProvider** - 优先级:30 - 功能:注册全局快捷键 - 依赖:无 ### 3. Service(业务服务层) 封装业务逻辑,协调 Repository 和其他服务。 **ImageService** - 图片列表查询和分页 - 扫描未分类图片 - 图片标签管理 - 孤儿图片清理 **UserService** - 用户登录认证 - 密码管理 **TagService** - 标签树形结构管理 - 标签层级支持(最多4级) - 标签排序 ## 业务流转流程 ### 1. 应用启动流程 ``` main.go ↓ wails.Run() ↓ App.Startup(ctx) ↓ Bootstrap.Start() ↓ ┌─────────────────────────────────────┐ │ 按优先级初始化所有组件: │ │ 1. DatabaseProvider (100) │ │ - 建立数据库连接 │ │ - 执行数据库迁移 │ │ 2. EventProvider (50) │ │ - 注册事件监听器 │ │ 3. SystrayProvider (30) │ │ - 初始化系统托盘 │ │ 4. HotkeyProvider (30) │ │ - 注册全局快捷键 │ └─────────────────────────────────────┘ ↓ 初始化 Service 层 ↓ 应用就绪 ``` ### 2. 本地图片加载流程 ``` 前端 ↓ 请求本地图片路径 ↓ FileLoader.ServeHTTP() ↓ 读取文件并返回 ↓ 前端显示图片 ``` ## 配置说明 ### Wails 配置 (wails.json) ```json { "name": "img-cms-v2", "outputfilename": "img-cms-v2", "frontend:install": "npm install", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto" } ``` ### 应用配置 (backend/config/config.yaml) ```yaml app: services: image: temp_dir: ./temp allowed_formats: [jpg, jpeg, webp, png, gif] max_file_size: 10485760 user: session_timeout: 3600 bootstrap: components: - name: database enabled: true config: type: sqlite3 dsn: ./data/app.db migrate: true ``` ## 开发指南 ### 环境要求 - Go 1.25+ - Node.js 16+ - npm 或 yarn ### 安装依赖 ```bash # 安装 Go 依赖 go mod download # 安装前端依赖 cd frontend npm install cd .. ``` ### 开发模式 ```bash wails dev ``` ### 构建生产版本 ```bash wails build ``` ## 许可证 MIT License