Explorar o código

refactor(beautify): F 批 TagChip 组件化 + PlayfulButton block 修复

- 新增 components/TagChip.vue:抽取 4 页重复的 .tag-btn/.tag-active 标签胶囊样式
  (variant=brand 成长记录页品牌橙 / variant=body 身维度首页身体色),
  支持 label/active/variant + tap 事件
- growth/diet-checkin、growth/exercise-checkin、health/diet-index、health/exercise-index
  改用 TagChip,删除 4 页各自的 tag 样式(共约 40 行重复 CSS)
- PlayfulButton:修复 block 属性只声明不生效(:class 未绑定 playful-btn--block),
  wishes/index 的全宽按钮恢复预期样式
- 6 个文件 node --check + style 块 dart-sass 编译全部通过
- SelectableCard(user-edit/service-role-apply 视觉不一致)与 StepDots(仅 1 页使用)
  经核对不满足抽取前提,按计划保留
liaoxg hai 5 días
pai
achega
b344594c81

+ 2 - 1
cfc-frontend/components/PlayfulButton.vue

@@ -3,7 +3,8 @@
     class="playful-btn"
     :class="['playful-btn--' + variant, 'playful-btn--' + size, {
       'playful-btn--disabled': disabled,
-      'playful-btn--clay': variant === 'clay'
+      'playful-btn--clay': variant === 'clay',
+      'playful-btn--block': block
     }]"
     :disabled="disabled"
     @click="emitClick"

+ 91 - 0
cfc-frontend/components/TagChip.vue

