SpringBoot3+Vue3全栈开发漫画阅读平台:多端适配与前后端分离实践
2026/9/25 0:05:36 网站建设 项目流程

最近在开发漫画阅读类应用时,发现很多开发者对多端适配和前后端分离架构的实现存在困惑。本文将以"漫小天漫画阅读平台"为例,完整分享基于SpringBoot3+Vue3的全栈开发方案,涵盖Web端、微信小程序端的统一架构设计。

这套方案采用前后端分离模式,后端使用SpringBoot3提供统一的REST API,前端通过Vue3实现Web管理端,微信小程序端则基于uni-app框架开发。学完本文后,你将掌握多端漫画平台的核心开发技能,能够独立完成从数据库设计到前后端联调的完整流程。

1. 项目架构与技术选型

1.1 整体架构设计

漫小天漫画平台采用典型的前后端分离架构,后端API服务统一为Web端和小程序端提供数据支持。这种架构的优势在于业务逻辑统一、便于维护和扩展。

前端层(用户界面) ├── Web管理端(Vue3 + Element Plus) - 供管理员使用 └── 微信小程序端(uni-app + Vue3) - 供终端用户使用 后端层(业务逻辑) └── SpringBoot3 API服务 ├── 用户管理模块 ├── 漫画管理模块 ├── 阅读记录模块 └── 文件存储模块 数据层(数据持久化) └── MySQL + Redis(缓存)

1.2 技术栈说明

后端技术栈:

  • SpringBoot 3.x:现代Spring框架,支持JDK17+
  • MyBatis Plus 3.5+:简化数据库操作
  • MySQL 8.0:主数据库
  • Redis 7.0:缓存和会话管理
  • JWT:身份认证
  • Maven:依赖管理

前端技术栈:

  • Vue 3.2+:组合式API,更好的TypeScript支持
  • Vite 4.0+:快速构建工具
  • Element Plus:UI组件库
  • Axios:HTTP客户端
  • Vue Router:路由管理

小程序技术栈:

  • uni-app 3.0+:跨端开发框架
  • Vue 3:语法一致性
  • uni-ui:小程序UI组件

2. 开发环境准备

2.1 基础环境配置

在开始项目开发前,需要确保本地环境满足以下要求:

操作系统要求:

  • Windows 10/11 或 macOS 10.15+ 或 Ubuntu 18.04+
  • 至少8GB内存,推荐16GB
  • 至少20GB可用磁盘空间

开发工具安装:

# 安装Node.js(前端开发) node -v # 要求版本16.0+ npm -v # 要求版本8.0+ # 安装Java开发环境 java -version # 要求JDK17+ mvn -v # 要求Maven 3.6+ # 安装数据库 mysql --version # 要求MySQL 8.0+ redis-server --version # 要求Redis 7.0+

2.2 IDE和工具配置

推荐使用以下开发工具组合:

后端开发:

  • IntelliJ IDEA Ultimate:强大的Java IDE
  • 安装插件:Lombok、MyBatisX、Spring Assistant

前端开发:

  • VS Code:轻量级代码编辑器
  • 安装扩展:Volar、TypeScript Vue Plugin、Element Plus Helper

小程序开发:

  • HBuilder X:uni-app官方IDE
  • 微信开发者工具:小程序调试和发布

3. 数据库设计与建模

3.1 核心表结构设计

漫画平台的核心业务涉及用户、漫画、章节、阅读记录等实体,以下是关键表的设计:

-- 用户表 CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(200) DEFAULT NULL COMMENT '头像', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 漫画表 CREATE TABLE `comic` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '漫画标题', `author` varchar(50) DEFAULT NULL COMMENT '作者', `cover_image` varchar(200) DEFAULT NULL COMMENT '封面图', `description` text COMMENT '描述', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `status` tinyint DEFAULT '1' COMMENT '状态:1-连载中 2-已完结', `view_count` int DEFAULT '0' COMMENT '浏览量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 漫画章节表 CREATE TABLE `comic_chapter` ( `id` bigint NOT NULL AUTO_INCREMENT, `comic_id` bigint NOT NULL COMMENT '漫画ID', `chapter_number` int NOT NULL COMMENT '章节编号', `title` varchar(100) NOT NULL COMMENT '章节标题', `page_count` int DEFAULT '0' COMMENT '页数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_comic_id` (`comic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 阅读记录表 CREATE TABLE `reading_history` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '用户ID', `comic_id` bigint NOT NULL COMMENT '漫画ID', `chapter_id` bigint NOT NULL COMMENT '章节ID', `current_page` int DEFAULT '1' COMMENT '当前阅读页数', `read_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间', PRIMARY KEY (`id`), KEY `idx_user_comic` (`user_id`,`comic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.2 索引优化策略

