2026-06-15-family-member-cards.md 23 KB

家庭成员卡片 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) for tracking.

Goal: Add horizontal-scrolling family member card strip to four tab pages (home/body/mind/action), with per-member data filtering and show_to_family visibility control.

Architecture: Add show_to_family column to children and users tables via DatabaseInitializer. Create FamilyMemberCard + FamilyMemberStrip components. Two new API endpoints under FamilyUserController for visible members query and visibility toggle. Integrate strip into all four dimension pages above FamilyEnergyBar.

Tech Stack: Spring Boot 2.7.18 / MyBatis-Plus / uni-app Vue 2 (Options API, no ?. operator, no CSS Grid)


Task 1: Database migration — add show_to_family column

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

  • [ ] Step 1: Add migration block after existing family_member_attributes migration (~line 1556)

    // ===== 迁移: children + users 表添加 show_to_family 字段(v1.3 家庭成员可见性) =====
    try {
    String checkChildCol = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'children' AND COLUMN_NAME = 'show_to_family'";
    if (jdbcTemplate.queryForObject(checkChildCol, Integer.class) == 0) {
        jdbcTemplate.execute("ALTER TABLE children ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'");
        log.info("已添加 show_to_family 列到 children 表");
    } else {
        log.info("children.show_to_family 已存在,跳过");
    }
    } catch (Exception e) {
    log.warn("添加 children.show_to_family 列失败: {}", e.getMessage());
    }
    
    try {
    String checkUserCol = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'show_to_family'";
    if (jdbcTemplate.queryForObject(checkUserCol, Integer.class) == 0) {
        jdbcTemplate.execute("ALTER TABLE users ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'");
        log.info("已添加 show_to_family 列到 users 表");
    } else {
        log.info("users.show_to_family 已存在,跳过");
    }
    } catch (Exception e) {
    log.warn("添加 users.show_to_family 列失败: {}", e.getMessage());
    }
    
  • [ ] Step 2: Verify compilation

Run: cd cfc-backend && mvn clean compile -q Expected: BUILD SUCCESS

  • [ ] Step 3: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
    git commit -m "feat: add show_to_family column migration to children and users tables"
    

Task 2: Update DTOs — add showToFamily field

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateChildDTO.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/dto/ChildInfoDTO.java

  • [ ] Step 1: Add showToFamily to UpdateChildDTO

    // After line 17 (private String theme;)
    private Integer showToFamily; // 是否对家庭成员可见: 1=是, 0=否
    
  • [ ] Step 2: Add showToFamily to ChildInfoDTO

    // After line 21 (private Integer focusRemaining;)
    private Integer showToFamily; // 是否对家庭成员可见: 1=是, 0=否
    
  • [ ] Step 3: Verify compilation

Run: cd cfc-backend && mvn clean compile -q Expected: BUILD SUCCESS

  • [ ] Step 4: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateChildDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/ChildInfoDTO.java
    git commit -m "feat: add showToFamily field to ChildInfoDTO and UpdateChildDTO"
    

Task 3: UserService — add getVisibleMembers and updateVisibility methods

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java

  • [ ] Step 1: Create FamilyMemberVO DTO

    package com.etotem.cfc.dto;
    
    import lombok.Data;
    
    @Data
    public class FamilyMemberVO {
    private Long id;
    private String nickname;
    private String avatar;
    private String memberType; // 'child' 或 'parent'
    private String roleLabel;  // '孩子' 或 '家长'
    private Integer totalPoints;
    private Integer showToFamily; // 1=可见, 0=不可见
    }
    
  • [ ] Step 2: Add getVisibleMembers method to UserService

Insert after getChildren() method (after line 749):

public List<FamilyMemberVO> getVisibleMembers(Long userId) {
    User user = userMapper.selectById(userId);
    if (user == null || user.getFamilyId() == null) {
        return Collections.emptyList();
    }

    Long familyId = user.getFamilyId();
    List<FamilyMemberVO> members = new ArrayList<>();

    // 查询可见的家长成员
    List<User> parents = userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(User::getFamilyId, familyId)
            .eq(User::getRole, "parent")
            .eq(User::getShowToFamily, 1)
    );
    for (User p : parents) {
        FamilyMemberVO vo = new FamilyMemberVO();
        vo.setId(p.getId());
        vo.setNickname(p.getNickname());
        vo.setAvatar(p.getAvatar());
        vo.setMemberType("parent");
        vo.setRoleLabel("家长");
        vo.setTotalPoints(p.getTotalPoints());
        vo.setShowToFamily(p.getShowToFamily());
        members.add(vo);
    }

    // 查询可见的孩子成员
    List<Child> children = childMapper.selectList(
        new LambdaQueryWrapper<Child>()
            .eq(Child::getFamilyId, familyId)
            .eq(Child::getShowToFamily, 1)
    );
    for (Child c : children) {
        FamilyMemberVO vo = new FamilyMemberVO();
        vo.setId(c.getId());
        vo.setNickname(c.getNickname());
        vo.setAvatar(null);
        vo.setMemberType("child");
        vo.setRoleLabel("孩子");
        vo.setTotalPoints(c.getTotalPoints());
        vo.setShowToFamily(c.getShowToFamily());
        members.add(vo);
    }

    return members;
}

Add imports at top of file:

import com.etotem.cfc.dto.FamilyMemberVO;
import java.util.ArrayList;
import java.util.Collections;
  • Step 3: Add updateChildVisibility method to UserService

Insert after getVisibleMembers:

public boolean updateVisibility(Long userId, Long memberId, String memberType, Integer showToFamily) {
    User user = userMapper.selectById(userId);
    if (user == null) return false;

    if ("child".equals(memberType)) {
        // 只有家长可以修改孩子的可见性
        if (!"parent".equals(user.getRole())) return false;
        Child child = childMapper.selectById(memberId);
        if (child == null || !child.getFamilyId().equals(user.getFamilyId())) return false;
        child.setShowToFamily(showToFamily);
        child.setUpdatedAt(new Date());
        childMapper.updateById(child);
        return true;
    } else if ("parent".equals(memberType)) {
        // 用户只能修改自己的可见性
        if (!memberId.equals(userId)) return false;
        user.setShowToFamily(showToFamily);
        user.setUpdatedAt(new Date());
        userMapper.updateById(user);
        return true;
    }
    return false;
}
  • Step 4: Update toChildInfoDTO to include showToFamily

In the existing toChildInfoDTO method (line 802+), add after line ~819:

dto.setShowToFamily(child.getShowToFamily());
  • Step 5: Update updateChild method to handle showToFamily

In updateChild method (line 751), add before child.setUpdatedAt(new Date()):

if (dto.getShowToFamily() != null) {
    child.setShowToFamily(dto.getShowToFamily());
}
  • Step 6: Verify compilation

Run: cd cfc-backend && mvn clean compile -q Expected: BUILD SUCCESS

  • [ ] Step 7: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
    git commit -m "feat: add getVisibleMembers and updateVisibility to UserService"
    

Task 4: FamilyUserController — add two new API endpoints

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java

  • [ ] Step 1: Add getVisibleFamilyMembers endpoint

Insert before getFamilyMembers (before line 167):

@Operation(summary = "获取对当前用户可见的家庭成员列表")
@PostMapping("/members/visible")
public Result<List<FamilyMemberVO>> getVisibleFamilyMembers(javax.servlet.http.HttpServletRequest request) {
    Long userId = getUserId(request);
    List<FamilyMemberVO> members = userService.getVisibleMembers(userId);
    return Result.success(members);
}
  • Step 2: Add updateMemberVisibility endpoint

Insert after the above:

