Qwen-Image-2512-Pixel-Art-LoRA 与MySQL集成:构建带管理后台的素材库系统
Qwen-Image-2512-Pixel-Art-LoRA 与MySQL集成:构建带管理后台的素材库系统
1. 引言
你有没有遇到过这样的场景?团队里的小伙伴用AI模型生成了大量精美的像素画素材,今天张三存桌面,明天李四放网盘,后天王五自己又生成了一套风格类似的。等到真正要用的时候,谁也找不到谁的东西,重复劳动不说,好不容易找到一张图,却不知道当初是用什么提示词生成的,想微调一下都无从下手。
这其实就是很多创意团队在引入AI工具后的真实写照。工具很强大,但产出的内容缺乏有效的管理和沉淀,最终导致效率不升反降。
今天,我们就来聊聊怎么解决这个问题。我会带你一步步搭建一个专为像素画素材设计的“数字仓库”——一个基于SpringBoot、MySQL,并深度集成了Qwen-Image-2512-Pixel-Art-LoRA模型的管理系统。这个系统不仅能让你一键生成像素画,还能自动把生成的作品、用的提示词、各种参数,甚至你打的标签,都整整齐齐地存进数据库。前端提供一个清爽的后台,让你和你的团队可以轻松地搜索、筛选、收藏和下载这些素材。
简单来说,我们要做的就是把“生成”和“管理”这两件事打通,让创意从灵光一现到成为团队可复用的资产,整个过程丝滑流畅。无论你是独立开发者、小型工作室的负责人,还是对AI应用落地感兴趣的工程师,这套方案都能给你带来直接的启发和可运行的代码。
2. 系统核心设计思路
在动手写代码之前,我们先得把整个系统的“骨架”搭清楚。这个系统的核心目标就一个:让AI生成的像素画素材,从一次性消耗品变成可管理、可检索、可复用的团队数字资产。
2.1 我们要解决什么问题?
想象一下,如果没有这个系统,一个典型的像素画生成工作流可能是这样的:
- 打开模型API调试工具或界面。
- 输入提示词,调整参数,生成图片。
- 觉得不错,手动保存图片到本地文件夹。
- 新建一个文本文件,或者用脑子记住这次用的提示词和参数。
- 下次需要类似风格时,要么重新调参,要么去翻那个可能已经忘记命名的文件夹。
这个过程充满了断裂点。图片和它的“生成配方”(提示词、参数)是分离的;素材分散在个人电脑上,无法团队共享;查找完全依赖记忆和手工浏览。
2.2 我们的解决方案是什么?
我们的系统设计,就是要填平这些断裂点,打造一个闭环工作流:
生成即入库:用户在前端界面输入提示词、选择风格参数,点击生成。系统后端在调用Qwen-Image-2512-Pixel-Art-LoRA的API生成图片后,不是简单地返回图片给前端就完事了,而是立刻将图片文件保存到服务器(或对象存储),同时将图片的访问路径、本次使用的所有提示词、参数、以及用户添加的标签,作为一条完整的记录,存入MySQL数据库。这样,每一张图片都自带“出生证明”。
结构化存储:MySQL数据库在这里扮演了“素材档案室”的角色。我们不会只存一个图片文件名。我们会设计专门的表,来记录素材的元数据(Metadata),比如:
- 素材本身:ID、名称、描述、存储路径、生成时间、作者。
- 生成配方:正向提示词、负向提示词、采样器、步数、引导系数、分辨率等。
- 分类信息:用户自定义的标签(如“森林”、“16bit风格”、“角色”)、系统自动识别的类别。
高效检索与复用:有了结构化的数据,前端的搜索和筛选功能就变得非常强大。你可以通过关键词搜索提示词中包含“城堡”的所有素材;可以筛选出所有使用了“像素风”标签的作品;甚至可以找到某位同事在上周生成的所有素材。更重要的是,当你看到一张喜欢的素材,你可以直接查看它的完整生成参数,并点击“以此参数重新生成”或“微调”,快速获得风格一致的新作品,极大提升了创意的迭代效率。
团队协作友好:所有素材集中管理,权限清晰(虽然本文简化了权限设计,但预留了接口),避免了素材散落和丢失。收藏、下载、统计等功能,让素材在团队内部流动起来。
下面这张图概括了从创意输入到素材入库,再到管理使用的核心数据流,你可以先有个直观的印象:
graph TD A[用户前端输入创意] --> B[提交生成请求至后端] B --> C[后端调用 Qwen-Image API] C --> D{生成成功?} D -- 是 --> E[保存图片至存储服务] E --> F[将素材元数据存入MySQL] F --> G[前端展示新素材并加入库] D -- 否 --> H[返回错误信息] G --> I[用户可通过后台搜索/筛选/下载] F --> I这个闭环,就是我们整个系统的灵魂。接下来,我们就开始动手,先把系统的“地基”——数据库和后端服务搭建起来。
3. 后端搭建:SpringBoot与MySQL
后端是整个系统的大脑,负责处理业务逻辑、与数据库对话、以及调用AI模型API。我们选用SpringBoot,因为它能让我们快速搭建一个结构清晰、易于维护的Web服务。
3.1 数据库设计
数据库表设计是重中之重,它直接决定了数据怎么存、怎么取。我们主要设计以下几张核心表:
1. 素材主表 (asset)这张表存放素材最核心的信息。
CREATE TABLE `asset` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(255) DEFAULT NULL COMMENT '素材名称', `description` text COMMENT '素材描述', `image_url` varchar(500) NOT NULL COMMENT '图片存储路径/URL', `thumbnail_url` varchar(500) DEFAULT NULL COMMENT '缩略图路径', `author_id` bigint(20) DEFAULT NULL COMMENT '作者用户ID', `is_public` tinyint(1) DEFAULT '1' COMMENT '是否公开', `view_count` int(11) DEFAULT '0' COMMENT '查看次数', `download_count` int(11) DEFAULT '0' COMMENT '下载次数', `favorite_count` int(11) DEFAULT '0' COMMENT '收藏次数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_author` (`author_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='像素素材主表';2. 生成记录表 (generation_record)这张表专门保存生成这张素材时所用的“配方”,是未来复现或微调的关键。
CREATE TABLE `generation_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `asset_id` bigint(20) NOT NULL COMMENT '关联的素材ID', `positive_prompt` text NOT NULL COMMENT '正向提示词', `negative_prompt` text COMMENT '负向提示词', `model_name` varchar(100) DEFAULT 'Qwen-Image-2512-Pixel-Art-LoRA' COMMENT '模型名称', `sampler` varchar(50) DEFAULT NULL COMMENT '采样器', `steps` int(11) DEFAULT NULL COMMENT '采样步数', `cfg_scale` decimal(5,2) DEFAULT NULL COMMENT '引导系数', `seed` bigint(20) DEFAULT NULL COMMENT '随机种子', `width` int(11) DEFAULT NULL COMMENT '图片宽度', `height` int(11) DEFAULT NULL COMMENT '图片高度', `extra_params` json DEFAULT NULL COMMENT '其他扩展参数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_asset` (`asset_id`), -- 一张素材对应一条生成记录 CONSTRAINT `fk_record_asset` FOREIGN KEY (`asset_id`) REFERENCES `asset` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='素材生成记录表';3. 标签表 (tag) 与素材-标签关联表 (asset_tag)为了实现灵活的筛选,我们采用多对多关系。
CREATE TABLE `tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '标签名称', `type` varchar(20) DEFAULT 'USER' COMMENT '标签类型(SYSTEM系统/USER用户)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; CREATE TABLE `asset_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `asset_id` bigint(20) NOT NULL, `tag_id` bigint(20) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_asset_tag` (`asset_id`,`tag_id`), KEY `idx_tag` (`tag_id`), CONSTRAINT `fk_at_asset` FOREIGN KEY (`asset_id`) REFERENCES `asset` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_at_tag` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='素材-标签关联表';(注:为了简化,用户表(user)等基础表的设计在此省略,你可以根据实际需求添加。)
3.2 SpringBoot项目结构与核心代码
我们用SpringBoot快速初始化一个项目。核心的目录结构会是这样:
src/main/java/com/yourdomain/pixelasset/ ├── PixelAssetApplication.java // 启动类 ├── config/ // 配置类 ├── controller/ // 控制器 ├── service/ // 服务层 │ ├── AssetService.java │ ├── GenerationService.java // 负责调用AI生成API │ └── impl/ ├── repository/ // 数据访问层(使用Spring Data JPA或MyBatis) ├── entity/ // 实体类,对应数据库表 ├── dto/ // 数据传输对象 └── util/ // 工具类核心实体类 (Asset.java)
@Entity @Table(name = "asset") @Data // 使用Lombok简化getter/setter public class Asset { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; @Column(name = "image_url", nullable = false) private String imageUrl; private String thumbnailUrl; private Long authorId; private Boolean isPublic = true; private Integer viewCount = 0; private Integer downloadCount = 0; private Integer favoriteCount = 0; @CreationTimestamp private LocalDateTime createTime; @UpdateTimestamp private LocalDateTime updateTime; // 关联的生成记录(一对一) @OneToOne(mappedBy = "asset", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private GenerationRecord generationRecord; // 关联的标签(多对多) @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "asset_tag", joinColumns = @JoinColumn(name = "asset_id"), inverseJoinColumns = @JoinColumn(name = "tag_id")) private Set<Tag> tags = new HashSet<>(); }素材创建服务层核心逻辑 (AssetService.java)这是业务逻辑的核心,它协调了生成、存储、入库的完整流程。
@Service @Slf4j public class AssetService { @Autowired private AssetRepository assetRepository; @Autowired private GenerationService generationService; // 负责调用AI API @Autowired private FileStorageService fileStorageService; // 负责文件存储 @Transactional public AssetDTO createAsset(AssetCreationRequest request, Long authorId) { // 1. 调用AI生成服务,获取生成的图片文件 log.info("开始调用AI模型生成像素画,提示词:{}", request.getPositivePrompt()); File generatedImageFile = generationService.generatePixelArt( request.getPositivePrompt(), request.getNegativePrompt(), request.getSampler(), request.getSteps(), request.getCfgScale(), request.getSeed(), request.getWidth(), request.getHeight() ); // 2. 上传图片文件到存储服务(如本地磁盘、云存储) String imageUrl = fileStorageService.upload(generatedImageFile); // 可选:生成缩略图 String thumbnailUrl = fileStorageService.generateThumbnail(generatedImageFile); // 3. 保存素材主信息到数据库 Asset asset = new Asset(); asset.setName(request.getName()); asset.setDescription(request.getDescription()); asset.setImageUrl(imageUrl); asset.setThumbnailUrl(thumbnailUrl); asset.setAuthorId(authorId); asset.setIsPublic(request.getIsPublic()); Asset savedAsset = assetRepository.save(asset); // 4. 保存生成记录 GenerationRecord record = new GenerationRecord(); record.setAsset(savedAsset); record.setPositivePrompt(request.getPositivePrompt()); // ... 设置其他所有参数 savedAsset.setGenerationRecord(record); // 建立关联 // 5. 处理标签(关联已存在标签或创建新标签) if (request.getTagNames() != null) { Set<Tag> tags = processTags(request.getTagNames()); savedAsset.setTags(tags); } // 6. 最终保存(级联保存记录和标签关联) assetRepository.save(savedAsset); log.info("素材创建成功,ID: {}, 图片URL: {}", savedAsset.getId(), imageUrl); // 7. 返回DTO给前端 return convertToDTO(savedAsset); } private Set<Tag> processTags(Set<String> tagNames) { // 实现标签查询与创建的逻辑 // ... } }这段代码清晰地展示了“生成即入库”的闭环:接收前端参数 -> 调用AI API -> 保存文件 -> 组装数据模型 -> 存入数据库。@Transactional注解保证了这些步骤要么全部成功,要么全部回滚,确保了数据的一致性。
4. 核心集成:调用Qwen-Image生成API
现在,我们来处理这个系统中最具“魔法”的部分——如何与Qwen-Image-2512-Pixel-Art-LoRA模型对话,让它根据我们的描述画出像素画。
4.1 封装模型调用服务
我们创建一个专门的GenerationService,将调用AI模型API的细节封装起来。这里假设模型通过HTTP API提供服务(例如部署在ModelScope或自行部署的端点)。
@Service @Slf4j public class GenerationService { // 从配置文件读取,例如:generation.api.url=http://your-model-service/v1/images/generations @Value("${generation.api.url}") private String apiUrl; @Value("${generation.api.key:}") private String apiKey; @Autowired private RestTemplate restTemplate; // 需要配置RestTemplate Bean public File generatePixelArt(String positivePrompt, String negativePrompt, String sampler, Integer steps, Float cfgScale, Long seed, Integer width, Integer height) { // 1. 构建请求体,匹配模型API所需的格式 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "Qwen-Image-2512-Pixel-Art-LoRA"); requestBody.put("prompt", positivePrompt); if (StringUtils.hasText(negativePrompt)) { requestBody.put("negative_prompt", negativePrompt); } requestBody.put("sampler", sampler != null ? sampler : "DPM++ 2M Karras"); requestBody.put("steps", steps != null ? steps : 20); requestBody.put("cfg_scale", cfgScale != null ? cfgScale : 7.5f); if (seed != null) { requestBody.put("seed", seed); } requestBody.put("width", width != null ? width : 512); requestBody.put("height", height != null ? height : 512); // 像素画LoRA可能需要特定参数触发,例如添加触发词 // requestBody.put("prompt", "pixel art, " + positivePrompt + ", <lora:pixel-art-lora:1.0>"); // 2. 设置请求头(如认证) HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); if (StringUtils.hasText(apiKey)) { headers.set("Authorization", "Bearer " + apiKey); } HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers); // 3. 发送请求 log.debug("调用生成API,请求参数:{}", requestBody); ResponseEntity<Map> response = restTemplate.postForEntity(apiUrl, requestEntity, Map.class); // 4. 处理响应 if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { // 假设API返回一个包含图片Base64编码或URL的字段,这里以Base64为例 List<Map<String, Object>> images = (List<Map<String, Object>>) response.getBody().get("data"); if (images != null && !images.isEmpty()) { String b64Json = (String) images.get(0).get("b64_json"); // 5. 将Base64解码为图片文件 return decodeBase64ToImageFile(b64Json, "generated_pixel_art.png"); } } log.error("生成API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody()); throw new RuntimeException("AI图像生成失败"); } private File decodeBase64ToImageFile(String b64Json, String fileName) throws IOException { // 实现Base64字符串解码并保存为临时文件 byte[] imageBytes = Base64.getDecoder().decode(b64Json); File tempFile = File.createTempFile("pixel_art_", ".png"); Files.write(tempFile.toPath(), imageBytes); return tempFile; } }关键点说明:
- 参数映射:你需要根据Qwen-Image模型API的实际文档,调整
requestBody中的参数名和结构。上面的代码是一个通用示例。 - 触发LoRA:对于特定的Pixel-Art LoRA,通常需要在提示词中加入特定的触发词或LoRA权重标识(如
<lora:pixel-art-lora:1.0>)。这部分需要你根据具体使用的LoRA进行调整。 - 错误处理:生产环境中需要更完善的错误处理、重试机制和超时控制。
- 异步处理:图像生成可能耗时较长,可以考虑采用异步方式(如
@Async)调用API,通过WebSocket或轮询通知前端结果。
4.2 文件存储策略
生成的图片需要存起来。对于小型系统,可以先存在服务器本地磁盘。
@Service public class LocalFileStorageService implements FileStorageService { @Value("${file.upload.dir}") private String uploadDir; @Override public String upload(File file) { String fileName = System.currentTimeMillis() + "_" + file.getName(); Path targetPath = Paths.get(uploadDir, fileName); try { Files.copy(file.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); // 返回可供访问的URL路径,例如 /assets/images/{fileName} return "/assets/images/" + fileName; } catch (IOException e) { throw new RuntimeException("文件存储失败", e); } } }随着素材量增长,你应该考虑使用云对象存储服务(如阿里云OSS、腾讯云COS),它们能提供更好的可扩展性、可靠性和访问速度。
5. 前端管理后台功能实现
后端API准备好了,我们需要一个界面来使用它们。前端管理后台是团队用户与这个素材库交互的主要窗口。我们使用Vue 3 + Element Plus来快速构建一个简洁高效的管理界面。
5.1 核心页面与组件
1. 素材生成页 (GeneratePage.vue)这是系统的入口,用户在这里进行创作。
<template> <div class="generate-page"> <el-card> <template #header> <span>生成新的像素画素材</span> </template> <el-form :model="form" label-width="100px"> <el-form-item label="素材名称"> <el-input v-model="form.name" placeholder="给你的素材起个名字"></el-input> </el-form-item> <el-form-item label="正向提示词" required> <el-input v-model="form.positivePrompt" type="textarea" :rows="3" placeholder="详细描述你想要的像素画,例如:'16-bit style pixel art of a friendly green dragon, isometric view, vibrant colors, game asset'" ></el-input> </el-form-item> <el-form-item label="负向提示词"> <el-input v-model="form.negativePrompt" type="textarea" :rows="2" placeholder="描述你不想要的内容,例如:'blurry, noisy, ugly, text, watermark'" ></el-input> </el-form-item> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="采样器"> <el-select v-model="form.sampler"> <el-option label="DPM++ 2M Karras" value="DPM++ 2M Karras"></el-option> <el-option label="Euler a" value="Euler a"></el-option> <!-- 更多选项 --> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="步数"> <el-slider v-model="form.steps" :min="10" :max="50" show-input></el-slider> </el-form-item> </el-col> </el-row> <!-- 更多参数:CFG Scale, Seed, 分辨率等 --> <el-form-item label="标签"> <el-select v-model="form.selectedTags" multiple filterable allow-create placeholder="输入或选择标签"> <el-option v-for="tag in existingTags" :key="tag.id" :label="tag.name" :value="tag.name"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" :loading="generating" @click="handleGenerate">开始生成并入库</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </el-card> <!-- 生成结果预览区域 --> <el-card v-if="latestAsset" style="margin-top: 20px;"> <template #header> <span>生成结果</span> <el-button style="float: right;" type="success" @click="goToDetail(latestAsset.id)">查看详情</el-button> </template> <el-image :src="latestAsset.imageUrl" fit="contain" style="width: 256px; height: 256px;"></el-image> <div>提示词:{{ latestAsset.generationRecord.positivePrompt }}</div> </el-card> </div> </template> <script setup> import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' import { generateAsset } from '@/api/asset' import { useRouter } from 'vue-router' const router = useRouter() const generating = ref(false) const latestAsset = ref(null) const form = reactive({ name: '', positivePrompt: '', negativePrompt: '', sampler: 'DPM++ 2M Karras', steps: 20, cfgScale: 7.5, seed: null, width: 512, height: 512, selectedTags: [] }) const handleGenerate = async () => { if (!form.positivePrompt.trim()) { ElMessage.warning('请输入正向提示词') return } generating.value = true try { const res = await generateAsset(form) latestAsset.value = res.data ElMessage.success('素材生成并保存成功!') // 可以触发全局事件,通知素材列表页刷新 } catch (error) { ElMessage.error('生成失败:' + error.message) } finally { generating.value = false } } const goToDetail = (assetId) => { router.push({ name: 'AssetDetail', params: { id: assetId } }) } </script>2. 素材列表/搜索页 (AssetListPage.vue)这是素材库的“主页”,支持按多种条件筛选和搜索。
<template> <div class="asset-list-page"> <el-card> <template #header> <div class="list-header"> <span>像素素材库</span> <div> <el-input v-model="searchQuery" placeholder="搜索提示词、描述或标签..." style="width: 300px;" @keyup.enter="loadData"> <template #append> <el-button :icon="Search" @click="loadData" /> </template> </el-input> <el-button type="primary" :icon="Plus" @click="goToGenerate" style="margin-left: 10px;">生成新素材</el-button> </div> </div> </template> <!-- 筛选条件 --> <el-form :inline="true" :model="filters"> <el-form-item label="标签"> <el-select v-model="filters.tagIds" multiple collapse-tags placeholder="选择标签"> <el-option v-for="tag in allTags" :key="tag.id" :label="tag.name" :value="tag.id"></el-option> </el-select> </el-form-item> <el-form-item label="作者"> <el-select v-model="filters.authorId" placeholder="选择作者"> <!-- 作者选项 --> </el-select> </el-form-item> <el-form-item label="排序"> <el-select v-model="filters.sortBy" placeholder="排序方式"> <el-option label="最新创建" value="createTime_desc"></el-option> <el-option label="最多收藏" value="favoriteCount_desc"></el-option> <el-option label="最多下载" value="downloadCount_desc"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">筛选</el-button> <el-button @click="resetFilters">重置</el-button> </el-form-item> </el-form> <!-- 素材网格列表 --> <div v-loading="loading"> <el-empty v-if="assetList.length === 0" description="暂无素材" /> <el-row :gutter="20" v-else> <el-col :xs="12" :sm="8" :md="6" :lg="4" v-for="asset in assetList" :key="asset.id"> <AssetCard :asset="asset" @click-card="goToDetail(asset.id)" /> </el-col> </el-row> <!-- 分页 --> <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total" layout="total, prev, pager, next, jumper" @current-change="handlePageChange" style="margin-top: 20px; justify-content: center;" /> </div> </el-card> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { Search, Plus } from '@element-plus/icons-vue' import { useRouter } from 'vue-router' import { getAssetList, getAllTags } from '@/api/asset' import AssetCard from '@/components/AssetCard.vue' const router = useRouter() const loading = ref(false) const assetList = ref([]) const allTags = ref([]) const searchQuery = ref('') const currentPage = ref(1) const pageSize = ref(24) const total = ref(0) const filters = reactive({ tagIds: [], authorId: null, sortBy: 'createTime_desc' }) const loadData = async () => { loading.value = true try { const params = { page: currentPage.value, size: pageSize.value, keyword: searchQuery.value, ...filters } const res = await getAssetList(params) assetList.value = res.data.list total.value = res.data.total } catch (error) { console.error('加载素材列表失败', error) } finally { loading.value = false } } const goToDetail = (id) => { router.push({ name: 'AssetDetail', params: { id } }) } const goToGenerate = () => { router.push({ name: 'Generate' }) } onMounted(() => { loadData() // 加载所有标签用于筛选 getAllTags().then(res => allTags.value = res.data) }) </script>3. 素材详情页 (AssetDetailPage.vue)展示素材的完整信息,并提供“复用生成”等操作。
<template> <div class="asset-detail-page" v-loading="loading"> <el-card v-if="asset"> <template #header> <div class="detail-header"> <h2>{{ asset.name }}</h2> <div> <el-button type="primary" @click="handleReuse">复用此参数生成</el-button> <el-button :icon="Download" @click="handleDownload">下载原图</el-button> <el-button :icon="Star" :type="isFavorited ? 'warning' : ''" @click="toggleFavorite"> {{ isFavorited ? '已收藏' : '收藏' }} ({{ asset.favoriteCount }}) </el-button> </div> </div> </template> <el-row :gutter="40"> <el-col :span="12"> <el-image :src="asset.imageUrl" fit="contain" style="max-height: 70vh;" :preview-src-list="[asset.imageUrl]" /> </el-col> <el-col :span="12"> <el-descriptions title="素材信息" :column="1" border> <el-descriptions-item label="描述">{{ asset.description || '无' }}</el-descriptions-item> <el-descriptions-item label="作者">用户 {{ asset.authorId }}</el-descriptions-item> <el-descriptions-item label="创建时间">{{ formatTime(asset.createTime) }}</el-descriptions-item> <el-descriptions-item label="标签"> <el-tag v-for="tag in asset.tags" :key="tag.id" style="margin-right: 5px;">{{ tag.name }}</el-tag> </el-descriptions-item> <el-descriptions-item label="查看/下载/收藏"> {{ asset.viewCount }} / {{ asset.downloadCount }} / {{ asset.favoriteCount }} </el-descriptions-item> </el-descriptions> <el-card style="margin-top: 20px;" header="生成参数"> <el-descriptions :column="1"> <el-descriptions-item label="模型">{{ asset.generationRecord.modelName }}</el-descriptions-item> <el-descriptions-item label="正向提示词"> <pre style="background: #f5f7fa; padding: 10px; border-radius: 4px;">{{ asset.generationRecord.positivePrompt }}</pre> </el-descriptions-item> <el-descriptions-item label="负向提示词">{{ asset.generationRecord.negativePrompt || '无' }}</el-descriptions-item> <el-descriptions-item label="采样器/步数/CFG"> {{ asset.generationRecord.sampler }} / {{ asset.generationRecord.steps }}步 / CFG {{ asset.generationRecord.cfgScale }} </el-descriptions-item> <el-descriptions-item label="种子/分辨率"> 种子:{{ asset.generationRecord.seed }} / {{ asset.generationRecord.width }}x{{ asset.generationRecord.height }} </el-descriptions-item> </el-descriptions> </el-card> </el-col> </el-row> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Download, Star } from '@element-plus/icons-vue' import { getAssetDetail, downloadAsset, toggleFavoriteAsset } from '@/api/asset' import { formatTime } from '@/utils' const route = useRoute() const router = useRouter() const loading = ref(false) const asset = ref(null) const isFavorited = ref(false) const loadDetail = async () => { loading.value = true try { const res = await getAssetDetail(route.params.id) asset.value = res.data // 这里可以根据用户信息判断是否已收藏 // isFavorited.value = ... } catch (error) { console.error('加载素材详情失败', error) } finally { loading.value = false } } const handleReuse = () => { // 跳转到生成页,并携带当前素材的参数 router.push({ name: 'Generate', query: { reuse: asset.value.id } }) } const handleDownload = async () => { try { await downloadAsset(asset.value.id) // 更新下载计数 asset.value.downloadCount += 1 } catch (error) { console.error('下载失败', error) } } const toggleFavorite = async () => { try { await toggleFavoriteAsset(asset.value.id) isFavorited.value = !isFavorited.value asset.value.favoriteCount += isFavorited.value ? 1 : -1 } catch (error) { console.error('收藏操作失败', error) } } onMounted(() => { loadDetail() }) </script>5.2 前后端联调关键点
- API接口定义:前后端需要约定好API的路径、方法、请求和响应格式。可以使用Swagger/OpenAPI生成文档。
- 跨域问题:在SpringBoot后端通过
@CrossOrigin注解或配置全局CORS过滤器解决。 - 文件上传/下载:图片上传到后端后,需要配置静态资源映射,让前端能通过URL访问。下载接口需要正确设置响应头。
- 状态管理:对于用户登录状态、收藏状态等,可以使用Vuex或Pinia进行管理。
- 异步加载与反馈:生成图片是耗时操作,前端需要提供明确的加载状态(如按钮loading、进度条),并考虑使用WebSocket或长轮询来获取生成结果。
6. 总结
走完这一整套流程,我们从零开始搭建了一个功能完整的像素画素材管理系统。回过头看,它的价值远不止是“又一个管理后台”。它真正解决的是AI创作从个人工具到团队资产的转化问题。
通过SpringBoot和MySQL,我们构建了一个稳定、结构清晰的数据后台,每一张生成的像素画都带着它完整的“基因序列”(提示词和参数)被妥善归档。前端界面则让搜索、筛选和复用这些素材变得像在电商网站购物一样简单直观。最核心的,是我们把Qwen-Image-2512-Pixel-Art-LoRA模型的生成能力,无缝地编织进了这个“生成-管理-复用”的工作流里,形成了一个创意生产的闭环。
实际用下来,这套方案对于小团队或者个人创作者来说,已经能解决大部分素材管理混乱的痛点了。你可以基于这个基础,继续添砖加瓦,比如加入用户权限管理、素材审核流程、更高级的以图搜图功能,或者对接更多的AI模型。
技术实现本身并不复杂,难能可贵的是这种将工具串联起来,服务于实际工作流的思路。希望这个项目能给你带来一些启发,无论是直接使用,还是作为你构思自己AI应用集成方案的蓝本。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
