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
1
Merge Requests
1
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
86d3179e
Commit
86d3179e
authored
Jul 31, 2026
by
DaiJiezhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement phone asset management modal
parent
c95e91c1
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
532 additions
and
35 deletions
+532
-35
PhoneAssetController.java
...om/xyw/console/asset/controller/PhoneAssetController.java
+10
-1
PhoneAssetSaveRequest.java
...java/com/xyw/console/asset/dto/PhoneAssetSaveRequest.java
+20
-0
PhoneAssetNotFoundException.java
.../console/asset/exception/PhoneAssetNotFoundException.java
+4
-0
PhoneAssetValidationException.java
...onsole/asset/exception/PhoneAssetValidationException.java
+12
-0
PhoneAssetService.java
...java/com/xyw/console/asset/service/PhoneAssetService.java
+34
-4
WebConfig.java
backend/src/main/java/com/xyw/console/config/WebConfig.java
+1
-1
PhoneAssetControllerTest.java
...yw/console/asset/controller/PhoneAssetControllerTest.java
+84
-0
package.json
frontend/package.json
+1
-1
PhoneAssetView.js
frontend/src/modules/phone/PhoneAssetView.js
+0
-0
phone-api-client.js
frontend/src/modules/phone/phone-api-client.js
+12
-15
app.css
frontend/src/styles/app.css
+242
-0
phone-asset.spec.js
frontend/tests/phone-asset.spec.js
+112
-13
No files found.
backend/src/main/java/com/xyw/console/asset/controller/PhoneAssetController.java
View file @
86d3179e
...
@@ -7,7 +7,15 @@ import org.springframework.web.bind.annotation.*;
...
@@ -7,7 +7,15 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RestController
@RequestMapping
(
"/api/phone-assets"
)
@RequestMapping
(
"/api/phone-assets"
)
public
class
PhoneAssetController
{
public
class
PhoneAssetController
{
private
final
PhoneAssetService
service
;
public
PhoneAssetController
(
PhoneAssetService
service
){
this
.
service
=
service
;}
private
final
PhoneAssetService
service
;
/** 代码作用(白话):接收手机号资产接口所需的业务服务。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):浏览器请求 -> Controller -> Service -> Mapper。 */
public
PhoneAssetController
(
PhoneAssetService
service
){
this
.
service
=
service
;}
/** 代码作用(白话):接收浏览器分页查询并返回统一 JSON。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):GET /api/phone-assets -> Service -> ApiResponse -> Vue 表格。 */
/** 代码作用(白话):接收浏览器分页查询并返回统一 JSON。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):GET /api/phone-assets -> Service -> ApiResponse -> Vue 表格。 */
@GetMapping
public
ApiResponse
<
PhoneAssetPageResponse
>
page
(
@Valid
PhoneAssetPageQuery
query
){
return
ApiResponse
.
success
(
service
.
page
(
query
));}
@GetMapping
public
ApiResponse
<
PhoneAssetPageResponse
>
page
(
@Valid
PhoneAssetPageQuery
query
){
return
ApiResponse
.
success
(
service
.
page
(
query
));}
/** 代码作用(白话):接收新增表单并创建手机号资产。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):POST -> DTO -> Service.create -> ApiResponse -> 弹窗。 */
@PostMapping
public
ApiResponse
<
PhoneAssetResponse
>
create
(
@Valid
@RequestBody
PhoneAssetSaveRequest
request
){
return
ApiResponse
.
success
(
"新增成功"
,
service
.
create
(
request
));}
/** 代码作用(白话):接收编辑表单并更新允许修改的字段。关联文件:PhoneAssetSaveRequest.java、PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):PUT -> Service.update -> ApiResponse -> 列表刷新。 */
@PutMapping
(
"/{id}"
)
public
ApiResponse
<
PhoneAssetResponse
>
update
(
@PathVariable
Long
id
,
@Valid
@RequestBody
PhoneAssetSaveRequest
request
){
return
ApiResponse
.
success
(
"编辑成功"
,
service
.
update
(
id
,
request
));}
/** 代码作用(白话):软删除没有关联阻止的手机号资产。关联文件:PhoneAssetService.java、phone-api-client.js。关联逻辑(调用链/数据流):DELETE -> Service.softDelete -> ApiResponse -> 列表刷新。 */
@DeleteMapping
(
"/{id}"
)
public
ApiResponse
<
Void
>
delete
(
@PathVariable
Long
id
){
service
.
softDelete
(
id
);
return
ApiResponse
.
success
(
"删除成功"
,
null
);}
}
}
\ No newline at end of file
backend/src/main/java/com/xyw/console/asset/dto/PhoneAssetSaveRequest.java
0 → 100644
View file @
86d3179e
package
com
.
xyw
.
console
.
asset
.
dto
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.Size
;
/**
* 代码作用(白话):限定新增和编辑时浏览器可以提交的手机号资产字段。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。
* 关联逻辑(调用链/数据流):弹窗表单 -> DTO 校验 -> Service 写入允许修改的实体字段。
*/
public
record
PhoneAssetSaveRequest
(
@NotBlank
(
message
=
"手机号不能为空"
)
String
phoneNumber
,
String
cardType
,
String
iccid
,
@Size
(
max
=
20
,
message
=
"实名归属最多 20 个字符"
)
String
realNameOwner
,
String
managementType
,
String
disposalStatus
,
Long
deviceId
)
{
}
backend/src/main/java/com/xyw/console/asset/exception/PhoneAssetNotFoundException.java
0 → 100644
View file @
86d3179e
package
com
.
xyw
.
console
.
asset
.
exception
;
/** 代码作用(白话):表示请求的手机号资产不存在或已软删除。关联文件:PhoneAssetService.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查无记录 -> 异常 -> 404 响应。 */
public
class
PhoneAssetNotFoundException
extends
RuntimeException
{
public
PhoneAssetNotFoundException
(
String
message
){
super
(
message
);}
}
\ No newline at end of file
backend/src/main/java/com/xyw/console/asset/exception/PhoneAssetValidationException.java
0 → 100644
View file @
86d3179e
package
com
.
xyw
.
console
.
asset
.
exception
;
/**
* 代码作用(白话):表示手机号规范化后仍不符合保存规则。
* 关联文件:PhoneAssetService.java、PhoneAssetExceptionHandler.java。
* 关联逻辑(调用链/数据流):Service 校验失败 -> 异常处理 -> 400 响应 -> 前端保留弹窗并提示。
*/
public
class
PhoneAssetValidationException
extends
RuntimeException
{
public
PhoneAssetValidationException
(
String
message
)
{
super
(
message
);
}
}
backend/src/main/java/com/xyw/console/asset/service/PhoneAssetService.java
View file @
86d3179e
...
@@ -3,15 +3,44 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
...
@@ -3,15 +3,44 @@ 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.PhoneAssetEntity
;
import
com.xyw.console.asset.entity.PhoneAssetEntity
;
import
com.xyw.console.asset.exception.PhoneAssetNotFoundException
;
import
com.xyw.console.asset.exception.PhoneAssetValidationException
;
import
com.xyw.console.asset.mapper.PhoneAssetMapper
;
import
com.xyw.console.asset.mapper.PhoneAssetMapper
;
import
java.time.LocalDateTime
;
import
java.util.List
;
import
java.util.List
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
@Service
@Service
public
class
PhoneAssetService
{
public
class
PhoneAssetService
{
private
final
PhoneAssetMapper
mapper
;
private
final
PhoneAssetMapper
mapper
;
public
PhoneAssetService
(
PhoneAssetMapper
mapper
){
this
.
mapper
=
mapper
;
}
/** 代码作用(白话):接收手机号资产表的数据库访问入口。关联文件:PhoneAssetMapper.java、PhoneAssetController.java。关联逻辑(调用链/数据流):Controller -> Service -> Mapper -> as_phone_asset。 */
public
PhoneAssetService
(
PhoneAssetMapper
mapper
)
{
this
.
mapper
=
mapper
;
}
/** 代码作用(白话):查询未删除手机号资产并转换成前端列表字段。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):GET 请求 -> page -> Mapper -> as_phone_asset -> Response。 */
/** 代码作用(白话):查询未删除手机号资产并转换成前端列表字段。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):GET 请求 -> page -> Mapper -> as_phone_asset -> Response。 */
public
PhoneAssetPageResponse
page
(
PhoneAssetPageQuery
query
){
Page
<
PhoneAssetEntity
>
page
=
mapper
.
selectPage
(
new
Page
<>(
query
.
resolvedPage
(),
query
.
resolvedSize
()),
new
LambdaQueryWrapper
<
PhoneAssetEntity
>().
eq
(
PhoneAssetEntity:
:
getDeleteTime
,
0L
).
eq
(
query
.
phoneNumber
()!=
null
&&!
query
.
phoneNumber
().
isBlank
(),
PhoneAssetEntity:
:
getPhoneNumber
,
query
.
phoneNumber
()).
eq
(
query
.
iccid
()!=
null
&&!
query
.
iccid
().
isBlank
(),
PhoneAssetEntity:
:
getIccid
,
query
.
iccid
()).
orderByDesc
(
PhoneAssetEntity:
:
getId
));
List
<
PhoneAssetResponse
>
records
=
page
.
getRecords
().
stream
().
map
(
this
::
toResponse
).
toList
();
return
new
PhoneAssetPageResponse
(
records
,
page
.
getTotal
(),
query
.
resolvedPage
(),
query
.
resolvedSize
());
}
public
PhoneAssetPageResponse
page
(
PhoneAssetPageQuery
query
)
{
/** 代码作用(白话):只挑选页面列表需要展示的字段,避免把关联快照直接暴露给列表。关联文件:PhoneAssetResponse.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):数据库实体 -> DTO -> ApiResponse -> 前端表格。 */
Page
<
PhoneAssetEntity
>
page
=
mapper
.
selectPage
(
new
Page
<>(
query
.
resolvedPage
(),
query
.
resolvedSize
()),
activeQuery
().
eq
(
query
.
phoneNumber
()!=
null
&&!
query
.
phoneNumber
().
isBlank
(),
PhoneAssetEntity:
:
getPhoneNumber
,
query
.
phoneNumber
()).
eq
(
query
.
iccid
()!=
null
&&!
query
.
iccid
().
isBlank
(),
PhoneAssetEntity:
:
getIccid
,
query
.
iccid
()).
orderByDesc
(
PhoneAssetEntity:
:
getId
));
private
PhoneAssetResponse
toResponse
(
PhoneAssetEntity
e
){
return
new
PhoneAssetResponse
(
e
.
getId
(),
e
.
getPhoneNumber
(),
e
.
getCardType
(),
e
.
getIccid
(),
e
.
getRealNameOwner
(),
e
.
getManagementType
(),
e
.
getDisposalStatus
(),
e
.
getDeviceId
(),
e
.
getRelationSyncedAt
());
}
List
<
PhoneAssetResponse
>
records
=
page
.
getRecords
().
stream
().
map
(
this
::
toResponse
).
toList
();
return
new
PhoneAssetPageResponse
(
records
,
page
.
getTotal
(),
query
.
resolvedPage
(),
query
.
resolvedSize
());
}
/** 代码作用(白话):新增手机号资产并初始化审计字段和内部关联快照。关联文件: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
);
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
);
}
/** 代码作用(白话):更新有效资产的用户可写字段。关联文件: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
);
}
/** 代码作用(白话):将无关联检查的资产标记为删除,供 Controller 删除接口调用。关联文件:PhoneAssetController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):DELETE 请求 -> Service -> Mapper.updateById -> delete_time 更新。 */
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
(
"手机号资产不存在或已删除"
);
}
/** 代码作用(白话):构造只包含未删除资产的查询条件。关联文件:PhoneAssetEntity.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):Service 查询 -> delete_time = 0 -> Mapper。 */
private
LambdaQueryWrapper
<
PhoneAssetEntity
>
activeQuery
(){
return
new
LambdaQueryWrapper
<
PhoneAssetEntity
>().
eq
(
PhoneAssetEntity:
:
getDeleteTime
,
0L
);
}
/** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */
private
PhoneAssetEntity
requireActiveEntity
(
Long
id
){
PhoneAssetEntity
entity
=
mapper
.
selectOne
(
activeQuery
().
eq
(
PhoneAssetEntity:
:
getId
,
id
));
if
(
entity
==
null
)
throw
new
PhoneAssetNotFoundException
(
"手机号资产不存在或已删除"
);
return
entity
;
}
/** 代码作用(白话):把表单允许提交的字段写入实体,并统一处理手机号和默认处置状态。关联文件:PhoneAssetSaveRequest.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):弹窗表单 -> DTO -> Entity -> Mapper 保存。 */
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。 */
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 -> 表格。 */
private
PhoneAssetResponse
toResponse
(
PhoneAssetEntity
entity
){
return
new
PhoneAssetResponse
(
entity
.
getId
(),
entity
.
getPhoneNumber
(),
entity
.
getCardType
(),
entity
.
getIccid
(),
entity
.
getRealNameOwner
(),
entity
.
getManagementType
(),
entity
.
getDisposalStatus
(),
entity
.
getDeviceId
(),
entity
.
getRelationSyncedAt
());
}
}
}
\ No newline at end of file
backend/src/main/java/com/xyw/console/config/WebConfig.java
View file @
86d3179e
...
@@ -15,7 +15,7 @@ public class WebConfig implements WebMvcConfigurer {
...
@@ -15,7 +15,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
@Override
public
void
addCorsMappings
(
CorsRegistry
registry
)
{
public
void
addCorsMappings
(
CorsRegistry
registry
)
{
registry
.
addMapping
(
"/api/**"
)
registry
.
addMapping
(
"/api/**"
)
.
allowedOrigins
(
"http://localhost:8000"
,
"http://127.0.0.1:8000"
)
.
allowedOrigins
(
"http://localhost:8000"
,
"http://127.0.0.1:8000"
,
"http://localhost:5173"
,
"http://127.0.0.1:5173"
)
.
allowedMethods
(
"GET"
,
"POST"
,
"PUT"
,
"DELETE"
,
"OPTIONS"
)
.
allowedMethods
(
"GET"
,
"POST"
,
"PUT"
,
"DELETE"
,
"OPTIONS"
)
.
allowedHeaders
(
"*"
);
.
allowedHeaders
(
"*"
);
}
}
...
...
backend/src/test/java/com/xyw/console/asset/controller/PhoneAssetControllerTest.java
0 → 100644
View file @
86d3179e
package
com
.
xyw
.
console
.
asset
.
controller
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
import
static
org
.
mockito
.
Mockito
.
mock
;
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
.
post
;
import
static
org
.
springframework
.
test
.
web
.
servlet
.
request
.
MockMvcRequestBuilders
.
put
;
import
static
org
.
springframework
.
test
.
web
.
servlet
.
result
.
MockMvcResultMatchers
.
status
;
import
com.xyw.console.asset.entity.PhoneAssetEntity
;
import
com.xyw.console.asset.mapper.PhoneAssetMapper
;
import
com.xyw.console.asset.service.PhoneAssetService
;
import
org.junit.jupiter.api.Test
;
import
org.springframework.http.MediaType
;
import
org.springframework.test.web.servlet.MockMvc
;
import
org.springframework.test.web.servlet.setup.MockMvcBuilders
;
class
PhoneAssetControllerTest
{
/**
* 代码作用(白话):证明浏览器可以通过新增接口创建手机号资产。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetSaveRequest.java。
* 关联逻辑(调用链/数据流):POST /api/phone-assets -> Controller -> Service -> Mapper -> as_phone_asset。
*/
@Test
void
createsPhoneAssetThroughPostEndpoint
()
throws
Exception
{
PhoneAssetMapper
mapper
=
mock
(
PhoneAssetMapper
.
class
);
when
(
mapper
.
selectCount
(
any
())).
thenReturn
(
0L
);
when
(
mapper
.
insert
(
any
(
PhoneAssetEntity
.
class
))).
thenReturn
(
1
);
MockMvc
mockMvc
=
MockMvcBuilders
.
standaloneSetup
(
new
PhoneAssetController
(
new
PhoneAssetService
(
mapper
))).
build
();
mockMvc
.
perform
(
post
(
"/api/phone-assets"
).
contentType
(
MediaType
.
APPLICATION_JSON
)
.
content
(
"{\"phoneNumber\":\"13812345678\",\"realNameOwner\":\"张三\"}"
))
.
andExpect
(
status
().
isOk
());
}
/**
* 代码作用(白话):证明列表中的编辑操作可以调用更新接口。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。
* 关联逻辑(调用链/数据流):PUT /api/phone-assets/1 -> Service.update -> Mapper.updateById -> 列表刷新。
*/
@Test
void
updatesPhoneAssetThroughPutEndpoint
()
throws
Exception
{
PhoneAssetMapper
mapper
=
mock
(
PhoneAssetMapper
.
class
);
PhoneAssetEntity
entity
=
activeEntity
();
when
(
mapper
.
selectOne
(
any
())).
thenReturn
(
entity
);
when
(
mapper
.
selectCount
(
any
())).
thenReturn
(
0L
);
when
(
mapper
.
updateById
(
any
(
PhoneAssetEntity
.
class
))).
thenReturn
(
1
);
MockMvc
mockMvc
=
MockMvcBuilders
.
standaloneSetup
(
new
PhoneAssetController
(
new
PhoneAssetService
(
mapper
))).
build
();
mockMvc
.
perform
(
put
(
"/api/phone-assets/1"
).
contentType
(
MediaType
.
APPLICATION_JSON
)
.
content
(
"{\"phoneNumber\":\"13912345678\",\"realNameOwner\":\"李四\"}"
))
.
andExpect
(
status
().
isOk
());
}
/**
* 代码作用(白话):证明用户确认后可通过删除接口软删除无关联资产。
* 关联文件:PhoneAssetController.java、PhoneAssetService.java、PhoneAssetView.js。
* 关联逻辑(调用链/数据流):DELETE /api/phone-assets/1 -> Service.softDelete -> Mapper.updateById -> 列表刷新。
*/
@Test
void
softDeletesPhoneAssetThroughDeleteEndpoint
()
throws
Exception
{
PhoneAssetMapper
mapper
=
mock
(
PhoneAssetMapper
.
class
);
when
(
mapper
.
selectOne
(
any
())).
thenReturn
(
activeEntity
());
when
(
mapper
.
updateById
(
any
(
PhoneAssetEntity
.
class
))).
thenReturn
(
1
);
MockMvc
mockMvc
=
MockMvcBuilders
.
standaloneSetup
(
new
PhoneAssetController
(
new
PhoneAssetService
(
mapper
))).
build
();
mockMvc
.
perform
(
delete
(
"/api/phone-assets/1"
))
.
andExpect
(
status
().
isOk
());
}
/**
* 代码作用(白话):构造一条仍有效的资产,供编辑和删除接口测试使用。
* 关联文件:PhoneAssetService.java、PhoneAssetEntity.java。
* 关联逻辑(调用链/数据流):测试输入 -> Service 查询有效资产 -> 更新或软删除。
*/
private
PhoneAssetEntity
activeEntity
()
{
PhoneAssetEntity
entity
=
new
PhoneAssetEntity
();
entity
.
setId
(
1L
);
entity
.
setPhoneNumber
(
"13812345678"
);
entity
.
setDeleteTime
(
0L
);
return
entity
;
}
}
frontend/package.json
View file @
86d3179e
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
"name"
:
"xyw-console-frontend"
,
"name"
:
"xyw-console-frontend"
,
"private"
:
true
,
"private"
:
true
,
"scripts"
:
{
"scripts"
:
{
"dev"
:
"vite"
,
"dev"
:
"vite
--host 0.0.0.0
"
,
"build"
:
"vite build"
,
"build"
:
"vite build"
,
"preview"
:
"vite preview"
,
"preview"
:
"vite preview"
,
"test:e2e"
:
"playwright test"
"test:e2e"
:
"playwright test"
...
...
frontend/src/modules/phone/PhoneAssetView.js
View file @
86d3179e
This diff is collapsed.
Click to expand it.
frontend/src/modules/phone/phone-api-client.js
View file @
86d3179e
/**
/**
* 代码作用(白话):统一发送手机号资产
列表请求,避免页面自己处理返回格式
。
* 代码作用(白话):统一发送手机号资产
请求并把后端错误转换为页面可提示的文字
。
* 关联文件:PhoneAssetView.js、PhoneAssetController.java。
* 关联文件:PhoneAssetView.js、PhoneAssetController.java。
* 关联逻辑(调用链/数据流):页面
加载 -> listPhoneAssets -> GET API -> 表格数据
。
* 关联逻辑(调用链/数据流):页面
事件 -> fetch -> ApiResponse -> 成功数据或 ElMessage 错误
。
*/
*/
async
function
request
(
path
)
{
async
function
request
(
path
,
options
=
{}
)
{
const
response
=
await
fetch
(
path
);
const
response
=
await
fetch
(
path
,
{
headers
:
{
'Content-Type'
:
'application/json'
},
...
options
}
);
const
payload
=
await
response
.
json
();
const
payload
=
await
response
.
json
();
if
(
!
response
.
ok
||
payload
.
code
!==
200
)
throw
new
Error
(
payload
.
message
||
'手机号资产请求失败'
);
if
(
!
response
.
ok
||
payload
.
code
!==
200
)
throw
new
Error
(
payload
.
message
||
'手机号资产请求失败'
);
return
payload
.
data
;
return
payload
.
data
;
}
}
/**
/** 代码作用(白话):按筛选条件读取手机号资产列表。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):筛选条件 -> GET -> 表格。 */
* 代码作用(白话):根据筛选条件读取手机号资产分页数据。
export
function
listPhoneAssets
(
query
)
{
const
params
=
new
URLSearchParams
();
Object
.
entries
(
query
).
forEach
(([
key
,
value
])
=>
{
if
(
value
!==
''
&&
value
!==
null
&&
value
!==
undefined
)
params
.
set
(
key
,
value
);
});
return
request
(
'/api/phone-assets?'
+
params
.
toString
());
}
* 关联文件:PhoneAssetView.js、PhoneAssetController.java。
/** 代码作用(白话):提交新增表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):新增弹窗 -> POST -> 后端创建。 */
* 关联逻辑(调用链/数据流):筛选条件 -> URL 参数 -> 后端分页结果 -> 表格。
export
function
createPhoneAsset
(
form
)
{
return
request
(
'/api/phone-assets'
,
{
method
:
'POST'
,
body
:
JSON
.
stringify
(
form
)
});
}
*/
/** 代码作用(白话):提交编辑表单。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):编辑弹窗 -> PUT -> 后端更新。 */
export
function
listPhoneAssets
(
query
)
{
export
function
updatePhoneAsset
(
id
,
form
)
{
return
request
(
'/api/phone-assets/'
+
id
,
{
method
:
'PUT'
,
body
:
JSON
.
stringify
(
form
)
});
}
const
params
=
new
URLSearchParams
();
/** 代码作用(白话):确认后删除一条手机号资产。关联文件:PhoneAssetView.js、PhoneAssetController.java。关联逻辑(调用链/数据流):删除确认 -> DELETE -> 后端软删除。 */
Object
.
entries
(
query
).
forEach
(([
key
,
value
])
=>
{
if
(
value
!==
null
&&
value
!==
undefined
&&
value
!==
''
)
params
.
set
(
key
,
value
);
});
export
function
deletePhoneAsset
(
id
)
{
return
request
(
'/api/phone-assets/'
+
id
,
{
method
:
'DELETE'
});
}
return
request
(
`/api/phone-assets?
${
params
.
toString
()}
`
);
}
\ No newline at end of file
frontend/src/styles/app.css
View file @
86d3179e
...
@@ -17,3 +17,244 @@ body { margin: 0; }
...
@@ -17,3 +17,244 @@ body { margin: 0; }
.reference-card
{
padding
:
24px
;
border
:
1px
solid
#e2e8f0
;
border-radius
:
14px
;
background
:
white
;
box-shadow
:
0
8px
28px
rgba
(
15
,
23
,
42
,
.05
);
}
.reference-card
{
padding
:
24px
;
border
:
1px
solid
#e2e8f0
;
border-radius
:
14px
;
background
:
white
;
box-shadow
:
0
8px
28px
rgba
(
15
,
23
,
42
,
.05
);
}
.toolbar
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin-bottom
:
18px
;
}
.toolbar
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin-bottom
:
18px
;
}
@media
(
max-width
:
760px
)
{
.app-shell
{
grid-template-columns
:
1
fr
;
}
.sidebar
nav
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
.page-header
{
display
:
grid
;
}
.content
{
padding
:
24px
;
}
}
@media
(
max-width
:
760px
)
{
.app-shell
{
grid-template-columns
:
1
fr
;
}
.sidebar
nav
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
.page-header
{
display
:
grid
;
}
.content
{
padding
:
24px
;
}
}
/* 手机号资产弹窗:所有规则均以该弹窗类名开头,避免影响项目中的其他 Element Plus 弹窗。 */
.phone-asset-modal-mask
{
background
:
rgba
(
15
,
23
,
42
,
0.34
);
}
.phone-asset-modal-mask
.el-overlay-dialog
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
min-height
:
100%
;
padding
:
24px
;
}
.phone-asset-modal
{
width
:
min
(
560px
,
100%
)
!important
;
max-height
:
calc
(
100vh
-
48px
);
margin
:
0
!important
;
overflow
:
hidden
;
border
:
1px
solid
rgba
(
228
,
228
,
231
,
0.9
);
border-radius
:
12px
;
background
:
#ffffff
;
box-shadow
:
0
18px
50px
rgba
(
15
,
23
,
42
,
0.14
);
}
.phone-asset-modal
.el-dialog__header
{
display
:
flex
;
align-items
:
flex-start
;
justify-content
:
space-between
;
gap
:
16px
;
flex
:
0
0
auto
;
margin
:
0
;
padding
:
22px
24px
15px
;
background
:
rgba
(
255
,
255
,
255
,
0.96
);
backdrop-filter
:
blur
(
10px
);
}
.phone-asset-modal
.el-dialog__title
{
color
:
#18181b
;
font-size
:
17px
;
font-weight
:
650
;
line-height
:
1.45
;
letter-spacing
:
-0.01em
;
white-space
:
nowrap
;
}
.phone-asset-modal
.el-dialog__headerbtn
{
top
:
18px
;
right
:
18px
;
width
:
28px
;
height
:
28px
;
border-radius
:
6px
;
color
:
#71717a
;
transition
:
background
0.16s
ease
,
color
0.16s
ease
;
}
.phone-asset-modal
.el-dialog__headerbtn
:hover
{
color
:
#18181b
;
background
:
#f4f4f5
;
}
.phone-asset-modal
.el-dialog__close
{
width
:
15px
;
height
:
15px
;
}
.phone-asset-modal
.el-dialog__body
{
max-height
:
calc
(
100vh
-
206px
);
min-height
:
0
;
padding
:
4px
24px
8px
;
overflow-y
:
auto
;
}
.phone-asset-modal__form
{
display
:
grid
;
gap
:
14px
;
}
.phone-asset-modal__form-row.el-form-item
{
display
:
grid
;
grid-template-columns
:
96px
minmax
(
0
,
1
fr
);
align-items
:
center
;
gap
:
14px
;
margin
:
0
;
}
.phone-asset-modal__form-row
.el-form-item__label-wrap
{
float
:
none
;
min-width
:
0
;
}
.phone-asset-modal__form-row
.el-form-item__label
{
display
:
block
;
width
:
96px
!important
;
height
:
36px
;
padding
:
0
;
color
:
#52525b
;
font-size
:
14px
;
line-height
:
36px
;
text-align
:
right
;
white-space
:
nowrap
;
}
.phone-asset-modal__form-row
.el-form-item__content
{
min-width
:
0
;
min-height
:
36px
;
margin-left
:
0
!important
;
line-height
:
36px
;
}
.phone-asset-modal__form-row
.el-input
,
.phone-asset-modal__form-row
.el-select
{
width
:
100%
;
}
.phone-asset-modal__form-row
.el-input__wrapper
,
.phone-asset-modal__form-row
.el-select__wrapper
{
min-height
:
36px
;
padding
:
0
11px
;
border-radius
:
6px
;
background
:
#ffffff
;
box-shadow
:
0
0
0
1px
#e4e4e7
inset
;
transition
:
box-shadow
0.16s
ease
,
background
0.16s
ease
;
}
.phone-asset-modal__form-row
.el-input__wrapper
:hover
,
.phone-asset-modal__form-row
.el-select__wrapper
:hover
{
box-shadow
:
0
0
0
1px
#c4c4cc
inset
;
}
.phone-asset-modal__form-row
.el-input.is-focus
.el-input__wrapper
,
.phone-asset-modal__form-row
.el-select__wrapper.is-focused
{
box-shadow
:
0
0
0
1px
#18181b
inset
,
0
0
0
2px
rgba
(
24
,
24
,
27
,
0.09
);
}
.phone-asset-modal__form-row
.el-input__inner
,
.phone-asset-modal__form-row
.el-select__selected-item
,
.phone-asset-modal__form-row
.el-select__placeholder
{
color
:
#27272a
;
font-size
:
14px
;
}
.phone-asset-modal__form-row
.el-input__inner
::placeholder
,
.phone-asset-modal__form-row
.el-select__placeholder
{
color
:
#a1a1aa
;
}
.phone-asset-modal__count-input
.el-input__inner
{
padding-right
:
50px
;
}
.phone-asset-modal__character-count
{
color
:
#a1a1aa
;
font-size
:
12px
;
line-height
:
1
;
white-space
:
nowrap
;
pointer-events
:
none
;
user-select
:
none
;
}
.phone-asset-modal
.el-dialog__footer
{
display
:
flex
;
justify-content
:
flex-end
;
gap
:
9px
;
flex
:
0
0
auto
;
margin-top
:
22px
;
padding
:
16px
24px
20px
;
border-top
:
1px
solid
#eeeeef
;
}
.phone-asset-modal
.el-dialog__footer
.el-button
{
min-width
:
74px
;
height
:
36px
;
margin
:
0
;
padding
:
0
17px
;
border-radius
:
6px
;
font-size
:
14px
;
font-weight
:
550
;
}
.phone-asset-modal
.el-dialog__footer
.el-button--default
{
border-color
:
#e4e4e7
;
color
:
#52525b
;
background
:
#ffffff
;
}
.phone-asset-modal
.el-dialog__footer
.el-button--default
:hover
{
border-color
:
#d4d4d8
;
background
:
#fafafa
;
}
.phone-asset-modal
.el-dialog__footer
.el-button--primary
{
border-color
:
#18181b
;
background
:
#18181b
;
}
.phone-asset-modal
.el-dialog__footer
.el-button--primary
:hover
{
border-color
:
#27272a
;
background
:
#27272a
;
}
.phone-asset-modal
.el-dialog__footer
.el-button
:active
{
transform
:
translateY
(
1px
);
}
.phone-asset-modal
.el-dialog__footer
.el-button.is-disabled
,
.phone-asset-modal
.el-dialog__footer
.el-button.is-disabled
:hover
{
border-color
:
#f4f4f5
;
color
:
#a1a1aa
;
background
:
#f4f4f5
;
}
@media
(
max-width
:
600px
)
{
.phone-asset-modal-mask
.el-overlay-dialog
{
align-items
:
flex-start
;
padding
:
14px
;
}
.phone-asset-modal
{
width
:
100%
!important
;
max-height
:
calc
(
100vh
-
52px
);
margin-top
:
12px
!important
;
}
.phone-asset-modal
.el-dialog__header
,
.phone-asset-modal
.el-dialog__body
,
.phone-asset-modal
.el-dialog__footer
{
padding-left
:
18px
;
padding-right
:
18px
;
}
.phone-asset-modal
.el-dialog__body
{
max-height
:
calc
(
100vh
-
210px
);
}
.phone-asset-modal__form-row.el-form-item
{
grid-template-columns
:
minmax
(
0
,
1
fr
);
gap
:
7px
;
}
.phone-asset-modal__form-row
.el-form-item__label
{
width
:
auto
!important
;
text-align
:
left
;
}
}
/* 窄窗口中的资产表格在卡片内部处理宽度,避免宽列把整个页面撑出屏幕。 */
@media
(
max-width
:
760px
)
{
.content
{
min-width
:
0
;
}
.phone-asset-page
.reference-card
{
min-width
:
0
;
overflow-x
:
auto
;
}
}
/* 手机号弹窗下拉菜单:由 popper-class 限定,不影响其他页面的下拉选项。 */
.phone-asset-modal__select-popper.el-popper
{
border
:
1px
solid
#e4e4e7
;
border-radius
:
6px
;
box-shadow
:
0
10px
30px
rgba
(
15
,
23
,
42
,
0.14
);
}
.phone-asset-modal__select-popper
.el-select-dropdown__item
{
height
:
36px
;
padding
:
0
11px
;
color
:
#27272a
;
font-size
:
14px
;
line-height
:
36px
;
}
.phone-asset-modal__select-popper
.el-select-dropdown__item.is-hovering
,
.phone-asset-modal__select-popper
.el-select-dropdown__item
:hover
{
background
:
#fafafa
;
}
.phone-asset-modal__select-popper
.el-select-dropdown__item.is-selected
{
color
:
#18181b
;
font-weight
:
550
;
background
:
#f4f4f5
;
}
\ No newline at end of file
frontend/tests/phone-asset.spec.js
View file @
86d3179e
import
{
expect
,
test
}
from
'@playwright/test'
;
import
{
expect
,
test
}
from
'@playwright/test'
;
/**
/**
* 代码作用(白话):
模拟手机号资产列表接口,先证明新页面尚未实现时测试会失败,后续用于保护页面加载流程
。
* 代码作用(白话):
拦截手机号资产列表请求,给弹窗测试准备不依赖后端的空列表
。
* 关联文件:
frontend/src/modules/phone/PhoneAssetView.js、frontend/src/modules/phone/
phone-api-client.js。
* 关联文件:
PhoneAssetView.js、
phone-api-client.js。
* 关联逻辑(调用链/
消息链/数据流):浏览器打开手机号资产路由 -> View 请求列表 API -> route.fulfill 返回测试数据 -> 页面表格断言
。
* 关联逻辑(调用链/
数据流):页面打开 -> listPhoneAssets -> 浏览器路由拦截 -> 空列表 -> 新增弹窗
。
*/
*/
test
(
'phone asset workspace loads the first page from its API'
,
async
({
page
})
=>
{
async
function
mockPhoneAssetList
(
page
)
{
await
page
.
route
(
'**/api/phone-assets?**'
,
async
(
route
)
=>
{
await
page
.
route
(
'**/api/phone-assets?**'
,
async
(
route
)
=>
{
await
route
.
fulfill
({
await
route
.
fulfill
({
contentType
:
'application/json'
,
contentType
:
'application/json'
,
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
code
:
200
,
code
:
200
,
message
:
'success'
,
message
:
'success'
,
data
:
{
data
:
{
records
:
[],
total
:
0
,
page
:
1
,
size
:
20
}
records
:
[{
id
:
1
,
phoneNumber
:
'13812345678'
,
cardType
:
'中国移动'
,
disposalStatus
:
'正常使用'
}],
total
:
1
,
page
:
1
,
size
:
20
}
})
})
});
});
});
});
}
/**
* 代码作用(白话):打开新增手机号资产弹窗,供多个验收场景复用。
* 关联文件:PhoneAssetView.js、phone-asset.spec.js。
* 关联逻辑(调用链/数据流):测试页面 -> 新增按钮 -> openCreate -> dialogVisible -> 弹窗。
*/
async
function
openCreateDialog
(
page
)
{
await
mockPhoneAssetList
(
page
);
await
page
.
goto
(
'/#/phone-assets'
);
await
page
.
goto
(
'/#/phone-assets'
);
await
page
.
getByRole
(
'button'
,
{
name
:
'新增手机号资产'
}).
click
();
return
page
.
getByRole
(
'dialog'
,
{
name
:
'新增手机号资产'
});
}
/**
* 代码作用(白话):在浏览器中派发带剪贴板文字的真实粘贴事件。
* 关联文件:PhoneAssetView.js、phone-asset.spec.js。
* 关联逻辑(调用链/数据流):测试数据 -> paste 事件 -> handlePhonePaste/handleIccidPaste -> 表单字段。
*/
async
function
pasteText
(
locator
,
value
)
{
await
locator
.
evaluate
((
input
,
pastedValue
)
=>
{
const
clipboardData
=
new
DataTransfer
();
clipboardData
.
setData
(
'text'
,
pastedValue
);
input
.
dispatchEvent
(
new
ClipboardEvent
(
'paste'
,
{
bubbles
:
true
,
cancelable
:
true
,
clipboardData
}));
},
value
);
}
/**
* 代码作用(白话):验证新增弹窗保留七个字段,并将两个字数计数器放在各自输入框内部。
* 关联文件:PhoneAssetView.js、app.css。
* 关联逻辑(调用链/数据流):新增按钮 -> 弹窗渲染 -> 表单字段和 suffix 计数器。
*/
test
(
'新增弹窗保留七个字段,并在输入框内显示初始计数器'
,
async
({
page
})
=>
{
const
dialog
=
await
openCreateDialog
(
page
);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__form-row'
)).
toHaveCount
(
7
);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__character-count'
)).
toHaveText
([
'0/11'
,
'0/20'
]);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__character-count'
).
first
()).
toBeVisible
();
});
/**
* 代码作用(白话):验证手机号和 ICCID 的输入、粘贴、删除都会实时更新计数器且不会超过长度上限。
* 关联文件:PhoneAssetView.js、app.css。
* 关联逻辑(调用链/数据流):输入事件 -> limitPhoneNumber/limitIccid -> form -> suffix 计数器。
*/
test
(
'手机号和 ICCID 会过滤并截断超长输入,同时更新框内计数器'
,
async
({
page
})
=>
{
const
dialog
=
await
openCreateDialog
(
page
);
const
phoneInput
=
dialog
.
getByLabel
(
'手机号'
);
const
iccidInput
=
dialog
.
getByLabel
(
'ICCID'
);
await
pasteText
(
phoneInput
,
'138-1234567899'
);
await
expect
(
phoneInput
).
toHaveValue
(
'13812345678'
);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__character-count'
).
first
()).
toHaveText
(
'11/11'
);
await
phoneInput
.
press
(
'Control+A'
);
await
phoneInput
.
press
(
'Backspace'
);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__character-count'
).
first
()).
toHaveText
(
'0/11'
);
await
pasteText
(
iccidInput
,
'8986032594020999545B-extra'
);
await
expect
(
iccidInput
).
toHaveValue
(
'8986032594020999545B'
);
await
expect
(
dialog
.
locator
(
'.phone-asset-modal__character-count'
).
nth
(
1
)).
toHaveText
(
'20/20'
);
});
await
expect
(
page
.
getByRole
(
'heading'
,
{
name
:
'手机号资产'
})).
toBeVisible
();
/**
await
expect
(
page
.
getByText
(
'13812345678'
)).
toBeVisible
();
* 代码作用(白话):验证取消和右上角关闭按钮仍会关闭同一个业务弹窗。
await
expect
(
page
.
getByRole
(
'button'
,
{
name
:
'新增手机号资产'
})).
toBeVisible
();
* 关联文件:PhoneAssetView.js。
* 关联逻辑(调用链/数据流):按钮点击 -> dialogVisible=false -> Element Plus 弹窗关闭。
*/
test
(
'取消和右上角关闭按钮仍可关闭弹窗'
,
async
({
page
})
=>
{
let
dialog
=
await
openCreateDialog
(
page
);
await
dialog
.
getByRole
(
'button'
,
{
name
:
'取消'
}).
click
();
await
expect
(
dialog
).
toBeHidden
();
dialog
=
await
openCreateDialog
(
page
);
await
dialog
.
locator
(
'.el-dialog__headerbtn'
).
click
();
await
expect
(
dialog
).
toBeHidden
();
});
/**
* 代码作用(白话):验证确认保存仍将原有表单字段通过新增接口提交。
* 关联文件:PhoneAssetView.js、phone-api-client.js、PhoneAssetController.java。
* 关联逻辑(调用链/数据流):确认保存 -> createPhoneAsset -> POST /api/phone-assets -> 成功提示和关闭弹窗。
*/
test
(
'确认保存仍调用原有新增接口并提交表单数据'
,
async
({
page
})
=>
{
let
submittedPayload
;
await
page
.
route
(
'**/api/phone-assets'
,
async
(
route
)
=>
{
if
(
route
.
request
().
method
()
!==
'POST'
)
return
route
.
fallback
();
submittedPayload
=
route
.
request
().
postDataJSON
();
await
route
.
fulfill
({
contentType
:
'application/json'
,
body
:
JSON
.
stringify
({
code
:
200
,
message
:
'新增成功'
,
data
:
{
id
:
1
}
})
});
});
const
dialog
=
await
openCreateDialog
(
page
);
await
dialog
.
getByLabel
(
'手机号'
).
fill
(
'13812345678'
);
await
dialog
.
getByRole
(
'button'
,
{
name
:
'确认保存'
}).
click
();
await
expect
.
poll
(()
=>
submittedPayload
?.
phoneNumber
).
toBe
(
'13812345678'
);
await
expect
(
dialog
).
toBeHidden
();
});
/**
* 代码作用(白话):验证窄窗口里的弹窗保持在可视区域内,页面不出现横向滚动条。
* 关联文件:PhoneAssetView.js、app.css。
* 关联逻辑(调用链/数据流):窄视口 -> 响应式 CSS -> 单列表单和受限弹窗宽度。
*/
test
(
'窄窗口下弹窗不超出屏幕且页面没有横向滚动'
,
async
({
page
})
=>
{
await
page
.
setViewportSize
({
width
:
320
,
height
:
640
});
const
dialog
=
await
openCreateDialog
(
page
);
const
bounds
=
await
dialog
.
boundingBox
();
expect
(
bounds
).
not
.
toBeNull
();
expect
(
bounds
.
x
).
toBeGreaterThanOrEqual
(
0
);
expect
(
bounds
.
x
+
bounds
.
width
).
toBeLessThanOrEqual
(
320
);
await
expect
.
poll
(()
=>
page
.
evaluate
(()
=>
document
.
documentElement
.
scrollWidth
<=
window
.
innerWidth
)).
toBe
(
true
);
});
});
\ No newline at end of file
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