@Operation(summary = "更新家庭成员可见性")
@PostMapping("/members/update-visibility")
public Result<Boolean> updateMemberVisibility(javax.servlet.http.HttpServletRequest request,
                                               @RequestBody Map<String, Object> body) {
    Long userId = getUserId(request);
    Long memberId = Long.valueOf(body.get("memberId").toString());
    String memberType = (String) body.get("memberType");
    Integer showToFamily = Integer.valueOf(body.get("showToFamily").toString());
    boolean success = userService.updateVisibility(userId, memberId, memberType, showToFamily);
    return Result.success(success);
}
  • Step 3: Add missing import

Ensure FamilyMemberVO is imported via the existing import com.etotem.cfc.dto.*; on line 5 — it's already a wildcard import, so FamilyMemberVO will be auto-included.

  • Step 4: Verify compilation

Run: cd cfc-backend && mvn clean compile -q Expected: BUILD SUCCESS

  • [ ] Step 5: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
    git commit -m "feat: add /family/user/members/visible and /members/update-visibility endpoints"
    

Task 5: Frontend API — add new API functions

Files:

  • Modify: cfc-frontend/utils/api.js

  • [ ] **Step 1: Add getVisibleFamilyMembers and updateMemberVisibility exports

Insert after getFamilyMembers (after line 146):

// 获取对当前用户可见的家庭成员列表(已过滤 show_to_family=0 的成员)
export const getVisibleFamilyMembers = () => {
  return request('/api/family/user/members/visible', 'POST')
}

// 更新成员可见性
export const updateMemberVisibility = (memberId, memberType, showToFamily) => {
  return request('/api/family/user/members/update-visibility', 'POST', { memberId, memberType, showToFamily })
}
  • [ ] Step 2: Commit

    git add cfc-frontend/utils/api.js
    git commit -m "feat: add getVisibleFamilyMembers and updateMemberVisibility API functions"
    

Task 6: FamilyMemberCard component