@@ -0,0 +1,91 @@
+<template>
+  <view
+    class="tag-chip"
+    :class="['tag-chip--' + variant, { 'is-active': active }]"
+    @tap="onTap"
+  >
+    <text class="tag-chip__label">{{ label }}</text>
+  </view>
+</template>
+
+<script>
+/**
+ * TagChip — 单选/多选标签胶囊
+ *
+ * 抽取自 diet-checkin / exercise-checkin / diet-index / exercise-index 的
+ * .tag-btn + .tag-active 重复样式(F 批 · 重复模式组件化)。
+ *
+ * Props:
+ *   label    — 文案
+ *   active   — 是否选中
+ *   variant  — 'brand'(成长记录页,品牌橙选中态)| 'body'(身维度首页,身体维度色选中态)
+ * Events:
+ *   tap      — 点击
+ */
+export default {
+  name: 'TagChip',
+  props: {
+    label: { type: String, default: '' },
+    active: { type: Boolean, default: false },
+    variant: { type: String, default: 'brand' }
+  },
+  methods: {
+    onTap() {
+      this.$emit('tap')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.tag-chip {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12rpx 24rpx;
+  border: 2rpx solid $facade-fill-4;
+  border-radius: 30rpx;
+  background: $facade-fill-ghost;
+  transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.tag-chip__label {
+  font-size: 26rpx;
+  color: $facade-text-body;
+}
+
+.tag-chip.is-active {
+  border-color: $brand-primary;
+  background: $fortune-1-tint;
+  box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.15);
+}
+
+.tag-chip.is-active .tag-chip__label {
+  color: $brand-primary;
+  font-weight: 600;
+}
+
+/* ---- 身维度首页(饮食/运动)选中态 ---- */
+.tag-chip--body {
+  border-width: 1rpx;
+  border-color: $facade-border-soft;
+  border-radius: 30rpx;
+  background-color: $facade-fill-ghost;
+  transition: none;
+}
+
+.tag-chip--body .tag-chip__label {
+  font-size: 24rpx;
+}
+
+.tag-chip--body.is-active {
+  border-color: $dim-body;
+  background-color: $flat-card;
+  box-shadow: none;
+}
+
+.tag-chip--body.is-active .tag-chip__label {
+  color: $dim-body;
+  font-weight: 400;
+}
+</style>

+ 5 - 9
cfc-frontend/pages/growth/diet-checkin.vue

@@ -23,11 +23,9 @@
         </view>
         <view class="card-body">
           <view class="tag-row">
-            <view class="tag-btn" v-for="m in mealTypes" :key="m.value"
-              :class="form.mealType === m.value ? 'tag-active' : ''"
-              @tap="form.mealType = m.value">
-              <text>{{ m.label }}</text>
-            </view>
+            <TagChip v-for="m in mealTypes" :key="m.value" :label="m.label"
+              :active="form.mealType === m.value"
+              @tap="form.mealType = m.value" />
           </view>
         </view>
       </view>
@@ -164,8 +162,10 @@ import { parseDate } from '../../utils/format.js'
 import { sttTranscribe, recognizeDietFood } from '../../utils/api.js'
 import shareMixin from '../../components/share-mixin.js'
 import { pickImage } from '../../utils/image-picker.js'
+import TagChip from '../../components/TagChip.vue'
 
 export default {
+  components: { TagChip },
   mixins: [shareMixin],
   data() {
     return {
@@ -515,10 +515,6 @@ success: function(res) {
 .form-group { margin-bottom: 20rpx; }
 .form-label { font-size: 26rpx; color: $facade-text-strong; font-weight: 500; margin-bottom: 10rpx; display: block; }
 .tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; }
-.tag-btn { padding: 12rpx 24rpx; border-radius: 30rpx; border: 2rpx solid $facade-fill-4; background: $facade-fill-ghost; transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; }
-.tag-active { border-color: $brand-primary; background: $fortune-1-tint; box-shadow: 0 4rpx 12rpx rgba(249,115,22,0.15); }
-.tag-btn text { font-size: 26rpx; color: $facade-text-body; }
-.tag-active text { color: $brand-primary; font-weight: 600; }
 .form-textarea { width: 100%; height: 120rpx; border: 1rpx solid $facade-fill-4; border-radius: 14rpx; padding: 16rpx; font-size: 26rpx; background: $facade-fill-ghost; box-sizing: border-box; font-family: inherit; }
 .form-input { width: 100%; height: 76rpx; border: 1rpx solid $facade-fill-4; border-radius: 14rpx; padding: 0 16rpx; font-size: 26rpx; background: $facade-fill-ghost; box-sizing: border-box; }
 

+ 8 - 14
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -17,11 +17,9 @@
         </view>
         <view class="card-body">
           <view class="tag-row">
-            <view class="tag-btn" v-for="t in exerciseTypes" :key="t.value"
-              :class="form.exerciseType === t.value ? 'tag-active' : ''"
-              @tap="form.exerciseType = t.value">
-              <text>{{ t.label }}</text>
-            </view>
+            <TagChip v-for="t in exerciseTypes" :key="t.value" :label="t.label"
+              :active="form.exerciseType === t.value"
+              @tap="form.exerciseType = t.value" />
           </view>
         </view>
       </view>
@@ -40,11 +38,9 @@
           <view class="form-group">
             <text class="form-label">强度</text>
             <view class="tag-row">
-              <view class="tag-btn" v-for="s in intensityOptions" :key="s.value"
-                :class="form.intensity === s.value ? 'tag-active' : ''"
-                @tap="form.intensity = s.value">
-                <text>{{ s.label }}</text>
-              </view>
+              <TagChip v-for="s in intensityOptions" :key="s.value" :label="s.label"
+                :active="form.intensity === s.value"
+                @tap="form.intensity = s.value" />
             </view>
           </view>
           <view class="form-group">
@@ -157,8 +153,10 @@ import config from '../../config'
 import { parseDate } from '../../utils/format.js'
 import { sttTranscribe } from '../../utils/api.js'
 import { pickImage } from '../../utils/image-picker.js'
+import TagChip from '../../components/TagChip.vue'
 
 export default {
+  components: { TagChip },
   data() {
     return {
       memberId: null,
@@ -515,10 +513,6 @@ export default {
 .form-group { margin-bottom: 20rpx; }
 .form-label { font-size: 26rpx; color: $facade-text-strong; font-weight: 500; margin-bottom: 10rpx; display: block; }
 .tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; }
-.tag-btn { padding: 12rpx 24rpx; border-radius: 30rpx; border: 2rpx solid $facade-fill-4; background: $facade-fill-ghost; transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; }
-.tag-active { border-color: $brand-primary; background: $fortune-1-tint; box-shadow: 0 4rpx 12rpx rgba(249,115,22,0.15); }
-.tag-btn text { font-size: 26rpx; color: $facade-text-body; }
-.tag-active text { color: $brand-primary; font-weight: 600; }
 .form-input { width: 100%; height: 76rpx; border: 1rpx solid $facade-fill-4; border-radius: 14rpx; padding: 0 16rpx; font-size: 26rpx; background: $facade-fill-ghost; box-sizing: border-box; }
 .step-sync-row { display: flex; align-items: center; gap: 12rpx; }
 .step-input { flex: 1; }

+ 5 - 18
cfc-frontend/pages/health/diet-index.vue

@@ -79,11 +79,9 @@
           <view class="form-group">
             <text class="form-label">餐次 *</text>
             <view class="tag-row">
-              <view class="tag-btn" v-for="m in mealTypes" :key="m.value"
-                :class="form.mealType === m.value ? 'tag-active' : ''"
-                @tap="form.mealType = m.value">
-                <text>{{ m.label }}</text>
-              </view>
+              <TagChip v-for="m in mealTypes" :key="m.value" :label="m.label" variant="body"
+                :active="form.mealType === m.value"
+                @tap="form.mealType = m.value" />
             </view>
           </view>
           <view class="form-group">
@@ -131,8 +129,10 @@
 <script>
   import config from '@/config.js'
   import { parseDate } from '@/utils/format.js'
+  import TagChip from '@/components/TagChip.vue'
 
 export default {
+  components: { TagChip },
   data() {
     return {
       memberId: null,
@@ -325,19 +325,6 @@ export default {
   flex-wrap: wrap;
   gap: 16rpx;
 }
-.tag-btn {
-  padding: 12rpx 24rpx;
-  border: 1rpx solid $facade-border-soft;
-  border-radius: 30rpx;
-  font-size: 24rpx;
-  color: $facade-text-body;
-  background-color: $facade-fill-ghost;
-}
-.tag-active {
-  border-color: $dim-body;
-  color: $dim-body;
-  background-color: $flat-card;
-}
 .submit-btn {
   background-color: $dim-body;
   color: $app-surface;

+ 8 - 23
cfc-frontend/pages/health/exercise-index.vue

@@ -71,11 +71,9 @@
           <view class="form-group">
             <text class="form-label">运动类型 *</text>
             <view class="tag-row">
-              <view class="tag-btn" v-for="t in exerciseTypes" :key="t.value"
-                :class="form.exerciseType === t.value ? 'tag-active' : ''"
-                @tap="form.exerciseType = t.value">
-                <text>{{ t.label }}</text>
-              </view>
+              <TagChip v-for="t in exerciseTypes" :key="t.value" :label="t.label" variant="body"
+                :active="form.exerciseType === t.value"
+                @tap="form.exerciseType = t.value" />
             </view>
           </view>
           <view class="form-group">
@@ -85,11 +83,9 @@
           <view class="form-group">
             <text class="form-label">强度</text>
             <view class="tag-row">
-              <view class="tag-btn" v-for="s in intensityOptions" :key="s.value"
-                :class="form.intensity === s.value ? 'tag-active' : ''"
-                @tap="form.intensity = s.value">
-                <text>{{ s.label }}</text>
-              </view>
+              <TagChip v-for="s in intensityOptions" :key="s.value" :label="s.label" variant="body"
+                :active="form.intensity === s.value"
+                @tap="form.intensity = s.value" />
             </view>
           </view>
           <view class="form-group">
@@ -133,8 +129,10 @@
 <script>
 import config from '@/config.js'
 import { parseDate } from '@/utils/format.js'
+import TagChip from '@/components/TagChip.vue'
 
 export default {
+  components: { TagChip },
   data() {
     return {
       memberId: null,
@@ -332,19 +330,6 @@ export default {
   flex-wrap: wrap;
   gap: 16rpx;
 }
-.tag-btn {
-  padding: 12rpx 24rpx;
-  border: 1rpx solid $facade-border-soft;
-  border-radius: 30rpx;
-  font-size: 24rpx;
-  color: $facade-text-body;
-  background-color: $facade-fill-ghost;
-}
-.tag-active {
-  border-color: $dim-body;
-  color: $dim-body;
-  background-color: $flat-card;
-}
 .submit-btn {
   background-color: $dim-body;
   color: $app-surface;