SpringBoot+Vue仓库管理系统实战:从零到部署的完整避坑指南(附源码)
SpringBootVue仓库管理系统实战从零到部署的完整避坑指南在当今快速发展的互联网时代企业信息化管理已成为提升运营效率的关键。仓库管理作为企业供应链中的重要环节其信息化程度直接影响着企业的库存周转率和运营成本。对于Java全栈开发学习者而言构建一个功能完善的仓库管理系统不仅能巩固技术栈知识更能为未来职场发展积累宝贵的实战经验。本文将带领读者从零开始使用SpringBoot 2.7和Vue 3技术栈构建一个具备完整CRUD功能、权限控制和数据可视化的仓库管理系统。不同于简单的教程式讲解我们将重点关注实际开发中容易遇到的坑点如前后端联调时的跨域问题、JWT认证的实现细节、Element Plus表格性能优化等确保读者能够获得可直接应用于生产环境的开发经验。1. 项目环境搭建与技术选型1.1 开发环境准备一个稳定的开发环境是项目成功的基础。推荐使用以下工具组合JDK 17SpringBoot 2.7.x的最佳Java版本支持Node.js 16Vue 3开发的基础运行时IntelliJ IDEAJava开发的首选IDEVisual Studio Code前端开发的轻量级编辑器MySQL 8.0关系型数据库选择环境配置常见问题解决方案# 检查Java版本 java -version # 解决Node.js版本冲突 nvm install 16.14.0 nvm use 16.14.01.2 技术栈深度解析后端技术栈选择依据SpringBoot 2.7.x提供了自动配置、快速启动的特性MyBatis-Plus 3.5.x简化了传统MyBatis的CRUD操作Hutool 5.8.xJava工具库提高开发效率JWT无状态认证方案适合前后端分离架构前端技术栈对比分析技术选项优势适用场景Vue 3组合式API更好的TypeScript支持复杂交互应用Element Plus丰富的组件库企业级UI后台管理系统Axios拦截器机制完善易于扩展HTTP请求处理2. 后端核心模块设计与实现2.1 数据库设计与优化合理的数据库设计是系统性能的基石。仓库管理系统的核心表包括物资表(material)存储物资基本信息库存表(stock)记录实时库存数量用户表(user)系统用户信息角色表(role)权限控制基础操作日志表(log)记录关键操作CREATE TABLE material ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 物资名称, type_id bigint NOT NULL COMMENT 物资类型, specification varchar(200) DEFAULT NULL COMMENT 规格型号, unit varchar(20) NOT NULL COMMENT 计量单位, status tinyint NOT NULL DEFAULT 1 COMMENT 状态(1可用 0不可用), create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type (type_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT物资信息表;2.2 SpringBoot关键配置详解application.yml配置要点spring: datasource: url: jdbc:mysql://localhost:3306/warehouse?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: database: 0 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值跨域问题解决方案Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600) .allowedHeaders(*); } }3. 前端工程化与性能优化3.1 Vue 3项目结构设计现代前端项目需要合理的目录结构来保证可维护性src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── composables/ # 组合式函数 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue # 根组件 └── main.js # 入口文件3.2 Element Plus表格性能优化大型数据表格渲染是后台系统的性能瓶颈之一。以下是优化方案template el-table :datatableData stylewidth: 100% heightcalc(100vh - 200px) v-loadingloading :row-keygetRowKey sort-changehandleSortChange el-table-column propid labelID width80 sortable / !-- 其他列定义 -- /el-table /template script setup import { ref } from vue import { useMaterialStore } from /stores/material const materialStore useMaterialStore() const loading ref(false) const fetchData async () { loading.value true try { await materialStore.fetchMaterials() } finally { loading.value false } } fetchData() /script4. 系统部署与持续集成4.1 生产环境部署方案后端部署脚本示例#!/bin/bash # 打包应用 mvn clean package -DskipTests # 构建Docker镜像 docker build -t warehouse-backend:1.0.0 . # 运行容器 docker run -d -p 8080:8080 \ -e SPRING_DATASOURCE_URLjdbc:mysql://mysql-server:3306/warehouse \ -e SPRING_DATASOURCE_USERNAMEprod_user \ -e SPRING_DATASOURCE_PASSWORDsecure_password \ --name warehouse-backend \ warehouse-backend:1.0.0前端Nginx配置优化server { listen 80; server_name warehouse.example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 开启gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; } location /api { proxy_pass http://backend-server:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }4.2 常见部署问题排查前端路由404问题确保Nginx配置了try_files $uri $uri/ /index.html检查Vue Router是否使用了history模式数据库连接失败验证数据库服务是否正常运行检查连接字符串中的用户名密码是否正确确认数据库用户有远程连接权限跨域问题再现生产环境应通过Nginx反向代理避免跨域检查响应头中是否包含Access-Control-Allow-Origin// SpringBoot中检查CORS配置是否生效 RestController RequestMapping(/test) public class TestController { GetMapping public String test() { return CORS test successful; } }5. 项目进阶与扩展方向5.1 微服务架构改造随着业务规模扩大单体架构可能面临性能瓶颈。可考虑以下拆分方案用户服务处理认证授权、用户管理库存服务核心库存管理功能报表服务数据分析与报表生成通知服务处理系统通知和消息5.2 高级功能实现库存预警功能代码示例Service RequiredArgsConstructor public class StockAlertService { private final StockMapper stockMapper; private final MessageProducer messageProducer; Scheduled(cron 0 0 9 * * ?) // 每天上午9点执行 public void checkStockLevel() { ListStockAlertDTO alerts stockMapper.selectLowStockMaterials(); alerts.forEach(alert - { if (alert.getCurrentStock() alert.getSafetyStock()) { messageProducer.sendStockAlert(alert); } }); } }数据可视化方案对比方案优点缺点适用场景ECharts功能强大图表类型丰富学习曲线较陡复杂数据展示Chart.js简单易用轻量级功能相对简单基础图表需求D3.js高度自定义灵活性高开发成本高特殊可视化需求6. 开发效率提升技巧6.1 代码生成器应用MyBatis-Plus代码生成器配置示例public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create(jdbc:mysql://localhost:3306/warehouse, root, 123456) .globalConfig(builder - { builder.author(Developer) // 设置作者 .outputDir(System.getProperty(user.dir) /src/main/java); // 输出目录 }) .packageConfig(builder - { builder.parent(com.example.warehouse) // 父包名 .moduleName(system) // 模块名 .entity(entity) .service(service) .controller(controller); }) .strategyConfig(builder - { builder.addInclude(material, stock) // 包含的表名 .entityBuilder() .enableLombok() .controllerBuilder() .enableRestStyle(); }) .execute(); } }6.2 前端组件封装策略通用表格组件封装示例template div classpro-table div classtoolbar slot nametoolbar/slot el-button v-ifshowAdd typeprimary clickhandleAdd 新增 /el-button /div el-table :datatableData v-bind$attrs v-loadingloading slot/slot el-table-column v-ifshowAction label操作 width180 template #defaultscope el-button sizesmall clickhandleEdit(scope.row) 编辑 /el-button el-button sizesmall typedanger clickhandleDelete(scope.row) 删除 /el-button /template /el-table-column /el-table el-pagination v-ifshowPagination :current-pagecurrentPage :page-sizepageSize :totaltotal current-changehandlePageChange / /div /template script setup defineProps({ tableData: { type: Array, default: () [] }, loading: Boolean, showAdd: { type: Boolean, default: true }, showAction: { type: Boolean, default: true }, showPagination: { type: Boolean, default: true }, currentPage: { type: Number, default: 1 }, pageSize: { type: Number, default: 10 }, total: { type: Number, default: 0 } }) const emit defineEmits([add, edit, delete, page-change]) const handleAdd () emit(add) const handleEdit (row) emit(edit, row) const handleDelete (row) emit(delete, row) const handlePageChange (page) emit(page-change, page) /script7. 安全防护最佳实践7.1 常见Web安全漏洞防护XSS防护方案前端使用vue-dompurify对富文本内容进行净化后端设置HTTP头X-XSS-Protection: 1; modeblock所有用户输入进行严格的验证和转义CSRF防护实现Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() .antMatchers(/api/auth/**).permitAll() .anyRequest().authenticated(); } }7.2 接口安全增强措施接口签名验证拦截器Component public class ApiSignInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String sign request.getHeader(X-API-SIGN); String timestamp request.getHeader(X-API-TIMESTAMP); // 验证时间戳有效性防止重放攻击 if (System.currentTimeMillis() - Long.parseLong(timestamp) 300000) { throw new ApiException(请求已过期); } // 验证签名 String expectedSign generateSign(request, timestamp); if (!expectedSign.equals(sign)) { throw new ApiException(签名验证失败); } return true; } private String generateSign(HttpServletRequest request, String timestamp) { // 实际项目中应使用更复杂的签名算法 String path request.getRequestURI(); String method request.getMethod(); String secret your_api_secret; return DigestUtils.md5DigestAsHex( (path method timestamp secret).getBytes() ); } }8. 性能监控与优化8.1 SpringBoot应用监控Actuator配置与端点保护management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always prometheus: enabled: true metrics: export: prometheus: enabled: true自定义健康检查指标Component public class DatabaseHealthIndicator implements HealthIndicator { private final DataSource dataSource; public DatabaseHealthIndicator(DataSource dataSource) { this.dataSource dataSource; } Override public Health health() { try (Connection connection dataSource.getConnection()) { if (connection.isValid(1000)) { return Health.up().withDetail(database, Available).build(); } } catch (SQLException e) { return Health.down().withException(e).build(); } return Health.unknown().build(); } }8.2 前端性能监控方案使用Sentry进行错误追踪import * as Sentry from sentry/vue import { Integrations } from sentry/tracing export function setupSentry(app) { Sentry.init({ app, dsn: your_dsn_here, integrations: [ new Integrations.BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation(router), tracingOrigins: [localhost, your-production-domain.com], }), ], tracesSampleRate: 0.2, environment: process.env.NODE_ENV, }) }性能指标采集代码export function trackPerf() { const observer new PerformanceObserver((list) { for (const entry of list.getEntries()) { console.log([Performance], entry.name, entry.duration) // 实际项目中应发送到监控服务器 } }) observer.observe({ entryTypes: [navigation, resource, paint] }) }9. 测试策略与质量保障9.1 后端测试金字塔实践单元测试示例JUnit 5 MockitoExtendWith(MockitoExtension.class) class MaterialServiceTest { Mock private MaterialMapper materialMapper; InjectMocks private MaterialServiceImpl materialService; Test void shouldReturnMaterialWhenValidIdGiven() { // Arrange Long materialId 1L; Material mockMaterial new Material(); mockMaterial.setId(materialId); mockMaterial.setName(Test Material); when(materialMapper.selectById(materialId)).thenReturn(mockMaterial); // Act Material result materialService.getById(materialId); // Assert assertNotNull(result); assertEquals(materialId, result.getId()); verify(materialMapper).selectById(materialId); } }集成测试配置SpringBootTest AutoConfigureMockMvc TestPropertySource(locations classpath:test.properties) Transactional class MaterialControllerIT { Autowired private MockMvc mockMvc; Autowired private ObjectMapper objectMapper; Test void shouldCreateMaterialWhenValidInputGiven() throws Exception { MaterialCreateDTO dto new MaterialCreateDTO(); dto.setName(New Material); dto.setTypeId(1L); dto.setUnit(个); mockMvc.perform(post(/api/materials) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(dto))) .andExpect(status().isCreated()) .andExpect(jsonPath($.data.name).value(New Material)); } }9.2 前端测试方案组件测试示例Vitest Testing Libraryimport { render, screen } from testing-library/vue import MaterialTable from ./MaterialTable.vue import { describe, expect, it } from vitest describe(MaterialTable, () { it(renders material data correctly, async () { const materials [ { id: 1, name: Material 1, type: Type A }, { id: 2, name: Material 2, type: Type B } ] render(MaterialTable, { props: { data: materials, loading: false } }) expect(screen.getByText(Material 1)).toBeDefined() expect(screen.getByText(Type B)).toBeDefined() }) })E2E测试配置Cypressdescribe(Material Management, () { beforeEach(() { cy.login(admin, password123) cy.visit(/materials) }) it(should add new material, () { cy.get([data-testadd-button]).click() cy.get([data-testname-input]).type(New Test Material) cy.get([data-testtype-select]).click() cy.get(.el-select-dropdown__item).first().click() cy.get([data-testsubmit-button]).click() cy.contains(.el-notification, 创建成功).should(be.visible) cy.contains([data-testmaterial-name], New Test Material).should(exist) }) })10. 项目文档与协作规范10.1 API文档生成SpringDoc OpenAPI配置示例Configuration OpenAPIDefinition( info Info( title 仓库管理系统API文档, version 1.0.0, description 仓库管理系统后端API文档, contact Contact(name 开发团队, email devexample.com) ), servers { Server(url http://localhost:8080, description 本地环境), Server(url https://api.warehouse.com, description 生产环境) } ) public class OpenApiConfig { Bean public GroupedOpenApi publicApi() { return GroupedOpenApi.builder() .group(public) .pathsToMatch(/api/**) .build(); } }接口注释示例Operation(summary 获取物资详情, description 根据ID获取物资详细信息) ApiResponses({ ApiResponse(responseCode 200, description 成功返回物资数据), ApiResponse(responseCode 404, description 物资不存在) }) GetMapping(/{id}) public ResultMaterialVO getById( Parameter(description 物资ID) PathVariable Long id ) { MaterialVO material materialService.getById(id); return Result.success(material); }10.2 Git协作流程规范分支管理策略main生产环境对应分支受保护develop集成测试分支feature/*功能开发分支hotfix/*紧急修复分支Commit Message规范类型(范围): 简要描述 详细描述可选 BREAKING CHANGE: 重大变更说明可选常用类型feat新功能fix错误修复docs文档更新style代码格式调整refactor代码重构test测试相关chore构建过程或辅助工具的变动11. 移动端适配方案11.1 响应式布局实现Element Plus响应式断点配置// 覆盖默认断点 $--sm: 576px; $--md: 768px; $--lg: 992px; $--xl: 1200px; // 混合媒体查询 mixin respond-to($breakpoint) { if $breakpoint sm { media (max-width: $--sm) { content; } } else if $breakpoint md { media (max-width: $--md) { content; } } else if $breakpoint lg { media (max-width: $--lg) { content; } } else if $breakpoint xl { media (max-width: $--xl) { content; } } }表格响应式处理方案template el-table :datatableData stylewidth: 100% :class{ small-table: isMobile } el-table-column v-forcolumn in columns :keycolumn.prop v-bindcolumn :show-overflow-tooltipisMobile / /el-table /template script setup import { useWindowSize } from vueuse/core const { width } useWindowSize() const isMobile computed(() width.value 768) const columns ref([ { prop: id, label: ID, width: 80 }, { prop: name, label: 名称, minWidth: 120 }, // 其他列配置 ]) /script style scoped .small-table { font-size: 12px; :deep(.el-table__cell) { padding: 4px 0; } } /style11.2 PWA离线应用支持Vue PWA配置// vite.config.js import { defineConfig } from vite import { VitePWA } from vite-plugin-pwa export default defineConfig({ plugins: [ VitePWA({ registerType: autoUpdate, includeAssets: [favicon.ico, apple-touch-icon.png], manifest: { name: 仓库管理系统, short_name: 仓库管理, description: 企业级仓库管理解决方案, theme_color: #409EFF, icons: [ { src: /pwa-192x192.png, sizes: 192x192, type: image/png }, { src: /pwa-512x512.png, sizes: 512x512, type: image/png } ] }, workbox: { globPatterns: [**/*.{js,css,html,ico,png,svg}] } }) ] })Service Worker更新处理// src/utils/pwa-update.js import { register } from register-service-worker export function setupPWAUpdate() { if (serviceWorker in navigator) { register(/sw.js, { ready() { console.log(Service worker is active.) }, registered() { console.log(Service worker has been registered.) }, updated(registration) { console.log(New content is available; please refresh.) // 显示更新提示 ElMessageBox.confirm( 发现新版本是否立即更新, 更新提示, { confirmButtonText: 更新, cancelButtonText: 取消, type: info } ).then(() { registration.waiting.postMessage({ type: SKIP_WAITING }) window.location.reload() }) } }) // 监听controllerchange事件 navigator.serviceWorker.addEventListener(controllerchange, () { window.location.reload() }) } }12. 国际化与多语言支持12.1 Vue i18n集成方案基础配置// src/i18n.js import { createI18n } from vue-i18n import en from ./locales/en.json import zh from ./locales/zh.json const messages { en, zh } const i18n createI18n({ legacy: false, locale: localStorage.getItem(locale) || zh, fallbackLocale: en, messages }) export default i18nElement Plus语言包配置import { ElButton, ElPagination } from element-plus import zhCn from element-plus/es/locale/lang/zh-cn import en from element-plus/es/locale/lang/en export function setupElementPlusLocale(app) { const locale computed(() { return i18n.global.locale.value zh ? zhCn : en }) app.use(ElButton) app.use(ElPagination) app.config.globalProperties.$ELEMENT { locale } }12.2 后端国际化处理SpringBoot多语言配置Configuration public class I18nConfig { Bean public LocaleResolver localeResolver() { SessionLocaleResolver slr new SessionLocaleResolver(); slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return slr; } Bean public LocaleChangeInterceptor localeChangeInterceptor() { LocaleChangeInterceptor lci new LocaleChangeInterceptor(); lci.setParamName(lang); return lci; } Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource new ResourceBundleMessageSource(); messageSource.setBasename(i18n/messages); messageSource.setDefaultEncoding(UTF-8); messageSource.setUseCodeAsDefaultMessage(true); return messageSource; } }统一返回结果国际化public class ResultT implements Serializable { private int code; private String message; private T data; public static T ResultT success(T data) { return new Result(200, getMessage(200), data); } public static T ResultT error(int code) { return new Result(code, getMessage(code), null); } private static String getMessage(int code) { Locale locale LocaleContextHolder.getLocale(); return ApplicationContextHolder.getBean(MessageSource.class) .getMessage(result. code, null, locale); } }13. 第三方服务集成13.1 短信服务接入阿里云短信服务封装Service RequiredArgsConstructor public class SmsService { private final SmsProperties smsProperties; public void sendVerificationCode(String phone, String code) { Config config new Config() .setAccessKeyId(smsProperties.getAccessKeyId()) .setAccessKeySecret(smsProperties.getAccessKeySecret()); com.aliyun.dysmsapi20170525.Client client new com.aliyun.dysmsapi20170525.Client(config); SendSmsRequest request new SendSmsRequest() .setPhoneNumbers(phone) .setSignName(smsProperties.getSignName()) .setTemplateCode(smsProperties.getTemplateCode()) .setTemplateParam({\code\:\ code \}); try { SendSmsResponse response client.sendSms(request); if (!OK.equals(response.getBody().getCode())) { throw new SmsException(短信发送失败: response.getBody().getMessage()); } } catch (Exception e) { throw new SmsException(短信服务异常, e); } } }13.2 支付对接方案微信支付SDK封装Service public class WechatPayService { private final WechatPayProperties properties; private final WechatPayHttpClientBuilder builder; public WechatPayService(WechatPayProperties properties) { this.properties properties; this.builder WechatPayHttpClientBuilder.create() .withMerchant(properties.getMerchantId(), properties.getMerchantSerialNumber(), properties.getPrivateKey()) .withValidator(new WechatPay2Validator(properties.getApiV3Key())); } public PaymentResponse createOrder(PaymentRequest request) { HttpClient httpClient builder.build(); HttpPost httpPost new HttpPost(https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi); JSONObject body new JSONObject(); body.put(appid, properties.getAppId()); body.put(mchid, properties.getMerchantId()); body.put(description, request.getDescription()); body.put(out_trade_no, request.getOrderNo()); body.put(notify_url, properties.getNotifyUrl()); body.put(amount, new JSONObject() .put(total, request.getAmount()) .put(currency, CNY)); httpPost.setEntity(new StringEntity(body.toString(), StandardCharsets.UTF_8)); httpPost.setHeader(Accept, application/json); httpPost.setHeader(Content-type, application/json; charsetutf-8); try { HttpResponse response httpClient.execute(httpPost); String responseBody EntityUtils.toString(response.getEntity()); if (response.getStatusLine().getStatusCode() 200) { return JSON.parseObject(responseBody, PaymentResponse.class); } else { throw new PaymentException(微信支付创建订单失败: responseBody); } } catch (IOException e) { throw new PaymentException(微信支付请求异常, e); } } }14. 大数据分析与报表14.1 数据统计方案设计库存周转率计算逻辑Service RequiredArgsConstructor public class InventoryAnalysisService { private final StockMapper stockMapper; private final StockRecordMapper recordMapper; public InventoryTurnoverResult calculateTurnoverRate(LocalDate startDate, LocalDate endDate) { // 计算平均库存 BigDecimal avgInventory stockMapper.selectAvgInventory(startDate, endDate); // 计算销售成本 BigDecimal costOfGoodsSold recordMapper.selectCostOfGoodsSold(startDate, endDate); // 计算周转率 BigDecimal turnoverRate costOfGoodsSold.divide(avgInventory, 2, RoundingMode.HALF_UP); return new InventoryTurnoverResult(avgInventory, costOfGoodsSold, turnoverRate); } public ListMaterialUsage getTopUsedMaterials(int limit) { return recordMapper.selectTopUsedMaterials(limit).stream() .map(item - new MaterialUsage( item.getMaterialName(), item.getTotalQuantity(), item.getUnit() )) .collect(Collectors.toList()); } }14.2 ECharts可视化实现库存分析图表组件template div refchart stylewidth: 100%; height: 400px;/div /template script setup import { ref, onMounted, watch } from vue import * as echarts from echarts import { useMaterialStore } from /stores/material const materialStore useMaterialStore() const chart ref(null) let chartInstance null const initChart () { chartInstance echarts.init(chart.value) const updateChart () { const option { title: { text: 库存物资分类统计, left: center }, tooltip: { trigger: item, formatter: {a} br/{b}: {c} ({d}%) }, legend: { orient: vertical, left: left, data: materialStore.categoryStats.map(item item.name) }, series: [ { name: 库存数量, type: pie, radius: [50%, 70%], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: #fff, borderWidth: 2 }, label: { show: false, position: center }, emphasis: { label: { show: true, fontSize: 18, fontWeight: bold } }, labelLine: { show: false }, data: materialStore.categoryStats.map(item ({ value: item.count,