Files:

  • Create: cfc-frontend/components/FamilyMemberCard.vue

  • [ ] Step 1: Create component

    <template>
    <view class="family-member-card" :class="{ 'is-active': isSelected }" @click="onSelect">
    <view class="member-avatar-wrap">
      <image v-if="member.avatar" class="member-avatar" :src="member.avatar" mode="aspectFill" />
      <view v-else class="member-avatar-default">
        <text class="avatar-letter">{{ displayLetter }}</text>
      </view>
      <text class="member-role-badge">{{ member.memberType === 'parent' ? '家长' : '孩子' }}</text>
    </view>
    <text class="member-name">{{ member.nickname }}</text>
    <text class="member-dan" v-if="member.danLevel">{{ member.danLevel }}</text>
    <view class="selected-check" v-if="isSelected">
      <text class="check-icon">✓</text>
    </view>
    </view>
    </template>
    
    <script>
    export default {
    props: {
    member: { type: Object, required: true },
    isSelected: { type: Boolean, default: false }
    },
    computed: {
    displayLetter: function() {
      if (!this.member.nickname) return '?'
      return this.member.nickname[0]
    }
    },
    methods: {
    onSelect: function() {
      this.$emit('select', this.member)
    }
    }
    }
    </script>
    
    <style scoped>
    .family-member-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 16rpx 20rpx;
    margin-right: 16rpx;
    background: #fff;
    border-radius: 20rpx;
    min-width: 130rpx;
    position: relative;
    box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
    border: 3rpx solid transparent;
    transition: all 0.2s;
    }
    .family-member-card.is-active {
    border-color: #F97316;
    background: #FFF7ED;
    }
    .member-avatar-wrap {
    position: relative;
    margin-bottom: 8rpx;
    }
    .member-avatar {
    width: 72rpx;
    height: 72rpx;
    border-radius: 50%;
    }
    .member-avatar-default {
    width: 72rpx;
    height: 72rpx;
    border-radius: 50%;
    background: #FDE68A;
    display: flex;
    align-items: center;
    justify-content: center;
    }
    .avatar-letter {
    font-size: 32rpx;
    color: #92400E;
    font-weight: bold;
    }
    .member-role-badge {
    position: absolute;
    bottom: -4rpx;
    right: -8rpx;
    font-size: 18rpx;
    color: #fff;
    background: #F97316;
    padding: 2rpx 8rpx;
    border-radius: 12rpx;
    line-height: 1.4;
    transform: scale(0.85);
    }
    .member-name {
    font-size: 24rpx;
    color: #333;
    font-weight: 500;
    text-align: center;
    max-width: 100rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }
    .member-dan {
    font-size: 18rpx;
    color: #F97316;
    margin-top: 2rpx;
    }
    .selected-check {
    position: absolute;
    top: 4rpx;
    right: 4rpx;
    width: 32rpx;
    height: 32rpx;
    background: #F97316;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    }
    .check-icon {
    color: #fff;
    font-size: 20rpx;
    font-weight: bold;
    }
    </style>
    
  • [ ] Step 2: Commit

    git add cfc-frontend/components/FamilyMemberCard.vue
    git commit -m "feat: add FamilyMemberCard component"
    

Task 7: FamilyMemberStrip component

Files:

  • Create: cfc-frontend/components/FamilyMemberStrip.vue

  • [ ] Step 1: Create component

    <template>
    <view class="member-strip" v-if="members && members.length > 0">
    <scroll-view class="strip-scroll" scroll-x :show-scrollbar="false" :enhanced="true">
      <view class="strip-inner">
        <FamilyMemberCard
          v-for="m in members"
          :key="m.memberType + '-' + m.id"
          :member="m"
          :isSelected="m.id === selectedMemberId && m.memberType === selectedMemberType"
          @select="onSelectMember"
        />
      </view>
    </scroll-view>
    </view>
    </template>
    
    <script>
    import FamilyMemberCard from './FamilyMemberCard.vue'
    
    export default {
    components: { FamilyMemberCard },
    props: {
    members: { type: Array, default: function() { return [] } },
    selectedMemberId: { type: Number, default: null },
    selectedMemberType: { type: String, default: '' }
    },
    methods: {
    onSelectMember: function(member) {
      this.$emit('select', member)
    }
    }
    }
    </script>
    
    <style scoped>
    .member-strip {
    margin: 0 30rpx 16rpx;
    }
    .strip-scroll {
    width: 100%;
    white-space: nowrap;
    }
    .strip-inner {
    display: flex;
    flex-direction: row;
    padding: 4rpx 0;
    }
    </style>
    
  • [ ] Step 2: Commit

    git add cfc-frontend/components/FamilyMemberStrip.vue
    git commit -m "feat: add FamilyMemberStrip horizontal scrolling component"
    

Task 8: Integrate FamilyMemberStrip into parent-index.vue

Files:

  • Modify: cfc-frontend/pages/index/parent-index.vue

  • [ ] Step 1: Add component import (after line ~361)

    import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
    
  • [ ] Step 2: Add to components block (around line ~375)

    FamilyMemberStrip,
    
  • [ ] Step 3: Add data properties (in data() around line ~390)

    familyMembersVisible: [],
    selectedMemberId: null,
    selectedMemberType: 'child',
    
  • [ ] Step 4: Insert FamilyMemberStrip in template after PageBanner

Insert between PageBanner (line ~10) and the children-row section (line ~13):

<!-- ===== 家庭成员卡片条 ===== -->
<FamilyMemberStrip
  v-if="isLoggedIn"
  :members="familyMembersVisible"
  :selectedMemberId="selectedMemberId"
  :selectedMemberType="selectedMemberType"
  @select="onFamilyMemberSelect" />
  • Step 5: Add data loading in loadData() method

After the existing getFamilyMembers() call (~line 488), add:

try {
  const visibleRes = await getVisibleFamilyMembers()
  const visibleMembers = visibleRes.data || []
  this.familyMembersVisible = visibleMembers
  // 默认选中第一个孩子
  var firstChild = visibleMembers.find(function(m) { return m.memberType === 'child' })
  if (firstChild) {
    this.selectedMemberId = firstChild.id
    this.selectedMemberType = firstChild.memberType
  }
} catch (e) {
  console.log('获取可见家庭成员失败', e)
}
  • [ ] Step 6: Add onFamilyMemberSelect method

    onFamilyMemberSelect: function(member) {
    this.selectedMemberId = member.id
    this.selectedMemberType = member.memberType
    // 切换选中孩子以刷新数据
    if (member.memberType === 'child') {
    this.selectedChild = { id: member.id, nickname: member.nickname }
    this.loadChildData(member.id)
    }
    // 如果是家长,切换为家长视图
    }
    
  • [ ] Step 7: Add import for getVisibleFamilyMembers

Find the existing import line ~354:

import { getFamilyMembers, getChildren, ... } from '../../utils/api.js'

Change to:

import { getFamilyMembers, getChildren, getVisibleFamilyMembers, getPendingReviewTasks, ... } from '../../utils/api.js'
  • [ ] Step 8: Commit

    git add cfc-frontend/pages/index/parent-index.vue
    git commit -m "feat: integrate FamilyMemberStrip into parent-index page"
    

Task 9: Integrate FamilyMemberStrip into body/index.vue

Files:

  • Modify: cfc-frontend/pages/body/index.vue

  • [ ] Step 1: Add component import after existing imports (~line 1-5)

    import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
    import { getVisibleFamilyMembers } from '../../utils/api.js'
    
  • [ ] Step 2: Add to components block

    FamilyMemberStrip,
    
  • [ ] Step 3: Add data properties (in data())

    familyMembersVisible: [],
    selectedMemberId: null,
    selectedMemberType: '',
    
  • [ ] Step 4: Insert FamilyMemberStrip in template after PageBanner (after line 6)

    <!-- ===== 家庭成员卡片条 ===== -->
    <FamilyMemberStrip
    v-if="isLoggedIn"
    :members="familyMembersVisible"
    :selectedMemberId="selectedMemberId"
    :selectedMemberType="selectedMemberType"
    @select="onFamilyMemberSelect" />
    
  • [ ] Step 5: Add data loading and onFamilyMemberSelect method

In onShow or a dedicated loadFamilyMembers method:

loadVisibleMembers: function() {
  var self = this
  getVisibleFamilyMembers().then(function(res) {
    if (res && res.data) {
      self.familyMembersVisible = res.data
    }
  }).catch(function(e) {
    console.log('获取可见家庭成员失败', e)
  })
},
onFamilyMemberSelect: function(member) {
  this.selectedMemberId = member.id
  this.selectedMemberType = member.memberType
  // 切换该成员的健康数据
  this.loadHealthData(member)
}
  • [ ] Step 6: Commit

    git add cfc-frontend/pages/body/index.vue
    git commit -m "feat: integrate FamilyMemberStrip into body page"
    

Task 10: Integrate FamilyMemberStrip into mind/index.vue

Files:

  • Modify: cfc-frontend/pages/mind/index.vue

  • [ ] Step 1: Add import + component + data + template insertion

Same pattern as Task 9 — insert FamilyMemberStrip after PageBanner, load getVisibleFamilyMembers, add onFamilyMemberSelect.

Find the template block and add after PageBanner closing tag (after line ~6):

<FamilyMemberStrip
  v-if="isLoggedIn"
  :members="familyMembersVisible"
  :selectedMemberId="selectedMemberId"
  :selectedMemberType="selectedMemberType"
  @select="onFamilyMemberSelect" />
  • [ ] Step 2: Commit

    git add cfc-frontend/pages/mind/index.vue
    git commit -m "feat: integrate FamilyMemberStrip into mind page"
    

Task 11: Integrate FamilyMemberStrip into action/index.vue

Files:

  • Modify: cfc-frontend/pages/action/index.vue

  • [ ] Step 1: Same pattern as Task 9/10

Insert after PageBanner (~line 6):

<FamilyMemberStrip
  v-if="isLoggedIn"
  :members="familyMembersVisible"
  :selectedMemberId="selectedMemberId"
  :selectedMemberType="selectedMemberType"
  @select="onFamilyMemberSelect" />
  • [ ] Step 2: Commit

    git add cfc-frontend/pages/action/index.vue
    git commit -m "feat: integrate FamilyMemberStrip into action page"
    

Task 12: Visibility toggle UI — child edit page

Files:

  • Modify: cfc-frontend/pages/profile/edit-child.vue

  • [ ] Step 1: Add visibility toggle switch after the theme picker (after line ~65)

    <!-- 对家庭成员可见 -->
    <view class="form-item">
    <text class="label">对家庭成员可见</text>
    <switch :checked="form.showToFamily !== 0" @change="onVisibilityChange" color="#F97316" />
    <text class="hint">关闭后其他家庭成员看不到该孩子</text>
    </view>
    
  • [ ] Step 2: Add change handler and form default

    onVisibilityChange: function(e) {
    this.form.showToFamily = e.detail.value ? 1 : 0
    }
    

In data(), set form default:

form: {
  // ... existing fields
  showToFamily: 1
}

In onLoad, when loading child data:

// after setting form fields
self.form.showToFamily = child.showToFamily !== undefined ? child.showToFamily : 1
  • [ ] Step 3: Commit

    git add cfc-frontend/pages/profile/edit-child.vue
    git commit -m "feat: add showToFamily toggle to child edit page"
    

Task 13: Visibility toggle UI — profile page (for self)

Files:

  • Modify: cfc-frontend/pages/profile/profile.vue

  • [ ] Step 1: Add visibility toggle in settings section

Find the profile settings area and add:

<view class="setting-item">
  <text class="setting-label">对家庭成员可见</text>
  <switch :checked="showToFamily !== 0" @change="onSelfVisibilityChange" color="#F97316" />
</view>
  • [ ] Step 2: Add handler

    onSelfVisibilityChange: function(e) {
    var show = e.detail.value ? 1 : 0
    var self = this
    updateMemberVisibility(self.userId, 'parent', show).then(function(res) {
    if (res && res.code === 200) {
      uni.showToast({ title: '已更新', icon: 'success' })
    }
    })
    }
    
  • [ ] Step 3: Commit

    git add cfc-frontend/pages/profile/profile.vue
    git commit -m "feat: add showToFamily toggle to profile page"
    

Task 14: Final verification

  • Step 1: Backend compilation

Run: cd cfc-backend && mvn clean compile -q Expected: BUILD SUCCESS

  • Step 2: Quick LSP check on modified frontend files

Check for template syntax issues (no ?., no CSS Grid).

  • [ ] Step 3: Verify file checklist

  • [ ] DatabaseInitializer.java — migration added

  • [ ] UpdateChildDTO.java — showToFamily field

  • [ ] ChildInfoDTO.java — showToFamily field

  • [ ] FamilyMemberVO.java — new DTO

  • [ ] UserService.java — getVisibleMembers + updateVisibility

  • [ ] FamilyUserController.java — 2 new endpoints

  • [ ] api.js — 2 new API functions

  • [ ] FamilyMemberCard.vue — new component

  • [ ] FamilyMemberStrip.vue — new component

  • [ ] parent-index.vue — strip integrated

  • [ ] body/index.vue — strip integrated

  • [ ] mind/index.vue — strip integrated

  • [ ] action/index.vue — strip integrated

  • [ ] edit-child.vue — visibility toggle

  • [ ] profile.vue — visibility toggle (self)