Commit 13400512 by DaiJiezhang

feat: 手机号资产按设备名称关联并统一列表与弹窗字段

关联设备原本只显示和输入设备 ID,用户得自己去设备资产页记数字。
改为按设备名称搜索选择,列表也直接显示名称。

- 新增 GET /api/phone-assets/lookups/devices,走手机号页 READ 权限而不是设备页的
  管理员权限,避免只有手机号权限的用户搜不到设备
- PhoneAssetResponse 增加 deviceName,分页时一次 IN 查询批量解析,不逐行查库
- 弹窗设备 ID 输入框改为可搜索下拉,编辑时预置当前设备保证名称回显
- 弹窗字段名统一为列表叫法:卡类型→运营商、实名归属→实名人、管理方式→管理模式、
  处置状态→使用状态、设备 ID→关联设备
- 运营商增加「电销卡「联通」」
- 列表与弹窗字段顺序调整为 手机号、实名人、运营商
- ICCID 不再单独占列,收进手机号单元格由图标点击弹出,图标用内联 SVG 以免引入
  不在 package.json 里的 @element-plus/icons-vue
- 修复下拉已选中的值被 placeholder 灰色规则一起染灰
- 修复筛选框写死宽度导致手机号提示语截断、重置按钮换行

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 10a82a20
...@@ -4,6 +4,7 @@ import com.xyw.console.asset.service.PhoneAssetService; ...@@ -4,6 +4,7 @@ import com.xyw.console.asset.service.PhoneAssetService;
import com.xyw.console.auth.PagePermissionService; import com.xyw.console.auth.PagePermissionService;
import com.xyw.console.common.ApiResponse; import com.xyw.console.common.ApiResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/api/phone-assets") @RequestMapping("/api/phone-assets")
...@@ -21,4 +22,6 @@ public class PhoneAssetController { ...@@ -21,4 +22,6 @@ public class PhoneAssetController {
@PutMapping("/{id}") public ApiResponse<PhoneAssetResponse> update(@PathVariable Long id,@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("编辑成功",service.update(id,request));} @PutMapping("/{id}") public ApiResponse<PhoneAssetResponse> update(@PathVariable Long id,@Valid @RequestBody PhoneAssetSaveRequest request){permissions.require(PagePermissionService.PHONE,"EDIT");return ApiResponse.success("编辑成功",service.update(id,request));}
/** 代码作用(白话):软删除没有关联阻止的手机号资产。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):DELETE -> Service.softDelete -> ApiResponse -> 列表刷新。 */ /** 代码作用(白话):软删除没有关联阻止的手机号资产。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):DELETE -> Service.softDelete -> ApiResponse -> 列表刷新。 */
@DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.PHONE,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);} @DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id){permissions.require(PagePermissionService.PHONE,"EDIT");service.softDelete(id);return ApiResponse.success("删除成功",null);}
/** 代码作用(白话):按设备名称关键字搜索可关联的设备,供弹窗"关联设备"下拉使用。这里用手机号页面的 READ 权限而不是设备页的管理员权限,避免只有手机号权限的用户搜不到设备。关联文件:DeviceAssetLookupResponse.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):下拉输入 -> GET /lookups/devices -> Service 模糊查询 -> 选项列表。 */
@GetMapping("/lookups/devices") public ApiResponse<List<DeviceAssetLookupResponse>> devices(@RequestParam(defaultValue="") String keyword){permissions.require(PagePermissionService.PHONE,"READ");return ApiResponse.success(service.searchDevices(keyword));}
} }
package com.xyw.console.asset.dto;
/** 文件用途(白话):承载设备资产的搜索结果,供手机号资产弹窗按设备名称选择关联设备时使用。 */
public record DeviceAssetLookupResponse(Long id, String deviceName) {}
package com.xyw.console.asset.dto; package com.xyw.console.asset.dto;
import java.time.LocalDateTime; import java.time.LocalDateTime;
public record PhoneAssetResponse(Long id,String phoneNumber,String numberType,String sourceAssetType,Long sourceAssetId,String cardType,String iccid,String realNameOwner,String managementType,String disposalStatus,Long deviceId,LocalDateTime relationSyncedAt) {} /** 说明:deviceName 由 Service 按 deviceId 批量解析后填入,列表只展示设备名称,deviceId 仅供编辑弹窗回填选中项。 */
\ No newline at end of file public record PhoneAssetResponse(Long id,String phoneNumber,String numberType,String sourceAssetType,Long sourceAssetId,String cardType,String iccid,String realNameOwner,String managementType,String disposalStatus,Long deviceId,String deviceName,LocalDateTime relationSyncedAt) {}
\ No newline at end of file
...@@ -3,25 +3,43 @@ package com.xyw.console.asset.service; ...@@ -3,25 +3,43 @@ package com.xyw.console.asset.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xyw.console.asset.dto.*; import com.xyw.console.asset.dto.*;
import com.xyw.console.asset.entity.AssetDeviceEntity;
import com.xyw.console.asset.entity.PhoneAssetEntity; import com.xyw.console.asset.entity.PhoneAssetEntity;
import com.xyw.console.asset.exception.PhoneAssetNotFoundException; import com.xyw.console.asset.exception.PhoneAssetNotFoundException;
import com.xyw.console.asset.exception.PhoneAssetValidationException; import com.xyw.console.asset.exception.PhoneAssetValidationException;
import com.xyw.console.asset.mapper.AssetDeviceMapper;
import com.xyw.console.asset.mapper.PhoneAssetMapper; import com.xyw.console.asset.mapper.PhoneAssetMapper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
public class PhoneAssetService { public class PhoneAssetService {
/** 设备下拉一次最多返回的条数,避免设备量大时把整表推给浏览器;与设备资产页人员搜索保持同一上限。 */
private static final int DEVICE_LOOKUP_LIMIT = 20;
private final PhoneAssetMapper mapper; private final PhoneAssetMapper mapper;
/** 代码作用(白话):接收手机号资产表的数据库访问入口。关联文件:PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_phone_asset。 */ private final AssetDeviceMapper deviceMapper;
public PhoneAssetService(PhoneAssetMapper mapper) { this.mapper = mapper; } /** 代码作用(白话):接收手机号资产表和设备资产表的数据库访问入口。关联文件:PhoneAssetMapper.java、AssetDeviceMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_phone_asset / as_asset_device。 */
/** 代码作用(白话):按分页、手机号片段、ICCID、实名人和使用状态读取未删除资产。关联文件:PhoneAssetPageQuery.java、PhoneAssetController.java。关联逻辑(调用链/数据流):GET 参数 -> 查询条件 -> Mapper -> Response。 */ public PhoneAssetService(PhoneAssetMapper mapper, AssetDeviceMapper deviceMapper) { this.mapper = mapper; this.deviceMapper = deviceMapper; }
/** 代码作用(白话):按分页、手机号片段、ICCID、实名人和使用状态读取未删除资产,并补上关联设备名称。关联文件:PhoneAssetPageQuery.java、PhoneAssetController.java。关联逻辑(调用链/数据流):GET 参数 -> 查询条件 -> Mapper -> 设备名批量解析 -> Response。 */
public PhoneAssetPageResponse page(PhoneAssetPageQuery query) { public PhoneAssetPageResponse page(PhoneAssetPageQuery query) {
Page<PhoneAssetEntity> page = mapper.selectPage(new Page<>(query.resolvedPage(), query.resolvedSize()), activeQuery(query)); Page<PhoneAssetEntity> page = mapper.selectPage(new Page<>(query.resolvedPage(), query.resolvedSize()), activeQuery(query));
List<PhoneAssetResponse> records = page.getRecords().stream().map(this::toResponse).toList(); Map<Long, String> deviceNames = deviceNames(collectDeviceIds(page.getRecords()));
List<PhoneAssetResponse> records = page.getRecords().stream().map(item -> toResponse(item, deviceNames)).toList();
return new PhoneAssetPageResponse(records, page.getTotal(), query.resolvedPage(), query.resolvedSize()); return new PhoneAssetPageResponse(records, page.getTotal(), query.resolvedPage(), query.resolvedSize());
} }
/** 代码作用(白话):按设备名称关键字返回可关联的有效设备,供新增/编辑弹窗的搜索下拉使用。关联文件:DeviceAssetLookupResponse.java、PhoneAssetController.java。关联逻辑(调用链/数据流):下拉输入 -> GET lookups/devices -> as_asset_device 模糊查询 -> 选项列表。 */
public List<DeviceAssetLookupResponse> searchDevices(String keyword) {
return deviceMapper.selectList(new LambdaQueryWrapper<AssetDeviceEntity>()
.eq(AssetDeviceEntity::getDeleteTime, 0L)
.like(hasText(keyword), AssetDeviceEntity::getDeviceName, keyword)
.orderByDesc(AssetDeviceEntity::getId))
.stream().limit(DEVICE_LOOKUP_LIMIT).map(item -> new DeviceAssetLookupResponse(item.getId(), item.getDeviceName())).toList();
}
/** 代码作用(白话):组合现有资产筛选条件,保证旧的完整号码与 ICCID 参数继续可用。关联文件:PhoneAssetPageQuery.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):Query DTO -> LambdaQueryWrapper -> SQL where 条件。 */ /** 代码作用(白话):组合现有资产筛选条件,保证旧的完整号码与 ICCID 参数继续可用。关联文件:PhoneAssetPageQuery.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):Query DTO -> LambdaQueryWrapper -> SQL where 条件。 */
private LambdaQueryWrapper<PhoneAssetEntity> activeQuery(PhoneAssetPageQuery query) { private LambdaQueryWrapper<PhoneAssetEntity> activeQuery(PhoneAssetPageQuery query) {
LambdaQueryWrapper<PhoneAssetEntity> wrapper = new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime, 0L); LambdaQueryWrapper<PhoneAssetEntity> wrapper = new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime, 0L);
...@@ -43,9 +61,9 @@ public class PhoneAssetService { ...@@ -43,9 +61,9 @@ public class PhoneAssetService {
/** 代码作用(白话):判断筛选文本是否有内容,避免空字符串参与数据库筛选。关联文件:PhoneAssetPageQuery.java、PhoneAssetService.java。关联逻辑(调用链/数据流):请求参数 -> hasText -> 是否追加条件。 */ /** 代码作用(白话):判断筛选文本是否有内容,避免空字符串参与数据库筛选。关联文件:PhoneAssetPageQuery.java、PhoneAssetService.java。关联逻辑(调用链/数据流):请求参数 -> hasText -> 是否追加条件。 */
private boolean hasText(String value) { return value != null && !value.isBlank(); } private boolean hasText(String value) { return value != null && !value.isBlank(); }
/** 代码作用(白话):新增手机号资产并初始化审计字段和内部关联快照。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):POST 请求 -> DTO -> Service.create -> Mapper.insert -> as_phone_asset。 */ /** 代码作用(白话):新增手机号资产并初始化审计字段和内部关联快照。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):POST 请求 -> DTO -> Service.create -> Mapper.insert -> as_phone_asset。 */
public PhoneAssetResponse create(PhoneAssetSaveRequest request) { PhoneAssetEntity entity=new PhoneAssetEntity(); applyEditableFields(entity,request); entity.setNumberType("SELF"); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); entity.setLinkedWecomAccounts("[]"); entity.setLinkedWechatAccounts("[]"); entity.setLinkedDouyinAccounts("[]"); entity.setLinkedDomainAccounts("[]"); entity.setLinkedMerchants("[]"); if(mapper.insert(entity)!=1) throw new IllegalStateException("手机号资产新增失败"); return toResponse(entity); } public PhoneAssetResponse create(PhoneAssetSaveRequest request) { PhoneAssetEntity entity=new PhoneAssetEntity(); applyEditableFields(entity,request); entity.setNumberType("SELF"); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); entity.setLinkedWecomAccounts("[]"); entity.setLinkedWechatAccounts("[]"); entity.setLinkedDouyinAccounts("[]"); entity.setLinkedDomainAccounts("[]"); entity.setLinkedMerchants("[]"); if(mapper.insert(entity)!=1) throw new IllegalStateException("手机号资产新增失败"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); }
/** 代码作用(白话):更新有效资产的用户可写字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):PUT 请求 -> Service 查找 -> Mapper.updateById -> Response。 */ /** 代码作用(白话):更新有效资产的用户可写字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):PUT 请求 -> Service 查找 -> Mapper.updateById -> Response。 */
public PhoneAssetResponse update(Long id, PhoneAssetSaveRequest request) { PhoneAssetEntity entity=requireActiveEntity(id); applyEditableFields(entity,request); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); return toResponse(entity); } public PhoneAssetResponse update(Long id, PhoneAssetSaveRequest request) { PhoneAssetEntity entity=requireActiveEntity(id); applyEditableFields(entity,request); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); return toResponse(entity,deviceNames(singleDeviceId(entity.getDeviceId()))); }
/** 代码作用(白话):将资产标记为删除,供 Controller 删除接口调用。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):DELETE 请求 -> Service -> Mapper.updateById。 */ /** 代码作用(白话):将资产标记为删除,供 Controller 删除接口调用。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):DELETE 请求 -> Service -> Mapper.updateById。 */
public void softDelete(Long id) { PhoneAssetEntity entity=requireActiveEntity(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); } public void softDelete(Long id) { PhoneAssetEntity entity=requireActiveEntity(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now()); if(mapper.updateById(entity)!=1) throw new PhoneAssetNotFoundException("手机号资产不存在或已删除"); }
/** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */ /** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */
...@@ -54,6 +72,12 @@ public class PhoneAssetService { ...@@ -54,6 +72,12 @@ public class PhoneAssetService {
private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); } private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); }
/** 代码作用(白话):按已确认规则清理手机号并验证其最终格式。关联文件:PhoneAssetSaveRequest.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):前端输入 -> 去空格和 +86 -> 格式失败返回 400。 */ /** 代码作用(白话):按已确认规则清理手机号并验证其最终格式。关联文件:PhoneAssetSaveRequest.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):前端输入 -> 去空格和 +86 -> 格式失败返回 400。 */
private String normalizePhoneNumber(String value){ String normalized=value==null?"":value.trim(); if(normalized.startsWith("+86")) normalized=normalized.substring(3); if(!normalized.matches("\\d{11}")) throw new PhoneAssetValidationException("手机号必须是 11 位数字"); return normalized; } private String normalizePhoneNumber(String value){ String normalized=value==null?"":value.trim(); if(normalized.startsWith("+86")) normalized=normalized.substring(3); if(!normalized.matches("\\d{11}")) throw new PhoneAssetValidationException("手机号必须是 11 位数字"); return normalized; }
/** 代码作用(白话):只挑选列表需要展示的字段。关联文件:PhoneAssetResponse.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):实体 -> DTO -> ApiResponse -> 表格。 */ /** 代码作用(白话):只挑选列表需要展示的字段,并按传入的设备名映射补上关联设备名称。关联文件:PhoneAssetResponse.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):实体 + 设备名映射 -> DTO -> ApiResponse -> 表格。 */
private PhoneAssetResponse toResponse(PhoneAssetEntity entity){ return new PhoneAssetResponse(entity.getId(),entity.getPhoneNumber(),entity.getNumberType(),entity.getSourceAssetType(),entity.getSourceAssetId(),entity.getCardType(),entity.getIccid(),entity.getRealNameOwner(),entity.getManagementType(),entity.getDisposalStatus(),entity.getDeviceId(),entity.getRelationSyncedAt()); } private PhoneAssetResponse toResponse(PhoneAssetEntity entity, Map<Long,String> deviceNames){ return new PhoneAssetResponse(entity.getId(),entity.getPhoneNumber(),entity.getNumberType(),entity.getSourceAssetType(),entity.getSourceAssetId(),entity.getCardType(),entity.getIccid(),entity.getRealNameOwner(),entity.getManagementType(),entity.getDisposalStatus(),entity.getDeviceId(),entity.getDeviceId()==null?null:deviceNames.get(entity.getDeviceId()),entity.getRelationSyncedAt()); }
/** 代码作用(白话):收集本页所有非空设备 ID,供一次性查设备名使用。关联文件:PhoneAssetEntity.java、AssetDeviceMapper.java。关联逻辑(调用链/数据流):分页结果 -> 设备 ID 集合 -> 批量查询。 */
private Set<Long> collectDeviceIds(List<PhoneAssetEntity> items){ Set<Long> ids=new HashSet<>(); items.forEach(item->{ if(item.getDeviceId()!=null) ids.add(item.getDeviceId()); }); return ids; }
/** 代码作用(白话):把单条记录的设备 ID 转成批量查询需要的集合形状。关联文件:PhoneAssetService.java、AssetDeviceMapper.java。关联逻辑(调用链/数据流):新增/编辑结果 -> 单个 ID -> deviceNames。 */
private Set<Long> singleDeviceId(Long id){ return id==null?Set.of():Set.of(id); }
/** 代码作用(白话):用一次 IN 查询把设备 ID 换成设备名称,避免逐行查库。设备被软删时查不到,名称留空由前端显示占位符。关联文件:AssetDeviceEntity.java、PhoneAssetResponse.java。关联逻辑(调用链/数据流):ID 集合 -> Mapper IN 查询 -> ID→名称映射 -> Response。 */
private Map<Long,String> deviceNames(Set<Long> ids){ if(ids.isEmpty()) return Map.of(); Map<Long,String> result=new HashMap<>(); deviceMapper.selectList(new LambdaQueryWrapper<AssetDeviceEntity>().in(AssetDeviceEntity::getId,ids).eq(AssetDeviceEntity::getDeleteTime,0L)).forEach(item->result.put(item.getId(),item.getDeviceName())); return result; }
} }
\ No newline at end of file
...@@ -4,12 +4,17 @@ import static org.mockito.ArgumentMatchers.any; ...@@ -4,12 +4,17 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.xyw.console.asset.entity.AssetDeviceEntity;
import com.xyw.console.asset.entity.PhoneAssetEntity; import com.xyw.console.asset.entity.PhoneAssetEntity;
import com.xyw.console.asset.mapper.AssetDeviceMapper;
import com.xyw.console.asset.mapper.PhoneAssetMapper; import com.xyw.console.asset.mapper.PhoneAssetMapper;
import java.util.List;
import com.xyw.console.asset.service.PhoneAssetService; import com.xyw.console.asset.service.PhoneAssetService;
import com.xyw.console.auth.PagePermissionService; import com.xyw.console.auth.PagePermissionService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -28,7 +33,7 @@ class PhoneAssetControllerTest { ...@@ -28,7 +33,7 @@ class PhoneAssetControllerTest {
PhoneAssetMapper mapper = mock(PhoneAssetMapper.class); PhoneAssetMapper mapper = mock(PhoneAssetMapper.class);
when(mapper.selectCount(any())).thenReturn(0L); when(mapper.selectCount(any())).thenReturn(0L);
when(mapper.insert(any(PhoneAssetEntity.class))).thenReturn(1); when(mapper.insert(any(PhoneAssetEntity.class))).thenReturn(1);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper), mock(PagePermissionService.class))).build(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper, mock(AssetDeviceMapper.class)), mock(PagePermissionService.class))).build();
mockMvc.perform(post("/api/phone-assets").contentType(MediaType.APPLICATION_JSON) mockMvc.perform(post("/api/phone-assets").contentType(MediaType.APPLICATION_JSON)
.content("{\"phoneNumber\":\"13812345678\",\"realNameOwner\":\"张三\"}")) .content("{\"phoneNumber\":\"13812345678\",\"realNameOwner\":\"张三\"}"))
...@@ -47,7 +52,7 @@ class PhoneAssetControllerTest { ...@@ -47,7 +52,7 @@ class PhoneAssetControllerTest {
when(mapper.selectOne(any())).thenReturn(entity); when(mapper.selectOne(any())).thenReturn(entity);
when(mapper.selectCount(any())).thenReturn(0L); when(mapper.selectCount(any())).thenReturn(0L);
when(mapper.updateById(any(PhoneAssetEntity.class))).thenReturn(1); when(mapper.updateById(any(PhoneAssetEntity.class))).thenReturn(1);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper), mock(PagePermissionService.class))).build(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper, mock(AssetDeviceMapper.class)), mock(PagePermissionService.class))).build();
mockMvc.perform(put("/api/phone-assets/1").contentType(MediaType.APPLICATION_JSON) mockMvc.perform(put("/api/phone-assets/1").contentType(MediaType.APPLICATION_JSON)
.content("{\"phoneNumber\":\"13912345678\",\"realNameOwner\":\"李四\"}")) .content("{\"phoneNumber\":\"13912345678\",\"realNameOwner\":\"李四\"}"))
...@@ -64,13 +69,34 @@ class PhoneAssetControllerTest { ...@@ -64,13 +69,34 @@ class PhoneAssetControllerTest {
PhoneAssetMapper mapper = mock(PhoneAssetMapper.class); PhoneAssetMapper mapper = mock(PhoneAssetMapper.class);
when(mapper.selectOne(any())).thenReturn(activeEntity()); when(mapper.selectOne(any())).thenReturn(activeEntity());
when(mapper.updateById(any(PhoneAssetEntity.class))).thenReturn(1); when(mapper.updateById(any(PhoneAssetEntity.class))).thenReturn(1);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper), mock(PagePermissionService.class))).build(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper, mock(AssetDeviceMapper.class)), mock(PagePermissionService.class))).build();
mockMvc.perform(delete("/api/phone-assets/1")) mockMvc.perform(delete("/api/phone-assets/1"))
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
/** /**
* 代码作用(白话):证明弹窗的关联设备下拉可以按设备名称搜到有效设备。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。
* 关联逻辑(调用链/数据流):GET /api/phone-assets/lookups/devices -> Service.searchDevices -> as_asset_device -> 下拉选项。
*/
@Test
void searchesLinkableDevicesByDeviceName() throws Exception {
PhoneAssetMapper mapper = mock(PhoneAssetMapper.class);
AssetDeviceMapper deviceMapper = mock(AssetDeviceMapper.class);
AssetDeviceEntity device = new AssetDeviceEntity();
device.setId(7L);
device.setDeviceName("测试机A");
when(deviceMapper.selectList(any())).thenReturn(List.of(device));
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PhoneAssetController(new PhoneAssetService(mapper, deviceMapper), mock(PagePermissionService.class))).build();
mockMvc.perform(get("/api/phone-assets/lookups/devices").param("keyword", "测试"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].id").value(7))
.andExpect(jsonPath("$.data[0].deviceName").value("测试机A"));
}
/**
* 代码作用(白话):构造一条仍有效的资产,供编辑和删除接口测试使用。 * 代码作用(白话):构造一条仍有效的资产,供编辑和删除接口测试使用。
* 关联文件:PhoneAssetService.java、PhoneAssetEntity.java。 * 关联文件:PhoneAssetService.java、PhoneAssetEntity.java。
* 关联逻辑(调用链/数据流):测试输入 -> Service 查询有效资产 -> 更新或软删除。 * 关联逻辑(调用链/数据流):测试输入 -> Service 查询有效资产 -> 更新或软删除。
......
...@@ -2,7 +2,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue/dist/vue.esm-b ...@@ -2,7 +2,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue/dist/vue.esm-b
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn.mjs'; import zhCn from 'element-plus/es/locale/lang/zh-cn.mjs';
import { authState } from '../auth/auth-store.js'; import { authState } from '../auth/auth-store.js';
import { createPhoneAsset, deletePhoneAsset, listPhoneAssets, updatePhoneAsset } from './phone-api-client.js'; import { createPhoneAsset, deletePhoneAsset, listPhoneAssets, searchLinkableDevices, updatePhoneAsset } from './phone-api-client.js';
/** /**
* 代码作用(白话):显示手机号资产列表,并管理新增、编辑、保存和删除弹窗状态。 * 代码作用(白话):显示手机号资产列表,并管理新增、编辑、保存和删除弹窗状态。
...@@ -23,6 +23,8 @@ export default { ...@@ -23,6 +23,8 @@ export default {
const total = ref(0); const total = ref(0);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const editingId = ref(null); const editingId = ref(null);
/** 关联设备下拉的候选项;编辑时先塞入当前行的设备,保证未搜索前也能显示名称而不是空白。 */
const deviceOptions = ref([]);
/** 代码作用(白话):把当前手机号资产权限转换为是否展示写操作;关联文件:auth-store.js、PhoneAssetController.java。关联逻辑(调用链/数据流):/api/auth/me -> EDIT 判断 -> 新增/编辑/删除控件 -> 后端二次校验。 */ /** 代码作用(白话):把当前手机号资产权限转换为是否展示写操作;关联文件:auth-store.js、PhoneAssetController.java。关联逻辑(调用链/数据流):/api/auth/me -> EDIT 判断 -> 新增/编辑/删除控件 -> 后端二次校验。 */
const canEdit = computed(() => authState.user?.pagePermissions?.['phone-assets'] === 'EDIT'); const canEdit = computed(() => authState.user?.pagePermissions?.['phone-assets'] === 'EDIT');
const filters = reactive({ page: 1, size: 20, phoneNumber: '', iccid: '', realNameOwner: '', disposalStatus: 'ALL' }); const filters = reactive({ page: 1, size: 20, phoneNumber: '', iccid: '', realNameOwner: '', disposalStatus: 'ALL' });
...@@ -169,10 +171,28 @@ export default { ...@@ -169,10 +171,28 @@ export default {
function openCreate() { function openCreate() {
editingId.value = null; editingId.value = null;
resetForm(); resetForm();
deviceOptions.value = [];
loadDeviceOptions('');
dialogVisible.value = true; dialogVisible.value = true;
} }
/** /**
* 代码作用(白话):按设备名称关键字拉取可关联的设备选项,并保留当前已选设备。
* 关联文件:phone-api-client.js、PhoneAssetController.java。
* 关联逻辑(调用链/数据流):打开弹窗或下拉输入 -> GET lookups/devices -> deviceOptions -> 下拉列表。
*/
async function loadDeviceOptions(keyword) {
try {
const options = await searchLinkableDevices(keyword);
// 后端只返回前 20 条,已选设备可能不在结果里;直接覆盖会让选择框丢掉名称回落成裸 ID,所以把它补回列表头部。
const selected = deviceOptions.value.find((item) => item.id === form.deviceId);
deviceOptions.value = selected && !options.some((item) => item.id === selected.id) ? [selected, ...options] : options;
} catch (error) {
ElMessage.error(error.message);
}
}
/**
* 代码作用(白话):用当前表格行的数据回填同一个编辑弹窗。 * 代码作用(白话):用当前表格行的数据回填同一个编辑弹窗。
* 关联文件:PhoneAssetView.js、phone-api-client.js。 * 关联文件:PhoneAssetView.js、phone-api-client.js。
* 关联逻辑(调用链/数据流):点击编辑 -> row -> form -> PUT 保存。 * 关联逻辑(调用链/数据流):点击编辑 -> row -> form -> PUT 保存。
...@@ -188,6 +208,9 @@ export default { ...@@ -188,6 +208,9 @@ export default {
disposalStatus: row.disposalStatus || '正常使用', disposalStatus: row.disposalStatus || '正常使用',
deviceId: row.deviceId ?? null deviceId: row.deviceId ?? null
}); });
// 先用行数据兜底一条选项:设备已被软删时后端查不到名称,这里退回显示 ID,避免选择框空白让人以为没关联。
deviceOptions.value = row.deviceId ? [{ id: row.deviceId, deviceName: row.deviceName || `设备 ${row.deviceId}` }] : [];
loadDeviceOptions('');
dialogVisible.value = true; dialogVisible.value = true;
} }
...@@ -267,6 +290,7 @@ export default { ...@@ -267,6 +290,7 @@ export default {
changeStatus, changeStatus,
canEdit, canEdit,
confirmDelete, confirmDelete,
deviceOptions,
dialogVisible, dialogVisible,
editingId, editingId,
elementLocale, elementLocale,
...@@ -278,6 +302,7 @@ export default { ...@@ -278,6 +302,7 @@ export default {
limitIccid, limitIccid,
limitPhoneNumber, limitPhoneNumber,
limitSearchPhone, limitSearchPhone,
loadDeviceOptions,
loading, loading,
openCreate, openCreate,
openEdit, openEdit,
...@@ -296,7 +321,7 @@ export default { ...@@ -296,7 +321,7 @@ export default {
<el-config-provider :locale="elementLocale"><section class="phone-asset-page phone-asset-list-page"> <el-config-provider :locale="elementLocale"><section class="phone-asset-page phone-asset-list-page">
<header class="phone-asset-list-page__header"><h2>手机号资产</h2><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增手机号资产</el-button></header> <header class="phone-asset-list-page__header"><h2>手机号资产</h2><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增手机号资产</el-button></header>
<section class="phone-asset-list-page__panel phone-asset-list-page__search" aria-label="筛选手机号资产"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.phoneNumber" maxlength="11" inputmode="numeric" placeholder="手机号前3位、后4位或完整号码" @input="limitSearchPhone" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.iccid" maxlength="20" placeholder="请输入 ICCID" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.realNameOwner" placeholder="请输入实名人" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-select v-model="filters.disposalStatus" placeholder="使用状态:" clearable @change="changeStatus" @clear="restoreAllDisposalStatuses"><template #prefix>使用状态:</template><el-option label="全部" value="ALL" /><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><el-option label="停机" value="停机" /><el-option label="注销" value="已注销" /></el-select><el-button @click="resetSearch">重置</el-button></el-form></section> <section class="phone-asset-list-page__panel phone-asset-list-page__search" aria-label="筛选手机号资产"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.phoneNumber" maxlength="11" inputmode="numeric" placeholder="手机号前3位、后4位或完整号码" @input="limitSearchPhone" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.iccid" maxlength="20" placeholder="请输入 ICCID" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.realNameOwner" placeholder="请输入实名人" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-select v-model="filters.disposalStatus" placeholder="使用状态:" clearable @change="changeStatus" @clear="restoreAllDisposalStatuses"><template #prefix>使用状态:</template><el-option label="全部" value="ALL" /><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><el-option label="停机" value="停机" /><el-option label="注销" value="已注销" /></el-select><el-button @click="resetSearch">重置</el-button></el-form></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid"><el-table-column prop="phoneNumber" label="手机号" min-width="150" show-overflow-tooltip /><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip /><el-table-column prop="iccid" label="ICCID" min-width="220" show-overflow-tooltip /><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column prop="deviceId" label="关联设备(ID)" min-width="150" show-overflow-tooltip /><el-table-column v-if="canEdit" label="操作" width="120"><template #default="{ row }"><span class="phone-asset-list-page__actions"><el-button link @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="confirmDelete(row)">删除</el-button></span></template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize" /></section> <section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid"><el-table-column label="手机号" min-width="180"><template #default="{ row }"><span class="phone-asset-list-page__phone"><span class="phone-asset-list-page__phone-text">{{ row.phoneNumber }}</span><el-popover v-if="row.iccid" placement="right" trigger="click" :width="250" popper-class="phone-asset-list-page__iccid-popper"><template #reference><button type="button" class="phone-asset-list-page__iccid-btn" title="查看 ICCID" aria-label="查看 ICCID"><svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><rect x="2" y="3.2" width="12" height="9.6" rx="1.6" /><path d="M5 6.4h6M5 9.2h3.5" /></svg></button></template><div class="phone-asset-list-page__iccid-pop"><span class="phone-asset-list-page__iccid-pop-label">ICCID</span><span class="phone-asset-list-page__iccid-pop-value">{{ row.iccid }}</span></div></el-popover></span></template></el-table-column><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip /><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column label="关联设备" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.deviceName || '-' }}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120"><template #default="{ row }"><span class="phone-asset-list-page__actions"><el-button link @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="confirmDelete(row)">删除</el-button></span></template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize" /></section>
<el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId === null ? '新增手机号资产' : '编辑手机号资产'" width="560px" @opened="resetDialogScroll"> <el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId === null ? '新增手机号资产' : '编辑手机号资产'" width="560px" @opened="resetDialogScroll">
<el-form class="phone-asset-modal__form" label-width="96px"> <el-form class="phone-asset-modal__form" label-width="96px">
<el-form-item class="phone-asset-modal__form-row" label="手机号" required> <el-form-item class="phone-asset-modal__form-row" label="手机号" required>
...@@ -304,18 +329,18 @@ export default { ...@@ -304,18 +329,18 @@ export default {
<template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.phoneNumber.length }}/11</span></template> <template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.phoneNumber.length }}/11</span></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="卡类型"> <el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" maxlength="20" placeholder="请输入实名人" /></el-form-item>
<el-select v-model="form.cardType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select> <el-form-item class="phone-asset-modal__form-row" label="运营商">
<el-select v-model="form.cardType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="电销卡「联通」" value="电销卡「联通」" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select>
</el-form-item> </el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="ICCID"> <el-form-item class="phone-asset-modal__form-row" label="ICCID">
<el-input v-model="form.iccid" class="phone-asset-modal__count-input" maxlength="20" autocomplete="off" placeholder="请输入 ICCID" @input="limitIccid" @paste="handleIccidPaste"> <el-input v-model="form.iccid" class="phone-asset-modal__count-input" maxlength="20" autocomplete="off" placeholder="请输入 ICCID" @input="limitIccid" @paste="handleIccidPaste">
<template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.iccid.length }}/20</span></template> <template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.iccid.length }}/20</span></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="实名归属"><el-input v-model="form.realNameOwner" maxlength="20" placeholder="请输入实名归属" /></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="管理模式"><el-select v-model="form.managementType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="自有" value="自有" /><el-option label="租用" value="租用" /><el-option label="代运营" value="代运营" /></el-select></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="管理方式"><el-select v-model="form.managementType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="自有" value="自有" /><el-option label="租用" value="租用" /><el-option label="代运营" value="代运营" /></el-select></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="使用状态"><el-select v-model="form.disposalStatus" placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><el-option label="停机" value="停机" /><el-option label="注销" value="已注销" /></el-select></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="处置状态"><el-select v-model="form.disposalStatus" placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><el-option label="停机" value="停机" /><el-option label="注销" value="已注销" /></el-select></el-form-item> <el-form-item class="phone-asset-modal__form-row" label="关联设备"><el-select v-model="form.deviceId" filterable remote clearable :remote-method="loadDeviceOptions" placeholder="输入设备名称搜索" popper-class="phone-asset-modal__select-popper"><el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /></el-select></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="设备 ID"><el-input v-model.number="form.deviceId" type="number" placeholder="请输入设备 ID" /></el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
......
...@@ -19,3 +19,5 @@ export function createPhoneAsset(form) { return request('/api/phone-assets', { m ...@@ -19,3 +19,5 @@ export function createPhoneAsset(form) { return request('/api/phone-assets', { m
export function updatePhoneAsset(id, form) { return request('/api/phone-assets/' + id, { method: 'PUT', body: JSON.stringify(form) }); } export function updatePhoneAsset(id, form) { return request('/api/phone-assets/' + id, { method: 'PUT', body: JSON.stringify(form) }); }
/** 代码作用(白话):确认后删除一条手机号资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):删除确认 -> DELETE -> 后端软删除。 */ /** 代码作用(白话):确认后删除一条手机号资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):删除确认 -> DELETE -> 后端软删除。 */
export function deletePhoneAsset(id) { return request('/api/phone-assets/' + id, { method: 'DELETE' }); } export function deletePhoneAsset(id) { return request('/api/phone-assets/' + id, { method: 'DELETE' }); }
/** 代码作用(白话):按设备名称搜索可关联的设备资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):关联设备下拉输入 -> GET lookups/devices -> 设备选项。 */
export function searchLinkableDevices(keyword) { return request('/api/phone-assets/lookups/devices?keyword=' + encodeURIComponent(keyword || '')); }
...@@ -160,8 +160,10 @@ body { margin: 0; } ...@@ -160,8 +160,10 @@ body { margin: 0; }
color: #27272a; color: #27272a;
font-size: 14px; font-size: 14px;
} }
/* Element Plus 的下拉把"已选中的值"和"占位文字"渲染在同一个 .el-select__placeholder 上,只有真占位才多带 is-transparent。
这里必须限定 .is-transparent,否则已选中的值会跟着变灰,和输入框里深色的输入值对不上。 */
.phone-asset-modal__form-row .el-input__inner::placeholder, .phone-asset-modal__form-row .el-input__inner::placeholder,
.phone-asset-modal__form-row .el-select__placeholder { .phone-asset-modal__form-row .el-select__placeholder.is-transparent {
color: #a1a1aa; color: #a1a1aa;
} }
.phone-asset-modal__count-input .el-input__inner { .phone-asset-modal__count-input .el-input__inner {
...@@ -287,8 +289,28 @@ body { margin: 0; } ...@@ -287,8 +289,28 @@ body { margin: 0; }
.phone-asset-list-page__panel { border:1px solid #e5e5e8; border-radius:12px; background:#fff; box-shadow:0 10px 30px rgba(15,23,42,.05); } .phone-asset-list-page__panel { border:1px solid #e5e5e8; border-radius:12px; background:#fff; box-shadow:0 10px 30px rgba(15,23,42,.05); }
.phone-asset-list-page__search { margin-bottom:16px; padding:20px 22px; } .phone-asset-list-page__search { margin-bottom:16px; padding:20px 22px; }
.phone-asset-list-page__filters { display:flex; align-items:center; flex-wrap:wrap; gap:12px; } .phone-asset-list-page__filters { display:flex; align-items:center; flex-wrap:wrap; gap:12px; }
.phone-asset-list-page__filters .el-input { width:210px; }.phone-asset-list-page__filters .el-input:nth-child(2){width:240px}.phone-asset-list-page__filters .el-input:nth-child(3){width:190px}.phone-asset-list-page__filters .el-select{width:160px} /* 筛选框宽度按文案长度分配,别再写死等宽:手机号提示语最长("手机号前3位、后4位或完整号码"),
固定给足 250px 保证一次显示完整;ICCID 与实名人提示语短,改成弹性平分剩余空间,窗口变窄时优先压缩它们,
这样"重置"按钮不会因为总宽超出面板而被挤到第二行。 */
.phone-asset-list-page__filters .el-input{flex:0 1 auto;width:auto}
.phone-asset-list-page__filters .el-input:nth-child(1){width:250px;min-width:250px}
.phone-asset-list-page__filters .el-input:nth-child(2){width:220px;min-width:140px}
.phone-asset-list-page__filters .el-input:nth-child(3){width:180px;min-width:140px}
.phone-asset-list-page__filters .el-select{flex:0 0 168px;width:auto}
.phone-asset-list-page__filters .el-button{flex:0 0 auto}
.phone-asset-list-page__filters .el-input__wrapper,.phone-asset-list-page__filters .el-select__wrapper { min-height:38px; border-radius:6px; box-shadow:0 0 0 1px #e5e5e8 inset; }.phone-asset-list-page__filters .el-button{height:38px;border-radius:6px;color:#52525b;border-color:#e5e5e8}.phone-asset-list-page__filters .el-input__wrapper:hover,.phone-asset-list-page__filters .el-select__wrapper:hover{box-shadow:0 0 0 1px #d4d4d8 inset} .phone-asset-list-page__filters .el-input__wrapper,.phone-asset-list-page__filters .el-select__wrapper { min-height:38px; border-radius:6px; box-shadow:0 0 0 1px #e5e5e8 inset; }.phone-asset-list-page__filters .el-button{height:38px;border-radius:6px;color:#52525b;border-color:#e5e5e8}.phone-asset-list-page__filters .el-input__wrapper:hover,.phone-asset-list-page__filters .el-select__wrapper:hover{box-shadow:0 0 0 1px #d4d4d8 inset}
/* 手机号单元格内嵌 ICCID 触发按钮:ICCID 有 20 位,单独占一列会挤掉后面的字段,
收进手机号格里按需查看。图标用内联 SVG,避免为一个图标引入 @element-plus/icons-vue(它不在 package.json 依赖里)。 */
.phone-asset-list-page__phone{display:inline-flex;align-items:center;gap:7px;min-width:0}
.phone-asset-list-page__phone-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.phone-asset-list-page__iccid-btn{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:5px;background:transparent;color:#a1a1aa;cursor:pointer;transition:color .16s ease,background .16s ease}
.phone-asset-list-page__iccid-btn:hover{background:#f1f1f3;color:#52525b}
.phone-asset-list-page__iccid-btn svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.35;stroke-linecap:round}
.phone-asset-list-page__iccid-popper{padding:12px 14px!important}
.phone-asset-list-page__iccid-pop{display:flex;flex-direction:column;gap:5px}
.phone-asset-list-page__iccid-pop-label{color:#8b8b93;font-size:12px;letter-spacing:.02em}
/* ICCID 是长串数字,等宽字体便于逐位核对;allow-select 让用户能直接框选复制。 */
.phone-asset-list-page__iccid-pop-value{color:#18181b;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;word-break:break-all;user-select:text}
.phone-asset-list-page__table { overflow:hidden; }.phone-asset-list-page__table-header{display:flex;align-items:center;gap:9px;min-height:62px;padding:0 22px;border-bottom:1px solid #e5e5e8}.phone-asset-list-page__table-header h3{margin:0;color:#18181b;font-size:15px;font-weight:650}.phone-asset-list-page__table-header span{color:#8b8b93;font-size:13px} .phone-asset-list-page__table { overflow:hidden; }.phone-asset-list-page__table-header{display:flex;align-items:center;gap:9px;min-height:62px;padding:0 22px;border-bottom:1px solid #e5e5e8}.phone-asset-list-page__table-header h3{margin:0;color:#18181b;font-size:15px;font-weight:650}.phone-asset-list-page__table-header span{color:#8b8b93;font-size:13px}
.phone-asset-list-page__grid .el-table__header th.el-table__cell{height:46px;background:#fafafa;color:#71717a;font-size:13px;font-weight:600}.phone-asset-list-page__grid .el-table__cell{height:56px;color:#3f3f46;font-size:14px}.phone-asset-list-page__grid .el-table__row:hover>td.el-table__cell{background:#fafafa}.phone-asset-list-page__status{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.phone-asset-list-page__status-dot{width:7px;height:7px;border-radius:50%;background:#a1a1aa;box-shadow:0 0 0 3px rgba(161,161,170,.12)}.phone-asset-list-page__status-dot.正常使用{background:#2f9e68;box-shadow:0 0 0 3px rgba(47,158,104,.1)}.phone-asset-list-page__status-dot.闲置,.phone-asset-list-page__status-dot.停机{background:#d28b24;box-shadow:0 0 0 3px rgba(210,139,36,.11)}.phone-asset-list-page__actions{display:inline-flex;gap:14px}.phone-asset-list-page__actions .el-button{padding:0;color:#3f3f46;font-size:13px}.phone-asset-list-page__actions .el-button--danger{color:#e5484d} .phone-asset-list-page__grid .el-table__header th.el-table__cell{height:46px;background:#fafafa;color:#71717a;font-size:13px;font-weight:600}.phone-asset-list-page__grid .el-table__cell{height:56px;color:#3f3f46;font-size:14px}.phone-asset-list-page__grid .el-table__row:hover>td.el-table__cell{background:#fafafa}.phone-asset-list-page__status{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.phone-asset-list-page__status-dot{width:7px;height:7px;border-radius:50%;background:#a1a1aa;box-shadow:0 0 0 3px rgba(161,161,170,.12)}.phone-asset-list-page__status-dot.正常使用{background:#2f9e68;box-shadow:0 0 0 3px rgba(47,158,104,.1)}.phone-asset-list-page__status-dot.闲置,.phone-asset-list-page__status-dot.停机{background:#d28b24;box-shadow:0 0 0 3px rgba(210,139,36,.11)}.phone-asset-list-page__actions{display:inline-flex;gap:14px}.phone-asset-list-page__actions .el-button{padding:0;color:#3f3f46;font-size:13px}.phone-asset-list-page__actions .el-button--danger{color:#e5484d}
/* 列表页统一分页器(AppPagination.js):容器高度恒定,空数据时按钮灰化但不塌陷,四个列表页共用。 */ /* 列表页统一分页器(AppPagination.js):容器高度恒定,空数据时按钮灰化但不塌陷,四个列表页共用。 */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment