Commit fc0e6702 by DaiJiezhang

Merge branch 'codex/merge-unmerged-auth-commits' into 'master'

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

See merge request !10
parents 3d2fe0a8 13400512
...@@ -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
...@@ -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 查询有效资产 -> 更新或软删除。
......
...@@ -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