Commit f7030db4 by DaiJiezhang

feat: sync frontend backend and clean local artifacts

parent 0804c22b
......@@ -7,3 +7,7 @@ node_modules/
.agents/
.claude/
*.log
backend/target/
F_TMP_WRITE_TEST.txt
localhost
wechat-page.png
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.1</version>
<relativePath />
</parent>
<groupId>com.xyw</groupId>
<artifactId>xyw-console-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xyw-console-backend</name>
<description>Wechat asset backend for xyw_console</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.xyw.console;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.xyw.console.wechat.mapper")
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 接口。
*/
public static void main(String[] args) {
SpringApplication.run(XywConsoleBackendApplication.class, args);
}
}
package com.xyw.console.common;
public record ApiResponse<T>(Integer code, String message, T data) {
/**
* 代码作用(白话):构造成功返回体,统一输出前端正在使用的 code/message/data 结构;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):Controller 成功分支 -> ApiResponse.success() -> 前端 parseJsonResponse() 判断成功。
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
/**
* 代码作用(白话):构造带自定义文案的成功返回体,让新增和编辑可以返回更明确的提示信息;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):Controller 保存成功 -> ApiResponse.success(message, data) -> 前端保存成功提示。
*/
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(200, message, data);
}
/**
* 代码作用(白话):构造失败返回体,让前端即使拿到 4xx 响应也能读到明确错误信息;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):Controller 失败分支 -> ApiResponse.error() -> 前端 parseJsonResponse() 抛出 message。
*/
public static <T> ApiResponse<T> error(Integer code, String message) {
return new ApiResponse<>(code, message, null);
}
}
package com.xyw.console.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
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 正常接收请求。
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8000", "http://127.0.0.1:8000")
.allowedMethods("GET", "POST", "PUT", "OPTIONS")
.allowedHeaders("*");
}
}
package com.xyw.console.wechat.controller;
import com.xyw.console.common.ApiResponse;
import com.xyw.console.wechat.dto.WxDataSaveRequest;
import com.xyw.console.wechat.entity.WxDataEntity;
import com.xyw.console.wechat.service.WxDataService;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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/wechat-records")
public class WxDataController {
private final WxDataService wxDataService;
public WxDataController(WxDataService wxDataService) {
this.wxDataService = wxDataService;
}
/**
* 代码作用(白话):返回企微资料列表给前端页面首屏和刷新动作使用;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/service/WxDataService.java、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):前端 listWechatRecords() -> GET /api/wechat-records -> listRecords() -> ApiResponse.success(data)。
*/
@GetMapping
public ResponseEntity<ApiResponse<List<WxDataEntity>>> listRecords() {
return ResponseEntity.ok(ApiResponse.success(wxDataService.listRecords()));
}
/**
* 代码作用(白话):返回单条企微资料给详情抽屉和编辑弹窗读取最新值;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/service/WxDataService.java、F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):前端 getWechatRecordById() -> GET /api/wechat-records/{id} -> Service.getRecordById() -> 详情/编辑打开。
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<WxDataEntity>> getRecord(@PathVariable Long id) {
WxDataEntity entity = wxDataService.getRecordById(id);
if (entity == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(404, "未找到对应企微资料"));
}
return ResponseEntity.ok(ApiResponse.success(entity));
}
/**
* 代码作用(白话):校验新增表单并写入一条新的企微资料;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/dto/WxDataSaveRequest.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):前端保存新增 -> POST /api/wechat-records -> validateSaveRequest() -> Service.createRecord() -> 列表刷新。
*/
@PostMapping
public ResponseEntity<ApiResponse<WxDataEntity>> createRecord(@RequestBody WxDataSaveRequest request) {
String validationMessage = validateSaveRequest(request);
if (validationMessage != null) {
return ResponseEntity.badRequest().body(ApiResponse.error(400, validationMessage));
}
WxDataEntity entity = wxDataService.createRecord(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success("新增成功", entity));
}
/**
* 代码作用(白话):校验编辑表单并更新指定企微资料;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/dto/WxDataSaveRequest.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):前端保存编辑 -> PUT /api/wechat-records/{id} -> validateSaveRequest() -> Service.updateRecord() -> 列表刷新。
*/
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<WxDataEntity>> updateRecord(@PathVariable Long id, @RequestBody WxDataSaveRequest request) {
String validationMessage = validateSaveRequest(request);
if (validationMessage != null) {
return ResponseEntity.badRequest().body(ApiResponse.error(400, validationMessage));
}
WxDataEntity entity = wxDataService.updateRecord(id, request);
if (entity == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(404, "未找到对应企微资料"));
}
return ResponseEntity.ok(ApiResponse.success("编辑成功", entity));
}
/**
* 代码作用(白话):集中校验新增和编辑必填项,避免空数据直接写进数据库;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/dto/WxDataSaveRequest.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):createRecord()/updateRecord() -> validateSaveRequest() -> 返回 400 或继续保存。
*/
public String validateSaveRequest(WxDataSaveRequest request) {
if (request == null) {
return "请求体不能为空";
}
if (!hasText(request.getRealName())) {
return "真实姓名不能为空";
}
if (!hasText(request.getAccount())) {
return "企微账号不能为空";
}
if (!hasText(request.getDepartmentName())) {
return "部门不能为空";
}
if (!hasText(request.getPhoneNumber())) {
return "绑定手机号不能为空";
}
if (!hasText(request.getRealNameOwner())) {
return "实名人不能为空";
}
return null;
}
/**
* 代码作用(白话):判断字符串是否含有有效文本,给表单必填校验复用;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/service/WxDataService.java;关联逻辑(调用链/消息链/数据流):validateSaveRequest() -> hasText() -> 决定返回 400 还是继续保存。
*/
public boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
}
package com.xyw.console.wechat.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class WxDataSaveRequest {
@JsonProperty("real_name")
private String realName;
@JsonProperty("account")
private String account;
@JsonProperty("alias_name")
private String aliasName;
@JsonProperty("department_name")
private String departmentName;
@JsonProperty("gender")
private String gender;
@JsonProperty("phone_number")
private String phoneNumber;
@JsonProperty("real_name_owner")
private String realNameOwner;
@JsonProperty("account_location")
private String accountLocation;
@JsonProperty("phone_image")
private String phoneImage;
@JsonProperty("real_name_owner_status")
private Integer realNameOwnerStatus;
}
package com.xyw.console.wechat.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 com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@TableName("wx_data")
public class WxDataEntity {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@JsonProperty("real_name")
@TableField("real_name")
private String realName;
@JsonProperty("account")
@TableField("account")
private String account;
@JsonProperty("alias_name")
@TableField("alias_name")
private String aliasName;
@JsonProperty("department_name")
@TableField("department_name")
private String departmentName;
@JsonProperty("gender")
@TableField("gender")
private String gender;
@JsonProperty("phone_number")
@TableField("phone_number")
private String phoneNumber;
@JsonProperty("real_name_owner")
@TableField("real_name_owner")
private String realNameOwner;
@JsonProperty("account_location")
@TableField("账号位置")
private String accountLocation;
@JsonProperty("phone_image")
@TableField("手机图片")
private String phoneImage;
@JsonProperty("real_name_owner_status")
@TableField("real_name_owner_status")
private Integer realNameOwnerStatus;
}
package com.xyw.console.wechat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xyw.console.wechat.entity.WxDataEntity;
public interface WxDataMapper extends BaseMapper<WxDataEntity> {
}
package com.xyw.console.wechat.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xyw.console.wechat.dto.WxDataSaveRequest;
import com.xyw.console.wechat.entity.WxDataEntity;
import com.xyw.console.wechat.mapper.WxDataMapper;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class WxDataService {
private final WxDataMapper wxDataMapper;
public WxDataService(WxDataMapper wxDataMapper) {
this.wxDataMapper = wxDataMapper;
}
/**
* 代码作用(白话):查询企微资料全量列表,给前端当前的本地筛选和分页继续使用;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):GET /api/wechat-records -> listRecords() -> Mapper.selectList() -> 前端列表渲染。
*/
public List<WxDataEntity> listRecords() {
QueryWrapper<WxDataEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return wxDataMapper.selectList(queryWrapper);
}
/**
* 代码作用(白话):按主键读取单条企微资料,给详情抽屉和编辑弹窗拿最新数据;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):GET /api/wechat-records/{id} -> getRecordById() -> Mapper.selectById() -> 前端详情/编辑打开。
*/
public WxDataEntity getRecordById(Long id) {
return wxDataMapper.selectById(id);
}
/**
* 代码作用(白话):把前端新增表单写入 wx_data 表,并返回带主键的新记录;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):POST /api/wechat-records -> createRecord() -> copyRequestToEntity() -> Mapper.insert() -> 前端刷新列表。
*/
public WxDataEntity createRecord(WxDataSaveRequest request) {
WxDataEntity entity = new WxDataEntity();
copyRequestToEntity(request, entity);
wxDataMapper.insert(entity);
return wxDataMapper.selectById(entity.getId());
}
/**
* 代码作用(白话):更新已有企微资料,并把数据库里最新记录返回给前端;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/controller/WxDataController.java、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):PUT /api/wechat-records/{id} -> updateRecord() -> Mapper.updateById() -> 前端刷新列表。
*/
public WxDataEntity updateRecord(Long id, WxDataSaveRequest request) {
WxDataEntity entity = wxDataMapper.selectById(id);
if (entity == null) {
return null;
}
copyRequestToEntity(request, entity);
wxDataMapper.updateById(entity);
return wxDataMapper.selectById(id);
}
/**
* 代码作用(白话):把接口入参安全地拷贝到数据库实体,集中处理默认值和空字符串清洗;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/dto/WxDataSaveRequest.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/entity/WxDataEntity.java;关联逻辑(调用链/消息链/数据流):createRecord()/updateRecord() -> copyRequestToEntity() -> Entity -> Mapper 持久化。
*/
public void copyRequestToEntity(WxDataSaveRequest request, WxDataEntity entity) {
entity.setRealName(normalizeText(request.getRealName()));
entity.setAccount(normalizeText(request.getAccount()));
entity.setAliasName(normalizeText(request.getAliasName()));
entity.setDepartmentName(normalizeText(request.getDepartmentName()));
entity.setGender(normalizeText(request.getGender()));
entity.setPhoneNumber(normalizeText(request.getPhoneNumber()));
entity.setRealNameOwner(normalizeText(request.getRealNameOwner()));
entity.setAccountLocation(normalizeText(request.getAccountLocation()));
entity.setPhoneImage(normalizeText(request.getPhoneImage()));
entity.setRealNameOwnerStatus(request.getRealNameOwnerStatus() == null ? 1 : request.getRealNameOwnerStatus());
}
/**
* 代码作用(白话):把前端传来的空白文本裁掉首尾空格,并把空字符串转成 null,减少数据库里出现一堆无意义空格;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/service/WxDataService.java、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/wechat/entity/WxDataEntity.java;关联逻辑(调用链/消息链/数据流):copyRequestToEntity() -> normalizeText() -> Entity 字段写入数据库。
*/
public String normalizeText(String value) {
if (value == null) {
return null;
}
String trimmedValue = value.trim();
return trimmedValue.isEmpty() ? null : trimmedValue;
}
}
server:
port: 8080
spring:
application:
name: xyw-console-backend
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${XYW_DB_URL:jdbc:mysql://localhost:3306/xyw_private_data?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
username: ${XYW_DB_USERNAME:root}
password: ${XYW_DB_PASSWORD:}
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
......@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>学有为资产管理台</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="node_modules/element-plus/dist/index.css" />
<link rel="stylesheet" href="styles-v2.css" />
</head>
......@@ -14,57 +15,56 @@
<aside class="shell-nav" id="sidebar">
<div class="brand-panel">
<div class="brand-mark">
<img class="brand-logo" src="img/学有为logo.jpg" alt="瀛︽湁涓?LOGO" />
<img class="brand-logo" src="img/学有为logo.jpg" alt="学有为 LOGO" />
</div>
<div class="brand-copy">
<h1>&#23398;&#26377;&#20026;&#25968;&#25454;&#36164;&#20135;&#21518;&#21488;</h1>
<h1>学有为数据资产后台</h1>
</div>
</div>
<div class="nav-main">
<nav class="primary-nav" aria-label="&#20027;&#23548;&#33322;">
<nav class="primary-nav" aria-label="主导航">
<button class="nav-link" type="button" data-module="overview">
<span class="nav-glyph"><img class="nav-icon" src="img/账户总览.png" alt="鎬昏" /></span>
<span class="nav-label">&#24635;&#35272;</span>
<span class="nav-glyph"><img class="nav-icon" src="img/账户总览.png" alt="总览" /></span>
<span class="nav-label">总览</span>
</button>
<button class="nav-link" type="button" data-module="domain">
<span class="nav-glyph"><img class="nav-icon" src="img/域名申请.png" alt="鍩熷悕璧勬枡" /></span>
<span class="nav-label">&#22495;&#21517;&#36164;&#26009;</span>
<span class="nav-glyph"><img class="nav-icon" src="img/域名申请.png" alt="域名资料" /></span>
<span class="nav-label">域名资料</span>
</button>
<button class="nav-link" type="button" data-module="wechat">
<span class="nav-glyph"><img class="nav-icon" src="img/企微_企微.png" alt="浼佸井璧勬枡" /></span>
<span class="nav-label">&#20225;&#24494;&#36164;&#26009;</span>
<span class="nav-glyph"><img class="nav-icon" src="img/企微_企微.png" alt="企微资料" /></span>
<span class="nav-label">企微资料</span>
</button>
<button class="nav-link" type="button" data-module="phone">
<span class="nav-glyph"><img class="nav-icon" src="img/电话号码 (1).png" alt="鎵嬫満鍙峰崱" /></span>
<span class="nav-label">&#25163;&#26426;&#21495;&#21345;</span>
<span class="nav-glyph"><img class="nav-icon" src="img/电话号码 (1).png" alt="手机号卡" /></span>
<span class="nav-label">手机号卡</span>
</button>
<button class="nav-link" type="button" data-module="alerts">
<span class="nav-glyph"><img class="nav-icon" src="img/提醒中心.png" alt="鎻愰啋涓績" /></span>
<span class="nav-label">&#25552;&#37266;&#20013;&#24515;</span>
<span class="nav-glyph"><img class="nav-icon" src="img/提醒中心.png" alt="提醒中心" /></span>
<span class="nav-label">提醒中心</span>
</button>
</nav>
</div>
<div class="nav-bottom">
<div class="nav-toggle-wrap">
<button class="nav-toggle-button" id="sidebarToggle" type="button"
aria-label="&#25910;&#36215;&#20391;&#36793;&#23548;&#33322;" aria-expanded="true">
<button class="nav-toggle-button" id="sidebarToggle" type="button" aria-label="收起侧边导航" aria-expanded="true">
<span class="nav-toggle-glyph" aria-hidden="true">
<span></span>
<span></span>
</span>
<span class="nav-toggle-label">&#25910;&#36215;</span>
<span class="nav-toggle-label">收起</span>
</button>
</div>
<div class="nav-footer">
<div class="nav-user-avatar">&#9679;</div>
<div class="nav-user-avatar"></div>
<div class="nav-user-copy">
<p class="nav-user-name">&#31649;&#29702;&#21592;</p>
<p class="nav-user-role">&#36229;&#32423;&#31649;&#29702;&#21592;</p>
<p class="nav-user-name">管理员</p>
<p class="nav-user-role">超级管理员</p>
</div>
<button class="nav-settings" type="button" aria-label="&#35774;&#32622;">&#9881;</button>
<button class="nav-settings" type="button" aria-label="设置"></button>
</div>
</div>
</aside>
......@@ -92,10 +92,11 @@
<script src="src/modules/phone/phone-add-dialog.js"></script>
<script src="src/modules/phone/phone-detail-drawer.js"></script>
<script src="src/modules/phone/phone-list-runtime.js"></script>
<script src="src/modules/shared/record-adapters.js"></script>
<script src="src/modules/shared/wechat-api-client.js"></script>
<script src="src/modules/shared/phone-api-client.js"></script>
<script src="src/router/index.js"></script>
<script src="app-v2.js"></script>
</body>
</html>
(function attachPhoneApiClient() {
const BASE_URL = 'http://localhost:8080/api/wx-phones';
/**
* 代码作用(白话):统一处理手机号接口返回,遇到后端报错时直接抛出明确错误,避免列表和弹窗各自重复判断;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):listPhones()/savePhone()/deletePhone() -> parseJsonResponse() -> app-v2.js/手机号模块错误提示。
*/
async function parseJsonResponse(response) {
const json = await response.json();
if (!response.ok || json.code !== 200) {
throw new Error(json.message || '手机号接口请求失败');
}
return json;
}
/**
* 代码作用(白话):从本地手机号接口拉取完整列表,返回后端 data 数组给外层适配;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):phone 页面挂载/刷新 -> listPhones() -> app-v2.js -> normalizePhoneRecord() -> 列表渲染。
*/
async function listPhones() {
const response = await fetch(BASE_URL);
const json = await parseJsonResponse(response);
return Array.isArray(json.data) ? json.data : [];
}
/**
* 代码作用(白话):按新增或编辑模式把手机号表单提交给后端,统一 POST/PUT 分支;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):PhoneAddDialogActions.save() -> savePhone() -> 后端保存 -> afterSave() 刷新列表。
*/
async function savePhone(record) {
const payload = { ...record };
const isEdit = Boolean(payload.id);
const targetUrl = isEdit ? `${BASE_URL}/${payload.id}` : BASE_URL;
const response = await fetch(targetUrl, {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const json = await parseJsonResponse(response);
return json.data || json;
}
/**
* 代码作用(白话):删除指定手机号记录,并把失败信息统一抛给外层处理;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):手机号列表删除按钮 -> deletePhoneRecord() -> deletePhone() -> 刷新列表或提示失败。
*/
async function deletePhone(id) {
const response = await fetch(`${BASE_URL}/${id}`, {
method: 'DELETE'
});
const json = await parseJsonResponse(response);
return json.data || json;
}
window.PhoneApiClient = {
listPhones,
savePhone,
deletePhone
};
})();
(function attachRecordAdapters() {
/**
* 代码作用(白话):把企微原始记录整理成列表、详情、编辑弹窗都能直接消费的统一字段;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):后端/本地记录 -> normalizeWechatRecord() -> app-v2.js 注入 -> 企微列表/详情/编辑弹窗。
*/
function normalizeWechatRecord(row) {
const source = row || {};
const departmentMap = {
Marketing: '市场部',
Operations: '运营部',
Sales: '销售部'
};
const ownerStatusRaw = source.ownerStatus ?? source.real_name_owner_status ?? source.realNameOwnerStatus;
const status = ownerStatusRaw === 'abnormal' || ownerStatusRaw === '异常' || ownerStatusRaw === 0 ? '异常' : '正常';
const remark = source.remark || source.project || '暂无备注';
return {
...source,
realName: source.realName || source.real_name || '',
real_name: source.real_name || source.realName || '',
alias: source.alias || source.alias_name || source.aliasName || '',
alias_name: source.alias_name || source.alias || source.aliasName || '',
account: source.account || source.wxId || '',
department: departmentMap[source.department] || source.department || source.department_name || source.departmentName || '未分配',
department_name: source.department_name || source.department || source.departmentName || '',
phone: source.phone || source.phone_number || source.phoneNumber || '',
phone_number: source.phone_number || source.phone || source.phoneNumber || '',
owner: source.owner || source.real_name_owner || source.realNameOwner || '',
real_name_owner: source.real_name_owner || source.owner || source.realNameOwner || '',
ownerStatus: status,
real_name_owner_status: status === '正常' ? 1 : 0,
status,
wxId: source.wxId || source.account || '',
createdAt: source.createdAt || source.created_at || '',
created_at: source.created_at || source.createdAt || '',
updatedAt: source.updatedAt || source.updated_at || '',
updated_at: source.updated_at || source.updatedAt || '',
remark,
project: source.project || remark,
gender: source.gender || '男',
account_location: source.account_location || source.accountLocation || source['账号位置'] || '',
phone_image: source.phone_image || source.phoneImage || source['手机图片'] || ''
};
}
/**
* 代码作用(白话):把手机号原始记录整理成列表、详情抽屉、编辑弹窗共用的统一字段,避免每个组件自己猜字段名;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):PhoneApiClient/DATA_SOURCES.phone -> normalizePhoneRecord() -> app-v2.js 注入 -> 手机号列表/详情/编辑弹窗。
*/
function normalizePhoneRecord(row) {
const source = row || {};
const cardStatus = source.cardStatus || source.status || '正常';
const phoneNumber = source.phoneNumber || source.phone || '';
const channelOperator = source.channelOperator || source.carrier || '';
const updatedAt = source.updatedAt || source.updateTime || '';
const usageLocation = source.cardUsageLocation || source.usageLocation || '';
const wechatStatus = source.wechatStatus || (source.numberStatus === false ? '异常停机' : '正常');
const outbound = source.outbound || (source.outboundCall ? '可外呼' : '不可外呼');
return {
...source,
id: source.id,
phone: phoneNumber,
phoneNumber,
realPerson: source.realPerson || '',
city: source.city || '',
usageLocation,
cardUsageLocation: usageLocation,
carrier: channelOperator,
channelOperator,
status: cardStatus,
cardStatus,
wechatStatus,
outbound,
outboundCall: source.outboundCall === true || source.outbound === 'available' || source.outbound === '可外呼',
wechat: source.wechat === true || source.wechat === 'enabled',
wecom: source.wecom === true || source.wecom === 'enabled',
updatedAt,
updateTime: source.updateTime || updatedAt,
owner: source.owner || source.realPerson || '',
project: source.project || source.note || '暂无备注',
note: source.note || source.project || '',
iccid: source.iccid || '',
douyinAccount: source.douyinAccount || '',
miniProgramFiling: source.miniProgramFiling === true,
packageChange5yuan: source.packageChange5yuan === true,
numberStatus: source.numberStatus !== false,
linkedWecom: source.linkedWecom || '',
numberRetentionStatus: source.numberRetentionStatus !== false
};
}
window.RecordAdapters = {
normalizeWechatRecord,
normalizePhoneRecord
};
})();
(function attachWechatApiClient() {
const BASE_URL = 'http://localhost:8080/api/wechat-records';
/**
* 代码作用(白话):统一解析企微接口返回,保证前端只认 code/message/data 这一套结构;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):listWechatRecords()/getWechatRecord()/saveWechatRecord() -> parseJsonResponse() -> 页面列表/弹窗提示。
*/
async function parseJsonResponse(response) {
const json = await response.json();
if (!response.ok || json.code !== 200) {
throw new Error(json.message || '企微接口请求失败');
}
return json;
}
/**
* 代码作用(白话):从后端拉取企微资料全量列表,交给页面继续做本地筛选和分页;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):wechat 页面挂载/刷新 -> listWechatRecords() -> app-v2.js -> normalizeWechatRecord() -> 列表渲染。
*/
async function listWechatRecords() {
const response = await fetch(BASE_URL);
const json = await parseJsonResponse(response);
return Array.isArray(json.data) ? json.data : [];
}
/**
* 代码作用(白话):按主键读取单条企微资料,让详情抽屉和编辑弹窗拿到数据库最新值;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-detail-drawer.js;关联逻辑(调用链/消息链/数据流):查看/编辑按钮 -> getWechatRecord() -> app-v2.js -> 详情抽屉或编辑弹窗打开。
*/
async function getWechatRecord(id) {
const response = await fetch(`${BASE_URL}/${id}`);
const json = await parseJsonResponse(response);
return json.data || null;
}
/**
* 代码作用(白话):按新增或编辑模式提交企微资料表单,统一收口 POST 和 PUT 两条保存分支;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):保存按钮 -> saveWechatRecord() -> 后端保存 -> afterSave() 刷新列表。
*/
async function saveWechatRecord(record) {
const payload = { ...record };
const isEdit = Boolean(payload.id);
const targetUrl = isEdit ? `${BASE_URL}/${payload.id}` : BASE_URL;
if (!isEdit) {
delete payload.id;
}
const response = await fetch(targetUrl, {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const json = await parseJsonResponse(response);
return json.data || json;
}
window.WechatApiClient = {
listWechatRecords,
getWechatRecord,
saveWechatRecord
};
})();
// Reusable wrapper template that provides the DOM structure required by app-v2.js
// Reusable wrapper template that provides the DOM structure required by app-v2.js
const LegacyShellTemplate = `
<div class="legacy-wrapper">
<div id="pageHeader"></div>
......@@ -11,7 +11,7 @@ const LegacyShellTemplate = `
<div id="detailDrawer" class="detail-drawer hidden">
<div class="detail-drawer-head" id="detailShellTitle"></div>
<div class="detail-drawer-body" id="detailBody"></div>
<button class="drawer-close" id="drawerClose"></button>
<button class="drawer-close" id="drawerClose">×</button>
</div>
</div>
`;
......@@ -19,22 +19,19 @@ const LegacyShellTemplate = `
function createLegacyWrapper(moduleName) {
return {
template: LegacyShellTemplate,
/** 代码作用(白话):在当前路由页面挂载后,把旧版 app-v2 的状态和渲染入口接起来,这样 Vue Router 只是负责切页,真正的页面内容仍沿用原来的渲染逻辑;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/index.html;关联逻辑:调用链是“RouterView mounted -> updateDOMRefs() -> state.activeModule 切换 -> renderModule() -> updateActiveNav()”。 */
/** 代码作用(白话):路由页面挂载后,把旧版 app-v2 的状态和渲染入口重新接到当前 DOM 上,这样切页后还能沿用原来的页面渲染;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/index.html;关联逻辑:调用链是“RouterView mounted -> updateDOMRefs()/refreshDOMRefs() -> state.activeModule 切换 -> renderModule() -> updateActiveNav()”。 */
mounted() {
// Re-initialize the DOM refs because the DOM was just created by Vue.
if (window.updateDOMRefs) {
window.updateDOMRefs();
const domRefUpdater = window.updateDOMRefs || window.refreshDOMRefs;
if (typeof domRefUpdater === 'function') {
domRefUpdater();
}
// Defensive initialization: if the legacy runtime has not exposed state yet, stop here instead of crashing the page.
if (!window.state) {
return;
}
// Force app-v2 state to update.
window.state.activeModule = moduleName;
// Let app-v2 render the module and sync nav highlight.
if (window.renderModule) {
window.renderModule();
}
......@@ -65,36 +62,33 @@ const router = window.VueRouter.createRouter({
routes
});
// Setup global Vue app that mounts the router
const app = window.Vue.createApp({});
app.use(router);
// We need to wait for DOMContentLoaded to mount the Vue app
document.addEventListener('DOMContentLoaded', () => {
// Let the sidebar buttons trigger router.push instead of the old vanilla js navigation
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.addEventListener('click', (event) => {
/** 代码作用(白话):拦截左侧导航点击,把旧按钮的 `data-module` 映射成 Vue Router 路由地址,避免旧脚本和新路由重复处理一次点击;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/index.html;关联逻辑:消息链是“sidebar click -> handleSidebarRouteClick() -> router.push() -> createLegacyWrapper().mounted()”。 */
function handleSidebarRouteClick(event) {
const button = event.target.closest('.nav-link[data-module]');
if (!button) return;
const mod = button.dataset.module;
// Update UI active state manually or let updateActiveNav do it
// Map module to path
let path = '/' + mod;
if (mod === 'phone') path = '/phone-card';
const mod = button.dataset.module;
let path = `/${mod}`;
if (mod === 'phone') {
path = '/phone-card';
}
router.push(path);
// We stop propagation so app-v2.js doesn't try to handle it
// (Wait, we can just remove the old listener from app-v2.js)
event.preventDefault();
event.stopPropagation();
}, true); // use capture to intercept
}
/** 代码作用(白话):在页面初次加载时绑定路由壳层需要的导航事件,并把整个 Vue Router 应用挂到主容器;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/app-v2.js;关联逻辑:调用链是“DOMContentLoaded -> initRouterApp() -> 绑定 sidebar 路由点击 -> app.mount('#vue-router-app')”。 */
function initRouterApp() {
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.addEventListener('click', handleSidebarRouteClick, true);
}
// Mount router to the shell-main
app.mount('#vue-router-app');
});
}
document.addEventListener('DOMContentLoaded', initRouterApp);
@echo off
chcp 65001 >nul
set "BACKEND_DIR=%~dp0backend"
set "PROJECT_JAVA_HOME=F:\jdk-17.0.12_windows-x64_bin\jdk-17.0.12"
set "XYW_DB_USERNAME=root"
set "XYW_DB_PASSWORD=root"
echo ===================================================
echo [Starting Backend]
echo Backend Dir: %BACKEND_DIR%
echo Backend URL: http://localhost:8080
echo JAVA_HOME: %PROJECT_JAVA_HOME%
echo DB User: %XYW_DB_USERNAME%
echo ===================================================
echo.
if not exist "%BACKEND_DIR%\pom.xml" (
echo [Error] Backend pom.xml not found: %BACKEND_DIR%\pom.xml
pause
exit /b 1
)
if not exist "%PROJECT_JAVA_HOME%\bin\java.exe" (
echo [Error] java.exe not found: %PROJECT_JAVA_HOME%\bin\java.exe
pause
exit /b 1
)
if not exist "%PROJECT_JAVA_HOME%\bin\javac.exe" (
echo [Error] javac.exe not found: %PROJECT_JAVA_HOME%\bin\javac.exe
pause
exit /b 1
)
set "JAVA_HOME=%PROJECT_JAVA_HOME%"
set "PATH=%JAVA_HOME%\bin;%PATH%"
java -version
cd /d "%BACKEND_DIR%"
echo (Press Ctrl+C or close this window to stop backend)
mvn spring-boot:run
\ No newline at end of file
@echo off
chcp 65001 >nul
set PORT=8000
set URL=http://localhost:%PORT%
echo ===================================================
echo [Starting Frontend]
echo Frontend URL: %URL%
echo ===================================================
echo.
echo (Press Ctrl+C or close this window to stop frontend)
python -m http.server %PORT%
\ No newline at end of file
@echo off
:: Switch code page to UTF-8 to prevent garbled text
chcp 65001 >nul
set PORT=8000
set URL=http://localhost:%PORT%
:: ==========================================
:: Please set BACKEND_DIR to your Spring Boot project path
:: Example: set BACKEND_DIR=..\phone-card-backend
:: ==========================================
set BACKEND_DIR=backend
set FRONTEND_PORT=8000
set FRONTEND_URL=http://localhost:%FRONTEND_PORT%
set BACKEND_URL=http://localhost:8080
set FRONTEND_SCRIPT=%~dp0start-frontend.bat
set BACKEND_SCRIPT=%~dp0start-backend.bat
echo ===================================================
echo [Starting Project]
echo Frontend URL: %URL%
echo Backend URL: http://localhost:8080
echo Frontend URL: %FRONTEND_URL%
echo Backend URL: %BACKEND_URL%
echo ===================================================
:: Copy to clipboard
echo | set /p dummy="%URL%" | clip
echo [Success] URL copied to clipboard!
echo | set /p dummy="%FRONTEND_URL%" | clip
echo [Success] Frontend URL copied to clipboard!
echo.
echo [1/2] Starting Spring Boot Backend...
if exist "%BACKEND_DIR%\pom.xml" (
start "Backend Service" cmd /k "cd /d %BACKEND_DIR% && mvn spring-boot:run"
echo Backend is starting in a new window...
if exist "%BACKEND_SCRIPT%" (
echo [1/2] Starting backend in a separate window...
start "XYW Backend" "%BACKEND_SCRIPT%"
) else (
echo [Warning] Backend directory '%BACKEND_DIR%' not found or no pom.xml inside!
echo Please edit start.bat and change BACKEND_DIR to your Spring Boot project.
echo [Warning] Missing backend launcher: %BACKEND_SCRIPT%
)
:: Open in browser
start "" "%URL%#/phone-card"
echo.
echo [2/2] Starting Python HTTP server for Frontend...
echo (Press Ctrl+C or close this window to stop)
if exist "%FRONTEND_SCRIPT%" (
echo [2/2] Starting frontend in a separate window...
start "XYW Frontend" "%FRONTEND_SCRIPT%"
start "" "%FRONTEND_URL%/#/phone-card"
) else (
echo [Warning] Missing frontend launcher: %FRONTEND_SCRIPT%
)
echo.
python -m http.server %PORT%
echo [Done] Frontend and backend launchers were triggered.
pause
\ No newline at end of file
@echo off
set PORT=8000
setlocal EnableExtensions EnableDelayedExpansion
chcp 65001 >nul
set "FRONTEND_PORT=8000"
set "BACKEND_PORT=8080"
echo ===================================================
echo [Stopping Project]
echo Looking for processes on port %PORT%...
echo Frontend Port: %FRONTEND_PORT%
echo Backend Port: %BACKEND_PORT%
echo ===================================================
echo.
for /f "tokens=5" %%a in ('netstat -aon ^| findstr "LISTENING" ^| findstr ":%PORT%"') do (
echo Found process PID: %%a, terminating...
taskkill /F /PID %%a
echo Server successfully stopped!
goto :end
)
call :stopByPort %FRONTEND_PORT% Frontend
call :stopByPort %BACKEND_PORT% Backend
echo No local server found running on port %PORT%.
:end
echo.
pause
exit /b
:stopByPort
REM Purpose: collect every listening PID on the target port and stop them one by one.
REM Related files: start-frontend.bat, start-backend.bat.
REM Related flow: stop.bat -> stopByPort -> killProcess -> release port 8000 or 8080.
set "TARGET_PORT=%~1"
set "TARGET_NAME=%~2"
set "FOUND_ANY=0"
for /f "tokens=5" %%a in ('netstat -aon ^| findstr "LISTENING" ^| findstr ":%TARGET_PORT%"') do (
if not defined SEEN_%%a (
set "SEEN_%%a=1"
set "FOUND_ANY=1"
call :killProcess %%a %TARGET_PORT% %TARGET_NAME%
)
)
if "!FOUND_ANY!"=="0" (
echo [Info] %TARGET_NAME% service on port %TARGET_PORT% is not running.
)
exit /b
:killProcess
REM Purpose: stop one PID and print whether stop succeeded or failed.
REM Related file: stop.bat.
REM Related flow: stopByPort -> killProcess -> taskkill -> print result.
set "TARGET_PID=%~1"
set "TARGET_PORT=%~2"
set "TARGET_NAME=%~3"
echo [Found] %TARGET_NAME% service on port %TARGET_PORT% with PID %TARGET_PID%.
taskkill /F /PID %TARGET_PID% >nul 2>&1
if errorlevel 1 (
echo [Error] Failed to stop %TARGET_NAME% service on port %TARGET_PORT% with PID %TARGET_PID%.
) else (
echo [Stopped] %TARGET_NAME% service on port %TARGET_PORT% with PID %TARGET_PID%.
)
exit /b
\ No newline at end of file
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