为了提高查询性能,需要为常用查询字段添加合适的索引:

-- 为漫画表添加分类索引 ALTER TABLE `comic` ADD INDEX `idx_category_status` (`category_id`, `status`); -- 为章节表添加漫画和章节号索引 ALTER TABLE `comic_chapter` ADD INDEX `idx_comic_chapter` (`comic_id`, `chapter_number`); -- 为阅读记录表添加时间索引 ALTER TABLE `reading_history` ADD INDEX `idx_user_time` (`user_id`, `read_time`);

4. SpringBoot3后端实现

4.1 项目结构规划

采用标准的分层架构,确保代码的可维护性:

src/main/java/com/mantian/comic/ ├── config/ # 配置类 ├── controller/ # 控制层 ├── service/ # 业务层 ├── mapper/ # 数据访问层 ├── entity/ # 实体类 ├── dto/ # 数据传输对象 ├── common/ # 通用组件 └── Application.java # 启动类

4.2 核心依赖配置

在pom.xml中配置SpringBoot3和必要依赖:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.0</version> <relativePath/> </parent> <groupId>com.mantian</groupId> <artifactId>comic-platform</artifactId> <version>1.0.0</version> <properties> <java.version>17</java.version> <mybatis-plus.version>3.5.3</mybatis-plus.version> </properties> <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> </dependencies> </project>

4.3 数据层实现

使用MyBatis Plus简化数据库操作,首先配置实体类:

// 漫画实体类 @Data @TableName("comic") public class Comic { @TableId(type = IdType.AUTO) private Long id; private String title; private String author; private String coverImage; private String description; private Long categoryId; private Integer status; private Integer viewCount; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; } // Mapper接口 public interface ComicMapper extends BaseMapper<Comic> { @Select("SELECT * FROM comic WHERE category_id = #{categoryId} ORDER BY view_count DESC LIMIT #{limit}") List<Comic> selectHotComicsByCategory(@Param("categoryId") Long categoryId, @Param("limit") Integer limit); }

4.4 业务层实现

实现漫画相关的业务逻辑:

