Commit 85fee5d2 by DaiJiezhang

Add phone backend support

parent ee4045b8
...@@ -9,9 +9,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; ...@@ -9,9 +9,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
public class XywConsoleBackendApplication { public class XywConsoleBackendApplication {
/** /**
* 代码作用(白话):启动企微后端服务,让前端可以通过 localhost:8080 访问真实接口;关联文件:F:/Project/xyw_console/backend/src/main/resources/application.yml、F:/Project/xyw_console/start.bat;关联逻辑(调用链/消息链/数据流):mvn spring-boot:run -> main() -> Spring 容器启动 -> Controller 对外提供 HTTP 接口。 * 代码作用(白话):启动后端服务,让前端可以通过 localhost:8080 访问真实接口。
* 关联文件:F:/Project/xyw_console/backend/src/main/resources/application.yml、F:/Project/xyw_console/start.bat。
* 关联逻辑(调用链/消息链/数据流):main() -> Spring 容器启动 -> Controller 提供 HTTP 接口。
*/ */
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(XywConsoleBackendApplication.class, args); SpringApplication.run(XywConsoleBackendApplication.class, args);
} }
} }
...@@ -8,13 +8,15 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; ...@@ -8,13 +8,15 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebConfig implements WebMvcConfigurer { public class WebConfig implements WebMvcConfigurer {
/** /**
* 代码作用(白话):允许本地 8000 端口的前端页面跨域调用 8080 后端接口,否则浏览器会直接拦截请求;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/start.bat;关联逻辑(调用链/消息链/数据流):浏览器 fetch(http://localhost:8080) -> addCorsMappings() 放行 -> Controller 正常接收请求。 * 代码作用(白话):允许本地 8000 端口前端跨域访问后端接口,避免浏览器把 fetch 请求挡掉。
* 关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js。
* 关联逻辑(调用链/消息链/数据流):浏览器 fetch -> /api/** -> addCorsMappings() -> 后端控制器正常返回 JSON。
*/ */
@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")
.allowedMethods("GET", "POST", "PUT", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*"); .allowedHeaders("*");
} }
} }
package com.xyw.console.phone.config;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan(basePackages = "com.xyw.console.phone.mapper", sqlSessionTemplateRef = "phoneSqlSessionTemplate")
public class PhoneMybatisConfig {
/**
* 代码作用(白话):读取手机号模块独立数据库的连接配置,和主库配置分开,避免 phone 表误连到别的库。
* 关联文件:F:/Project/xyw_console/backend/src/main/resources/application.yml。
* 关联逻辑(调用链/消息链/数据流):application.yml -> app.datasource.phone -> phoneDataSourceProperties() -> 后续数据源初始化。
*/
@Bean
@ConfigurationProperties(prefix = "app.datasource.phone")
public DataSourceProperties phoneDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 代码作用(白话):按 phone 独立配置创建 MySQL 连接池,让 wx_phone 走 xyw_data_test 库而不影响主库。
* 关联文件:F:/Project/xyw_console/backend/src/main/resources/application.yml、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/mapper/WxPhoneMapper.java。
* 关联逻辑(调用链/消息链/数据流):phoneDataSourceProperties() -> phoneDataSource() -> phoneSqlSessionFactory() -> WxPhoneMapper。
*/
@Bean(name = "phoneDataSource")
public DataSource phoneDataSource(@Qualifier("phoneDataSourceProperties") DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
/**
* 代码作用(白话):给手机号模块单独建 MyBatis-Plus 会话工厂,让 phone Mapper 只连自己的数据源。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/mapper/WxPhoneMapper.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/entity/WxPhoneEntity.java。
* 关联逻辑(调用链/消息链/数据流):phoneDataSource() -> phoneSqlSessionFactory() -> MyBatis-Plus BaseMapper -> CRUD SQL。
*/
@Bean(name = "phoneSqlSessionFactory")
public SqlSessionFactory phoneSqlSessionFactory(@Qualifier("phoneDataSource") DataSource phoneDataSource)
throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(phoneDataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setLogImpl(StdOutImpl.class);
factoryBean.setConfiguration(configuration);
factoryBean.setTypeAliasesPackage("com.xyw.console.phone.entity");
SqlSessionFactory sqlSessionFactory = factoryBean.getObject();
if (sqlSessionFactory == null) {
throw new IllegalStateException("手机号数据源 SqlSessionFactory 初始化失败");
}
return sqlSessionFactory;
}
/**
* 代码作用(白话):把手机号模块的 SqlSessionFactory 包成模板对象,供 @MapperScan 指向 phone mapper 使用。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/mapper/WxPhoneMapper.java。
* 关联逻辑(调用链/消息链/数据流):phoneSqlSessionFactory() -> phoneSqlSessionTemplate() -> WxPhoneMapper CRUD 执行。
*/
@Bean(name = "phoneSqlSessionTemplate")
public SqlSessionTemplate phoneSqlSessionTemplate(@Qualifier("phoneSqlSessionFactory") SqlSessionFactory phoneSqlSessionFactory) {
return new SqlSessionTemplate(phoneSqlSessionFactory);
}
}
package com.xyw.console.phone.controller;
import com.xyw.console.common.ApiResponse;
import com.xyw.console.phone.dto.WxPhoneSaveRequest;
import com.xyw.console.phone.entity.WxPhoneEntity;
import com.xyw.console.phone.service.WxPhoneService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/wx-phones")
@RequiredArgsConstructor
public class WxPhoneController {
private final WxPhoneService wxPhoneService;
/**
* 代码作用(白话):返回手机号卡列表,给前端列表页和筛选页直接用。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/service/WxPhoneService.java、F:/Project/xyw_console/src/modules/shared/phone-api-client.js。
* 关联逻辑(调用链/消息链/数据流):GET /api/wx-phones -> listRecords() -> ApiResponse.success(data) -> 前端列表渲染。
*/
@GetMapping
public ResponseEntity<ApiResponse<List<WxPhoneEntity>>> listRecords() {
return ResponseEntity.ok(ApiResponse.success(wxPhoneService.listRecords()));
}
/**
* 代码作用(白话):按 id 读取单条手机号卡,给详情抽屉和编辑弹窗用。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/service/WxPhoneService.java、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js。
* 关联逻辑(调用链/消息链/数据流):GET /api/wx-phones/{id} -> getRecordById() -> ApiResponse.success(data) -> 详情/编辑打开。
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<WxPhoneEntity>> getRecord(@PathVariable Long id) {
WxPhoneEntity entity = wxPhoneService.getRecordById(id);
if (entity == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.<WxPhoneEntity>error(404, "未找到对应手机号卡"));
}
return ResponseEntity.ok(ApiResponse.success(entity));
}
/**
* 代码作用(白话):校验并新增手机号卡,新增成功后返回最新入库结果。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js。
* 关联逻辑(调用链/消息链/数据流):POST /api/wx-phones -> validateSaveRequest() -> createRecord() -> 列表刷新。
*/
@PostMapping
public ResponseEntity<ApiResponse<WxPhoneEntity>> createRecord(@RequestBody WxPhoneSaveRequest request) {
String validationMessage = validateSaveRequest(request);
if (validationMessage != null) {
return ResponseEntity.badRequest().body(ApiResponse.<WxPhoneEntity>error(400, validationMessage));
}
WxPhoneEntity entity = wxPhoneService.createRecord(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success("新增成功", entity));
}
/**
* 代码作用(白话):校验并编辑指定手机号卡,编辑成功后返回最新数据库记录。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js。
* 关联逻辑(调用链/消息链/数据流):PUT /api/wx-phones/{id} -> validateSaveRequest() -> updateRecord() -> 列表刷新。
*/
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<WxPhoneEntity>> updateRecord(@PathVariable Long id, @RequestBody WxPhoneSaveRequest request) {
String validationMessage = validateSaveRequest(request);
if (validationMessage != null) {
return ResponseEntity.badRequest().body(ApiResponse.<WxPhoneEntity>error(400, validationMessage));
}
WxPhoneEntity entity = wxPhoneService.updateRecord(id, request);
if (entity == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.<WxPhoneEntity>error(404, "未找到对应手机号卡"));
}
return ResponseEntity.ok(ApiResponse.success("编辑成功", entity));
}
/**
* 代码作用(白话):删除指定手机号卡,删除成功后给前端一个明确结果。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/service/WxPhoneService.java、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js。
* 关联逻辑(调用链/消息链/数据流):DELETE /api/wx-phones/{id} -> deleteRecord() -> ApiResponse.success() -> 列表重新加载。
*/
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Boolean>> deleteRecord(@PathVariable Long id) {
boolean deleted = wxPhoneService.deleteRecord(id);
if (!deleted) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.<Boolean>error(404, "未找到对应手机号卡"));
}
return ResponseEntity.ok(ApiResponse.success("删除成功", Boolean.TRUE));
}
/**
* 代码作用(白话):集中校验新增和编辑的必填项,避免空手机号或空运营商直接写进数据库。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js。
* 关联逻辑(调用链/消息链/数据流):createRecord()/updateRecord() -> validateSaveRequest() -> 返回 400 或继续保存。
*/
public String validateSaveRequest(WxPhoneSaveRequest request) {
if (request == null) {
return "请求体不能为空";
}
if (!hasText(request.getPhoneNumber())) {
return "手机号不能为空";
}
if (!hasText(request.getChannelOperator())) {
return "渠道运营商不能为空";
}
return null;
}
/**
* 代码作用(白话):判断字符串是不是有效内容,给表单必填校验复用。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/service/WxPhoneService.java。
* 关联逻辑(调用链/消息链/数据流):validateSaveRequest() -> hasText() -> 决定返回 400 还是继续保存。
*/
public boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
}
package com.xyw.console.phone.dto;
import lombok.Data;
@Data
public class WxPhoneSaveRequest {
private Long id;
private String phoneNumber;
private String realPerson;
private String iccid;
private String city;
private String imageAttachment1;
private String imageAttachment2;
private String cardStatus;
private String cardUsageLocation;
private Boolean wecom;
private Boolean wechat;
private Boolean outboundCall;
private String douyinAccount;
private Boolean miniProgramFiling;
private Boolean packageChange5yuan;
private String channelOperator;
private Boolean numberStatus;
private String linkedWecom;
private Boolean numberRetentionStatus;
}
package com.xyw.console.phone.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("wx_phone")
public class WxPhoneEntity {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("phone_number")
private String phoneNumber;
@TableField("real_person")
private String realPerson;
@TableField("iccid")
private String iccid;
@TableField("city")
private String city;
@TableField("image_attachment_1")
private String imageAttachment1;
@TableField("image_attachment_2")
private String imageAttachment2;
@TableField("card_status")
private String cardStatus;
@TableField("card_usage_location")
private String cardUsageLocation;
@TableField("wecom")
private Boolean wecom;
@TableField("wechat")
private Boolean wechat;
@TableField("outbound_call")
private Boolean outboundCall;
@TableField("douyin_account")
private String douyinAccount;
@TableField("mini_program_filing")
private Boolean miniProgramFiling;
@TableField("package_change_5yuan")
private Boolean packageChange5yuan;
@TableField("channel_operator")
private String channelOperator;
@TableField("mobile_status")
private Boolean numberStatus;
@TableField("linked_wecom")
private String linkedWecom;
@TableField("number_retention_status")
private Boolean numberRetentionStatus;
@TableField(exist = false)
private String wechatStatus;
@TableField(exist = false)
private String outbound;
}
package com.xyw.console.phone.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xyw.console.phone.entity.WxPhoneEntity;
public interface WxPhoneMapper extends BaseMapper<WxPhoneEntity> {
}
package com.xyw.console.phone.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xyw.console.phone.dto.WxPhoneSaveRequest;
import com.xyw.console.phone.entity.WxPhoneEntity;
import com.xyw.console.phone.mapper.WxPhoneMapper;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class WxPhoneService {
private final WxPhoneMapper wxPhoneMapper;
/**
* 代码作用(白话):把手机号列表按 id 倒序读出来,给前端列表页直接渲染。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/src/modules/shared/phone-api-client.js。
* 关联逻辑(调用链/消息链/数据流):GET /api/wx-phones -> listRecords() -> Mapper.selectList() -> 前端 normalizePhoneRecord() -> 列表展示。
*/
public List<WxPhoneEntity> listRecords() {
return wxPhoneMapper.selectList(new QueryWrapper<WxPhoneEntity>().orderByDesc("id"))
.stream()
.map(this::normalizeEntity)
.toList();
}
/**
* 代码作用(白话):按主键读一条手机号卡,给详情抽屉和编辑弹窗用。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js。
* 关联逻辑(调用链/消息链/数据流):GET /api/wx-phones/{id} -> getRecordById() -> Mapper.selectById() -> 详情/编辑打开。
*/
public WxPhoneEntity getRecordById(Long id) {
return normalizeEntity(wxPhoneMapper.selectById(id));
}
/**
* 代码作用(白话):把前端新增表单写入 wx_phone 表,并返回带主键的新记录。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js。
* 关联逻辑(调用链/消息链/数据流):POST /api/wx-phones -> createRecord() -> copyRequestToEntity() -> Mapper.insert() -> 前端刷新列表。
*/
public WxPhoneEntity createRecord(WxPhoneSaveRequest request) {
WxPhoneEntity entity = new WxPhoneEntity();
copyRequestToEntity(request, entity);
wxPhoneMapper.insert(entity);
return normalizeEntity(wxPhoneMapper.selectById(entity.getId()));
}
/**
* 代码作用(白话):更新指定手机号卡,并把数据库里的最新记录返回给前端。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js。
* 关联逻辑(调用链/消息链/数据流):PUT /api/wx-phones/{id} -> updateRecord() -> Mapper.updateById() -> 前端刷新列表。
*/
public WxPhoneEntity updateRecord(Long id, WxPhoneSaveRequest request) {
WxPhoneEntity entity = wxPhoneMapper.selectById(id);
if (entity == null) {
return null;
}
copyRequestToEntity(request, entity);
wxPhoneMapper.updateById(entity);
return normalizeEntity(wxPhoneMapper.selectById(id));
}
/**
* 代码作用(白话):删除指定手机号卡,给控制器判断是否需要返回 404。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js。
* 关联逻辑(调用链/消息链/数据流):DELETE /api/wx-phones/{id} -> deleteRecord() -> Mapper.deleteById() -> 列表重新加载。
*/
public boolean deleteRecord(Long id) {
return wxPhoneMapper.deleteById(id) > 0;
}
/**
* 代码作用(白话):把接口入参安全地拷贝到数据库实体里,顺手做空白清洗和默认值处理。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/entity/WxPhoneEntity.java。
* 关联逻辑(调用链/消息链/数据流):createRecord()/updateRecord() -> copyRequestToEntity() -> Entity -> Mapper 持久化。
*/
public void copyRequestToEntity(WxPhoneSaveRequest request, WxPhoneEntity entity) {
entity.setPhoneNumber(normalizeText(request.getPhoneNumber()));
entity.setRealPerson(normalizeText(request.getRealPerson()));
entity.setIccid(normalizeText(request.getIccid()));
entity.setCity(normalizeText(request.getCity()));
entity.setImageAttachment1(normalizeText(request.getImageAttachment1()));
entity.setImageAttachment2(normalizeText(request.getImageAttachment2()));
entity.setCardStatus(normalizeCardStatus(request.getCardStatus()));
entity.setCardUsageLocation(normalizeText(request.getCardUsageLocation()));
entity.setWecom(normalizeBoolean(request.getWecom(), Boolean.FALSE));
entity.setWechat(normalizeBoolean(request.getWechat(), Boolean.FALSE));
entity.setOutboundCall(normalizeBoolean(request.getOutboundCall(), Boolean.FALSE));
entity.setDouyinAccount(normalizeText(request.getDouyinAccount()));
entity.setMiniProgramFiling(normalizeBoolean(request.getMiniProgramFiling(), Boolean.FALSE));
entity.setPackageChange5yuan(normalizeBoolean(request.getPackageChange5yuan(), Boolean.FALSE));
entity.setChannelOperator(normalizeText(request.getChannelOperator()));
entity.setNumberStatus(normalizeBoolean(request.getNumberStatus(), Boolean.TRUE));
entity.setLinkedWecom(normalizeText(request.getLinkedWecom()));
entity.setNumberRetentionStatus(normalizeBoolean(request.getNumberRetentionStatus(), Boolean.TRUE));
entity.setWechatStatus(Boolean.TRUE.equals(entity.getWechat()) ? "正常" : "异常");
entity.setOutbound(Boolean.TRUE.equals(entity.getOutboundCall()) ? "可外呼" : "不可外呼");
}
/**
* 代码作用(白话):把数据库里取回来的手机号卡统一整理成前端能直接吃的状态文本和空值格式。
* 关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js。
* 关联逻辑(调用链/消息链/数据流):Mapper.selectList()/selectById() -> normalizeEntity() -> controller response -> 前端 normalizePhoneRecord()。
*/
public WxPhoneEntity normalizeEntity(WxPhoneEntity entity) {
if (entity == null) {
return null;
}
entity.setPhoneNumber(normalizeText(entity.getPhoneNumber()));
entity.setRealPerson(normalizeText(entity.getRealPerson()));
entity.setIccid(normalizeText(entity.getIccid()));
entity.setCity(normalizeText(entity.getCity()));
entity.setImageAttachment1(normalizeText(entity.getImageAttachment1()));
entity.setImageAttachment2(normalizeText(entity.getImageAttachment2()));
entity.setCardStatus(normalizeCardStatus(entity.getCardStatus()));
entity.setCardUsageLocation(normalizeText(entity.getCardUsageLocation()));
entity.setDouyinAccount(normalizeText(entity.getDouyinAccount()));
entity.setChannelOperator(normalizeText(entity.getChannelOperator()));
entity.setLinkedWecom(normalizeText(entity.getLinkedWecom()));
entity.setWechatStatus(Boolean.TRUE.equals(entity.getWechat()) ? "正常" : "异常");
entity.setOutbound(Boolean.TRUE.equals(entity.getOutboundCall()) ? "可外呼" : "不可外呼");
return entity;
}
/**
* 代码作用(白话):把前端可能传来的 0/1、正常/异常等卡状态,统一成页面能稳定显示的文本。
* 关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js。
* 关联逻辑(调用链/消息链/数据流):request.cardStatus/DB.card_status -> normalizeCardStatus() -> response.cardStatus -> 列表和详情显示。
*/
public String normalizeCardStatus(String value) {
String normalized = normalizeText(value);
if (normalized == null) {
return "正常";
}
return switch (normalized) {
case "1", "正常", "正常在用" -> "正常";
case "0", "异常", "异常停机", "停机" -> "异常停机";
default -> normalized;
};
}
/**
* 代码作用(白话):把字符串里的首尾空格去掉,空白内容直接当成没填。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/entity/WxPhoneEntity.java。
* 关联逻辑(调用链/消息链/数据流):request/entity 字段 -> normalizeText() -> 入库/出库前的统一清洗。
*/
public String normalizeText(String value) {
if (value == null) {
return null;
}
String trimmedValue = value.trim();
return trimmedValue.isEmpty() ? null : trimmedValue;
}
/**
* 代码作用(白话):给可空布尔字段补默认值,避免数据库和页面出现一堆 null。
* 关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/entity/WxPhoneEntity.java。
* 关联逻辑(调用链/消息链/数据流):request.Boolean -> normalizeBoolean() -> Entity.Boolean -> MyBatis-Plus 保存。
*/
public Boolean normalizeBoolean(Boolean value, Boolean defaultValue) {
return value == null ? defaultValue : value;
}
}
...@@ -43,14 +43,15 @@ public class WxDataEntity { ...@@ -43,14 +43,15 @@ public class WxDataEntity {
private String realNameOwner; private String realNameOwner;
@JsonProperty("account_location") @JsonProperty("account_location")
@TableField("账号位置") @TableField("account_position")
private String accountLocation; private String accountLocation;
@JsonProperty("phone_image") @JsonProperty("phone_image")
@TableField("手机图片") @TableField("phone_image_url")
private String phoneImage; private String phoneImage;
@JsonProperty("real_name_owner_status") @JsonProperty("real_name_owner_status")
@TableField("real_name_owner_status") @TableField("real_name_owner_status")
private Integer realNameOwnerStatus; private Integer realNameOwnerStatus;
} }
...@@ -12,6 +12,41 @@ ...@@ -12,6 +12,41 @@
'calc((100% - 48px) * 0.12)' 'calc((100% - 48px) * 0.12)'
]; ];
/**
* 代码作用(白话):把各种日期输入统一转成可比较的时间戳,方便手机号卡页面按更新时间筛选;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):表格记录.updatedAt -> toTimestamp() -> matchesDateRange() -> filteredRecords()。
*/
function toTimestamp(value) {
if (!value) {
return NaN;
}
if (value instanceof Date) {
return value.getTime();
}
const normalizedValue = String(value).replace(/-/g, '/');
return new Date(normalizedValue).getTime();
}
/**
* 代码作用(白话):判断一条手机号记录是否落在当前日期筛选范围内,没有选择日期时直接放行;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):filterDate -> matchesDateRange() -> filteredRecords() -> visibleRows()。
*/
function matchesDateRange(record, rangeValue) {
if (!Array.isArray(rangeValue) || rangeValue.length !== 2) {
return true;
}
const recordTime = toTimestamp(record.updatedAt);
const startTime = toTimestamp(rangeValue[0]);
const endTime = toTimestamp(rangeValue[1]);
if (Number.isNaN(recordTime) || Number.isNaN(startTime) || Number.isNaN(endTime)) {
return true;
}
return recordTime >= startTime && recordTime <= endTime + 24 * 60 * 60 * 1000 - 1;
}
const PhoneListTable = { const PhoneListTable = {
name: 'PhoneListTable', name: 'PhoneListTable',
props: { props: {
...@@ -27,6 +62,9 @@ ...@@ -27,6 +62,9 @@
getPhoneStatusDotClass: { type: Function, required: true }, getPhoneStatusDotClass: { type: Function, required: true },
notifyError: { type: Function, required: true } notifyError: { type: Function, required: true }
}, },
/**
* 代码作用(白话):初始化手机号卡页面的分页、筛选、选中和操作入口,让壳层传入的数据与动作变成真正可交互的表格;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime('phone') -> mountPhoneListComponent() -> PhoneListTable.setup() -> 列表渲染/弹窗/抽屉/删除。
*/
setup(props) { setup(props) {
const { computed, ref } = window.Vue; const { computed, ref } = window.Vue;
const currentPage = ref(props.mountState.currentPage || 1); const currentPage = ref(props.mountState.currentPage || 1);
...@@ -43,21 +81,33 @@ ...@@ -43,21 +81,33 @@
const multipleSelection = ref([]); const multipleSelection = ref([]);
const multipleTableRef = ref(null); const multipleTableRef = ref(null);
const totalCount = computed(() => allRecordsCache.value.length); /**
const normalCount = computed(() => allRecordsCache.value.filter((record) => record.status === '正常').length); * 代码作用(白话):从当前缓存里提取运营商选项,避免筛选下拉框写死,后端换数据时前端还能跟着显示;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):loadAllRecords() -> allRecordsCache -> getCarrierOptions() -> 运营商筛选下拉。
const abnormalCount = computed(() => allRecordsCache.value.filter((record) => record.status !== '正常').length); */
function getCarrierOptions() {
const values = allRecordsCache.value
.map((record) => record.carrier)
.filter(Boolean);
return Array.from(new Set(values));
}
/** /**
* 代码作用(白话):把分页、每页条数和勾选结果同步回外层挂载状态,保证路由切换后还能恢复现场;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):列表交互 -> syncMountState() -> mountState -> 下次重新挂载复用 * 代码作用(白话):从当前缓存里提取外呼能力选项,保证筛选项和真实记录一致;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):loadAllRecords() -> allRecordsCache -> getOutboundOptions() -> 外呼能力筛选下拉
*/ */
function syncMountState() { function getOutboundOptions() {
props.mountState.currentPage = currentPage.value; const values = allRecordsCache.value
props.mountState.pageSize = pageSize.value; .map((record) => record.outbound)
props.mountState.selectedIds = [].concat(checkedIds.value); .filter(Boolean);
return Array.from(new Set(values));
} }
const carrierOptions = computed(getCarrierOptions);
const outboundOptions = computed(getOutboundOptions);
/** /**
* 代码作用(白话):向外层统一数据入口请求手机号列表,并把结果缓存到当前组件;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):页面挂载/删除成功/保存成功 -> loadAllRecords() -> loadPhoneRows() -> 列表渲染 * 代码作用(白话):向后端拉手机号卡数据并刷新本地缓存,失败时把错误交给壳层提示;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):PhoneListTable mounted/删除后刷新 -> loadAllRecords() -> props.loadPhoneRows() -> allRecordsCache -> filteredRecords()/visibleRows()
*/ */
async function loadAllRecords() { async function loadAllRecords() {
loading.value = true; loading.value = true;
...@@ -72,148 +122,190 @@ ...@@ -72,148 +122,190 @@
} }
} }
const filteredRecords = computed(() => { /**
let result = allRecordsCache.value; * 代码作用(白话):按状态、运营商、外呼能力、日期和关键词筛出当前应显示的手机号记录;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):筛选控件 -> getFilteredRecords() -> totalFiltered()/visibleRows() -> 表格。
*/
function getFilteredRecords() {
let result = allRecordsCache.value.slice();
if (filterStatus.value !== 'all') { if (filterStatus.value !== 'all') {
result = result.filter((record) => record.status === filterStatus.value); result = result.filter((record) => record.status === filterStatus.value);
} }
if (filterCarrier.value !== 'all') { if (filterCarrier.value !== 'all') {
result = result.filter((record) => record.carrier === filterCarrier.value); result = result.filter((record) => record.carrier === filterCarrier.value);
} }
if (filterOutbound.value !== 'all') { if (filterOutbound.value !== 'all') {
result = result.filter((record) => record.outbound === filterOutbound.value); result = result.filter((record) => record.outbound === filterOutbound.value);
} }
if (filterDate.value && filterDate.value.length === 2) {
const start = new Date(filterDate.value[0]).getTime(); result = result.filter((record) => matchesDateRange(record, filterDate.value));
const end = new Date(filterDate.value[1]).getTime() + 86400000;
result = result.filter((record) => { const query = searchQuery.value.trim().toLowerCase();
if (!record.updatedAt) return false; if (!query) {
const updatedAt = new Date(record.updatedAt).getTime(); return result;
return updatedAt >= start && updatedAt < end;
});
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter((record) => {
return (record.phone && record.phone.toLowerCase().includes(query)) ||
(record.realPerson && record.realPerson.toLowerCase().includes(query)) ||
(record.city && record.city.toLowerCase().includes(query));
});
} }
return result;
});
const totalFiltered = computed(() => filteredRecords.value.length); return result.filter((record) => {
const totalPages = computed(() => Math.max(1, Math.ceil(totalFiltered.value / pageSize.value))); return [
const visibleRows = computed(() => { record.phone,
record.realPerson,
record.city,
record.carrier,
record.project,
record.owner,
record.usageLocation,
record.iccid
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(query));
});
}
const filteredRecords = computed(getFilteredRecords);
/**
* 代码作用(白话):返回筛选后的总条数,给标题统计和分页器共用;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):getFilteredRecords() -> getTotalFiltered() -> 工具栏标题 / 分页 total。
*/
function getTotalFiltered() {
return filteredRecords.value.length;
}
/**
* 代码作用(白话):根据当前页大小算出总页数,防止分页器落到 0 页;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):filteredRecords/pageSize -> getTotalPages() -> handleFilterStateChange()/分页器。
*/
function getTotalPages() {
return Math.max(1, Math.ceil(totalFiltered.value / pageSize.value));
}
/**
* 代码作用(白话):切出当前页应显示的手机号记录,让表格只渲染这一页数据;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):filteredRecords/currentPage/pageSize -> getVisibleRows() -> el-table。
*/
function getVisibleRows() {
const start = (currentPage.value - 1) * pageSize.value; const start = (currentPage.value - 1) * pageSize.value;
return filteredRecords.value.slice(start, start + pageSize.value); return filteredRecords.value.slice(start, start + pageSize.value);
}); }
const totalFiltered = computed(getTotalFiltered);
const totalPages = computed(getTotalPages);
const visibleRows = computed(getVisibleRows);
window.Vue.watch([filterStatus, filterCarrier, filterOutbound, filterDate, searchQuery], () => { /**
* 代码作用(白话):筛选条件变化后把页码拉回第一页,并把当前组件状态同步回壳层缓存;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):筛选控件变化 -> handleFilterStateChange() -> currentPage/syncMountState() -> 再次渲染。
*/
function handleFilterStateChange() {
currentPage.value = 1; currentPage.value = 1;
syncMountState(); syncMountState();
}); }
window.Vue.watch([filterStatus, filterCarrier, filterOutbound, filterDate, searchQuery], handleFilterStateChange);
/** /**
* 代码作用(白话):记录当前聚焦的手机号行,并同步给外层全局状态,保证查看详情和返回列表时选中项一致;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):表格行点击/操作按钮 -> focusRow() -> globalState.selectedBySource.phone -> 详情抽屉或下次挂载 * 代码作用(白话):把当前页码、分页大小和批量勾选状态存回壳层 mountState,保证切走再切回来还能恢复现场;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):列表交互 -> syncMountState() -> moduleMountState.phone -> renderExternalRuntime('phone') 恢复
*/ */
function focusRow(recordId) { function syncMountState() {
selectedRecordId.value = recordId; props.mountState.currentPage = currentPage.value;
props.globalState.selectedBySource.phone = recordId; props.mountState.pageSize = pageSize.value;
syncMountState(); props.mountState.selectedIds = checkedIds.value.slice();
} }
/** /**
* 代码作用(白话):判断一行手机号是否为当前选中行,用于表格高亮;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):selectedRecordId -> isRowSelected() -> 行 class 绑定 * 代码作用(白话):给表格行判断是否是当前选中的手机号记录,用于高亮或兼容旧壳层选中态;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):focusRow()/openDrawer() -> selectedRecordId -> isRowSelected() -> 表格选中表现
*/ */
function isRowSelected(recordId) { function isRowSelected(rowId) {
return selectedRecordId.value === recordId; return selectedRecordId.value === rowId;
} }
/** /**
* 代码作用(白话):把状态文字映射到外层约定的圆点颜色 class,保持状态样式统一;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):row.status/wechatStatus/outbound -> getStatusClass() -> status-dot class。 * 代码作用(白话):把不同状态映射成状态点颜色,让正常、异常、外呼能力等字段在列表里更直观;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):status/wechatStatus/outbound -> getStatusClass() -> props.getPhoneStatusDotClass() -> status-dot class。
*/ */
function getStatusClass(status) { function getStatusClass(status) {
return props.getPhoneStatusDotClass(status); return props.getPhoneStatusDotClass(status);
} }
/** /**
* 代码作用(白话):同步 Element Plus 表格的多选结果,给底部统计和路由恢复使用;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):selection-change -> handleSelectionChange() -> checkedIds/mountState * 代码作用(白话):把某一行设为当前选中手机号,并同步到壳层全局状态,方便详情抽屉和编辑弹窗拿到当前对象;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):点击表格手机号/操作按钮 -> focusRow() -> globalState.selectedBySource.phone -> openDrawer()/详情联动
*/ */
function handleSelectionChange(rows) { function focusRow(recordId) {
multipleSelection.value = rows; selectedRecordId.value = recordId;
checkedIds.value = rows.map((record) => record.id); props.globalState.selectedBySource.phone = recordId;
}
/**
* 代码作用(白话):重置手机号页所有筛选条件和分页位置,回到默认浏览视图;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):点击“重置” -> resetTableState() -> 筛选 ref 清空 -> filteredRecords()/visibleRows() 重算。
*/
function resetTableState() {
filterDate.value = '';
searchQuery.value = '';
filterStatus.value = 'all';
filterCarrier.value = 'all';
filterOutbound.value = 'all';
currentPage.value = 1;
syncMountState(); syncMountState();
} }
/** /**
* 代码作用(白话):处理分页页码变化,并把结果写回外层挂载状态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):分页器 current-change -> handleCurrentChange() -> currentPage/mountState * 代码作用(白话):同步表格勾选结果,给底部已选数量和壳层缓存共用;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):el-table selection-change -> handleSelectionChange() -> checkedIds/mountState -> 底部统计
*/ */
function handleCurrentChange(pageNumber) { function handleSelectionChange(rows) {
currentPage.value = Math.min(Math.max(1, pageNumber), totalPages.value); multipleSelection.value = rows.slice();
checkedIds.value = rows.map((row) => row.id);
syncMountState(); syncMountState();
} }
/** /**
* 代码作用(白话):处理每页条数变化,并把当前页重置回第一页,避免落在空页;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):分页器 size-change -> handleSizeChange() -> pageSize/currentPage -> visibleRows * 代码作用(白话):切页时更新当前页码并回写壳层状态;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):el-pagination current-change -> handleCurrentChange() -> currentPage -> visibleRows()
*/ */
function handleSizeChange(size) { function handleCurrentChange(page) {
pageSize.value = size; currentPage.value = Math.min(Math.max(page, 1), totalPages.value);
currentPage.value = 1;
syncMountState(); syncMountState();
} }
/** /**
* 代码作用(白话):把筛选、搜索、分页和多选状态恢复成初始值,方便用户重新开始筛选;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):重置按钮 -> resetTableState() -> 各筛选状态归零 -> mountState 同步 * 代码作用(白话):切换每页条数时重置到第一页,避免分页落在不存在的页码上;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):el-pagination size-change -> handleSizeChange() -> pageSize/currentPage -> visibleRows()
*/ */
function resetTableState() { function handleSizeChange(size) {
pageSize.value = size;
currentPage.value = 1; currentPage.value = 1;
pageSize.value = props.phoneTablePageSizes[0];
checkedIds.value = selectedRecordId.value ? [selectedRecordId.value] : [];
filterStatus.value = 'all';
filterCarrier.value = 'all';
filterOutbound.value = 'all';
filterDate.value = '';
searchQuery.value = '';
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
}
syncMountState(); syncMountState();
} }
/** /**
* 代码作用(白话):统一处理手机号列表的查看、编辑、删除动作,把副作用交给外层注入的动作入口;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):操作按钮 -> handleRowAction() -> openDrawer()/deletePhoneRecord() -> 详情抽屉或接口删除 -> reload。 * 代码作用(白话):处理查看、编辑、删除三类行操作,并在删除后主动刷新列表;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):操作按钮 -> handleRowAction() -> openDrawer()/deletePhoneRecord() -> reload。
*/ */
async function handleRowAction(mode, recordId) { async function handleRowAction(mode, recordId) {
focusRow(recordId); focusRow(recordId);
const record = allRecordsCache.value.find((item) => item.id === recordId); const record = allRecordsCache.value.find((item) => item.id === recordId);
if (!record) {
return;
}
if (mode === 'view' || mode === 'edit') { if (!record && mode !== 'delete') {
props.openDrawer(mode, recordId); props.notifyError('未找到对应手机号记录');
return; return;
} }
if (!confirm(props.phoneText.deleteConfirm)) { if (mode === 'delete') {
if (!confirm(props.phoneText.deleteConfirm)) {
return;
}
try {
await props.deletePhoneRecord(recordId);
await loadAllRecords();
} catch (error) {
props.notifyError(error.message || props.phoneText.deleteFailed);
}
return; return;
} }
loading.value = true;
try { try {
await props.deletePhoneRecord(recordId); await props.openDrawer(mode, recordId);
await loadAllRecords();
} catch (error) { } catch (error) {
props.notifyError(error.message || props.phoneText.deleteFailed); props.notifyError(error.message || '打开详情失败');
} finally {
loading.value = false;
} }
} }
/** /**
* 代码作用(白话):打开手机号新增弹窗,让新增动作也走外层统一入口;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):新增按钮 -> handleAdd() -> openAddDialog() -> PhoneAddDialogAPI.open('add')。 * 代码作用(白话):打开新增号卡弹窗,让手机号页通过壳层继续复用已有新增表单;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):点击“新增号卡” -> handleAdd() -> openAddDialog() -> PhoneAddDialogAPI.open('add')。
*/ */
function handleAdd() { function handleAdd() {
props.openAddDialog('add'); props.openAddDialog('add');
...@@ -224,9 +316,8 @@ ...@@ -224,9 +316,8 @@
return { return {
PHONE_TABLE_COLUMN_WIDTHS, PHONE_TABLE_COLUMN_WIDTHS,
totalCount, carrierOptions,
normalCount, outboundOptions,
abnormalCount,
currentPage, currentPage,
pageSize, pageSize,
visibleRows, visibleRows,
...@@ -242,6 +333,7 @@ ...@@ -242,6 +333,7 @@
filterCarrier, filterCarrier,
filterOutbound, filterOutbound,
totalFiltered, totalFiltered,
totalPages,
loading, loading,
handleAdd, handleAdd,
multipleSelection, multipleSelection,
...@@ -252,182 +344,141 @@ ...@@ -252,182 +344,141 @@
}; };
}, },
template: ` template: `
<div style="padding: 24px; box-sizing: border-box; display: flex; flex-direction: column; gap: 24px;"> <div class="table-area table-area-refined">
<div class="cards-row"> <div class="table-toolbar table-toolbar-refined">
<div class="kcard"> <div class="toolbar-left">
<div class="kcard-left"> <div class="toolbar-title-wrap">
<div class="kcard-icon blue"> <div class="toolbar-title">全部号卡 ({{ totalFiltered.toLocaleString('zh-CN') }})</div>
<svg viewBox="0 0 24 24"><path d="M17 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2Zm-5 18a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>
</div>
<div class="kcard-info">
<div class="kcard-title">手机号总数</div>
<div class="kcard-num">{{ totalCount }}</div>
<div class="kcard-sub">全部在管实名号卡</div>
</div>
</div> </div>
</div> <div class="filter-selects filter-selects-refined" style="display: flex; align-items: center; gap: 8px;">
<div class="kcard"> <el-select v-model="filterStatus" class="custom-fselect" style="width: 120px; height: 32px;">
<div class="kcard-left"> <el-option label="状态:全部" value="all"></el-option>
<div class="kcard-icon green"> <el-option v-for="label in phoneTableFilterLabels.filter((item) => item !== '全部')" :key="label" :label="label" :value="label"></el-option>
<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> </el-select>
</div> <el-select v-model="filterCarrier" class="custom-fselect" style="width: 140px; height: 32px;">
<div class="kcard-info"> <el-option label="运营商:全部" value="all"></el-option>
<div class="kcard-title">正常在用</div> <el-option v-for="carrier in carrierOptions" :key="carrier" :label="carrier" :value="carrier"></el-option>
<div class="kcard-num">{{ normalCount }}</div> </el-select>
<div class="kcard-sub">状态正常且可继续使用</div> <el-select v-model="filterOutbound" class="custom-fselect" style="width: 150px; height: 32px;">
</div> <el-option label="外呼:全部" value="all"></el-option>
<el-option v-for="outbound in outboundOptions" :key="outbound" :label="outbound" :value="outbound"></el-option>
</el-select>
<el-date-picker
v-model="filterDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px; height: 32px;"
></el-date-picker>
<el-input
v-model="searchQuery"
:placeholder="phoneText.searchPlaceholder"
clearable
style="width: 260px; height: 32px;"
>
<template #prefix>
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
</template>
</el-input>
</div> </div>
</div> </div>
<div class="kcard"> <div class="toolbar-right">
<div class="kcard-left"> <div class="toolbar-primary-actions">
<div class="kcard-icon orange"> <button class="btn btn-primary" type="button" @click="handleAdd" style="height: 32px; padding: 0 16px;">{{ phoneText.add }}</button>
<svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4m0 4h.01"/></svg>
</div>
<div class="kcard-info">
<div class="kcard-title">异常或停机</div>
<div class="kcard-num">{{ abnormalCount }}</div>
<div class="kcard-sub">需要复核或已停机</div>
</div>
</div> </div>
<button class="btn-reset btn-reset-refined" type="button" @click="resetTableState">{{ phoneText.reset }}</button>
</div> </div>
</div> </div>
<div class="table-wrap table-wrap-refined" v-loading="loading">
<div class="table-area table-area-refined"> <el-table
<div class="table-toolbar table-toolbar-refined"> ref="multipleTableRef"
<div class="toolbar-left"> :data="visibleRows"
<div class="tabs tabs-refined"> row-key="id"
<button style="width: 100%"
v-for="label in phoneTableFilterLabels" class="domain-table domain-table-refined"
:key="label" @selection-change="handleSelectionChange"
type="button" :header-cell-style="{ background: '#fafafa', color: '#4b5563', fontWeight: 500, padding: '12px 20px', borderBottom: '1px solid #f3f4f6' }"
class="tab-btn tab-btn-refined" :cell-style="{ padding: '12px 20px', borderBottom: '1px solid #f3f4f6', color: '#111827' }"
:class="{ active: filterStatus === (label === '全部' ? 'all' : label) }" >
@click="filterStatus = (label === '全部' ? 'all' : label)" <el-table-column type="selection" width="50" />
>{{ label }}</button> <el-table-column label="手机号" min-width="140">
</div> <template #default="scope">
<div style="width: 1px; height: 24px; background: #e5e7eb; margin: 0 8px;"></div> <a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone }}</a>
<div style="display: flex; gap: 8px; align-items: center;" class="filter-selects filter-selects-refined"> <div class="cell-sub">{{ scope.row.project || '-' }}</div>
<el-select v-model="filterCarrier" placeholder="运营商" class="custom-fselect" style="width: 120px; height: 32px;"> </template>
<el-option label="全部" value="all"></el-option> </el-table-column>
<el-option label="中国移动" value="中国移动"></el-option> <el-table-column label="实名人" min-width="120">
<el-option label="中国联通" value="中国联通"></el-option> <template #default="scope">
<el-option label="中国电信" value="中国电信"></el-option> <div class="date-stack">
<el-option label="中国广电" value="中国广电"></el-option> <span>{{ scope.row.realPerson }}</span>
<el-option label="虚商" value="虚商"></el-option> <div class="cell-sub">{{ scope.row.owner || '-' }}</div>
</el-select>
<el-select v-model="filterOutbound" placeholder="外呼权限" class="custom-fselect" style="width: 120px; height: 32px;">
<el-option label="全部" value="all"></el-option>
<el-option label="可外呼" value="可外呼"></el-option>
<el-option label="不可外呼" value="不可外呼"></el-option>
</el-select>
<el-date-picker
v-model="filterDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px; height: 32px;"
></el-date-picker>
<div class="search-box">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" v-model="searchQuery" :placeholder="phoneText.searchPlaceholder">
</div> </div>
</div> </template>
</div> </el-table-column>
<div class="toolbar-right"> <el-table-column label="归属地" min-width="120">
<button class="btn btn-primary" type="button" @click="handleAdd" style="height: 32px; padding: 0 16px;">{{ phoneText.add }}</button> <template #default="scope">
<button class="btn-reset btn-reset-refined" type="button" @click="resetTableState" style="height: 32px;">{{ phoneText.reset }}</button> <div class="date-stack">
</div> <span>{{ scope.row.city }}</span>
<div class="cell-sub">{{ scope.row.usageLocation || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column property="carrier" label="运营商" min-width="100" />
<el-table-column label="号卡状态" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
<span>{{ scope.row.status }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="微信开通" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.wechatStatus)"></span>
<span>{{ scope.row.wechatStatus }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="外呼能力" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.outbound)"></span>
<span>{{ scope.row.outbound }}</span>
</span>
</template>
</el-table-column>
<el-table-column property="updatedAt" label="更新时间" min-width="160" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<div class="action-cluster" style="display: flex; gap: 8px;">
<button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', scope.row.id)" style="border: none; background: transparent; color: #2563eb; cursor: pointer; padding: 4px; font-weight: 500;">查看</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('edit', scope.row.id)" style="border: none; background: transparent; color: #4b5563; cursor: pointer; padding: 4px;">编辑</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('delete', scope.row.id)" style="border: none; background: transparent; color: #ef4444; cursor: pointer; padding: 4px;">删除</button>
</div>
</template>
</el-table-column>
<template #empty>
<div style="padding: 24px; text-align: center; color: #9ca3af;">暂无手机号卡资料</div>
</template>
</el-table>
</div>
<div class="table-footer table-footer-refined">
<div class="tfoot-left tfoot-left-refined">
<span>已选 {{ checkedIds.length }} 项</span>
</div> </div>
<div class="tfoot-right tfoot-right-refined">
<div class="table-wrap table-wrap-refined"> <span>共 {{ totalFiltered.toLocaleString('zh-CN') }} 条</span>
<el-table <el-pagination
ref="multipleTableRef" v-model:current-page="currentPage"
:data="visibleRows" v-model:page-size="pageSize"
row-key="id" :page-sizes="phoneTablePageSizes"
style="width: 100%" layout="sizes, prev, pager, next, jumper"
class="domain-table domain-table-refined" :total="totalFiltered"
@selection-change="handleSelectionChange" @size-change="handleSizeChange"
v-loading="loading" @current-change="handleCurrentChange"
header-cell-style="{ background: '#fafafa', color: '#4b5563', fontWeight: 500, padding: '12px 20px', borderBottom: '1px solid #f3f4f6' }" />
cell-style="{ padding: '12px 20px', borderBottom: '1px solid #f3f4f6', color: '#111827' }"
>
<el-table-column type="selection" width="50" />
<el-table-column label="手机号" min-width="140">
<template #default="scope">
<a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone }}</a>
<div class="cell-sub">{{ scope.row.project || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="实名人" min-width="120">
<template #default="scope">
<div class="date-stack">
<span>{{ scope.row.realPerson }}</span>
<div class="cell-sub">{{ scope.row.owner || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="城市" min-width="120">
<template #default="scope">
<div class="date-stack">
<span>{{ scope.row.city }}</span>
<div class="cell-sub">{{ scope.row.usageLocation || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column property="carrier" label="运营商" min-width="100" />
<el-table-column label="号卡状态" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
<span>{{ scope.row.status }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="微信状态" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.wechatStatus)"></span>
<span>{{ scope.row.wechatStatus }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="外呼状态" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.outbound)"></span>
<span>{{ scope.row.outbound }}</span>
</span>
</template>
</el-table-column>
<el-table-column property="updatedAt" label="更新时间" min-width="160" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<div class="action-cluster" style="display: flex; gap: 8px;">
<button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', scope.row.id)" style="border: none; background: transparent; color: #2563eb; cursor: pointer; padding: 4px; font-weight: 500;">查看</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('edit', scope.row.id)" style="border: none; background: transparent; color: #4b5563; cursor: pointer; padding: 4px;">编辑</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('delete', scope.row.id)" style="border: none; background: transparent; color: #ef4444; cursor: pointer; padding: 4px;">删除</button>
</div>
</template>
</el-table-column>
</el-table>
<div class="table-footer table-footer-refined">
<div class="tfoot-left tfoot-left-refined">
<span>已选 {{ checkedIds.length }} 项</span>
</div>
<div class="tfoot-right tfoot-right-refined">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="phoneTablePageSizes"
layout="total, sizes, prev, pager, next, jumper"
:total="totalFiltered"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -435,7 +486,7 @@ ...@@ -435,7 +486,7 @@
}; };
/** /**
* 代码作用(白话):把手机号列表组件挂载到指定 DOM 根节点,并注入 Element Plus 能力;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime('phone') -> mountPhoneListComponent() -> createApp().mount()。 * 代码作用(白话):把手机号卡列表组件挂到壳层指定 DOM,并注入 Element Plus 组件能力;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime('phone') -> mountPhoneListComponent() -> createApp().mount() -> PhoneListTable.setup()。
*/ */
function mountPhoneListComponent(options) { function mountPhoneListComponent(options) {
const { createApp } = window.Vue; const { createApp } = window.Vue;
......
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