Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xyw_asset_console
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DaiJiezhang
xyw_asset_console
Commits
fc0e6702
Commit
fc0e6702
authored
Aug 06, 2026
by
DaiJiezhang
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'codex/merge-unmerged-auth-commits' into 'master'
feat: 手机号资产按设备名称关联并统一列表与弹窗字段 See merge request
!10
parents
3d2fe0a8
13400512
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
131 additions
and
23 deletions
+131
-23
PhoneAssetController.java
...om/xyw/console/asset/controller/PhoneAssetController.java
+3
-0
DeviceAssetLookupResponse.java
.../com/xyw/console/asset/dto/DeviceAssetLookupResponse.java
+4
-0
PhoneAssetResponse.java
...in/java/com/xyw/console/asset/dto/PhoneAssetResponse.java
+3
-2
PhoneAssetService.java
...java/com/xyw/console/asset/service/PhoneAssetService.java
+33
-8
PhoneAssetControllerTest.java
...yw/console/asset/controller/PhoneAssetControllerTest.java
+29
-3
PhoneAssetView.js
frontend/src/modules/phone/PhoneAssetView.js
+33
-8
phone-api-client.js
frontend/src/modules/phone/phone-api-client.js
+2
-0
app.css
frontend/src/styles/app.css
+24
-2
No files found.
backend/src/main/java/com/xyw/console/asset/controller/PhoneAssetController.java
View file @
fc0e6702
...
@@ -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
));}
}
}
backend/src/main/java/com/xyw/console/asset/dto/DeviceAssetLookupResponse.java
0 → 100644
View file @
fc0e6702
package
com
.
xyw
.
console
.
asset
.
dto
;
/** 文件用途(白话):承载设备资产的搜索结果,供手机号资产弹窗按设备名称选择关联设备时使用。 */
public
record
DeviceAssetLookupResponse
(
Long
id
,
String
deviceName
)
{}
backend/src/main/java/com/xyw/console/asset/dto/PhoneAssetResponse.java
View file @
fc0e6702
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
backend/src/main/java/com/xyw/console/asset/service/PhoneAssetService.java
View file @
fc0e6702
...
@@ -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
backend/src/test/java/com/xyw/console/asset/controller/PhoneAssetControllerTest.java
View file @
fc0e6702
...
@@ -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 查询有效资产 -> 更新或软删除。
...
...
frontend/src/modules/phone/PhoneAssetView.js
View file @
fc0e6702
...
@@ -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>
...
...
frontend/src/modules/phone/phone-api-client.js
View file @
fc0e6702
...
@@ -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
||
''
));
}
frontend/src/styles/app.css
View file @
fc0e6702
...
@@ -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):容器高度恒定,空数据时按钮灰化但不塌陷,四个列表页共用。 */
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment