|
|
@@ -0,0 +1,452 @@
|
|
|
+# 家庭成员页可编辑家庭名称 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 家庭成员页 `pages/profile-extra/family-members.vue` 顶部家庭名称可被家庭管理员编辑。
|
|
|
+
|
|
|
+**架构:** 新增 `POST /api/family/member/rename-family`(`FamilyMembersController` → `FamilyMemberService.renameFamily`),鉴权复用 `isFamilyAdminOperator`(家庭创建者或成员 `is_admin=1`);前端在头部加编辑图标 + 自定义弹窗,保存后回填名称。
|
|
|
+
|
|
|
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus(后端),uni-app 小程序 Vue 2(前端)。
|
|
|
+
|
|
|
+**规格:** `docs/superpowers/specs/2026-11-01-family-members-rename-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 1:后端 Service `renameFamily` + 单元测试
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java`(在 `isFamilyAdminOperator` 附近新增方法)
|
|
|
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberServiceRenameTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写失败的测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.FamilyMemberService;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+public class FamilyMemberServiceRenameTest {
|
|
|
+
|
|
|
+ @Mock private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock private UserMapper userMapper;
|
|
|
+ @Mock private FamilyMapper familyMapper;
|
|
|
+
|
|
|
+ @InjectMocks private FamilyMemberService service;
|
|
|
+
|
|
|
+ private User user(Long id, Long familyId) {
|
|
|
+ User u = new User(); u.setId(id); u.setFamilyId(familyId); return u;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test void rename_管理员_成功更新() {
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 7L));
|
|
|
+ Family f = new Family(); f.setId(7L); f.setName("旧家庭"); f.setCreatorId(10L);
|
|
|
+ when(familyMapper.selectById(7L)).thenReturn(f);
|
|
|
+
|
|
|
+ String out = service.renameFamily(10L, " 新家庭 ");
|
|
|
+ assertEquals("新家庭", out);
|
|
|
+ verify(familyMapper).updateById(argThat(fam -> "新家庭".equals(fam.getName())));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test void rename_非管理员_抛错() {
|
|
|
+ when(userMapper.selectById(20L)).thenReturn(user(20L, 7L));
|
|
|
+ Family f = new Family(); f.setId(7L); f.setName("旧家庭"); f.setCreatorId(10L);
|
|
|
+ when(familyMapper.selectById(7L)).thenReturn(f);
|
|
|
+ when(familyMemberMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
|
|
+
|
|
|
+ assertThrows(RuntimeException.class, () -> service.renameFamily(20L, "新家庭"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test void rename_空白名_抛错() {
|
|
|
+ assertThrows(RuntimeException.class, () -> service.renameFamily(10L, " "));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test void rename_超长名_抛错() {
|
|
|
+ assertThrows(RuntimeException.class, () -> service.renameFamily(10L, "一二三四五六七八九十一二三四五六七八九十一"));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行测试验证失败**
|
|
|
+
|
|
|
+运行:`mvn test -Dtest=FamilyMemberServiceRenameTest -DfailIfNoTests=false`
|
|
|
+预期:FAIL(编译失败——`renameFamily` 方法不存在)
|
|
|
+
|
|
|
+- [ ] **步骤 3:编写最少实现代码**
|
|
|
+
|
|
|
+在 `FamilyMemberService.java` 的 `isFamilyAdminOperator`(约 line 1046)之后新增:
|
|
|
+
|
|
|
+```java
|
|
|
+ /**
|
|
|
+ * 修改家庭名称(仅家庭管理员:家庭创建者或成员记录 is_admin=1)。
|
|
|
+ * 返回更新后的家庭名称。
|
|
|
+ */
|
|
|
+ public String renameFamily(Long userId, String name) {
|
|
|
+ if (userId == null || userId <= 0) {
|
|
|
+ throw new IllegalArgumentException("请先登录");
|
|
|
+ }
|
|
|
+ String newName = name == null ? "" : name.trim();
|
|
|
+ if (newName.isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("家庭名称不能为空");
|
|
|
+ }
|
|
|
+ if (newName.length() > 20) {
|
|
|
+ throw new IllegalArgumentException("家庭名称不能超过20个字");
|
|
|
+ }
|
|
|
+ User operator = userMapper.selectById(userId);
|
|
|
+ if (operator == null) {
|
|
|
+ throw new IllegalArgumentException("用户不存在");
|
|
|
+ }
|
|
|
+ Long familyId = operator.getFamilyId();
|
|
|
+ if (familyId == null) {
|
|
|
+ throw new IllegalArgumentException("请先创建或加入家庭");
|
|
|
+ }
|
|
|
+ if (!isFamilyAdminOperator(userId, familyId)) {
|
|
|
+ throw new IllegalArgumentException("仅家庭管理员可修改家庭名称");
|
|
|
+ }
|
|
|
+ Family family = familyMapper.selectById(familyId);
|
|
|
+ if (family == null) {
|
|
|
+ throw new IllegalArgumentException("家庭不存在");
|
|
|
+ }
|
|
|
+ family.setName(newName);
|
|
|
+ family.setUpdatedAt(new Date());
|
|
|
+ familyMapper.updateById(family);
|
|
|
+ return newName;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+`User`/`Family`/`Date` 已在本文件 import(line 8/15/32),无需新增 import。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试验证通过**
|
|
|
+
|
|
|
+运行:`mvn test -Dtest=FamilyMemberServiceRenameTest -DfailIfNoTests=false`
|
|
|
+预期:PASS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberServiceRenameTest.java
|
|
|
+git commit -m "feat(family): 新增 renameFamily 服务(仅家庭管理员可改家庭名称)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 2:后端 Controller 接口 + 测试
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java`(在 `updateMember` 后新增)
|
|
|
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerRenameFamilyTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写失败的测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.controller.family;
|
|
|
+
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.FamilyMemberService;
|
|
|
+import com.etotem.cfc.service.UnlockGateService;
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
+
|
|
|
+public class FamilyMembersControllerRenameFamilyTest {
|
|
|
+
|
|
|
+ @Mock private FamilyMemberService familyMemberService;
|
|
|
+ @Mock private UserService userService;
|
|
|
+ @Mock private UnlockGateService unlockGateService;
|
|
|
+ @Mock private UserMapper userMapper;
|
|
|
+
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ public void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ FamilyMembersController controller = new FamilyMembersController();
|
|
|
+ ReflectionTestUtils.setField(controller, "familyMemberService", familyMemberService);
|
|
|
+ ReflectionTestUtils.setField(controller, "unlockGateService", unlockGateService);
|
|
|
+ ReflectionTestUtils.setField(controller, "userService", userService);
|
|
|
+ ReflectionTestUtils.setField(controller, "userMapper", userMapper);
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void renameFamily_success() throws Exception {
|
|
|
+ when(familyMemberService.renameFamily(1L, "新家庭")).thenReturn("新家庭");
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/family/member/rename-family")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"name\":\"新家庭\"}")
|
|
|
+ .requestAttr("userId", 1L))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data").value("新家庭"));
|
|
|
+
|
|
|
+ verify(familyMemberService).renameFamily(1L, "新家庭");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void renameFamily_notAdmin_returnsError() throws Exception {
|
|
|
+ when(familyMemberService.renameFamily(1L, "新家庭"))
|
|
|
+ .thenThrow(new IllegalArgumentException("仅家庭管理员可修改家庭名称"));
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/family/member/rename-family")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"name\":\"新家庭\"}")
|
|
|
+ .requestAttr("userId", 1L))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
+ .andExpect(jsonPath("$.message").value("仅家庭管理员可修改家庭名称"));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行测试验证失败**
|
|
|
+
|
|
|
+运行:`mvn test -Dtest=FamilyMembersControllerRenameFamilyTest -DfailIfNoTests=false`
|
|
|
+预期:FAIL(路由不存在,404)
|
|
|
+
|
|
|
+- [ ] **步骤 3:编写最少实现代码**
|
|
|
+
|
|
|
+在 `FamilyMembersController.java` 的 `updateMember` 方法之后新增:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Operation(summary = "修改家庭名称(仅家庭管理员)")
|
|
|
+ @PostMapping("/rename-family")
|
|
|
+ public Result<String> renameFamily(@RequestBody Map<String, Object> body,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null || userId <= 0) {
|
|
|
+ return Result.error(401, "请先登录");
|
|
|
+ }
|
|
|
+ String name = body != null && body.get("name") != null ? body.get("name").toString() : null;
|
|
|
+ try {
|
|
|
+ String newName = familyMemberService.renameFamily(userId, name);
|
|
|
+ return Result.success(newName);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+`Map`/`Result`/`RequestBody`/`RequestAttribute`/`PostMapping`/`Operation` 均已 import。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试验证通过**
|
|
|
+
|
|
|
+运行:`mvn test -Dtest=FamilyMembersControllerRenameFamilyTest -DfailIfNoTests=false`
|
|
|
+预期:PASS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerRenameFamilyTest.java
|
|
|
+git commit -m "feat(family): 新增 POST /api/family/member/rename-family 接口"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 3:前端 api.js + 家庭成员页弹窗
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/utils/api.js`(`updateFamilyMember` 之后新增)
|
|
|
+- 修改:`cfc-frontend/pages/profile-extra/family-members.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:api.js 新增封装**
|
|
|
+
|
|
|
+在 `utils/api.js` 的 `updateFamilyMember`(约 line 692)之后新增:
|
|
|
+
|
|
|
+```js
|
|
|
+// 修改家庭名称(仅家庭管理员)
|
|
|
+export const renameFamily = (data) => {
|
|
|
+ return request('/api/family/member/rename-family', 'POST', data)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:family-members.vue 头部加编辑图标**
|
|
|
+
|
|
|
+模板头部(line 7-10)改为:
|
|
|
+
|
|
|
+```html
|
|
|
+<view class="family-header-text">
|
|
|
+ <view class="family-name-row">
|
|
|
+ <text class="family-name">{{ familyName || '我的家庭' }}</text>
|
|
|
+ <text class="family-name-edit" v-if="isAdmin" @click="openRenameModal">✏️</text>
|
|
|
+ </view>
|
|
|
+ <text class="family-member-count">{{ filteredMembers.length }} 位成员</text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:data 新增状态**
|
|
|
+
|
|
|
+在 `data()` 返回对象的 `permGranting: false,` 之后新增:
|
|
|
+
|
|
|
+```js
|
|
|
+ // ===== 家庭名称编辑弹窗状态 =====
|
|
|
+ showRenameModal: false,
|
|
|
+ renameInput: '',
|
|
|
+ renameSaving: false,
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:methods 新增方法**
|
|
|
+
|
|
|
+在 `closeReportPermission()` 之后新增:
|
|
|
+
|
|
|
+```js
|
|
|
+ // ===== 家庭名称编辑(仅管理员) =====
|
|
|
+ openRenameModal() {
|
|
|
+ this.renameInput = this.familyName || ''
|
|
|
+ this.showRenameModal = true
|
|
|
+ },
|
|
|
+ closeRenameModal() {
|
|
|
+ this.showRenameModal = false
|
|
|
+ },
|
|
|
+ saveRename() {
|
|
|
+ var self = this
|
|
|
+ if (this.renameSaving) return
|
|
|
+ var name = (this.renameInput || '').trim()
|
|
|
+ if (!name) {
|
|
|
+ uni.showToast({ title: '家庭名称不能为空', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.renameSaving = true
|
|
|
+ renameFamily({ name: name }).then(function(res) {
|
|
|
+ self.renameSaving = false
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ self.familyName = res.data
|
|
|
+ self.showRenameModal = false
|
|
|
+ uni.showToast({ title: '已更新', icon: 'success' })
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '更新失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.renameSaving = false
|
|
|
+ uni.showToast({ title: '更新失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+并在 import 区(line 180-184 的 `reportAppEnter` 之后)加入:
|
|
|
+
|
|
|
+```js
|
|
|
+ renameFamily,
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:新增弹窗模板**
|
|
|
+
|
|
|
+在「报告权限管理弹窗」闭合 `</view>`(`showPermModal` 的 `</view>`,约 line 151)之后、底部操作栏之前新增:
|
|
|
+
|
|
|
+```html
|
|
|
+ <!-- ===== 编辑家庭名称弹窗(仅管理员) ===== -->
|
|
|
+ <view class="modal-mask" v-if="showRenameModal" @click="closeRenameModal">
|
|
|
+ <view class="rename-modal-card" @click.stop>
|
|
|
+ <view class="perm-header">
|
|
|
+ <text class="perm-title">✏️ 编辑家庭名称</text>
|
|
|
+ <text class="perm-close" @click="closeRenameModal">×</text>
|
|
|
+ </view>
|
|
|
+ <input class="rename-input" v-model="renameInput" maxlength="20" placeholder="请输入家庭名称" />
|
|
|
+ <view class="rename-actions">
|
|
|
+ <button class="rename-btn rename-cancel" @click="closeRenameModal">取消</button>
|
|
|
+ <button class="rename-btn rename-save" :loading="renameSaving" @click="saveRename">保存</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 6:新增样式**
|
|
|
+
|
|
|
+在 `<style>` 中追加:
|
|
|
+
|
|
|
+```scss
|
|
|
+/* ===== 家庭名称编辑 ===== */
|
|
|
+.family-name-row { display: flex; align-items: center; }
|
|
|
+.family-name-edit { margin-left: 10rpx; font-size: calc(26rpx * var(--font-scale, 1)); }
|
|
|
+.rename-modal-card { width: 600rpx; background: $app-surface; border-radius: 24rpx; padding: 40rpx; }
|
|
|
+.rename-input { border: 1rpx solid $facade-border-soft; border-radius: 12rpx; padding: 20rpx; font-size: calc(30rpx * var(--font-scale, 1)); margin-top: 24rpx; }
|
|
|
+.rename-actions { display: flex; gap: 20rpx; margin-top: 32rpx; }
|
|
|
+.rename-btn { flex: 1; border-radius: 24rpx; font-size: calc(28rpx * var(--font-scale, 1)); }
|
|
|
+.rename-cancel { background: $facade-fill-2; color: $facade-text-body; }
|
|
|
+.rename-save { background: $brand-primary; color: $app-surface; }
|
|
|
+```
|
|
|
+
|
|
|
+(`.modal-mask`/`.perm-header`/`.perm-title`/`.perm-close` 已存在,复用。)
|
|
|
+
|
|
|
+- [ ] **步骤 7:前端校验**
|
|
|
+
|
|
|
+运行:`node scripts/verify-vue-compile.js pages/profile-extra/family-members.vue`
|
|
|
+预期:模板/样式/脚本三项 PASS
|
|
|
+运行:`node scripts/audit-mp-antipatterns.js`
|
|
|
+预期:ERROR 0
|
|
|
+
|
|
|
+- [ ] **步骤 8:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/utils/api.js cfc-frontend/pages/profile-extra/family-members.vue
|
|
|
+git commit -m "feat(family): 家庭成员页支持编辑家庭名称(管理员)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 4:接口文档同步
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+- [ ] **步骤 1:记录新接口**
|
|
|
+
|
|
|
+在 API_REFERENCE.md 家庭成员接口表(新增成员相关接口表格)追加一行:
|
|
|
+
|
|
|
+```markdown
|
|
|
+| 修改家庭名称 | `POST /api/family/member/rename-family` | 仅家庭管理员可改,body `{name}`,返回新名称 |
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/api/API_REFERENCE.md
|
|
|
+git commit -m "docs(api): 记录 /api/family/member/rename-family 接口"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 5:全量编译验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:后端编译**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 2:前端全站校验**
|
|
|
+
|
|
|
+运行:`cd cfc-frontend && node scripts/verify-vue-compile.js`
|
|
|
+预期:全部 PASS
|
|
|
+
|
|
|
+> 注:打包必须由用户在 HBuilderX 完成(AGENTS.md 规范),Agent 不执行 build。
|