@Service public class ComicService { @Autowired private ComicMapper comicMapper; @Autowired private RedisTemplate<String, Object> redisTemplate; public Page<Comic> getComicList(ComicQueryDTO queryDTO) { Page<Comic> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); LambdaQueryWrapper<Comic> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(queryDTO.getKeyword())) { wrapper.like(Comic::getTitle, queryDTO.getKeyword()); } if (queryDTO.getCategoryId() != null) { wrapper.eq(Comic::getCategoryId, queryDTO.getCategoryId()); } wrapper.orderByDesc(Comic::getViewCount); return comicMapper.selectPage(page, wrapper); } @Cacheable(value = "comic", key = "#id") public Comic getComicDetail(Long id) { Comic comic = comicMapper.selectById(id); if (comic != null) { // 增加浏览量 comicMapper.updateViewCount(id); } return comic; } }

4.5 控制层实现

提供REST API接口:

@RestController @RequestMapping("/api/comic") @Validated public class ComicController { @Autowired private ComicService comicService; @GetMapping("/list") public Result<Page<Comic>> getComicList(@Valid ComicQueryDTO queryDTO) { Page<Comic> page = comicService.getComicList(queryDTO); return Result.success(page); } @GetMapping("/detail/{id}") public Result<Comic> getComicDetail(@PathVariable Long id) { Comic comic = comicService.getComicDetail(id); return Result.success(comic); } @PostMapping("/{id}/view") public Result<Void> increaseViewCount(@PathVariable Long id) { comicService.increaseViewCount(id); return Result.success(); } } // 统一返回结果封装 @Data public class Result<T> { private Integer code; private String message; private T data; private Long timestamp; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); result.setTimestamp(System.currentTimeMillis()); return result; } }

5. Vue3前端管理端实现

5.1 项目初始化

使用Vite创建Vue3项目:

npm create vue@latest comic-admin cd comic-admin npm install

安装必要依赖:

npm install element-plus @element-plus/icons-vue npm install axios vue-router@4 pinia npm install sass -D

5.2 路由配置

配置前端路由:

// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '仪表板' } }, { path: '/comic', name: 'Comic', component: () => import('@/views/comic/ComicList.vue'), meta: { title: '漫画管理' } }, { path: '/comic/add', name: 'ComicAdd', component: () => import('@/views/comic/ComicAdd.vue'), meta: { title: '添加漫画' } }, { path: '/comic/edit/:id', name: 'ComicEdit', component: () => import('@/views/comic/ComicEdit.vue'), meta: { title: '编辑漫画' } } ] const router = createRouter({ history: createWebHistory(), routes }) export default router

5.3 状态管理

使用Pinia进行状态管理:

// stores/comic.js import { defineStore } from 'pinia' export const useComicStore = defineStore('comic', { state: () => ({ comicList: [], currentComic: null, loading: false, pagination: { page: 1, pageSize: 10, total: 0 } }), actions: { async fetchComicList(params = {}) { this.loading = true try { const response = await api.getComicList({ page: this.pagination.page, pageSize: this.pagination.pageSize, ...params }) this.comicList = response.data.records this.pagination.total = response.data.total } catch (error) { console.error('获取漫画列表失败:', error) } finally { this.loading = false } }, async fetchComicDetail(id) { try { const response = await api.getComicDetail(id) this.currentComic = response.data } catch (error) { console.error('获取漫画详情失败:', error) } } } })

5.4 漫画列表组件

实现漫画管理界面:

<template> <div class="comic-list"> <el-card> <template #header> <div class="card-header"> <span>漫画管理</span> <el-button type="primary" @click="handleAdd">添加漫画</el-button> </div> </template> <!-- 搜索条件 --> <el-form :model="queryParams" inline> <el-form-item label="关键词"> <el-input v-model="queryParams.keyword" placeholder="请输入漫画标题" /> </el-form-item> <el-form-item label="分类"> <el-select v-model="queryParams.categoryId" placeholder="请选择分类"> <el-option label="全部" value="" /> <el-option v-for="category in categoryList" :key="category.id" :label="category.name" :value="category.id" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">搜索</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <!-- 数据表格 --> <el-table :data="comicStore.comicList" v-loading="comicStore.loading"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="coverImage" label="封面" width="100"> <template #default="{ row }"> <el-image :src="row.coverImage" :preview-src-list="[row.coverImage]" fit="cover" style="width: 60px; height: 80px;" /> </template> </el-table-column> <el-table-column prop="title" label="标题" min-width="200" /> <el-table-column prop="author" label="作者" width="120" /> <el-table-column prop="viewCount" label="浏览量" width="100" /> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.status === 1 ? 'success' : 'info'"> {{ row.status === 1 ? '连载中' : '已完结' }} </el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="200" fixed="right"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <div class="pagination"> <el-pagination v-model:current-page="comicStore.pagination.page" v-model:page-size="comicStore.pagination.pageSize" :total="comicStore.pagination.total" @current-change="handlePageChange" layout="total, sizes, prev, pager, next, jumper" /> </div> </el-card> </div> </template> <script setup> import { onMounted, reactive } from 'vue' import { useRouter } from 'vue-router' import { useComicStore } from '@/stores/comic' import { ElMessage, ElMessageBox } from 'element-plus' const router = useRouter() const comicStore = useComicStore() const queryParams = reactive({ keyword: '', categoryId: '' }) onMounted(() => { comicStore.fetchComicList() }) const handleSearch = () => { comicStore.pagination.page = 1 comicStore.fetchComicList(queryParams) } const handleReset = () => { Object.keys(queryParams).forEach(key => { queryParams[key] = '' }) handleSearch() } const handleAdd = () => { router.push('/comic/add') } const handleEdit = (row) => { router.push(`/comic/edit/${row.id}`) } const handleDelete = async (row) => { try { await ElMessageBox.confirm('确定删除该漫画吗?', '提示', { type: 'warning' }) // 调用删除API await api.deleteComic(row.id) ElMessage.success('删除成功') comicStore.fetchComicList() } catch (error) { if (error !== 'cancel') { ElMessage.error('删除失败') } } } const handlePageChange = (page) => { comicStore.pagination.page = page comicStore.fetchComicList(queryParams) } </script>

6. uni-app微信小程序端开发

6.1 项目创建与配置

使用HBuilder X创建uni-app项目:

// manifest.json 配置文件 { "name": "漫小天漫画", "appid": "__UNI__XXXXXX", "description": "漫画阅读小程序", "versionName": "1.0.0", "versionCode": "100", "transformPx": false, "app-plus": { "usingComponents": true }, "mp-weixin": { "appid": "wxxxxxxxxxxxxxxx", "setting": { "urlCheck": false }, "usingComponents": true, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } } } }

6.2 小程序页面结构

实现小程序首页:

<template> <view class="container"> <!-- 搜索栏 --> <view class="search-bar"> <u-search v-model="searchKeyword" placeholder="搜索漫画" @search="handleSearch" @clear="handleClearSearch" /> </view> <!-- 轮播图 --> <swiper class="banner-swiper" indicator-dots autoplay circular> <swiper-item v-for="banner in bannerList" :key="banner.id"> <image :src="banner.image" mode="aspectFill" @click="handleBannerClick(banner)"/> </swiper-item> </swiper> <!-- 分类导航 --> <view class="category-nav"> <scroll-view class="nav-scroll" scroll-x> <view v-for="category in categoryList" :key="category.id" :class="['nav-item', activeCategory === category.id ? 'active' : '']" @click="handleCategoryChange(category.id)" > {{ category.name }} </view> </scroll-view> </view> <!-- 漫画列表 --> <view class="comic-list"> <view class="section-title">热门推荐</view> <view class="comic-grid"> <view v-for="comic in comicList" :key="comic.id" class="comic-item" @click="handleComicClick(comic)" > <image class="comic-cover" :src="comic.coverImage" mode="aspectFill" /> <view class="comic-info"> <text class="comic-title">{{ comic.title }}</text> <text class="comic-author">{{ comic.author }}</text> <view class="comic-stats"> <text class="view-count">🔥 {{ comic.viewCount }}</text> <text class="status">{{ comic.status === 1 ? '连载中' : '完结' }}</text> </view> </view> </view> </view> </view> <!-- 加载更多 --> <view class="load-more" v-if="hasMore"> <u-loadmore status="loading" /> </view> <view class="no-more" v-else> <text>没有更多数据了</text> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app' const searchKeyword = ref('') const activeCategory = ref(0) const comicList = ref([]) const bannerList = ref([]) const categoryList = ref([]) const currentPage = ref(1) const hasMore = ref(true) const loading = ref(false) onMounted(() => { loadInitialData() }) // 加载初始数据 const loadInitialData = async () => { await Promise.all([ loadBanners(), loadCategories(), loadComicList() ]) } // 加载轮播图 const loadBanners = async () => { try { const res = await uni.request({ url: '/api/banner/list', method: 'GET' }) bannerList.value = res.data.data } catch (error) { console.error('加载轮播图失败:', error) } } // 加载分类 const loadCategories = async () => { try { const res = await uni.request({ url: '/api/category/list', method: 'GET' }) categoryList.value = res.data.data } catch (error) { console.error('加载分类失败:', error) } } // 加载漫画列表 const loadComicList = async (reset = false) => { if (loading.value) return loading.value = true try { const page = reset ? 1 : currentPage.value const res = await uni.request({ url: '/api/comic/list', method: 'GET', data: { page, pageSize: 10, categoryId: activeCategory.value || undefined, keyword: searchKeyword.value || undefined } }) const newList = res.data.data.records if (reset) { comicList.value = newList } else { comicList.value = [...comicList.value, ...newList] } hasMore.value = res.data.data.current < res.data.data.pages currentPage.value = page + 1 } catch (error) { console.error('加载漫画列表失败:', error) } finally { loading.value = false uni.stopPullDownRefresh() } } // 搜索处理 const handleSearch = () => { currentPage.value = 1 loadComicList(true) } const handleClearSearch = () => { searchKeyword.value = '' handleSearch() } // 分类切换 const handleCategoryChange = (categoryId) => { activeCategory.value = categoryId currentPage.value = 1 loadComicList(true) } // 漫画点击 const handleComicClick = (comic) => { uni.navigateTo({ url: `/pages/comic/detail?id=${comic.id}` }) } // 上拉加载更多 onReachBottom(() => { if (hasMore.value && !loading.value) { loadComicList() } }) // 下拉刷新 onPullDownRefresh(() => { currentPage.value = 1 loadComicList(true) }) </script> <style scoped> .container { padding: 20rpx; background-color: #f5f5f5; } .search-bar { margin-bottom: 20rpx; } .banner-swiper { height: 300rpx; border-radius: 16rpx; overflow: hidden; margin-bottom: 30rpx; } .banner-swiper image { width: 100%; height: 100%; } .category-nav { margin-bottom: 30rpx; } .nav-scroll { white-space: nowrap; } .nav-item { display: inline-block; padding: 16rpx 32rpx; margin-right: 20rpx; background: #fff; border-radius: 32rpx; font-size: 28rpx; } .nav-item.active { background: #007aff; color: #fff; } .section-title { font-size: 32rpx; font-weight: bold; margin-bottom: 20rpx; } .comic-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; } .comic-item { background: #fff; border-radius: 16rpx; overflow: hidden; } .comic-cover { width: 100%; height: 300rpx; } .comic-info { padding: 20rpx; } .comic-title { display: block; font-size: 28rpx; font-weight: bold; margin-bottom: 8rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .comic-author { font-size: 24rpx; color: #666; margin-bottom: 12rpx; } .comic-stats { display: flex; justify-content: space-between; font-size: 22rpx; color: #999; } .load-more, .no-more { text-align: center; padding: 40rpx; color: #999; } </style>

7. 文件上传与存储方案

7.1 后端文件上传接口

实现漫画封面和章节图片的上传功能:

@RestController @RequestMapping("/api/upload") public class FileUploadController { @Value("${file.upload.path}") private String uploadPath; @Value("${file.access.url}") private String accessUrl; @PostMapping("/image") public Result<String> uploadImage(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } // 验证文件类型 String contentType = file.getContentType(); if (!contentType.startsWith("image/")) { return Result.error("只支持图片文件"); } // 生成文件名 String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + fileExtension; // 创建目录 File destDir = new File(uploadPath); if (!destDir.exists()) { destDir.mkdirs(); } // 保存文件 File destFile = new File(destDir, fileName); try { file.transferTo(destFile); String fileUrl = accessUrl + "/" + fileName; return Result.success(fileUrl); } catch (IOException e) { return Result.error("文件上传失败"); } } }

7.2 前端文件上传组件

实现通用的文件上传组件:

<template> <div class="upload-component"> <el-upload class="avatar-uploader" action="/api/upload/image" :show-file-list="false" :before-upload="beforeUpload" :on-success="handleSuccess" :on-error="handleError" > <img v-if="imageUrl" :src="imageUrl" class="avatar" /> <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> </el-upload> <div class="upload-tips">支持 JPG、PNG 格式,大小不超过 2MB</div> </div> </template> <script setup> import { ref } from 'vue' import { ElMessage } from 'element-plus' import { Plus } from '@element-plus/icons-vue' const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) const imageUrl = ref(props.modelValue) const beforeUpload = (file) => { const isJPGOrPNG = file.type === 'image/jpeg' || file.type === 'image/png' const isLt2M = file.size / 1024 / 1024 < 2 if (!isJPGOrPNG) { ElMessage.error('只能上传 JPG/PNG 格式的图片!') return false } if (!isLt2M) { ElMessage.error('图片大小不能超过 2MB!') return false } return true } const handleSuccess = (response) => { imageUrl.value = response.data emit('update:modelValue', response.data) ElMessage.success('上传成功') } const handleError = () => { ElMessage.error('上传失败,请重试') } </script> <style scoped> .avatar-uploader { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: border-color 0.3s; width: 178px; height: 178px; display: flex; align-items: center; justify-content: center; } .avatar-uploader:hover { border-color: #409eff; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; } .avatar { width: 100%; height: 100%; object-fit: cover; } .upload-tips { margin-top: 8px; color: #909399; font-size: 12px; } </style>

8. 性能优化与最佳实践

8.1 后端性能优化

数据库查询优化:

@Service public class ComicService { // 使用Redis缓存热门数据 @Cacheable(value = "hotComics", key = "#categoryId + ':' + #limit") public List<Comic> getHotComics(Long categoryId, Integer limit) { return comicMapper.selectHotComicsByCategory(categoryId, limit); } // 批量操作优化 @Transactional public void batchUpdateViewCount(List<Long> comicIds) { comicMapper.batchUpdateViewCount(comicIds); } } // MyBatis批量更新配置 @Mapper public interface ComicMapper { void batchUpdateViewCount(@Param("comicIds") List<Long> comicIds); }
<!-- batchUpdateViewCount SQL --> <update id="batchUpdateViewCount"> UPDATE comic SET view_count = view_count + 1 WHERE id IN <foreach collection="comicIds" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>

8.2 前端性能优化

图片懒加载优化:

<template> <img v-lazy="imageUrl" :alt="altText" class="lazy-image" /> </template> <script setup> // 自定义懒加载指令 const vLazy = { mounted(el, binding) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { el.src = binding.value observer.unobserve(el) } }) }) observer.observe(el) } } </script>

路由懒加载优化:

// 使用动态导入实现路由懒加载 const routes = [ { path: '/comic/detail', component: () => import(/* webpackChunkName: "comic-detail" */ '@/views/ComicDetail.vue') } ]

8.3 小程序优化技巧

图片优化策略:

<template> <image :src="imageUrl" mode="aspectFill" lazy-load :webp="supportWebp" @error="handleImageError" /> </template> <script setup> import { ref

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询