From 7a78c8a62a8c79d0c9abadb5d9d66b8b0744ef1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E4=B8=99?= <10604541+sanbing-os@user.noreply.gitee.com> Date: Tue, 9 Sep 2025 18:25:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=85=E7=94=B5=E6=9E=AA=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/controller/StationController.java | 11 ++ .../app/adapter/request/GunUpdateRequest.java | 13 ++ .../response/StationPileCascaderOption.java | 73 ++++++++++ .../jcpp/app/service/StationService.java | 6 + .../app/service/impl/DefaultGunService.java | 8 +- .../service/impl/DefaultStationService.java | 54 ++++++++ jcpp-web-ui/src/components/GunManagement.tsx | 131 +++++++++--------- jcpp-web-ui/src/components/PileManagement.tsx | 8 -- jcpp-web-ui/src/services/gunService.ts | 4 +- jcpp-web-ui/src/services/stationService.ts | 7 + jcpp-web-ui/src/types/index.ts | 3 +- 11 files changed, 239 insertions(+), 79 deletions(-) create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/adapter/response/StationPileCascaderOption.java diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/controller/StationController.java b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/controller/StationController.java index 6e170f7..1b0e1a9 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/controller/StationController.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/controller/StationController.java @@ -16,6 +16,7 @@ import sanbing.jcpp.app.adapter.request.StationUpdateRequest; import sanbing.jcpp.app.adapter.response.ApiResponse; import sanbing.jcpp.app.adapter.response.PageResponse; import sanbing.jcpp.app.adapter.response.StationOption; +import sanbing.jcpp.app.adapter.response.StationPileCascaderOption; import sanbing.jcpp.app.dal.entity.Station; import sanbing.jcpp.app.exception.JCPPException; import sanbing.jcpp.app.service.StationService; @@ -104,4 +105,14 @@ public class StationController extends BaseController { List options = stationService.searchStationOptions(keyword, page, size); return ResponseEntity.ok(ApiResponse.success("查询成功", options)); } + + /** + * 获取充电站-充电桩级联选择器数据(用于级联选择组件) + */ + @GetMapping("/pile-cascader") + public ResponseEntity>> getStationPileCascaderOptions( + @RequestParam(required = false) String keyword) { + List options = stationService.getStationPileCascaderOptions(keyword); + return ResponseEntity.ok(ApiResponse.success("查询成功", options)); + } } diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/request/GunUpdateRequest.java b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/request/GunUpdateRequest.java index d9c0149..d4d2ce2 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/request/GunUpdateRequest.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/request/GunUpdateRequest.java @@ -18,5 +18,18 @@ public class GunUpdateRequest { @NoXss private String gunName; + @NotBlank(message = "枪号不能为空") + private String gunNo; + + @NotBlank(message = "充电枪编码不能为空") + @NoXss + private String gunCode; + + @NotBlank(message = "所属充电站不能为空") + private String stationId; + + @NotBlank(message = "所属充电桩不能为空") + private String pileId; + private GunRunStatusEnum runStatus; } diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/response/StationPileCascaderOption.java b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/response/StationPileCascaderOption.java new file mode 100644 index 0000000..497d811 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/adapter/response/StationPileCascaderOption.java @@ -0,0 +1,73 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.app.adapter.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.UUID; + +/** + * 充电站-充电桩级联选择器选项响应 + * 用于Ant Design Cascader组件 + * + * @author 九筒 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StationPileCascaderOption { + + private String value; // 选项的值(充电站ID或充电桩ID) + private String label; // 显示的标签 + private boolean isLeaf; // 是否为叶子节点 + private List children; // 子选项(充电站下的充电桩) + + // 额外信息 + private String stationId; // 充电站ID(当是充电桩选项时) + private String stationName; // 充电站名称 + private String stationCode; // 充电站编码 + private String pileId; // 充电桩ID(当是充电桩选项时) + private String pileName; // 充电桩名称(当是充电桩选项时) + private String pileCode; // 充电桩编码(当是充电桩选项时) + + /** + * 创建充电站选项 + */ + public static StationPileCascaderOption createStationOption(UUID stationId, String stationName, String stationCode, List piles) { + StationPileCascaderOption option = new StationPileCascaderOption(); + option.setValue(stationId.toString()); + option.setLabel(stationName + " (" + stationCode + ")"); + option.setLeaf(false); + option.setChildren(piles); + option.setStationId(stationId.toString()); + option.setStationName(stationName); + option.setStationCode(stationCode); + return option; + } + + /** + * 创建充电桩选项 + */ + public static StationPileCascaderOption createPileOption(UUID stationId, String stationName, String stationCode, + UUID pileId, String pileName, String pileCode) { + StationPileCascaderOption option = new StationPileCascaderOption(); + option.setValue(pileId.toString()); + option.setLabel(pileName + " (" + pileCode + ")"); + option.setLeaf(true); + option.setChildren(null); + option.setStationId(stationId.toString()); + option.setStationName(stationName); + option.setStationCode(stationCode); + option.setPileId(pileId.toString()); + option.setPileName(pileName); + option.setPileCode(pileCode); + return option; + } +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/StationService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/StationService.java index d0f54e8..f1e750a 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/service/StationService.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/StationService.java @@ -11,6 +11,7 @@ import sanbing.jcpp.app.adapter.request.StationQueryRequest; import sanbing.jcpp.app.adapter.request.StationUpdateRequest; import sanbing.jcpp.app.adapter.response.PageResponse; import sanbing.jcpp.app.adapter.response.StationOption; +import sanbing.jcpp.app.adapter.response.StationPileCascaderOption; import sanbing.jcpp.app.dal.entity.Station; import sanbing.jcpp.app.exception.JCPPException; @@ -58,4 +59,9 @@ public interface StationService { * 搜索充电站选项列表(支持关键字搜索和分页) */ List searchStationOptions(String keyword, int page, int size); + + /** + * 获取充电站-充电桩级联选择器数据(用于级联选择组件) + */ + List getStationPileCascaderOptions(String keyword); } diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultGunService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultGunService.java index b2fd7c8..0ba04b7 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultGunService.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultGunService.java @@ -86,10 +86,10 @@ public class DefaultGunService implements GunService { .createdTime(existingGun.getCreatedTime()) .updatedTime(LocalDateTime.now()) // 更新时设置更新时间 .gunName(request.getGunName()) - .gunNo(existingGun.getGunNo()) // 编号不允许修改 - .gunCode(existingGun.getGunCode()) // 编码不允许修改 - .stationId(existingGun.getStationId()) // 所属充电站不允许修改 - .pileId(existingGun.getPileId()) // 所属充电桩不允许修改 + .gunNo(request.getGunNo()) // 允许修改枪号 + .gunCode(request.getGunCode()) // 允许修改编码 + .stationId(UUID.fromString(request.getStationId())) // 允许修改所属充电站 + .pileId(UUID.fromString(request.getPileId())) // 允许修改所属充电桩 .additionalInfo(existingGun.getAdditionalInfo()) .version(existingGun.getVersion()) .build(); diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultStationService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultStationService.java index bc9746d..8d99c19 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultStationService.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultStationService.java @@ -17,6 +17,8 @@ import sanbing.jcpp.app.adapter.request.StationQueryRequest; import sanbing.jcpp.app.adapter.request.StationUpdateRequest; import sanbing.jcpp.app.adapter.response.PageResponse; import sanbing.jcpp.app.adapter.response.StationOption; +import sanbing.jcpp.app.adapter.response.StationPileCascaderOption; +import sanbing.jcpp.app.dal.entity.Pile; import sanbing.jcpp.app.dal.entity.Station; import sanbing.jcpp.app.dal.mapper.PileMapper; import sanbing.jcpp.app.dal.mapper.StationMapper; @@ -27,6 +29,7 @@ import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -190,4 +193,55 @@ public class DefaultStationService implements StationService { )) .collect(Collectors.toList()); } + + @Override + public List getStationPileCascaderOptions(String keyword) { + // 查询充电站 + QueryWrapper stationWrapper = new QueryWrapper<>(); + stationWrapper.select("id", "station_name", "station_code"); + + // 如果有关键字,按站名或编码模糊搜索 + if (StringUtils.hasText(keyword)) { + stationWrapper.and(w -> w.like("station_name", keyword) + .or() + .like("station_code", keyword)); + } + + stationWrapper.orderByAsc("station_name"); + List stations = stationMapper.selectList(stationWrapper); + + if (stations.isEmpty()) { + return List.of(); + } + + // 查询所有充电桩 + QueryWrapper pileWrapper = new QueryWrapper<>(); + pileWrapper.select("id", "pile_name", "pile_code", "station_id") + .in("station_id", stations.stream().map(Station::getId).collect(Collectors.toList())) + .orderByAsc("pile_name"); + + List piles = pileMapper.selectList(pileWrapper); + + // 按充电站ID分组充电桩 + Map> pilesByStation = piles.stream() + .collect(Collectors.groupingBy(Pile::getStationId)); + + // 构建级联选择器数据 + return stations.stream() + .map(station -> { + List stationPiles = pilesByStation.getOrDefault(station.getId(), List.of()); + + List pileOptions = stationPiles.stream() + .map(pile -> StationPileCascaderOption.createPileOption( + station.getId(), station.getStationName(), station.getStationCode(), + pile.getId(), pile.getPileName(), pile.getPileCode() + )) + .collect(Collectors.toList()); + + return StationPileCascaderOption.createStationOption( + station.getId(), station.getStationName(), station.getStationCode(), pileOptions + ); + }) + .collect(Collectors.toList()); + } } diff --git a/jcpp-web-ui/src/components/GunManagement.tsx b/jcpp-web-ui/src/components/GunManagement.tsx index 7207863..508451d 100644 --- a/jcpp-web-ui/src/components/GunManagement.tsx +++ b/jcpp-web-ui/src/components/GunManagement.tsx @@ -8,6 +8,7 @@ import React, {useEffect, useMemo, useState} from 'react'; import { Button, Card, + Cascader, Checkbox, Col, Dropdown, @@ -29,7 +30,7 @@ import {getErrorMessage} from '../services/api'; import * as gunService from '../services/gunService'; import * as stationService from '../services/stationService'; import {pileService} from '../services/pileService'; -import type {Gun, GunCreateRequest, PileOption, StationOption} from '../types'; +import type {Gun, GunCreateRequest, GunUpdateRequest, PileOption, StationOption} from '../types'; const { confirm } = Modal; @@ -40,6 +41,7 @@ const GunManagement: React.FC = () => { const [form] = Form.useForm(); const [stationOptions, setStationOptions] = useState([]); const [pileOptions, setPileOptions] = useState([]); + const [cascaderOptions, setCascaderOptions] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [modalLoading, setModalLoading] = useState(false); const [isEdit, setIsEdit] = useState(false); @@ -236,9 +238,6 @@ const GunManagement: React.FC = () => { - { } }; + // 加载级联选择器数据 + const loadCascaderOptions = async () => { + try { + const response = await stationService.getStationPileCascaderOptions(); + setCascaderOptions(Array.isArray(response) ? response : []); + } catch (error: any) { + console.error('加载级联选择器数据失败:', error); + } + }; + // 监听搜索参数变化 useEffect(() => { loadData(); @@ -439,6 +448,7 @@ const GunManagement: React.FC = () => { useEffect(() => { loadStationOptions(); loadPileOptions(); + loadCascaderOptions(); }, []); // 处理表格变化 @@ -496,25 +506,23 @@ const GunManagement: React.FC = () => { setModalVisible(true); form.setFieldsValue({ ...record, - gunNo: record.gunNo.toString() + gunNo: record.gunNo, + stationPile: [record.stationId, record.pileId] // 设置级联选择器的值 }); }; - // 处理查看 - const handleView = (record: Gun) => { - showMessage.info('查看功能待实现'); - }; // 生成充电枪编码 const handleGenerateGunCode = () => { - const pileId = form.getFieldValue('pileId'); + const stationPile = form.getFieldValue('stationPile'); const gunNo = form.getFieldValue('gunNo'); - if (!pileId || !gunNo) { - showMessage.warning('请先选择充电桩和填写枪号'); + if (!stationPile || stationPile.length !== 2 || !gunNo) { + showMessage.warning('请先选择充电站和充电桩,并填写枪号'); return; } + const pileId = stationPile[1]; const selectedPile = pileOptions.find(p => p.id === pileId); if (selectedPile) { const code = generateGunCode(selectedPile.pileCode, gunNo); @@ -529,16 +537,38 @@ const GunManagement: React.FC = () => { setModalLoading(true); if (isEdit && currentRecord) { - // 编辑功能待实现 - showMessage.info('编辑功能待实现'); + // 编辑充电枪 + // 从级联选择器值中获取充电站ID和充电桩ID + const cascaderValue = values.stationPile; + if (!cascaderValue || cascaderValue.length !== 2) { + showMessage.error('请选择充电站和充电桩'); + return; + } + + const updateData: GunUpdateRequest = { + gunName: values.gunName, + gunNo: values.gunNo, + gunCode: values.gunCode, + stationId: cascaderValue[0], // 充电站ID + pileId: cascaderValue[1] // 充电桩ID + }; + await gunService.updateGun(currentRecord.id, updateData); + showMessage.success('充电枪更新成功'); } else { // 新建充电枪 + // 从级联选择器值中获取充电站ID和充电桩ID + const cascaderValue = values.stationPile; + if (!cascaderValue || cascaderValue.length !== 2) { + showMessage.error('请选择充电站和充电桩'); + return; + } + const createData: GunCreateRequest = { gunName: values.gunName, gunNo: values.gunNo, gunCode: values.gunCode, - stationId: values.stationId, - pileId: values.pileId + stationId: cascaderValue[0], // 充电站ID + pileId: cascaderValue[1] // 充电桩ID }; await gunService.createGun(createData); showMessage.success('充电枪创建成功'); @@ -870,52 +900,25 @@ const GunManagement: React.FC = () => { - - - - - - - - - - - - + + + path.some(option => + option.label.toLowerCase().includes(inputValue.toLowerCase()) + ) + }} + disabled={false} + changeOnSelect={false} + expandTrigger="hover" + /> + @@ -935,13 +938,13 @@ const GunManagement: React.FC = () => { > { - { setModalVisible(true); }; - // 查看充电桩详情 - const handleView = (record: Pile) => { - console.log('查看充电桩详情:', record); - showMessage.info('查看功能暂未实现,请查看控制台日志'); - }; // 生成充电桩编码 const handleGeneratePileCode = () => { diff --git a/jcpp-web-ui/src/services/gunService.ts b/jcpp-web-ui/src/services/gunService.ts index 241dd1b..c8868fe 100644 --- a/jcpp-web-ui/src/services/gunService.ts +++ b/jcpp-web-ui/src/services/gunService.ts @@ -5,7 +5,7 @@ * 付费课程知识星球:https://t.zsxq.com/aKtXo */ import {api} from './api'; -import type {Gun, GunCreateRequest, PageResponse, QueryParams} from '../types'; +import type {Gun, GunCreateRequest, GunUpdateRequest, PageResponse, QueryParams} from '../types'; export const getGuns = async (params: QueryParams): Promise> => { const response = await api.get('/api/guns', { params }); @@ -17,7 +17,7 @@ export const createGun = async (data: GunCreateRequest): Promise => { return response.data.data; }; -export const updateGun = async (id: string, data: Partial): Promise => { +export const updateGun = async (id: string, data: GunUpdateRequest): Promise => { const response = await api.put(`/api/guns/${id}`, data); return response.data.data; }; diff --git a/jcpp-web-ui/src/services/stationService.ts b/jcpp-web-ui/src/services/stationService.ts index 3bbd967..19b83d0 100644 --- a/jcpp-web-ui/src/services/stationService.ts +++ b/jcpp-web-ui/src/services/stationService.ts @@ -39,4 +39,11 @@ export const searchStationOptions = async (params: StationSearchRequest): Promis export const getStationOptions = async (): Promise => { const response = await api.get('/api/stations/options'); return response.data.data; +}; + +// 获取充电站-充电桩级联选择器数据 +export const getStationPileCascaderOptions = async (keyword?: string): Promise => { + const params = keyword ? { keyword } : {}; + const response = await api.get('/api/stations/pile-cascader', { params }); + return response.data.data; }; \ No newline at end of file diff --git a/jcpp-web-ui/src/types/index.ts b/jcpp-web-ui/src/types/index.ts index 8b79044..e4fe71e 100644 --- a/jcpp-web-ui/src/types/index.ts +++ b/jcpp-web-ui/src/types/index.ts @@ -120,7 +120,7 @@ export interface Gun { id: string; gunName: string; gunCode: string; - gunNo: number; + gunNo: string; stationId: string; stationName?: string; // 所属充电站名称 pileId: string; @@ -144,6 +144,7 @@ export interface GunCreateRequest { export interface GunUpdateRequest { gunName: string; gunNo: string; + gunCode: string; stationId: string; pileId: string; }