Procházet zdrojové kódy

feat: add contact management feature (ContactCard, ContactImport, contact-detail)

New components: ContactCard, ContactImport for contact import from phone. New page: contact-detail for viewing/editing contact info. Updated api.js with contact APIs, config.js with endpoint, pages.json with page registration. Updated action/index.vue with contact entry.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User před 4 měsíci
rodič
revize
67f745f6d6

+ 113 - 0
cfc-frontend/components/ContactCard.vue

@@ -0,0 +1,113 @@
+<template>
+  <view class="contact-card" @click="$emit('click', contact)">
+    <view class="card-avatar" :style="{ background: avatarColor }">
+      <text class="avatar-text">{{ initial }}</text>
+    </view>
+    <text class="card-name">{{ contact.name }}</text>
+    <view class="card-type-tag" :style="{ background: typeColor + '20', color: typeColor }">
+      {{ typeLabel }}
+    </view>
+    <text class="card-known" v-if="contact.knownSince">
+      认识 {{ knownYears }} 年
+    </text>
+    <text class="card-phone" v-if="contact.phone">{{ contact.phone }}</text>
+    <view class="card-intimacy">
+      <text class="intimacy-heart">❤️</text>
+      <text class="intimacy-value">{{ contact.intimacyLevel }}</text>
+    </view>
+    <text class="card-birthday" v-if="contact.birthdayCountdown">
+      🎂 {{ contact.birthdayLabel }} · {{ contact.birthdayCountdown }}
+    </text>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    contact: { type: Object, required: true }
+  },
+  computed: {
+    initial: function() {
+      return this.contact.name ? this.contact.name[0] : '?'
+    },
+    typeLabel: function() {
+      var map = { family: '家人', friend: '朋友', partner: '伴侣', colleague: '同事', other: '其他' }
+      return map[this.contact.relationshipType] || '其他'
+    },
+    typeColor: function() {
+      var map = { family: '#10B981', friend: '#F59E0B', partner: '#FF6B35', colleague: '#3B82F6', other: '#94A3B8' }
+      return map[this.contact.relationshipType] || '#94A3B8'
+    },
+    avatarColor: function() {
+      var colors = ['#FF6B35', '#667eea', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899']
+      var idx = (this.contact.id || 0) % colors.length
+      return colors[idx]
+    },
+    knownYears: function() {
+      if (!this.contact.knownSince) return 0
+      var now = new Date()
+      var known = new Date(this.contact.knownSince)
+      return Math.floor((now - known) / (365.25 * 86400000))
+    }
+  }
+}
+</script>
+
+<style scoped>
+.contact-card {
+  width: 240rpx;
+  flex-shrink: 0;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-right: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.contact-card:active { opacity: 0.7; }
+.card-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 12rpx;
+}
+.avatar-text {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #fff;
+}
+.card-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+.card-type-tag {
+  font-size: 20rpx;
+  padding: 2rpx 14rpx;
+  border-radius: 10rpx;
+  margin-bottom: 8rpx;
+}
+.card-known { font-size: 22rpx; color: #999; margin-bottom: 4rpx; }
+.card-phone { font-size: 22rpx; color: #bbb; margin-bottom: 8rpx; }
+.card-intimacy {
+  display: flex;
+  align-items: center;
+  margin-bottom: 6rpx;
+}
+.intimacy-heart { font-size: 22rpx; margin-right: 4rpx; }
+.intimacy-value {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #FF6B35;
+}
+.card-birthday {
+  font-size: 20rpx;
+  color: #EC4899;
+}
+</style>

+ 196 - 0
cfc-frontend/components/ContactImport.vue

@@ -0,0 +1,196 @@
+<template>
+  <view class="overlay" v-if="visible" @click.self="onCancel">
+    <view class="modal">
+      <text class="modal-title">添加联系人</text>
+      <view class="form">
+        <view class="form-row">
+          <text class="form-label">姓名</text>
+          <input class="form-input" v-model="form.name" disabled />
+        </view>
+        <view class="form-row">
+          <text class="form-label">手机号</text>
+          <input class="form-input" v-model="form.phone" disabled />
+        </view>
+        <view class="form-row">
+          <text class="form-label">关系类型</text>
+          <picker mode="selector" :range="typeOptions" @change="onTypeChange">
+            <view class="form-picker">{{ form.relationshipTypeLabel || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="form-row">
+          <text class="form-label">认识时间</text>
+          <picker mode="date" @change="onDateChange">
+            <view class="form-picker">{{ form.knownSince || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="form-row">
+          <text class="form-label">生日</text>
+          <picker mode="date" @change="onBirthdayChange">
+            <view class="form-picker">{{ form.birthday || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="form-row">
+          <text class="form-label">个人介绍</text>
+          <input class="form-input" v-model="form.bio" placeholder="选填" />
+        </view>
+      </view>
+      <view class="modal-actions">
+        <button class="btn-cancel" @click="onCancel">取消</button>
+        <button class="btn-confirm" @click="onConfirm">确认添加</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { importPhoneContact } from '../utils/api.js'
+
+export default {
+  props: {
+    visible: { type: Boolean, default: false },
+    name: { type: String, default: '' },
+    phone: { type: String, default: '' }
+  },
+  data: function() {
+    return {
+      form: {
+        name: '',
+        phone: '',
+        relationshipType: '',
+        relationshipTypeLabel: '',
+        knownSince: '',
+        birthday: '',
+        bio: ''
+      },
+      typeOptions: ['家人', '朋友', '伴侣', '同事', '其他'],
+      typeValues: { '家人': 'family', '朋友': 'friend', '伴侣': 'partner', '同事': 'colleague', '其他': 'other' }
+    }
+  },
+  watch: {
+    name: function(val) { this.form.name = val },
+    phone: function(val) { this.form.phone = val }
+  },
+  methods: {
+    onTypeChange: function(e) {
+      var label = this.typeOptions[e.detail.value]
+      this.form.relationshipType = this.typeValues[label] || 'other'
+      this.form.relationshipTypeLabel = label
+    },
+    onDateChange: function(e) {
+      this.form.knownSince = e.detail.value
+    },
+    onBirthdayChange: function(e) {
+      this.form.birthday = e.detail.value
+    },
+    onConfirm: function() {
+      var self = this
+      var extra = {}
+      if (this.form.relationshipType) extra.relationshipType = this.form.relationshipType
+      if (this.form.knownSince) extra.knownSince = this.form.knownSince
+      if (this.form.birthday) extra.birthday = this.form.birthday
+      if (this.form.bio) extra.bio = this.form.bio
+      importPhoneContact({
+        name: this.form.name,
+        phone: this.form.phone,
+        extra: extra
+      }).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '添加成功', icon: 'success' })
+          self.$emit('success')
+          self.reset()
+        } else {
+          uni.showToast({ title: res.message || '添加失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    onCancel: function() {
+      this.reset()
+      this.$emit('close')
+    },
+    reset: function() {
+      this.form = { name: '', phone: '', relationshipType: '', relationshipTypeLabel: '', knownSince: '', birthday: '', bio: '' }
+      this.$emit('close')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.5);
+  z-index: 999;
+  display: flex;
+  align-items: flex-end;
+}
+.modal {
+  background: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  padding: 40rpx 30rpx;
+  width: 100%;
+  box-sizing: border-box;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 30rpx;
+  display: block;
+  text-align: center;
+}
+.form-row {
+  margin-bottom: 24rpx;
+}
+.form-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.form-input {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  color: #333;
+}
+.form-picker {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.modal-actions {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 40rpx;
+}
+.btn-cancel {
+  flex: 1;
+  padding: 20rpx;
+  border-radius: 12rpx;
+  border: 1rpx solid #ddd;
+  background: #fff;
+  font-size: 28rpx;
+  color: #666;
+  text-align: center;
+}
+.btn-confirm {
+  flex: 2;
+  padding: 20rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #FF6B35, #F97316);
+  font-size: 28rpx;
+  color: #fff;
+  text-align: center;
+  border: none;
+}
+</style>

+ 1 - 1
cfc-frontend/config.js

@@ -23,7 +23,7 @@ try {
       case 'develop':
         // 开发版 - 本地开发
         API_BASE_URL = 'http://localhost:9082'
-        DANSHOP_BASE_URL = 'http://localhost:8081'
+        DANSHOP_BASE_URL = 'http://localhost:8888'
         break
       case 'trial':
         // 体验版

+ 7 - 0
cfc-frontend/pages.json

@@ -126,6 +126,13 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/action/contact-detail",
+      "style": {
+        "navigationBarTitleText": "联系人详情",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/profile/profile",
       "style": {

+ 387 - 0
cfc-frontend/pages/action/contact-detail.vue

@@ -0,0 +1,387 @@
+<template>
+  <view class="detail-page">
+    <!-- 头部 -->
+    <view class="detail-header">
+      <view class="header-top">
+        <text class="back-btn" @click="goBack">← 返回</text>
+        <text class="page-title">关系详情</text>
+        <text class="edit-btn" @click="toggleEdit">{{ isEditing ? '取消' : '编辑' }}</text>
+      </view>
+      <view class="header-avatar" :style="{ background: avatarColor }">
+        <text class="avatar-text">{{ initial }}</text>
+      </view>
+      <text class="header-name">{{ contact.name || '未知' }}</text>
+      <text class="header-meta">{{ typeLabel }}</text>
+      <text class="header-known" v-if="contact.knownSinceLabel">{{ contact.knownSinceLabel }}</text>
+      <view class="header-intimacy">
+        <text class="intimacy-heart">❤️</text>
+        <text class="intimacy-num">{{ contact.intimacyLevel || 0 }}</text>
+        <text class="intimacy-label">亲密度</text>
+      </view>
+    </view>
+
+    <!-- 基本信息 -->
+    <view class="info-section">
+      <text class="section-title">基本信息</text>
+      <view class="info-row" v-if="!isEditing">
+        <text class="info-icon">📞</text>
+        <text class="info-value">{{ contact.phone || '未填写' }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.birthdayLabel">
+        <text class="info-icon">🎂</text>
+        <text class="info-value">{{ contact.birthdayLabel }}
+          <text v-if="contact.birthdayCountdown" class="countdown"> · {{ contact.birthdayCountdown }}</text>
+        </text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.bio">
+        <text class="info-icon">📝</text>
+        <text class="info-value">{{ contact.bio }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.tags">
+        <text class="info-icon">🏷️</text>
+        <text class="info-value">{{ contact.tags }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.notes">
+        <text class="info-icon">💬</text>
+        <text class="info-value">{{ contact.notes }}</text>
+      </view>
+
+      <!-- 编辑模式 -->
+      <view v-if="isEditing">
+        <view class="edit-row">
+          <text class="edit-label">姓名</text>
+          <input class="edit-input" v-model="editForm.name" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">手机号</text>
+          <input class="edit-input" v-model="editForm.phone" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">关系类型</text>
+          <picker mode="selector" :range="typeOptions" @change="onTypeChange">
+            <view class="edit-picker">{{ editForm.relationshipTypeLabel || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">认识时间</text>
+          <picker mode="date" @change="onDateChange">
+            <view class="edit-picker">{{ editForm.knownSince || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">生日</text>
+          <picker mode="date" @change="onBirthdayChange">
+            <view class="edit-picker">{{ editForm.birthday || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">个人介绍</text>
+          <input class="edit-input" v-model="editForm.bio" placeholder="选填" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">备注</text>
+          <input class="edit-input" v-model="editForm.notes" placeholder="选填" />
+        </view>
+        <button class="btn-save" @click="saveEdit">保存</button>
+      </view>
+    </view>
+
+    <!-- 互动记录 -->
+    <view class="interaction-section">
+      <text class="section-title">互动记录</text>
+      <view class="interaction-item" v-if="contact.lastContactAt">
+        <text class="interaction-date">{{ formatDate(contact.lastContactAt) }}</text>
+        <text class="interaction-desc">标记了联系</text>
+      </view>
+      <view class="interaction-item" v-if="contact.createdAt">
+        <text class="interaction-date">{{ formatDate(contact.createdAt) }}</text>
+        <text class="interaction-desc">添加了联系人</text>
+      </view>
+      <view class="interaction-empty" v-if="!contact.lastContactAt && !contact.updatedAt">
+        <text>暂无互动记录</text>
+      </view>
+      <button class="btn-interaction" @click="markInteraction">📞 标记联系</button>
+    </view>
+
+    <!-- 删除 -->
+    <view class="danger-section">
+      <button class="btn-delete" @click="confirmDelete">删除联系人</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getContactDetail, updateContact, deleteContact, recordContactInteraction } from '../../utils/api.js'
+
+export default {
+  data: function() {
+    return {
+      contactId: null,
+      contact: {},
+      isEditing: false,
+      editForm: {},
+      typeOptions: ['家人', '朋友', '伴侣', '同事', '其他'],
+      typeValues: { '家人': 'family', '朋友': 'friend', '伴侣': 'partner', '同事': 'colleague', '其他': 'other' },
+      reverseTypeValues: { 'family': '家人', 'friend': '朋友', 'partner': '伴侣', 'colleague': '同事', 'other': '其他' }
+    }
+  },
+  computed: {
+    initial: function() {
+      return this.contact.name ? this.contact.name[0] : '?'
+    },
+    avatarColor: function() {
+      var colors = ['#FF6B35', '#667eea', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899']
+      var idx = (this.contact.id || 0) % colors.length
+      return colors[idx]
+    },
+    typeLabel: function() {
+      var map = { family: '家人', friend: '朋友', partner: '伴侣', colleague: '同事', other: '其他' }
+      return map[this.contact.relationshipType] || '其他'
+    }
+  },
+  onLoad: function(options) {
+    if (options.id) {
+      this.contactId = options.id
+      this.loadDetail()
+    }
+  },
+  methods: {
+    loadDetail: function() {
+      var self = this
+      if (!this.contactId) return
+      getContactDetail(this.contactId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.contact = res.data
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    toggleEdit: function() {
+      if (this.isEditing) {
+        this.isEditing = false
+      } else {
+        this.editForm = {
+          name: this.contact.name || '',
+          phone: this.contact.phone || '',
+          relationshipType: this.contact.relationshipType || '',
+          relationshipTypeLabel: this.reverseTypeValues[this.contact.relationshipType] || '其他',
+          knownSince: this.contact.knownSince ? this.contact.knownSince.substring(0, 10) : '',
+          birthday: this.contact.birthday ? this.contact.birthday.substring(0, 10) : '',
+          bio: this.contact.bio || '',
+          notes: this.contact.notes || ''
+        }
+        this.isEditing = true
+      }
+    },
+    onTypeChange: function(e) {
+      var label = this.typeOptions[e.detail.value]
+      this.editForm.relationshipType = this.typeValues[label] || 'other'
+      this.editForm.relationshipTypeLabel = label
+    },
+    onDateChange: function(e) {
+      this.editForm.knownSince = e.detail.value
+    },
+    onBirthdayChange: function(e) {
+      this.editForm.birthday = e.detail.value
+    },
+    saveEdit: function() {
+      var self = this
+      var payload = { id: this.contact.id, name: this.editForm.name, phone: this.editForm.phone }
+      if (this.editForm.relationshipType) payload.relationshipType = this.editForm.relationshipType
+      if (this.editForm.knownSince) payload.knownSince = this.editForm.knownSince
+      if (this.editForm.birthday) payload.birthday = this.editForm.birthday
+      if (this.editForm.bio !== undefined) payload.bio = this.editForm.bio
+      if (this.editForm.notes !== undefined) payload.notes = this.editForm.notes
+      updateContact(payload).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '保存成功', icon: 'success' })
+          self.isEditing = false
+          self.loadDetail()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    markInteraction: function() {
+      var self = this
+      recordContactInteraction(this.contact.id).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已标记联系', icon: 'success' })
+          self.loadDetail()
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    confirmDelete: function() {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除该联系人吗?',
+        success: function(r) {
+          if (r.confirm) {
+            deleteContact({ id: self.contact.id }).then(function(res) {
+              if (res.code === 200) {
+                uni.showToast({ title: '已删除', icon: 'success' })
+                setTimeout(function() { uni.navigateBack() }, 500)
+              } else {
+                uni.showToast({ title: res.message || '删除失败', icon: 'none' })
+              }
+            }).catch(function() {
+              uni.showToast({ title: '网络错误', icon: 'none' })
+            })
+          }
+        }
+      })
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return '-'
+      return dateStr.substring(0, 10)
+    },
+    goBack: function() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.detail-page {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 40rpx;
+}
+.detail-header {
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  padding: 60rpx 30rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: #fff;
+}
+.header-top {
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 30rpx;
+}
+.back-btn, .edit-btn {
+  font-size: 28rpx;
+  padding: 10rpx 20rpx;
+}
+.page-title { font-size: 32rpx; font-weight: bold; }
+.header-avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
+  border: 4rpx solid rgba(255,255,255,0.4);
+}
+.avatar-text { font-size: 48rpx; font-weight: bold; color: #fff; }
+.header-name { font-size: 36rpx; font-weight: bold; margin-bottom: 8rpx; }
+.header-meta { font-size: 26rpx; opacity: 0.9; margin-bottom: 4rpx; }
+.header-known { font-size: 24rpx; opacity: 0.7; margin-bottom: 12rpx; }
+.header-intimacy { display: flex; align-items: center; gap: 8rpx; }
+.intimacy-heart { font-size: 32rpx; }
+.intimacy-num { font-size: 40rpx; font-weight: bold; }
+.intimacy-label { font-size: 24rpx; opacity: 0.8; }
+
+.info-section, .interaction-section {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.info-row {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+}
+.info-icon { font-size: 28rpx; margin-right: 12rpx; width: 40rpx; }
+.info-value { font-size: 26rpx; color: #555; flex: 1; }
+.countdown { color: #EC4899; }
+
+.edit-row {
+  margin-bottom: 20rpx;
+}
+.edit-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.edit-input {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+}
+.edit-picker {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+}
+.btn-save {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #FF6B35, #F97316);
+  font-size: 30rpx;
+  color: #fff;
+  text-align: center;
+  border: none;
+  margin-top: 20rpx;
+}
+
+.interaction-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.interaction-date { font-size: 24rpx; color: #999; }
+.interaction-desc { font-size: 26rpx; color: #555; }
+.interaction-empty { text-align: center; padding: 30rpx; color: #999; font-size: 26rpx; }
+.btn-interaction {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  font-size: 30rpx;
+  color: #fff;
+  text-align: center;
+  border: none;
+  margin-top: 20rpx;
+}
+.danger-section { margin: 20rpx; }
+.btn-delete {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: #fff;
+  border: 1rpx solid #ff4d4f;
+  font-size: 28rpx;
+  color: #ff4d4f;
+  text-align: center;
+}
+</style>

+ 153 - 2
cfc-frontend/pages/action/index.vue

@@ -5,6 +5,14 @@
       tagline="知行合一 · 快乐成长"
       quote="千里之行,始于足下" />
 
+    <!-- 家庭成员切换条 -->
+    <FamilyMemberStrip
+      v-if="isLoggedIn && familyMembersVisible.length > 0"
+      :members="familyMembersVisible"
+      :selectedMemberId="selectedMemberId"
+      :selectedMemberType="selectedMemberType"
+      @select="onMemberSelect" />
+
     <!-- 游客登录引导卡 -->
     <template v-if="!isLoggedIn">
       <LoginGuideCard
@@ -55,6 +63,33 @@
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 
+    <!-- ===== 重要关系维护 ===== -->
+    <view class="contact-section" v-if="isLoggedIn">
+      <view class="contact-header">
+        <text class="contact-section-title">重要关系</text>
+        <view class="contact-header-right">
+          <text class="contact-more" @click="loadContacts">刷新</text>
+          <text class="contact-import-btn" @click="onShowImport">+ 导入</text>
+        </view>
+      </view>
+      <scroll-view class="contact-scroll" scroll-x enable-flex>
+        <view class="contact-scroll-inner">
+          <view class="contact-card-wrap" v-for="item in contactList" :key="item.id" @click="onContactClick(item)">
+            <ContactCard :contact="item" />
+          </view>
+          <view class="contact-card-wrap contact-add-card" @click="onShowImport">
+            <view class="add-card-inner">
+              <text class="add-card-icon">+</text>
+              <text class="add-card-label">导入通讯录</text>
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+      <view class="contact-empty" v-if="contactList.length === 0">
+        <text class="contact-empty-text">暂无重要联系人,点击导入添加</text>
+      </view>
+    </view>
+
     <!-- 功能入口(4个:活动、商品、课程) -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
@@ -67,6 +102,12 @@
       </view>
     </view>
 
+    <!-- 导入联系人弹窗 -->
+    <ContactImport
+      :visible="showImportModal"
+      @close="onCloseImport"
+      @success="onImportSuccess" />
+
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
   </view>
@@ -80,10 +121,13 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
-import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+import ContactCard from '../../components/ContactCard.vue'
+import ContactImport from '../../components/ContactImport.vue'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers } from '../../utils/api.js'
 
 export default {
-  components: { PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts },
+  components: { PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, ContactCard, ContactImport, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
@@ -92,12 +136,17 @@ export default {
       currentChildName: '',
       userName: '',
       children: [],
+      familyMembersVisible: [],
+      selectedMemberId: 0,
+      selectedMemberType: 'all',
       sandboxData: null,
       dualDimension: null,
       visibleSections: [],
       dimensionTasks: [],
       dimensionActivities: [],
       dimensionProducts: [],
+      contactList: [],
+      showImportModal: false,
       funcList: [
         { icon: '\u{1F3CB}', label: '任务', needLogin: true, page: '/pages/tasks/tasks' },
         { icon: '\u{1F3C0}', label: '活动', needLogin: false, page: '/pages/discover/index?type=activity' },
@@ -162,6 +211,7 @@ export default {
           if (self.currentChildId) {
             self.loadDimensionData()
           }
+          self.loadContacts()
         }
       }).catch(function(e) {
         console.log('获取孩子列表失败', e)
@@ -228,6 +278,29 @@ export default {
         }
       }).catch(function() { self.dimensionProducts = [] })
     },
+    loadContacts: function() {
+      var self = this
+      getContactList({}).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.contactList = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function(e) {
+        console.log('获取联系人失败', e)
+      })
+    },
+    onContactClick: function(item) {
+      uni.navigateTo({ url: '/pages/action/contact-detail?id=' + item.id })
+    },
+    onShowImport: function() {
+      this.showImportModal = true
+    },
+    onCloseImport: function() {
+      this.showImportModal = false
+    },
+    onImportSuccess: function() {
+      this.showImportModal = false
+      this.loadContacts()
+    },
     onChildChanged: function(child) {
       this.currentChildId = child.childId
       this.currentChildName = child.name || child.nickname || '孩子'
@@ -327,6 +400,84 @@ export default {
   font-weight: 500;
 }
 
+/* ===== 重要关系 ===== */
+.contact-section {
+  margin: 20rpx 20rpx 0;
+}
+.contact-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.contact-section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.contact-header-right {
+  display: flex;
+  gap: 16rpx;
+  align-items: center;
+}
+.contact-more {
+  font-size: 24rpx;
+  color: #999;
+}
+.contact-import-btn {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+.contact-scroll {
+  white-space: nowrap;
+  overflow: hidden;
+}
+.contact-scroll-inner {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+  padding: 8rpx 0 16rpx;
+}
+.contact-card-wrap {
+  flex-shrink: 0;
+  width: 220rpx;
+}
+.contact-card-wrap:active {
+  opacity: 0.8;
+}
+.contact-add-card {
+  width: 160rpx;
+}
+.add-card-inner {
+  width: 160rpx;
+  height: 280rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  border: 2rpx dashed #ddd;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+}
+.add-card-icon {
+  font-size: 48rpx;
+  color: #ccc;
+}
+.add-card-label {
+  font-size: 22rpx;
+  color: #999;
+}
+.contact-empty {
+  padding: 40rpx 0;
+  text-align: center;
+}
+.contact-empty-text {
+  font-size: 26rpx;
+  color: #ccc;
+}
+
 /* ===== 底部 ===== */
 .bottom-spacer {
   height: 20rpx;

+ 20 - 1
cfc-frontend/utils/api.js

@@ -145,6 +145,16 @@ export const getFamilyMembers = () => {
 	return request('/api/family/user/family-members', 'POST')
 }
 
+// 获取对家庭成员可见的成员列表
+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 })
+}
+
 export const switchMode = (mode) => {
 	return request('/api/user/switch-mode', 'POST', { mode })
 }
@@ -1180,7 +1190,7 @@ const danshopRequest = (path, method = 'POST', data = {}) => {
 
 export const danshopCategoryList = (data) => danshopRequest('/danshop/category/list', 'POST', data)
 export const danshopCategoryTree = (data) => danshopRequest('/danshop/category/tree', 'POST', data)
-export const danshopProductList = (data) => danshopRequest('/danshop/product/list', 'POST', data)
+export const danshopProductList = (data) => danshopRequest('/danshop/goods/list', 'POST', data)
 export const danshopProductDetail = (data) => danshopRequest('/danshop/product/detail', 'POST', data)
 export const danshopCartList = (data) => danshopRequest('/danshop/cart/list', 'POST', data)
 export const danshopCartAdd = (data) => danshopRequest('/danshop/cart/add', 'POST', data)
@@ -1226,3 +1236,12 @@ export const getProductsByDomain = (domain, page, size) => {
 // ===== 健康报告 =====
 export const getLatestHealthReport = (childId) => request('/api/health/report/latest', 'POST', { childId })
 export const getHealthIndicatorList = (childId) => request('/api/health/indicator/list', 'POST', { childId })
+
+// ===== 重要关系(联系人) =====
+export const getContactList = (data) => request('/api/contact/list', 'POST', data)
+export const getContactDetail = (id) => request('/api/contact/detail', 'POST', { id: id })
+export const createContact = (data) => request('/api/contact/create', 'POST', data)
+export const updateContact = (data) => request('/api/contact/update', 'POST', data)
+export const deleteContact = (id) => request('/api/contact/delete', 'POST', { id: id })
+export const importPhoneContact = (data) => request('/api/contact/import-phone', 'POST', data)
+export const recordContactInteraction = (data) => request('/api/contact/interaction', 'POST', data)